Live page: block DAG redrawn for 20 miners at 4 blocks/s
One lane per miner ranked by share, overflow in an others lane with a count. Lane labels in a left gutter in the miner's colour (3-character tag on phones). No per-block labels: short id on the chain tip, the lock, and on hover or tap with a canvas tooltip (id, blue, DAA, parents, chain or side). Same-lane overlaps nudged in a fixed sequence. Chain as one path, side blocks linked to their first parent only, clamped cubics so no edge becomes a tall loop. Block size and time scale step down together at density. One rAF loop, DPR aware, paused when hidden or scrolled out. Final band and lock marker. Before and after screenshots in docs/design/live-dag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BIN
docs/design/live-dag/after-replay-1bps-desktop.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
docs/design/live-dag/after-replay-4bps-20miners-desktop.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
BIN
docs/design/live-dag/after-replay-4bps-20miners-mobile.png
Normal file
|
After Width: | Height: | Size: 919 KiB |
BIN
docs/design/live-dag/after-replay-hover-tooltip.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
docs/design/live-dag/after-replay-tap-tooltip-mobile.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/design/live-dag/before-desktop.png
Normal file
|
After Width: | Height: | Size: 547 KiB |
BIN
docs/design/live-dag/before-mobile.png
Normal file
|
After Width: | Height: | Size: 427 KiB |
179
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}
|
|||
<div class="legend">
|
||||
<span><i class="sw" style="background:var(--ember)"></i>chain block</span>
|
||||
<span><i class="sw" style="border:2px solid var(--ash)"></i>side block (blue or red, off the selected chain)</span>
|
||||
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint, final</span>
|
||||
<span><i class="sw" style="border:2px solid var(--molten);border-radius:50%;background:transparent"></i>locked checkpoint, final</span>
|
||||
<span><i class="sw" style="border:2px solid var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)"></i>just arrived</span>
|
||||
<span><i class="sw" style="width:22px;height:0;border-top:1px solid var(--ash)"></i>parent edge</span>
|
||||
<span><span class="mono" style="color:var(--molten)">2ea5116e</span>miner short id, one lane each</span>
|
||||
<span><i class="sw" style="width:22px;height:0;border-top:2px solid var(--ember)"></i>selected chain</span>
|
||||
<span><i class="sw" style="width:22px;height:0;border-top:1px solid var(--ash)"></i>first parent of a side block</span>
|
||||
<span><span class="mono" style="color:#5CB8FF">5d3fd79c</span>one lane per miner, colour from its id</span>
|
||||
<span style="color:var(--ash)">hover or tap a block for its id, scores and parents</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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,'<').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<id.length;i++)v=(v*33+id.charCodeAt(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&&proj<down[level-1])next=level-1;
|
||||
if(next!==level){level=next;applyLevel();}
|
||||
}
|
||||
function layoutLanes(){
|
||||
// rank miners by blocks in the window; one lane each, the overflow shares the last lane with a count
|
||||
var ids=Object.keys(miners);
|
||||
ids.sort(function(a,b){return miners[b].n-miners[a].n||(a<b?-1:1);});
|
||||
laneN=Math.max(1,Math.min(maxLanes,ids.length));
|
||||
othersN=ids.length>laneN?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||l<laneN-1)lanes[l]=m;});
|
||||
while(laneLast.length<laneN){laneLast.push(0);laneRun.push(0);}
|
||||
}
|
||||
function ingest(d){
|
||||
var now=performance.now();serverNow=new Date(d.now).getTime();serverAt=now;
|
||||
var seen={};
|
||||
d.blocks.forEach(function(b){seen[b.hash]=1;var e=byHash[b.hash];
|
||||
d.blocks.forEach(function(b){var e=byHash[b.hash];
|
||||
if(e){e.chain=b.chain;e.locked=!!b.locked;e.final=!!b.final;return;}
|
||||
laneFor(b.miner);
|
||||
e={hash:b.hash,ts:b.ts,rx:b.rx,blue_score:b.blue_score,parents:b.parents,chain:b.chain,miner:b.miner||'?',born:now,j:jitter(b.hash),locked:!!b.locked,final:!!b.final};
|
||||
e={hash:b.hash,ts:b.ts,blue_score:b.blue_score,daa:b.daa,parents:b.parents||[],chain:b.chain,miner:b.miner||'?',born:now,j:jitter(b.hash),locked:!!b.locked,final:!!b.final,px:0,py:0,on:false};
|
||||
byHash[b.hash]=e;blocks.push(e);});
|
||||
// drop what fell off the left edge long ago
|
||||
var cut=vnow()-(W/pxPerSec)*1000-20000;
|
||||
var cut=vnow()-((W-G)/Math.max(8,pxPerSec))*1000-20000;
|
||||
blocks=blocks.filter(function(b){var keep=b.ts>cut;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;i<blocks.length;i++){var b=blocks[i];if(!b.on)continue;var dx=Math.abs(b.px-qx),dy=Math.abs(b.py-qy);if(dx<bd&&dy<bd&&dx+dy<bd*1.5){best=b;bd=Math.max(dx,dy);}}
|
||||
return best;
|
||||
}
|
||||
function draw(now){
|
||||
pxPerSec+=(targetPx-pxPerSec)*0.05;
|
||||
var t=vnow();
|
||||
if(needSize)size();if(!W||!H)return;
|
||||
pxPerSec+=(targetPx-pxPerSec)*0.06;
|
||||
var t=vnow(),xr=W-S,i,b,m,n=blocks.length,id;
|
||||
for(id in miners){m=miners[id];var ty=laneY(m.lane);m.y+=(ty-m.y)*0.12;if(Math.abs(ty-m.y)<0.05)m.y=ty;}
|
||||
for(i=0;i<laneN;i++){laneLast[i]=-1e9;laneRun[i]=0;}
|
||||
// positions: time on x, lane on y; same-lane neighbours closer than a block are nudged up and down in a fixed sequence
|
||||
for(i=0;i<n;i++){b=blocks[i];m=miners[b.miner];if(!m)continue;var l=m.lane;b.px=Math.min(xr,xr-(t-b.ts)*pxPerSec/1000);
|
||||
if(b.px-laneLast[l]<S*1.25)laneRun[l]++;else laneRun[l]=0;laneLast[l]=b.px;
|
||||
var k=laneRun[l]%3,dy=k===0?0:(k===1?1:-1)*S*0.62;b.py=m.y+dy;b.on=b.px>G-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;l<n;l++){var y=laneY(l);x.strokeStyle='rgba(42,42,48,0.7)';x.lineWidth=1;x.beginPath();x.moveTo(0,y);x.lineTo(W,y);x.stroke();
|
||||
var names=laneOrder.filter(function(m,i){return i%n===l;});if(names.length){x.fillStyle='rgba(154,154,158,0.75)';x.fillText(names.slice(0,2).join(' ')+(names.length>2?' +'+(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;i<n;i++){b=blocks[i];if(b.locked&&(!lk||b.ts>lk.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;i<laneN;i++){var y=Math.round(laneY(i))+0.5;x.moveTo(G,y);x.lineTo(W,y);}x.stroke();
|
||||
x.save();x.beginPath();x.rect(G,0,W-G,H);x.clip();
|
||||
// side links: each side block to its first parent on screen, faint and short
|
||||
x.strokeStyle='rgba(154,154,158,0.3)';x.lineWidth=1;x.beginPath();
|
||||
for(i=0;i<n;i++){b=blocks[i];if(b.chain||!b.on)continue;var q=byHash[b.parents[0]];if(q&&q.px>G-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;i<n;i++){b=blocks[i];if(!b.chain)continue;if(prev&&b.px>G-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;i<hv.parents.length;i++){var pq=byHash[hv.parents[i]];if(pq)link(pq,hv);}x.stroke();}
|
||||
// blocks
|
||||
blocks.forEach(function(b){var p=pos[b.hash],h=S/2,age=now-b.born,fl=Math.max(0,1-age/900);
|
||||
if(fl>0){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;i<n;i++){b=blocks[i];if(!b.on)continue;m=miners[b.miner];var age=now-b.born,fl=age<900?1-age/900:0;
|
||||
if(fl>0){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<G+4)tx=G+4;tty=Math.max(4,Math.min(H-th-4,tty));
|
||||
x.fillStyle='rgba(22,22,26,0.96)';rrect(tx,tty,tw,th,6);x.fill();x.strokeStyle=miners[hv.miner]?miners[hv.miner].col:'#3A3A42';x.lineWidth=1;rrect(tx+0.5,tty+0.5,tw-1,th-1,6);x.stroke();
|
||||
x.textAlign='left';x.fillStyle='#F4F1EC';x.fillText(l1,tx+10,tty+16);x.fillStyle='#C9C7C2';x.fillText(l2,tx+10,tty+31);x.fillStyle='#9A9A9E';x.fillText(l3,tx+10,tty+46);}
|
||||
x.restore();
|
||||
// gutter: lane labels in the miner's colour, over anything that scrolled under it
|
||||
x.fillStyle='#0C0C0E';x.fillRect(0,0,G,H);x.strokeStyle='rgba(42,42,48,0.9)';x.lineWidth=1;x.beginPath();x.moveTo(G-0.5,0);x.lineTo(G-0.5,H);x.stroke();
|
||||
x.textAlign='left';x.font='500 '+(narrow?10:11)+'px IBM Plex Mono, monospace';
|
||||
for(i=0;i<laneN;i++){var ly2=laneY(i)+4,lm=lanes[i];
|
||||
if(lm){x.fillStyle=lm.col;x.fillText(narrow?lm.tag:lm.id,8,ly2);}
|
||||
else{x.fillStyle='rgba(154,154,158,0.9)';x.fillText(narrow?'+'+othersN:'+'+othersN+' more',8,ly2);}}
|
||||
if(!n){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',G+(W-G)/2,H/2);}
|
||||
if(note){x.fillStyle='rgba(12,12,14,0.72)';x.fillRect(0,H/2-22,W,44);x.fillStyle='#F4F1EC';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(note,W/2,H/2+5);}
|
||||
}
|
||||
function frame(now){draw(now);if(!reduce)requestAnimationFrame(frame);}
|
||||
requestAnimationFrame(frame);
|
||||
function frame(now){if(!running)return;draw(now);requestAnimationFrame(frame);}
|
||||
function start(){if(running||reduce)return;if(document.hidden||!inView)return;running=true;requestAnimationFrame(frame);}
|
||||
function stop(){running=false;}
|
||||
function kick(){if(!running)requestAnimationFrame(function(now){draw(now);});}
|
||||
document.addEventListener('visibilitychange',function(){if(document.hidden)stop();else start();});
|
||||
if('IntersectionObserver' in window){new IntersectionObserver(function(es){inView=es[0].isIntersecting;if(inView)start();else stop();},{threshold:0}).observe(c);}
|
||||
if('ResizeObserver' in window){new ResizeObserver(function(){needSize=true;kick();}).observe(c);}
|
||||
else window.addEventListener('resize',function(){needSize=true;kick();});
|
||||
c.addEventListener('pointermove',function(e){if(e.pointerType==='touch')return;ptrX=e.offsetX;ptrY=e.offsetY;pin=null;if(reduce)kick();});
|
||||
c.addEventListener('pointerleave',function(){ptrX=-1;ptrY=-1;if(reduce)kick();});
|
||||
c.addEventListener('pointerdown',function(e){if(e.pointerType!=='touch')return;ptrX=e.offsetX;ptrY=e.offsetY;var old=pin;pin=null;var b=hit();pin=b&&b.hash!==old?b.hash:null;ptrX=-1;ptrY=-1;if(reduce)kick();});
|
||||
start();
|
||||
|
||||
// ---------- sparkline ----------
|
||||
var sc=$('spark'),sx=sc.getContext('2d');
|
||||
|
|
|
|||