igneum/site/scenes/feed.js
igneum-labs bbb22eb986 Home scene: the step view returns, with real blocks, animated transitions and a one-line caption
The owner, 6 October 2026: "I liked it when we could see each step of the algo working." The home page's scene is again the
lane chart where a square moves through mined, shards being proven, proven and locked checkpoint, now as site/live-steps.js on
the shared feed (site/scenes/feed.js, which gains onData):
- every square is a real block from /api/live (a 300 s window so late proofs still reach the caption), released one every
  2.5 s so a square crosses the scene in about 34 s; the chain merges a pending block in about 10 s, so its step changes
  while it is still on screen (at the original 10 s crossing most transitions landed after the square had left). The feed is
  sampled for the stream; item 0's counters read the chain. The first reply seeds the scene across its width, so nothing
  starts empty;
- each step is drawn as before: an outline (grey while pending, ember once included), molten quarter cells filling as shards
  are verified or paid, an ember fill when proven, a ring on a locked checkpoint with the dashed "final" line to its left,
  excluded blocks dimmed; every transition glows for a second and a lock ripples;
- one caption line under the scene for the newest block that changed step, e.g. "block 144,564: mined 21:16:03, on the
  selected chain; 1 shard planned", or with its shards "8 shards on 3 cards; proven in 48 s" and "locked at checkpoint 7,084".
  /api/live now carries each block's chain number for it. When no block changes step for a minute the caption says why in
  the ledger's words: finality paused (under two thirds of the weight signing), no proof landed in the last 10 minutes
  (with the median lag), or no block changed step;
- the wheel is never touched: the module has no wheel handler, so the page scrolls through the scene;
- light and dark from the tokens, 390 px, a still frame under reduced motion. The DAG module stays as /live's scene and the
  three scenes stay unlinked on /scenes.

Proved headless over 40 s against the live feed: thirteen captioned transitions, no console errors at 1440 or 390, no
overflow. Captures: docs/plans/site-ui-3-shots/after/index-steps-1440-dark-clip.jpg, index-steps-390-dark-clip.jpg and
index-steps-1440-dark.webm (ten seconds of blocks moving through the steps). Checks: identity grep 0 hits, link check 931
links 0 broken, ledger text 43 of 43, contrast 32 pairs 0 under 4.5:1, api tests pass.

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

40 lines
4 KiB
JavaScript

/* One poll of /api/live every 2 s, pushed to every scene on the page (docs/plans/site-ui-3.md, the scenes lab, 6 Oct 2026).
IgneumFeed.start({ url, window, onState, subs: [scene, ...] }); each scene has push(reply). The state words are the site's:
connecting, live, stale (observer offline, last update N ago), failed (live feed unavailable). */
(function (root) {
'use strict';
function fmtAge(sec) { sec = Math.round(sec); return sec < 90 ? sec + ' s' : sec < 5400 ? Math.round(sec / 60) + ' min' : Math.round(sec / 3600) + ' h'; }
function start(opts) {
var subs = opts.subs || [], failures = 0, state = 'connecting', reason = 'connecting';
function set(s, r) { if (s === state && r === reason) return; state = s; reason = r; if (opts.onState) opts.onState(s, r); }
function poll() {
if (document.hidden) return;
fetch((opts.url || '/api/live') + '?window=' + (opts.window || 120), { cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function (d) {
if (!d || !d.ok || !d.state || !d.blocks) throw new Error('bad reply');
failures = 0;
if (d.state.stale) set('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); else set('live', null);
subs.forEach(function (s) { s.push(d); }); if (opts.onData) opts.onData(d);
}).catch(function () { failures++; if (failures >= 2) set('failed', 'live feed unavailable'); });
}
poll(); var t = setInterval(poll, 2000);
document.addEventListener('visibilitychange', function () { if (!document.hidden) poll(); });
return { stop: function () { clearInterval(t); }, state: function () { return { state: state, reason: reason }; } };
}
// shared helpers for the three scenes
function cssVar(name, fallback) { try { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; } }
function theme() { return { ember: cssVar('--ember', '#F2541B'), molten: cssVar('--molten', '#FFB35C'), bone: cssVar('--bone', '#F4F1EC'), ash: cssVar('--ash', '#9A9A9E'), line: cssVar('--line', '#2A2A30'), row: cssVar('--row', '#111114'), ink2: cssVar('--ink-2', '#C9C7C2') }; }
function rgba(hex, a) { var h = hex.replace('#', ''); if (h.length !== 6) return hex; return 'rgba(' + parseInt(h.slice(0, 2), 16) + ',' + parseInt(h.slice(2, 4), 16) + ',' + parseInt(h.slice(4, 6), 16) + ',' + a + ')'; }
// the ledger's word for a block
function word(b) { return b.locked ? 'locked' : b.proven ? 'proven' : (b.chain || b.color === 'blue') ? 'included' : b.color === 'red' ? 'excluded' : 'pending'; }
function hash32(s) { var h = 2166136261; for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0) / 4294967296; }
function sizer(canvas, ctx) { var dpr = Math.min(root.devicePixelRatio || 1, 2); return function () { var W = canvas.clientWidth, H = canvas.clientHeight; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return { W: W, H: H }; }; }
function loop(draw, canvas) {
var reduce = root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches, running = false, inView = true;
function frame(t) { if (!running) return; draw(t); if (reduce || document.hidden || !inView) { running = false; return; } requestAnimationFrame(frame); }
function kick() { if (!running) { running = true; requestAnimationFrame(frame); } }
if ('IntersectionObserver' in root) new IntersectionObserver(function (es) { es.forEach(function (e) { inView = e.isIntersecting; if (inView) kick(); }); }, { threshold: 0.05 }).observe(canvas);
document.addEventListener('visibilitychange', function () { if (!document.hidden) kick(); });
return { kick: kick, reduce: reduce };
}
root.IgneumFeed = { start: start, theme: theme, rgba: rgba, word: word, hash32: hash32, sizer: sizer, loop: loop };
})(window);