/* Igneum live steps (6 October 2026). The owner: "I liked it when we could see each step of the algo working", and later, "can we have a dag animation like we had before? with the shards making up the block, then the final line and the other bits that looked really cool but brought up to date?" The lane chart with the original bits, every one driven by the live feed: a real block arrives from the right with a glow and its chain number under it; its outline is grey while pending and ember once included; as its shards are verified or paid, particles fly in from the edges and its quarter cells fill molten; when every shard is proven the block turns ember and its provers are paid (the caption says to how many); a locked checkpoint takes a ring and a ripple and the dashed "final" line sweeps in from the right to it, drawn only while finality is active. A proof or a lock that lands on a block which has already left the screen re-enters from the right as its own event, so every step is seen when it happens. The stream is sampled (one block every 2.5 s, a crossing of about 34 s); the counters elsewhere read the chain. No wheel handler: 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 }); 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 isDone(x) { return x.state === 'verified' || x.state === 'paid'; } function stepOf(b) { var sh = b.shards || []; if (b.proven || (sh.length && sh.every(isDone))) return 'proven'; if (sh.some(function (x) { return x.state === 'proving' || isDone(x); })) return 'proving'; return 'mined'; } function shardKey(b) { return (b.shards || []).map(function (x) { return x.state[0]; }).join(''); } 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, RELEASE_MS = 2500, blocks = [], byHash = {}, known = {}, queue = [], events = [], sparks = [], ripples = [], laneOf = {}, laneN = 0, last = 0, prevT = 0; var caption = opts.caption || null, lastChange = 0, latest = null, finality = null, proving = null, seeded = false, lockX = null, lockTarget = null, lockHash = null; 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.70 / (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; } function provers(b) { var set = {}; (b.shards || []).forEach(function (x) { if (x.prover) set[x.prover] = 1; }); return Object.keys(set).length; } 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 done = sh.filter(isDone), np = provers(b), paid = sh.filter(function (x) { return x.state === 'paid'; }).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') + ' proven in ' + (lag > 0 ? lag : Math.max(1, Math.round((Date.now() - b.ts) / 1000))) + ' s'); if (paid) parts.push(np ? np + (np === 1 ? ' prover paid' : ' provers paid') : 'provers paid'); } else if (done.length) parts.push(done.length + ' of ' + sh.length + ' shards proven' + (np ? ' on ' + np + (np === 1 ? ' card' : ' cards') : '')); else if (sh.some(function (x) { return x.state === 'proving'; })) parts.push(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 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 cellOf(b, i) { var h = S / 2, q = S / 2 - 3; return { x: b.x - h + 3 + (i % 2) * q + (q - 1) / 2, y: b.y - h + 3 + Math.floor(i / 2) * q + (q - 1) / 2 }; } // a particle per newly proven shard, from an edge to the block's quarter cell (the original scene's sparks, now real) function sparkFor(e, i) { var fromTop = Math.random() < 0.5; sparks.push({ x: Math.random() * dims.W, y: fromTop ? -6 : dims.H + 6, b: e, i: i, p: 0 }); } function announce(e, now, reason) { e.glow = 1; lastChange = now; latest = e; setCaption(describe(e)); if (opts.onChange) opts.onChange(e, reason); } function apply(e, src, now) { var before = e.step + '|' + e.src.color + '|' + e.src.locked + '|' + (e.src.chain ? 1 : 0) + '|' + shardKey(e.src); var oldSh = e.src.shards || []; e.src = src; e.step = stepOf(src); var after = e.step + '|' + src.color + '|' + src.locked + '|' + (src.chain ? 1 : 0) + '|' + shardKey(src); if (after === before) return false; (src.shards || []).forEach(function (x, i) { if (i < 4 && isDone(x) && !(oldSh[i] && isDone(oldSh[i])) && e.x !== null) sparkFor(e, i); }); if (src.locked && !e.ringed) { e.ringed = true; if (e.x !== null) { ripples.push({ x: e.x, y: e.y, r: S, a: 1 }); ripples.push({ x: e.x, y: e.y, r: S * 0.5, a: 1 }); } } announce(e, now, after.split('|')[0]); return true; } function push(d) { var now = performance.now(); finality = d.finality || null; proving = d.proving || null; if (finality && !finality.active) { lockTarget = null; lockX = null; lockHash = null; } if (!seeded) { 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, ringed: !!b.locked, born: now }; byHash[b.hash] = e; known[b.hash] = e; blocks.push(e); }); } d.blocks.forEach(function (b) { var e = known[b.hash]; if (e) { var changed = apply(e, b, now); if (changed && e.x === null && blocks.indexOf(e) < 0 && (e.step === 'proven' || b.locked)) { e.event = true; queue.push(e); } return; } e = { hash: b.hash, src: b, step: stepOf(b), x: null, y: null, glow: 0, ringed: !!b.locked, born: 0 }; known[b.hash] = e; queue.push(e); }); // forget blocks that have left both the feed and the screen var inFeed = {}; d.blocks.forEach(function (b) { inFeed[b.hash] = 1; }); Object.keys(known).forEach(function (h) { var e = known[h]; if (!inFeed[h] && blocks.indexOf(e) < 0 && queue.indexOf(e) < 0) delete known[h]; }); if (queue.length > 12) queue = queue.filter(function (e) { return e.event; }).concat(queue.filter(function (e) { return !e.event; }).slice(-6)); 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 spawn(e, t) { e.x = dims.W + S; e.y = laneY(laneFor(e.src.miner || '?')); e.born = t; e.glow = 1; blocks.push(e); byHash[e.hash] = e; if (e.event) { (e.src.shards || []).forEach(function (x, i) { if (i < 4 && isDone(x)) sparkFor(e, i); }); if (e.src.locked) { ripples.push({ x: e.x, y: e.y, r: S, a: 1 }); } } if (blocks.length > 80) { var old = blocks.shift(); 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) { var ev = queue.findIndex(function (e) { return e.event; }); spawn(ev >= 0 ? queue.splice(ev, 1)[0] : queue.pop(), t); if (ev < 0) queue = queue.filter(function (e) { return e.event; }); last = t; } blocks.forEach(function (b) { b.x -= speed * dt; if (b.glow > 0) b.glow -= 0.015; }); blocks = blocks.filter(function (b) { if (b.x < -S * 2) { b.x = null; delete byHash[b.hash]; return false; } return true; }); sparks.forEach(function (sp) { sp.p += 0.035; }); sparks = sparks.filter(function (sp) { return sp.p < 1 && blocks.indexOf(sp.b) >= 0; }); ripples.forEach(function (r) { r.r += 2.2; r.a -= 0.012; r.x -= speed * dt; }); ripples = ripples.filter(function (r) { return r.a > 0; }); 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(); } blocks.forEach(function (b) { (b.src.parents || []).forEach(function (h) { var q = byHash[h]; if (!q || q.x === null) 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 final line: drawn only while finality is active; it sweeps in from the right to the newest locked block on screen if (finality && finality.active) { var lk = null; blocks.forEach(function (b) { if (b.src.locked && (!lk || b.x > lk.x)) lk = b; }); if (lk) { if (lockHash !== lk.hash) { lockHash = lk.hash; if (lockX === null) lockX = W + 20; } lockTarget = lk.x; lockX += (lockTarget - lockX) * 0.08; ctx.strokeStyle = F.rgba(T.ember, 0.55); ctx.lineWidth = 1.5; ctx.setLineDash([5, 6]); ctx.beginPath(); ctx.moveTo(lockX, 6); ctx.lineTo(lockX, 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', lockX - 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(); 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(); } // the chain number under the block (the short hash off the chain), at the wide size only if (S >= 26) { ctx.fillStyle = F.rgba(T.ash, ex ? 0.6 : 0.9); ctx.font = '400 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(src.number !== null && src.number !== undefined ? fmt(src.number) : src.hash.slice(0, 6), b.x, b.y + h + 5); } ctx.globalAlpha = 1; }); sparks.forEach(function (sp) { var c = cellOf(sp.b, sp.i), e = 1 - Math.pow(1 - sp.p, 3), sx = sp.x + (c.x - sp.x) * e, sy = sp.y + (c.y - sp.y) * e; ctx.strokeStyle = F.rgba(T.molten, 0.5 * (1 - sp.p)); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(sp.x + (c.x - sp.x) * Math.max(0, e - 0.15), sp.y + (c.y - sp.y) * Math.max(0, e - 0.15)); ctx.lineTo(sx, sy); ctx.stroke(); ctx.fillStyle = T.molten; ctx.beginPath(); ctx.arc(sx, sy, 2.2, 0, Math.PI * 2); ctx.fill(); }); ripples.forEach(function (r) { ctx.strokeStyle = F.rgba(T.ember, r.a); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(r.x, r.y, r.r, 0, Math.PI * 2); ctx.stroke(); }); } var L = F.loop(draw, canvas); L.kick(); return { push: push, redraw: L.kick }; } root.IgneumSteps = { mount: mount }; })(window);