@@ -485,31 +504,99 @@ footer .wrap{padding-block:48px 32px}
var bio=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){bar.querySelectorAll('div').forEach(function(d){d.style.width=d.getAttribute('data-w');});bio.disconnect();}});},{threshold:.4});
bio.observe(bar);
- // hero background: a block graph that grows left to right, proves, and locks
+ // live observer: ONE fetch of /api/live every 2 s feeds the hero, the stats strip and the chain scene.
+ // Subscribers get (fresh, d): the sampled new blocks since the last pull and the whole response, or ([], null) when off or stale.
+ var live={on:false,rate:1.3,seen:{},minerOf:{},subs:[]};
+ live.sub=function(fn){live.subs.push(fn);};
+ function emitLive(fresh,d){live.subs.forEach(function(fn){fn(fresh,d);});}
+ function pullLive(first){return fetch('/api/live',{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(d){
+ if(!d||!d.ok||!d.state||d.state.stale||!d.blocks||!d.miners){live.on=false;emitLive([],null);return false;}
+ live.on=true;
+ var fresh=d.blocks.filter(function(b){return !live.seen[b.hash];});if(first)fresh=fresh.slice(-12);
+ // the response is a 90 s window, so both maps are rebuilt from it and stay bounded
+ var seen={},minerOf={};d.blocks.forEach(function(b){seen[b.hash]=1;minerOf[b.hash]=b.miner||'?';});live.seen=seen;live.minerOf=minerOf;
+ /* the scenes keep a calm pace; faster chains are sampled, the counters stay true */
+ if(fresh.length>3){var step=fresh.length/3,pick=[];for(var i=0;i<3;i++)pick.push(fresh[Math.floor(i*step)]);fresh=pick;}
+ emitLive(fresh,d);return true;
+ }).catch(function(){live.on=false;emitLive([],null);return false;});}
+
+ // live stats strip under the hero buttons: shown only while the observer is fresh
+ var strip=document.getElementById('livestrip');
+ function lsSet(id,v){var el=document.getElementById(id);if(el)el.textContent=v;}
+ function fmtHash(h){if(h===null||h===undefined||!(h>0))return 'pending';return h>=1e9?(h/1e9).toFixed(1)+' GH/s':(h/1e6).toFixed(1)+' MH/s';}
+ live.sub(function(fresh,d){var on=live.on&&!!d;if(strip)strip.classList.toggle('on',on);if(!on)return;var s=d.state;
+ lsSet('ls-miners',s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:d.miners.length);
+ lsSet('ls-bps',s.blocks_60s!==null&&s.blocks_60s!==undefined?(s.blocks_60s/60).toFixed(1):'pending');
+ lsSet('ls-hash',fmtHash(s.hashes_per_second_estimate));
+ lsSet('ls-blocks',s.block_count!==null&&s.block_count!==undefined?Number(s.block_count).toLocaleString('en-GB'):'pending');});
+
+ // hero background: a block graph that grows left to right, proves, and locks.
+ // Live: each dot is one miner seen in the last ten minutes (at most 24, no ids drawn); it flashes when that
+ // miner finds a sampled block and a faint line joins it to the miners of the block's parents for a moment.
var cv=document.getElementById('bg'),ctx=cv.getContext('2d'),nodes=[],W,H,dpr=Math.min(window.devicePixelRatio||1,2),lanes=5,count=0;
function size(){W=cv.clientWidth;H=cv.clientHeight;cv.width=W*dpr;cv.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);}
size();window.addEventListener('resize',size);
var spacing=function(){return W<700?74:92;};
- function spawn(x){count++;var lane=Math.floor(Math.random()*lanes);var y=H*(0.18+lane*(0.64/(lanes-1)))+(Math.random()*18-9);
+ function laneY(l){return H*(0.18+l*(0.64/(lanes-1)));}
+ function edge(a,b){ctx.beginPath();ctx.moveTo(a.x,a.y);var mx=(a.x+b.x)/2;ctx.bezierCurveTo(mx,a.y,mx,b.y,b.x,b.y);ctx.stroke();}
+ function spawn(x){count++;var lane=Math.floor(Math.random()*lanes);var y=laneY(lane)+(Math.random()*18-9);
var cands=nodes.slice(-7).filter(function(p){return p.x
60)nodes.shift();}
+ function seed(){nodes=[];count=0;var sp=spacing();for(var i=0;i6)hq=hq.slice(-6);
+ });
function frame(t){
- if(t-last>1000){spawn(W+16);last=t;}
ctx.clearRect(0,0,W,H);
- nodes.forEach(function(n){n.x-=speed;var age=t-n.born;if(age>3500)n.state=2;else if(age>1800)n.state=1;});
+ // simulation: unchanged while the observer is off or stale, fades out behind the live dots when it is on
+ simA=Math.max(0,Math.min(1,simA+(hLive?-0.02:0.02)));
+ if(!hLive){if(!nodes.length)seed();if(t-last>1000){spawn(W+16);last=t;}}
+ if(simA>0){
+ ctx.globalAlpha=simA;
+ nodes.forEach(function(n){n.x-=speed;var age=t-n.born;if(age>3500)n.state=2;else if(age>1800)n.state=1;});
+ ctx.lineWidth=1;
+ nodes.forEach(function(n){n.parents.forEach(function(p){if(nodes.indexOf(p)<0)return;var proven=n.state===2&&p.state===2;ctx.strokeStyle=proven?'rgba(242,84,27,0.35)':'rgba(154,154,158,0.18)';edge(n,p);});});
+ nodes.forEach(function(n){var r=n.state===2?5:4;
+ if(n.state===2&&n.lock){ctx.beginPath();ctx.arc(n.x,n.y,11,0,Math.PI*2);ctx.strokeStyle='rgba(242,84,27,0.9)';ctx.lineWidth=1.5;ctx.stroke();ctx.lineWidth=1;}
+ if(n.state===2){var g=ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,16);g.addColorStop(0,'rgba(242,84,27,0.35)');g.addColorStop(1,'rgba(242,84,27,0)');ctx.fillStyle=g;ctx.beginPath();ctx.arc(n.x,n.y,16,0,Math.PI*2);ctx.fill();}
+ ctx.beginPath();ctx.arc(n.x,n.y,r,0,Math.PI*2);ctx.fillStyle=n.state===2?'#F2541B':(n.state===1?'#FFB35C':'#0C0C0E');ctx.fill();
+ if(n.state===0){ctx.strokeStyle='rgba(154,154,158,0.8)';ctx.stroke();}});
+ ctx.globalAlpha=1;
+ } else if(nodes.length) nodes=[];
+ // live: release one sampled block at the chain scene's pace; its miner flashes, lines reach its parents' miners
+ if(hLive&&hq.length&&t-hLast>Math.max(60,1000/live.rate)){var b=hq.shift();hLast=t;var m=joinMiner(b.miner||'?');
+ if(m){m.flash=1;var done={};(b.parents||[]).forEach(function(h){var pid=live.minerOf[h];if(!pid||pid===m.id||done[pid])return;done[pid]=1;var p=minerDot(pid);if(p&&!p.gone)links.push({a:m,b:p,born:t});});if(links.length>24)links=links.slice(-24);}}
+ hm.forEach(function(m){m.a=Math.max(0,Math.min(1,m.a+(m.gone?-0.012:0.012)));
+ m.fx-=speed/W;if(m.fx<-0.02){m.fx+=1.04;m.a=0;} // the same slow drift; a dot that leaves on the left fades back in on the right
+ m.x=W*m.fx+Math.sin(t/9000+m.ph)*14;m.y=laneY(m.lane)+m.jy+Math.cos(t/12000+m.ph*1.7)*6;
+ if(m.flash>0)m.flash=Math.max(0,m.flash-0.018);});
+ hm=hm.filter(function(m){return !(m.gone&&m.a<=0);});
+ links=links.filter(function(l){return t-l.born<1600&&hm.indexOf(l.a)>=0&&hm.indexOf(l.b)>=0;});
ctx.lineWidth=1;
- nodes.forEach(function(n){n.parents.forEach(function(p){if(nodes.indexOf(p)<0)return;var proven=n.state===2&&p.state===2;ctx.strokeStyle=proven?'rgba(242,84,27,0.35)':'rgba(154,154,158,0.18)';ctx.beginPath();ctx.moveTo(n.x,n.y);var mx=(n.x+p.x)/2;ctx.bezierCurveTo(mx,n.y,mx,p.y,p.x,p.y);ctx.stroke();});});
- nodes.forEach(function(n){var r=n.state===2?5:4;
- if(n.state===2&&n.lock){ctx.beginPath();ctx.arc(n.x,n.y,11,0,Math.PI*2);ctx.strokeStyle='rgba(242,84,27,0.9)';ctx.lineWidth=1.5;ctx.stroke();ctx.lineWidth=1;}
- if(n.state===2){var g=ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,16);g.addColorStop(0,'rgba(242,84,27,0.35)');g.addColorStop(1,'rgba(242,84,27,0)');ctx.fillStyle=g;ctx.beginPath();ctx.arc(n.x,n.y,16,0,Math.PI*2);ctx.fill();}
- ctx.beginPath();ctx.arc(n.x,n.y,r,0,Math.PI*2);ctx.fillStyle=n.state===2?'#F2541B':(n.state===1?'#FFB35C':'#0C0C0E');ctx.fill();
- if(n.state===0){ctx.strokeStyle='rgba(154,154,158,0.8)';ctx.stroke();}});
+ links.forEach(function(l){if(Math.abs(l.a.x-l.b.x)>W*0.6)return;var p=(t-l.born)/1600;ctx.strokeStyle='rgba(242,84,27,'+(0.35*(1-p)*Math.min(l.a.a,l.b.a)).toFixed(3)+')';edge(l.a,l.b);});
+ hm.forEach(function(m){if(m.a<=0)return;var f=m.flash;ctx.globalAlpha=m.a;var gr=16+8*f;
+ var g=ctx.createRadialGradient(m.x,m.y,0,m.x,m.y,gr);g.addColorStop(0,'rgba(242,84,27,'+(0.2+0.35*f).toFixed(3)+')');g.addColorStop(1,'rgba(242,84,27,0)');ctx.fillStyle=g;ctx.beginPath();ctx.arc(m.x,m.y,gr,0,Math.PI*2);ctx.fill();
+ ctx.beginPath();ctx.arc(m.x,m.y,4+2*f,0,Math.PI*2);ctx.fillStyle='#F2541B';ctx.fill();
+ if(f>0){ctx.beginPath();ctx.arc(m.x,m.y,2+1.5*f,0,Math.PI*2);ctx.fillStyle='rgba(255,179,92,'+f.toFixed(3)+')';ctx.fill();}
+ ctx.globalAlpha=1;});
requestAnimationFrame(frame);
}
- if(!reduce){var sp=spacing();for(var i=0;i=0&&b.parents.length<4)b.parents.push(q);});if(!b.parents.length){var prev=blocks.slice(-6).filter(function(q){return q.x1&&Math.random()<0.5)b.parents.push(prev[1]);}}
else{var cands=blocks.slice(-8).filter(function(q){return q.xMath.max(60,1000/liveRate)){spawn(W+S,liveQ.shift());last=t;}}
+ if(liveMode){if(liveQ.length&&t-last>Math.max(60,1000/live.rate)){spawn(W+S,liveQ.shift());last=t;}}
else if(t-last>1000){spawn(W+S);last=t;}
var dt=16;
- var sp=liveMode?speed*liveRate:speed;blocks.forEach(function(b){b.x-=sp*dt;var age=t-b.born;
+ var sp=liveMode?speed*live.rate:speed;blocks.forEach(function(b){b.x-=sp*dt;var age=t-b.born;
if(b.state==='mined'&&age>1200*k)b.state='proving';
if(b.state==='proving'){for(var i=0;i<4;i++){if(!b.claimed[i]&&Math.random()<0.012/k){b.claimed[i]=true;var fromTop=Math.random()<0.5;sparks.push({x:Math.random()*W,y:fromTop?-6:H+6,tx:b.x,ty:b.y,b:b,i:i,p:0});}}
if(b.shards.every(function(v){return v>=1;})){b.state='proven';b.glow=1;proven++;cp.textContent=proven;if(b.cp)setTimeout(function(){b.locked=true;locked++;cl.textContent=locked;ripples.push({x:b.x,y:b.y,r:S,a:1});ripples.push({x:b.x,y:b.y,r:S*0.5,a:1});},1800*k);}}
@@ -563,21 +650,16 @@ footer .wrap{padding-block:48px 32px}
var seedN=Math.ceil(W/GAP)+1,pv=Math.min(5,Math.max(2,Math.floor(seedN*0.45)));for(var i=0;ipv){b.state='proven';b.shards=[1,1,1,1];b.claimed=[true,true,true,true];proven++;if(b.cp){b.locked=true;locked++;}}else if(i>1){b.state='proving';b.claimed=[true,true,false,false];b.shards=[1,1,0,0];}}
cb.textContent=n;cp.textContent=proven;cl.textContent=locked;
requestAnimationFrame(frame);
- // live path: one fetch on load; if the observer updated within 30 s, the label links to /live and real blocks feed the scene
+ // live path: the shared poller above feeds sampled real blocks; while fresh, the label links to /live
var label=document.getElementById('viz-label'),labelHTML=label?label.innerHTML:'';
function laneFor(m){if(!(m in liveLanes))liveLanes[m]=Object.keys(liveLanes).length%lanes;return liveLanes[m];}
function setLive(on){if(on===liveMode)return;liveMode=on;if(label)label.innerHTML=on?'live devnet':labelHTML;}
- function pullLive(first){return fetch('/api/live',{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(d){
- if(!d||!d.ok||!d.state||d.state.stale){setLive(false);return false;}
- setLive(true);liveRate=1.3; /* the scene keeps a calm pace; faster chains are sampled, the counter stays true */
- var fresh=d.blocks.filter(function(b){return !liveSeen[b.hash];});if(first)fresh=fresh.slice(-12);
- d.blocks.forEach(function(b){liveSeen[b.hash]=1;});
- if(fresh.length>3){var step=fresh.length/3,pick=[];for(var i=0;i<3;i++)pick.push(fresh[Math.floor(i*step)]);fresh=pick;}
- fresh.forEach(function(b){liveQ.push({hash:b.hash,parents:b.parents,lane:laneFor(b.miner||'?')});});
- if(liveQ.length>6)liveQ=liveQ.slice(-6);return true;
- }).catch(function(){setLive(false);return false;});}
- pullLive(true).then(function(on){if(on)setInterval(function(){pullLive(false);},2000);});
+ live.sub(function(fresh){setLive(live.on);if(!live.on)return;
+ fresh.forEach(function(b){liveQ.push({hash:b.hash,parents:b.parents||[],lane:laneFor(b.miner||'?')});});
+ if(liveQ.length>6)liveQ=liveQ.slice(-6);});
})();
+ // one fetch on load; only if the observer is fresh does the 2 s poll start (it feeds the hero, the strip and the chain scene)
+ pullLive(true).then(function(on){if(on)setInterval(function(){pullLive(false);},2000);});
// hourly program ticker
var ops=['rotl','rotr','xor','add','sub','mul','mulhi','mad','or'];