// pixel-live.jsx — the LIVE animated pixel "S" for SoftLAN. // At rest it's the baseline concept-5 S. On hover (and at random idle moments) // it "comes alive": slithers like the game Snake across the grid then reforms, // or glitches with an RGB datamosh. Exposes play(trick) via ref. // // Grid geometry matches the baseline MarkPixel exactly so the swap is seamless. const { useState, useEffect, useRef, useImperativeHandle, forwardRef } = React; // ── shared grid geometry (identical to baseline MarkPixel) ── const LCELL = 17, LOFF = 7.5, LGAP = 2.4, LR = 3; const LSIZE = LCELL - LGAP; // 14.6 // the "S" as an ORDERED walk (tail → head). Each step is grid-adjacent. // head = (0,4) bottom-left, tail = (4,0) top-right. const SORD = [ [4,0],[3,0],[2,0],[1,0],[0,0], // top bar (R→L) [0,1], // left drop [0,2],[1,2],[2,2],[3,2],[4,2], // middle bar (L→R) [4,3], // right drop [4,4],[3,4],[2,4],[1,4],[0,4], // bottom bar (R→L) ]; // boustrophedon sweep of the whole 5×5 board, (0,4) → (4,0) — the Snake "playfield". const BOUS = (() => { const b = []; for (let col = 0; col < 5; col++) { const rows = col % 2 === 0 ? [4,3,2,1,0] : [0,1,2,3,4]; rows.forEach(r => b.push([col, r])); } return b; // 25 cells, b[0]=(0,4), b[24]=(4,0) })(); // full head path P: lay the S, sweep the board, re-lay the S. const PATH = (() => { const p = SORD.map(c => c.slice()); // 0..16 (full S) for (let i = 1; i < BOUS.length; i++) p.push(BOUS[i].slice()); // 17..40 for (let i = 1; i < SORD.length; i++) p.push(SORD[i].slice()); // 41..56 return p; // length 57 })(); const I0 = 16, I1 = PATH.length - 1; // animate head index 16 → 56 // snake length schedule: full S → short snake → full S function lenAt(i) { if (i <= 16) return 17; if (i <= 26) return Math.round(17 + (7 - 17) * ((i - 16) / 10)); if (i <= 40) return 7; if (i <= 56) return Math.round(7 + (17 - 7) * ((i - 40) / 16)); return 17; } // hex → rgb mixed toward white(t>0)/black(t<0) function lshade(hex, t) { let h = (hex || '#000').replace('#', ''); if (h.length === 3) h = h.split('').map(x => x + x).join(''); let r = parseInt(h.slice(0,2),16), g = parseInt(h.slice(2,4),16), b = parseInt(h.slice(4,6),16); const tgt = t >= 0 ? 255 : 0, a = Math.min(1, Math.abs(t)); r = Math.round(r+(tgt-r)*a); g = Math.round(g+(tgt-g)*a); b = Math.round(b+(tgt-b)*a); return `rgb(${r},${g},${b})`; } function px(col, row) { return { x: LOFF + col * LCELL + LGAP / 2, y: LOFF + row * LCELL + LGAP / 2 }; } function Cell({ col, row, fill, opacity = 1, dx = 0, dy = 0, w = LSIZE, h = LSIZE, rx = LR }) { const { x, y } = px(col, row); return ; } // ── renderers ── function renderStatic(accent) { return SORD.map(([c, r], i) => ); } function renderSnake(p, accent) { const i = Math.min(I1, Math.max(I0, I0 + Math.round(p * (I1 - I0)))); const L = lenAt(i); const start = Math.max(0, i - L + 1); const body = PATH.slice(start, i + 1); const n = body.length; return body.map((cell, j) => { const t = n > 1 ? j / (n - 1) : 1; // 0 tail → 1 head const fill = lshade(accent, (t - 0.45) * 0.5); // head brighter, tail darker const opacity = 0.42 + 0.58 * t; const isHead = j === n - 1; const grow = isHead ? 1.06 : 1; const s = LSIZE * grow, d = (LSIZE - s) / 2; return ; }); } const GLX = [11, -7, 13, -5, 9]; // per-row horizontal kick function renderGlitch(p, accent) { const burst = Math.max(0, 1 - p) * (0.45 + 0.55 * Math.abs(Math.sin(p * 26))); return SORD.map(([c, r], i) => { const dx = GLX[r] * burst; const drop = burst > 0.55 && Math.sin(i * 12.9 + p * 50) > 0.82; // occasional dropout const flick = 0.7 + 0.3 * Math.abs(Math.sin(p * 70 + i)); return ; }); } function renderRGB(p, accent) { const burst = Math.pow(Math.max(0, 1 - p), 1.25); const jit = Math.sin(p * 52) * 0.5 + Math.sin(p * 31) * 0.5; const off = (4.5 + 2.5 * jit) * burst; const voff = 1.6 * jit * burst; const layers = [ { fill: '#ff2d55', dx: -off, dy: -voff, op: 0.8 }, { fill: '#21e3a8', dx: off, dy: voff, op: 0.8 }, { fill: accent, dx: 0, dy: 0, op: 1 }, ]; return layers.flatMap((ly, li) => SORD.map(([c, r], i) => ( )) ); } const RENDER = { snake: renderSnake, glitch: renderGlitch, rgb: renderRGB }; const TRICK_DUR = { snake: 2600, glitch: 1100, rgb: 1350 }; const POOL = ['snake', 'snake', 'snake', 'rgb', 'glitch']; // snake-weighted function randomTrick() { return POOL[Math.floor(Math.random() * POOL.length)]; } const LiveLogo = forwardRef(function LiveLogo( { accent = '#3b82f6', size = 140, idleAuto = true, idleMin = 5000, idleMax = 11000 }, ref ) { const [anim, setAnim] = useState(null); // {trick, p} | null const rafRef = useRef(0); const playingRef = useRef(false); const play = (trick) => { if (playingRef.current) return; playingRef.current = true; const dur = TRICK_DUR[trick] || 2000; const t0 = performance.now(); const step = (now) => { const p = (now - t0) / dur; if (p >= 1) { setAnim(null); playingRef.current = false; return; } setAnim({ trick, p }); rafRef.current = requestAnimationFrame(step); }; setAnim({ trick, p: 0 }); rafRef.current = requestAnimationFrame(step); }; useImperativeHandle(ref, () => ({ play, isPlaying: () => playingRef.current })); useEffect(() => { if (!idleAuto) return; let timer; const schedule = () => { timer = setTimeout(() => { if (!document.hidden && !playingRef.current) play(randomTrick()); schedule(); }, idleMin + Math.random() * (idleMax - idleMin)); }; schedule(); return () => clearTimeout(timer); }, [idleAuto, idleMin, idleMax]); useEffect(() => () => cancelAnimationFrame(rafRef.current), []); const content = anim ? RENDER[anim.trick](anim.p, accent) : renderStatic(accent); return ( play(randomTrick())} onClick={() => play('snake')}> {content} ); }); Object.assign(window, { LiveLogo, renderSnake, renderGlitch, renderRGB, renderStatic });