7581d519f206

fix lock rune canvas — cyclic string scroll matching onboarding mechanic, 30% density

time
2026-04-28 05:32 UTC · 6 months ago
machine
koh2
parent
26c99f63f576
1 file src/popup/components/LockRuneCanvas.tsx+45 −53
src/popup/components/LockRuneCanvas.tsx+45 −53
@@ -1,13 +1,26 @@11 import { onCleanup, onMount } from "solid-js";22 33 const FONT_SIZE = 15;4-const LINE_H = FONT_SIZE * 2.8;5-const CHAR_W = FONT_SIZE * 1.7;6-const DENSITY = 0.30;4+const LINE_HEIGHT = FONT_SIZE * 2.3;5+const LETTER_WIDTH = FONT_SIZE * 1.35;6+const LETTER_SPACING = FONT_SIZE * 0.18;77 const SPEEDS = [5, 7, 4];88 const GLOW_COUNT = 2;99 const FALLBACK_ACCENT = "#F49C70";1010 11+// 30% of string positions are runes; the rest are null (skipped in draw).12+// Same cyclic-scroll mechanic as RuneCanvas — no isolated-character weirdness.13+const RUNE_STRING_LEN = 80;14+const DENSITY = 0.30;15+const RUNE_STRING: (string | null)[] = Array.from(16+  { length: RUNE_STRING_LEN },17+  (_, i) => {18+    const hash = (i * 2654435761) >>> 0;19+    if ((hash % 100) >= DENSITY * 100) return null;20+    return String.fromCodePoint(0xe000 + (hash % 256));21+  },22+);23+1124 function readAccentHex(): string {1225   const raw = getComputedStyle(document.documentElement)1326     .getPropertyValue("--peach-accent")@@ -35,9 +48,7 @@3548 3649     setAccent(readAccentHex());3750 38-    const accentObserver = new MutationObserver(() => {39-      setAccent(readAccentHex());40-    });51+    const accentObserver = new MutationObserver(() => setAccent(readAccentHex()));4152     accentObserver.observe(document.documentElement, {4253       attributes: true,4354       attributeFilter: ["style"],@@ -48,23 +59,11 @@4859     canvas.width = W;4960     canvas.height = H;5061 51-    const NUM_ROWS = Math.ceil(H / LINE_H) + 1;52-    const CHARS_PER_ROW = Math.ceil(W / CHAR_W) + 2;62+    const CELL_W = LETTER_WIDTH + LETTER_SPACING;63+    const SYMBOLS_PER_ROW = Math.ceil(W / CELL_W) + 4;64+    const NUM_ROWS = Math.floor(H / LINE_HEIGHT) + 1;65+    const TOTAL_STRING_W = RUNE_STRING_LEN * CELL_W || 1;5366 54-    // Deterministic sparse grid — 30% of positions occupied55-    const runeString: (string | null)[] = Array.from(56-      { length: NUM_ROWS * CHARS_PER_ROW },57-      (_, i) => {58-        const hash = (i * 2654435761) >>> 0;59-        if ((hash % 100) >= DENSITY * 100) return null;60-        return String.fromCodePoint(0xe000 + (hash % 256));61-      },62-    );63-64-    const offsets = Array.from({ length: NUM_ROWS }, () =>65-      Math.random() * CHARS_PER_ROW * CHAR_W,66-    );67-6867     interface Glow {6968       row: number;7069       col: number;@@ -74,24 +73,21 @@7473       age: number;7574     }7675 77-    function spawnGlow(): Glow {78-      let row = 0, col = 0, attempts = 0;79-      do {80-        row = Math.floor(Math.random() * NUM_ROWS);81-        col = Math.floor(Math.random() * CHARS_PER_ROW);82-        attempts++;83-      } while (!runeString[row * CHARS_PER_ROW + col] && attempts < 20);84-      return {85-        row, col,86-        phase: Math.random(),87-        speed: 0.3 + Math.random() * 0.3,88-        lifetime: 3000 + Math.random() * 4000,89-        age: 0,90-      };91-    }76+    const spawn = (): Glow => ({77+      row: Math.floor(Math.random() * NUM_ROWS),78+      col: Math.floor(Math.random() * SYMBOLS_PER_ROW),79+      phase: Math.random(),80+      speed: 0.3 + Math.random() * 0.3,81+      lifetime: 3000 + Math.random() * 4000,82+      age: 0,83+    });9284 93-    const glows: Glow[] = Array.from({ length: GLOW_COUNT }, spawnGlow);85+    const glows: Glow[] = Array.from({ length: GLOW_COUNT }, spawn);9486 87+    const offsets: number[] = Array.from({ length: NUM_ROWS }, () =>88+      Math.random() * TOTAL_STRING_W,89+    );90+9591     let lastTime = performance.now();9692     let raf = 0;9793     let running = false;@@ -113,23 +109,24 @@113109         const env = Math.sin((g.age / g.lifetime) * Math.PI);114110         const pulse = Math.sin(g.phase * Math.PI * 2) * 0.5 + 0.5;115111         glowMap.set(`${g.row},${g.col}`, Math.max(0, env) * pulse * 0.60);116-        if (g.age >= g.lifetime) Object.assign(g, spawnGlow());112+        if (g.age >= g.lifetime) Object.assign(g, spawn());117113       }118114 119115       const [ar, ag, ab] = accentRgb;120116 121117       for (let row = 0; row < NUM_ROWS; row++) {122118         const speed = SPEEDS[row % SPEEDS.length];123-        const totalW = CHARS_PER_ROW * CHAR_W;124-        offsets[row] = (offsets[row] + (speed * dt) / 1000) % totalW;125-        const y = row * LINE_H + FONT_SIZE + 12;119+        offsets[row] = (offsets[row] + (speed * dt) / 1000) % TOTAL_STRING_W;120+        const y = row * LINE_HEIGHT + FONT_SIZE + 12;126121 127-        for (let col = 0; col < CHARS_PER_ROW; col++) {128-          const char = runeString[row * CHARS_PER_ROW + col];129-          if (!char) continue;122+        for (let col = 0; col < SYMBOLS_PER_ROW; col++) {123+          const x = col * CELL_W - (offsets[row] % CELL_W);124+          if (x < -CELL_W || x > W + CELL_W) continue;130125 131-          const x = col * CHAR_W - offsets[row] + CHAR_W;132-          if (x < -CHAR_W || x > W + CHAR_W) continue;126+          const baseIdx = Math.floor(offsets[row] / CELL_W);127+          const strIdx = (((baseIdx + col) % RUNE_STRING_LEN) + RUNE_STRING_LEN) % RUNE_STRING_LEN;128+          const char = RUNE_STRING[strIdx];129+          if (!char) continue; // sparse position — blank133130 134131           const glowA = glowMap.get(`${row},${col}`) ?? 0;135132 @@ -178,10 +175,5 @@178175     });179176   });180177 181-  return (182-    <canvas183-      ref={canvas}184-      class="peach-lock-rune-canvas"185-    />186-  );178+  return <canvas ref={canvas} class="peach-lock-rune-canvas" />;187179 }

view file