igneum/site/scenes/pulse.js
igneum-labs 575153408a Scenes lab: three animated views of the live feed on /scenes (Forge, Lattice, Pulse), unlinked, with captures and ten-second recordings
the project lead, 6 October 2026, item 3: three alternative scenes for the same live data, side by side at 1440, each a self-contained
module on one shared feed, the ledger's words in the legend (pending, included, excluded, proven, locked).

site/scenes/feed.js polls /api/live every 2 s and pushes the reply to every scene; it carries the site's state words
(connecting, live devnet, observer offline with the age, live feed unavailable) and the shared helpers (theme tokens from
CSS, the word for a block, DPR-sharp sizing, a frame loop that stops when hidden or off screen and draws a still frame under
reduced motion). Every mark in every scene is a block the observer stored; nothing is invented.

A, Forge (site/scenes/forge.js): blocks rise from the hearth as embers, one column per miner, at the height of their header
age over a 120 s window; a block cools as its word changes (pending molten, included ember, proven bone, excluded ash);
a new arrival glows for a second; a lock is a dark band that sweeps up the scene at the newest locked checkpoint, and
everything under it is final and settles to ash.

B, Lattice (site/scenes/lattice.js): a slow isometric DAG, time left to right, the selected chain as the lit spine in the
front lane, the other miners one shallow step deeper each (ranked by blocks, the shear capped to a third of the width so a
narrow card keeps its time axis), parent threads reaching forward, chain edges heavy with a soft halo, checkpoints as rings
that close around the spine when they lock with the lock weight beside them, new blocks glowing in; the camera drifts at
chain speed.

C, Pulse (site/scenes/pulse.js): the tip at the centre breathing, time outward with a ring every 30 s, each miner a ray,
each block a bead on its ray drifting outward, filling when proven, hollow while pending; the selected chain as short arcs
winding between consecutive beads (never a chord through the centre); a checkpoint is a ring that hardens from dashed to
solid when it locks.

site/scenes.html: the three cards side by side at 1440 and stacked under 1000 px, the shared legend, a note on the window
and reduced motion; ?only=a|b|c shows one scene full width (the card filter copies the children before removing, so the
right card survives). noindex, not linked from any page; registered in the build's PAGES so it takes the shared chrome.

Captures (docs/plans/site-ui-3-shots/scenes/, one niced headless Chromium): scenes-1440-dark.jpg (the three side by side),
scene-{a,b,c}-1440-dark.jpg and scene-{a,b,c}-390-dark.jpg, and scene-{a,b,c}-1440-dark.webm, ten seconds each at 1440 by
900 (VP8, recorded by Playwright and cut with its ffmpeg, which has no mp4 muxer). No console errors on any scene at either
width; no horizontal overflow at 390. Tonight's chain shows in all three: finality paused, so no band, closed ring or hardened
ring appears, and the checkpoint marks read pending.

Checks: identity grep 0 hits over 232 export files and 33 served site files, link check 931 links across 16 pages 0 broken,
ledger text 43 of 43, contrast 32 pairs 0 under 4.5:1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 20:02:57 +00:00

60 lines
5.4 KiB
JavaScript

/* Scene C, Pulse. A radial view: the tip at the centre, time outward in rings, each miner a ray. A block is a bead on its
miner's ray at the radius of its age; a proven bead fills; a locked checkpoint is a ring that hardens from dashed to
solid at the radius where it locked. Beads drift outward as time passes. */
(function (root) {
'use strict';
var F = root.IgneumFeed;
function mount(canvas, opts) {
opts = opts || {};
var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme();
var blocks = [], byHash = {}, rays = {}, rayN = 0, cps = [], windowS = opts.window || 120;
root.addEventListener('resize', function () { dims = size(); L.kick(); });
function vnow() { return Date.now() - 2500; }
function ray(id) { if (!(id in rays)) rays[id] = rayN++; return rays[id]; }
function push(d) {
var now = performance.now();
d.blocks.forEach(function (b) { var e = byHash[b.hash]; if (e) { e.src = b; e.word = F.word(b); return; } e = { hash: b.hash, src: b, word: F.word(b), ray: ray(b.miner || '?'), born: now }; byHash[b.hash] = e; blocks.push(e); });
var cut = vnow() - windowS * 1000 - 20000;
blocks = blocks.filter(function (e) { if (e.src.ts < cut) { delete byHash[e.hash]; return false; } return true; });
cps = (d.finality && d.finality.checkpoints) || [];
L.kick();
}
function polar(e) { var R = Math.min(dims.W, dims.H) / 2 - 14, cx = dims.W / 2, cy = dims.H / 2; var r = 10 + (R - 10) * ((vnow() - e.src.ts) / (windowS * 1000)); var ang = ((e.ray * 0.618034) % 1) * Math.PI * 2 - Math.PI / 2; return { x: cx + Math.cos(ang) * r, y: cy + Math.sin(ang) * r, r: r, ang: ang }; }
function draw(t) {
var W = dims.W, H = dims.H; T = F.theme(); var cx = W / 2, cy = H / 2, R = Math.min(W, H) / 2 - 14;
ctx.clearRect(0, 0, W, H);
// time rings every 30 s
ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1;
for (var s = 30; s < windowS; s += 30) { var rr = 10 + (R - 10) * (s / windowS); ctx.beginPath(); ctx.arc(cx, cy, rr, 0, Math.PI * 2); ctx.stroke(); }
// the tip
var pulse = 0.5 + 0.5 * Math.sin(t / 900); var tg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 26); tg.addColorStop(0, F.rgba(T.ember, 0.35 + 0.2 * pulse)); tg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = tg; ctx.beginPath(); ctx.arc(cx, cy, 26, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = T.ember; ctx.beginPath(); ctx.arc(cx, cy, 3.5, 0, Math.PI * 2); ctx.fill();
// checkpoint rings, hardening when locked
cps.forEach(function (c) {
var ref = null, best = Infinity; blocks.forEach(function (e) { if (e.src.blue_score === null) return; var dd = Math.abs(e.src.blue_score - c.blue_score); if (dd < best) { best = dd; ref = e; } });
if (!ref || best > 3) return; var p = polar(ref); if (p.r > R) return; var locked = c.state === 'locked', age = (performance.now() - ref.born) / 1000, hard = locked ? Math.min(1, age / 2) : 0;
ctx.strokeStyle = F.rgba(T.ember, locked ? 0.35 + 0.5 * hard : 0.3); ctx.lineWidth = locked ? 1 + hard : 1; ctx.setLineDash(locked && hard > 0.9 ? [] : [3, 6]);
ctx.beginPath(); ctx.arc(cx, cy, p.r, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
});
// the selected chain as a soft spiral thread through its beads
var chain = blocks.filter(function (e) { return e.src.chain; }).sort(function (a, b) { return a.src.ts - b.src.ts; });
// the selected chain as short arcs between consecutive chain beads (the arc follows the ring between the two radii, never a chord through the centre)
if (chain.length > 1) { ctx.strokeStyle = F.rgba(T.ember, 0.28); ctx.lineWidth = 1; for (var i = 1; i < chain.length; i++) { var p0 = polar(chain[i - 1]), p1 = polar(chain[i]); if (p0.r > R || p1.r > R) continue; var a0 = p0.ang, a1 = p1.ang, da = ((a1 - a0 + Math.PI * 3) % (Math.PI * 2)) - Math.PI; var steps = 10; ctx.beginPath(); for (var k = 0; k <= steps; k++) { var f = k / steps, rr = p0.r + (p1.r - p0.r) * f, aa = a0 + da * f; var x = cx + Math.cos(aa) * rr, y = cy + Math.sin(aa) * rr; if (k) ctx.lineTo(x, y); else ctx.moveTo(x, y); } ctx.stroke(); } }
// beads
blocks.forEach(function (e) {
var p = polar(e); if (p.r > R + 6) return; var w = e.word, age = (t - e.born) / 1000;
var col = w === 'proven' ? T.bone : (w === 'included' || w === 'locked') ? T.ember : w === 'excluded' ? T.ash : T.molten;
var rad = e.src.chain ? 4 : 3;
if (age < 1.2) { var gl = 1 - age / 1.2; var rg = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 16); rg.addColorStop(0, F.rgba(T.ember, 0.5 * gl)); rg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(p.x, p.y, 16, 0, Math.PI * 2); ctx.fill(); }
ctx.globalAlpha = w === 'excluded' ? 0.45 : 1;
ctx.beginPath(); ctx.arc(p.x, p.y, rad, 0, Math.PI * 2);
if (w === 'proven' || w === 'included' || w === 'locked') { ctx.fillStyle = col; ctx.fill(); } else { ctx.fillStyle = T.row; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.2; ctx.stroke(); }
if (w === 'locked') { ctx.strokeStyle = F.rgba(T.ember, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(p.x, p.y, rad + 4, 0, Math.PI * 2); ctx.stroke(); }
ctx.globalAlpha = 1;
});
}
var L = F.loop(draw, canvas); L.kick();
return { push: push, redraw: L.kick };
}
root.IgneumPulse = { mount: mount };
})(window);