/* 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);