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.