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
-
+