mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Revamped Lib Files for Svelte 5 Rewrite
This commit is contained in:
@@ -1,34 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { navPage, activeManga } from "../../store";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import Home from "../pages/Home.svelte";
|
||||
import Library from "../pages/Library.svelte";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import Home from "../pages/Home.svelte";
|
||||
import Library from "../pages/Library.svelte";
|
||||
import SeriesDetail from "../pages/SeriesDetail.svelte";
|
||||
import History from "../pages/History.svelte";
|
||||
import Search from "../pages/Search.svelte";
|
||||
import Discover from "../pages/Discover.svelte";
|
||||
import Downloads from "../pages/Downloads.svelte";
|
||||
import Extensions from "../pages/Extensions.svelte";
|
||||
import History from "../pages/History.svelte";
|
||||
import Search from "../pages/Search.svelte";
|
||||
import Discover from "../pages/Discover.svelte";
|
||||
import Downloads from "../pages/Downloads.svelte";
|
||||
import Extensions from "../pages/Extensions.svelte";
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Sidebar />
|
||||
<main class="main">
|
||||
{#if $activeManga}
|
||||
{#if activeManga}
|
||||
<SeriesDetail />
|
||||
{:else if $navPage === "home"}
|
||||
{:else if navPage === "home"}
|
||||
<Home />
|
||||
{:else if $navPage === "library"}
|
||||
{:else if navPage === "library"}
|
||||
<Library />
|
||||
{:else if $navPage === "search"}
|
||||
{:else if navPage === "search"}
|
||||
<Search />
|
||||
{:else if $navPage === "history"}
|
||||
{:else if navPage === "history"}
|
||||
<History />
|
||||
{:else if $navPage === "explore" || $navPage === "sources"}
|
||||
{:else if navPage === "explore" || navPage === "sources"}
|
||||
<Discover />
|
||||
{:else if $navPage === "downloads"}
|
||||
{:else if navPage === "downloads"}
|
||||
<Downloads />
|
||||
{:else if $navPage === "extensions"}
|
||||
{:else if navPage === "extensions"}
|
||||
<Extensions />
|
||||
{:else}
|
||||
<Home />
|
||||
@@ -38,10 +38,5 @@
|
||||
|
||||
<style>
|
||||
.root { display: flex; height: 100%; background: var(--bg-base); overflow: hidden; }
|
||||
.main {
|
||||
flex: 1; overflow: hidden;
|
||||
background: var(--bg-surface);
|
||||
transform: translateZ(0);
|
||||
contain: layout style;
|
||||
}
|
||||
.main { flex: 1; overflow: hidden; background: var(--bg-surface); transform: translateZ(0); contain: layout style; }
|
||||
</style>
|
||||
|
||||
@@ -14,91 +14,55 @@
|
||||
];
|
||||
|
||||
function navigate(id: NavPage) {
|
||||
navPage.set(id);
|
||||
activeManga.set(null);
|
||||
genreFilter.set("");
|
||||
if (id !== "explore") activeSource.set(null);
|
||||
navPage = id;
|
||||
activeManga = null;
|
||||
genreFilter = "";
|
||||
if (id !== "explore") activeSource = null;
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
navPage.set("home");
|
||||
activeSource.set(null);
|
||||
activeManga.set(null);
|
||||
libraryFilter.set("library");
|
||||
genreFilter.set("");
|
||||
navPage = "home";
|
||||
activeSource = null;
|
||||
activeManga = null;
|
||||
libraryFilter = "library";
|
||||
genreFilter = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="root">
|
||||
<button class="logo" on:click={goHome} title="Home" aria-label="Go to Home">
|
||||
<button class="logo" onclick={goHome} title="Home" aria-label="Go to Home">
|
||||
<div class="logo-icon"></div>
|
||||
</button>
|
||||
<nav class="nav">
|
||||
{#each TABS as tab}
|
||||
<button class="tab" class:active={$navPage === tab.id}
|
||||
title={tab.label} on:click={() => navigate(tab.id)}>
|
||||
<svelte:component this={tab.icon} size={18} weight="light" />
|
||||
<button class="tab" class:active={navPage === tab.id}
|
||||
title={tab.label} onclick={() => navigate(tab.id)}>
|
||||
<tab.icon size={18} weight="light" />
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="bottom">
|
||||
<button class="settings-btn" on:click={() => settingsOpen.set(true)} title="Settings">
|
||||
<button class="settings-btn" onclick={() => settingsOpen = true} title="Settings">
|
||||
<GearSix size={18} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
width: var(--sidebar-width); flex-shrink: 0;
|
||||
background: var(--bg-void); display: flex; flex-direction: column;
|
||||
align-items: center; padding: var(--sp-4) 0;
|
||||
}
|
||||
.logo {
|
||||
width: 80px; height: 80px; display: flex; align-items: center; justify-content: center;
|
||||
margin-bottom: var(--sp-3); background: none; border: none; outline: none;
|
||||
cursor: pointer; border-radius: var(--radius-lg);
|
||||
transition: opacity var(--t-base), transform var(--t-base);
|
||||
padding: 0; appearance: none; -webkit-appearance: none;
|
||||
}
|
||||
.root { width: var(--sidebar-width); flex-shrink: 0; background: var(--bg-void); display: flex; flex-direction: column; align-items: center; padding: var(--sp-4) 0; }
|
||||
.logo { width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; margin-bottom: var(--sp-3); background: none; border: none; outline: none; cursor: pointer; border-radius: var(--radius-lg); transition: opacity var(--t-base), transform var(--t-base); padding: 0; appearance: none; -webkit-appearance: none; }
|
||||
.logo:hover { opacity: 0.8; transform: scale(0.96); }
|
||||
.logo:active { transform: scale(0.92); }
|
||||
.logo:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.logo-icon {
|
||||
width: 80px; height: 80px; background-color: var(--accent);
|
||||
mask-image: url("../../assets/moku-icon.svg");
|
||||
mask-repeat: no-repeat; mask-position: center; mask-size: contain;
|
||||
-webkit-mask-image: url("../../assets/moku-icon.svg");
|
||||
-webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: contain;
|
||||
filter: drop-shadow(0 0 8px rgba(107,143,107,0.35)); pointer-events: none;
|
||||
}
|
||||
.nav {
|
||||
flex: 1; display: flex; flex-direction: column; align-items: center;
|
||||
gap: var(--sp-1); width: 100%; padding: 0 var(--sp-2);
|
||||
}
|
||||
.tab {
|
||||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--radius-md); color: var(--text-faint);
|
||||
background: none; border: none; outline: none; cursor: pointer; padding: 0;
|
||||
appearance: none; -webkit-appearance: none;
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.logo-icon { width: 80px; height: 80px; background-color: var(--accent); mask-image: url("../../assets/moku-icon.svg"); mask-repeat: no-repeat; mask-position: center; mask-size: contain; -webkit-mask-image: url("../../assets/moku-icon.svg"); -webkit-mask-repeat: no-repeat; -webkit-mask-position: center; -webkit-mask-size: contain; filter: drop-shadow(0 0 8px rgba(107,143,107,0.35)); pointer-events: none; }
|
||||
.nav { flex: 1; display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); width: 100%; padding: 0 var(--sp-2); }
|
||||
.tab { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; outline: none; cursor: pointer; padding: 0; appearance: none; -webkit-appearance: none; transition: color var(--t-base), background var(--t-base); }
|
||||
.tab:hover { color: var(--text-muted); background: var(--bg-raised); }
|
||||
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
.tab.active { color: var(--accent-fg); background: var(--accent-muted); }
|
||||
.tab.active:hover { color: var(--accent-fg); background: var(--accent-muted); }
|
||||
.bottom {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
width: 100%; padding: var(--sp-3) var(--sp-2) 0;
|
||||
border-top: 1px solid var(--border-dim); margin-top: var(--sp-3);
|
||||
}
|
||||
.settings-btn {
|
||||
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--radius-md); color: var(--text-faint);
|
||||
background: none; border: none; outline: none; cursor: pointer; padding: 0;
|
||||
appearance: none; -webkit-appearance: none;
|
||||
transition: color var(--t-base), background var(--t-base), transform var(--t-slow);
|
||||
}
|
||||
.bottom { display: flex; flex-direction: column; align-items: center; width: 100%; padding: var(--sp-3) var(--sp-2) 0; border-top: 1px solid var(--border-dim); margin-top: var(--sp-3); }
|
||||
.settings-btn { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; outline: none; cursor: pointer; padding: 0; appearance: none; -webkit-appearance: none; transition: color var(--t-base), background var(--t-base), transform var(--t-slow); }
|
||||
.settings-btn:hover { color: var(--text-muted); background: var(--bg-raised); transform: rotate(30deg); }
|
||||
.settings-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
</style>
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import logoUrl from "../../assets/moku-icon.svg";
|
||||
|
||||
export let mode: "loading" | "idle" = "loading";
|
||||
export let ringFull: boolean = false;
|
||||
export let failed: boolean = false;
|
||||
export let showCards: boolean = true;
|
||||
export let showFps: boolean = false;
|
||||
export let onReady: (() => void) | undefined = undefined;
|
||||
export let onRetry: (() => void) | undefined = undefined;
|
||||
export let onDismiss: (() => void) | undefined = undefined;
|
||||
interface Props {
|
||||
mode?: "loading" | "idle";
|
||||
ringFull?: boolean;
|
||||
failed?: boolean;
|
||||
showCards?: boolean;
|
||||
showFps?: boolean;
|
||||
onReady?: () => void;
|
||||
onRetry?: () => void;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
let { mode = "loading", ringFull = false, failed = false, showCards = true,
|
||||
showFps = false, onReady, onRetry, onDismiss }: Props = $props();
|
||||
|
||||
const EXIT_MS = 320;
|
||||
|
||||
let dots = "";
|
||||
let ringProg = 0.025;
|
||||
let exiting = false;
|
||||
let dots = $state("");
|
||||
let ringProg = $state(0.025);
|
||||
let exiting = $state(false);
|
||||
let exitLock = false;
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let fpsEl: HTMLSpanElement;
|
||||
|
||||
function triggerExit(cb?: () => void) {
|
||||
@@ -29,10 +33,12 @@
|
||||
setTimeout(() => cb?.(), EXIT_MS);
|
||||
}
|
||||
|
||||
$: if (ringFull) {
|
||||
ringProg = 1;
|
||||
setTimeout(() => triggerExit(onReady), 650);
|
||||
}
|
||||
$effect(() => {
|
||||
if (ringFull) {
|
||||
ringProg = 1;
|
||||
setTimeout(() => triggerExit(onReady), 650);
|
||||
}
|
||||
});
|
||||
|
||||
const dotsInterval = setInterval(() => {
|
||||
dots = dots.length >= 3 ? "" : dots + ".";
|
||||
@@ -48,22 +54,16 @@
|
||||
}, 200);
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
clearInterval(dotsInterval);
|
||||
window.removeEventListener("keydown", handler);
|
||||
window.removeEventListener("mousedown", handler);
|
||||
window.removeEventListener("touchstart", handler);
|
||||
};
|
||||
}
|
||||
return () => clearInterval(dotsInterval);
|
||||
});
|
||||
|
||||
onDestroy(() => clearInterval(dotsInterval));
|
||||
|
||||
// ── canvas animation ────────────────────────────────────────────────────────
|
||||
|
||||
interface CardDef {
|
||||
cx: number; w: number; h: number; lines: number; alpha: number;
|
||||
speed: number; cycleSec: number; phase: number; travel: number;
|
||||
yStart: number; angleStart: number; tilt: number;
|
||||
}
|
||||
interface CardDef { cx: number; w: number; h: number; lines: number; alpha: number; speed: number; cycleSec: number; phase: number; travel: number; yStart: number; angleStart: number; tilt: number; }
|
||||
interface CardTrig { cosA: number; sinA: number; tiltRad: number; }
|
||||
|
||||
const LAYER_CFG = [
|
||||
@@ -90,42 +90,26 @@
|
||||
const h = w * 1.44;
|
||||
const speed = cfg.speedMin + hash(seed + 5) * (cfg.speedMax - cfg.speedMin);
|
||||
const travel = vh + h + BUF;
|
||||
cards.push({
|
||||
cx: (col + 0.5) * laneW + (hash(seed + 2) * 2 - 1) * Math.max(0, (laneW - w) / 2 - 2),
|
||||
w, h,
|
||||
lines: 1 + Math.floor(hash(seed + 7) * 3),
|
||||
alpha: cfg.alpha, speed,
|
||||
cycleSec: travel / speed,
|
||||
phase: ((col / COLS) + hash(seed + 6) * 0.6 + layer * 0.23) % 1,
|
||||
travel,
|
||||
yStart: vh + h / 2 + BUF / 2,
|
||||
angleStart: hash(seed + 3) * 50 - 25,
|
||||
tilt: (hash(seed + 4) * 2 - 1) * 18,
|
||||
});
|
||||
cards.push({ cx: (col + 0.5) * laneW + (hash(seed + 2) * 2 - 1) * Math.max(0, (laneW - w) / 2 - 2), w, h, lines: 1 + Math.floor(hash(seed + 7) * 3), alpha: cfg.alpha, speed, cycleSec: travel / speed, phase: ((col / COLS) + hash(seed + 6) * 0.6 + layer * 0.23) % 1, travel, yStart: vh + h / 2 + BUF / 2, angleStart: hash(seed + 3) * 50 - 25, tilt: (hash(seed + 4) * 2 - 1) * 18 });
|
||||
}
|
||||
}
|
||||
const trigs: CardTrig[] = cards.map(c => ({
|
||||
cosA: Math.cos(c.angleStart * (Math.PI / 180)),
|
||||
sinA: Math.sin(c.angleStart * (Math.PI / 180)),
|
||||
tiltRad: c.tilt * (Math.PI / 180),
|
||||
}));
|
||||
const trigs: CardTrig[] = cards.map(c => ({ cosA: Math.cos(c.angleStart * (Math.PI / 180)), sinA: Math.sin(c.angleStart * (Math.PI / 180)), tiltRad: c.tilt * (Math.PI / 180) }));
|
||||
return { cards, trigs };
|
||||
}
|
||||
|
||||
function rrect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r);
|
||||
ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r);
|
||||
ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
||||
ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r);
|
||||
ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r);
|
||||
ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r);
|
||||
ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
const STAMP_PAD = 6;
|
||||
|
||||
function buildStamp(c: CardDef, dpr: number): HTMLCanvasElement {
|
||||
const oc = document.createElement("canvas");
|
||||
const oc = document.createElement("canvas");
|
||||
oc.width = Math.round(Math.ceil(c.w + STAMP_PAD * 2) * dpr);
|
||||
oc.height = Math.round(Math.ceil(c.h + STAMP_PAD * 2) * dpr);
|
||||
const ctx = oc.getContext("2d")!;
|
||||
@@ -133,17 +117,11 @@
|
||||
const x0 = STAMP_PAD, y0 = STAMP_PAD;
|
||||
const coverH = (c.w * 0.72) * 1.05;
|
||||
const lineY0 = y0 + 3 + coverH + 5;
|
||||
ctx.fillStyle = "rgba(0,0,0,0.5)";
|
||||
rrect(ctx, x0 + 2, y0 + 2, c.w, c.h, 4); ctx.fill();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.07)";
|
||||
rrect(ctx, x0, y0, c.w, c.h, 4); ctx.fill();
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.75)";
|
||||
ctx.lineWidth = 1.2;
|
||||
rrect(ctx, x0, y0, c.w, c.h, 4); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.15)";
|
||||
rrect(ctx, x0 + 3, y0 + 3, c.w - 6, coverH, 3); ctx.fill();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.08)";
|
||||
rrect(ctx, x0 + 3, y0 + 3, (c.w - 6) * 0.45, coverH, 3); ctx.fill();
|
||||
ctx.fillStyle = "rgba(0,0,0,0.5)"; rrect(ctx, x0 + 2, y0 + 2, c.w, c.h, 4); ctx.fill();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.07)"; rrect(ctx, x0, y0, c.w, c.h, 4); ctx.fill();
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.75)"; ctx.lineWidth = 1.2; rrect(ctx, x0, y0, c.w, c.h, 4); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.15)"; rrect(ctx, x0 + 3, y0 + 3, c.w - 6, coverH, 3); ctx.fill();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.08)"; rrect(ctx, x0 + 3, y0 + 3, (c.w - 6) * 0.45, coverH, 3); ctx.fill();
|
||||
for (let li = 0; li < c.lines; li++) {
|
||||
ctx.fillStyle = li === 0 ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.20)";
|
||||
ctx.fillRect(x0 + 4, lineY0 + li * 8, (c.w - 8) * (li === 0 ? 0.78 : 0.52), li === 0 ? 3 : 2);
|
||||
@@ -152,23 +130,17 @@
|
||||
}
|
||||
|
||||
function buildVignette(vw: number, vh: number, dpr: number): HTMLCanvasElement {
|
||||
const oc = document.createElement("canvas");
|
||||
oc.width = Math.round(vw * dpr);
|
||||
oc.height = Math.round(vh * dpr);
|
||||
const oc = document.createElement("canvas");
|
||||
oc.width = Math.round(vw * dpr); oc.height = Math.round(vh * dpr);
|
||||
const ctx = oc.getContext("2d")!;
|
||||
ctx.scale(dpr, dpr);
|
||||
const g = ctx.createRadialGradient(vw / 2, vh / 2, 0, vw / 2, vh / 2, Math.max(vw, vh) * 0.65);
|
||||
g.addColorStop(0.15, "rgba(0,0,0,0)");
|
||||
g.addColorStop(1, "rgba(0,0,0,0.82)");
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, vw, vh);
|
||||
g.addColorStop(0.15, "rgba(0,0,0,0)"); g.addColorStop(1, "rgba(0,0,0,0.82)");
|
||||
ctx.fillStyle = g; ctx.fillRect(0, 0, vw, vh);
|
||||
return oc;
|
||||
}
|
||||
|
||||
function drawFrame(
|
||||
ctx: CanvasRenderingContext2D, t: number, cw: number, ch: number, dpr: number,
|
||||
cards: CardDef[], trigs: CardTrig[], stamps: HTMLCanvasElement[], vignette: HTMLCanvasElement,
|
||||
) {
|
||||
function drawFrame(ctx: CanvasRenderingContext2D, t: number, cw: number, ch: number, dpr: number, cards: CardDef[], trigs: CardTrig[], stamps: HTMLCanvasElement[], vignette: HTMLCanvasElement) {
|
||||
ctx.clearRect(0, 0, cw, ch);
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
const c = cards[i];
|
||||
@@ -185,14 +157,11 @@
|
||||
const sw = stamps[i].width / dpr, sh = stamps[i].height / dpr;
|
||||
ctx.drawImage(stamps[i], -sw / 2, -sh / 2, sw, sh);
|
||||
}
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalAlpha = 1;
|
||||
ctx.drawImage(vignette, 0, 0, cw, ch);
|
||||
}
|
||||
|
||||
// ── FPS counter ─────────────────────────────────────────────────────────────
|
||||
let fps = 0, fpsFrames = 0, fpsLast = 0;
|
||||
|
||||
function tickFps(now: number) {
|
||||
fpsFrames++;
|
||||
if (now - fpsLast >= 500) {
|
||||
@@ -202,19 +171,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── canvas mount ─────────────────────────────────────────────────────────────
|
||||
function mountCanvas(el: HTMLCanvasElement) {
|
||||
const win = getCurrentWindow();
|
||||
const ctx = el.getContext("2d")!;
|
||||
|
||||
interface RenderState {
|
||||
cards: CardDef[]; trigs: CardTrig[];
|
||||
stamps: HTMLCanvasElement[]; vignette: HTMLCanvasElement;
|
||||
CW: number; CH: number; scale: number;
|
||||
}
|
||||
interface RenderState { cards: CardDef[]; trigs: CardTrig[]; stamps: HTMLCanvasElement[]; vignette: HTMLCanvasElement; CW: number; CH: number; scale: number; }
|
||||
let live: RenderState | null = null;
|
||||
let lastLogW = 0, lastLogH = 0, lastScale = 0, buildGen = 0;
|
||||
|
||||
async function syncSize() {
|
||||
const gen = ++buildGen;
|
||||
const [phys, scale] = await Promise.all([win.innerSize(), win.scaleFactor()]);
|
||||
@@ -225,15 +187,11 @@
|
||||
const built = buildCards(logW, logH);
|
||||
const stamps = built.cards.map(c => buildStamp(c, scale));
|
||||
const vig = buildVignette(logW, logH, scale);
|
||||
el.width = phys.width;
|
||||
el.height = phys.height;
|
||||
el.width = phys.width; el.height = phys.height;
|
||||
live = { cards: built.cards, trigs: built.trigs, stamps, vignette: vig, CW: phys.width, CH: phys.height, scale };
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(() => syncSize());
|
||||
ro.observe(el);
|
||||
syncSize();
|
||||
|
||||
ro.observe(el); syncSize();
|
||||
let raf = 0, t0 = -1;
|
||||
function frame(now: number) {
|
||||
raf = requestAnimationFrame(frame);
|
||||
@@ -244,31 +202,22 @@
|
||||
drawFrame(ctx, (now - t0) / 1000, CW, CH, scale, cards, trigs, stamps, vignette);
|
||||
}
|
||||
raf = requestAnimationFrame(frame);
|
||||
|
||||
return () => { cancelAnimationFrame(raf); ro.disconnect(); };
|
||||
}
|
||||
|
||||
// ── ring ─────────────────────────────────────────────────────────────────────
|
||||
$: ringR = 44;
|
||||
$: ringPad = 8;
|
||||
$: ringSize = (ringR + ringPad) * 2;
|
||||
$: ringC = ringR + ringPad;
|
||||
$: ringCirc = 2 * Math.PI * ringR;
|
||||
$: ringArc = ringCirc * Math.min(Math.max(ringProg, 0.025), 0.999);
|
||||
$: ringTop = -((ringSize - 80) / 2);
|
||||
$: ringLeft = -((ringSize - 80) / 2);
|
||||
const ringR = $derived(44);
|
||||
const ringPad = $derived(8);
|
||||
const ringSize = $derived((ringR + ringPad) * 2);
|
||||
const ringC = $derived(ringR + ringPad);
|
||||
const ringCirc = $derived(2 * Math.PI * ringR);
|
||||
const ringArc = $derived(ringCirc * Math.min(Math.max(ringProg, 0.025), 0.999));
|
||||
const ringTop = $derived(-((ringSize - 80) / 2));
|
||||
const ringLeft = $derived(-((ringSize - 80) / 2));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="splash"
|
||||
class:exiting
|
||||
style="cursor: {mode === 'idle' ? 'pointer' : 'default'}"
|
||||
>
|
||||
<div class="splash" class:exiting style="cursor: {mode === 'idle' ? 'pointer' : 'default'}">
|
||||
{#if showCards}
|
||||
<canvas
|
||||
style="position:absolute;inset:0;pointer-events:none;width:100%;height:100%"
|
||||
use:mountCanvas
|
||||
></canvas>
|
||||
<canvas style="position:absolute;inset:0;pointer-events:none;width:100%;height:100%" use:mountCanvas></canvas>
|
||||
{#if showFps}
|
||||
<span bind:this={fpsEl} style="position:absolute;top:8px;right:8px;font-family:var(--font-ui);font-size:10px;color:var(--text-faint);z-index:2;pointer-events:none"></span>
|
||||
{/if}
|
||||
@@ -285,21 +234,9 @@
|
||||
{:else}
|
||||
<div style="position:relative;width:80px;height:80px;margin-bottom:20px;z-index:1">
|
||||
{#if !failed}
|
||||
<svg
|
||||
width={ringSize} height={ringSize}
|
||||
style="position:absolute;pointer-events:none;top:{ringTop}px;left:{ringLeft}px"
|
||||
>
|
||||
<svg width={ringSize} height={ringSize} style="position:absolute;pointer-events:none;top:{ringTop}px;left:{ringLeft}px">
|
||||
<circle cx={ringC} cy={ringC} r={ringR} fill="none" stroke="var(--border-base)" stroke-width="2" />
|
||||
<circle
|
||||
cx={ringC} cy={ringC} r={ringR}
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="{ringArc} {ringCirc}"
|
||||
transform="rotate(-90 {ringC} {ringC})"
|
||||
style="transition: stroke-dasharray 0.4s cubic-bezier(0.4,0,0.2,1)"
|
||||
/>
|
||||
<circle cx={ringC} cy={ringC} r={ringR} fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-dasharray="{ringArc} {ringCirc}" transform="rotate(-90 {ringC} {ringC})" style="transition: stroke-dasharray 0.4s cubic-bezier(0.4,0,0.2,1)" />
|
||||
</svg>
|
||||
{/if}
|
||||
<img src={logoUrl} alt="Moku" style="width:80px;height:80px;border-radius:18px;display:block" />
|
||||
@@ -307,13 +244,9 @@
|
||||
<p class="title-label">moku</p>
|
||||
<div style="z-index:1;display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||
{#if failed}
|
||||
<p style="font-family:var(--font-ui);font-size:11px;color:var(--color-error);letter-spacing:0.1em;margin:0">
|
||||
Could not reach Suwayomi
|
||||
</p>
|
||||
<p style="font-family:var(--font-ui);font-size:10px;color:var(--text-faint);letter-spacing:0.05em;margin:0;text-align:center;max-width:240px;line-height:1.6">
|
||||
Make sure tachidesk-server is on your PATH
|
||||
</p>
|
||||
<button class="retry-btn" on:click={onRetry}>Retry</button>
|
||||
<p style="font-family:var(--font-ui);font-size:11px;color:var(--color-error);letter-spacing:0.1em;margin:0">Could not reach Suwayomi</p>
|
||||
<p style="font-family:var(--font-ui);font-size:10px;color:var(--text-faint);letter-spacing:0.05em;margin:0;text-align:center;max-width:240px;line-height:1.6">Make sure tachidesk-server is on your PATH</p>
|
||||
<button class="retry-btn" onclick={onRetry}>Retry</button>
|
||||
{:else}
|
||||
<p style="font-family:var(--font-ui);font-size:10px;color:var(--text-faint);letter-spacing:0.12em;margin:0;min-width:160px;text-align:center">
|
||||
{ringFull ? "Ready" : `Initializing server${dots}`}
|
||||
@@ -324,48 +257,15 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.splash {
|
||||
position: fixed; inset: 0; z-index: 9999;
|
||||
background: var(--bg-base); overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
animation: spIn 0.35s cubic-bezier(0,0,0.2,1) both;
|
||||
}
|
||||
.splash.exiting {
|
||||
animation: spOut 320ms cubic-bezier(0.4,0,1,1) both;
|
||||
}
|
||||
.splash { position: fixed; inset: 0; z-index: 9999; background: var(--bg-base); overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; animation: spIn 0.35s cubic-bezier(0,0,0.2,1) both; }
|
||||
.splash.exiting { animation: spOut 320ms cubic-bezier(0.4,0,1,1) both; }
|
||||
@keyframes spIn { from { opacity:0; transform:scale(1.015) } to { opacity:1; transform:scale(1) } }
|
||||
@keyframes spOut { from { opacity:1; transform:scale(1) } to { opacity:0; transform:scale(0.96) } }
|
||||
@keyframes logoBreathe {
|
||||
0%,100% { transform:scale(1); filter:drop-shadow(0 0 0px rgba(255,255,255,0)) }
|
||||
50% { transform:scale(1.04);filter:drop-shadow(0 0 18px rgba(255,255,255,0.12)) }
|
||||
}
|
||||
@keyframes logoBreathe { 0%,100% { transform:scale(1); filter:drop-shadow(0 0 0px rgba(255,255,255,0)) } 50% { transform:scale(1.04); filter:drop-shadow(0 0 18px rgba(255,255,255,0.12)) } }
|
||||
@keyframes hintFade { 0%,100% { opacity:0.35 } 50% { opacity:0.7 } }
|
||||
|
||||
.logo-glow {
|
||||
position: absolute; inset: -20px; border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 70%);
|
||||
animation: logoBreathe 4s ease-in-out infinite;
|
||||
}
|
||||
.logo-glow { position: absolute; inset: -20px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 70%); animation: logoBreathe 4s ease-in-out infinite; }
|
||||
.logo-breathe { animation: logoBreathe 4s ease-in-out infinite; }
|
||||
|
||||
.hint {
|
||||
font-family: var(--font-ui); font-size: 10px; color: var(--text-faint);
|
||||
letter-spacing: 0.22em; text-transform: uppercase;
|
||||
margin: 0; user-select: none;
|
||||
animation: hintFade 3.5s ease-in-out infinite;
|
||||
}
|
||||
.title-label {
|
||||
font-family: var(--font-ui); font-size: 11px; font-weight: 500;
|
||||
letter-spacing: 0.26em; text-transform: uppercase;
|
||||
color: var(--text-secondary); margin: 0 0 8px; z-index: 1; user-select: none;
|
||||
}
|
||||
.retry-btn {
|
||||
margin-top: 4px; padding: 5px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-raised);
|
||||
color: var(--text-muted); cursor: pointer;
|
||||
font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.08em;
|
||||
}
|
||||
.hint { font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); letter-spacing: 0.22em; text-transform: uppercase; margin: 0; user-select: none; animation: hintFade 3.5s ease-in-out infinite; }
|
||||
.title-label { font-family: var(--font-ui); font-size: 11px; font-weight: 500; letter-spacing: 0.26em; text-transform: uppercase; color: var(--text-secondary); margin: 0 0 8px; z-index: 1; user-select: none; }
|
||||
.retry-btn { margin-top: 4px; padding: 5px 16px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-muted); cursor: pointer; font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.08em; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user