The owner, 6 October 2026: "Im not sure about the site can we revert it but polish it? minimal, simple but everything needed
for the dopamine and emotional triggers of the gpu miner", then "cant 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 upto date?"
Restored (a new commit, no history rewrite): site/index.html as it stood at 3322835, the step-view page with its hero, facts,
scene, three things to check, downloads, build, wallet, journey, economics and the ledger band; nothing from tonight's other
work is undone (the litepaper, /live, the roadmap and benchmark wording, ledger X31 to X33, the scroll-zoom fix). Where the
page named a month it now carries tonight's sentences: the proofs card reads "Live rows arrive with the public testnet. The
public testnet is weeks away: three seed nodes and the public RPC are up, and it opens when the go checklist closes."; the
journey lead reads "Six phases, four public gates ... No calendar dates: each phase closes at its gate." and its inlined
phases are the gate-worded ones. No "August 2027", no month anywhere on the page.
The scene (site/live-steps.js): 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 the caption says how many provers were paid; 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 (the legend no longer says "final to its
left"; the wallet card's state word "final, checkpoint 5" is the wallet's own vocabulary and stays). A proof or a lock that
lands on a block already off screen re-enters from the right as its own event, so every step is seen when it happens. The
counters gain blocks per second. The caption follows the newest block that changed step.
The miner's triggers, each one element with live or measured numbers, none invented: a network strip under the facts
(hash rate, blocks in the last ten minutes, the latest block's age ticking every second with its chain number and word,
shards proven and paid with the last ten minutes), each value flashing molten when it changes; a card picker from the bench
table with the time a card alone takes to find a block at the live hash rate (RTX 5090 127.7 MH/s at 227 W and RTX 4070
28.8 MH/s at 76 W from the 6 October Ember Tune on the three-card Windows rig, RX 9070 XT 17.8 MH/s from the 5 October eGPU
entry, Apple M5 Max 26.7 MH/s from the first live swap; the RTX 4090 and RX 7900 XTX shown as not yet measured with the
bench table linked) with the pool note; the fairness line (graphics cards only, a new program every hour, your card proves
the blocks, 80% to the miner and 20% to the provers, nothing to anyone else); the download buttons with sizes; a Discord join
line (the standing invite, recorded in docs/community/discord-hooks.md so nobody asks again); the testnet sentence.
docs/fud-ledger.md: G4, C2 and X8 each gain a status line saying the restored home page carries their sentence again; the
ledger-text check guards them on the home page again (55 sentences, 0 missing).
Measured headless over 40 s against the live feed: 19 captioned transitions, the strip reading 2.3 GH/s, 562 blocks in ten
minutes, 8 s ago, 2,970 shards paid, the picker's times 18 s (5090), 79 s (4070), 2 min (9070 XT), 86 s (M5 Max) at that
hash rate, 0.92 blocks/s; no console errors at 1440 or 390 in either theme; no horizontal overflow. Captures in
docs/plans/site-ui-3-shots/after-v2/: home-{1440,390}-{dark,light}-fold.jpg, home-{1440,390}-{dark,light}.jpg and
home-steps-1440-dark.webm (ten seconds of the scene). Checks: identity grep 0 hits on served files, link check 934 links
across 16 pages 0 broken, ledger text 55 of 55, contrast 32 pairs 0 under 4.5:1, api tests 5 pass. Not deployed: the owner
sees it first.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
124 lines
14 KiB
JavaScript
124 lines
14 KiB
JavaScript
/* 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);
|