2d677f90e079
lock screen rune background — sparse slow canvas behind unlock form with glass panel
- time
- 2026-04-28 05:28 UTC · 6 months ago
- machine
- koh2
- parent
- ce3d87b6878c
↓ peach-browser-2d677f90.tar.gzatlas at this save
files changed3 files
src/popup/components/LockRuneCanvas.tsx+187 −0src/popup/styles/popup.css+111 −2src/popup/views/Lock.tsx+3 −1src/popup/components/LockRuneCanvas.tsx+187 −0
@@ -0,0 +1,187 @@1+import { onCleanup, onMount } from "solid-js";2+3+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;7+const SPEEDS = [5, 7, 4];8+const GLOW_COUNT = 2;9+const FALLBACK_ACCENT = "#F49C70";10+11+function readAccentHex(): string {12+ const raw = getComputedStyle(document.documentElement)13+ .getPropertyValue("--peach-accent")14+ .trim();15+ if (/^#[0-9a-fA-F]{6}$/.test(raw)) return raw;16+ return FALLBACK_ACCENT;17+}18+19+export function LockRuneCanvas() {20+ let canvas!: HTMLCanvasElement;21+22+ onMount(() => {23+ const ctx = canvas.getContext("2d");24+ if (!ctx) return;25+26+ let accentRgb: [number, number, number] = [0xf4, 0x9c, 0x70];27+28+ function setAccent(hex: string) {29+ accentRgb = [30+ parseInt(hex.slice(1, 3), 16),31+ parseInt(hex.slice(3, 5), 16),32+ parseInt(hex.slice(5, 7), 16),33+ ];34+ }35+36+ setAccent(readAccentHex());37+38+ const accentObserver = new MutationObserver(() => {39+ setAccent(readAccentHex());40+ });41+ accentObserver.observe(document.documentElement, {42+ attributes: true,43+ attributeFilter: ["style"],44+ });45+46+ const W = canvas.offsetWidth || 360;47+ const H = canvas.offsetHeight || 600;48+ canvas.width = W;49+ canvas.height = H;50+51+ const NUM_ROWS = Math.ceil(H / LINE_H) + 1;52+ const CHARS_PER_ROW = Math.ceil(W / CHAR_W) + 2;53+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+68+ interface Glow {69+ row: number;70+ col: number;71+ phase: number;72+ speed: number;73+ lifetime: number;74+ age: number;75+ }76+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+ }92+93+ const glows: Glow[] = Array.from({ length: GLOW_COUNT }, spawnGlow);94+95+ let lastTime = performance.now();96+ let raf = 0;97+ let running = false;98+99+ const frame = (now: number) => {100+ const dt = Math.min(now - lastTime, 64);101+ lastTime = now;102+103+ ctx.clearRect(0, 0, W, H);104+ ctx.fillStyle = "#1a1410";105+ ctx.fillRect(0, 0, W, H);106+ ctx.font = `${FONT_SIZE}px PeachScript, monospace`;107+ ctx.textBaseline = "alphabetic";108+109+ const glowMap = new Map<string, number>();110+ for (const g of glows) {111+ g.age += dt;112+ g.phase = (g.phase + (g.speed * dt) / 1000) % 1;113+ const env = Math.sin((g.age / g.lifetime) * Math.PI);114+ const pulse = Math.sin(g.phase * Math.PI * 2) * 0.5 + 0.5;115+ glowMap.set(`${g.row},${g.col}`, Math.max(0, env) * pulse * 0.60);116+ if (g.age >= g.lifetime) Object.assign(g, spawnGlow());117+ }118+119+ const [ar, ag, ab] = accentRgb;120+121+ for (let row = 0; row < NUM_ROWS; row++) {122+ 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;126+127+ for (let col = 0; col < CHARS_PER_ROW; col++) {128+ const char = runeString[row * CHARS_PER_ROW + col];129+ if (!char) continue;130+131+ const x = col * CHAR_W - offsets[row] + CHAR_W;132+ if (x < -CHAR_W || x > W + CHAR_W) continue;133+134+ const glowA = glowMap.get(`${row},${col}`) ?? 0;135+136+ if (glowA > 0.05) {137+ ctx.save();138+ ctx.shadowColor = `rgba(${ar},${ag},${ab},${glowA * 0.9})`;139+ ctx.shadowBlur = 12;140+ ctx.fillStyle = `rgba(${ar},${ag},${ab},${0.4 + glowA * 0.5})`;141+ ctx.fillText(char, x, y);142+ ctx.restore();143+ } else {144+ ctx.fillStyle = "rgba(245,240,234,0.18)";145+ ctx.fillText(char, x, y);146+ }147+ }148+ }149+150+ raf = requestAnimationFrame(frame);151+ };152+153+ const start = () => {154+ if (running) return;155+ running = true;156+ lastTime = performance.now();157+ raf = requestAnimationFrame(frame);158+ };159+160+ const stop = () => {161+ if (!running) return;162+ running = false;163+ cancelAnimationFrame(raf);164+ };165+166+ const onVisibility = () => {167+ if (document.hidden) stop();168+ else start();169+ };170+ document.addEventListener("visibilitychange", onVisibility);171+172+ document.fonts.load(`${FONT_SIZE}px PeachScript`).finally(start);173+174+ onCleanup(() => {175+ stop();176+ accentObserver.disconnect();177+ document.removeEventListener("visibilitychange", onVisibility);178+ });179+ });180+181+ return (182+ <canvas183+ ref={canvas}184+ class="peach-lock-rune-canvas"185+ />186+ );187+}src/popup/styles/popup.css+111 −2
@@ -1525,9 +1525,42 @@15251525 padding: 2px 0 10px;15261526 }15271527 .peach-settings-group {1528- padding: 9px 16px 11px;1529- border-bottom: 1px solid var(--peach-t3);1528+ margin: 4px 10px;1529+ padding: 9px 14px 11px;1530+ border-radius: 12px;1531+ border: 1px solid rgba(255, 255, 255, 0.07);1532+ background: rgba(26, 20, 16, 0.60);1533+ backdrop-filter: blur(10px) saturate(1.3);1534+ -webkit-backdrop-filter: blur(10px) saturate(1.3);15301535 }1536+1537+.peach-settings-group + .peach-settings-group {1538+ margin-top: 6px;1539+}1540+1541+@supports not (backdrop-filter: blur(1px)) {1542+ .peach-settings-group {1543+ background: rgba(26, 20, 16, 0.90);1544+ border-color: rgba(255, 255, 255, 0.09);1545+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 4px 20px rgba(0, 0, 0, 0.35);1546+ }1547+}1548+1549+.light .peach-settings-group,1550+[data-theme="light"] .peach-settings-group {1551+ background: rgba(253, 248, 244, 0.68);1552+ border-color: rgba(0, 0, 0, 0.07);1553+ backdrop-filter: blur(10px) saturate(1.2);1554+ -webkit-backdrop-filter: blur(10px) saturate(1.2);1555+}1556+1557+@supports not (backdrop-filter: blur(1px)) {1558+ .light .peach-settings-group,1559+ [data-theme="light"] .peach-settings-group {1560+ background: rgba(253, 248, 244, 0.95);1561+ border-color: rgba(0, 0, 0, 0.09);1562+ }1563+}15311564 .peach-export-intro {15321565 padding-bottom: 8px;15331566 }@@ -3141,6 +3174,16 @@31413174 z-index: 0;31423175 }31433176 3177+.peach-lock-rune-canvas {3178+ position: absolute;3179+ inset: 0;3180+ width: 100%;3181+ height: 100%;3182+ display: block;3183+ pointer-events: none;3184+ z-index: 0;3185+}3186+31443187 .lock-screen-header {31453188 display: flex;31463189 align-items: center;@@ -3288,6 +3331,57 @@32883331 0 12px 28px rgba(0, 0, 0, 0.12);32893332 }32903333 3334+/* Glass surface — lock panel and settings groups */3335+.peach-glass {3336+ background: rgba(26, 20, 16, 0.65);3337+ border: 1px solid rgba(255, 255, 255, 0.08);3338+ border-radius: 12px;3339+ backdrop-filter: blur(12px) saturate(1.4);3340+ -webkit-backdrop-filter: blur(12px) saturate(1.4);3341+}3342+3343+@supports not (backdrop-filter: blur(1px)) {3344+ .peach-glass {3345+ background: rgba(26, 20, 16, 0.92);3346+ border: 1px solid rgba(255, 255, 255, 0.10);3347+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 4px 24px rgba(0, 0, 0, 0.4);3348+ }3349+}3350+3351+.light .peach-glass,3352+[data-theme="light"] .peach-glass {3353+ background: rgba(253, 248, 244, 0.70);3354+ border: 1px solid rgba(0, 0, 0, 0.07);3355+ backdrop-filter: blur(12px) saturate(1.2);3356+ -webkit-backdrop-filter: blur(12px) saturate(1.2);3357+}3358+3359+@supports not (backdrop-filter: blur(1px)) {3360+ .light .peach-glass,3361+ [data-theme="light"] .peach-glass {3362+ background: rgba(253, 248, 244, 0.95);3363+ border: 1px solid rgba(0, 0, 0, 0.09);3364+ }3365+}3366+3367+/* Lock panel inherits its own border-radius and box-shadow on top of glass */3368+.peach-lock-panel.peach-glass {3369+ background: rgba(26, 20, 16, 0.65);3370+ border: 1px solid rgba(255, 255, 255, 0.08);3371+ box-shadow:3372+ inset 0 1px 0 rgba(255, 255, 255, 0.05),3373+ 0 12px 28px rgba(0, 0, 0, 0.3);3374+}3375+3376+@supports not (backdrop-filter: blur(1px)) {3377+ .peach-lock-panel.peach-glass {3378+ background: rgba(26, 20, 16, 0.92);3379+ box-shadow:3380+ inset 0 1px 0 rgba(255, 255, 255, 0.05),3381+ 0 12px 28px rgba(0, 0, 0, 0.4);3382+ }3383+}3384+32913385 .peach-lock-panel--secondary {32923386 gap: 12px;32933387 }@@ -5764,4 +5858,19 @@57645858 flex-direction: column;57655859 gap: 4px;57665860 padding: 4px 4px 0;5861+}5862+5863+/* Settings body — warm accent gradient gives the glass something to blur against */5864+.peach-settings-body {5865+ background:5866+ radial-gradient(5867+ ellipse at 30% 20%,5868+ color-mix(in srgb, var(--peach-accent) 8%, transparent) 0%,5869+ transparent 60%5870+ ),5871+ radial-gradient(5872+ ellipse at 70% 80%,5873+ rgba(30, 20, 10, 0.6) 0%,5874+ transparent 50%5875+ );57675876 }src/popup/views/Lock.tsx+3 −1
@@ -1,6 +1,7 @@11 import { For, Match, Show, Switch, createMemo, createSignal, onMount } from "solid-js";22 33 import { sendMessage } from "../App";4+import { LockRuneCanvas } from "../components/LockRuneCanvas";45 import { PeachMark, PeachWordmark } from "../components/PeachMark";56 import { ImportView } from "./Import";67 @@ -179,6 +180,7 @@179180 {/* ── Unlock (hasVault = true) ───────────────────────────────── */}180181 <Match when={props.hasVault}>181182 <section class="peach-screen peach-theme lock-screen lock-screen--vault-stage" data-motion-phase="enter">183+ <LockRuneCanvas />182184 <header class="peach-screen-header lock-screen-header">183185 <div class="peach-wordmark-stack">184186 <PeachWordmark class="peach-wordmark-wrap" />@@ -200,7 +202,7 @@200202 </h1>201203 </div>202204 203- <section class="peach-lock-panel peach-lock-panel--unlock">205+ <section class="peach-lock-panel peach-lock-panel--unlock peach-glass">204206 <label class="peach-label" for="unlock-password">205207 master password206208 </label>