e056e5981747
add RuneBackground component for atmospheric rune drift behind onboarding
- time
- 2026-04-28 04:40 UTC · 6 months ago
- machine
- koh2
- parent
- e7cf01cbd96c
↓ peach-browser-e056e598.tar.gzatlas at this save
files changed1 file
src/popup/components/RuneBackground.tsx+114 −0src/popup/components/RuneBackground.tsx+114 −0
@@ -0,0 +1,114 @@1+import { onCleanup, onMount } from "solid-js";2+3+interface RuneBackgroundProps {4+ opacity?: number;5+}6+7+const FONT_SIZE = 14;8+const LINE_H = FONT_SIZE * 2.4;9+const CHAR_W = FONT_SIZE * 1.4;10+// Three slow drift speeds (px/sec) — much quieter than the export canvas.11+// Atmosphere, not focus.12+const SPEEDS = [8, 11, 6];13+const RUNE_LEN = 120;14+15+// Deterministic rune string drawn from the PeachScript PUA range16+// (U+E000..U+E0FF). Same characters every render so the background17+// has a consistent texture across mounts.18+function buildRuneString(): string {19+ const out: string[] = [];20+ for (let i = 0; i < RUNE_LEN; i++) {21+ out.push(String.fromCodePoint(0xe000 + ((i * 37 + 13) & 0xff)));22+ }23+ return out.join("");24+}25+26+export function RuneBackground(props: RuneBackgroundProps) {27+ let canvas!: HTMLCanvasElement;28+ let raf = 0;29+ let running = false;30+31+ onMount(() => {32+ const ctx = canvas.getContext("2d");33+ if (!ctx) return;34+35+ const W = canvas.width;36+ const H = canvas.height;37+ const baseOpacity = props.opacity ?? 0.07;38+39+ const runes = buildRuneString();40+ const NUM_ROWS = Math.ceil(H / LINE_H) + 1;41+ const CHARS_PER_ROW = Math.ceil(W / CHAR_W) + 4;42+ const TOTAL_W = runes.length * CHAR_W;43+44+ const offsets = Array.from({ length: NUM_ROWS }, () =>45+ Math.random() * TOTAL_W,46+ );47+48+ let last = performance.now();49+50+ const frame = (now: number) => {51+ const dt = Math.min(now - last, 64);52+ last = now;53+54+ ctx.clearRect(0, 0, W, H);55+ ctx.font = `${FONT_SIZE}px PeachScript, monospace`;56+ ctx.fillStyle = `rgba(245, 240, 234, ${baseOpacity})`;57+ ctx.textBaseline = "alphabetic";58+59+ for (let row = 0; row < NUM_ROWS; row++) {60+ const speed = SPEEDS[row % SPEEDS.length];61+ offsets[row] = (offsets[row] + (speed * dt) / 1000) % TOTAL_W;62+63+ const y = row * LINE_H + FONT_SIZE + 8;64+ const baseIdx = Math.floor(offsets[row] / CHAR_W);65+ const xOffset = offsets[row] % CHAR_W;66+67+ for (let col = 0; col < CHARS_PER_ROW; col++) {68+ const x = col * CHAR_W - xOffset;69+ if (x < -CHAR_W || x > W + CHAR_W) continue;70+ const idx = (((baseIdx + col) % runes.length) + runes.length) % runes.length;71+ ctx.fillText(runes[idx]!, x, y);72+ }73+ }74+75+ raf = requestAnimationFrame(frame);76+ };77+78+ const start = () => {79+ if (running) return;80+ running = true;81+ last = performance.now();82+ raf = requestAnimationFrame(frame);83+ };84+85+ const stop = () => {86+ if (!running) return;87+ running = false;88+ cancelAnimationFrame(raf);89+ };90+91+ const onVisibility = () => {92+ if (document.hidden) stop();93+ else start();94+ };95+ document.addEventListener("visibilitychange", onVisibility);96+97+ document.fonts.load(`${FONT_SIZE}px PeachScript`).finally(start);98+99+ onCleanup(() => {100+ stop();101+ document.removeEventListener("visibilitychange", onVisibility);102+ });103+ });104+105+ return (106+ <canvas107+ ref={canvas}108+ width={320}109+ height={520}110+ class="peach-rune-background"111+ aria-hidden="true"112+ />113+ );114+}