site: live page pins a tapped block until the reader lets it go
A tap or click on a block or a shard cell now pins its details. The panel stays until a tap or click lands anywhere else, Escape, the close control, or the block scrolls out of the strip (then a 200 ms fade, no snap). Another block switches the pin. The pinned block carries a molten ring. The pinned panel sits at a fixed spot at the top left of the strip, so new blocks never move it, and shows a PINNED label and a 32 px close control for a thumb. Hover is unchanged on pointer devices: another block shows its floating panel for as long as the pointer is on it, then the panel returns to the pinned block. Hover never clears a pin. Pointer events only: one pointer, down to up, under 10 px of travel counts as a tap, so a scroll on a phone never pins, and a touch tap fires once (the canvas ignores click). A touch gets a wider hit radius than a pointer; the hover radius is as before. Verified with real mouse, touch and key input through CDP on the cached headless Chromium against the local page fed by igneum.network/api/live: 20 of 20 checks at 1200 px and 375 px, no console errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9835f493c7
commit
04b85b9cd7
1 changed files with 82 additions and 27 deletions
109
site/live.html
109
site/live.html
|
|
@ -238,9 +238,9 @@ main{padding-bottom:var(--sec)}
|
|||
</div>
|
||||
<canvas id="dag" aria-hidden="true"></canvas>
|
||||
<div class="legend">
|
||||
<div><span class="lk">blocks</span><span><i class="sw" style="background:var(--ember);border:1.5px solid #5CB8FF"></i>chain</span><span><i class="sw" style="background:rgba(92,184,255,.7);border:1.5px solid #5CB8FF"></i>included, paid</span><span><i class="sw" style="border:1.5px solid var(--ash)"></i>pending</span><span><i class="sw" style="border:1.5px solid rgba(154,154,158,.6);background:linear-gradient(45deg,transparent 44%,var(--ash) 44%,var(--ash) 56%,transparent 56%) center/8px 8px no-repeat"></i>excluded</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:2px solid var(--ember)"></i>selected chain</span><span class="txt">one lane per miner, colour from its id; hover or tap a block</span></div>
|
||||
<div><span class="lk">blocks</span><span><i class="sw" style="background:var(--ember);border:1.5px solid #5CB8FF"></i>chain</span><span><i class="sw" style="background:rgba(92,184,255,.7);border:1.5px solid #5CB8FF"></i>included, paid</span><span><i class="sw" style="border:1.5px solid var(--ash)"></i>pending</span><span><i class="sw" style="border:1.5px solid rgba(154,154,158,.6);background:linear-gradient(45deg,transparent 44%,var(--ash) 44%,var(--ash) 56%,transparent 56%) center/8px 8px no-repeat"></i>excluded</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:2px solid var(--ember)"></i>selected chain</span><span class="txt">one lane per miner, colour from its id; hover a block, or tap it to keep its details up</span></div>
|
||||
<div><span class="lk">finality</span><span><i class="sw" style="width:22px;height:10px;background:rgba(242,84,27,.22);border-radius:2px"></i>final</span><span><i class="sw" style="width:3px;height:12px;background:var(--molten);border-radius:1px"></i>locked checkpoint</span><span><i class="sw" style="width:22px;height:10px;background:rgba(154,154,158,.10);border:1px solid rgba(154,154,158,.3);border-radius:2px"></i>proposed</span><span class="txt">reads "paused" whenever under two thirds of the weight is signing</span></div>
|
||||
<div><span class="lk">proving</span><span><i class="sw" style="width:12px;height:9px;border:1px solid rgba(154,154,158,.55);border-radius:2px"></i>shard planned</span><span><i class="sw" style="width:12px;height:9px;border:1px solid var(--molten);background:rgba(255,179,92,.25);border-radius:2px"></i>proving</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px"></i>verified, prover colour</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px;position:relative"><i style="position:absolute;left:3px;top:1px;width:3px;height:5px;border:solid #0C0C0E;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)"></i></i>paid</span><span><i class="sw" style="width:0;height:12px;border-left:1px dashed var(--molten)"></i>where proofs land behind the tip</span><span class="txt">hover or tap a cell</span></div>
|
||||
<div><span class="lk">proving</span><span><i class="sw" style="width:12px;height:9px;border:1px solid rgba(154,154,158,.55);border-radius:2px"></i>shard planned</span><span><i class="sw" style="width:12px;height:9px;border:1px solid var(--molten);background:rgba(255,179,92,.25);border-radius:2px"></i>proving</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px"></i>verified, prover colour</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px;position:relative"><i style="position:absolute;left:3px;top:1px;width:3px;height:5px;border:solid #0C0C0E;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)"></i></i>paid</span><span><i class="sw" style="width:0;height:12px;border-left:1px dashed var(--molten)"></i>where proofs land behind the tip</span><span class="txt">hover a cell, or tap it to keep its details up</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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){var best=null,bd=S*0.9;
|
||||
if(qy<BH){var best=null,bd=S*(touch?1.3:0.9);
|
||||
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?{b:best,i:-1}:null;}
|
||||
if(qy>=BH+FIN_H&&provOn()){var bb=null,bdx=1e9;
|
||||
for(i=0;i<blocks.length;i++){b=blocks[i];if(!b.on||!b.chain||!b.shards.length)continue;var ddx=Math.abs(b.px-qx);if(ddx<bdx){bdx=ddx;bb=b;}}
|
||||
if(!bb)return null;var g=cellGeom(bb.shards.length);if(bdx>Math.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[0]+r[2]&&qy>=r[1]&&qy<r[1]+r[3]);}
|
||||
function setPin(t){pin=t?{h:t.b.hash,i:t.i,b:t.b}:null;pinFade=null;if(reduce)kick();}
|
||||
function clearPin(){if(!pin&&!pinFade)return;pin=null;pinFade=null;if(reduce)kick();}
|
||||
// a tap or click at (qx, qy): the close control clears, the panel itself is inert, a block or cell pins (a second tap on the
|
||||
// pinned one keeps it), anything else on the canvas clears
|
||||
function tap(qx,qy,touch){
|
||||
if(inRect(closeRect,qx,qy)){clearPin();return;}
|
||||
if(inRect(panelRect,qx,qy))return;
|
||||
var t=hitAt(qx,qy,touch);
|
||||
if(t)setPin(t);else clearPin();
|
||||
}
|
||||
// the three panel lines for a target, its anchor y for the floating panel, and its border colour
|
||||
function lines(t){var b=t.b,tcol=miners[b.miner]?miners[b.miner].col:'#3A3A42',l1,l2,l3,ay;
|
||||
if(t.i<0){l1=b.hash.slice(0,8)+' '+(b.locked?'locked':b.chain?'chain':b.color==='blue'?'included, paid':b.color==='red'?'excluded':'pending')+(b.chain&&b.shards.length?(b.proven?', proven':', '+b.shards.length+(b.shards.length===1?' shard':' shards')):'');l2='blue '+compact(b.blue_score)+' daa '+compact(b.daa);l3=b.parents.length+(b.parents.length===1?' parent':' parents')+' '+b.miner;ay=b.py;}
|
||||
else{var sd=b.shards[t.i]||{};l1=b.hash.slice(0,8)+' shard '+(t.i+1)+' of '+b.shards.length+' '+(sd.state||'gone');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);}
|
||||
return [l1,l2,l3,ay,tcol];}
|
||||
function provOn(){return !!(prov&&prov.supported&&prov.active);}
|
||||
function draw(now){
|
||||
if(needSize)size();if(!W||!H)return;
|
||||
|
|
@ -434,9 +458,14 @@ main{padding-bottom:var(--sec)}
|
|||
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(),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<hb.parents.length;i++){var pq=byHash[hb.parents[i]];if(pq)link(pq,hb);}x.stroke();}
|
||||
// the pinned block scrolled out: let it go with a short fade instead of a snap (reduced motion: at once)
|
||||
var pv=pinned();
|
||||
if(pin&&!pv){pinFade=reduce?null:{b:pin.b,i:pin.i,at:now};pin=null;}
|
||||
if(pinFade&&now-pinFade.at>=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;i<pb.parents.length;i++){var pq0=byHash[pb.parents[i]];if(pq0)link(pq0,pb);}x.stroke();}
|
||||
if(hb&&hv.i<0&&hb!==pb){x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1.5;x.beginPath();for(i=0;i<hb.parents.length;i++){var pq=byHash[hb.parents[i]];if(pq)link(pq,hb);}x.stroke();}
|
||||
// blocks
|
||||
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;
|
||||
|
|
@ -446,7 +475,9 @@ main{padding-bottom:var(--sec)}
|
|||
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(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(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=tcol;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);}
|
||||
// the detail panel on the canvas, no DOM: a block, or one shard cell. A hovered target that is not the pinned one gets
|
||||
// the floating panel beside it, as before. Otherwise the pinned target (or the one fading out) gets the fixed panel at
|
||||
// the top left of the strip, where the oldest blocks are: it never moves as new blocks arrive.
|
||||
x.save();x.beginPath();x.rect(G,0,W-G,H);x.clip();x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left';
|
||||
closeRect=null;panelRect=null;
|
||||
var show=hv&&!same(hv,pv)?hv:(pv||pinFade),fixed=show&&show!==hv;
|
||||
if(show){var L=lines(show),tw=Math.max(x.measureText(L[0]).width,x.measureText(L[1]).width,x.measureText(L[2]).width)+20,th,tx,tty;
|
||||
if(!fixed){th=52;tx=show.b.px+h+10;tty=L[3]-th/2;
|
||||
if(tx+tw>W-4)tx=show.b.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=L[4];x.lineWidth=1;rrect(tx+0.5,tty+0.5,tw-1,th-1,6);x.stroke();
|
||||
x.fillStyle='#F4F1EC';x.fillText(L[0],tx+10,tty+16);x.fillStyle='#C9C7C2';x.fillText(L[1],tx+10,tty+31);x.fillStyle='#9A9A9E';x.fillText(L[2],tx+10,tty+46);}
|
||||
else{th=72;tw=Math.max(tw,112);tx=G+6;tty=6;var CL=32,fade=show===pinFade?Math.max(0,1-(now-pinFade.at)/PIN_FADE):1;
|
||||
x.globalAlpha=fade;
|
||||
x.fillStyle='rgba(22,22,26,0.96)';rrect(tx,tty,tw,th,6);x.fill();x.strokeStyle=L[4];x.lineWidth=1;rrect(tx+0.5,tty+0.5,tw-1,th-1,6);x.stroke();
|
||||
// header: a small pinned label and a 32 px close control for a thumb
|
||||
x.font='500 9px IBM Plex Mono, monospace';x.fillStyle='#FFB35C';x.fillText('PINNED',tx+10,tty+14);
|
||||
var cxm=tx+tw-CL/2,cym=tty+CL/2;x.strokeStyle='rgba(154,154,158,0.55)';x.lineWidth=1;x.beginPath();x.arc(cxm,cym,8.5,0,Math.PI*2);x.stroke();
|
||||
x.strokeStyle='#F4F1EC';x.lineWidth=1.5;x.beginPath();x.moveTo(cxm-3.5,cym-3.5);x.lineTo(cxm+3.5,cym+3.5);x.moveTo(cxm+3.5,cym-3.5);x.lineTo(cxm-3.5,cym+3.5);x.stroke();
|
||||
x.font='500 10px IBM Plex Mono, monospace';x.fillStyle='#F4F1EC';x.fillText(L[0],tx+10,tty+36);x.fillStyle='#C9C7C2';x.fillText(L[1],tx+10,tty+51);x.fillStyle='#9A9A9E';x.fillText(L[2],tx+10,tty+66);
|
||||
x.globalAlpha=1;
|
||||
if(show===pv){closeRect=[tx+tw-CL,tty,CL,CL];panelRect=[tx,tty,tw,th];}}}
|
||||
x.restore();
|
||||
// pointer devices: a hand over a block, a cell or the close control
|
||||
var cur=hv||(ptrX>=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 ----------
|
||||
|
|
|
|||
Loading…
Reference in a new issue