a0ab75fd9e93

lock screen rune string — static 9-char span row, occasional single glow, between hero and panel

time
2026-04-28 15:54 UTC · 6 months ago
machine
koh2
parent
d109a2559fe3
3 files src/popup/components/LockRuneCanvas.tsx+35 −210src/popup/styles/popup.css+24 −7src/popup/views/Lock.tsx+2 −1
src/popup/components/LockRuneCanvas.tsx+35 −210
@@ -1,222 +1,47 @@1-import { onCleanup, onMount } from "solid-js";1+import { Index, onCleanup, onMount } from "solid-js";2+import { createSignal } from "solid-js";23 3-const FONT_SIZE = 14;4-const PARTICLE_COUNT = 5;5-const GRAVITY = 14;        // px/s² pull toward target — gentle, intentional6-const MAX_SPEED = 28;      // px/s cap — slow drift, not a rush7-const ABSORB_DIST = 18;    // px from target center — particle disappears8-const FADE_OUT_DIST = 80;  // px — fade starts here, long and gradual9-const FADE_IN_MS = 1400;   // slow materialise on spawn10-const FALLBACK_ACCENT = "#F49C70";11-const FALLBACK_RUNE_IDLE_RGB = [245, 240, 234] as const;12-const FALLBACK_RUNE_IDLE_LIGHT_RGB = [34, 24, 9] as const;13-14-const RUNE_CHARS = Array.from(15-  { length: 80 },16-  (_, i) => String.fromCodePoint(0xe000 + i),4+const RUNE_COUNT = 9;5+const RUNES = Array.from(6+  { length: RUNE_COUNT },7+  () => String.fromCodePoint(0xe000 + Math.floor(Math.random() * 256)),178 );189 19-function randomRune(): string {20-  return RUNE_CHARS[Math.floor(Math.random() * RUNE_CHARS.length)];21-}22-23-function readAccentHex(): string {24-  const raw = getComputedStyle(document.documentElement)25-    .getPropertyValue("--peach-accent")26-    .trim();27-  if (/^#[0-9a-fA-F]{6}$/.test(raw)) return raw;28-  return FALLBACK_ACCENT;29-}30-31-function isLightTheme(): boolean {32-  return document.documentElement.classList.contains("light");33-}34-35-interface Particle {36-  x: number;37-  y: number;38-  vx: number;39-  vy: number;40-  char: string;41-  age: number;42-  glow: boolean;43-}44-45-function spawnParticle(46-  tx: number,47-  ty: number,48-  W: number,49-  H: number,50-): Particle {51-  // Spawn anywhere in the canvas that isn't already near the target.52-  // Bias toward the upper region since the input is near the bottom.53-  let x: number, y: number, attempts = 0;54-  do {55-    x = 20 + Math.random() * (W - 40);56-    y = 10 + Math.random() * (H * 0.80);57-    attempts++;58-  } while (Math.hypot(x - tx, y - ty) < 80 && attempts < 12);59-60-  // Initial velocity: slow drift with a slight clockwise or counterclockwise61-  // curl so particles trace curves rather than straight lines.62-  const dx = tx - x, dy = ty - y;63-  const len = Math.hypot(dx, dy) || 1;64-  const speed = 1.5 + Math.random() * 3;65-  const curl = (Math.random() < 0.5 ? 1 : -1) * (0.2 + Math.random() * 0.4);66-  const vx = (dx / len + (-dy / len) * curl) * speed;67-  const vy = (dy / len + (dx / len) * curl) * speed;68-69-  return { x, y, vx, vy, char: randomRune(), age: 0, glow: Math.random() < 0.18 };70-}71-7210 export function LockRuneCanvas() {73-  let canvas!: HTMLCanvasElement;11+  const [glowIdx, setGlowIdx] = createSignal(-1);7412 7513   onMount(() => {76-    const ctx = canvas.getContext("2d");77-    if (!ctx) return;14+    let timer: ReturnType<typeof setTimeout>;7815 79-    let accentRgb: [number, number, number] = [0xf4, 0x9c, 0x70];80-    let light = isLightTheme();81-82-    function setAccent(hex: string) {83-      accentRgb = [84-        parseInt(hex.slice(1, 3), 16),85-        parseInt(hex.slice(3, 5), 16),86-        parseInt(hex.slice(5, 7), 16),87-      ];16+    function scheduleNext() {17+      timer = setTimeout(18+        () => {19+          setGlowIdx(Math.floor(Math.random() * RUNE_COUNT));20+          timer = setTimeout(21+            () => {22+              setGlowIdx(-1);23+              scheduleNext();24+            },25+            2000 + Math.random() * 1500,26+          );27+        },28+        1500 + Math.random() * 3000,29+      );8830     }8931 90-    setAccent(readAccentHex());91-92-    const observer = new MutationObserver(() => {93-      setAccent(readAccentHex());94-      light = isLightTheme();95-    });96-    observer.observe(document.documentElement, {97-      attributes: true,98-      attributeFilter: ["style", "class"],99-    });100-101-    const W = canvas.offsetWidth || 320;102-    const H = canvas.offsetHeight || 520;103-    canvas.width = W;104-    canvas.height = H;105-106-    // Target: center of the password input, measured relative to the canvas.107-    // Default to center-bottom if input not found.108-    let tx = W / 2;109-    let ty = H * 0.80;110-    const inputEl = canvas.closest(".lock-screen")111-      ?.querySelector(".lock-password-input") as HTMLElement | null;112-    if (inputEl) {113-      const cr = canvas.getBoundingClientRect();114-      const ir = inputEl.getBoundingClientRect();115-      tx = ir.left + ir.width / 2 - cr.left;116-      ty = ir.top + ir.height / 2 - cr.top;117-    }118-119-    const particles: Particle[] = Array.from({ length: PARTICLE_COUNT }, () =>120-      spawnParticle(tx, ty, W, H),121-    );122-123-    // Stagger ages so they don't all fade in simultaneously124-    for (const p of particles) p.age = Math.random() * FADE_IN_MS;125-126-    let lastTime = performance.now();127-    let raf = 0;128-    let running = false;129-130-    const frame = (now: number) => {131-      const dt = Math.min(now - lastTime, 64);132-      lastTime = now;133-      const dt_s = dt / 1000;134-135-      ctx.clearRect(0, 0, W, H);136-      ctx.font = `${FONT_SIZE}px PeachScript, monospace`;137-      ctx.textBaseline = "middle";138-139-      const idleRgb = light ? FALLBACK_RUNE_IDLE_LIGHT_RGB : FALLBACK_RUNE_IDLE_RGB;140-      const [ar, ag, ab] = accentRgb;141-      const [ir, ig, ib] = idleRgb;142-143-      for (const p of particles) {144-        p.age += dt;145-146-        const dx = tx - p.x;147-        const dy = ty - p.y;148-        const dist = Math.hypot(dx, dy) || 1;149-150-        // Absorbed — respawn151-        if (dist < ABSORB_DIST) {152-          Object.assign(p, spawnParticle(tx, ty, W, H));153-          continue;154-        }155-156-        // Gravitational pull — stronger as particle gets closer157-        const gForce = GRAVITY * (200 / dist);158-        p.vx += (dx / dist) * gForce * dt_s;159-        p.vy += (dy / dist) * gForce * dt_s;160-161-        // Speed cap162-        const spd = Math.hypot(p.vx, p.vy);163-        if (spd > MAX_SPEED) {164-          p.vx = (p.vx / spd) * MAX_SPEED;165-          p.vy = (p.vy / spd) * MAX_SPEED;166-        }167-168-        p.x += p.vx * dt_s;169-        p.y += p.vy * dt_s;170-171-        // Opacity envelope: fade in over FADE_IN_MS, fade out near target172-        const fadeIn = Math.min(1, p.age / FADE_IN_MS);173-        const fadeOut = Math.min(1, (dist - ABSORB_DIST) / FADE_OUT_DIST);174-        const alpha = 0.14 * fadeIn * fadeOut;175-        if (alpha < 0.005) continue;176-177-        if (p.glow) {178-          ctx.save();179-          ctx.shadowColor = `rgba(${ar},${ag},${ab},${alpha * 3.5})`;180-          ctx.shadowBlur = 10;181-          ctx.fillStyle = `rgba(${ar},${ag},${ab},${alpha * 2.5})`;182-          ctx.fillText(p.char, p.x, p.y);183-          ctx.restore();184-        } else {185-          ctx.fillStyle = `rgba(${ir},${ig},${ib},${alpha})`;186-          ctx.fillText(p.char, p.x, p.y);187-        }188-      }189-190-      raf = requestAnimationFrame(frame);191-    };192-193-    const start = () => {194-      if (running) return;195-      running = true;196-      lastTime = performance.now();197-      raf = requestAnimationFrame(frame);198-    };199-200-    const stop = () => {201-      if (!running) return;202-      running = false;203-      cancelAnimationFrame(raf);204-    };205-206-    const onVisibility = () => {207-      if (document.hidden) stop();208-      else start();209-    };210-    document.addEventListener("visibilitychange", onVisibility);211-212-    document.fonts.load(`${FONT_SIZE}px PeachScript`).finally(start);213-214-    onCleanup(() => {215-      stop();216-      observer.disconnect();217-      document.removeEventListener("visibilitychange", onVisibility);218-    });32+    scheduleNext();33+    onCleanup(() => clearTimeout(timer));21934   });22035 221-  return <canvas ref={canvas} class="peach-lock-rune-canvas" />;36+  return (37+    <div class="peach-lock-rune-string" aria-hidden="true">38+      <Index each={RUNES}>39+        {(char, idx) => (40+          <span classList={{ "is-glowing": glowIdx() === idx }}>41+            {char()}42+          </span>43+        )}44+      </Index>45+    </div>46+  );22247 }

view file

src/popup/styles/popup.css+24 −7
@@ -3174,14 +3174,31 @@31743174   z-index: 0;31753175 }31763176 3177-.peach-lock-rune-canvas {3178-  position: absolute;3179-  inset: 0;3180-  width: 100%;3181-  height: 100%;3182-  display: block;3177+.peach-lock-rune-string {3178+  display: flex;3179+  justify-content: center;3180+  gap: 6px;3181+  padding: 2px 0;3182+  font-family: PeachScript, monospace;3183+  font-size: 13px;3184+  letter-spacing: 0.05em;31833185   pointer-events: none;3184-  z-index: 0;3186+  user-select: none;3187+  position: relative;3188+  z-index: 1;3189+}3190+3191+.peach-lock-rune-string span {3192+  color: color-mix(in srgb, var(--peach-t0) 12%, transparent);3193+  text-shadow: none;3194+  transition: color 0.9s ease, text-shadow 0.9s ease;3195+}3196+3197+.peach-lock-rune-string span.is-glowing {3198+  color: color-mix(in srgb, var(--peach-accent) 65%, transparent);3199+  text-shadow:3200+    0 0 8px color-mix(in srgb, var(--peach-accent) 50%, transparent),3201+    0 0 20px color-mix(in srgb, var(--peach-accent) 22%, transparent);31853202 }31863203 31873204 .lock-screen-header {

view file

src/popup/views/Lock.tsx+2 −1
@@ -180,7 +180,6 @@180180       {/* ── Unlock (hasVault = true) ───────────────────────────────── */}181181       <Match when={props.hasVault}>182182         <section class="peach-screen peach-theme lock-screen lock-screen--vault-stage" data-motion-phase="enter">183-          <LockRuneCanvas />184183           <header class="peach-screen-header lock-screen-header">185184             <div class="peach-wordmark-stack">186185               <PeachWordmark class="peach-wordmark-wrap" />@@ -201,6 +200,8 @@201200               your vault.202201             </h1>203202           </div>203+204+          <LockRuneCanvas />204205 205206           <section class="peach-lock-panel peach-lock-panel--unlock peach-glass">206207             <label class="peach-label" for="unlock-password">

view file