diff --git a/site/live.html b/site/live.html index dd121c0c9..b53a0a916 100644 --- a/site/live.html +++ b/site/live.html @@ -238,9 +238,9 @@ main{padding-bottom:var(--sec)}
-
blockschainincluded, paidpendingexcludedjust arrivedselected chainone lane per miner, colour from its id; hover or tap a block
+
blockschainincluded, paidpendingexcludedjust arrivedselected chainone lane per miner, colour from its id; hover a block, or tap it to keep its details up
finalityfinallocked checkpointproposedreads "paused" whenever under two thirds of the weight is signing
-
provingshard plannedprovingverified, prover colourpaidwhere proofs land behind the tiphover or tap a cell
+
provingshard plannedprovingverified, prover colourpaidwhere proofs land behind the tiphover a cell, or tap it to keep its details up
@@ -325,7 +325,10 @@ main{padding-bottom:var(--sec)} var pxPerSec=40,targetPx=40,bps=0,live=false,serverNow=0,serverAt=0,frozenAt=null,note=''; // ?window=N (30 to 300 s): a fixed long view for diagnosis, the API returns the same span; the default stays the adaptive scale var fixedWindow=Math.max(0,Math.min(300,Number(new URLSearchParams(location.search).get('window'))||0));if(fixedWindow&&fixedWindow<30)fixedWindow=30; - var running=false,needSize=true,inView=true,ptrX=-1,ptrY=-1,pin=null,laneLast=[],laneRun=[],lagNote=''; + var running=false,needSize=true,inView=true,ptrX=-1,ptrY=-1,laneLast=[],laneRun=[],lagNote=''; + // pin: a tapped or clicked block (i = -1) or shard cell (i = shard index). It stays until a tap or click lands elsewhere, + // Escape, or the block leaves the strip (then pinFade holds it for a 200 ms fade). Hover never touches it. + var pin=null,pinFade=null,PIN_FADE=200,closeRect=null,panelRect=null,down=null,TAP_SLOP=10; var TOP=20,BOT=12; // 12 hues that sit beside ember on obsidian; the index comes from the short id, so a miner (or a prover) keeps its colour across reloads var PAL=['#5CB8FF','#3FD39A','#F5C542','#C49BFF','#FF7AA8','#8FD15F','#FFB08A','#6F8CFF','#D9E25A','#FF8FD6','#4FD8D8','#E0A96D']; @@ -393,20 +396,41 @@ main{padding-bottom:var(--sec)} 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);} // shard cells: a column under each chain block, shard 0 at the top; sizes from the zoom level, never wider than the block function cellGeom(n){var avail=PROV_H-12,gap=2,ch=Math.min(narrow?7:9,Math.floor((avail-(n-1)*gap)/Math.max(1,n))),cw=Math.max(3,Math.min(narrow?10:12,Math.round(S*0.7),Math.round(pxPerSec*0.85)));return {ch:ch,cw:cw,gap:gap,top:BH+FIN_H+6+Math.max(0,Math.floor((avail-(n*ch+(n-1)*gap))/2))};} - function hit(){ - // returns {b, i}: a block (i = -1) in the blocks strip, or a shard cell (i = shard index) in the proving strip - var qx=ptrX,qy=ptrY; - if(pin){var p=byHash[pin.h];return p&&p.on?{b:p,i:pin.i}:null;} + function hitAt(qx,qy,touch){ + // returns {b, i}: a block (i = -1) in the blocks strip, or a shard cell (i = shard index) in the proving strip. + // A finger is wider than a pointer: a touch gets a larger radius, the nearest target still wins; hover is unchanged if(qx<0)return null; - if(qy=BH+FIN_H&&provOn()){var bb=null,bdx=1e9; for(i=0;iMath.max(g.cw,S*0.5))return null; - var row=Math.floor((qy-g.top)/(g.ch+g.gap));if(row<0||row>=bb.shards.length)return null;return {b:bb,i:row};} + if(!bb)return null;var g=cellGeom(bb.shards.length),ns=bb.shards.length;if(bdx>Math.max(g.cw,S*0.5)*(touch?1.8:1))return null; + var row=Math.floor((qy-g.top)/(g.ch+g.gap)); + if(touch&&(row<0||row>=ns)&&qy>=g.top-8&&qy<=g.top+ns*(g.ch+g.gap)+8)row=Math.max(0,Math.min(ns-1,row)); + if(row<0||row>=ns)return null;return {b:bb,i:row};} return null; } + function hover(){return ptrX<0?null:hitAt(ptrX,ptrY,false);} + // the pinned target while its block is still drawn; null once it has scrolled out of the strip + function pinned(){if(!pin)return null;var p=byHash[pin.h];return p&&p.on?{b:p,i:pin.i}:null;} + function same(a,b){return !!(a&&b&&a.b===b.b&&a.i===b.i);} + function inRect(r,qx,qy){return !!(r&&qx>=r[0]&&qx=r[1]&&qyG-S*2)link(prev,b);prev=b;} x.stroke(); - // hovered block: all of its parent edges, bright - var hv=hit(),hb=hv?hv.b:null; - if(hb&&hv.i<0){x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1.5;x.beginPath();for(i=0;i=PIN_FADE)pinFade=null; + // hovered block (pointer devices only) and pinned block: all of their parent edges, bright + var hv=hover(),hb=hv?hv.b:null,pb=pv?pv.b:null; + if(pb&&pv.i<0){x.strokeStyle='rgba(255,179,92,0.7)';x.lineWidth=1.5;x.beginPath();for(i=0;i0?'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(hb===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();}} + if(hb===b&&b!==pb){x.strokeStyle='#F4F1EC';x.lineWidth=1.5;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();} + // the pinned block: a molten ring with a glow, so the reader can find it as the strip moves + if(pb===b){x.save();x.shadowColor='rgba(255,179,92,0.8)';x.shadowBlur=8;x.strokeStyle='#FFB35C';x.lineWidth=2;rrect(b.px-h-5,b.py-h-5,S+10,S+10,r+3);x.stroke();x.restore();}} x.font='500 10px IBM Plex Mono, monospace'; // the chain tip carries its short id; nothing else is labelled unless hovered or pinned if(tip){var lt=tip.hash.slice(0,8),lw=x.measureText(lt).width,lh=11,pad=3; @@ -508,20 +539,35 @@ main{padding-bottom:var(--sec)} if(st==='paid'){x.strokeStyle='#0C0C0E';x.lineWidth=1.5;x.beginPath();x.moveTo(cx0+ng.cw*0.25,cy0+ng.ch*0.55);x.lineTo(cx0+ng.cw*0.45,cy0+ng.ch*0.8);x.lineTo(cx0+ng.cw*0.78,cy0+ng.ch*0.25);x.stroke();}} else if(st==='proving'){x.fillStyle='rgba(255,179,92,0.25)';rrect(cx0,cy0,ng.cw,ng.ch,2);x.fill();x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1;rrect(cx0+0.5,cy0+0.5,ng.cw-1,ng.ch-1,2);x.stroke();} else{x.strokeStyle='rgba(154,154,158,0.55)';x.lineWidth=1;rrect(cx0+0.5,cy0+0.5,ng.cw-1,ng.ch-1,2);x.stroke();} - if(hb===b&&hv.i===si){x.strokeStyle='#F4F1EC';x.lineWidth=1;rrect(cx0-1.5,cy0-1.5,ng.cw+3,ng.ch+3,3);x.stroke();}}} + if(pb===b&&pv.i===si){x.save();x.shadowColor='rgba(255,179,92,0.8)';x.shadowBlur=6;x.strokeStyle='#FFB35C';x.lineWidth=1.5;rrect(cx0-2.5,cy0-2.5,ng.cw+5,ng.ch+5,3);x.stroke();x.restore();} + else if(hb===b&&hv.i===si){x.strokeStyle='#F4F1EC';x.lineWidth=1;rrect(cx0-1.5,cy0-1.5,ng.cw+3,ng.ch+3,3);x.stroke();}}} } x.textBaseline='alphabetic';x.restore(); - // tooltips on the canvas, no DOM: a block, or one shard cell - x.save();x.beginPath();x.rect(G,0,W-G,H);x.clip();x.font='500 10px IBM Plex Mono, monospace'; - if(hb){var l1,l2,l3,ay,tcol=miners[hb.miner]?miners[hb.miner].col:'#3A3A42'; - if(hv.i<0){l1=hb.hash.slice(0,8)+' '+(hb.locked?'locked':hb.chain?'chain':hb.color==='blue'?'included, paid':hb.color==='red'?'excluded':'pending')+(hb.chain&&hb.shards.length?(hb.proven?', proven':', '+hb.shards.length+(hb.shards.length===1?' shard':' shards')):'');l2='blue '+compact(hb.blue_score)+' daa '+compact(hb.daa);l3=hb.parents.length+(hb.parents.length===1?' parent':' parents')+' '+hb.miner;ay=hb.py;} - else{var sd=hb.shards[hv.i];l1=hb.hash.slice(0,8)+' shard '+(hv.i+1)+' of '+hb.shards.length+' '+sd.state;l2='prover '+(sd.prover||'none yet')+' lag '+(sd.lag!==null&&sd.lag!==undefined?sd.lag+' s':'n/a');l3=(sd.payout!==null&&sd.payout!==undefined?sd.payout+' IGN':'unpaid')+(sd.pgas?' '+compact(sd.pgas)+' pgas':'');ay=BH+FIN_H+PROV_H/2;if(sd.prover)tcol=colour(sd.prover);} - var tw=Math.max(x.measureText(l1).width,x.measureText(l2).width,x.measureText(l3).width)+20,th=52,tx=hb.px+h+10,tty=ay-th/2; - if(tx+tw>W-4)tx=hb.px-h-10-tw;if(txW-4)tx=show.b.px-h-10-tw;if(tx=0&&inRect(closeRect,ptrX,ptrY))?'pointer':'';if(c.style.cursor!==cur)c.style.cursor=cur; // gutter: lane labels in the miner's colour, strip names, 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'; @@ -538,7 +584,7 @@ main{padding-bottom:var(--sec)} // ?perf=1 exposes window.__igneumPerf = {frames, drawMs, since} so the draw cost can be read from the console. var PERF=/[?&]perf=1/.test(location.search)?(window.__igneumPerf={frames:0,drawMs:0,since:performance.now()}):null,lastDraw=0; function frame(now){if(!running)return; - if(ptrX>=0||pin||now-lastDraw>=31){lastDraw=now;var t0=PERF?performance.now():0;draw(now);if(PERF){PERF.frames++;PERF.drawMs+=performance.now()-t0;}} + if(ptrX>=0||pin||pinFade||now-lastDraw>=31){lastDraw=now;var t0=PERF?performance.now():0;draw(now);if(PERF){PERF.frames++;PERF.drawMs+=performance.now()-t0;}} requestAnimationFrame(frame);} function start(){if(running||reduce)return;if(document.hidden||!inView)return;running=true;requestAnimationFrame(frame);} function stop(){running=false;} @@ -547,9 +593,18 @@ main{padding-bottom:var(--sec)} 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();}); + // hover: pointer devices only, never a touch; it reads the strip and leaves the pin alone + c.addEventListener('pointermove',function(e){if(e.pointerType==='touch')return;ptrX=e.offsetX;ptrY=e.offsetY;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 t2=hit();pin=t2&&!(old&&old.h===t2.b.hash&&old.i===t2.i)?{h:t2.b.hash,i:t2.i}:null;ptrX=-1;ptrY=-1;if(reduce)kick();}); + // tap or click: one pointer, down to up, under 10 px of travel, so a scroll on a phone never pins. Pointer events only; + // the canvas ignores click, so a touch tap fires once (its synthetic click is dropped below) + c.addEventListener('pointerdown',function(e){if(e.button&&e.button!==0)return;down=down?null:{id:e.pointerId,x:e.offsetX,y:e.offsetY};}); + c.addEventListener('pointerup',function(e){if(!down||down.id!==e.pointerId){down=null;return;} + var dx=e.offsetX-down.x,dy=e.offsetY-down.y;down=null;if(dx*dx+dy*dy>TAP_SLOP*TAP_SLOP)return;tap(e.offsetX,e.offsetY,e.pointerType==='touch');}); + c.addEventListener('pointercancel',function(){down=null;}); + // anywhere else on the page clears the pin; Escape too + document.addEventListener('click',function(e){if(e.target!==c)clearPin();}); + document.addEventListener('keydown',function(e){if(e.key==='Escape')clearPin();}); start(); // ---------- sparkline ----------