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>
77 lines
6.2 KiB
JavaScript
77 lines
6.2 KiB
JavaScript
/* Scene B, Lattice. A slow isometric DAG: time runs left to right, each miner lane sits one step deeper, so parents stand
|
|
behind their children. The selected chain is a lit spine; every other edge is a thin thread. Checkpoints are rings that
|
|
close around the spine where they lock. The camera drifts at chain speed. */
|
|
(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 = {}, lanes = {}, laneN = 0, cps = [], windowS = opts.window || 120, rank = {};
|
|
var DEPTH = 8; // up to DEPTH lanes deep; the isometric step scales with the canvas (about 4.5% of its height per lane)
|
|
// the step scales with the canvas; the whole shear never takes more than a third of the width, so a narrow card keeps its time axis
|
|
function stepY() { return Math.max(7, Math.min(dims.H * 0.045, (dims.W * 0.33) / (DEPTH * 1.4))); } function stepX() { return stepY() * 1.4; }
|
|
root.addEventListener('resize', function () { dims = size(); L.kick(); });
|
|
function lane(id) { if (!(id in lanes)) lanes[id] = laneN++; return lanes[id]; }
|
|
function vnow() { return Date.now() - 2500; }
|
|
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), lane: lane(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; });
|
|
blocks.sort(function (a, b) { return a.src.ts - b.src.ts; });
|
|
// lanes ranked: the miners of the selected chain nearest the front, then by blocks in the window
|
|
var score = {}; blocks.forEach(function (e) { var m = e.src.miner || '?'; score[m] = (score[m] || 0) + 1 + (e.src.chain ? 3 : 0); });
|
|
var ids = Object.keys(score).sort(function (a, b) { return score[b] - score[a] || (a < b ? -1 : 1); }); rank = {}; ids.forEach(function (id, i) { rank[id] = i; });
|
|
cps = (d.finality && d.finality.checkpoints) || [];
|
|
L.kick();
|
|
}
|
|
function project(e) {
|
|
// the selected chain is the front lane; the other blocks sit one step deeper per miner rank, the many small miners sharing the back
|
|
var W = dims.W, H = dims.H, r = rank[e.src.miner || '?'] || 0, z = e.src.chain ? 0 : (1 + (r % (DEPTH - 1))) / DEPTH;
|
|
var SX = stepX(), SY = stepY();
|
|
var x = 40 + (W - 80 - DEPTH * SX) * (1 - (vnow() - e.src.ts) / (windowS * 1000)) + z * DEPTH * SX;
|
|
var y = H * 0.74 - z * DEPTH * SY;
|
|
return { x: x, y: y, z: z };
|
|
}
|
|
function draw(t) {
|
|
var W = dims.W, H = dims.H; T = F.theme();
|
|
ctx.clearRect(0, 0, W, H);
|
|
// the ground plane: three faint isometric rails
|
|
ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1;
|
|
for (var k = 0; k <= DEPTH; k += 2) { var yy = H * 0.74 - k * stepY(), xx = k * stepX(); ctx.beginPath(); ctx.moveTo(xx, yy); ctx.lineTo(W, yy); ctx.stroke(); }
|
|
var pos = {}; blocks.forEach(function (e) { pos[e.hash] = project(e); });
|
|
// threads, back to front
|
|
var ordered = blocks.slice().sort(function (a, b) { return pos[b.hash].z - pos[a.hash].z; });
|
|
ordered.forEach(function (e) {
|
|
var p = pos[e.hash]; if (p.x < -20) return;
|
|
(e.src.parents || []).forEach(function (h) { var q = byHash[h]; if (!q) return; var pq = pos[h]; var spine = e.src.chain && q.src.chain;
|
|
ctx.strokeStyle = spine ? F.rgba(T.ember, 0.9) : F.rgba(T.ash, e.word === 'excluded' ? 0.1 : 0.2); ctx.lineWidth = spine ? 2.2 : 1;
|
|
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(pq.x, pq.y); ctx.stroke();
|
|
if (spine) { ctx.strokeStyle = F.rgba(T.ember, 0.16); ctx.lineWidth = 8; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(pq.x, pq.y); ctx.stroke(); } });
|
|
});
|
|
// checkpoint rings around the spine
|
|
cps.forEach(function (c) {
|
|
var ref = null, best = Infinity; blocks.forEach(function (e) { if (!e.src.chain || 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 = pos[ref.hash]; if (p.x < 0 || p.x > W) return;
|
|
var locked = c.state === 'locked', age = (performance.now() - ref.born) / 1000, close = locked ? Math.min(1, age / 1.5) : 0.35;
|
|
ctx.strokeStyle = F.rgba(T.ember, locked ? 0.85 : 0.35); ctx.lineWidth = locked ? 1.8 : 1; ctx.setLineDash(locked ? [] : [3, 5]);
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y, 22 - 8 * close, 9 - 3 * close, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
|
if (locked && !opts.compact) { ctx.fillStyle = T.ash; ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText(Math.round((c.fraction_total || 0) * 100) + '%', p.x, p.y - 16); }
|
|
});
|
|
// blocks: small isometric tiles, lit by word
|
|
ordered.forEach(function (e) {
|
|
var p = pos[e.hash]; if (p.x < -20 || p.x > W + 20) 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 s = e.src.chain ? 6 : 4.5, a = w === 'excluded' ? 0.45 : 1 - p.z * 0.35;
|
|
if (age < 1.4) { var gl = 1 - age / 1.4; var rg = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 20); rg.addColorStop(0, F.rgba(T.ember, 0.45 * gl)); rg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(p.x, p.y, 20, 0, Math.PI * 2); ctx.fill(); }
|
|
ctx.globalAlpha = a; ctx.fillStyle = w === 'pending' ? T.row : col; ctx.strokeStyle = col; ctx.lineWidth = 1.2;
|
|
ctx.beginPath(); ctx.moveTo(p.x, p.y - s); ctx.lineTo(p.x + s * 1.3, p.y); ctx.lineTo(p.x, p.y + s); ctx.lineTo(p.x - s * 1.3, p.y); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
ctx.globalAlpha = 1;
|
|
});
|
|
}
|
|
var L = F.loop(draw, canvas); L.kick();
|
|
return { push: push, redraw: L.kick };
|
|
}
|
|
root.IgneumLattice = { mount: mount };
|
|
})(window);
|