diff --git a/docs/design/live-dag/after-replay-1bps-desktop.png b/docs/design/live-dag/after-replay-1bps-desktop.png new file mode 100644 index 00000000..cf4f0f83 Binary files /dev/null and b/docs/design/live-dag/after-replay-1bps-desktop.png differ diff --git a/docs/design/live-dag/after-replay-4bps-20miners-desktop.png b/docs/design/live-dag/after-replay-4bps-20miners-desktop.png new file mode 100644 index 00000000..1aac2638 Binary files /dev/null and b/docs/design/live-dag/after-replay-4bps-20miners-desktop.png differ diff --git a/docs/design/live-dag/after-replay-4bps-20miners-mobile.png b/docs/design/live-dag/after-replay-4bps-20miners-mobile.png new file mode 100644 index 00000000..91b58f00 Binary files /dev/null and b/docs/design/live-dag/after-replay-4bps-20miners-mobile.png differ diff --git a/docs/design/live-dag/after-replay-hover-tooltip.png b/docs/design/live-dag/after-replay-hover-tooltip.png new file mode 100644 index 00000000..c472d3f4 Binary files /dev/null and b/docs/design/live-dag/after-replay-hover-tooltip.png differ diff --git a/docs/design/live-dag/after-replay-tap-tooltip-mobile.png b/docs/design/live-dag/after-replay-tap-tooltip-mobile.png new file mode 100644 index 00000000..1d62c009 Binary files /dev/null and b/docs/design/live-dag/after-replay-tap-tooltip-mobile.png differ diff --git a/docs/design/live-dag/before-desktop.png b/docs/design/live-dag/before-desktop.png new file mode 100644 index 00000000..85b15ec1 Binary files /dev/null and b/docs/design/live-dag/before-desktop.png differ diff --git a/docs/design/live-dag/before-mobile.png b/docs/design/live-dag/before-mobile.png new file mode 100644 index 00000000..dbf6b429 Binary files /dev/null and b/docs/design/live-dag/before-mobile.png differ diff --git a/site/live.html b/site/live.html index e580cd0b..da9adc03 100644 --- a/site/live.html +++ b/site/live.html @@ -93,7 +93,7 @@ p{margin:0} .viz-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px 20px;margin-bottom:12px} .viz-stats{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:12px;color:var(--ash)} .viz-stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums} -#dag{display:block;width:100%;height:clamp(240px,38vw,380px);border-radius:12px;background:var(--obsidian)} +#dag{display:block;width:100%;height:clamp(240px,38vw,380px);border-radius:12px;background:var(--obsidian);touch-action:pan-y} .legend{display:flex;flex-wrap:wrap;gap:16px 20px;margin-top:16px;font-size:13px;color:var(--ink-2)} .legend span{display:inline-flex;align-items:center;gap:8px} .sw{width:14px;height:14px;border-radius:4px;display:inline-block} @@ -182,10 +182,12 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px}
chain block side block (blue or red, off the selected chain) - locked checkpoint, final + locked checkpoint, final just arrived - parent edge - 2ea5116eminer short id, one lane each + selected chain + first parent of a side block + 5d3fd79cone lane per miner, colour from its id + hover or tap a block for its id, scores and parents
@@ -259,66 +261,149 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px} function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>');} // ---------- DAG scene ---------- - var c=$('dag'),x=c.getContext('2d'),W,H,S,blocks=[],byHash={},lanesOf={},laneOrder=[],maxLanes=6,pxPerSec=40,targetPx=40; - var live=false,serverNow=0,serverAt=0,frozenAt=null,note='',bps=0; - function size(){W=c.clientWidth;H=c.clientHeight;c.width=W*dpr;c.height=H*dpr;x.setTransform(dpr,0,0,dpr,0,0);S=W<600?16:22;maxLanes=H<260?4:6;} - size();window.addEventListener('resize',function(){size();draw(performance.now());}); - function laneCount(){return Math.max(1,Math.min(maxLanes,laneOrder.length));} - function laneFor(m){m=m||'?';if(!(m in lanesOf)){lanesOf[m]=laneOrder.length;laneOrder.push(m);}return lanesOf[m]%laneCount();} - function laneY(l){var n=laneCount();if(n===1)return H*0.5;return H*0.16+l*(H*0.62/(n-1));} - function jitter(h){var v=0;for(var i=0;i<8;i++)v=(v*31+h.charCodeAt(i))%1000;return (v/1000-0.5)*S*0.5;} + // One canvas, one requestAnimationFrame loop. Lanes: one per miner in the visible window, most blocks on top, + // the least active share an "others" lane. Labels live in a left gutter, never on blocks. Edges go under blocks. + var c=$('dag'),x=c.getContext('2d'),W=0,H=0,S=22,G=78,narrow=false,level=0,maxLanes=8; + var blocks=[],byHash={},miners={},lanes=[],laneN=1,othersN=0; + var LV=[{S:22,s:14,pps:40,npps:28},{S:16,s:12,pps:26,npps:20},{S:12,s:10,pps:18,npps:14}]; + var pxPerSec=40,targetPx=40,bps=0,live=false,serverNow=0,serverAt=0,frozenAt=null,note=''; + var running=false,needSize=true,inView=true,ptrX=-1,ptrY=-1,ptrTouch=false,pin=null,laneLast=[],laneRun=[]; + var TOP=20,BOT=12; + // 12 hues that sit beside ember on obsidian; the index comes from the short id, so a miner keeps its colour across reloads + var PAL=['#5CB8FF','#3FD39A','#F5C542','#C49BFF','#FF7AA8','#8FD15F','#FFB08A','#6F8CFF','#D9E25A','#FF8FD6','#4FD8D8','#E0A96D']; + function colour(id){var v=0;for(var i=0;i>>0;return PAL[v%PAL.length];} + function jitter(h){var v=0;for(var i=0;i<8;i++)v=(v*31+h.charCodeAt(i))%1000;return v/1000-0.5;} function vnow(){if(frozenAt!==null)return frozenAt;return serverNow+(performance.now()-serverAt);} + function laneY(l){var u=H-TOP-BOT,p=u/Math.max(1,laneN);return TOP+(l+0.5)*p;} + function size(){ + needSize=false;W=c.clientWidth;H=c.clientHeight;if(!W||!H)return; + c.width=Math.round(W*dpr);c.height=Math.round(H*dpr);x.setTransform(dpr,0,0,dpr,0,0); + narrow=W<560;G=narrow?36:80;applyLevel(); + } + function applyLevel(){ + var L=LV[level];S=narrow?L.s:L.S;targetPx=narrow?L.npps:L.pps; + maxLanes=Math.max(2,Math.min(14,Math.floor((H-TOP-BOT)/(S*1.7)))); + layoutLanes(); + } + function pickLevel(){ + // projected blocks on screen at the base scale drives the step, with hysteresis so it never flickers + var proj=bps*(W-G)/(narrow?LV[0].npps:LV[0].pps),up=[48,110],down=[36,85],next=level; + if(level<2&&proj>up[level])next=level+1;else if(level>0&&projlaneN?ids.length-(laneN-1):0; + lanes=[]; + ids.forEach(function(id,i){var m=miners[id],l=othersN&&i>=laneN-1?laneN-1:i;m.lane=l;if(m.y===undefined)m.y=laneY(l);if(!othersN||lcut;if(!keep)delete byHash[b.hash];return keep;}); blocks.sort(function(a,b){return a.ts-b.ts;}); + for(var id in miners)miners[id].n=0; + blocks.forEach(function(b){var m=miners[b.miner];if(!m){m=miners[b.miner]={id:b.miner,n:0,col:colour(b.miner),tag:b.miner.slice(0,3),lane:0};}m.n++;}); + for(id in miners)if(!miners[id].n)delete miners[id]; bps=d.state.blocks_per_second_60s||0; - targetPx=Math.max(S*0.4,Math.min(420,bps*S*1.7)); - var chain=0;blocks.forEach(function(b){if(b.chain)chain++;}); - $('c-screen').textContent=blocks.length;$('c-chain').textContent=chain;$('c-side').textContent=blocks.length-chain; + pickLevel();layoutLanes(); + var vis=vnow()-((W-G)/Math.max(8,targetPx))*1000,on=0,chain=0; + blocks.forEach(function(b){if(b.ts>vis){on++;if(b.chain)chain++;}}); + $('c-screen').textContent=on;$('c-chain').textContent=chain;$('c-side').textContent=on-chain; $('c-newest').textContent=blocks.length?compact(blocks[blocks.length-1].blue_score):'0'; var f=d.finality;$('c-locked').textContent=f&&f.latest_locked_index?('#'+f.latest_locked_index+' at blue '+compact(f.latest_locked_blue_score)):(f&&f.supported?'none yet':'n/a'); + if(!running&&!document.hidden)kick(); } function rrect(px,py,w,h,r){x.beginPath();x.moveTo(px+r,py);x.lineTo(px+w-r,py);x.quadraticCurveTo(px+w,py,px+w,py+r);x.lineTo(px+w,py+h-r);x.quadraticCurveTo(px+w,py+h,px+w-r,py+h);x.lineTo(px+r,py+h);x.quadraticCurveTo(px,py+h,px,py+h-r);x.lineTo(px,py+r);x.quadraticCurveTo(px,py,px+r,py);x.closePath();} - function xOf(b,t){return Math.min(W-S,W-S*1.2-(t-b.ts)*pxPerSec/1000);} + // a cubic with horizontal tangents; the control offset is clamped so a link across many lanes reads as a gentle S, never a loop + function link(a,b){var d=Math.max(S*0.6,Math.min(48,Math.abs(b.px-a.px)*0.5));x.moveTo(a.px,a.py);x.bezierCurveTo(a.px+d,a.py,b.px-d,b.py,b.px,b.py);} + function hit(){ + var best=null,bd=S*0.9,qx=ptrX,qy=ptrY; + if(pin){var p=byHash[pin];return p&&p.on?p:null;} + if(qx<0)return null; + for(var i=0;iG-S;} x.clearRect(0,0,W,H); - // lane rails and miner labels - var n=laneCount();x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left'; - for(var l=0;l2?' +'+(names.length-2):''),8,y-S*0.75);}} - var pos={};blocks.forEach(function(b){pos[b.hash]={x:xOf(b,t),y:laneY(laneFor(b.miner))+b.j};}); - // edges - blocks.forEach(function(b){var p=pos[b.hash];b.parents.forEach(function(h){var q=pos[h];if(!q)return;var both=b.chain&&byHash[h]&&byHash[h].chain; - x.strokeStyle=both?'rgba(242,84,27,0.75)':'rgba(201,199,194,0.45)';x.lineWidth=both?2:1.25;x.beginPath();x.moveTo(p.x-S/2,p.y);var mx=(p.x+q.x)/2;x.bezierCurveTo(mx,p.y,mx,q.y,q.x+S/2,q.y);x.stroke();});}); - // lock line: everything left of the newest locked checkpoint is final (mirrors the homepage simulation) - var lk=null;blocks.forEach(function(b){if(b.locked&&(!lk||b.ts>lk.ts))lk=b;}); - if(lk){var lp=pos[lk.hash];x.strokeStyle='rgba(242,84,27,0.55)';x.lineWidth=1.5;x.setLineDash([5,6]);x.beginPath();x.moveTo(lp.x,6);x.lineTo(lp.x,H-6);x.stroke();x.setLineDash([]);x.fillStyle='rgba(154,154,158,0.9)';x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillText('final',lp.x-8,14);} + // final region: everything left of the newest locked checkpoint + var lk=null,fx=-1; + for(i=0;ilk.ts))lk=b;if(b.final&&b.px>fx)fx=b.px;} + if(lk)fx=lk.px; + if(fx>G){x.fillStyle='rgba(242,84,27,0.06)';x.fillRect(G,0,fx-G,H);x.strokeStyle='rgba(255,179,92,0.55)';x.lineWidth=1;x.setLineDash([4,5]);x.beginPath();x.moveTo(fx,4);x.lineTo(fx,H-4);x.stroke();x.setLineDash([]);} + // lane rails + x.strokeStyle='rgba(42,42,48,0.75)';x.lineWidth=1;x.beginPath();for(i=0;iG-S*2)link(q,b);} + x.stroke(); + // the selected chain as one path through the chain blocks + x.strokeStyle=level?'rgba(242,84,27,0.6)':'rgba(242,84,27,0.85)';x.lineWidth=narrow||level?1.5:2;x.beginPath();var prev=null; + for(i=0;iG-S*2)link(prev,b);prev=b;} + x.stroke(); + // hovered block: all of its parent edges, bright + var hv=hit(); + if(hv){x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1.5;x.beginPath();for(i=0;i0){var g=x.createRadialGradient(p.x,p.y,0,p.x,p.y,S*1.8);g.addColorStop(0,'rgba(255,179,92,'+(0.55*fl)+')');g.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g;x.beginPath();x.arc(p.x,p.y,S*1.8,0,Math.PI*2);x.fill();} - if(b.chain){x.fillStyle='#F2541B';rrect(p.x-h,p.y-h,S,S,S*0.24);x.fill();} - else{x.fillStyle='#0C0C0E';rrect(p.x-h,p.y-h,S,S,S*0.24);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':'rgba(154,154,158,0.9)';x.lineWidth=2;rrect(p.x-h,p.y-h,S,S,S*0.24);x.stroke();} - if(fl>0&&b.chain){x.strokeStyle='rgba(255,179,92,'+fl+')';x.lineWidth=2;rrect(p.x-h-2,p.y-h-2,S+4,S+4,S*0.3);x.stroke();} - if(b.locked){x.strokeStyle='#F2541B';x.lineWidth=3;x.beginPath();x.arc(p.x,p.y,S*0.95,0,Math.PI*2);x.stroke();} - x.fillStyle=b.chain?'rgba(244,241,236,0.9)':'rgba(154,154,158,0.9)';x.font='500 '+(S<20?8:9)+'px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(b.miner,p.x,p.y+h+11);}); - // tip marker - if(blocks.length){var tip=blocks[blocks.length-1],tp=pos[tip.hash];x.fillStyle='rgba(154,154,158,0.9)';x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillText('tip',Math.min(W-6,tp.x+S*0.5),Math.max(12,tp.y-S*0.75));} - if(!blocks.length){x.fillStyle='rgba(154,154,158,0.9)';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(live?'waiting for the first block':'no observer data',W/2,H/2);} + var h=S/2,r=S*0.24,tip=null; + for(i=0;i0){var g=x.createRadialGradient(b.px,b.py,0,b.px,b.py,S*1.6);g.addColorStop(0,'rgba(255,179,92,'+(0.5*fl)+')');g.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g;x.beginPath();x.arc(b.px,b.py,S*1.6,0,Math.PI*2);x.fill();} + if(b.chain){x.fillStyle='#F2541B';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=m?m.col:'#9A9A9E';x.lineWidth=1.5;rrect(b.px-h-1,b.py-h-1,S+2,S+2,r+1);x.stroke();tip=b;} + else{x.fillStyle='#0C0C0E';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':'rgba(154,154,158,0.85)';x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();} + if(fl>0&&b.chain){x.strokeStyle='rgba(255,179,92,'+fl+')';x.lineWidth=2;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();} + if(b.locked){x.strokeStyle='#FFB35C';x.lineWidth=2.5;x.beginPath();x.arc(b.px,b.py,S*0.95,0,Math.PI*2);x.stroke();} + if(hv===b){x.strokeStyle='#F4F1EC';x.lineWidth=1.5;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();}} + x.font='500 10px IBM Plex Mono, monospace'; + // the chain tip carries its short id; nothing else is labelled unless hovered, pinned or locked + if(tip){x.fillStyle='rgba(244,241,236,0.92)';x.textAlign='right';var ly=tip.py-h-6;if(ly<10){ly=tip.py+h+12;}x.fillText(tip.hash.slice(0,8),Math.min(W-4,tip.px+h+2),ly);} + if(fx>G){var ft=lk?'final #'+lk.hash.slice(0,8):'final',fw=x.measureText(ft).width;x.fillStyle='#FFB35C'; + if(fx-G>fw+14){x.textAlign='right';x.fillText(ft,fx-6,12);}else{x.textAlign='left';x.fillText(ft,fx+6,12);}} + // tooltip on the canvas, no DOM + if(hv){var l1=hv.hash.slice(0,8)+(hv.locked?' locked':(hv.chain?' chain':' side')),l2='blue '+compact(hv.blue_score)+' daa '+compact(hv.daa),l3=hv.parents.length+(hv.parents.length===1?' parent':' parents')+' '+hv.miner; + var tw=Math.max(x.measureText(l1).width,x.measureText(l2).width,x.measureText(l3).width)+20,th=52,tx=hv.px+h+10,tty=hv.py-th/2; + if(tx+tw>W-4)tx=hv.px-h-10-tw;if(tx