igneum/site/live-steps.js
igneum-josh 62bfaea269 Home page redrawn: the scene is the page; a 56 px header, one statement, two buttons; three facts, the downloads, one line to the ledger
The owner, 6 October 2026, on the live home page: "too left and text heavy ... we want the home page to suck people in and let
the litepaper carry the weight; also minimise the header." The home page only; nothing else above the fold.

Header (this page only, a page-block override of the shared nav): the wordmark, Litepaper, Live devnet and Download the miner,
56 px, transparent over the hero; the phone sheet shows the same three.
Hero: one statement, eleven words ("A proof-of-work chain for graphics cards, whose miners prove the blocks."), two buttons,
then the step scene (site/live-steps.js on the shared feed) full bleed with its one caption line. Hero copy 17 words with the
buttons. At 1440 by 900 the hero ends at 849 px; at 390 by 844 at 699 px.
Below the fold: three facts, each one number and one sentence: the live hash rate with the vote keys of the last ten
minutes, the shards proven and paid on the chain (with the last ten minutes), and the chip model's one line with its link;
a note under them says the chain's state in the ledger's words (tonight: finality paused) and that the chip figures are a
cost model, not a measurement. Then the downloads row (three platforms) with the devnet and testnet notices and the dev-fee
sentence, then one line to the ledger and to what Igneum does not claim. The footer is unchanged.
Moved, nothing cut: the light-client card to /live (its verifier module with it); the testnet terms to the litepaper's For
miners section (with an id for the metamask page's link); "Live rows arrive with the public testnet, August 2027" to the
litepaper's proving section; "since 2019 (approximate)" onto the litepaper's RandomX date; the journey, economics, wallet,
build and RandomX sections were already in the litepaper (roadmap, economics, wallet, building, vs RandomX). The footer's
Journey link points at the litepaper's roadmap. tools/ci/ledger-text-check.mjs: the home page is checked for E5, X2 and
X7; G4, C2, X3 and X8 are checked on the litepaper (G4 and X8 were already there). The ledger's own "stated on" notes for
those four rows are owed an update by the ledger owner.
Polish lane Q5: the word "final" is drawn and captioned only while finality is active (site/live-steps.js), so nothing on
the page says final while finality is paused; the hero is under 40 words; the Open Graph card is the 1200 by 630 image.

Measured headless at 1440 and 390, dark and light: header 56 px with three items, no "final" anywhere, no horizontal
overflow, no console errors; the caption advanced on every capture. Captures: docs/plans/site-ui-3-shots/after/
home-{1440,390}-{dark,light}.jpg (full page) and home-{1440,390}-{dark,light}-fold.jpg (above the fold).
Checks: link check 912 links across 16 pages 0 broken, ledger text 50 of 50, contrast 32 pairs 0 under 4.5:1, orphan check
0 site headings, api tests 5 pass. The identity grep's one hit is docs/analysis/horizon/polish.md line 433 on master,
untouched here (the polish lane's own table of regex literals); it is main's to route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 21:30:09 +01:00

116 lines
12 KiB
JavaScript

/* Igneum live steps (6 October 2026, the owner: "I liked it when we could see each step of the algo working").
The original step view with real blocks: a lane chart where every square is a block the observer stored, released at a
calm pace (the feed is sampled, the counters elsewhere read the chain), and every square shows its step: mined (an outline,
grey while pending, ember once included), shards being proven (molten quarter cells fill as shards are verified or paid),
proven (ember fill), locked checkpoint (a ring; a dashed "final" line marks that everything to its left is final).
Each transition is animated as it happens (a glow and a ring pulse) and captioned in one line for the newest block that
changed. The wheel is never touched: the page scrolls through the scene. Light and dark from the CSS tokens; a still frame
under reduced motion. Needs /scenes/feed.js (IgneumFeed) on the page.
Usage: var steps = IgneumSteps.mount(canvas, { caption: el, window: 300 }); then IgneumFeed.start({ subs: [steps], window: 300 }). */
(function (root) {
'use strict';
var F = root.IgneumFeed;
function pad(n) { return String(n).padStart(2, '0'); }
function hms(ms) { var d = new Date(ms); return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); }
function fmt(n) { return n === null || n === undefined ? null : Number(n).toLocaleString('en-GB'); }
function stepOf(b) {
var sh = b.shards || [], done = function (x) { return x.state === 'verified' || x.state === 'paid'; };
if (b.proven || (sh.length && sh.every(done))) return 'proven';
if (sh.some(function (x) { return x.state === 'proving' || done(x); })) return 'proving';
return 'mined';
}
function mount(canvas, opts) {
opts = opts || {};
var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme();
var lanes = 5, S, GAP, speed, blocks = [], byHash = {}, queue = [], laneOf = {}, laneN = 0, last = 0, prevT = 0;
// one block released every 2.5 s and a crossing of about 34 s at 1440: the chain merges a pending block in about 10 s, so a
// square is still on screen when its step changes (the original pace of 10 s lost most transitions off the left edge)
var RELEASE_MS = 2500;
var caption = opts.caption || null, lastChange = 0, latest = null, finality = null, proving = null, serverNow = 0;
function metrics() { S = dims.W < 600 ? 18 : 26; GAP = dims.W < 600 ? S * 2.5 : S * 3.1; speed = GAP / RELEASE_MS; }
metrics();
root.addEventListener('resize', function () { dims = size(); metrics(); L.kick(); });
function laneY(l) { return dims.H * 0.14 + l * (dims.H * 0.72 / (lanes - 1)); }
function laneFor(m) { if (!(m in laneOf)) laneOf[m] = laneN++ % lanes; return laneOf[m]; }
function cpIndexOf(hash) { var cps = (finality && finality.checkpoints) || []; for (var i = cps.length - 1; i >= 0; i--) if (cps[i].hash === hash) return cps[i].index; return null; }
// the record of a block for the caption: its words with their times
function describe(e) {
var b = e.src, parts = [], name = b.number !== null && b.number !== undefined ? 'block ' + fmt(b.number) : 'block ' + b.hash.slice(0, 8);
parts.push('mined ' + hms(b.ts) + (b.color === 'red' ? ', excluded' : b.chain ? ', on the selected chain' : b.color === 'blue' ? ', included' : ', pending'));
var sh = b.shards || [];
if (sh.length) {
var proving = sh.filter(function (x) { return x.state === 'proving'; }), done = sh.filter(function (x) { return x.state === 'verified' || x.state === 'paid'; });
var provers = {}; sh.forEach(function (x) { if (x.prover) provers[x.prover] = 1; }); var np = Object.keys(provers).length;
if (e.step === 'proven') { var lag = Math.max.apply(null, sh.map(function (x) { return x.lag || 0; })); parts.push(sh.length + (sh.length === 1 ? ' shard' : ' shards') + (np ? ' on ' + np + (np === 1 ? ' card' : ' cards') : '')); parts.push('proven in ' + (lag > 0 ? lag : Math.max(1, Math.round((Date.now() - b.ts) / 1000))) + ' s'); }
else if (proving.length || done.length) parts.push((done.length ? done.length + ' of ' + sh.length + ' shards proven' : sh.length + (sh.length === 1 ? ' shard' : ' shards') + ' proving') + (np ? ' on ' + np + (np === 1 ? ' card' : ' cards') : ''));
else parts.push(sh.length + (sh.length === 1 ? ' shard planned' : ' shards planned'));
}
if (b.locked) { var idx = cpIndexOf(b.hash); parts.push('locked at checkpoint' + (idx !== null ? ' ' + fmt(idx) : '')); }
else if (b.final && finality && finality.active) parts.push('final under the newest lock');
return name + ': ' + parts.join('; ');
}
function setCaption(text, muted) { if (!caption) return; caption.textContent = text; caption.classList.toggle('muted', !!muted); }
function apply(e, src, now, announce) {
var before = e.step + '|' + e.src.color + '|' + e.src.locked + '|' + e.src.final + '|' + (e.src.chain ? 1 : 0);
e.src = src; e.step = stepOf(src);
var after = e.step + '|' + src.color + '|' + src.locked + '|' + src.final + '|' + (src.chain ? 1 : 0);
if (announce && after !== before) { e.glow = 1; if (src.locked && !e.ringed) { e.ringed = true; e.ripple = 1; } lastChange = now; latest = e; setCaption(describe(e)); if (opts.onChange) opts.onChange(e, before, after); }
}
var seeded = false;
function push(d) {
var now = performance.now(); finality = d.finality || null; proving = d.proving || null; serverNow = new Date(d.now).getTime();
if (!seeded) {
// the first reply fills the scene: the newest blocks placed across the width, oldest at the left, so nothing starts empty
seeded = true; var n = Math.ceil(dims.W / GAP) + 1, recent = d.blocks.slice(-n);
recent.forEach(function (b, i) { var e = { hash: b.hash, src: b, step: stepOf(b), x: dims.W - S - (recent.length - 1 - i) * GAP, y: laneY(laneFor(b.miner || '?')), glow: 0, ripple: 0, ringed: !!b.locked, born: now }; byHash[b.hash] = e; blocks.push(e); });
}
d.blocks.forEach(function (b) {
var e = byHash[b.hash];
if (e) { apply(e, b, now, true); return; }
e = { hash: b.hash, src: b, step: stepOf(b), x: null, y: null, glow: 0, ripple: 0, ringed: !!b.locked, born: 0 };
byHash[b.hash] = e; queue.push(e); if (queue.length > 6) { var dropped = queue.shift(); delete byHash[dropped.hash]; }
});
var seen = {}; d.blocks.forEach(function (b) { seen[b.hash] = 1; });
// a block the feed no longer carries keeps its last words on screen until it leaves at the left
if (!latest || now - lastChange > 60000) {
var why = finality && finality.supported && !finality.active ? 'finality paused: under two thirds of the weight is signing, so no checkpoint locks' : proving && proving.supported && proving.active && !(proving.shards_paid_10m > 0) ? 'no proof landed in the last 10 minutes; proofs land about ' + (proving.median_proof_lag_s ? Math.round(proving.median_proof_lag_s) : 380) + ' s behind the tip' : 'no block changed step in the last minute';
setCaption('Blocks arrive every second. ' + why.charAt(0).toUpperCase() + why.slice(1) + '.', true);
}
L.kick();
}
function rr(x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
function spawn(e, t) { e.x = dims.W + S; e.y = laneY(laneFor(e.src.miner || '?')); e.born = t; blocks.push(e); if (blocks.length > 80) { var old = blocks.shift(); if (!queue.some(function (q) { return q === old; })) delete byHash[old.hash]; } }
function draw(t) {
var W = dims.W, H = dims.H; T = F.theme();
var dt = prevT ? Math.min(50, t - prevT) : 16; prevT = t;
if (queue.length && t - last > RELEASE_MS) { spawn(queue.pop(), t); queue.length = 0; last = t; } // the newest waiting block; the rest are sampled out
blocks.forEach(function (b) { b.x -= speed * dt; if (b.glow > 0) b.glow -= 0.02; if (b.ripple > 0) b.ripple -= 0.012; });
blocks = blocks.filter(function (b) { if (b.x < -S * 2) { if (byHash[b.hash] === b && queue.indexOf(b) < 0) delete byHash[b.hash]; return false; } return true; });
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1; for (var l = 0; l < lanes; l++) { ctx.beginPath(); ctx.moveTo(0, laneY(l)); ctx.lineTo(W, laneY(l)); ctx.stroke(); }
// parent edges between blocks on screen
blocks.forEach(function (b) { (b.src.parents || []).forEach(function (h) { var q = byHash[h]; if (!q || q.x === null || blocks.indexOf(q) < 0) return; var both = b.step === 'proven' && q.step === 'proven'; ctx.strokeStyle = both ? F.rgba(T.ember, 0.45) : F.rgba(T.ash, 0.22); ctx.lineWidth = both ? 1.5 : 1; ctx.beginPath(); ctx.moveTo(b.x, b.y); var mx = (b.x + q.x) / 2; ctx.bezierCurveTo(mx, b.y, mx, q.y, q.x, q.y); ctx.stroke(); }); });
// the lock line: everything left of the newest locked block is final
// the lock line and the word final are drawn only while finality is active (the owner's rule: no "final" anywhere while finality is paused)
var lk = null; if (!finality || finality.active) blocks.forEach(function (b) { if (b.src.locked && (!lk || b.x > lk.x)) lk = b; });
if (lk) { ctx.strokeStyle = F.rgba(T.ember, 0.55); ctx.lineWidth = 1.5; ctx.setLineDash([5, 6]); ctx.beginPath(); ctx.moveTo(lk.x, 6); ctx.lineTo(lk.x, H - 6); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = T.ash; ctx.font = '500 ' + Math.max(10, Math.round(S * 0.42)) + 'px IBM Plex Mono, monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'alphabetic'; ctx.fillText('final', lk.x - 8, 14); }
blocks.forEach(function (b) {
var h = S / 2, w = b.step, src = b.src, ex = src.color === 'red';
if (b.glow > 0) { var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, S * 1.6); g.addColorStop(0, F.rgba(T.ember, 0.45 * b.glow)); g.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, S * 1.6, 0, Math.PI * 2); ctx.fill(); }
if (ex) ctx.globalAlpha = 0.45;
if (w === 'proven') { ctx.fillStyle = T.ember; rr(b.x - h, b.y - h, S, S, S * 0.24); ctx.fill(); }
else {
ctx.fillStyle = T.row; rr(b.x - h, b.y - h, S, S, S * 0.24); ctx.fill();
ctx.strokeStyle = w === 'proving' ? F.rgba(T.molten, 0.9) : (src.chain || src.color === 'blue') ? F.rgba(T.ember, 0.8) : F.rgba(T.ash, 0.7); ctx.lineWidth = 2; rr(b.x - h, b.y - h, S, S, S * 0.24); ctx.stroke();
if (w === 'proving') { var sh = src.shards || [], n = Math.min(4, sh.length), q = S / 2 - 3; for (var i = 0; i < n; i++) { var st = sh[i].state; if (st === 'verified' || st === 'paid' || st === 'proving') { ctx.fillStyle = st === 'proving' ? F.rgba(T.molten, 0.45) : T.molten; var qx = b.x - h + 3 + (i % 2) * q, qy = b.y - h + 3 + Math.floor(i / 2) * q; rr(qx, qy, q - 1, q - 1, 2); ctx.fill(); } } }
}
if (src.locked) { var pulse = 0.8 + 0.2 * Math.sin(t / 350); ctx.strokeStyle = F.rgba(T.ember, pulse); ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(b.x, b.y, S * 1.15, 0, Math.PI * 2); ctx.stroke(); ctx.fillStyle = T.bone; ctx.beginPath(); ctx.arc(b.x, b.y, S * 0.16, 0, Math.PI * 2); ctx.fill(); }
if (b.ripple > 0) { ctx.strokeStyle = F.rgba(T.ember, b.ripple); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.x, b.y, S * (1.3 + (1 - b.ripple) * 2), 0, Math.PI * 2); ctx.stroke(); }
ctx.globalAlpha = 1;
});
}
var L = F.loop(draw, canvas); L.kick();
return { push: push, redraw: L.kick };
}
root.IgneumSteps = { mount: mount };
})(window);