/* Scene A, Forge. Blocks arrive as embers rising from the bottom, one column per miner. A block cools as its word changes: pending is molten, included is ember, proven is bone. A lock is a dark band that sweeps up the scene at the newest locked checkpoint; everything below it is final and settles to ash. Every ember is a block the observer stored. */ (function (root) { 'use strict'; var F = root.IgneumFeed; function mount(canvas, opts) { opts = opts || {}; var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme(); var blocks = [], byHash = {}, lanes = {}, laneN = 0, lockBlue = null, lockY = null, lockTarget = null, lastT = 0; var WIN = opts.window || 120; // seconds from the hearth to the top; a block's height is its header age, its glow is its arrival function vnow() { return Date.now() - 2500; } root.addEventListener('resize', function () { dims = size(); L.kick(); }); function laneX(id) { if (!(id in lanes)) lanes[id] = laneN++; var n = Math.max(1, laneN); var i = lanes[id]; return 24 + ((i * 0.618034) % 1) * (dims.W - 48); } function push(d) { var now = performance.now(); d.blocks.forEach(function (b) { var e = byHash[b.hash]; if (e) { e.src = b; e.word = F.word(b); return; } e = { hash: b.hash, src: b, word: F.word(b), x: laneX(b.miner || '?') + (F.hash32(b.hash) - 0.5) * 10, born: now, ts: b.ts, glow: 1, settle: 0 }; byHash[b.hash] = e; blocks.push(e); }); var cps = (d.finality && d.finality.checkpoints) || [], locked = cps.filter(function (c) { return c.state === 'locked'; }); lockBlue = locked.length ? locked[locked.length - 1].blue_score : null; blocks.forEach(function (e) { e.final = lockBlue !== null && e.src.blue_score !== null && e.src.blue_score <= lockBlue; }); var cut = vnow() - WIN * 1000 - 8000; blocks = blocks.filter(function (e) { if (e.ts < cut) { delete byHash[e.hash]; return false; } return true; }); L.kick(); } function yOf(e) { return dims.H - 16 - ((vnow() - e.ts) / 1000) * ((dims.H - 32) / WIN); } function draw(t) { var W = dims.W, H = dims.H; T = F.theme(); ctx.clearRect(0, 0, W, H); // the hearth: a faint warm floor var g = ctx.createLinearGradient(0, H, 0, H - 90); g.addColorStop(0, F.rgba(T.ember, 0.16)); g.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = g; ctx.fillRect(0, H - 90, W, 90); // the lock band: the lowest final block's height, eased var target = null; blocks.forEach(function (e) { if (e.final) { var y = yOf(e); if (target === null || y > target) target = y; } }); if (target !== null) { lockY = lockY === null ? target : lockY + (target - lockY) * 0.08; } if (lockY !== null) { var band = ctx.createLinearGradient(0, lockY - 70, 0, lockY); band.addColorStop(0, F.rgba(T.row, 0)); band.addColorStop(1, F.rgba(T.row, 0.9)); ctx.fillStyle = band; ctx.fillRect(0, lockY - 70, W, 70); ctx.fillStyle = F.rgba(T.row, 0.9); ctx.fillRect(0, 0, W, Math.max(0, lockY - 70)); ctx.strokeStyle = F.rgba(T.ember, 0.6); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, lockY); ctx.lineTo(W, lockY); ctx.stroke(); ctx.fillStyle = T.ash; ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'bottom'; ctx.fillText('locked', W - 10, lockY - 4); } blocks.forEach(function (e) { var y = yOf(e); if (y < -20) return; var age = (t - e.born) / 1000, w = e.word; var col = w === 'proven' ? T.bone : w === 'included' || w === 'locked' ? T.ember : w === 'excluded' ? T.ash : T.molten; var r = w === 'excluded' ? 2.2 : w === 'proven' ? 3.6 : 3; var a = e.final ? 0.45 : 1; if (age < 1.2) { var gl = 1 - age / 1.2; var rg = ctx.createRadialGradient(e.x, y, 0, e.x, y, 18); rg.addColorStop(0, F.rgba(T.molten, 0.5 * gl)); rg.addColorStop(1, F.rgba(T.molten, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(e.x, y, 18, 0, Math.PI * 2); ctx.fill(); } var flicker = e.final ? 0 : Math.sin(t / 420 + e.x) * 0.6; ctx.globalAlpha = a; ctx.fillStyle = col; ctx.beginPath(); ctx.arc(e.x + flicker, y, r, 0, Math.PI * 2); ctx.fill(); if (w === 'locked') { ctx.strokeStyle = F.rgba(T.ember, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(e.x, y, r + 4, 0, Math.PI * 2); ctx.stroke(); } // a thin trail: the ember's rise ctx.strokeStyle = F.rgba(col, 0.12); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(e.x, y + r); ctx.lineTo(e.x, Math.min(H - 16, y + 14)); ctx.stroke(); ctx.globalAlpha = 1; }); } var L = F.loop(draw, canvas); L.kick(); return { push: push, redraw: L.kick }; } root.IgneumForge = { mount: mount }; })(window);