/* Scene C, Pulse. A radial view: the tip at the centre, time outward in rings, each miner a ray. A block is a bead on its miner's ray at the radius of its age; a proven bead fills; a locked checkpoint is a ring that hardens from dashed to solid at the radius where it locked. Beads drift outward as time passes. */ (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 = {}, rays = {}, rayN = 0, cps = [], windowS = opts.window || 120; root.addEventListener('resize', function () { dims = size(); L.kick(); }); function vnow() { return Date.now() - 2500; } function ray(id) { if (!(id in rays)) rays[id] = rayN++; return rays[id]; } 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), ray: ray(b.miner || '?'), born: now }; byHash[b.hash] = e; blocks.push(e); }); var cut = vnow() - windowS * 1000 - 20000; blocks = blocks.filter(function (e) { if (e.src.ts < cut) { delete byHash[e.hash]; return false; } return true; }); cps = (d.finality && d.finality.checkpoints) || []; L.kick(); } function polar(e) { var R = Math.min(dims.W, dims.H) / 2 - 14, cx = dims.W / 2, cy = dims.H / 2; var r = 10 + (R - 10) * ((vnow() - e.src.ts) / (windowS * 1000)); var ang = ((e.ray * 0.618034) % 1) * Math.PI * 2 - Math.PI / 2; return { x: cx + Math.cos(ang) * r, y: cy + Math.sin(ang) * r, r: r, ang: ang }; } function draw(t) { var W = dims.W, H = dims.H; T = F.theme(); var cx = W / 2, cy = H / 2, R = Math.min(W, H) / 2 - 14; ctx.clearRect(0, 0, W, H); // time rings every 30 s ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1; for (var s = 30; s < windowS; s += 30) { var rr = 10 + (R - 10) * (s / windowS); ctx.beginPath(); ctx.arc(cx, cy, rr, 0, Math.PI * 2); ctx.stroke(); } // the tip var pulse = 0.5 + 0.5 * Math.sin(t / 900); var tg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 26); tg.addColorStop(0, F.rgba(T.ember, 0.35 + 0.2 * pulse)); tg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = tg; ctx.beginPath(); ctx.arc(cx, cy, 26, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = T.ember; ctx.beginPath(); ctx.arc(cx, cy, 3.5, 0, Math.PI * 2); ctx.fill(); // checkpoint rings, hardening when locked cps.forEach(function (c) { var ref = null, best = Infinity; blocks.forEach(function (e) { if (e.src.blue_score === null) return; var dd = Math.abs(e.src.blue_score - c.blue_score); if (dd < best) { best = dd; ref = e; } }); if (!ref || best > 3) return; var p = polar(ref); if (p.r > R) return; var locked = c.state === 'locked', age = (performance.now() - ref.born) / 1000, hard = locked ? Math.min(1, age / 2) : 0; ctx.strokeStyle = F.rgba(T.ember, locked ? 0.35 + 0.5 * hard : 0.3); ctx.lineWidth = locked ? 1 + hard : 1; ctx.setLineDash(locked && hard > 0.9 ? [] : [3, 6]); ctx.beginPath(); ctx.arc(cx, cy, p.r, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); }); // the selected chain as a soft spiral thread through its beads var chain = blocks.filter(function (e) { return e.src.chain; }).sort(function (a, b) { return a.src.ts - b.src.ts; }); // the selected chain as short arcs between consecutive chain beads (the arc follows the ring between the two radii, never a chord through the centre) if (chain.length > 1) { ctx.strokeStyle = F.rgba(T.ember, 0.28); ctx.lineWidth = 1; for (var i = 1; i < chain.length; i++) { var p0 = polar(chain[i - 1]), p1 = polar(chain[i]); if (p0.r > R || p1.r > R) continue; var a0 = p0.ang, a1 = p1.ang, da = ((a1 - a0 + Math.PI * 3) % (Math.PI * 2)) - Math.PI; var steps = 10; ctx.beginPath(); for (var k = 0; k <= steps; k++) { var f = k / steps, rr = p0.r + (p1.r - p0.r) * f, aa = a0 + da * f; var x = cx + Math.cos(aa) * rr, y = cy + Math.sin(aa) * rr; if (k) ctx.lineTo(x, y); else ctx.moveTo(x, y); } ctx.stroke(); } } // beads blocks.forEach(function (e) { var p = polar(e); if (p.r > R + 6) return; var w = e.word, age = (t - e.born) / 1000; var col = w === 'proven' ? T.bone : (w === 'included' || w === 'locked') ? T.ember : w === 'excluded' ? T.ash : T.molten; var rad = e.src.chain ? 4 : 3; if (age < 1.2) { var gl = 1 - age / 1.2; var rg = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 16); rg.addColorStop(0, F.rgba(T.ember, 0.5 * gl)); rg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(p.x, p.y, 16, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = w === 'excluded' ? 0.45 : 1; ctx.beginPath(); ctx.arc(p.x, p.y, rad, 0, Math.PI * 2); if (w === 'proven' || w === 'included' || w === 'locked') { ctx.fillStyle = col; ctx.fill(); } else { ctx.fillStyle = T.row; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.2; ctx.stroke(); } if (w === 'locked') { ctx.strokeStyle = F.rgba(T.ember, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(p.x, p.y, rad + 4, 0, Math.PI * 2); ctx.stroke(); } ctx.globalAlpha = 1; }); } var L = F.loop(draw, canvas); L.kick(); return { push: push, redraw: L.kick }; } root.IgneumPulse = { mount: mount }; })(window);