0xa89f3db4…071asent to0xecb92cc7…1463·#25,767,118·view on Etherscan
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