0xa89f3db4…071asent to0xecb92cc7…1463·#25,767,887·view on Etherscan
l budget: at future scale a full sweep would be soup — the earliest
// arrivals (the founders) get named first, zoom + click reach the rest
if (st.artists) for (let i = 0; i < G.A && done.size < 900; i++) put(i, aName(i));
if (st.patrons) for (let p = 0; p < G.P && done.size < 1800; p++) put(G.A + p, pName(G.A + p));
}
// ---------------- main loop ----------------
// moving: cleared sketch each frame (sampled threads, boosted alpha)
// at rest: one clear, then the full ink settles back in over frames
const SKETCH_MAX = 30000;
let rafId = 0;
function tick(now) {
if (!G) { rafId = requestAnimationFrame(tick); return; }
const w = window.innerWidth, h = window.innerHeight;
// idle: the globe turns, slowly, always — the ink gets its 3s to settle first
if (now - view.lastInput > 3000 && !drag.on) view.tYaw += 0.0008;
// stats fade with the observer's attention; never while the chain is streaming
sBox.classList.toggle('hid',
now - uiLast > 4000 && !st.progress && !!G && st.drawn >= G.links);
const d = Math.abs(view.tYaw - view.yaw) + Math.abs(view.tPitch - view.pitch) +
Math.abs(view.tZoom - view.zoom);
const moving = d > 0.0006;
if (moving) {
view.yaw += (view.tYaw - view.yaw) * 0.12;
view.pitch += (view.tPitch - view.pitch) * 0.12;
view.zoom += (view.tZoom - view.zoom) * 0.18;
}
setMatrix(w, h);
// growth advances regardless of rotation
if (st.drawn < G.links) {
st.drawn = Math.min(G.links, st.drawn + st.chunk);
stats();
}
if (moving) {
clearAll();
const stride = Math.max(1, Math.ceil(st.drawn / SKETCH_MAX));
if (st.drawn) drawLinks3(0, st.drawn, Math.min(0.7, st.alpha * Math.sqrt(stride)), stride);
drawArtists();
view.dirty = true;
view.full = 0;
} else {
if (view.dirty) { // first still frame: restart the ink
clearAll();
drawArtists();
view.full = 0;
view.dirty = false;
}
if (view.full < st.drawn) {
const fast = Math.max(st.chunk * 12, 20000);
const to = Math.min(st.drawn, view.full + fast);
drawLinks3(view.full, to, st.alpha, 1);
view.full = to;
}
}
drawLabels();
if (FLASH.length) { // fresh threads glow, then fade into the web
FLASH = FLASH.filter(f => now - f.t < 5000);
for (const f of FLASH) {
const a = 0.9 * (1 - (now - f.t) / 5000);
const p1 = project(G.dP[f.i]);
const x1 = p1[0], y1 = p1[1];
const p2 = project(G.dA[f.i]);
octx.strokeStyle = `rgba(255,255,255,${a})`;
octx.lineWidth = 1.6;
octx.beginPath();
octx.moveTo(x1, y1);
octx.lineTo(p2[0], p2[1]);
octx.stroke();
}
}
rafId = requestAnimationFrame(tick);
}
// ---------------- input ----------------
// grab the globe: drag rotates so the face under the cursor follows the hand,
// release keeps a little inertia
const drag = { on: false, x: 0, y: 0, x0: 0, y0: 0, vx: 0, vy: 0 };
const clampPitch = v => Math.min(1.4, Math.max(-1.4, v));
function dragBy(dx, dy) {
const k = Math.PI / Math.min(window.innerWidth, window.innerHeight);
drag.vx = -dx * k; drag.vy = dy * k;
view.tYaw += drag.vx;
view.tPitch = clampPitch(view.tPitch + drag.vy);
view.lastInput = performance.now();
}
cv.style.cursor = 'grab';
window.addEventListener('mousedown', e => {
drag.on = true; drag.x = drag.x0 = e.clientX; drag.y = drag.y0 = e.clientY;
drag.vx = drag.vy = 0; cv.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', e => {
if (!drag.on) {
cv.style.cursor = hitLabel(e.clientX, e.clientY) >= 0 ? 'pointer' : 'grab';
return;
}
dragBy(e.clientX - drag.x, e.clientY - drag.y);
drag.x = e.clientX; drag.y = e.clientY;
});
window.addEventListener('mouseup', e => {
drag.on = false; cv.style.cursor = 'grab';
if (Math.hypot(e.clientX - drag.x0, e.clientY - drag.y0) > 5) {
view.tYaw += drag.vx * 12; // a real drag: the throw carries
view.tPitch = clampPitch(view.tPitch + drag.vy * 12);
} else if (G && e.target === cv) { // a click: jump to that node's network
let hit = hitLabel(e.clientX, e.clientY); // a name counts as its node
if (hit < 0) hit = hitTest(e.clientX, e.clientY);
if (hit >= 0) select(hit); else clearSel();
}
});
window.addEventListener('mouseleave', () => { drag.on = false; cv.style.cursor = 'grab'; });
// zoom: wheel dollies exponentially (trackpad pinch arrives as ctrl+wheel),
// two-finger pinch on touch, double-click resets — the orbit-view conventions
function zoomBy(f) {
view.tZoom = Math.min(10, Math.max(0.4, view.tZoom * f));
view.lastInput = performance.now();
}
window.addEventListener('wheel', e => {
e.preventDefault();
zoomBy(Math.exp(-e.deltaY * (e.ctrlKey ? 0.01 : 0.0015)));
}, { passive: false });
window.addEventListener('dblclick', () => { view.tZoom = 1; view.lastInput = performance.now(); });
let pinchD = 0;
window.addEventListener('touchstart', e => {
if (e.touches.length === 2)
pinchD = Math.hypot(e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY);
});
window.addEventListener('touchstart', e => {
if (e.touches.length === 1) {
drag.x = drag.x0 = e.touches[0].clientX;
drag.y = drag.y0 = e.touches[0].clientY;
}
});
window.addEventListener('touchmove', e => {
e.preventDefault();
if (e.touches.length === 2) {
const d = Math.hypot(e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY);
if (pinchD) zoomBy(d / pinchD);
pinchD = d;
} else {
const t = e.touches[0];
dragBy(t.clientX - drag.x, t.clientY - drag.y);
drag.x = t.clientX; drag.y = t.clientY;
}
}, { passive: false });
window.addEventListener('touchend', e => {
const wasPinch = pinchD !== 0;
pinchD = 0;
if (wasPinch || !G || !e.changedTouches.length) return;
const t = e.changedTouches[0];
if (Math.hypot(t.clientX - drag.x0, t.clientY - drag.y0) <= 8 && e.target === cv) {
let hit = hitLabel(t.clientX, t.clientY); // a tap on a name counts as its node
if (hit < 0) hit = hitTest(t.clientX, t.clientY);
if (hit >= 0) select(hit); else clearSel();
}
});
window.addEventListener('keydown', e => {
const el = document.activeElement;
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' ||
el.tagName === 'SELECT' || el.isContentEditable)) return;
const k = e.key.toLowerCase();
if (k === 'a') toggleArtists();
else if (k === 'p') togglePatrons();
else if (k === 'escape') clearSel();
});
let rT = 0;
window.addEventListener('resize', () => {
clearTimeout(rT);
rT = setTimeout(() => { if (G) { tiers(); sizeCanvas(); view.dirty = true; } }, 100);
});
// ---------------- boot ----------------
async function boot() {
sizeCanvas();
rafId = requestAnimationFrame(tick);
status('reading the vessel…');
try {
await loadVessel();
await loadChain();
st.progress = '';
if (G) stats();
} catch (e) {
st.progress = '';
if (G) stats();
else {
status('the chain is unreachable');
ctx.fillStyle = '#fff';
ctx.fillRect(window.innerWidth / 2 - 1, window.innerHeight / 2 - 1, 2, 2);
rpcPrompt();
}
}
booted = true; // from here on, new threads flash as they land
setTimeout(poll, 60000);
}
// last resort: let the viewer bring their own endpoint. heals in memory and
// in place — on-chain contexts (data: URIs, sandboxed iframes) have no
// storage and no query string, so persistence is a bonus, never a dependency
function rpcPrompt() {
if (document.getElementById('rp')) return;
const inp = document.createElement('input');
inp.id = 'rp';
inp.placeholder = 'paste a mainnet rpc url and press enter…';
inp.style.cssText = 'position:fixed;left:12px;bottom:28px;width:300px;' +
'font:10px monospace;color:#fff;background:#000;border:1px solid #fff;' +
'padding:3px 5px;outline:none';
inp.addEventListener('keydown', async e => {
if (e.key !== 'Enter') return;
const v = inp.value.trim();
if (!/^https?:\/\//.test(v)) return;
try { localStorage.setItem(RPC_KEY, v); } catch {} // remembered where origins allow
RPCS.unshift(v); // healed right now, regardless
inp.disabled = true;
status('reading the chain…');
try {
await loadChain();
st.progress = '';
stats();
inp.remove();
} catch (err) {
status('the chain is unreachable');
inp.disabled = false;
inp.select();
}
});
document.body.appendChild(inp);
}
boot();
})();
</script>
</body>
</html>