Merge site-ui-3 e5a1f2c: the /scenes lab, three animated chain scenes on one live feed (Forge, Lattice, Pulse), unlinked, for Josh to pick
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BIN
docs/plans/site-ui-3-shots/scenes/scene-a-1440-dark.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/plans/site-ui-3-shots/scenes/scene-a-1440-dark.webm
Normal file
BIN
docs/plans/site-ui-3-shots/scenes/scene-a-390-dark.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/plans/site-ui-3-shots/scenes/scene-b-1440-dark.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/plans/site-ui-3-shots/scenes/scene-b-1440-dark.webm
Normal file
BIN
docs/plans/site-ui-3-shots/scenes/scene-b-390-dark.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/plans/site-ui-3-shots/scenes/scene-c-1440-dark.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/plans/site-ui-3-shots/scenes/scene-c-1440-dark.webm
Normal file
BIN
docs/plans/site-ui-3-shots/scenes/scene-c-390-dark.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/plans/site-ui-3-shots/scenes/scenes-1440-dark.jpg
Normal file
|
After Width: | Height: | Size: 153 KiB |
|
|
@ -345,7 +345,9 @@ function renderEvidence(html) {
|
|||
|
||||
const PAGES = [['index.html', ''], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['miner.html', 'miner'], ['wallet.html', 'wallet'], ['metamask.html', ''], ['faucet.html', ''], ['404.html', ''],
|
||||
// the DAG explorer (5 Oct 2026): /explorer, /block/<hash> and /address/<addr> (vercel.json rewrites the last two)
|
||||
['explorer.html', 'live'], ['block.html', 'live'], ['address.html', 'live']];
|
||||
['explorer.html', 'live'], ['block.html', 'live'], ['address.html', 'live'],
|
||||
// the scenes lab (6 Oct 2026): three animated views of the live feed, unlinked, noindex
|
||||
['scenes.html', '']];
|
||||
for (const [file, active] of PAGES) {
|
||||
const p = join(here, file);
|
||||
if (!existsSync(p)) throw new Error(`missing page ${file}`);
|
||||
|
|
|
|||
212
site/scenes.html
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum scenes, a lab</title>
|
||||
<meta name="description" content="Three animated views of the same live devnet feed: Forge, Lattice and Pulse. An unlinked lab page.">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="theme-color" content="#0C0C0E" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#F4F1EC" media="(prefers-color-scheme: light)">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
<!-- head:start -->
|
||||
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/site.css">
|
||||
<style>
|
||||
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
|
||||
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
|
||||
</style>
|
||||
<script>
|
||||
/* theme before first paint: the footer control stores "light" or "dark" under igneum-theme; nothing stored means the system setting */
|
||||
(function(){try{var t=localStorage.getItem("igneum-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t);}catch(e){}})();
|
||||
</script>
|
||||
<!-- head:end -->
|
||||
<style>
|
||||
/* scenes lab block; tokens, type and components are in /site.css */
|
||||
.lab{padding:calc(var(--sec) * .5) 0 var(--sec)}
|
||||
.lab-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:var(--s-3);margin-bottom:var(--s-5)}
|
||||
.scenes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4)}
|
||||
.scenes.only{grid-template-columns:1fr}
|
||||
.scene-card{display:flex;flex-direction:column;gap:var(--s-3)}
|
||||
.scene-card .head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-3)}
|
||||
.scene-card .head .eyebrow{margin:0}
|
||||
.scene-card canvas{width:100%;height:420px;border-radius:var(--r-row);background:var(--row);border:1px solid var(--line);display:block}
|
||||
.scenes.only .scene-card canvas{height:560px}
|
||||
.scene-card p{margin:0;color:var(--ink-2);font-size:var(--t-base)}
|
||||
@media (max-width:1000px){.scenes{grid-template-columns:1fr}.scene-card canvas{height:320px}.scenes.only .scene-card canvas{height:420px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- nav:start -->
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<nav class="nav" aria-label="Main">
|
||||
<div class="wrap">
|
||||
<a href="/" class="brand" aria-label="Igneum home">
|
||||
<svg class="mark" viewBox="0 0 1024 1024" width="28" height="28" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
|
||||
<span class="word">IGNEUM</span>
|
||||
</a>
|
||||
<div class="links" id="nav-links">
|
||||
<a href="/litepaper" data-nav="litepaper">Litepaper</a>
|
||||
<a href="/live" data-nav="live">Live devnet</a>
|
||||
<a href="/bench" data-nav="bench">Engineering log</a>
|
||||
<a href="/miner" data-nav="miner">Miner</a>
|
||||
<a href="/wallet" data-nav="wallet">Wallet</a>
|
||||
<a href="/evidence" data-nav="evidence">Evidence</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener">GitHub</a>
|
||||
</div>
|
||||
<a href="/miner#get" class="btn primary small cta">Download the miner</a>
|
||||
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Menu"><span class="bars"></span></button>
|
||||
<div class="nav-sheet" id="nav-sheet">
|
||||
<a href="/litepaper" data-nav="litepaper">Litepaper</a>
|
||||
<a href="/live" data-nav="live">Live devnet</a>
|
||||
<a href="/bench" data-nav="bench">Engineering log</a>
|
||||
<a href="/miner" data-nav="miner">Miner</a>
|
||||
<a href="/wallet" data-nav="wallet">Wallet</a>
|
||||
<a href="/evidence" data-nav="evidence">Evidence</a>
|
||||
<a href="/explorer">Explorer</a>
|
||||
<a href="/ledger">Ledger</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener">GitHub</a>
|
||||
<a href="/miner#get" class="btn primary">Download the miner</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script>
|
||||
(function(){
|
||||
// the menu: open on the button, closed by any link, Escape (focus back on the button) or a resize past 900 px
|
||||
var n=document.querySelector('.nav'),b=document.getElementById('nav-burger'),s=document.getElementById('nav-sheet');if(!n||!b||!s)return;
|
||||
function set(o){n.classList.toggle('open',o);b.setAttribute('aria-expanded',o?'true':'false');document.documentElement.style.overflow=o?'hidden':'';}
|
||||
b.addEventListener('click',function(){set(!n.classList.contains('open'));});
|
||||
s.addEventListener('click',function(e){if(e.target.closest('a'))set(false);});
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('open')){set(false);b.focus();}});
|
||||
var mq=window.matchMedia('(min-width:901px)');if(mq.addEventListener)mq.addEventListener('change',function(){set(false);});
|
||||
})();
|
||||
</script>
|
||||
<!-- nav:end -->
|
||||
<main id="main">
|
||||
<section class="lab">
|
||||
<div class="wrap">
|
||||
<div class="lab-head">
|
||||
<div>
|
||||
<div class="eyebrow">A lab, not linked</div>
|
||||
<h1>Three scenes, one feed</h1>
|
||||
<p class="lead" style="margin-top:var(--s-3)">The same live devnet reply, drawn three ways. Every mark is a block the observer stored; the words are the ledger's.</p>
|
||||
</div>
|
||||
<div class="state" id="feed-state"><span class="dot off"></span>connecting to the devnet observer</div>
|
||||
</div>
|
||||
<div class="scenes" id="scenes">
|
||||
<div class="scene-card card" data-scene="a">
|
||||
<div class="head"><h3>A · Forge</h3><div class="eyebrow">embers rise</div></div>
|
||||
<canvas id="sc-a" aria-label="Forge: blocks rise as embers from the bottom and cool from molten to bone as they go from pending to included to proven; a dark band sweeps up at the lock"></canvas>
|
||||
<p>Blocks arrive as embers from the hearth, one column per miner, and cool as they go: pending molten, included ember, proven bone. A lock is the dark band sweeping up; everything under it is final.</p>
|
||||
</div>
|
||||
<div class="scene-card card" data-scene="b">
|
||||
<div class="head"><h3>B · Lattice</h3><div class="eyebrow">a slow isometric DAG</div></div>
|
||||
<canvas id="sc-b" aria-label="Lattice: an isometric DAG with parents behind children, the selected chain a lit spine, checkpoints as rings closing around it"></canvas>
|
||||
<p>Time runs left to right, each miner one step deeper, so parents stand behind their children. The selected chain is the lit spine; a checkpoint is a ring that closes around it when it locks. The camera drifts at chain speed.</p>
|
||||
</div>
|
||||
<div class="scene-card card" data-scene="c">
|
||||
<div class="head"><h3>C · Pulse</h3><div class="eyebrow">the tip at the centre</div></div>
|
||||
<canvas id="sc-c" aria-label="Pulse: the tip at the centre, time outward in rings, each block a bead on its miner's ray, proofs filling beads, locks as a ring hardening"></canvas>
|
||||
<p>The tip is the centre and time runs outward, a ring every 30 seconds. Each miner is a ray; a block is a bead on it that drifts outward, filling when proven. A lock is a ring that hardens from dashed to solid.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend" style="margin-top:var(--s-5)">
|
||||
<span><i class="sw" style="border:2px solid var(--molten);border-radius:50%"></i>pending</span>
|
||||
<span><i class="sw" style="background:var(--ember);border-radius:50%"></i>included</span>
|
||||
<span><i class="sw" style="border:2px solid var(--line-2);border-radius:50%;opacity:.45"></i>excluded</span>
|
||||
<span><i class="sw" style="background:var(--bone);border-radius:50%"></i>proven</span>
|
||||
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint</span>
|
||||
</div>
|
||||
<p class="note">One node is read every 2 s; the window is the last 120 s. Under reduced motion each scene draws a still frame on every reply. Add <code>?only=a</code>, <code>b</code> or <code>c</code> to the address for one scene full width.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- footer:start -->
|
||||
<footer class="foot">
|
||||
<div class="wrap">
|
||||
<div class="foot-grid">
|
||||
<div class="foot-brand">
|
||||
<a href="/" class="brand" aria-label="Igneum home"><svg class="mark" viewBox="0 0 1024 1024" width="26" height="26" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span></a>
|
||||
<p>Mined by GPUs. Proven by fire.</p>
|
||||
</div>
|
||||
<nav class="foot-col" aria-label="Read">
|
||||
<div class="eyebrow">Read</div>
|
||||
<a href="/litepaper">Litepaper</a>
|
||||
<a href="/litepaper#limits">What Igneum does not claim</a>
|
||||
<a href="/litepaper#randomx">Igneum vs RandomX</a>
|
||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||
<a href="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<div class="eyebrow">Run</div>
|
||||
<a href="/miner">The miner</a>
|
||||
<a href="/wallet">The wallet</a>
|
||||
<a href="/miners">GPU bench table</a>
|
||||
<a href="/miner#fee">The dev fee</a>
|
||||
<a href="/faucet">Testnet faucet</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Follow">
|
||||
<div class="eyebrow">Follow</div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/explorer">Explorer</a>
|
||||
<a href="/#journey">Journey</a>
|
||||
<a href="/metamask">Add Igneum to MetaMask</a>
|
||||
<a href="https://github.com/igneum-network/spec" rel="noopener"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"></path></svg>GitHub, spec and vectors</a>
|
||||
<a href="/miner#get">Miner downloads: devnet build</a>
|
||||
<a href="mailto:hello@igneum.network">Report a flaw: hello@igneum.network</a>
|
||||
<a href="https://github.com/igneum-network/spec/issues" rel="noopener">Or open an issue on the spec</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="foot-base">
|
||||
<span class="foot-imprint">Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre · <a href="mailto:hello@igneum.network">hello@igneum.network</a></span>
|
||||
<span>© 2026 Igneum. Nothing on this page is an offer to sell anything.</span>
|
||||
<span class="theme" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></span>
|
||||
<span class="mono">igneum.network</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// theme control: stores light or dark under igneum-theme; system clears it. The head applied the stored value before paint.
|
||||
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
|
||||
function cur(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
|
||||
function paint(){var c=cur();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});}
|
||||
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}
|
||||
if(v==='system')document.documentElement.removeAttribute('data-theme');else document.documentElement.setAttribute('data-theme',v);paint();});});
|
||||
paint();
|
||||
})();
|
||||
</script>
|
||||
<!-- footer:end -->
|
||||
<script src="/scenes/feed.js"></script>
|
||||
<script src="/scenes/forge.js"></script>
|
||||
<script src="/scenes/lattice.js"></script>
|
||||
<script src="/scenes/pulse.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
var only=new URLSearchParams(location.search).get('only'),grid=document.getElementById('scenes');
|
||||
if(only){grid.classList.add('only');Array.prototype.slice.call(grid.children).forEach(function(c){if(c.getAttribute('data-scene')!==only)c.remove();});}
|
||||
var subs=[];
|
||||
if(document.getElementById('sc-a'))subs.push(IgneumForge.mount(document.getElementById('sc-a'),{}));
|
||||
if(document.getElementById('sc-b'))subs.push(IgneumLattice.mount(document.getElementById('sc-b'),{window:120,compact:window.innerWidth<700}));
|
||||
if(document.getElementById('sc-c'))subs.push(IgneumPulse.mount(document.getElementById('sc-c'),{window:120}));
|
||||
var st=document.getElementById('feed-state');
|
||||
IgneumFeed.start({window:120,subs:subs,onState:function(s,r){st.className='state '+(s==='live'?'live':s==='failed'?'bad':'');st.innerHTML='<span class="dot '+(s==='live'?'live':s==='failed'?'bad':'off')+'"></span>'+(s==='live'?'live devnet':s==='connecting'?'connecting to the devnet observer':(r||'live feed unavailable'));}});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
site/scenes/feed.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* One poll of /api/live every 2 s, pushed to every scene on the page (docs/plans/site-ui-3.md, the scenes lab, 6 Oct 2026).
|
||||
IgneumFeed.start({ url, window, onState, subs: [scene, ...] }); each scene has push(reply). The state words are the site's:
|
||||
connecting, live, stale (observer offline, last update N ago), failed (live feed unavailable). */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
function fmtAge(sec) { sec = Math.round(sec); return sec < 90 ? sec + ' s' : sec < 5400 ? Math.round(sec / 60) + ' min' : Math.round(sec / 3600) + ' h'; }
|
||||
function start(opts) {
|
||||
var subs = opts.subs || [], failures = 0, state = 'connecting', reason = 'connecting';
|
||||
function set(s, r) { if (s === state && r === reason) return; state = s; reason = r; if (opts.onState) opts.onState(s, r); }
|
||||
function poll() {
|
||||
if (document.hidden) return;
|
||||
fetch((opts.url || '/api/live') + '?window=' + (opts.window || 120), { cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function (d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) throw new Error('bad reply');
|
||||
failures = 0;
|
||||
if (d.state.stale) set('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); else set('live', null);
|
||||
subs.forEach(function (s) { s.push(d); });
|
||||
}).catch(function () { failures++; if (failures >= 2) set('failed', 'live feed unavailable'); });
|
||||
}
|
||||
poll(); var t = setInterval(poll, 2000);
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) poll(); });
|
||||
return { stop: function () { clearInterval(t); }, state: function () { return { state: state, reason: reason }; } };
|
||||
}
|
||||
// shared helpers for the three scenes
|
||||
function cssVar(name, fallback) { try { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; } }
|
||||
function theme() { return { ember: cssVar('--ember', '#F2541B'), molten: cssVar('--molten', '#FFB35C'), bone: cssVar('--bone', '#F4F1EC'), ash: cssVar('--ash', '#9A9A9E'), line: cssVar('--line', '#2A2A30'), row: cssVar('--row', '#111114'), ink2: cssVar('--ink-2', '#C9C7C2') }; }
|
||||
function rgba(hex, a) { var h = hex.replace('#', ''); if (h.length !== 6) return hex; return 'rgba(' + parseInt(h.slice(0, 2), 16) + ',' + parseInt(h.slice(2, 4), 16) + ',' + parseInt(h.slice(4, 6), 16) + ',' + a + ')'; }
|
||||
// the ledger's word for a block
|
||||
function word(b) { return b.locked ? 'locked' : b.proven ? 'proven' : (b.chain || b.color === 'blue') ? 'included' : b.color === 'red' ? 'excluded' : 'pending'; }
|
||||
function hash32(s) { var h = 2166136261; for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0) / 4294967296; }
|
||||
function sizer(canvas, ctx) { var dpr = Math.min(root.devicePixelRatio || 1, 2); return function () { var W = canvas.clientWidth, H = canvas.clientHeight; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return { W: W, H: H }; }; }
|
||||
function loop(draw, canvas) {
|
||||
var reduce = root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches, running = false, inView = true;
|
||||
function frame(t) { if (!running) return; draw(t); if (reduce || document.hidden || !inView) { running = false; return; } requestAnimationFrame(frame); }
|
||||
function kick() { if (!running) { running = true; requestAnimationFrame(frame); } }
|
||||
if ('IntersectionObserver' in root) new IntersectionObserver(function (es) { es.forEach(function (e) { inView = e.isIntersecting; if (inView) kick(); }); }, { threshold: 0.05 }).observe(canvas);
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) kick(); });
|
||||
return { kick: kick, reduce: reduce };
|
||||
}
|
||||
root.IgneumFeed = { start: start, theme: theme, rgba: rgba, word: word, hash32: hash32, sizer: sizer, loop: loop };
|
||||
})(window);
|
||||
61
site/scenes/forge.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/* Scene A, Forge. Blocks arrive as embers rising from the bottom, one column per miner. A block cools as its word changes:
|
||||
pending is molten, included is ember, proven is bone. A lock is a dark band that sweeps up the scene at the newest locked
|
||||
checkpoint; everything below it is final and settles to ash. Every ember is a block the observer stored. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var F = root.IgneumFeed;
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme();
|
||||
var blocks = [], byHash = {}, lanes = {}, laneN = 0, lockBlue = null, lockY = null, lockTarget = null, lastT = 0;
|
||||
var WIN = opts.window || 120; // seconds from the hearth to the top; a block's height is its header age, its glow is its arrival
|
||||
function vnow() { return Date.now() - 2500; }
|
||||
root.addEventListener('resize', function () { dims = size(); L.kick(); });
|
||||
function laneX(id) { if (!(id in lanes)) lanes[id] = laneN++; var n = Math.max(1, laneN); var i = lanes[id]; return 24 + ((i * 0.618034) % 1) * (dims.W - 48); }
|
||||
function push(d) {
|
||||
var now = performance.now();
|
||||
d.blocks.forEach(function (b) {
|
||||
var e = byHash[b.hash];
|
||||
if (e) { e.src = b; e.word = F.word(b); return; }
|
||||
e = { hash: b.hash, src: b, word: F.word(b), x: laneX(b.miner || '?') + (F.hash32(b.hash) - 0.5) * 10, born: now, ts: b.ts, glow: 1, settle: 0 };
|
||||
byHash[b.hash] = e; blocks.push(e);
|
||||
});
|
||||
var cps = (d.finality && d.finality.checkpoints) || [], locked = cps.filter(function (c) { return c.state === 'locked'; });
|
||||
lockBlue = locked.length ? locked[locked.length - 1].blue_score : null;
|
||||
blocks.forEach(function (e) { e.final = lockBlue !== null && e.src.blue_score !== null && e.src.blue_score <= lockBlue; });
|
||||
var cut = vnow() - WIN * 1000 - 8000;
|
||||
blocks = blocks.filter(function (e) { if (e.ts < cut) { delete byHash[e.hash]; return false; } return true; });
|
||||
L.kick();
|
||||
}
|
||||
function yOf(e) { return dims.H - 16 - ((vnow() - e.ts) / 1000) * ((dims.H - 32) / WIN); }
|
||||
function draw(t) {
|
||||
var W = dims.W, H = dims.H; T = F.theme();
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
// the hearth: a faint warm floor
|
||||
var g = ctx.createLinearGradient(0, H, 0, H - 90); g.addColorStop(0, F.rgba(T.ember, 0.16)); g.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = g; ctx.fillRect(0, H - 90, W, 90);
|
||||
// the lock band: the lowest final block's height, eased
|
||||
var target = null; blocks.forEach(function (e) { if (e.final) { var y = yOf(e); if (target === null || y > target) target = y; } });
|
||||
if (target !== null) { lockY = lockY === null ? target : lockY + (target - lockY) * 0.08; }
|
||||
if (lockY !== null) { var band = ctx.createLinearGradient(0, lockY - 70, 0, lockY); band.addColorStop(0, F.rgba(T.row, 0)); band.addColorStop(1, F.rgba(T.row, 0.9)); ctx.fillStyle = band; ctx.fillRect(0, lockY - 70, W, 70); ctx.fillStyle = F.rgba(T.row, 0.9); ctx.fillRect(0, 0, W, Math.max(0, lockY - 70));
|
||||
ctx.strokeStyle = F.rgba(T.ember, 0.6); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, lockY); ctx.lineTo(W, lockY); ctx.stroke();
|
||||
ctx.fillStyle = T.ash; ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'bottom'; ctx.fillText('locked', W - 10, lockY - 4); }
|
||||
blocks.forEach(function (e) {
|
||||
var y = yOf(e); if (y < -20) return;
|
||||
var age = (t - e.born) / 1000, w = e.word;
|
||||
var col = w === 'proven' ? T.bone : w === 'included' || w === 'locked' ? T.ember : w === 'excluded' ? T.ash : T.molten;
|
||||
var r = w === 'excluded' ? 2.2 : w === 'proven' ? 3.6 : 3;
|
||||
var a = e.final ? 0.45 : 1;
|
||||
if (age < 1.2) { var gl = 1 - age / 1.2; var rg = ctx.createRadialGradient(e.x, y, 0, e.x, y, 18); rg.addColorStop(0, F.rgba(T.molten, 0.5 * gl)); rg.addColorStop(1, F.rgba(T.molten, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(e.x, y, 18, 0, Math.PI * 2); ctx.fill(); }
|
||||
var flicker = e.final ? 0 : Math.sin(t / 420 + e.x) * 0.6;
|
||||
ctx.globalAlpha = a; ctx.fillStyle = col; ctx.beginPath(); ctx.arc(e.x + flicker, y, r, 0, Math.PI * 2); ctx.fill();
|
||||
if (w === 'locked') { ctx.strokeStyle = F.rgba(T.ember, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(e.x, y, r + 4, 0, Math.PI * 2); ctx.stroke(); }
|
||||
// a thin trail: the ember's rise
|
||||
ctx.strokeStyle = F.rgba(col, 0.12); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(e.x, y + r); ctx.lineTo(e.x, Math.min(H - 16, y + 14)); ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
var L = F.loop(draw, canvas); L.kick();
|
||||
return { push: push, redraw: L.kick };
|
||||
}
|
||||
root.IgneumForge = { mount: mount };
|
||||
})(window);
|
||||
77
site/scenes/lattice.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* Scene B, Lattice. A slow isometric DAG: time runs left to right, each miner lane sits one step deeper, so parents stand
|
||||
behind their children. The selected chain is a lit spine; every other edge is a thin thread. Checkpoints are rings that
|
||||
close around the spine where they lock. The camera drifts at chain speed. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var F = root.IgneumFeed;
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme();
|
||||
var blocks = [], byHash = {}, lanes = {}, laneN = 0, cps = [], windowS = opts.window || 120, rank = {};
|
||||
var DEPTH = 8; // up to DEPTH lanes deep; the isometric step scales with the canvas (about 4.5% of its height per lane)
|
||||
// the step scales with the canvas; the whole shear never takes more than a third of the width, so a narrow card keeps its time axis
|
||||
function stepY() { return Math.max(7, Math.min(dims.H * 0.045, (dims.W * 0.33) / (DEPTH * 1.4))); } function stepX() { return stepY() * 1.4; }
|
||||
root.addEventListener('resize', function () { dims = size(); L.kick(); });
|
||||
function lane(id) { if (!(id in lanes)) lanes[id] = laneN++; return lanes[id]; }
|
||||
function vnow() { return Date.now() - 2500; }
|
||||
function push(d) {
|
||||
var now = performance.now();
|
||||
d.blocks.forEach(function (b) { var e = byHash[b.hash]; if (e) { e.src = b; e.word = F.word(b); return; } e = { hash: b.hash, src: b, word: F.word(b), lane: lane(b.miner || '?'), born: now }; byHash[b.hash] = e; blocks.push(e); });
|
||||
var cut = vnow() - windowS * 1000 - 20000;
|
||||
blocks = blocks.filter(function (e) { if (e.src.ts < cut) { delete byHash[e.hash]; return false; } return true; });
|
||||
blocks.sort(function (a, b) { return a.src.ts - b.src.ts; });
|
||||
// lanes ranked: the miners of the selected chain nearest the front, then by blocks in the window
|
||||
var score = {}; blocks.forEach(function (e) { var m = e.src.miner || '?'; score[m] = (score[m] || 0) + 1 + (e.src.chain ? 3 : 0); });
|
||||
var ids = Object.keys(score).sort(function (a, b) { return score[b] - score[a] || (a < b ? -1 : 1); }); rank = {}; ids.forEach(function (id, i) { rank[id] = i; });
|
||||
cps = (d.finality && d.finality.checkpoints) || [];
|
||||
L.kick();
|
||||
}
|
||||
function project(e) {
|
||||
// the selected chain is the front lane; the other blocks sit one step deeper per miner rank, the many small miners sharing the back
|
||||
var W = dims.W, H = dims.H, r = rank[e.src.miner || '?'] || 0, z = e.src.chain ? 0 : (1 + (r % (DEPTH - 1))) / DEPTH;
|
||||
var SX = stepX(), SY = stepY();
|
||||
var x = 40 + (W - 80 - DEPTH * SX) * (1 - (vnow() - e.src.ts) / (windowS * 1000)) + z * DEPTH * SX;
|
||||
var y = H * 0.74 - z * DEPTH * SY;
|
||||
return { x: x, y: y, z: z };
|
||||
}
|
||||
function draw(t) {
|
||||
var W = dims.W, H = dims.H; T = F.theme();
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
// the ground plane: three faint isometric rails
|
||||
ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1;
|
||||
for (var k = 0; k <= DEPTH; k += 2) { var yy = H * 0.74 - k * stepY(), xx = k * stepX(); ctx.beginPath(); ctx.moveTo(xx, yy); ctx.lineTo(W, yy); ctx.stroke(); }
|
||||
var pos = {}; blocks.forEach(function (e) { pos[e.hash] = project(e); });
|
||||
// threads, back to front
|
||||
var ordered = blocks.slice().sort(function (a, b) { return pos[b.hash].z - pos[a.hash].z; });
|
||||
ordered.forEach(function (e) {
|
||||
var p = pos[e.hash]; if (p.x < -20) return;
|
||||
(e.src.parents || []).forEach(function (h) { var q = byHash[h]; if (!q) return; var pq = pos[h]; var spine = e.src.chain && q.src.chain;
|
||||
ctx.strokeStyle = spine ? F.rgba(T.ember, 0.9) : F.rgba(T.ash, e.word === 'excluded' ? 0.1 : 0.2); ctx.lineWidth = spine ? 2.2 : 1;
|
||||
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(pq.x, pq.y); ctx.stroke();
|
||||
if (spine) { ctx.strokeStyle = F.rgba(T.ember, 0.16); ctx.lineWidth = 8; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(pq.x, pq.y); ctx.stroke(); } });
|
||||
});
|
||||
// checkpoint rings around the spine
|
||||
cps.forEach(function (c) {
|
||||
var ref = null, best = Infinity; blocks.forEach(function (e) { if (!e.src.chain || e.src.blue_score === null) return; var dd = Math.abs(e.src.blue_score - c.blue_score); if (dd < best) { best = dd; ref = e; } });
|
||||
if (!ref || best > 3) return; var p = pos[ref.hash]; if (p.x < 0 || p.x > W) return;
|
||||
var locked = c.state === 'locked', age = (performance.now() - ref.born) / 1000, close = locked ? Math.min(1, age / 1.5) : 0.35;
|
||||
ctx.strokeStyle = F.rgba(T.ember, locked ? 0.85 : 0.35); ctx.lineWidth = locked ? 1.8 : 1; ctx.setLineDash(locked ? [] : [3, 5]);
|
||||
ctx.beginPath(); ctx.ellipse(p.x, p.y, 22 - 8 * close, 9 - 3 * close, 0, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||
if (locked && !opts.compact) { ctx.fillStyle = T.ash; ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText(Math.round((c.fraction_total || 0) * 100) + '%', p.x, p.y - 16); }
|
||||
});
|
||||
// blocks: small isometric tiles, lit by word
|
||||
ordered.forEach(function (e) {
|
||||
var p = pos[e.hash]; if (p.x < -20 || p.x > W + 20) return; var w = e.word, age = (t - e.born) / 1000;
|
||||
var col = w === 'proven' ? T.bone : (w === 'included' || w === 'locked') ? T.ember : w === 'excluded' ? T.ash : T.molten;
|
||||
var s = e.src.chain ? 6 : 4.5, a = w === 'excluded' ? 0.45 : 1 - p.z * 0.35;
|
||||
if (age < 1.4) { var gl = 1 - age / 1.4; var rg = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 20); rg.addColorStop(0, F.rgba(T.ember, 0.45 * gl)); rg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(p.x, p.y, 20, 0, Math.PI * 2); ctx.fill(); }
|
||||
ctx.globalAlpha = a; ctx.fillStyle = w === 'pending' ? T.row : col; ctx.strokeStyle = col; ctx.lineWidth = 1.2;
|
||||
ctx.beginPath(); ctx.moveTo(p.x, p.y - s); ctx.lineTo(p.x + s * 1.3, p.y); ctx.lineTo(p.x, p.y + s); ctx.lineTo(p.x - s * 1.3, p.y); ctx.closePath(); ctx.fill(); ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
var L = F.loop(draw, canvas); L.kick();
|
||||
return { push: push, redraw: L.kick };
|
||||
}
|
||||
root.IgneumLattice = { mount: mount };
|
||||
})(window);
|
||||
60
site/scenes/pulse.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* Scene C, Pulse. A radial view: the tip at the centre, time outward in rings, each miner a ray. A block is a bead on its
|
||||
miner's ray at the radius of its age; a proven bead fills; a locked checkpoint is a ring that hardens from dashed to
|
||||
solid at the radius where it locked. Beads drift outward as time passes. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var F = root.IgneumFeed;
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme();
|
||||
var blocks = [], byHash = {}, rays = {}, rayN = 0, cps = [], windowS = opts.window || 120;
|
||||
root.addEventListener('resize', function () { dims = size(); L.kick(); });
|
||||
function vnow() { return Date.now() - 2500; }
|
||||
function ray(id) { if (!(id in rays)) rays[id] = rayN++; return rays[id]; }
|
||||
function push(d) {
|
||||
var now = performance.now();
|
||||
d.blocks.forEach(function (b) { var e = byHash[b.hash]; if (e) { e.src = b; e.word = F.word(b); return; } e = { hash: b.hash, src: b, word: F.word(b), ray: ray(b.miner || '?'), born: now }; byHash[b.hash] = e; blocks.push(e); });
|
||||
var cut = vnow() - windowS * 1000 - 20000;
|
||||
blocks = blocks.filter(function (e) { if (e.src.ts < cut) { delete byHash[e.hash]; return false; } return true; });
|
||||
cps = (d.finality && d.finality.checkpoints) || [];
|
||||
L.kick();
|
||||
}
|
||||
function polar(e) { var R = Math.min(dims.W, dims.H) / 2 - 14, cx = dims.W / 2, cy = dims.H / 2; var r = 10 + (R - 10) * ((vnow() - e.src.ts) / (windowS * 1000)); var ang = ((e.ray * 0.618034) % 1) * Math.PI * 2 - Math.PI / 2; return { x: cx + Math.cos(ang) * r, y: cy + Math.sin(ang) * r, r: r, ang: ang }; }
|
||||
function draw(t) {
|
||||
var W = dims.W, H = dims.H; T = F.theme(); var cx = W / 2, cy = H / 2, R = Math.min(W, H) / 2 - 14;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
// time rings every 30 s
|
||||
ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1;
|
||||
for (var s = 30; s < windowS; s += 30) { var rr = 10 + (R - 10) * (s / windowS); ctx.beginPath(); ctx.arc(cx, cy, rr, 0, Math.PI * 2); ctx.stroke(); }
|
||||
// the tip
|
||||
var pulse = 0.5 + 0.5 * Math.sin(t / 900); var tg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 26); tg.addColorStop(0, F.rgba(T.ember, 0.35 + 0.2 * pulse)); tg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = tg; ctx.beginPath(); ctx.arc(cx, cy, 26, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = T.ember; ctx.beginPath(); ctx.arc(cx, cy, 3.5, 0, Math.PI * 2); ctx.fill();
|
||||
// checkpoint rings, hardening when locked
|
||||
cps.forEach(function (c) {
|
||||
var ref = null, best = Infinity; blocks.forEach(function (e) { if (e.src.blue_score === null) return; var dd = Math.abs(e.src.blue_score - c.blue_score); if (dd < best) { best = dd; ref = e; } });
|
||||
if (!ref || best > 3) return; var p = polar(ref); if (p.r > R) return; var locked = c.state === 'locked', age = (performance.now() - ref.born) / 1000, hard = locked ? Math.min(1, age / 2) : 0;
|
||||
ctx.strokeStyle = F.rgba(T.ember, locked ? 0.35 + 0.5 * hard : 0.3); ctx.lineWidth = locked ? 1 + hard : 1; ctx.setLineDash(locked && hard > 0.9 ? [] : [3, 6]);
|
||||
ctx.beginPath(); ctx.arc(cx, cy, p.r, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||
});
|
||||
// the selected chain as a soft spiral thread through its beads
|
||||
var chain = blocks.filter(function (e) { return e.src.chain; }).sort(function (a, b) { return a.src.ts - b.src.ts; });
|
||||
// the selected chain as short arcs between consecutive chain beads (the arc follows the ring between the two radii, never a chord through the centre)
|
||||
if (chain.length > 1) { ctx.strokeStyle = F.rgba(T.ember, 0.28); ctx.lineWidth = 1; for (var i = 1; i < chain.length; i++) { var p0 = polar(chain[i - 1]), p1 = polar(chain[i]); if (p0.r > R || p1.r > R) continue; var a0 = p0.ang, a1 = p1.ang, da = ((a1 - a0 + Math.PI * 3) % (Math.PI * 2)) - Math.PI; var steps = 10; ctx.beginPath(); for (var k = 0; k <= steps; k++) { var f = k / steps, rr = p0.r + (p1.r - p0.r) * f, aa = a0 + da * f; var x = cx + Math.cos(aa) * rr, y = cy + Math.sin(aa) * rr; if (k) ctx.lineTo(x, y); else ctx.moveTo(x, y); } ctx.stroke(); } }
|
||||
// beads
|
||||
blocks.forEach(function (e) {
|
||||
var p = polar(e); if (p.r > R + 6) return; var w = e.word, age = (t - e.born) / 1000;
|
||||
var col = w === 'proven' ? T.bone : (w === 'included' || w === 'locked') ? T.ember : w === 'excluded' ? T.ash : T.molten;
|
||||
var rad = e.src.chain ? 4 : 3;
|
||||
if (age < 1.2) { var gl = 1 - age / 1.2; var rg = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 16); rg.addColorStop(0, F.rgba(T.ember, 0.5 * gl)); rg.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(p.x, p.y, 16, 0, Math.PI * 2); ctx.fill(); }
|
||||
ctx.globalAlpha = w === 'excluded' ? 0.45 : 1;
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, rad, 0, Math.PI * 2);
|
||||
if (w === 'proven' || w === 'included' || w === 'locked') { ctx.fillStyle = col; ctx.fill(); } else { ctx.fillStyle = T.row; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.2; ctx.stroke(); }
|
||||
if (w === 'locked') { ctx.strokeStyle = F.rgba(T.ember, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(p.x, p.y, rad + 4, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
var L = F.loop(draw, canvas); L.kick();
|
||||
return { push: push, redraw: L.kick };
|
||||
}
|
||||
root.IgneumPulse = { mount: mount };
|
||||
})(window);
|
||||