memoscan
← all memos

Memo 0x602b7577…49ac28 on Ethereum

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 (!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) { const artist = edToA.get(ed); if (artist !== undefined) 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(i) ? '#000' : 'rgba(0,0,0,0.12)'; const p = project(i); const r = Math.max(1, (2.4 - 1.6 * (i / G.A)) * p[2] * 0.8); ctx.fillRect(p[0] - r / 2, p[1] - r / 2, r, r); } } // one stroking pass over [from,to): fixed absolute batches so accumulation is // repaint-invariant; pred filters which links belong to this pass function strokePass(from, to, stride, alpha, pred, lw) { const { dP, dA } = G; ctx.strokeStyle = `rgba(0,0,0,${alpha})`; ctx.lineWidth = lw || (G.links > 150000 ? 0.5 : 0.8); const B = (G.links > 200000 ? 64 : 8) * stride; let i = from; while (i < to) { const end = Math.min(to, (Math.floor(i / B) + 1) * B); ctx.beginPath(); for (; i < end; i += stride) { if (pred && !pred(i)) continue; const a = project(dP[i]); const x1 = a[0], y1 = a[1]; const b = project(dA[i]); ctx.moveTo(x1, y1); ctx.lineTo(b[0], b[1]); } ctx.stroke(); } } function drawLinks3(from, to, alpha, stride) { if (SEL) { // the focused node's threads at full strength, the rest of the web ghosted strokePass(from, to, stride, alpha * 0.1, i => !inSel(i)); strokePass(from, to, stride, Math.min(0.75, Math.max(0.5, alpha * 3)), inSel, 1.4); } else { strokePass(from, to, stride, alpha, null); } if (st.dots && stride === 1) { const { dP } = G; for (let j = from; j < to; j++) if (dP[j] >= G.A) { ctx.fillStyle = !SEL || dP[j] === SEL.node || SEL.nbrs.has(dP[j]) ? 'rgba(0,0,0,0.9)' : 'rgba(0,0,0,0.12)'; const p = project(dP[j]); const r = Math.max(0.6, p[2] * 0.9); ctx.fillRect(p[0] - r / 2, p[1] - r / 2, r, r); } } } const fmt = n => n.toLocaleString('en-US'); function status(t) { sEl.textContent = t; } // while a node is focused, the stats line says exactly what you are looking at function selInfo() { let a = 0, p = 0; for (const nb of SEL.nbrs) nb < G.A ? a++ : p++; const nm = SEL.node < G.A ? (G.addrs ? nameOf(G.addrs[SEL.node]) : 'artist ' + SEL.node) : (G.paddrs && G.paddrs[SEL.node - G.A] ? nameOf(G.paddrs[SEL.node - G.A]) : 'patron ' + (SEL.node - G.A)); const parts = []; if (a) parts.push(`collects from ${fmt(a)} artist${a > 1 ? 's' : ''}`); if (p) parts.push(`collected by ${fmt(p)} patron${p > 1 ? 's' : ''}`); return nm + (parts.length ? ' · ' + parts.join(' · ') : ' · no connections yet'); } function stats() { // while growing, count only the wallets the web has reached so far; // complete, count the whole constellation (unlinked artists included) const w = st.drawn < G.links ? G.wcum[st.drawn] : G.A + G.P; status((SEL ? selInfo() : `${fmt(w)} wallets · ${fmt(st.drawn)} connections`) + (st.progress ? ` · ${st.progress}` : '')); const show = G && G.addrs ? '' : 'none'; // names need real addresses abEl.style.display = show; pbEl.style.display = show; } function toggleArtists() { st.artists = !st.artists; abEl.classList.toggle('on', st.artists); if (st.artists && G) ensureNames(G.addrs); } function togglePatrons() { st.patrons = !st.patrons; pbEl.classList.toggle('on', st.patrons); if (st.patrons && G) ensureNames(G.paddrs); } abEl.addEventListener('clic