diff --git a/site/live.html b/site/live.html index 39a7efe90..b007f1625 100644 --- a/site/live.html +++ b/site/live.html @@ -176,20 +176,19 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px}
block dag
-
on screen 0chain 0side 0pending 0newest blue 0locked none
+
on screen 0chain 0side 0newest blue 0locked none
chain block included (blue): paid pending - excluded (red) + excluded (red) locked checkpoint, final just arrived selected chain first parent - 12:34ruler, a tick every 5 s - 5d3fd79cring colour = miner; the gutter counts each miner's blocks on screen + 5d3fd79cone lane per miner, colour from its id hover or tap a block for its id, scores and parents
@@ -264,29 +263,29 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px} function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>');} // ---------- DAG scene ---------- - // One canvas, one requestAnimationFrame loop. The selected chain runs along one horizontal rail; side blocks hang - // above and below it in time order, packed into rows so none overlap. The miner is the ring colour; the left gutter - // lists the active miners with their block counts. A time ruler sits under the blocks. Edges draw under blocks. - var c=$('dag'),x=c.getContext('2d'),W=0,H=0,S=22,G=104,narrow=false,level=0; - var blocks=[],byHash={},miners={},minerList=[],rail=0,maxRows=1,rowGap=33; - var LV=[{S:22,s:14,pps:40,npps:14},{S:16,s:12,pps:26,npps:11},{S:12,s:10,pps:18,npps:9}]; + // 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:14},{S:16,s:12,pps:26,npps:10},{S:12,s:10,pps:18,npps:8}]; 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,pin=null,above=[],below=[],crow=[],CO=[0,-1.1,1.1],tickLabels={},tickCount=0; - var TOP=18,BOT=4,RUL=20,SLIDE=450; + 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?54:104;applyLevel(); + 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; - rowGap=S*1.5;rail=TOP+(H-TOP-RUL-BOT)/2; - maxRows=Math.max(1,Math.floor((rail-TOP-S*2.8)/rowGap)+1); + 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 @@ -294,6 +293,16 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px} 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)};}m.n++;}); - minerList=[];for(id in miners){if(!miners[id].n)delete miners[id];else minerList.push(miners[id]);} - minerList.sort(function(a,b){return b.n-a.n||(a.idvis){on++;if(b.chain)chain++;else if(b.color==='pending')pend++;}}); - $('c-screen').textContent=on;$('c-chain').textContent=chain;$('c-side').textContent=on-chain;$('c-pending').textContent=pend; + 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();} - // a cubic with horizontal tangents; the control offset is clamped so a link across many rows reads as a gentle S, never a loop + // 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; @@ -326,40 +334,30 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px} for(var i=0;i240){tickLabels={};tickCount=0;}var d=new Date(tt);s=(d.getMinutes()<10?'0':'')+d.getMinutes()+':'+(d.getSeconds()<10?'0':'')+d.getSeconds();tickLabels[tt]=s;tickCount++;return s;} function draw(now){ 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; - // positions: time on x, the chain on the rail; side blocks hang above or below it in time order, packed into rows - above.length=0;below.length=0;crow.length=0; - for(i=0;iG-S;} + 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;iG-S;} x.clearRect(0,0,W,H); - var bottom=H-RUL-BOT; // 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,bottom);x.strokeStyle='rgba(255,179,92,0.55)';x.lineWidth=1;x.setLineDash([4,5]);x.beginPath();x.moveTo(fx,4);x.lineTo(fx,bottom);x.stroke();x.setLineDash([]);} - // the rail - x.strokeStyle='rgba(58,58,66,0.9)';x.lineWidth=1;x.beginPath();x.moveTo(G,Math.round(rail)+0.5);x.lineTo(W,Math.round(rail)+0.5);x.stroke(); - // time ruler: a tick every 5 s, labelled with the clock, "now" at the right edge - var ry=H-RUL+0.5;x.strokeStyle='rgba(58,58,66,0.9)';x.beginPath();x.moveTo(G,ry);x.lineTo(W,ry);x.stroke(); - x.font='500 9px IBM Plex Mono, monospace';x.textAlign='center';x.fillStyle='rgba(154,154,158,0.75)';x.strokeStyle='rgba(90,90,98,0.9)';x.beginPath(); - var every=narrow?10000:5000,tt=Math.floor(t/5000)*5000,labelFrom=W-34; - for(;;tt-=5000){var xx=xr-(t-tt)*pxPerSec/1000;if(xxG+16)x.fillText(tickLabel(tt),xx,H-2);} - x.stroke();x.textAlign='right';x.fillStyle='rgba(255,179,92,0.85)';x.fillText('now',W-3,H-2); - x.save();x.beginPath();x.rect(G,0,W-G,bottom);x.clip(); + 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 along the rail + // 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(); @@ -368,36 +366,34 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px} if(hv){x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1.5;x.beginPath();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=mc;x.lineWidth=1.5;rrect(b.px-h-1,b.py-h-1,S+2,S+2,r+1);x.stroke();tip=b;} - else if(b.color==='blue'){x.fillStyle=mc+'B3';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':mc;x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();} - else if(b.color==='red'){x.fillStyle='rgba(242,84,27,0.22)';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle='rgba(242,84,27,0.5)';x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();} - 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(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 if(b.color==='blue'){var mc=m?m.col:'#9A9A9E';x.fillStyle=mc+'B3';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':mc;x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();} + 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)':(b.color==='red'?'rgba(154,154,158,0.6)':'rgba(154,154,158,0.85)');x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke(); + if(b.color==='red'){x.strokeStyle='rgba(154,154,158,0.9)';x.lineWidth=1.5;x.beginPath();x.moveTo(b.px-h*0.5,b.py+h*0.5);x.lineTo(b.px+h*0.5,b.py-h*0.5);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';x.fillText(tip.hash.slice(0,8),Math.min(W-4,tip.px+h+2),tip.py-h-6);} + 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':hv.color==='blue'?'included, paid':hv.color==='red'?'excluded':'pending'),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,tx2=hv.px+h+10,tty=hv.py-th/2; - if(tx2+tw>W-4)tx2=hv.px-h-10-tw;if(tx2W-4)tx=hv.px-h-10-tw;if(txfit?fit-1:minerList.length; - for(i=0;i