igneum/site/live-dag.js
igneum-labs b63be56016 Site UI 3: one design system, the live DAG module, the home page as one screen, the litepaper as a paper, every page on the shared tokens
site/site.css: the tokens of the miner app's third redesign in light and dark (a darker light-mode ember and molten for text
so every pair passes 4.5:1, checked by tools/ci/site-contrast-check.mjs), the type scale on Unbounded, Plex Sans and Plex
Mono, the nav with one primary, the footer with a System/Light/Dark control, cards, tiles, tables that scroll on a phone,
buttons, pills, notes, callouts, the state words, focus rings, reduced motion, print. Partials rebuilt; the head loads the
stylesheet and applies the stored theme before paint. Every page's own style block is reduced to its page rules.

site/live-dag.js: the shared DAG view for / and /live (docs/plans/site-ui-3.md section 6): blocks by header time and
miner lane, parent edges with the selected chain as one heavier path, blue lit, red dim, pending grey, proven filled,
checkpoint bands with their lock weight (locked solid, pending dashed), new blocks arriving with a glow, hover and tap
details in the ledger's words, wheel and pinch zoom of the window (30 to 300 s), a pause button, device-pixel sharpness,
a still frame under reduced motion; opts.mine marks the user's own blocks (the miner app embeds the same file),
opts.poll:false with dag.push(reply) lets a host feed it. Every pixel is a block the observer stored.

site/index.html: the one-screen story (what it is, the live scene with chain block, shards proven, last lock, vote keys and
hash rate as state words, three things a sceptic checks, the download, how it works, the wallet, the journey, economics),
every tick-list row kept, the chip model's numbers moved to the sceptic card, the testnet terms behind a chevron, the
scroll-reveal gone, the Open Graph set on the 1200 by 630 image, no horizontal overflow at 390.

site/litepaper.html: dark like the site with light on request, whole paper by default (the section mode kept, deep links
intact, print prints the paper), repository paths off the surface, the cover dated 6 October, the wallet at 0.1.4, the
roadmap's verifier figure aligned with the Mining section, the proof lag stated as measured (about 380 s against the 60 s
gate), the 0.3.6 plan dated with 0.3.14 stated, the two "tonight" placeholders said as not yet measured. Every ledger
sentence verbatim (tools/ci/ledger-text-check.mjs, 43 sentences).

site/live.html: the lane chart replaced by the module; "proven A/B in 10 min", "finality paused, last #N", "pending" for
"n/a"; the fee sentence true on both sides of DAA 210,000. site/metamask.html: its own canonical, the explorer linked, the
wallet's state said true. site/404.html: the page count and section count said true. site/build.mjs: the generated pages
on the system, each with its own eyebrow, the miners page's source notes as sentences. downloads.json refreshed from the
host (0.3.14). tools/ci/site-orphan-check.mjs: no site heading leaves one word alone at 390 px (log titles reported).

Checks: identity grep 0 hits, link check 854 links 0 broken, ledger text 43 of 43, contrast 32 pairs 0 under 4.5:1,
no horizontal overflow at 390 on 14 pages (the wallet page's timed table overflows by 5 px, for the wallet-ui-3 owner),
orphan check 0 site headings (the miner h1 at 96 characters is the site-miner agent's copy). After captures beside the
audit's: docs/plans/site-ui-3-shots/after/.

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

222 lines
19 KiB
JavaScript

/* Igneum live DAG view (docs/plans/site-ui-3.md section 6, 6 October 2026).
One module for /live and the home page. Reads /api/live only; every pixel is a block, edge or checkpoint the observer
stored. Blocks are placed by header time on x (the axis is labelled in DAA score) and by miner lane on y. Parent edges
are drawn; the edge between two selected-chain blocks is heavier and the chain glows as one path. Colour words are the
ledger's: pending, included (blue), excluded (red), proven (every shard verified or paid), locked (a checkpoint block).
Checkpoints are vertical bands at their blue score with the lock weight; a locked band is solid, a pending one dashed.
The view scrolls at chain speed, new blocks arrive from the right with a brief glow; wheel and pinch zoom the window
(30 to 300 s, the API's ?window=); a pause button; hover and tap show a block's details.
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, pauseButton, tooltip })
- url: the /api/live endpoint (default '/api/live'); poll: false stops the module's own 2 s fetch, the host then calls
dag.push(reply) with a reply of the same shape ({ok, state:{stale, age_s, height, ...}, blocks:[...], finality:{checkpoints:[...]}, proving})
whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
- block record (the /api/live shape): hash (string, 16 hex), ts (ms, header time), blue_score, daa (numbers), parents ([hash]),
chain (bool, selected chain), color ('blue' | 'red' | 'pending'), miner (string lane key, 8 hex of the vote key hash),
locked (bool, a locked checkpoint block), final (bool, at or before the newest lock), shards ([{state: 'planned' | 'proving' |
'verified' | 'paid'}]), proven (bool, every shard verified or paid). Checkpoints: {index, blue_score, state: 'locked' | 'pending', fraction_total}.
- mine: a miner lane key (string) or function(block) -> bool; the user's own blocks take a molten ring and the lane is labelled "you".
- The legend is the host's; the module draws no HTML. Colours come from the CSS custom properties on :root (--ember, --ember-hi,
--molten, --bone, --ash, --line, --line-2, --row or --obsidian), re-read on data-theme and prefers-color-scheme changes.
- Sizing: the canvas fills its CSS box (width and height from the stylesheet); 280 px wide works, 600 or more shows lane labels well.
onState(state, reason): 'connecting' | 'live' | 'stale' | 'failed'; onData(d) with every fresh reply. */
(function (root) {
'use strict';
var COL = {
ember: '#F2541B', emberHi: '#FF6A2B', molten: '#FFB35C', bone: '#F4F1EC', ash: '#9A9A9E', line: '#2A2A30', line2: '#3A3A42', obsidian: '#0C0C0E',
red: '#B0362B'
};
function cssVar(name, fallback) { try { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; } }
function readTheme() {
COL.ember = cssVar('--ember', COL.ember); COL.emberHi = cssVar('--ember-hi', COL.emberHi); COL.molten = cssVar('--molten', COL.molten);
COL.bone = cssVar('--bone', COL.bone); COL.ash = cssVar('--ash', COL.ash); COL.line = cssVar('--line', COL.line); COL.line2 = cssVar('--line-2', COL.line2);
COL.obsidian = cssVar('--row', cssVar('--obsidian', COL.obsidian));
}
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 + ')'; }
function fmt(n) { return n === null || n === undefined ? 'pending' : Number(n).toLocaleString('en-GB'); }
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 mount(canvas, opts) {
opts = opts || {};
var compact = !!opts.compact, windowS = Math.max(30, Math.min(300, opts.window || (compact ? 90 : 120)));
var reduce = root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches;
var isMine = typeof opts.mine === 'function' ? opts.mine : opts.mine ? function (b) { return b.miner === opts.mine; } : null;
var ctx = canvas.getContext('2d'), dpr = Math.min(root.devicePixelRatio || 1, 2), W = 0, H = 0;
var blocks = [], byHash = {}, lanes = [], laneOf = {}, cps = [], data = null, state = 'connecting', reason = 'connecting';
var paused = false, offsetMs = 0, hover = null, pinned = null, lastPoll = 0, failures = 0, running = false, inView = true, lastT = 0;
var vnow = function () { return Date.now() - 2500 - offsetMs; }; // the right edge sits 2.5 s behind the clock so a block has arrived before it is drawn
var padL = compact ? 10 : 56, padR = 14, padT = compact ? 12 : 22, padB = compact ? 10 : 26;
function size() { 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); layout(); }
root.addEventListener('resize', size);
// ---- data ----
function apply(d) {
data = d;
var seen = {}, now = performance.now();
d.blocks.forEach(function (b) {
seen[b.hash] = 1;
var e = byHash[b.hash];
if (e) { e.chain = !!b.chain; e.color = b.color || 'pending'; e.locked = !!b.locked; e.final = !!b.final; e.shards = b.shards || []; e.proven = !!b.proven; e.blue = b.blue_score; e.daa = b.daa; return; }
e = { hash: b.hash, ts: b.ts, blue: b.blue_score, daa: b.daa, parents: b.parents || [], chain: !!b.chain, color: b.color || 'pending', miner: b.miner || '?', locked: !!b.locked, final: !!b.final, shards: b.shards || [], proven: !!b.proven, born: now, x: 0, y: 0 };
byHash[b.hash] = e; blocks.push(e);
});
var cut = vnow() - windowS * 1000 - 20000;
blocks = blocks.filter(function (b) { if (b.ts < cut && !seen[b.hash]) { delete byHash[b.hash]; return false; } return true; });
blocks.sort(function (a, b) { return a.ts - b.ts; });
cps = (d.finality && d.finality.checkpoints) || [];
layout();
}
function layout() {
// lanes: miners of the visible window ordered by blocks, as many as fit at 22 px, the rest in one "others" lane
var vis = vnow() - windowS * 1000, count = {};
blocks.forEach(function (b) { if (b.ts >= vis) count[b.miner] = (count[b.miner] || 0) + 1; });
var ids = Object.keys(count).sort(function (a, b) { return count[b] - count[a] || (a < b ? -1 : 1); });
var usable = H - padT - padB, laneH = compact ? 26 : 22, maxLanes = Math.max(2, Math.floor(usable / laneH));
var shown = ids.length > maxLanes ? ids.slice(0, maxLanes - 1) : ids;
lanes = shown.slice(); if (ids.length > shown.length) lanes.push('others');
laneOf = {}; shown.forEach(function (id, i) { laneOf[id] = i; });
}
function laneY(b) { var i = b.miner in laneOf ? laneOf[b.miner] : lanes.length - 1; var n = Math.max(1, lanes.length); return padT + (i + 0.5) * ((H - padT - padB) / n); }
function xOf(ts) { return padL + (W - padL - padR) * (1 - (vnow() - ts) / (windowS * 1000)); }
// the host can feed replies itself (poll: false); the same path the fetch takes
function push(d) {
if (!d || !d.ok || !d.state || !d.blocks) { setState('failed', 'live feed unavailable'); return; }
failures = 0;
if (d.state.stale) setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown'));
else setState('live', null);
apply(d); if (opts.onData) opts.onData(d); kick();
}
function poll() {
if (opts.poll === false || document.hidden) return;
var u = (opts.url || '/api/live') + '?window=' + windowS;
fetch(u, { 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) { setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); }
else setState('live', null);
apply(d); if (opts.onData) opts.onData(d);
kick();
}).catch(function () { failures++; if (failures >= 2) setState('failed', 'live feed unavailable'); });
}
function setState(s, r) { if (s === state && r === reason) return; state = s; reason = r; if (opts.onState) opts.onState(s, r); }
// ---- drawing ----
function draw(t) {
if (!W || !H) return;
var dt = lastT ? Math.min(50, t - lastT) : 16; lastT = t;
ctx.clearRect(0, 0, W, H);
var vis = vnow() - windowS * 1000, n = Math.max(1, lanes.length), lh = (H - padT - padB) / n;
// lane rails and labels
ctx.lineWidth = 1;
for (var i = 0; i < n; i++) {
var y = padT + (i + 0.5) * lh;
ctx.strokeStyle = rgba(COL.line, 0.9); ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke();
if (!compact) { var mineLane = isMine && typeof opts.mine === 'string' && lanes[i] === opts.mine; ctx.fillStyle = mineLane ? COL.molten : COL.ash; ctx.font = (mineLane ? '500' : '400') + ' 10px IBM Plex Mono, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(mineLane ? 'you' : lanes[i] === 'others' ? 'others' : String(lanes[i]).slice(0, 8), 6, y); }
}
// checkpoint bands at their blue score (x from the chain block carrying that score, else the nearest)
cps.forEach(function (c) {
var ref = null, best = Infinity;
for (var k = blocks.length - 1; k >= 0; k--) { var b = blocks[k]; if (b.blue === null || b.blue === undefined) continue; var dd = Math.abs(b.blue - c.blue_score); if (dd < best) { best = dd; ref = b; } }
if (!ref || best > 3 || ref.ts < vis) return;
var x = xOf(ref.ts), locked = c.state === 'locked';
ctx.strokeStyle = rgba(COL.ember, locked ? 0.55 : 0.3); ctx.lineWidth = locked ? 1.5 : 1; ctx.setLineDash(locked ? [] : [4, 6]);
ctx.beginPath(); ctx.moveTo(x, padT - 6); ctx.lineTo(x, H - padB + 6); ctx.stroke(); ctx.setLineDash([]);
if (!compact) { ctx.fillStyle = rgba(COL.ember, 0.9); ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText((locked ? 'locked ' : 'pending ') + (c.fraction_total !== null && c.fraction_total !== undefined ? Math.round(c.fraction_total * 100) + '%' : ''), x, H - 8); }
});
// positions
blocks.forEach(function (b) { b.x = xOf(b.ts); b.y = laneY(b); b.on = b.ts >= vis && b.x >= padL - 10; });
// edges: merge edges thin, chain-to-chain heavier; the chain as one glowing path
blocks.forEach(function (b) {
if (!b.on) return;
b.parents.forEach(function (h) {
var p = byHash[h]; if (!p || !p.on) return;
var chainEdge = b.chain && p.chain;
ctx.strokeStyle = chainEdge ? rgba(COL.ember, 0.75) : rgba(COL.ash, b.color === 'red' ? 0.12 : 0.22);
ctx.lineWidth = chainEdge ? 2 : 1;
ctx.beginPath(); ctx.moveTo(b.x, b.y); var mx = (b.x + p.x) / 2; ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke();
if (chainEdge) { ctx.strokeStyle = rgba(COL.ember, 0.18); ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke(); }
});
});
// blocks
var S = compact ? 9 : 8, now = performance.now();
blocks.forEach(function (b) {
if (!b.on) return;
var age = now - b.born, glow = age < 1600 ? 1 - age / 1600 : 0, ex = b.color === 'red';
var r = b.chain ? S + 1 : S;
if (glow > 0) { var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r * 3); g.addColorStop(0, rgba(COL.ember, 0.4 * glow)); g.addColorStop(1, rgba(COL.ember, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, r * 3, 0, Math.PI * 2); ctx.fill(); }
ctx.globalAlpha = ex ? 0.45 : 1;
// fill: proven = ember, otherwise the row surface; outline: included (blue or chain) = ember, pending = grey, excluded = dim
var proving = !b.proven && b.shards.some(function (s) { return s.state === 'proving' || s.state === 'verified' || s.state === 'paid'; });
ctx.beginPath(); rr(b.x - r, b.y - r, 2 * r, 2 * r, 3);
ctx.fillStyle = b.proven ? COL.ember : COL.obsidian; ctx.fill();
ctx.lineWidth = b.chain ? 2 : 1.5;
// chain blocks in ember (the path), included blocks lit in bone, pending in ash, excluded dim
ctx.strokeStyle = b.proven ? COL.ember : proving ? COL.molten : b.chain ? rgba(COL.ember, 0.9) : b.color === 'blue' ? rgba(COL.bone, 0.8) : ex ? rgba(COL.ash, 0.5) : rgba(COL.ash, 0.75);
ctx.stroke();
if (b.locked) { ctx.strokeStyle = rgba(COL.ember, 0.8 + 0.2 * Math.sin(t / 350)); ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.9, 0, Math.PI * 2); ctx.stroke(); }
if (isMine && isMine(b)) { ctx.strokeStyle = rgba(COL.molten, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.5, 0, Math.PI * 2); ctx.stroke(); }
ctx.globalAlpha = 1;
});
// hover or pinned ring
var f = pinned || hover; if (f && f.on) { ctx.strokeStyle = COL.bone; ctx.lineWidth = 1; ctx.setLineDash([2, 3]); ctx.beginPath(); ctx.arc(f.x, f.y, S * 2.4, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); }
// axis in DAA at the left and right edges, and the lock line label
if (!compact) {
ctx.fillStyle = COL.ash; ctx.font = '400 10px IBM Plex Mono, monospace'; ctx.textBaseline = 'alphabetic';
var first = null, last = null; blocks.forEach(function (b) { if (!b.on) return; if (!first || b.ts < first.ts) first = b; if (!last || b.ts > last.ts) last = b; });
if (first && last) { ctx.textAlign = 'left'; ctx.fillText('daa ' + fmt(first.daa), padL, padT - 8); ctx.textAlign = 'right'; ctx.fillText('daa ' + fmt(last.daa) + (paused ? ' paused' : ''), W - padR, padT - 8); }
}
if (paused) { ctx.fillStyle = rgba(COL.obsidian, 0.0); }
}
function rr(x, y, w, h, r) { ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
function frame(t) { if (!running) return; if (!paused) draw(t); if (reduce || document.hidden || !inView) { running = false; return; } requestAnimationFrame(frame); }
function start() { if (running) return; running = true; requestAnimationFrame(frame); }
function kick() { if (!running) requestAnimationFrame(function (t) { draw(t); if (!reduce && !document.hidden && inView) start(); }); }
// ---- interaction ----
function hit(ev) {
var rect = canvas.getBoundingClientRect(), px = ev.clientX - rect.left, py = ev.clientY - rect.top, best = null, bd = 14 * 14;
blocks.forEach(function (b) { if (!b.on) return; var dx = b.x - px, dy = b.y - py, d = dx * dx + dy * dy; if (d < bd) { bd = d; best = b; } });
return best;
}
function words(b) {
var state = b.locked ? 'locked checkpoint' : b.proven ? 'proven' : b.chain ? 'included, selected chain' : b.color === 'blue' ? 'included' : b.color === 'red' ? 'excluded' : 'pending';
var sh = b.shards.length ? b.shards.map(function (s) { return s.state; }).join(', ') : 'no shard plan yet';
return { title: b.hash + ' ' + state, lines: [(isMine && isMine(b) ? 'your block, miner ' : 'miner ') + b.miner + (b.miner in laneOf ? '' : ' (others lane)'), 'blue score ' + fmt(b.blue) + ' daa ' + fmt(b.daa), b.parents.length + (b.parents.length === 1 ? ' parent' : ' parents'), 'proof: ' + sh + (b.final ? ' final' : '')] };
}
var tip = opts.tooltip || null;
function showTip(b, ev) {
if (!tip) return;
if (!b) { tip.hidden = true; return; }
var w = words(b); tip.hidden = false; tip.innerHTML = '<b>' + w.title + '</b>' + w.lines.map(function (l) { return '<span>' + l + '</span>'; }).join('');
var rect = canvas.getBoundingClientRect(); var x = Math.min(rect.width - tip.offsetWidth - 8, Math.max(8, b.x + 12)), y = Math.max(8, b.y - tip.offsetHeight - 10); if (y < 8) y = b.y + 14;
tip.style.transform = 'translate(' + Math.round(x) + 'px,' + Math.round(y) + 'px)';
}
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); });
canvas.addEventListener('mouseleave', function () { hover = null; if (!pinned) showTip(null); kick(); });
canvas.addEventListener('click', function (ev) { var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); });
canvas.addEventListener('wheel', function (ev) { if (!ev.ctrlKey && Math.abs(ev.deltaY) < 1) return; ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15); }, { passive: false });
var pinch = null;
canvas.addEventListener('touchstart', function (ev) { if (ev.touches.length === 2) pinch = dist(ev.touches); }, { passive: true });
canvas.addEventListener('touchmove', function (ev) { if (ev.touches.length === 2 && pinch) { var d = dist(ev.touches); if (Math.abs(d - pinch) > 12) { zoom(d > pinch ? 1 / 1.1 : 1.1); pinch = d; } } }, { passive: true });
canvas.addEventListener('touchend', function () { pinch = null; }, { passive: true });
function dist(t) { var dx = t[0].clientX - t[1].clientX, dy = t[0].clientY - t[1].clientY; return Math.sqrt(dx * dx + dy * dy); }
function zoom(f) { var w = Math.round(Math.max(30, Math.min(300, windowS * f))); if (w === windowS) return; windowS = w; layout(); if (opts.onWindow) opts.onWindow(w); poll(); kick(); }
function setPaused(p) { paused = !!p; if (paused) { offsetMs = 0; pausedAt = Date.now(); } else { offsetMs = 0; } if (opts.pauseButton) { opts.pauseButton.setAttribute('aria-pressed', paused ? 'true' : 'false'); opts.pauseButton.textContent = paused ? 'Resume' : 'Pause'; } kick(); }
var pausedAt = 0;
if (opts.pauseButton) opts.pauseButton.addEventListener('click', function () { setPaused(!paused); });
// while paused the clock stops: offsetMs grows so the scene holds still but keeps its blocks
setInterval(function () { if (paused) offsetMs = Date.now() - pausedAt; }, 250);
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) { poll(); kick(); } });
if (root.matchMedia) { try { root.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { readTheme(); kick(); }); } catch (e) { } }
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
readTheme(); size(); if (opts.poll !== false) { poll(); setInterval(poll, 2000); } kick();
return { push: push, setPaused: setPaused, zoom: zoom, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
}
root.IgneumDag = { mount: mount };
})(window);