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
↓ peach-browser-fa601515.tar.gzatlas at this save
files changed1 file
src/popup/components/RuneBackground.tsx+94 −2src/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