diff --git a/deploy/site/ambient.js b/deploy/site/ambient.js index bcb2f47..5f5552b 100644 --- a/deploy/site/ambient.js +++ b/deploy/site/ambient.js @@ -1,10 +1,111 @@ +(() => { + function randomFrom(sequence) { + let seed = Number(BigInt.asUintN(32, sequence ^ (sequence >> 32n))); + return () => { + seed = Math.imul(seed ^ seed >>> 16, 0x21f0aaad); + seed = Math.imul(seed ^ seed >>> 15, 0x735a2d97); + seed ^= seed >>> 15; + return (seed >>> 0) / 4294967296; + }; + } + function createCurrent() { + return { sequence: 0n, strokes: [], from: 0, to: 0, at: 0 }; + } + function position(current, now) { + const t = Math.max(0, Math.min(1, (now - current.at) / 5000)); + return current.from + (current.to - current.from) * t; + } + function receive(current, sequence, now) { + if (sequence <= 0n || sequence === current.sequence) return false; + const fresh = current.sequence === 0n || sequence < current.sequence; + const random = randomFrom(sequence); + const delta = fresh ? 0 : Number(sequence - current.sequence > 5000n ? 5000n : sequence - current.sequence); + current.from = fresh ? 0 : position(current, now); + current.to = current.from + delta / 50000; + current.at = now; + current.sequence = sequence; + if (fresh) current.strokes = []; + const count = fresh ? 42 : Math.min(6, Math.ceil(delta / 300)); + for (let i = 0; i < count; i++) { + current.strokes.push({ + x: (fresh ? random() * 1.3 - .15 : -.2 + random() * .3) - current.from, + y: .05 + random() * .9, + length: .12 + random() * .24, + bend: (random() - .5) * .035, + thickness: .7 + random() * 2, + pigment: random() < .18 ? '133,111,77' : '81,110,122', + opacity: .05 + random() * .09, + }); + } + current.strokes = current.strokes.filter(stroke => stroke.x + current.from < 1.1).slice(-80); + return true; + } + if (typeof module !== 'undefined') module.exports = { createCurrent, position, receive }; + if (typeof document === 'undefined') return; + const canvas = document.querySelector('canvas[data-current]'); + if (!canvas) return; + const context = canvas.getContext('2d'); + if (!context) return; + const current = createCurrent(); + const reduced = matchMedia('(prefers-reduced-motion: reduce)'); + let width = 0, height = 0, frame = 0, last = 0; + function paint(now) { + const phase = reduced.matches ? current.to : position(current, now); + context.clearRect(0, 0, width, height); + for (const stroke of current.strokes) { + const x = (stroke.x + phase) * width; + const y = stroke.y * height; + const length = stroke.length * width; + const bend = (stroke.bend + Math.sin(phase * 3 + stroke.y * 8) * .004) * Math.min(height, 800); + const thickness = stroke.thickness; + context.beginPath(); + context.moveTo(x, y); + context.bezierCurveTo(x + length * .28, y + bend, x + length * .64, y - bend, x + length, y); + context.bezierCurveTo(x + length * .68, y - bend + thickness, x + length * .3, y + bend + thickness, x, y); + context.fillStyle = `rgba(${stroke.pigment},${stroke.opacity})`; + context.fill(); + context.beginPath(); + context.moveTo(x + length * .12, y + thickness * .35); + context.bezierCurveTo(x + length * .28, y + bend, x + length * .64, y - bend, x + length * .85, y); + context.strokeStyle = `rgba(${stroke.pigment},${stroke.opacity * .4})`; + context.lineWidth = .45; + context.stroke(); + } + } + function animate(now) { + frame = 0; + if (document.hidden) return; + if (now - last >= 50) { paint(now); last = now; } + if (!reduced.matches && now < current.at + 5000) frame = requestAnimationFrame(animate); + else paint(now); + } + function wake() { + if (!frame && !document.hidden) frame = requestAnimationFrame(animate); + } + function resize() { + const ratio = Math.min(devicePixelRatio || 1, 2); + width = canvas.clientWidth; height = canvas.clientHeight; + canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio); + context.setTransform(ratio, 0, 0, ratio, 0, 0); + paint(performance.now()); + } + addEventListener('stream-sequence', event => { + if (receive(current, BigInt(event.detail), performance.now())) wake(); + }); + addEventListener('resize', resize); + document.addEventListener('visibilitychange', wake); + reduced.addEventListener('change', wake); + resize(); +})(); + // the window: what drifts past IS the firehose. every light is born from a // real event on the live websocket (posts filter, ~20KB/s); depth is blur. // budget: sprite-cached discs (no per-frame gradients), 30fps, socket closed // while the tab is hidden, poll-only fallback if websockets fail. (() => { + if (typeof document === 'undefined') return; const cv = document.getElementById("depth"); - if (!cv) return; + if (!cv || cv.hasAttribute('data-current')) return; const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; const ctx = cv.getContext("2d"); let W, H, dpr; diff --git a/deploy/site/index.html b/deploy/site/index.html index 6a78a8f..e63467e 100644 --- a/deploy/site/index.html +++ b/deploy/site/index.html @@ -19,7 +19,7 @@ :root{--bg:#050607;--ink:#b6c2c9;--dim:#7d8992;--faint:#39424a;--hot:#e2ebf0;--water:#6f9cb0} *{margin:0;padding:0;box-sizing:border-box} html,body{background:var(--bg);color:var(--ink);font:14px/1.65 ui-monospace,"SF Mono",Menlo,Consolas,monospace} -body{isolation:isolate;min-height:100vh;background:#080d12} +body{isolation:isolate;min-height:100vh;background:linear-gradient(155deg,#091214,#080d12 55%,#0c100e)} a{color:inherit;text-decoration:none} .wrap{max-width:78ch;margin:0 auto;padding:0 2ch;display:flex;flex-direction:column;min-height:100vh} header{display:flex;flex-direction:column;align-items:center;gap:.35rem;padding:1.7rem 0 1.9rem} @@ -51,12 +51,12 @@ footer a:hover{color:var(--hot);border-bottom-color:var(--water)} @media (max-width:640px){#river{font-size:11px}.wrap{padding:0 1.5ch} footer{gap:.45rem 2ch}#stats{flex-basis:100%;text-align:center}} footer .icon{width:11px;height:11px;vertical-align:-1px;margin-right:.6ch} -#depth{position:fixed;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none;opacity:.65} +#depth{position:fixed;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none;opacity:.55} .wrap{position:relative}
- +