diff --git a/deploy/site/ambient.js b/deploy/site/ambient.js index 5f5552b..790f879 100644 --- a/deploy/site/ambient.js +++ b/deploy/site/ambient.js @@ -1,6 +1,6 @@ (() => { function randomFrom(sequence) { - let seed = Number(BigInt.asUintN(32, sequence ^ (sequence >> 32n))); + let seed = Number(BigInt.asUintN(32, sequence ^ (sequence >> 32n))) || 1; return () => { seed = Math.imul(seed ^ seed >>> 16, 0x21f0aaad); seed = Math.imul(seed ^ seed >>> 15, 0x735a2d97); @@ -9,38 +9,64 @@ }; } function createCurrent() { - return { sequence: 0n, strokes: [], from: 0, to: 0, at: 0 }; + return { sequence: 0n, shards: [], from: 0, to: 0, at: 0 }; + } + function progress(current, now) { + return Math.max(0, Math.min(1, (now - current.at) / 5000)); } function position(current, now) { - const t = Math.max(0, Math.min(1, (now - current.at) / 5000)); - return current.from + (current.to - current.from) * t; + return current.from + (current.to - current.from) * progress(current, now); + } + function lightAt(shard, t) { + return shard.light + (shard.target - shard.light) * t; + } + function makeShards(sequence) { + const random = randomFrom(sequence); + const points = []; + const columns = 5, rows = 9; + for (let row = 0; row <= rows; row++) { + for (let col = 0; col <= columns; col++) { + points.push([ + (col + (col && col < columns ? (random() - .5) * .85 : 0)) / columns, + (row + (row && row < rows ? (random() - .5) * .85 : 0)) / rows, + ]); + } + } + const shards = []; + function shard(a, b, c) { + shards.push({ points: [points[a], points[b], points[c]], tint: random(), light: 0, target: 0 }); + } + for (let row = 0; row < rows; row++) { + for (let col = 0; col < columns; col++) { + const a = row * (columns + 1) + col, b = a + 1, c = a + columns + 1, d = c + 1; + if (random() < .5) { shard(a, b, c); shard(b, d, c); } + else { shard(a, b, d); shard(a, d, c); } + } + } + return shards; } - function receive(current, sequence, now) { + function receive(current, sequence, now, entropy = 0n) { if (sequence <= 0n || sequence === current.sequence) return false; const fresh = current.sequence === 0n || sequence < current.sequence; - const random = randomFrom(sequence); + const t = progress(current, now); const delta = fresh ? 0 : Number(sequence - current.sequence > 5000n ? 5000n : sequence - current.sequence); + if (fresh) current.shards = makeShards(sequence); + const random = randomFrom(entropy); + for (const shard of current.shards) { + shard.light = lightAt(shard, t); + shard.target = entropy === 0n ? 0 : random() ** 4; + } 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 }; + function absorb(seed, message) { + for (let i = 0; i < message.length; i++) seed = Math.imul(seed ^ message.charCodeAt(i), 16777619); + return seed >>> 0; + } + if (typeof module !== 'undefined') module.exports = { createCurrent, position, receive, absorb }; if (typeof document === 'undefined') return; const canvas = document.querySelector('canvas[data-current]'); if (!canvas) return; @@ -48,28 +74,43 @@ if (!context) return; const current = createCurrent(); const reduced = matchMedia('(prefers-reduced-motion: reduce)'); - let width = 0, height = 0, frame = 0, last = 0; + let width = 0, height = 0, frame = 0, last = 0, entropy = 0; + let socket, retry; + function connect() { + clearTimeout(retry); + if (document.hidden || socket) return; + const url = new URL('/subscribe?maxMessageSizeBytes=256', location.href); + url.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + socket = new WebSocket(url); + socket.onmessage = event => { if (typeof event.data === 'string') entropy = absorb(entropy || 2166136261, event.data); }; + socket.onclose = () => { socket = null; if (!document.hidden) retry = setTimeout(connect, 15000); }; + socket.onerror = () => socket.close(); + } function paint(now) { const phase = reduced.matches ? current.to : position(current, now); + const t = reduced.matches ? 1 : progress(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; + for (const shard of current.shards) { + const points = shard.points.map(([x, y]) => [x * width, y * height]); + const light = lightAt(shard, t); + const pigment = shard.tint < .18 ? '127,121,91' : shard.tint < .5 ? '71,108,99' : '79,107,128'; + const [a, b, c] = points; 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(); + context.moveTo(...a); context.lineTo(...b); context.lineTo(...c); context.closePath(); + const centerX = (a[0] + b[0] + c[0]) / 3, centerY = (a[1] + b[1] + c[1]) / 3; + const angle = shard.tint * Math.PI + phase * 2; + const reach = Math.min(width, height) * .22; + const dx = Math.cos(angle) * reach, dy = Math.sin(angle) * reach; + const glass = context.createLinearGradient(centerX - dx, centerY - dy, centerX + dx, centerY + dy); + glass.addColorStop(0, `rgba(${pigment},.015)`); + glass.addColorStop(.48, `rgba(${pigment},${.035 + light * .23})`); + glass.addColorStop(.52, `rgba(${pigment},${.018 + light * .06})`); + glass.addColorStop(1, `rgba(${pigment},.008)`); + context.fillStyle = glass; context.fill(); + context.strokeStyle = 'rgba(74,105,112,.09)'; context.lineWidth = .5; context.stroke(); + context.beginPath(); context.moveTo(...a); context.lineTo(...b); + context.strokeStyle = `rgba(155,183,175,${.025 + light * .2})`; + context.lineWidth = .65; context.stroke(); } } function animate(now) { @@ -90,12 +131,15 @@ paint(performance.now()); } addEventListener('stream-sequence', event => { - if (receive(current, BigInt(event.detail), performance.now())) wake(); + if (receive(current, BigInt(event.detail), performance.now(), BigInt(entropy))) wake(); }); addEventListener('resize', resize); - document.addEventListener('visibilitychange', wake); + document.addEventListener('visibilitychange', () => { + if (document.hidden) { clearTimeout(retry); if (socket) socket.close(); } + else { connect(); wake(); } + }); reduced.addEventListener('change', wake); - resize(); + resize(); connect(); })(); // the window: what drifts past IS the firehose. every light is born from a diff --git a/deploy/site/index.html b/deploy/site/index.html index e63467e..08309b2 100644 --- a/deploy/site/index.html +++ b/deploy/site/index.html @@ -81,7 +81,7 @@ footer .icon{width:11px;height:11px;vertical-align:-1px;margin-right:.6ch} source - +