PageSourceSearch

https://biodynai.com/neuron-figure.js

js biodynai.com collected 2026-10-08 10:07:47 UTC 38,031 bytes, 948 lines download raw bytes

1/* ========================================
2   BIODYN, Hero neuron, geometry
3   A neuron assembled from one function per
4   compartment, in the same point-cloud language
5   as the cell figure.
6
7   The cell version was legible only to people who
8   already knew what they were looking at. A
9   neuron has a silhouette almost nobody misreads:
10   an arbor above, a body, a long process below,
11   an arbor again at the end.
12   ======================================== */
13
14(() => {
15  'use strict';
16
17  const A = window.BiodynAbout;
18  if (!A) return;
19
20  /* ---------------------------------------
21     Compartments
22     Each returns skeleton nodes as
23     [x, y, z, intensity] plus a flat list of
24     index pairs. A compartment that has to sit
25     on another one takes the parent's nodes as
26     a second argument, because placing it blind
27     puts it in the gaps instead.
28     --------------------------------------- */
29
30  const soma = (rand) => {
31    const nodes = [], edges = [];
32    const N = 234, R = 0.16, GA = Math.PI * (3 - Math.sqrt(5));
33    for (let i = 0; i < N; i++) {
34      const uy = 1 - (2 * i + 1) / N;
35      const rr = Math.sqrt(Math.max(0, 1 - uy * uy));
36      const th = GA * i + (rand() - 0.5) * 0.12;
37      const ux = Math.cos(th) * rr, uz = Math.sin(th) * rr;
38      let lat = 1 + 0.20 * Math.max(0, uy) * (1 - 0.30 * uy);
39      if (uy < -0.30) { const q = (-uy - 0.30) / 0.70; lat *= 1 + 0.17 * Math.pow(q, 1.5); }
40      const wob = 1 + (rand() - 0.5) * 0.05;
41      nodes.push([
42        R * ux * lat * wob,
43        R * uy * (uy > 0 ? 0.90 : 1.08) * wob,
44        R * uz * lat * wob * 0.95,
45        0.30 + rand() * 0.05
46      ]);
47    }
48    const seen = {};
49    for (let i = 0; i < N; i++) {
50      const a = nodes[i];
51      const bi = [-1, -1, -1], bd = [9, 9, 9];
52      for (let j = 0; j < N; j++) {
53        if (j === i) continue;
54        const b = nodes[j];
55        const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
56        const d = dx * dx + dy * dy + dz * dz;
57        if (d < bd[0]) { bd[2] = bd[1]; bi[2] = bi[1]; bd[1] = bd[0]; bi[1] = bi[0]; bd[0] = d; bi[0] = j; }
58        else if (d < bd[1]) { bd[2] = bd[1]; bi[2] = bi[1]; bd[1] = d; bi[1] = j; }
59        else if (d < bd[2]) { bd[2] = d; bi[2] = j; }
60      }
61      for (let k = 0; k < 3; k++) {
62        const j = bi[k];
63        if (j < 0) continue;
64        const key = (i < j ? i : j) * 1000 + (i < j ? j : i);
65        if (seen[key]) continue;
66        seen[key] = 1;
67        edges.push(i, j);
68      }
69    }
70    const RINGS = 5, PER = 12, base = nodes.length;
71    for (let s = 0; s < RINGS; s++) {
72      const t = s / (RINGS - 1);
73      const y = -0.150 - 0.090 * t;
74      const r = 0.090 + (0.035 - 0.090) * Math.pow(t, 0.75);
75      for (let k = 0; k < PER; k++) {
76        const a = (k / PER) * Math.PI * 2 + s * 0.10;
77        const w = 1 + (rand() - 0.5) * 0.06;
78        nodes.push([
79          Math.cos(a) * r * w,
80          y + (rand() - 0.5) * 0.004,
81          Math.sin(a) * r * w * 0.95,
82          0.50 + rand() * 0.10
83        ]);
84      }
85    }
86    for (let s = 0; s < RINGS; s++) {
87      for (let k = 0; k < PER; k++) {
88        const i = base + s * PER + k;
89        edges.push(i, base + s * PER + ((k + 1) % PER));
90        if (s < RINGS - 1) edges.push(i, i + PER);
91      }
92    }
93    return { nodes, edges };
94  };
95
96  const nucleus = (rand) => {
97    const nodes = [], edges = [];
98    const CY = 0.015, R = 0.085, N = 112, GA = Math.PI * (3 - Math.sqrt(5));
99    for (let i = 0; i < N; i++) {
100      const uy = 1 - (2 * i + 1) / N;
101      const rr = Math.sqrt(Math.max(0, 1 - uy * uy));
102      const th = GA * i + (rand() - 0.5) * 0.12;
103      const w = R * (1 + (rand() - 0.5) * 0.07);
104      nodes.push([Math.cos(th) * rr * w, CY + uy * w * 0.97, Math.sin(th) * rr * w * 0.95, 0.58 + rand() * 0.08]);
105    }
106    const seen = {};
107    for (let i = 0; i < N; i++) {
108      const a = nodes[i];
109      const bi = [-1, -1, -1], bd = [9, 9, 9];
110      for (let j = 0; j < N; j++) {
111        if (j === i) continue;
112        const b = nodes[j];
113        const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
114        const d = dx * dx + dy * dy + dz * dz;
115        if (d < bd[0]) { bd[2] = bd[1]; bi[2] = bi[1]; bd[1] = bd[0]; bi[1] = bi[0]; bd[0] = d; bi[0] = j; }
116        else if (d < bd[1]) { bd[2] = bd[1]; bi[2] = bi[1]; bd[1] = d; bi[1] = j; }
117        else if (d < bd[2]) { bd[2] = d; bi[2] = j; }
118      }
119      for (let k = 0; k < 3; k++) {
120        const j = bi[k];
121        if (j < 0) continue;
122        const key = (i < j ? i : j) * 1000 + (i < j ? j : i);
123        if (seen[key]) continue;
124        seen[key] = 1;
125        edges.push(i, j);
126      }
127    }
128    const base = nodes.length, M = 44, NR = 0.028, cx = 0.030, cy = CY + 0.026, cz = 0.014;
129    for (let i = 0; i < M; i++) {
130      const uy = 1 - (2 * i + 1) / M;
131      const rr = Math.sqrt(Math.max(0, 1 - uy * uy));
132      const th = GA * i + (rand() - 0.5) * 0.12;
133      const w = NR * (0.62 + 0.38 * Math.pow(rand(), 0.35));
134      nodes.push([cx + Math.cos(th) * rr * w, cy + uy * w, cz + Math.sin(th) * rr * w, 0.94 + rand() * 0.06]);
135    }
136    const seen2 = {};
137    for (let i = 0; i < M; i++) {
138      const a = nodes[base + i];
139      const bi = [-1, -1, -1, -1], bd = [9, 9, 9, 9];
140      for (let j = 0; j < M; j++) {
141        if (j === i) continue;
142        const b = nodes[base + j];
143        const dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2];
144        const d = dx * dx + dy * dy + dz * dz;
145        if (d < bd[0]) { bd[3] = bd[2]; bi[3] = bi[2]; bd[2] = bd[1]; bi[2] = bi[1]; bd[1] = bd[0]; bi[1] = bi[0]; bd[0] = d; bi[0] = j; }
146        else if (d < bd[1]) { bd[3] = bd[2]; bi[3] = bi[2]; bd[2] = bd[1]; bi[2] = bi[1]; bd[1] = d; bi[1] = j; }
147        else if (d < bd[2]) { bd[3] = bd[2]; bi[3] = bi[2]; bd[2] = d; bi[2] = j; }
148        else if (d < bd[3]) { bd[3] = d; bi[3] = j; }
149      }
150      for (let k = 0; k < 4; k++) {
151        const j = bi[k];
152        if (j < 0) continue;
153        const key = (i < j ? i : j) * 1000 + (i < j ? j : i);
154        if (seen2[key]) continue;
155        seen2[key] = 1;
156        edges.push(base + i, base + j);
157      }
158    }
159    return { nodes, edges };
160  };
161
162  const nisslBodies = (rand) => {
163    const nodes = [], edges = [];
164    const ny = 0.015;
165    const norm = (v) => { const m = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / m, v[1] / m, v[2] / m]; };
166    const frame = (u) => {
167      const a = Math.abs(u[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
168      const v = norm([u[1] * a[2] - u[2] * a[1], u[2] * a[0] - u[0] * a[2], u[0] * a[1] - u[1] * a[0]]);
169      const w = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
170      return [v, w];
171    };
172    for (let s = 0; s < 5; s++) {
173      const th = 2 * Math.PI * (s / 5) + 0.7 * rand(), cp = 1 - 2 * (0.15 + 0.7 * rand());
174      const sp = Math.sqrt(Math.max(0, 1 - cp * cp));
175      const n = norm([sp * Math.cos(th), cp, sp * Math.sin(th) * 0.65]);
176      const fr = frame(n), u = fr[0], w = fr[1];
177      const d = 0.103 + 0.010 * rand();
178      const cx = n[0] * d, cy = ny + n[1] * d, cz = n[2] * d;
179      const half = 0.023 + 0.004 * rand(), gap = 0.0115, bow = 0.005 * (rand() - 0.5);
180      let prevCorner = -1;
181      for (let k = 0; k < 4; k++) {
182        const off = (k - 1.5) * gap, base = nodes.length;
183        for (let i = 0; i < 3; i++) {
184          for (let j = 0; j < 3; j++) {
185            const a = (i - 1) * half, b = (j - 1) * half;
186            const lift = off + bow * (a * a + b * b) / (half * half) + 0.0015 * (rand() - 0.5);
187            nodes.push([
188              cx + u[0] * a + w[0] * b + n[0] * lift,
189              cy + u[1] * a + w[1] * b + n[1] * lift,
190              cz + u[2] * a + w[2] * b + n[2] * lift,
191              0.55
192            ]);
193          }
194        }
195        for (let i = 0; i < 3; i++) {
196          for (let j = 0; j < 2; j++) {
197            edges.push(base + i * 3 + j, base + i * 3 + j + 1);
198            edges.push(base + j * 3 + i, base + (j + 1) * 3 + i);
199          }
200        }
201        if (prevCorner >= 0) edges.push(prevCorner, base + 8);
202        prevCorner = base + 8;
203        for (let g = 0; g < 5; g++) {
204          const gi = Math.min(8, Math.floor(rand() * 9)), sgn = rand() < 0.5 ? -1 : 1;
205          const p = nodes[base + gi], rb = nodes.length;
206          nodes.push([
207            p[0] + n[0] * sgn * 0.006 + u[0] * 0.004 * (rand() - 0.5),
208            p[1] + n[1] * sgn * 0.006 + u[1] * 0.004 * (rand() - 0.5),
209            p[2] + n[2] * sgn * 0.006 + u[2] * 0.004 * (rand() - 0.5),
210            0.95
211          ]);
212          edges.push(base + gi, rb);
213        }
214      }
215    }
216    return { nodes: nodes, edges: edges };
217  };
218
219  const microtubuleHalo = (rand) => {
220    const nodes = [];
221    const edges = [];
222    for (let s = 0; s < 90; s++) {
223      let u = Math.pow(rand(), 1 / 3) * 0.105;
224      // the nucleus fills the middle of the soma; half of these landed inside it
225      if (u < 0.098) u = 0.098 + rand() * 0.045;
226      const ct = rand() * 2 - 1;
227      const st = Math.sqrt(1 - ct * ct);
228      const ph = rand() * 6.2832;
229      const x = u * st * Math.cos(ph);
230      const y = u * ct * 1.12 + 0.012;
231      const z = u * st * Math.sin(ph) * 0.85;
232      const dct = rand() * 2 - 1;
233      const dst = Math.sqrt(1 - dct * dct);
234      const dph = rand() * 6.2832;
235      const len = 0.018 + rand() * 0.024;
236      nodes.push([x, y, z, 0.3]);
237      nodes.push([
238        x + dst * Math.cos(dph) * len,
239        y + dct * len,
240        z + dst * Math.sin(dph) * len * 0.8,
241        0.3
242      ]);
243      edges.push(nodes.length - 2, nodes.length - 1);
244    }
245    return { nodes: nodes, edges: edges };
246  };
247
248  const dendrites = (rand) => {
249    const nodes = [], edges = [];
250    const ZS = 0.55;
251    const add = (p, w) => { nodes.push([p[0], p[1], p[2] * ZS, w]); return nodes.length - 1; };
252    const norm = (v) => { const m = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) || 1; return [v[0] / m, v[1] / m, v[2] / m]; };
253    const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
254    const perp = (d) => norm(cross(d, Math.abs(d[1]) < 0.88 ? [0, 1, 0] : [1, 0, 0]));
255    const rot = (d, u, a) => { const c = Math.cos(a), s = Math.sin(a); return norm([d[0] * c + u[0] * s, d[1] * c + u[1] * s, d[2] * c + u[2] * s]); };
256    const upAxis = (d) => norm([-d[0] * d[1], 1 - d[1] * d[1], -d[2] * d[1]]);
257    const LEN = [0.300, 0.216, 0.156, 0.112];
258    const TH = [0.055, 0.0341, 0.0211, 0.0131];
259    const I0 = [0.90, 0.78, 0.68, 0.58];
260    const I1 = [0.78, 0.68, 0.58, 0.48];
261    // ring of 5 nodes girdling the segment axis, so proximal dendrites read as tubes
262    const ring = (p, d, r, roll) => {
263      const u = perp(d), v = cross(d, u), out = [];
264      for (let k = 0; k < 5; k++) {
265        const a = roll + k * 1.2566 + (rand() - 0.5) * 0.34, rr = r * (0.88 + rand() * 0.24);
266        const c = Math.cos(a) * rr, s = Math.sin(a) * rr;
267        out.push(add([p[0] + u[0] * c + v[0] * s, p[1] + u[1] * c + v[1] * s, p[2] + u[2] * c + v[2] * s], 0.28));
268      }
269      for (let k = 0; k < 5; k++) edges.push(out[k], out[(k + 1) % 5]);
270      return out;
271    };
272    const grow = (p0, d0, gen, parent) => {
273      const L = LEN[gen], th = TH[gen], N = 4 + Math.floor(rand() * 3);
274      const bendAx = perp(d0), bendPer = (rand() - 0.5) * 0.62 / N, upPer = (0.16 - gen * 0.03) / N, roll = rand() * 6.283;
275      let d = [d0[0], d0[1], d0[2]], p = [p0[0], p0[1], p0[2]], prev = parent;
276      const step = L / N, rings = [];
277      for (let i = 0; i < N; i++) {
278        d = rot(d, bendAx, bendPer);
279        d = rot(d, upAxis(d), upPer);
280        p = [p[0] + d[0] * step, p[1] + d[1] * step, p[2] + d[2] * step];
281        const t = (i + 1) / N;
282        const idx = add(p, I0[gen] + (I1[gen] - I0[gen]) * t);
283        edges.push(prev, idx);
284        prev = idx;
285        if (gen === 0 && i < 3) rings.push([ring(p, d, th * (1 - 0.34 * t), roll), idx]);
286        if (gen === 1 && i === (N >> 1)) rings.push([ring(p, d, th * (1 - 0.34 * t), roll), idx]);
287      }
288      for (const rg of rings) edges.push(rg[1], rg[0][0]);
289      if (gen === 3) return;
290      const n = rand() < 0.22 ? 3 : 2;
291      const u = perp(d), v = cross(d, u), br = rand() * 6.283;
292      for (let k = 0; k < n; k++) {
293        const a = br + k * (6.283 / n) + (rand() - 0.5) * 0.5;
294        const ax = [u[0] * Math.cos(a) + v[0] * Math.sin(a), u[1] * Math.cos(a) + v[1] * Math.sin(a), u[2] * Math.cos(a) + v[2] * Math.sin(a)];
295        let cd = rot(d, ax, 0.489 + rand() * 0.244);
296        cd = norm([cd[0], cd[1] + 0.09, cd[2]]);
297        if (cd[1] < -0.09) cd = norm([cd[0], -0.09, cd[2]]);
298        grow(p, cd, gen + 1, prev);
299      }
300    };
301    // Uneven on purpose. Evenly spaced primaries at matching elevations read
302    // as a diagram of a neuron rather than as one.
303    const elev = [1.46, 0.62, 1.02, -0.02, 0.30, 0.86];
304    const azim = [0.24, 1.02, 2.21, 3.02, 4.35, 5.29];
305    for (let i = 0; i < 6; i++) {
306      const az = azim[i] + (rand() - 0.5) * 0.55;
307      const el = elev[i] + (rand() - 0.5) * 0.34;
308      const d = norm([Math.cos(el) * Math.cos(az), Math.sin(el), Math.cos(el) * Math.sin(az)]);
309      const p = [d[0] * 0.16, d[1] * 0.16, d[2] * 0.16];
310      const root = add(p, 0.95);
311      grow(p, d, 0, root);
312    }
313    return { nodes, edges };
314  };
315
316  const dendriticSpines = (rand, parent) => {
317    const nodes = [], edges = [];
318    if (!parent || !parent.length) return { nodes, edges };
319    // Spines ride on real dendrite nodes. Placing them on an abstract shell,
320    // which is all a blind function can do, scatters them into the gaps
321    // between branches and reads as fog.
322    const host = [];
323    for (let i = 0; i < parent.length; i += 1) {
324      const p = parent[i];
325      const r = Math.hypot(p[0], p[1], p[2]);
326      if (r > 0.40 && p[1] > -0.02) host.push(p);
327    }
328    if (!host.length) return { nodes, edges };
329    const want = Math.min(170, host.length);
330    for (let s = 0; s < want; s += 1) {
331      const p = host[Math.floor(rand() * host.length)];
332      const rl = Math.hypot(p[0], p[1], p[2]) || 1;
333      let ox = p[0] / rl + (rand() - 0.5) * 1.1;
334      let oy = p[1] / rl + (rand() - 0.5) * 1.1;
335      let oz = p[2] / rl + (rand() - 0.5) * 1.1;
336      const ol = Math.hypot(ox, oy, oz) || 1;
337      ox /= ol; oy /= ol; oz /= ol;
338      const len = 0.020 + rand() * 0.012;
339      const base = nodes.length;
340      nodes.push([p[0], p[1], p[2], 0.72]);
341      nodes.push([p[0] + ox * len, p[1] + oy * len, p[2] + oz * len, 0.96]);
342      edges.push(base, base + 1);
343    }
344    return { nodes, edges };
345  };
346
347  const neurofilaments = (rand) => {
348    const nodes = [];
349    const edges = [];
350    // centre line of the axon, matched to the axon's own path
351    const axonAt = (t) => {
352      const y = -0.24 - 1.16 * t;
353      const x = 0.05 * t * t + 0.016 * Math.sin(4.9 * t + 0.7);
354      const z = 0.013 * Math.sin(3.6 * t + 1.5);
355      return [x, y, z];
356    };
357    // five strands spiralling gently around the axon centre line
358    for (let f = 0; f < 5; f++) {
359      const base = (f / 5) * Math.PI * 2 + rand() * 0.5;
360      const rad = 0.007 + rand() * 0.008;
361      const ph = rand() * 6.2832;
362      const twist = 1.2 + rand() * 2.0;
363      for (let i = 0; i < 34; i++) {
364        const t = i / 33;
365        const p = axonAt(t);
366        const a = base + twist * t + 0.55 * Math.sin(2.7 * t + ph);
367        const jx = 0.004 * Math.sin(8.5 * t + ph);
368        const jy = 0.003 * Math.sin(6.1 * t + ph * 1.7);
369        nodes.push([
370          p[0] + Math.cos(a) * rad + jx,
371          p[1] + jy,
372          p[2] + Math.sin(a) * rad * 0.8,
373          0.4
374        ]);
375        if (i > 0) edges.push(nodes.length - 2, nodes.length - 1);
376      }
377    }
378    // six shorter strands leaving the soma into the proximal dendrites
379    const dirs = [
380      [0.0, 1.0, 0.1], [-0.5, 0.86, -0.15], [0.52, 0.85, 0.12],
381      [-0.84, 0.52, 0.1], [0.86, 0.48, -0.12], [-0.24, 0.96, -0.08]
382    ];
383    for (let f = 0; f < 6; f++) {
384      const d = dirs[f];
385      const L = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
386      const ux = d[0] / L, uy = d[1] / L, uz = d[2] / L;
387      const px = -uy, py = ux, pz = 0.0;
388      const bend = (rand() - 0.5) * 0.10;
389      const ph = rand() * 6.2832;
390      const r0 = 0.07 + rand() * 0.03;
391      const r1 = 0.40 + rand() * 0.05;
392      for (let i = 0; i < 30; i++) {
393        const t = i / 29;
394        const r = r0 + (r1 - r0) * t;
395        const b = bend * t * t + 0.006 * Math.sin(7.0 * t + ph);
396        nodes.push([
397          ux * r + px * b,
398          uy * r + py * b,
399          uz * r + pz * b + 0.008 * Math.sin(5.0 * t + ph),
400          0.4
401        ]);
402        if (i > 0) edges.push(nodes.length - 2, nodes.length - 1);
403      }
404    }
405    return { nodes: nodes, edges: edges };
406  };
407
408  const axon = (rand) => {
409    const nodes = [], edges = [];
410    const cen = (t) => {
411      return [
412        0.05 * t * t * (3 - 2 * t),
413        -0.24 - 1.18 * t,
414        0
415      ];
416    };
417    const N = 70;
418    const path = [];
419    for (let i = 0; i < N; i++) {
420      const t = i / (N - 1);
421      const c = cen(t);
422      const w = Math.sin(Math.PI * t);
423      const p = [
424        c[0] + (rand() - 0.5) * 0.006 * w,
425        c[1] + (rand() - 0.5) * 0.004,
426        c[2] + (rand() - 0.5) * 0.006 * w
427      ];
428      path.push(p);
429      nodes.push([p[0], p[1], p[2], 0.86 - 0.06 * t]);
430      if (i > 0) edges.push(i - 1, i);
431    }
432    let prev = -1;
433    for (let i = 0; i < N; i += 3) {
434      const a = path[Math.max(0, i - 1)], b = path[Math.min(N - 1, i + 1)];
435      let tx = b[0] - a[0], ty = b[1] - a[1], tz = b[2] - a[2];
436      const L = Math.hypot(tx, ty, tz) || 1;
437      tx /= L; ty /= L; tz /= L;
438      let ux = ty, uy = -tx, uz = 0;
439      const M = Math.hypot(ux, uy, uz) || 1;
440      ux /= M; uy /= M; uz /= M;
441      const vx = ty * uz - tz * uy, vy = tz * ux - tx * uz, vz = tx * uy - ty * ux;
442      const base = nodes.length, ph = rand() * 0.6;
443      for (let k = 0; k < 4; k++) {
444        const ang = ph + k * Math.PI * 0.5, r = 0.030 * (0.94 + rand() * 0.12);
445        const ca = Math.cos(ang), sa = Math.sin(ang);
446        nodes.push([
447          path[i][0] + (ux * ca + vx * sa) * r,
448          path[i][1] + (uy * ca + vy * sa) * r,
449          path[i][2] + (uz * ca + vz * sa) * r,
450          0.46
451        ]);
452      }
453      for (let k = 0; k < 4; k++) edges.push(base + k, base + (k + 1) % 4);
454      if (prev >= 0) for (let k = 0; k < 4; k++) edges.push(prev + k, base + k);
455      prev = base;
456    }
457    return { nodes: nodes, edges: edges };
458  };
459
460  const myelin = (rand) => {
461    const nodes = [], edges = [];
462    const cen = (t) => {
463      const s = Math.sin(Math.PI * t);
464      return [
465        0.05 * t * t * (3 - 2 * t) + 0.035 * Math.sin(2.5 * Math.PI * t) * s,
466        -0.24 - 1.18 * t,
467        0.045 * Math.sin(1.7 * Math.PI * t + 0.4) * s
468      ];
469    };
470    const at = (y) => cen((-0.24 - y) / 1.18);
471    const RING = 12, ROWS = 5;
472    for (let s = 0; s < 7; s++) {
473      const y0 = -0.36 - 0.14 * s, y1 = y0 - 0.10;
474      const twist = rand() * 0.5;
475      let prev = -1;
476      for (let r = 0; r < ROWS; r++) {
477        const f = r / (ROWS - 1);
478        const y = y0 + (y1 - y0) * f;
479        const c = at(y);
480        const b = at(y - 0.004);
481        let tx = b[0] - c[0], ty = b[1] - c[1], tz = b[2] - c[2];
482        const L = Math.hypot(tx, ty, tz) || 1;
483        tx /= L; ty /= L; tz /= L;
484        let ux = ty, uy = -tx, uz = 0;
485        const M = Math.hypot(ux, uy, uz) || 1;
486        ux /= M; uy /= M; uz /= M;
487        const vx = ty * uz - tz * uy, vy = tz * ux - tx * uz, vz = tx * uy - ty * ux;
488        const rad = 0.062 * (0.82 + 0.18 * Math.sin(Math.PI * (0.18 + 0.64 * f)));
489        const base = nodes.length;
490        const inten = (r === 0 || r === ROWS - 1) ? 0.52 : 0.45;
491        for (let k = 0; k < RING; k++) {
492          const ang = twist + 0.12 * f + k * 2 * Math.PI / RING;
493          const rr = rad * (0.97 + rand() * 0.06);
494          const ca = Math.cos(ang), sa = Math.sin(ang);
495          nodes.push([
496            c[0] + (ux * ca + vx * sa) * rr,
497            c[1] + (uy * ca + vy * sa) * rr,
498            c[2] + (uz * ca + vz * sa) * rr,
499            inten
500          ]);
501        }
502        for (let k = 0; k < RING; k++) edges.push(base + k, base + (k + 1) % RING);
503        if (prev >= 0) for (let k = 0; k < RING; k++) edges.push(prev + k, base + k);
504        prev = base;
505      }
506    }
507    return { nodes: nodes, edges: edges };
508  };
509
510  const nodesOfRanvier = (rand) => {
511    const nodes = [], edges = [];
512    const cen = (t) => {
513      const s = Math.sin(Math.PI * t);
514      return [
515        0.05 * t * t * (3 - 2 * t) + 0.035 * Math.sin(2.5 * Math.PI * t) * s,
516        -0.24 - 1.18 * t,
517        0.045 * Math.sin(1.7 * Math.PI * t + 0.4) * s
518      ];
519    };
520    const at = (y) => cen((-0.24 - y) / 1.18);
521    const RING = 7;
522    for (let g = 0; g < 6; g++) {
523      const y = -0.48 - 0.14 * g;
524      const c = at(y);
525      const b = at(y - 0.004);
526      let tx = b[0] - c[0], ty = b[1] - c[1], tz = b[2] - c[2];
527      const L = Math.hypot(tx, ty, tz) || 1;
528      tx /= L; ty /= L; tz /= L;
529      let ux = ty, uy = -tx, uz = 0;
530      const M = Math.hypot(ux, uy, uz) || 1;
531      ux /= M; uy /= M; uz /= M;
532      const vx = ty * uz - tz * uy, vy = tz * ux - tx * uz, vz = tx * uy - ty * ux;
533      const base = nodes.length, ph = rand() * 0.9;
534      for (let k = 0; k < RING; k++) {
535        const ang = ph + k * 2 * Math.PI / RING;
536        const rr = 0.036 * (0.96 + rand() * 0.08);
537        const ca = Math.cos(ang), sa = Math.sin(ang);
538        nodes.push([
539          c[0] + (ux * ca + vx * sa) * rr,
540          c[1] + (uy * ca + vy * sa) * rr,
541          c[2] + (uz * ca + vz * sa) * rr,
542          1.0
543        ]);
544      }
545      for (let k = 0; k < RING; k++) edges.push(base + k, base + (k + 1) % RING);
546    }
547    return { nodes: nodes, edges: edges };
548  };
549
550  const terminalArbor = (rand) => {
551    const nodes = [];
552    const edges = [];
553    const add = (x, y, z) => {
554      // brighter near the axon, fading into the tips, the way the dendrites do
555      const t = Math.min(1, Math.max(0, (-1.42 - y) / 0.36));
556      nodes.push([x, y, z, 0.88 - 0.30 * t]);
557      return nodes.length - 1;
558    };
559    // grow one tapering segment chain from a parent node in direction (phi, tilt)
560    const grow = (from, fromIdx, phi, tilt, len, count, droop) => {
561      const ct = Math.cos(tilt), st = Math.sin(tilt);
562      const dx = Math.cos(phi) * ct, dy = -st, dz = 0.5 * Math.sin(phi) * ct;
563      let prev = fromIdx, ex = from[0], ey = from[1], ez = from[2];
564      for (let k = 1; k <= count; k++) {
565        const t = k / count;
566        ex = from[0] + dx * len * t + (rand() - 0.5) * 0.007;
567        ey = from[1] + dy * len * t - droop * t * t + (rand() - 0.5) * 0.005;
568        ez = from[2] + dz * len * t + (rand() - 0.5) * 0.007;
569        const idx = add(ex, ey, ez);
570        edges.push(prev, idx);
571        prev = idx;
572      }
573      return { p: [ex, ey, ez], i: prev, phi: phi };
574    };
575    const root = add(0.05, -1.42, 0);
576    const trunk = grow([0.05, -1.42, 0], root, 0, Math.PI / 2, 0.045, 7, 0);
577    const g1 = [];
578    for (let i = 0; i < 4; i++) {
579      const phi = (i + 0.5) * Math.PI * 0.5 + (rand() - 0.5) * 0.4;
580      g1.push(grow(trunk.p, trunk.i, phi, 0.60 + (rand() - 0.5) * 0.12, 0.25, 12, 0.012));
581    }
582    const g2 = [];
583    for (let i = 0; i < g1.length; i++) {
584      const par = g1[i];
585      for (let c = 0; c < 2; c++) {
586        const phi = par.phi + (c === 0 ? -0.55 : 0.55) + (rand() - 0.5) * 0.25;
587        g2.push(grow(par.p, par.i, phi, 0.48 + (rand() - 0.5) * 0.16, 0.182, 10, 0.010));
588      }
589    }
590    for (let i = 0; i < g2.length; i++) {
591      const par = g2[i];
592      const kids = i % 2 === 0 ? 2 : 1;
593      for (let c = 0; c < kids; c++) {
594        const phi = par.phi + (kids === 1 ? 0 : (c === 0 ? -0.45 : 0.45)) + (rand() - 0.5) * 0.3;
595        grow(par.p, par.i, phi, 0.30 + (rand() - 0.5) * 0.2, 0.108, 8, 0.008);
596      }
597    }
598    return { nodes: nodes, edges: edges };
599  };
600
601  const boutons = (rand, parent) => {
602    const nodes = [], edges = [];
603    if (!parent || !parent.length) return { nodes, edges };
604    // Boutons cap the actual branch tips. Placed independently they sit in the
605    // gaps between branches, which is exactly what a synaptic terminal must
606    // not do.
607    const tips = [];
608    for (let i = 0; i < parent.length; i += 1) {
609      if (parent[i][1] < -1.60) tips.push(parent[i]);
610    }
611    if (!tips.length) return { nodes, edges };
612    const used = [];
613    const want = Math.min(15, tips.length);
614    for (let b = 0; b < want; b += 1) {
615      let p = null;
616      for (let attempt = 0; attempt < 24 && !p; attempt += 1) {
617        const cand = tips[Math.floor(rand() * tips.length)];
618        let ok = true;
619        for (let u = 0; u < used.length; u += 1) {
620          const d = Math.hypot(cand[0] - used[u][0], cand[1] - used[u][1], cand[2] - used[u][2]);
621          if (d < 0.075) { ok = false; break; }
622        }
623        if (ok) p = cand;
624      }
625      if (!p) continue;
626      used.push(p);
627      const R = 0.030;
628      const base = nodes.length;
629      for (let k = 0; k < 9; k += 1) {
630        const uy = 1 - (2 * k + 1) / 9;
631        const rr = Math.sqrt(Math.max(0, 1 - uy * uy));
632        const th = k * 2.39996;
633        nodes.push([
634          p[0] + Math.cos(th) * rr * R,
635          p[1] + uy * R,
636          p[2] + Math.sin(th) * rr * R * 0.8,
637          0.58
638        ]);
639        if (k > 0) edges.push(base + k - 1, base + k);
640      }
641      edges.push(base + 8, base);
642      // synaptic vesicles, which is what makes it a bouton and not a swelling
643      for (let v = 0; v < 5; v += 1) {
644        nodes.push([
645          p[0] + (rand() - 0.5) * R * 1.1,
646          p[1] + (rand() - 0.5) * R * 1.1,
647          p[2] + (rand() - 0.5) * R * 0.9,
648          1
649        ]);
650      }
651    }
652    return { nodes, edges };
653  };
654
655  const mitochondria = (rand) => {
656    const nodes = [], edges = [];
657    const HL = 0.045, R = 0.022;
658    const norm = (v) => { const m = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / m, v[1] / m, v[2] / m]; };
659    const frame = (u) => {
660      const a = Math.abs(u[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
661      const v = norm([u[1] * a[2] - u[2] * a[1], u[2] * a[0] - u[0] * a[2], u[0] * a[1] - u[1] * a[0]]);
662      const w = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
663      return [v, w];
664    };
665    const profile = (tn) => Math.sqrt(Math.max(0.08, 1 - 0.82 * tn * tn));
666    const capsule = (cx, cy, cz, dx, dy, dz) => {
667      const u = norm([dx, dy, dz]), fr = frame(u), v = fr[0], w = fr[1];
668      const RINGS = 5, SEG = 6, base = nodes.length;
669      for (let i = 0; i < RINGS; i++) {
670        const tn = -1 + 2 * i / (RINGS - 1), r = R * profile(tn), ph = rand() * 1.0;
671        for (let j = 0; j < SEG; j++) {
672          const a = ph + 2 * Math.PI * j / SEG, ca = Math.cos(a) * r, sa = Math.sin(a) * r, tt = tn * HL;
673          nodes.push([
674            cx + u[0] * tt + v[0] * ca + w[0] * sa,
675            cy + u[1] * tt + v[1] * ca + w[1] * sa,
676            cz + u[2] * tt + v[2] * ca + w[2] * sa,
677            0.45
678          ]);
679        }
680      }
681      for (let i = 0; i < RINGS; i++) {
682        for (let j = 0; j < SEG; j++) {
683          edges.push(base + i * SEG + j, base + i * SEG + (j + 1) % SEG);
684          if (i < RINGS - 1) edges.push(base + i * SEG + j, base + (i + 1) * SEG + j);
685        }
686      }
687      for (let k = 0; k < 4; k++) {
688        const tn = -0.62 + 0.413 * k + 0.05 * (rand() - 0.5);
689        const half = R * profile(tn) * 0.84, tilt = 0.22 * (rand() - 0.5);
690        const ang = rand() * Math.PI, ca = Math.cos(ang), sa = Math.sin(ang);
691        const cb = nodes.length, N = 5;
692        for (let m = 0; m < N; m++) {
693          const f = -1 + 2 * m / (N - 1), s = f * half, zz = (m % 2 ? 1 : -1) * R * 0.15;
694          const px = ca * s - sa * zz, py = sa * s + ca * zz, tt = (tn + tilt * f * 0.5) * HL;
695          nodes.push([
696            cx + u[0] * tt + v[0] * px + w[0] * py,
697            cy + u[1] * tt + v[1] * px + w[1] * py,
698            cz + u[2] * tt + v[2] * px + w[2] * py,
699            0.95
700          ]);
701        }
702        for (let m = 0; m < N - 1; m++) edges.push(cb + m, cb + m + 1);
703      }
704    };
705    for (let s = 0; s < 3; s++) {
706      const th = 2 * Math.PI * (s / 3 + 0.11 * rand()), cp = 1 - 2 * (0.2 + 0.6 * rand());
707      const sp = Math.sqrt(Math.max(0, 1 - cp * cp));
708      const rr = norm([sp * Math.cos(th), cp, sp * Math.sin(th) * 0.7]);
709      const d = 0.103 + 0.010 * rand();
710      const g = [rand() - 0.5, rand() - 0.5, rand() - 0.5];
711      const dot = g[0] * rr[0] + g[1] * rr[1] + g[2] * rr[2];
712      const t = norm([g[0] - dot * rr[0], g[1] - dot * rr[1], g[2] - dot * rr[2]]);
713      capsule(rr[0] * d, 0.015 + rr[1] * d, rr[2] * d, t[0], t[1], t[2]);
714    }
715    const axonX = (y) => 0.05 * ((-0.22 - y) / 1.20);
716    for (let s = 0; s < 4; s++) {
717      const y = -0.44 - 0.295 * s - 0.04 * rand();
718      const off = (rand() - 0.5) * 0.026, offz = (rand() - 0.5) * 0.024;
719      capsule(axonX(y) + off, y, offz, 0.042 + 0.10 * (rand() - 0.5), -1, 0.06 * (rand() - 0.5));
720    }
721    for (let s = 0; s < 2; s++) {
722      const sgn = s === 0 ? -1 : 1;
723      const x = sgn * (0.18 + 0.09 * rand()), y = -1.58 - 0.05 * rand(), z = (rand() - 0.5) * 0.16;
724      capsule(x, y, z, sgn * (0.45 + 0.2 * rand()), -1, 0.12 * (rand() - 0.5));
725    }
726    return { nodes: nodes, edges: edges };
727  };
728
729  /* One accent. Four channels made the compartments legible but read as a
730     stain rather than as a cell; structure is carried by brightness, density
731     and the limb brightening on the closed surfaces instead. */
732  const PARTS = [
733    { fn: soma, seed: 1801, gain: 1.05, hue: 1, rim: true, name: 'soma' },
734    { fn: nucleus, seed: 1907, gain: 0.66, hue: 1, rim: true, name: 'nucleus' },
735    { fn: nisslBodies, seed: 2003, gain: 0.80, hue: 1, name: 'nisslBodies' },
736    { fn: microtubuleHalo, seed: 2111, gain: 0.50, hue: 1, name: 'microtubuleHalo' },
737    { fn: dendrites, seed: 2237, gain: 1.05, hue: 1, name: 'dendrites' },
738    { fn: dendriticSpines, seed: 2341, gain: 0.95, hue: 1, from: 'dendrites', name: 'dendriticSpines' },
739    { fn: neurofilaments, seed: 2447, gain: 0.55, hue: 1, name: 'neurofilaments' },
740    { fn: axon, seed: 2551, gain: 1.00, hue: 1, name: 'axon' },
741    { fn: myelin, seed: 2657, gain: 0.80, hue: 1, name: 'myelin' },
742    { fn: nodesOfRanvier, seed: 2749, gain: 0.95, hue: 1, name: 'nodesOfRanvier' },
743    { fn: terminalArbor, seed: 2861, gain: 1.00, hue: 1, name: 'terminalArbor' },
744    { fn: boutons, seed: 2963, gain: 1.00, hue: 1, from: 'terminalArbor', name: 'boutons' },
745    { fn: mitochondria, seed: 3079, gain: 1.00, hue: 1, name: 'mitochondria' },
746  ];
747
748  /* A neuron is long. Normalising on radius, the way the cell does, would put
749     the terminal arbor off the canvas, so the bounding box is what gets fitted
750     and the tall axis is what sets the scale. */
751  const fitBox = (pos, count, target) => {
752    let minX = Infinity, maxX = -Infinity;
753    let minY = Infinity, maxY = -Infinity;
754    let minZ = Infinity, maxZ = -Infinity;
755    for (let i = 0; i < count; i += 1) {
756      const x = pos[i * 3], y = pos[i * 3 + 1], z = pos[i * 3 + 2];
757      if (x < minX) minX = x; if (x > maxX) maxX = x;
758      if (y < minY) minY = y; if (y > maxY) maxY = y;
759      if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
760    }
761    const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2, cz = (minZ + maxZ) / 2;
762    const half = Math.max(maxX - minX, maxY - minY, maxZ - minZ) / 2;
763    const k = half > 0 ? target / half : 1;
764    for (let i = 0; i < count; i += 1) {
765      pos[i * 3] = (pos[i * 3] - cx) * k;
766      pos[i * 3 + 1] = (pos[i * 3 + 1] - cy) * k;
767      pos[i * 3 + 2] = (pos[i * 3 + 2] - cz) * k;
768    }
769  };
770
771  /* Real neurons lean and curve. Applying that to the assembled cloud rather
772     than inside each compartment is what keeps the axon, its myelin, its
773     filaments and its mitochondria on one path: they bend together because
774     they are bent by the same function. */
775  const organic = (pos, count) => {
776    for (let i = 0; i < count; i += 1) {
777      const x = pos[i * 3], y = pos[i * 3 + 1], z = pos[i * 3 + 2];
778      const t = Math.max(0, Math.min(1, (0.05 - y) / 1.85));
779      let nx = x + 0.30 * t * t - 0.055 * Math.sin(t * 3.4);
780      let nz = z + 0.13 * Math.sin(t * 2.1 + 0.5);
781      const u = Math.max(0, Math.min(1, y / 0.95));
782      nx -= 0.20 * u * u;
783      nz += 0.07 * u * Math.sin(u * 2.7);
784      const a = 0.20 * (y + 0.4);
785      const ca = Math.cos(a), sa = Math.sin(a);
786      pos[i * 3] = nx * ca - nz * sa;
787      pos[i * 3 + 1] = y;
788      pos[i * 3 + 2] = nx * sa + nz * ca;
789    }
790  };
791
792  const clampNode = (node) => {
793    const x = node[0] < -0.95 ? -0.95 : (node[0] > 0.95 ? 0.95 : node[0]);
794    const z = node[2] < -0.55 ? -0.55 : (node[2] > 0.55 ? 0.55 : node[2]);
795    const y = node[1] < -1.80 ? -1.80 : (node[1] > 1.00 ? 1.00 : node[1]);
796    return [x, y, z, node[3]];
797  };
798
799  /* ---------------------------------------
800     Assembly
801     --------------------------------------- */
802
803  let cache = null;
804
805  const build = (n) => {
806    if (cache && cache.n === n) return cache.figure;
807
808    const nodes = [];
809    const edges = [];
810    const shellOf = [];
811    const hueOf = [];
812    let shellId = 1;
813
814    const produced = {};
815
816    PARTS.forEach((part) => {
817      const rand = A.makeRandom(part.seed);
818      let out;
819      try {
820        out = part.from ? part.fn(rand, produced[part.from]) : part.fn(rand);
821      } catch (error) {
822        return;
823      }
824      if (part.name && out && out.nodes) produced[part.name] = out.nodes;
825      if (!out || !out.nodes || !out.nodes.length) return;
826      const offset = nodes.length;
827      const localCount = out.nodes.length;
828      const gain = part.gain === undefined ? 1 : part.gain;
829      const shell = part.rim ? shellId : 0;
830      if (part.rim) shellId += 1;
831      for (let i = 0; i < localCount; i += 1) {
832        const nd = out.nodes[i];
833        const inten = (nd[3] === undefined ? 0.7 : nd[3]) * gain;
834        nodes.push(clampNode([nd[0], nd[1], nd[2], inten > 1 ? 1 : inten]));
835        shellOf.push(shell);
836        hueOf.push(part.hue === undefined ? 1 : part.hue);
837      }
838      const localEdges = out.edges || [];
839      for (let e = 0; e + 1 < localEdges.length; e += 2) {
840        const a = localEdges[e], b = localEdges[e + 1];
841        if (a >= 0 && a < localCount && b >= 0 && b < localCount) {
842          edges.push(a + offset, b + offset);
843        }
844      }
845    });
846
847    const nodeCount = nodes.length;
848    const total = Math.max(n, nodeCount);
849    const pos = new Float32Array(total * 3);
850    const intensity = new Float32Array(total);
851
852    for (let i = 0; i < nodeCount; i += 1) {
853      pos[i * 3] = nodes[i][0];
854      pos[i * 3 + 1] = nodes[i][1];
855      pos[i * 3 + 2] = nodes[i][2];
856      intensity[i] = nodes[i][3];
857    }
858
859    const usable = [];
860    for (let e = 0; e + 1 < edges.length; e += 2) {
861      if (edges[e] < nodeCount && edges[e + 1] < nodeCount) {
862        usable.push(edges[e], edges[e + 1]);
863      }
864    }
865
866    const edgeCount = usable.length / 2;
867    const fillStart = nodeCount;
868
869    if (edgeCount > 0 && total > fillStart) {
870      const cumulative = new Float64Array(edgeCount);
871      let running = 0;
872      for (let e = 0; e < edgeCount; e += 1) {
873        const a = usable[e * 2], b = usable[e * 2 + 1];
874        running += Math.hypot(
875          nodes[b][0] - nodes[a][0],
876          nodes[b][1] - nodes[a][1],
877          nodes[b][2] - nodes[a][2]
878        );
879        cumulative[e] = running;
880      }
881      const rand = A.makeRandom(71077);
882      const wire = running || 1;
883      for (let i = fillStart; i < total; i += 1) {
884        const pick = rand() * wire;
885        let lo = 0, hi = edgeCount - 1;
886        while (lo < hi) {
887          const mid = (lo + hi) >> 1;
888          if (cumulative[mid] < pick) lo = mid + 1; else hi = mid;
889        }
890        const a = nodes[usable[lo * 2]], b = nodes[usable[lo * 2 + 1]];
891        const t = rand();
892        pos[i * 3] = a[0] + (b[0] - a[0]) * t + A.gaussian(rand) * 0.003;
893        pos[i * 3 + 1] = a[1] + (b[1] - a[1]) * t + A.gaussian(rand) * 0.003;
894        pos[i * 3 + 2] = a[2] + (b[2] - a[2]) * t + A.gaussian(rand) * 0.003;
895        intensity[i] = Math.min(a[3], b[3]) * 0.42;
896        shellOf[i] = 0;
897        hueOf[i] = hueOf[usable[lo * 2]] || 1;
898      }
899    } else {
900      for (let i = fillStart; i < total; i += 1) intensity[i] = 0;
901    }
902
903    organic(pos, total);
904    fitBox(pos, total, 1);
905
906    const normal = new Float32Array(total * 3);
907    const shell = new Uint8Array(total);
908    const hue = new Uint8Array(total);
909    for (let i = 0; i < total; i += 1) hue[i] = hueOf[i] || 1;
910
911    /* Normals for the closed compartments, derived after the fit because the
912       fit moves the points. These are what let the renderer brighten a surface
913       where it turns edge-on, which is the difference between a shell reading
914       as a boundary and reading as a smudge. */
915    for (let g = 1; g < shellId; g += 1) {
916      let cx = 0, cy = 0, cz = 0, count = 0;
917      for (let i = 0; i < total; i += 1) {
918        if (shellOf[i] !== g) continue;
919        cx += pos[i * 3]; cy += pos[i * 3 + 1]; cz += pos[i * 3 + 2]; count += 1;
920      }
921      if (!count) continue;
922      cx /= count; cy /= count; cz /= count;
923      for (let i = 0; i < total; i += 1) {
924        if (shellOf[i] !== g) continue;
925        const dx = pos[i * 3] - cx, dy = pos[i * 3 + 1] - cy, dz = pos[i * 3 + 2] - cz;
926        const l = Math.hypot(dx, dy, dz) || 1;
927        normal[i * 3] = dx / l; normal[i * 3 + 1] = dy / l; normal[i * 3 + 2] = dz / l;
928        shell[i] = 1;
929      }
930    }
931
932    let hx = 0, hy = 0;
933    for (let i = 0; i < total; i += 1) {
934      const ax = Math.abs(pos[i * 3]), ay = Math.abs(pos[i * 3 + 1]);
935      if (ax > hx) hx = ax;
936      if (ay > hy) hy = ay;
937    }
938
939    const figure = {
940      pos, intensity, edges: usable, n: total, normal, shell, hue,
941      halfExtent: [hx, hy]
942    };
943    cache = { n, figure };
944    return figure;
945  };
946
947  window.BiodynNeuron = { build, PARTS };
948})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.