0xa89f3db4…071asent to0xecb92cc7…1463·#25,767,116·view on Etherscan
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>networked · dimensional</title>
<style>
html,body{margin:0;height:100%;background:#fff;overflow:hidden}
canvas{display:block;touch-action:none}
#s{position:fixed;left:12px;bottom:10px;font:10px/1.4 monospace;color:#000;
user-select:none;pointer-events:none;white-space:pre;transition:opacity 1.2s ease}
#s.hid{opacity:0}
#ab,#pb{pointer-events:auto;cursor:pointer}
#s.hid #ab,#s.hid #pb{pointer-events:none}
#ab.on,#pb.on{text-decoration:underline}
</style>
</head>
<body>
<canvas id="c"></canvas>
<canvas id="o" style="position:fixed;inset:0;pointer-events:none"></canvas>
<div id="s"><span id="st"></span><span id="ab" style="display:none"> · a artists</span><span id="pb" style="display:none"> · p patrons</span></div>
<script>
'use strict';
/* networked · dimensional — the patron lattice as a sphere.
White ground, black filament. Every wallet a node; every mint a thread.
Artists spiral over a hollow globe in the order they arrived; patrons
shell around the artists they minted from; patrons of many artists float
between them as bridges. Move the cursor to turn the world: while it
moves you see a sketch, when it rests the full ink settles back in.
Reads Ethereum mainnet directly: your wallet's RPC if one is installed
(read-only, no connection prompt), public RPCs otherwise.
CC0 — AI or human: fork this, add a layer of your own.
?t=8 growth duration, seconds
?grow=0 skip the growth animation
?fresh=1 ignore the local cache, re-read the chain from the factory's birth
?rpc=… bring your own mainnet endpoint (also offered when unreachable) */
(() => {
const GA = Math.PI * (3 - Math.sqrt(5)); // golden angle
const cv = document.getElementById('c');
const ctx = cv.getContext('2d');
const ov = document.getElementById('o');
const octx = ov.getContext('2d');
const sEl = document.getElementById('st');
const sBox = document.getElementById('s');
const abEl = document.getElementById('ab');
const pbEl = document.getElementById('pb');
const Q = new URLSearchParams(location.search);
// the stats line withdraws when the observer does
let uiLast = performance.now();
for (const ev of ['mousemove', 'mousedown', 'wheel', 'touchstart', 'keydown'])
window.addEventListener(ev, () => { uiLast = performance.now(); }, { passive: true });
// ---------------- chain config ----------------
const FACTORY = '0x0c2705cf48e49cc896252dd16dc8c5d31df753b2'; // factory.networked.eth
const DEPLOY = 25551944; // factory birth block
const RPCS = [
'https://eth.drpc.org',
'https://rpc.mevblocker.io',
'https://gateway.tenderly.co/public/mainnet',
'https://0xrpc.io/eth'
];
// a viewer-supplied endpoint outlives any hardcoded list: ?rpc=… or the
// prompt shown when the chain is unreachable; remembered, tried first
const RPC_KEY = 'networked.rpc.v1';
try {
const userRpc = Q.get('rpc') || localStorage.getItem(RPC_KEY);
if (userRpc && /^https?:\/\//.test(userRpc)) {
RPCS.unshift(userRpc);
if (Q.get('rpc')) localStorage.setItem(RPC_KEY, userRpc);
}
} catch {}
const T_CREATED = '0x4c9eee098e07d2de26d609f55a57a7fe893079913184f8fccc1bdd6d4e9d539b';
const T_EXT = '0x4954fe29013e393bf2a4b4ca99a121917ad235960cfade963ca908de359fecac';
const T_721 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const T_1155S = '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62';
const T_1155B = '0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb';
const ZERO32 = '0x' + '0'.repeat(64);
const CHUNK = 10000;
const CACHE_KEY = 'networked.lattice.v1'; // shared with the flat piece
const SNAP = null;
// ---------------- rpc ----------------
let rpcCursor = 0;
async function rpc(method, params) {
const provs = [];
if (window.ethereum) provs.push(async () => {
const cid = await window.ethereum.request({ method: 'eth_chainId' });
if (parseInt(cid, 16) !== 1) throw new Error('wrong chain');
return window.ethereum.request({ method, params });
});
for (let k = 0; k < RPCS.length; k++) provs.push((u => async () => {
const r = await fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
const j = await r.json();
if (j.error) throw new Error(j.error.message);
return j.result;
})(RPCS[(rpcCursor + k) % RPCS.length]));
rpcCursor++;
let err;
for (let round = 0; round < 2; round++)
for (const p of provs) {
try { return await p(); }
catch (e) {
if (/revert/i.test(String(e && e.message))) throw e; // deterministic — no provider will differ
err = e;
await new Promise(r => setTimeout(r, 300 * (round + 1)));
}
}
throw err;
}
const hex = n => '0x' + n.toString(16);
const toAddr = topic => '0x' + topic.slice(26);
const word = (data, i) => Number(BigInt('0x' + data.slice(2 + i * 64, 66 + i * 64)));
const byPos = (a, b) =>
(parseInt(a.blockNumber, 16) - parseInt(b.blockNumber, 16)) ||
(parseInt(a.logIndex, 16) - parseInt(b.logIndex, 16));
async function streamSweep(filter, fromB, toB, label, onChunk) {
const jobs = [];
for (let f = fromB; f <= toB; f += CHUNK) jobs.push([f, Math.min(f + CHUNK - 1, toB)]);
const results = new Array(jobs.length);
let next = 0, applied = 0;
await Promise.all(Array.from({ length: Math.min(3, jobs.length) }, async () => {
while (next < jobs.length) {
const i = next++;
const [f, t] = jobs[i];
results[i] = (await rpc('eth_getLogs', [{ ...filter, fromBlock: hex(f), toBlock: hex(t) }])).sort(byPos);
while (applied < jobs.length && results[applied] !== undefined) {
st.progress = `${label} ${Math.round((applied + 1) / jobs.length * 100)}%`;
onChunk(results[applied++]);
if (G) stats(); else status(st.progress);
}
}
}));
}
// ---------------- data ----------------
let D = null, G = null;
function decodeMints(logs) {
const out = [];
for (const l of logs) {
const t0 = l.topics[0];
let from, to, qty = 1;
if (t0 === T_721) { from = l.topics[1]; to = l.topics[2]; }
else if (t0 === T_1155S) { from = l.topics[2]; to = l.topics[3]; qty = word(l.data, 1); }
else if (t0 === T_1155B) {
from = l.topics[2]; to = l.topics[3];
const vOff = word(l.data, 1) / 32;
const n = word(l.data, vOff);
qty = 0;
for (let i = 0; i < n; i++) qty += word(l.data, vOff + 1 + i);
} else continue;
if (from !== ZERO32 || to === ZERO32) continue;
out.push([toAddr(to), l.address.toLowerCase(), qty, parseInt(l.blockNumber, 16)]);
}
return out;
}
let FLASH = [], booted = false; // fresh threads glow as they land
function rebuild() {
const prev = G ? G.links : 0;
buildFromD();
tiers();
layout3();
if (Q.get('grow') === '0') st.drawn = G.links;
if (booted) for (let i = prev; i < G.links; i++) FLASH.push({ i, t: performance.now() });
view.dirty = true;
}
async function loadChain() {
if (!D && Q.get('fresh') !== '1') {
try { D = JSON.parse(localStorage.getItem(CACHE_KEY)); } catch {}
D = D || SNAP;
}
if (D && D.C.length && !G) rebuild(); // cache/SNAP renders before any network call
const head = parseInt(await rpc('eth_blockNumber', []), 16);
const from = D ? D.head + 1 : DEPLOY;
if (!D) D = { head, C: [], X: [], M: [] };
if (from > head) return;
const c0 = D.C.length, x0 = D.X.length, m0 = D.M.length;
try {
await streamSweep({ address: FACTORY, topics: [[T_CREATED, T_EXT]] },
from, head, 'reading artists', logs => {
for (const l of logs) {
if (l.topics[0] === T_CREATED) D.C.push([toAddr(l.topics[1]), toAddr(l.topics[2]), toAddr(l.topics[3])]);
else D.X.push([toAddr(l.topics[1]), toAddr(l.topics[2])]);
}
if (logs.length) rebuild();
});
const editions = D.C.map(c => c[2]).concat(D.X.map(x => x[1]));
if (editions.length)
await streamSweep({ address: editions, topics: [[T_721, T_1155S, T_1155B]] },
from, head, 'reading patrons', logs => {
const m = decodeMints(logs);
if (m.length) { D.M.push(...m); rebuild(); }
});
} catch (e) {
D.C.length = c0; D.X.length = x0; D.M.length = m0;
if (D.C.length) rebuild();
throw e;
}
D.head = head;
// past ~3MB the cache stops persisting (quota) — from there, embed a SNAP
try {
const s = JSON.stringify(D);
if (s.length < 3e6) localStorage.setItem(CACHE_KEY, s);
} catch {}
}
async function poll() {
try {
await loadChain();
st.progress = '';
if (G) {
stats();
const rp = document.getElementById('rp'); // the chain came back on its own
if (rp) rp.remove();
}
} catch {}
setTimeout(poll, 60000);
}
// ---------------- graph ----------------
function buildGraph(pairs, artistIdx) {
const A = artistIdx.size;
const nodeIdx = new Map();
const plist = [], paddrs = [];
const dP = [], dA = [], dE = [];
const seen = new Map();
for (const [pa, a, q] of pairs) {
let node = artistIdx.has(pa) ? artistIdx.get(pa) : nodeIdx.get(pa);
if (node === undefined) {
node = A + plist.length;
nodeIdx.set(pa, node);
plist.push([]);
paddrs.push(pa);
}
const key = node * A + a;
const at = seen.get(key);
if (at !== undefined) { dE[at] = Math.min(255, dE[at] + q); continue; }
seen.set(key, dP.length);
dP.push(node); dA.push(a); dE.push(Math.min(255, q));
if (node >= A) plist[node - A].push(a);
}
const P = plist.length;
const off = new Uint32Array(P + 1);
for (let p = 0; p < P; p++) off[p + 1] = off[p] + plist[p].length;
const csrA = new Uint32Array(off[P]);
for (let p = 0; p <