memoscan
← all memos

Memo 0x5453ab59…da247b on Ethereum

< 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 { const before = HOLDERS ? HOLDERS.size : 0; await loadVessel(); if (HOLDERS.size !== before && D) rebuild(); 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 < P; p++) csrA.set(plist[p], off[p]); G = { A, P, links: dP.length, dP: Uint32Array.from(dP), dA: Uint32Array.from(dA), dE: Uint8Array.from(dE), off, csrA, paddrs, wcum: walletsByThread(A + P, dP, dA), aFirst: artistArrivals(A, dP, dA) }; } // the thread at which each artist first appears — during growth an artist // is invisible until the web reaches them function artistArrivals(A, dP, dA) { const aFirst = new Uint32Array(A).fill(0xffffffff); for (let i = 0; i < dA.length; i++) { if (aFirst[dA[i]] === 0xffffffff) aFirst[dA[i]] = i; if (dP[i] < A && aFirst[dP[i]] === 0xffffffff) aFirst[dP[i]] = i; } return aFirst; } // wallets touched after each thread — the counter grows with the web function walletsByThread(n, dP, dA) { const seen = new Uint8Array(n); const wcum = new Uint32Array(dP.length + 1); let wc = 0; for (let i = 0; i < dP.length; i++) { if (!seen[dP[i]]) { seen[dP[i]] = 1; wc++; } if (!seen[dA[i]]) { seen[dA[i]] = 1; wc++; } wcum[i + 1] = wc; } return wcum; } function buildFromD() { const artistIdx = new Map(); const edToA = new Map(), artToA = new Map(); for (const [artist, art, ed] of D.C) { if (!HOLDERS || HOLDERS.has(artist)) { // the hull is absolute: holder artists only if (!artistIdx.has(artist)) artistIdx.set(artist, artistIdx.size); } edToA.set(ed, artist); artToA.set(art, artist); } for (const [art, ed] of D.X) if (artToA.has(art)) edToA.set(ed, artToA.get(art)); const pairs = []; for (const [patron, ed, qty] of D.M) { if (HOLDERS && !HOLDERS.has(patron)) continue; // patron must hold… const artist = edToA.get(ed); if (artist === undefined || !artistIdx.has(artist)) continue; // …and so must the artist pairs.push([patron, artistIdx.get(artist), qty]); } buildGraph(pairs, artistIdx); G.addrs = [...artistIdx.keys()]; // artist addresses, index-aligned if (st.artists) ensureNames(G.addrs); if (st.patrons) ensureNames(G.paddrs); if (SEL) { if (SEL.node < G.A + G.P) reselect(SEL.node); else SEL = null; } } // ---------------- 3d layout ---------------- // the flat spiral becomes a globe: artists wind over a hollow sphere in // arrival order, their patrons shell around them, bridges float between const hash01 = i => { let h = Math.imul(i ^ 0x9E3779B9, 0x85EBCA6B); h = Math.imul(h ^ h >>> 13, 0xC2B2AE35); return ((h ^ h >>> 16) >>> 0) / 4294967296; }; let mx, my, mz; // model space, radius ~1 function layout3() { const { A, P, off, csrA } = G; mx = new Float32Array(A + P); my = new Float32Array(A + P); mz = new Float32Array(A + P); const eye = A * 0.18; // directions live on a true fibonacci sphere, but are dealt out in hash-shuffled // order so the earliest artists ring the hollow core evenly instead of piling // at a pole; only the radius remembers arrival const perm = Uint32Array.from({ length: A }, (_, i) => i) .sort((a, b) => hash01(a) - hash01(b)); for (let i = 0; i < A; i++) { const d = perm[i]; const y = 1 - 2 * (d + 0.5) / A; const rr = Math.sqrt(Math.max(0, 1 - y * y)); const th = d * GA; const r = Math.sqrt((i + 0.5 + eye) / (A + eye)); // hollow-core radial growth mx[i] = r * rr * Math.cos(th); my[i] = r * y; mz[i] = r * rr * Math.sin(th); } const clusterR = 0.9 / Math.cbrt(A) + 0.03; const orbN = new Uint32Array(A); for (let p = 0; p < P; p++) if (off[p + 1] - off[p] === 1) orbN[csrA[off[p]]]++; const orbSeen = new Uint32Array(A); for (let p = 0; p < P; p++) { const k = off[p + 1] - off[p]; const n0 = A + p; if (k <= 1) { const a = csrA[off[p]]; const j = orbSeen[a]++; const n = orbN[a] || 1; const y = 1 - 2 * (j + 0.5) / n; // shell around the artist const rr = Math.sqrt(Math.max(0, 1 - y * y)); const th = j * GA + a * 1.7; const r = clusterR * (0.3 + 0.7 * Math.sqrt((j + 0.5) / n)); mx[n0] = mx[a] + r * rr * Math.cos(th); my[n0] = my[a] + r * y; mz[n0] = mz[a] + r * rr * Math.sin(th); } else { let sx = 0, sy = 0, sz = 0; for (let q = off[p]; q < off[p + 1]; q++) { sx += mx[csrA[q]]; sy += my[csrA[q]]; sz += mz[csrA[q]]; } const jr = clusterR * (0.4 + 0.35 * Math.sqrt(k)) * hash01(p); const u = hash01(p ^ 0x5F356495) * Math.PI * 2; const v = hash01(p ^ 0x2545F491) * 2 - 1; const vr = Math.sqrt(Math.max(0, 1 - v * v)); mx[n0] = sx / k + jr * vr * Math.cos(u); my[n0] = sy / k + jr * v; mz[n0] = sz / k + jr * vr * Math.sin(u); } } } // ---------------- view / projection ---------------- const view = { yaw: 0.6, pitch: -0.25, tYaw: 0.6, tPitch: -0.25, zoom: 1, tZoom: 1, lastInput: -1e9, dirty: true, full: 0, // F is the lens: focal distance in model units (globe radius ~1). // Short F = wide angle = rim bulge while turning; 9 is a calm telephoto. cy: 1, sy: 0, cx2: 1, sx2: 0, cxp: 0, cyp: 0, S: 1, F: 9 }; function setMatrix(w, h) { view.cy = Math.cos(view.yaw); view.sy = Math.sin(view.yaw); view.cx2 = Math.cos(view.pitch); view.sx2 = Math.sin(view.pitch); view.cxp = w / 2; view.cyp = h / 2; view.S = Math.min(w, h) * 0.33 * view.zoom; } // project node i -> screen [x, y, k] (k = perspective scale, for dot sizing) const pr = [0, 0, 0]; function project(i) { const x = mx[i], y = my[i], z = mz[i]; const x1 = x * view.cy + z * view.sy; const z1 = -x * view.sy + z * view.cy; const y1 = y * view.cx2 - z1 * view.sx2; const z2 = y * view.sx2 + z1 * view.cx2; const k = view.F / (view.F + z2); pr[0] = view.cxp + x1 * k * view.S; pr[1] = view.cyp + y1 * k * view.S; pr[2] = k; return pr; } // ---------------- render ---------------- const st = { drawn: 0, chunk: 0, alpha: 1, dots: true, dpr: 1, progress: '', artists: false, patrons: false }; function tiers() { const E = Math.max(1, G.links); st.alpha = Math.min(0.55, Math.max(0.004, 8 / Math.pow(E, 0.35))); st.dots = G.P < 60000; st.dpr = E > 500000 ? 1 : Math.min(2, window.devicePixelRatio || 1); const secs = Math.max(1, parseFloat(Q.get('t') || '8')); st.chunk = Math.max(8, Math.ceil(E / (secs * 60))); } function sizeCanvas() { const w = window.innerWidth, h = window.innerHeight; cv.width = w * st.dpr; cv.height = h * st.dpr; cv.style.width = w + 'px'; cv.style.height = h + 'px'; ctx.setTransform(st.dpr, 0, 0, st.dpr, 0, 0); ov.width = w * st.dpr; ov.height = h * st.dpr; ov.style.width = w + 'px'; ov.style.height = h + 'px'; octx.setTransform(st.dpr, 0, 0, st.dpr, 0, 0); return [w, h]; } function clearAll() { ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); } function drawArtists() { const growing = st.drawn < G.links; for (let i = 0; i < G.A; i++) { if (growing && G.aFirst[i] > st.drawn) continue; // not yet reached by the web ctx.fillStyle = !SEL || SEL.node === i || SEL.nbrs.has