From 08d45f82b9e626e96af985e03372382f883a5148 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:38:34 +0000 Subject: [PATCH] Homepage: hero dots go live as miners, one shared /api/live poll, live stats strip The hero canvas now has a live path fed by the same 2 s fetch as the chain scene (one poller, three subscribers: hero, stats strip, chain scene). When the observer is fresh each dot is one miner seen in the last ten minutes (at most 24, no id drawn), it flashes ember when that miner finds a sampled block, and faint lines reach the miners of the block's parents for a moment. Dots join on a new miner and fade out when one leaves the ten-minute set. The drift, colours and pace are the simulation's; the simulation runs unchanged when the observer is off or stale. A live stats strip under the hero buttons (miners active, blocks / s, network hash rate, blocks on the devnet) shows only while the observer is fresh and hides again when it goes stale. Two tiles per row at phone width. Co-Authored-By: Claude Fable 5.1 --- site/index.html | 134 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/site/index.html b/site/index.html index 3fbc4efc7..dbbb735de 100644 --- a/site/index.html +++ b/site/index.html @@ -92,6 +92,16 @@ p{margin:0} .hero-copy{display:flex;flex-direction:column;gap:24px} .hero p.lead{font-size:clamp(17px,2vw,20px);color:var(--ink-2);max-width:56ch} .cta{display:flex;flex-wrap:wrap;gap:12px} +/* live stats strip: hidden until the observer reports fresh numbers */ +.livestrip{display:none;flex-direction:column;gap:10px} +.livestrip.on{display:flex} +.livestrip-head{display:flex;align-items:center;gap:8px;font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--molten)} +.livestrip-head a{color:var(--molten)} +.livestrip-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px} +.livestrip .tile{padding:14px 16px;border-radius:12px;min-width:0} +.livestrip .tile .v{font-size:18px;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.livestrip .tile .k{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin-top:4px;line-height:1.35} +@media (max-width:700px){.livestrip-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} .verify{display:flex;flex-direction:column;gap:18px} .verify-head{display:flex;justify-content:space-between;align-items:center;gap:12px} .live{display:inline-flex;align-items:center;gap:8px;font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--molten)} @@ -241,6 +251,15 @@ footer .wrap{padding-block:48px 32px} Start mining Read the litepaper +
+ +
+
0
miners active
+
0.0
blocks / s
+
0.0 MH/s
network hash rate
+
0
blocks on the devnet
+
+
@@ -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.x60)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'];