Live DAG: the page's scroll passes through the scene; zoom only with ctrl or cmd, a pinch, or a click into the scene
the project lead, 6 October 2026: scrolling past the DAG zoomed it. The module's wheel listener took every vertical wheel. Now a plain wheel or a trackpad's two-finger scroll is never captured: the handler returns before preventDefault unless ctrl or cmd is held or the scene is engaged. A click into the scene engages it (the canvas takes an ember outline and a chip reads "scroll to zoom, Esc to release"); Escape, a click outside the scene or the chip releases it. Pinch zoom is unchanged. Same module on / and /live; both pages carry the chip and the note says how to zoom. opts.chip and opts.onEngage are new, optional; dag.engage(bool) is exposed. Proved headless on both pages: a plain wheel over the scene scrolled the page 300 px and left the window at 120 s; engaged, a wheel zoomed the window from 120 to 138 s with the default prevented; the chip showed on click and hid on Escape; no console errors. Link check 884 links 0 broken. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e69117a900
commit
6bc408b9c1
4 changed files with 32 additions and 12 deletions
|
|
@ -260,6 +260,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="scene">
|
<div class="scene">
|
||||||
<canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner, parent edges, the selected chain, checkpoints and proofs"></canvas>
|
<canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner, parent edges, the selected chain, checkpoints and proofs"></canvas>
|
||||||
|
<div class="chip-zoom" id="dag-chip" hidden><span class="dot live"></span>scroll to zoom, Esc to release</div>
|
||||||
<div class="tip" id="dag-tip" hidden></div>
|
<div class="tip" id="dag-tip" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scene-foot">
|
<div class="scene-foot">
|
||||||
|
|
@ -273,7 +274,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="cta"><button type="button" class="btn" id="dag-pause" aria-pressed="false">Pause</button><a href="/live" class="btn">The live page</a></div>
|
<div class="cta"><button type="button" class="btn" id="dag-pause" aria-pressed="false">Pause</button><a href="/live" class="btn">The live page</a></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="note" id="viz-note">One node is read every 2 s. Chain block is the EVM block number. Shards proven is every shard paid on the chain so far. Last lock is the latest checkpoint the finality rule locked. Hover or tap a block for its hash, miner, blue score, parents and proof state. Scroll or pinch to change the window.</p>
|
<p class="note" id="viz-note">One node is read every 2 s. Chain block is the EVM block number. Shards proven is every shard paid on the chain so far. Last lock is the latest checkpoint the finality rule locked. Hover or tap a block for its hash, miner, blue score, parents and proof state. Ctrl or cmd and scroll, a pinch, or a click into the scene then scroll, changes the window.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid c2" style="margin-top:var(--s-4)">
|
<div class="grid c2" style="margin-top:var(--s-4)">
|
||||||
<div class="card program">
|
<div class="card program">
|
||||||
|
|
@ -582,10 +583,10 @@
|
||||||
cm.textContent=fmtN(s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:(d.miners||[]).length);
|
cm.textContent=fmtN(s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:(d.miners||[]).length);
|
||||||
ch.textContent=fmtHash(s.hashes_per_second_estimate);
|
ch.textContent=fmtHash(s.hashes_per_second_estimate);
|
||||||
if(note&&!s.stale){var lag=pv&&pv.active&&pv.median_proof_lag_s?Math.round(pv.median_proof_lag_s):null;
|
if(note&&!s.stale){var lag=pv&&pv.active&&pv.median_proof_lag_s?Math.round(pv.median_proof_lag_s):null;
|
||||||
note.textContent='One node is read every 2 s. Chain block is the EVM block number. Shards proven is every shard paid on the chain so far'+(pv&&pv.active?' ('+fmtN(pv.shards_paid_10m||0)+' in the last 10 minutes'+(lag?', landing a median '+lag+' s behind the tip, so the newest blocks on screen are still pending':'')+')':'')+'. Last lock is the latest checkpoint the finality rule locked. A card runs several vote keys. Hover or tap a block for its details; scroll or pinch to change the window.';}
|
note.textContent='One node is read every 2 s. Chain block is the EVM block number. Shards proven is every shard paid on the chain so far'+(pv&&pv.active?' ('+fmtN(pv.shards_paid_10m||0)+' in the last 10 minutes'+(lag?', landing a median '+lag+' s behind the tip, so the newest blocks on screen are still pending':'')+')':'')+'. Last lock is the latest checkpoint the finality rule locked. A card runs several vote keys. Hover or tap a block for its details; ctrl or cmd and scroll, a pinch, or a click into the scene then scroll, changes the window.';}
|
||||||
}
|
}
|
||||||
function mountDag(){var c=document.getElementById('dag');if(!c||!window.IgneumDag)return;
|
function mountDag(){var c=document.getElementById('dag');if(!c||!window.IgneumDag)return;
|
||||||
IgneumDag.mount(c,{compact:window.innerWidth<700,window:90,onState:setWord,onData:onData,pauseButton:document.getElementById('dag-pause'),tooltip:document.getElementById('dag-tip')});}
|
IgneumDag.mount(c,{compact:window.innerWidth<700,window:90,onState:setWord,onData:onData,pauseButton:document.getElementById('dag-pause'),tooltip:document.getElementById('dag-tip'),chip:document.getElementById('dag-chip')});}
|
||||||
if(window.IgneumDag)mountDag();else document.addEventListener('DOMContentLoaded',function(){var s=document.querySelector('script[src="/live-dag.js"]');if(window.IgneumDag)mountDag();else if(s)s.addEventListener('load',mountDag);});
|
if(window.IgneumDag)mountDag();else document.addEventListener('DOMContentLoaded',function(){var s=document.querySelector('script[src="/live-dag.js"]');if(window.IgneumDag)mountDag();else if(s)s.addEventListener('load',mountDag);});
|
||||||
|
|
||||||
// hourly program ticker
|
// hourly program ticker
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
are drawn; the edge between two selected-chain blocks is heavier and the chain glows as one path. Colour words are the
|
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).
|
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.
|
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
|
The view scrolls at chain speed, new blocks arrive from the right with a brief glow. The page's scroll passes through the
|
||||||
(30 to 300 s, the API's ?window=); a pause button; hover and tap show a block's details.
|
scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene (engaged, released by Escape or a click
|
||||||
|
outside; opts.chip is the "scroll to zoom, Esc to release" element); 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 })
|
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, pauseButton, tooltip, chip })
|
||||||
- url: the /api/live endpoint (default '/api/live'); poll: false stops the module's own 2 s fetch, the host then calls
|
- 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})
|
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).
|
whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
|
||||||
|
|
@ -196,8 +198,20 @@
|
||||||
}
|
}
|
||||||
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); });
|
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('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('click', function (ev) { engage(true); 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 });
|
// the wheel is never captured by default (6 Oct 2026, the project lead: scrolling past the scene zoomed it): the page scrolls through the
|
||||||
|
// scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene, which engages it (opts.chip shows
|
||||||
|
// "scroll to zoom, Esc to release"); Escape, a click outside or leaving the scene releases it
|
||||||
|
var engaged = false, chip = opts.chip || null;
|
||||||
|
function engage(on) { if (on === engaged) return; engaged = on; canvas.classList.toggle('engaged', on); if (chip) chip.hidden = !on; if (opts.onEngage) opts.onEngage(on); }
|
||||||
|
canvas.addEventListener('wheel', function (ev) {
|
||||||
|
var zoomKey = ev.ctrlKey || ev.metaKey;
|
||||||
|
if (!zoomKey && !engaged) return; // a plain wheel or a two-finger scroll passes through to the page
|
||||||
|
if (Math.abs(ev.deltaY) < 1) return;
|
||||||
|
ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15);
|
||||||
|
}, { passive: false });
|
||||||
|
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && engaged) engage(false); });
|
||||||
|
document.addEventListener('pointerdown', function (ev) { if (engaged && !canvas.contains(ev.target) && !(chip && chip.contains(ev.target))) engage(false); });
|
||||||
var pinch = null;
|
var pinch = null;
|
||||||
canvas.addEventListener('touchstart', function (ev) { if (ev.touches.length === 2) pinch = dist(ev.touches); }, { passive: true });
|
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('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 });
|
||||||
|
|
@ -216,7 +230,7 @@
|
||||||
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||||
|
|
||||||
readTheme(); size(); if (opts.poll !== false) { poll(); setInterval(poll, 2000); } kick();
|
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 };
|
return { push: push, setPaused: setPaused, zoom: zoom, engage: engage, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
|
||||||
}
|
}
|
||||||
root.IgneumDag = { mount: mount };
|
root.IgneumDag = { mount: mount };
|
||||||
})(window);
|
})(window);
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ main{padding-bottom:var(--sec)}
|
||||||
<div class="state" id="dag-state"><span class="dot off"></span>connecting to the devnet observer</div>
|
<div class="state" id="dag-state"><span class="dot off"></span>connecting to the devnet observer</div>
|
||||||
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span>identities <b id="c-idents">0</b></span><span>last lock <b id="c-locked">pending</b></span><span>proven <b id="c-proven">pending</b></span></div>
|
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span>identities <b id="c-idents">0</b></span><span>last lock <b id="c-locked">pending</b></span><span>proven <b id="c-proven">pending</b></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scene"><canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner lane, parent edges, the selected chain, checkpoint bands with their lock weight, proofs filling blocks as segments are paid"></canvas><div class="tip" id="dag-tip" hidden></div></div>
|
<div class="scene"><canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner lane, parent edges, the selected chain, checkpoint bands with their lock weight, proofs filling blocks as segments are paid"></canvas><div class="tip" id="dag-tip" hidden></div><div class="chip-zoom" id="dag-chip" hidden><span class="dot live"></span>scroll to zoom, Esc to release</div></div>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<span><i class="sw" style="border:2px solid var(--line-2)"></i>pending</span>
|
<span><i class="sw" style="border:2px solid var(--line-2)"></i>pending</span>
|
||||||
<span><i class="sw" style="border:2px solid var(--bone)"></i>included</span><span><i class="sw" style="border:2px solid var(--ember)"></i>selected chain</span>
|
<span><i class="sw" style="border:2px solid var(--bone)"></i>included</span><span><i class="sw" style="border:2px solid var(--ember)"></i>selected chain</span>
|
||||||
|
|
@ -185,7 +185,7 @@ main{padding-bottom:var(--sec)}
|
||||||
<span><i class="sw" style="width:0;height:12px;border-left:2px solid var(--ember);border-radius:0"></i>finality band, locked, with its weight</span>
|
<span><i class="sw" style="width:0;height:12px;border-left:2px solid var(--ember);border-radius:0"></i>finality band, locked, with its weight</span>
|
||||||
<span><i class="sw" style="width:0;height:12px;border-left:2px dashed var(--ember);border-radius:0;opacity:.5"></i>finality band, pending</span>
|
<span><i class="sw" style="width:0;height:12px;border-left:2px dashed var(--ember);border-radius:0;opacity:.5"></i>finality band, pending</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scene-foot"><p class="note" style="margin:0"><span id="c-window">120 s of blocks on screen</span>. One lane per miner, the busiest first; the rest share the last lane. Hover or tap a block for its hash, miner, blue score, parents and proof state. Scroll or pinch to change the window; the page's <code>?window=</code> fixes it.</p><button type="button" class="btn small" id="dag-pause" aria-pressed="false">Pause</button></div>
|
<div class="scene-foot"><p class="note" style="margin:0"><span id="c-window">120 s of blocks on screen</span>. One lane per miner, the busiest first; the rest share the last lane. Hover or tap a block for its hash, miner, blue score, parents and proof state. Ctrl or cmd and scroll, a pinch, or a click into the scene then scroll, changes the window; the page's <code>?window=</code> fixes it.</p><button type="button" class="btn small" id="dag-pause" aria-pressed="false">Pause</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="two">
|
<div class="two">
|
||||||
|
|
@ -293,7 +293,7 @@ main{padding-bottom:var(--sec)}
|
||||||
var dagCanvas=$('dag'),dag=null,lastD=null;
|
var dagCanvas=$('dag'),dag=null,lastD=null;
|
||||||
function windowWord(w){return w+' s of blocks on screen';}
|
function windowWord(w){return w+' s of blocks on screen';}
|
||||||
function mountDag(){if(dag||!dagCanvas||!window.IgneumDag)return;
|
function mountDag(){if(dag||!dagCanvas||!window.IgneumDag)return;
|
||||||
dag=IgneumDag.mount(dagCanvas,{poll:false,window:fixedWindow||120,tooltip:$('dag-tip'),pauseButton:$('dag-pause'),
|
dag=IgneumDag.mount(dagCanvas,{poll:false,window:fixedWindow||120,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'),
|
||||||
onWindow:function(w){$('c-window').textContent=windowWord(w);},
|
onWindow:function(w){$('c-window').textContent=windowWord(w);},
|
||||||
onState:function(st,reason){var el=$('dag-state');if(!el)return;el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet':st==='connecting'?'connecting to the devnet observer':esc(reason||'live feed unavailable'));}});
|
onState:function(st,reason){var el=$('dag-state');if(!el)return;el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet':st==='connecting'?'connecting to the devnet observer':esc(reason||'live feed unavailable'));}});
|
||||||
$('c-window').textContent=windowWord(dag.getWindow());if(lastD)dag.push(lastD);}
|
$('c-window').textContent=windowWord(dag.getWindow());if(lastD)dag.push(lastD);}
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,11 @@ input:focus,select:focus,textarea:focus{border-color:var(--line-2)}
|
||||||
.legend span{display:inline-flex;align-items:center;gap:8px}
|
.legend span{display:inline-flex;align-items:center;gap:8px}
|
||||||
.sw{width:12px;height:12px;border-radius:3px;display:inline-block;flex:none}
|
.sw{width:12px;height:12px;border-radius:3px;display:inline-block;flex:none}
|
||||||
|
|
||||||
|
/* the engaged scene: a chip in the corner while the wheel zooms */
|
||||||
|
.scene canvas.engaged{outline:2px solid var(--ember);outline-offset:-2px}
|
||||||
|
.chip-zoom{position:absolute;right:12px;top:12px;display:inline-flex;align-items:center;gap:8px;background:var(--graphite);border:1px solid var(--line-2);border-radius:var(--r-pill);padding:5px 12px;font:500 var(--t-sm)/1.4 var(--f-mono);color:var(--ink-2);pointer-events:none}
|
||||||
|
.chip-zoom[hidden]{display:none}
|
||||||
|
|
||||||
/* ---------- 8. print ---------- */
|
/* ---------- 8. print ---------- */
|
||||||
@media print{
|
@media print{
|
||||||
:root{--obsidian:#fff;--graphite:#fff;--row:#f3f3f3;--line:#bbb;--line-2:#999;--bone:#000;--ink-2:#111;--ash:#444;--ember:#B8390C;--ember-text:#B8390C;--molten-text:#8F5810}
|
:root{--obsidian:#fff;--graphite:#fff;--row:#f3f3f3;--line:#bbb;--line-2:#999;--bone:#000;--ink-2:#111;--ash:#444;--ember:#B8390C;--ember-text:#B8390C;--molten-text:#8F5810}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue