fa601515c1a3

add accentGlow prop to RuneBackground with sine-wave pulse glow slots

time
2026-08-09 18:41 UTC · 2 months ago
machine
koh2
agent
opencode/laguna-s-2.1-free
session
lock screen rune background
parent
14c3e3aa08fa
1 file src/popup/components/RuneBackground.tsx+94 −2
src/popup/components/RuneBackground.tsx+94 −2
@@ -2,6 +2,10 @@22 33 interface RuneBackgroundProps {44   opacity?: number;5+  /** When true, a handful of runes pulse with the accent color at random6+   *  screen positions, mirroring the Android lock-screen's RuneGridBackground7+   *  glow. Reads `--peach-accent` from the root element. */8+  accentGlow?: boolean;59 }610 711 const FONT_SIZE = 14;@@ -13,6 +17,7 @@1317 // Atmosphere, not focus.1418 const SPEEDS = [8, 11, 6];1519 const RUNE_LEN = 120;20+const FALLBACK_ACCENT = "#F49C70";1621 1722 // Deterministic rune string drawn from the PeachScript PUA range1823 // (U+E000..U+E0FF). Same characters every render so the background@@ -25,6 +30,23 @@2530   return out.join("");2631 }2732 33+function readAccentHex(): string {34+  const raw = getComputedStyle(document.documentElement)35+    .getPropertyValue("--peach-accent")36+    .trim();37+  if (/^#[0-9a-fA-F]{6}$/.test(raw)) return raw;38+  return FALLBACK_ACCENT;39+}40+41+function hexToRgb(hex: string): [number, number, number] {42+  hex = hex.replace("#", "");43+  return [44+    parseInt(hex.slice(0, 2), 16),45+    parseInt(hex.slice(2, 4), 16),46+    parseInt(hex.slice(4, 6), 16),47+  ];48+}49+2850 export function RuneBackground(props: RuneBackgroundProps) {2951   let canvas!: HTMLCanvasElement;3052   let raf = 0;@@ -47,6 +69,47 @@4769       Math.random() * TOTAL_W,4870     );4971 72+    // ── Accent glow (lock screen) ─────────────────────────────────73+    let accentHex = readAccentHex();74+    let accentRgb = hexToRgb(accentHex);75+76+    const accentObserver = new MutationObserver(() => {77+      const next = readAccentHex();78+      if (next !== accentHex) {79+        accentHex = next;80+        accentRgb = hexToRgb(next);81+      }82+    });83+    accentObserver.observe(document.documentElement, {84+      attributes: true,85+      attributeFilter: ["style"],86+    });87+88+    interface Glow {89+      row: number;90+      col: number;91+      phase: number;92+      speed: number;93+      lifetime: number;94+      age: number;95+    }96+97+    const spawn = (): Glow => ({98+      row: Math.floor(Math.random() * NUM_ROWS),99+      col: Math.floor(Math.random() * CHARS_PER_ROW),100+      phase: Math.random(),101+      speed: 0.4 + Math.random() * 0.4,102+      lifetime: 2000 + Math.random() * 3000,103+      age: 0,104+    });105+106+    const GLOW_COUNT = 8;107+    const glows: Glow[] = props.accentGlow108+      ? Array.from({ length: GLOW_COUNT }, spawn)109+      : [];110+111+    const glowMap = new Map<string, number>();112+50113     let last = performance.now();51114 52115     const frame = (now: number) => {@@ -55,9 +118,24 @@55118 56119       ctx.clearRect(0, 0, W, H);57120       ctx.font = `${FONT_SIZE}px PeachScript, monospace`;58-      ctx.fillStyle = `rgba(245, 240, 234, ${baseOpacity})`;59121       ctx.textBaseline = "alphabetic";60122 123+      // Advance glow timers124+      glowMap.clear();125+      if (props.accentGlow) {126+        const [ar, ag, ab] = accentRgb;127+        for (const g of glows) {128+          g.age += dt;129+          g.phase = (g.phase + (g.speed * dt) / 1000) % 1;130+          const lifetimeFraction = g.age / g.lifetime;131+          const lifetimeEnvelope = Math.sin(lifetimeFraction * Math.PI);132+          const pulseAlpha = Math.sin(g.phase * Math.PI * 2) * 0.5 + 0.5;133+          const alpha = Math.max(0, lifetimeEnvelope) * pulseAlpha * 0.85;134+          glowMap.set(`${g.row},${g.col}`, alpha);135+          if (g.age >= g.lifetime) Object.assign(g, spawn());136+        }137+      }138+61139       for (let row = 0; row < NUM_ROWS; row++) {62140         const speed = SPEEDS[row % SPEEDS.length];63141         offsets[row] = (offsets[row] + (speed * dt) / 1000) % TOTAL_W;@@ -70,7 +148,20 @@70148           const x = col * CHAR_W - xOffset;71149           if (x < -CHAR_W || x > W + CHAR_W) continue;72150           const idx = (((baseIdx + col) % runes.length) + runes.length) % runes.length;73-          ctx.fillText(runes[idx]!, x, y);151+152+          const glowAlpha = glowMap.get(`${row},${col}`) ?? 0;153+          if (glowAlpha > 0.01) {154+            const [ar, ag, ab] = accentRgb;155+            ctx.save();156+            ctx.shadowColor = `rgba(${ar}, ${ag}, ${ab}, ${glowAlpha * 0.9})`;157+            ctx.shadowBlur = 10;158+            ctx.fillStyle = `rgba(${ar}, ${ag}, ${ab}, ${0.6 + glowAlpha * 0.4})`;159+            ctx.fillText(runes[idx]!, x, y);160+            ctx.restore();161+          } else {162+            ctx.fillStyle = `rgba(245, 240, 234, ${baseOpacity})`;163+            ctx.fillText(runes[idx]!, x, y);164+          }74165         }75166       }76167 @@ -106,6 +197,7 @@106197     onCleanup(() => {107198       stop();108199       document.removeEventListener("visibilitychange", onVisibility);200+      accentObserver.disconnect();109201     });110202   });111203 

view file