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 +
+
live devnet
+
+
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'];