igneum/site/hero-scene.js
igneum-labs 045102b81a site-redesign: the full-site redesign on our content, 22 pages served
The package's design system, layouts and interactions on our tokens and self-hosted fonts (site/site.css section 10),
our facts, numbers, records and live services on every page. The home page: the GPU hero (site/hero-scene.js, continuous
while in view, labelled as drawn), the live DAG panel on scenes/feed.js and live-dag.js 2.0.2, three reasons, the
economics split, the download row with the OS marks, the ledger line. The miner page: the download layout, platform
tabs stamped from the live download index with sha256, the steps, the card table, the fee section (the one source of
/dev-fee), the FAQ. The wallet page in the package's wallet-hero layout, the window slot ready for the wallet redesign.
The secondary pages rendered by the build from one source each: /claims and /randomx from the litepaper's sections,
/dev-fee from the miner's fee section, /provenance from docs/provenance.md through the scrub, /journey from
journey.json; /benchmarks rewrites to /miners, /connect redirects to /metamask. The generated pages (bench, bench
table, ledger) restyled onto the page hero and the docs layout with a section filter. The footer's secondary links point
at the real pages. The copy pass on every served page (docs/plans/site-copy-pass.md). The package's data tests and the
hero loop test in the gate (tools/site-redesign). No sample data is served; the package's review chrome is not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 10:30:18 +00:00

136 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* The home hero's GPU illustration (7 Oct 2026, from the redesign package's HardwareScene, rebuilt to run continuously).
* An illustration, never telemetry: nothing here reads the chain. The loop never idles while the hero is in view: the
* fans turn, the pulses travel the traces, the block breathes and drifts, the shards orbit, the lock cage shimmers.
* It stops only under reduced motion, a hidden tab or the hero scrolled out of view, and resumes where it left off
* (the clock advances by frame delta, never by wall time, so there is no jump on return). The still parts (the grid,
* the board, the traces, the card body) are painted once into two cached layers and redrawn per frame with drawImage;
* only the moving parts are drawn each frame, so an idle hero costs a few percent of a core at 60 fps.
* IgneumHero.mount(canvas, { steps, status, detail }) returns { setStage, destroy }. */
(function (root) {
'use strict';
var TAU = Math.PI * 2;
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
var LABELS = [['01 / Mining', 'The card runs this hour’s program. A block comes out.'], ['02 / Proving', 'The same card proves the block in shards.'], ['03 / Locking', 'Two thirds of the weight sign. The checkpoint locks.']];
var CONNECTIONS = [[-205,-93,-173,-93,-173,-68],[-210,77,-174,77,-174,49],[80,91,80,115,190,115],[184,-74,214,-74,214,-28],[-105,97,-105,125,-157,125],[144,71,201,71,201,107]];
function mount(canvas, opts) {
opts = opts || {};
var ctx = canvas.getContext('2d'); if (!ctx) throw new Error('A 2D canvas context is required.');
var w = 0, h = 0, dpr = 1, raf = 0, disposed = false, inView = true, last = 0, clock = 0;
var stage = 0, prevStage = 0, stageAt = 0, manualAt = -1e9, AUTO = 7000, HOLD = 25000;
var reduced = root.matchMedia ? root.matchMedia('(prefers-reduced-motion: reduce)') : { matches: false, addEventListener: function () {} };
var base = null, card = null, cardKey = '', pal = null;
function theme() {
var cs = getComputedStyle(document.documentElement), light = document.documentElement.getAttribute('data-theme') === 'light' || (!document.documentElement.getAttribute('data-theme') && root.matchMedia && root.matchMedia('(prefers-color-scheme: light)').matches);
pal = { light: light, ember: cs.getPropertyValue('--ember').trim() || '#F2541B', gold: cs.getPropertyValue('--molten').trim() || '#FFB35C', bone: cs.getPropertyValue('--bone').trim() || '#F4F1EC', ash: cs.getPropertyValue('--ash').trim() || '#9A9A9E', line: cs.getPropertyValue('--line').trim() || '#2A2A30' };
base = null; card = null; cardKey = '';
}
function geom() { var s = Math.min(w / 590, h / 490), cx = w * .51, cy = h * .52; return { s: s, cx: cx, cy: cy, p: function (x, y, z) { z = z || 0; return { x: cx + (x * .91 - y * .86) * s, y: cy + (x * .42 + y * .43 - z) * s }; } }; }
function layer(fn) { var c = document.createElement('canvas'); c.width = Math.max(1, Math.round(w * dpr)); c.height = Math.max(1, Math.round(h * dpr)); var k = c.getContext('2d'); k.setTransform(dpr, 0, 0, dpr, 0, 0); fn(k, geom()); return c; }
function poly(c, ps, fill, stroke, lw) { c.beginPath(); ps.forEach(function (a, i) { i ? c.lineTo(a.x, a.y) : c.moveTo(a.x, a.y); }); c.closePath(); if (fill) { c.fillStyle = fill; c.fill(); } if (stroke) { c.strokeStyle = stroke; c.lineWidth = lw || 1; c.stroke(); } }
function stroke(c, ps, col, lw) { c.beginPath(); ps.forEach(function (a, i) { i ? c.lineTo(a.x, a.y) : c.moveTo(a.x, a.y); }); c.strokeStyle = col; c.lineWidth = lw || 1; c.stroke(); }
function box(c, g, x, y, z, ww, hh, dep, top, edge) {
var p = g.p, light = pal.light;
poly(c, [p(x, y + hh, z), p(x + ww, y + hh, z), p(x + ww, y + hh, z - dep), p(x, y + hh, z - dep)], light ? '#e8e2d9' : '#0d0d10', edge);
poly(c, [p(x + ww, y, z), p(x + ww, y + hh, z), p(x + ww, y + hh, z - dep), p(x + ww, y, z - dep)], light ? '#d6d0c9' : '#17171b', edge);
poly(c, [p(x, y, z), p(x + ww, y, z), p(x + ww, y + hh, z), p(x, y + hh, z)], top, edge);
}
function plane(c, g, z, fn) { c.save(); c.transform(.91 * g.s, .42 * g.s, -.86 * g.s, .43 * g.s, g.cx, g.cy - z * g.s); fn(); c.restore(); }
// layer A: the grid, the glow, the board, the routed traces and their pads, the screws. Still.
function paintBase(c, g) {
var p = g.p, light = pal.light, j;
for (j = -6; j <= 6; j++) { stroke(c, [p(-245, j * 29, -60), p(245, j * 29, -60)], light ? '#d9d2c666' : '#35313955', .7); stroke(c, [p(j * 40, -155, -60), p(j * 40, 155, -60)], light ? '#d9d2c666' : '#35313955', .7); }
var gr = c.createRadialGradient(g.cx, g.cy + 15 * g.s, 5, g.cx, g.cy + 15 * g.s, 235 * g.s); gr.addColorStop(0, light ? '#F2541B16' : '#F2541B14'); gr.addColorStop(1, '#F2541B00'); c.fillStyle = gr; c.fillRect(0, 0, w, h);
box(c, g, -223, -112, -40, 446, 224, 8, light ? '#ede8e0' : '#101013', light ? '#c8c1b6' : '#383139');
CONNECTIONS.forEach(function (a, i) { var pts = [p(a[0], a[1], -38), p(a[2], a[3], -38), p(a[4], a[5], -38)]; stroke(c, pts, i % 2 ? (light ? '#af795a77' : '#a5613655') : (light ? '#be59208c' : '#f2541b66'), 1.1); var q = pts[2]; c.fillStyle = i % 2 ? pal.gold : pal.ember; c.fillRect(q.x - 2, q.y - 2, 4, 4); });
[[-200, -85], [181, -85], [-200, 89], [181, 89]].forEach(function (xy) { box(c, g, xy[0], xy[1], -36, 16, 10, 3, light ? '#e2dbd0' : '#24232a', pal.line); });
var q = p(-203, 90, -34); c.strokeStyle = pal.line; c.lineWidth = 1; c.beginPath(); c.moveTo(q.x, q.y); c.lineTo(q.x - 24 * g.s, q.y + 20 * g.s); c.lineTo(q.x - 52 * g.s, q.y + 20 * g.s); c.stroke();
}
// layer B: the backplate, the fins, the shroud, the fan housings, the chip and the labels, at one lift. Rebuilt only while the lift moves.
function paintCard(c, g, top) {
var p = g.p, light = pal.light, x;
box(c, g, -187, -61, top - 18, 374, 122, 17, light ? '#b7b0a7' : '#19191f', light ? '#888279' : '#444149');
for (x = -180; x < 180; x += 7) stroke(c, [p(x, 61, top - 20), p(x, 61, top - 32)], light ? '#6b655b' : '#48434a', 1);
box(c, g, -197, -67, top, 394, 134, 17, light ? '#d0c9bf' : '#222127', light ? '#898176' : '#62504b');
stroke(c, [p(-196, 68, top - 6), p(198, 68, top - 6)], pal.ember, 2.5);
plane(c, g, top + 1, function () {
var i; c.strokeStyle = light ? '#827a71' : '#555158'; c.lineWidth = 1;
for (i = 0; i < 12; i++) { x = -178 + i * 31; c.beginPath(); c.moveTo(x, -62); c.lineTo(x + 24, -62); c.stroke(); }
[-112, 112].forEach(function (fx) { c.beginPath(); c.arc(fx, 0, 54, 0, TAU); c.fillStyle = light ? '#a29b91' : '#101015'; c.fill(); c.strokeStyle = light ? '#6c655b' : '#4e464b'; c.stroke(); c.beginPath(); c.arc(fx, 0, 48, 0, TAU); c.strokeStyle = light ? '#756d63' : '#353039'; c.stroke(); });
c.fillStyle = light ? '#a99d8e' : '#17161b'; c.fillRect(-40, -45, 80, 90); c.strokeStyle = light ? '#9a8268' : '#8c4b2d'; c.strokeRect(-40, -45, 80, 90); c.fillStyle = pal.ember; c.fillRect(-27, -30, 54, 60); c.fillStyle = light ? '#f5e6d5' : '#261610'; c.fillRect(-23, -26, 46, 52); c.strokeStyle = pal.gold; c.strokeRect(-19, -22, 38, 44);
for (var a = -30; a <= 30; a += 8) { c.fillStyle = pal.gold; c.fillRect(-44, a, 4, 2); c.fillRect(40, a, 4, 2); }
c.fillStyle = pal.gold; c.font = '500 7px IBM Plex Mono, Plex Mono Fallback, monospace'; c.textAlign = 'center'; c.fillText('IGNEUM', 0, -4); c.font = '6px IBM Plex Mono, Plex Mono Fallback, monospace'; c.fillText('COMPUTE', 0, 9);
c.font = '500 7px IBM Plex Mono, Plex Mono Fallback, monospace'; c.textAlign = 'left'; c.fillStyle = light ? '#3f362b' : '#bcaaa0'; c.fillText('EMBER / GPU WORKER', -176, 60);
[[-184, -56], [183, -56], [-184, 54], [183, 54]].forEach(function (xy) { c.beginPath(); c.arc(xy[0], xy[1], 2, 0, TAU); c.fillStyle = light ? '#6a6057' : '#878087'; c.fill(); });
});
}
function ease(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
function lerp(a, b, t) { return a + (b - a) * t; }
function liftOf(st) { return st === 1 ? 15 : st === 2 ? 8 : 0; }
function paint(t) {
if (!w || !h || disposed) return; if (!pal) theme();
var g = geom(), p = g.p, s = g.s, light = pal.light, tr = ease((t - stageAt) / 950), bump = Math.sin(clamp((t - stageAt) / 950, 0, 1) * Math.PI);
var lift = lerp(liftOf(prevStage), liftOf(stage), tr), top = 18 - lift, key = Math.round(lift * 4);
if (!base) base = layer(paintBase);
if (!card || cardKey !== String(key)) { card = layer(function (c, gg) { paintCard(c, gg, top); }); cardKey = String(key); }
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.drawImage(base, 0, 0, w, h);
// pulses travel the traces all the time, each on its own phase; a little faster while a block is being mined
var speed = stage === 0 ? 1.25 : 1;
CONNECTIONS.forEach(function (a, i) { var pts = [p(a[0], a[1], -38), p(a[2], a[3], -38), p(a[4], a[5], -38)], u = ((t * .00045 * speed) + i * .37) % 1, seg = u < .5 ? 0 : 1, v = (u % .5) * 2, pt = { x: pts[seg].x + (pts[seg + 1].x - pts[seg].x) * v, y: pts[seg].y + (pts[seg + 1].y - pts[seg].y) * v }; ctx.beginPath(); ctx.arc(pt.x, pt.y, 2.2, 0, TAU); ctx.fillStyle = i % 2 ? pal.gold : pal.ember; ctx.globalAlpha = .55 + .45 * Math.sin(u * Math.PI); ctx.fill(); ctx.globalAlpha = 1; });
ctx.drawImage(card, 0, 0, w, h);
// the fans: always turning, faster while mining, a slow idle while proving and locking
var rpm = stage === 0 ? .0062 : .0032, rot = t * rpm;
plane(ctx, g, top + 1, function () {
[-112, 112].forEach(function (fx, fi) {
var i; for (i = 0; i < 11; i++) { ctx.save(); ctx.translate(fx, 0); ctx.rotate(i * TAU / 11 + rot * (fi ? -1 : 1)); ctx.beginPath(); ctx.moveTo(10, -4); ctx.bezierCurveTo(22, -21, 39, -23, 45, -8); ctx.quadraticCurveTo(25, -9, 15, 6); ctx.closePath(); ctx.fillStyle = light ? '#8f877c' : '#3a3540'; ctx.fill(); ctx.strokeStyle = light ? '#8d8478' : '#51454a'; ctx.lineWidth = .6; ctx.stroke(); ctx.restore(); }
ctx.beginPath(); ctx.arc(fx, 0, 13, 0, TAU); ctx.fillStyle = light ? '#ddd4c8' : '#2d272e'; ctx.fill(); ctx.strokeStyle = pal.ember; ctx.lineWidth = 1.4; ctx.stroke(); ctx.beginPath(); ctx.arc(fx, 0, 3, 0, TAU); ctx.fillStyle = pal.ember; ctx.fill();
});
});
// the block: lifted off the card, breathing and drifting a little, never still
var bob = Math.sin(t * .0013) * 3 + Math.sin(t * .0007) * 1.5, z = lerp(prevStage === 0 ? 105 : 135, stage === 0 ? 105 : 135, tr) + bob, px = 50 + Math.sin(t * .0005) * 2, py = -23 + Math.cos(t * .0006) * 2, blockS = lerp(prevStage === 2 ? 33 : 28, stage === 2 ? 33 : 28, tr), i;
var below = p(px, py, top + 7), above = p(px, py, z - 10); stroke(ctx, [below, above], light ? '#b7734580' : '#f2541b60', 1);
ctx.setLineDash([3, 5]); ctx.lineDashOffset = -t * .02; stroke(ctx, [p(-112, 0, top + 8), p(-112, 0, top + 39), p(px, py, z - 10)], light ? '#b78b6866' : '#d18e4750', .8); stroke(ctx, [p(112, 0, top + 8), p(112, 0, top + 36), p(px, py, z - 10)], light ? '#b78b6866' : '#d18e4750', .8); ctx.setLineDash([]); ctx.lineDashOffset = 0;
var glow = .55 + .45 * Math.sin(t * .0021);
for (i = 0; i < 3; i++) box(ctx, g, px - blockS, py - blockS, z + i * 9, blockS * 2, blockS * 2, 6, light ? '#efe4d7' : '#221a19', i === 2 ? pal.ember : (light ? '#b2825d' : '#8d4428'));
plane(ctx, g, z + 20, function () { ctx.strokeStyle = pal.gold; ctx.lineWidth = 1; ctx.strokeRect(px - 17, py - 17, 34, 34); ctx.fillStyle = light ? 'rgba(188,101,53,' + (.5 + .4 * glow) + ')' : 'rgba(242,84,27,' + (.12 + .25 * glow) + ')'; ctx.fillRect(px - 13, py - 13, 26, 26); if (stage > 0 && tr > .4) { ctx.strokeStyle = pal.gold; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px - 7, py); ctx.lineTo(px - 2, py + 5); ctx.lineTo(px + 9, py - 7); ctx.stroke(); } else { ctx.fillStyle = pal.ember; ctx.fillRect(px - 2, py - 2, 4, 4); } });
// the shards: four, orbiting the block slowly while proving and locking
var shardA = stage >= 1 ? ease((t - stageAt) / 950) : (prevStage >= 1 ? 1 - ease((t - stageAt) / 950) : 0);
if (shardA > .01) { var orbit = t * .00035; for (i = 0; i < 4; i++) { var angle = i * TAU / 4 + .25 + orbit, r = 85 * shardA, x = px + Math.cos(angle) * r, y = py + Math.sin(angle) * r, zz = z + 12 + Math.sin(t * .0015 + i) * 4; ctx.globalAlpha = shardA; stroke(ctx, [p(x, y, zz), p(px, py, z + 10)], light ? '#b6774799' : '#b8773e80', 1); box(ctx, g, x - 9, y - 9, zz, 18, 18, 5, stage === 2 ? (light ? '#deba8e' : '#75502c') : (light ? '#ddab89' : '#5a2d1d'), pal.gold); ctx.globalAlpha = 1; } }
// the lock cage: drawn while locking, its edges shimmering
var cageA = stage === 2 ? ease((t - stageAt) / 950) : (prevStage === 2 ? 1 - ease((t - stageAt) / 950) : 0);
if (cageA > .01) { var z0 = z - 8, z1 = z + 50, v = 42, sh = .6 + .4 * Math.sin(t * .003); ctx.globalAlpha = cageA * sh; [z0, z1].forEach(function (zz) { poly(ctx, [p(px - v, py - v, zz), p(px + v, py - v, zz), p(px + v, py + v, zz), p(px - v, py + v, zz)], null, pal.gold, .9); }); [[-v, -v], [v, -v], [v, v], [-v, v]].forEach(function (xy) { stroke(ctx, [p(px + xy[0], py + xy[1], z0), p(px + xy[0], py + xy[1], z1)], pal.gold, .9); }); var a = p(px, py, z1 + 12); ctx.strokeStyle = pal.gold; ctx.lineWidth = 1.2; ctx.strokeRect(a.x - 5 * s, a.y, 10 * s, 8 * s); ctx.beginPath(); ctx.arc(a.x, a.y, 3.4 * s, Math.PI, 0); ctx.stroke(); ctx.globalAlpha = 1; }
// a ripple at every beat change, and a slow breathing ring between beats
var ring = ((t - stageAt) % 3200) / 3200, pos = p(px, py, z + 18); ctx.save(); ctx.translate(pos.x, pos.y); ctx.scale(1, .5); ctx.beginPath(); ctx.arc(0, 0, (40 + ring * 75) * s, 0, TAU); ctx.strokeStyle = light ? '#b86b35' : '#ffb35c'; ctx.globalAlpha = (1 - ring) * (bump > 0 ? .8 : .3); ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); ctx.globalAlpha = 1;
}
function setStage(n, manual) {
n = clamp(n | 0, 0, 2); if (n === stage) { if (manual) manualAt = clock; return; }
prevStage = stage; stage = n; stageAt = clock; if (manual) manualAt = clock;
if (opts.steps) Array.prototype.forEach.call(opts.steps, function (b) { b.setAttribute('aria-pressed', String(Number(b.getAttribute('data-stage')) === n)); });
if (opts.status) opts.status.textContent = LABELS[n][0];
if (opts.detail) opts.detail.textContent = LABELS[n][1];
if (!raf) paint(clock);
}
function frame(ts) {
raf = 0; if (disposed) return;
if (document.hidden || !inView || reduced.matches) { last = 0; return; }
var dt = last ? Math.min(ts - last, 50) : 16; last = ts; clock += dt;
if (clock - stageAt > AUTO && clock - manualAt > HOLD) setStage((stage + 1) % 3, false);
paint(clock);
raf = requestAnimationFrame(frame);
}
function kick() { if (raf || disposed || document.hidden || !inView || reduced.matches) return; last = 0; raf = requestAnimationFrame(frame); }
function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; last = 0; }
function resize() { var nw = canvas.clientWidth, nh = canvas.clientHeight, nd = Math.min(root.devicePixelRatio || 1, 2); if (!nw || !nh) return; if (nw === w && nh === h && nd === dpr) return; w = nw; h = nh; dpr = nd; canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr); base = null; card = null; cardKey = ''; paint(clock); kick(); }
theme(); // before the observers: a ResizeObserver may fire before the first explicit paint
var ro = root.ResizeObserver ? new ResizeObserver(resize) : null; if (ro) ro.observe(canvas); else root.addEventListener('resize', resize);
var io = root.IntersectionObserver ? new IntersectionObserver(function (e) { inView = e[0].isIntersecting; if (inView) kick(); else stop(); }, { threshold: 0.02 }) : null; if (io) io.observe(canvas);
var mo = root.MutationObserver ? new MutationObserver(function () { theme(); paint(clock); }) : null; if (mo) mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
var scheme = root.matchMedia ? root.matchMedia('(prefers-color-scheme: light)') : null; function onScheme() { theme(); paint(clock); } if (scheme && scheme.addEventListener) scheme.addEventListener('change', onScheme);
function vis() { if (document.hidden) stop(); else kick(); } document.addEventListener('visibilitychange', vis);
function onReduce() { stop(); paint(clock); kick(); } if (reduced.addEventListener) reduced.addEventListener('change', onReduce);
if (opts.steps) Array.prototype.forEach.call(opts.steps, function (b) { b.addEventListener('click', function () { setStage(Number(b.getAttribute('data-stage')), true); }); });
resize(); paint(0); kick();
return { setStage: function (n) { setStage(n, true); }, destroy: function () { disposed = true; stop(); if (ro) ro.disconnect(); if (io) io.disconnect(); if (mo) mo.disconnect(); document.removeEventListener('visibilitychange', vis); if (reduced.removeEventListener) reduced.removeEventListener('change', onReduce); if (scheme && scheme.removeEventListener) scheme.removeEventListener('change', onScheme); } };
}
root.IgneumHero = { mount: mount, LABELS: LABELS };
})(window);