CoolFace
Apppublic

CyberSys/fruit-fly-simulation

sourceHugging Faceapache-2.0updated 16d agoView on Hugging Face
0likes
brain-view.js318 linesDownload Raw Back to src
1import { requestBytes } from './data-loader.js';2/** Soma coordinates projected into canvas space for rendering and hit tests. */3export class BrainView {4  constructor(canvas, onPulse, onSelection) {5    this.canvas = canvas;6    this.ctx = canvas.getContext('2d');7    this.base = document.createElement('canvas');8    this.bc = this.base.getContext('2d');9    this.selectedLayer = document.createElement('canvas');10    this.sc = this.selectedLayer.getContext('2d');11    this.fireLayer = document.createElement('canvas');12    this.fc = this.fireLayer.getContext('2d');13    this.selectionDirty = true;14    this.onPulse = onPulse;15    this.onSelection = onSelection;16    this.neurons = [];17    this.points = [];18    this.selection = new Set();19    this.brush = 21;20    this.mode = 'paint';21    this.pulseProfile = 'paint';22    this.replacePulse = false;23    this.projection = 'brain';24    this.tick = 0;25    this.lastTickAt = performance.now();26    this.hover = null;27    this.enabled = false;28    this.spark = document.createElement('canvas');29    this.spark.width = this.spark.height = 48;30    const glow = this.spark.getContext('2d'),31      gradient = glow.createRadialGradient(24, 24, 0, 24, 24, 24);32    gradient.addColorStop(0, '#fff8df');33    gradient.addColorStop(0.08, '#ffe59d');34    gradient.addColorStop(0.22, '#ffbf57ad');35    gradient.addColorStop(0.55, '#ffaa3329');36    gradient.addColorStop(1, '#ff922000');37    glow.fillStyle = gradient;38    glow.fillRect(0, 0, 48, 48);39    this.observer = new ResizeObserver(() => this.resize());40    this.observer.observe(canvas);41    this.down = (e) => {42      if (!this.enabled || !this.neurons.length || e.button !== 0) return;43      e.preventDefault();44      canvas.focus();45      canvas.setPointerCapture(e.pointerId);46      this.pointer = e.pointerId;47      this.dragging = true;48      this.last = null;49      this.segment(e);50    };51    this.move = (e) => {52      if (!this.enabled) return;53      this.hover = this.local(e);54      if (this.dragging && this.pointer === e.pointerId) this.segment(e);55      this.tooltip();56    };57    this.up = (e) => {58      if (this.pointer !== e.pointerId) return;59      this.dragging = false;60      this.last = null;61      this.pointer = null;62      if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId);63      this.finish();64    };65    this.leave = () => {66      if (!this.dragging) this.hover = null;67      document.getElementById('neuron-tooltip').hidden = true;68    };69    canvas.addEventListener('pointerdown', this.down);70    canvas.addEventListener('pointermove', this.move);71    canvas.addEventListener('pointerup', this.up);72    canvas.addEventListener('pointercancel', this.up);73    canvas.addEventListener('pointerleave', this.leave);74    this.animate = this.animate.bind(this);75    this.frame = requestAnimationFrame(this.animate);76  }77  async load() {78    const bytes = await requestBytes('./data/neurons.json.gz');79    const json = await new Response(80      new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip')),81    ).text();82    this.neurons = JSON.parse(json);83    this.resize();84    return this.neurons;85  }86  resize() {87    const rect = this.canvas.getBoundingClientRect();88    this.w = Math.max(1, rect.width);89    this.h = Math.max(1, rect.height);90    const dpr = Math.min(2, devicePixelRatio || 1);91    for (const c of [this.canvas, this.base, this.selectedLayer, this.fireLayer]) {92      c.width = Math.round(this.w * dpr);93      c.height = Math.round(this.h * dpr);94    }95    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);96    this.bc.setTransform(dpr, 0, 0, dpr, 0, 0);97    this.sc.setTransform(dpr, 0, 0, dpr, 0, 0);98    this.fc.setTransform(dpr, 0, 0, dpr, 0, 0);99    this.layout();100  }101  layout() {102    this.selectionDirty = true;103    // Projection changes the view; hit tests retain the original neuron indices.104    const brain = this.projection === 'brain',105      angle = brain ? (25 * Math.PI) / 180 : 0;106    const centerX = brain ? -48075.50325 : -48000,107      centerY = brain ? 37479.76546015124 : 72500;108    const spanX = brain ? 90971.2755 : 92000,109      spanY = brain ? 41180.64216351028 : 127000;110    const scale = Math.min(Math.max(1, this.w - 36) / spanX, Math.max(1, this.h - 36) / spanY);111    const sine = Math.sin(angle),112      cosine = Math.cos(angle);113    this.points = [];114    this.byIndex = new Map();115    this.grid = new Map();116    this.cell = 24;117    this.neurons.forEach((r, i) => {118      const p = r[6];119      if (!p || p[2] > (brain ? 58000 : 136000) || p[2] < 9000) return;120      const x = this.w / 2 + (-p[0] - centerX) * scale,121        y = this.h / 2 + (p[1] * sine + p[2] * cosine - centerY) * scale,122        v = { i, x, y };123      this.points.push(v);124      this.byIndex.set(i, v);125      const k = `${Math.floor(x / this.cell)},${Math.floor(y / this.cell)}`;126      if (!this.grid.has(k)) this.grid.set(k, []);127      this.grid.get(k).push(v);128    });129    this.bc.clearRect(0, 0, this.w, this.h);130    this.bc.fillStyle = '#add5f1';131    this.bc.globalAlpha = 0.72;132    this.bc.beginPath();133    for (const p of this.points) {134      this.bc.moveTo(p.x + 0.65, p.y);135      this.bc.arc(p.x, p.y, 0.65, 0, Math.PI * 2);136    }137    this.bc.fill();138    this.bc.globalAlpha = 1;139  }140  tooltip() {141    const tip = document.getElementById('neuron-tooltip');142    if (this.dragging || !this.hover) {143      tip.hidden = true;144      return;145    }146    const { x, y } = this.hover;147    let nearest,148      dist = 36;149    const gx = Math.floor(x / this.cell),150      gy = Math.floor(y / this.cell);151    for (let a = gx - 1; a <= gx + 1; a++)152      for (let b = gy - 1; b <= gy + 1; b++)153        for (const p of this.grid.get(`${a},${b}`) ?? []) {154          const d = (p.x - x) ** 2 + (p.y - y) ** 2;155          if (d < dist) {156            nearest = p;157            dist = d;158          }159        }160    if (!nearest) {161      tip.hidden = true;162      return;163    }164    const r = this.neurons[nearest.i];165    tip.textContent = `${r[1] || 'Unclassified type'}${r[3] ? ' · ' + (r[3] === 'L' ? 'Left' : r[3] === 'R' ? 'Right' : r[3]) : ''}\n${r[4] || 'Unknown transmitter'}\nDataset ID ${r[0]}`;166    tip.hidden = false;167    tip.style.left = Math.max(8, Math.min(this.w - 240, x + 18)) + 'px';168    tip.style.top = Math.max(6, Math.min(this.h - 82, y + 18)) + 'px';169  }170  local(e) {171    const r = this.canvas.getBoundingClientRect();172    return { x: e.clientX - r.left, y: e.clientY - r.top };173  }174  segment(e) {175    const point = this.local(e),176      start = this.last ?? point;177    const dx = point.x - start.x,178      dy = point.y - start.y;179    const steps = Math.max(1, Math.ceil(Math.hypot(dx, dy) / Math.max(2, this.brush * 0.3)));180    for (let k = 0; k <= steps; k++)181      this.stamp(start.x + (dx * k) / steps, start.y + (dy * k) / steps);182    this.last = point;183    this.onSelection(this.selection.size);184  }185  stamp(x, y) {186    this.pulseProfile = 'paint';187    this.replacePulse = false;188    this.selectionDirty = true;189    const r = this.brush;190    for (let gx = Math.floor((x - r) / this.cell); gx <= Math.floor((x + r) / this.cell); gx++)191      for (let gy = Math.floor((y - r) / this.cell); gy <= Math.floor((y + r) / this.cell); gy++)192        for (const p of this.grid.get(`${gx},${gy}`) ?? []) {193          if ((p.x - x) ** 2 + (p.y - y) ** 2 <= r * r) {194            if (this.mode === 'erase') this.selection.delete(p.i);195            else this.selection.add(p.i);196          }197        }198  }199  finish() {200    if (this.enabled && this.mode === 'paint' && this.selection.size) this.pulse();201  }202  pulse() {203    if (!this.enabled || !this.selection.size) return;204    this.pulseAt = performance.now();205    this.pulseTick = this.tick;206    this.onPulse(Uint32Array.from(this.selection), {207      profile: this.pulseProfile,208      replace: this.replacePulse,209    });210  }211  preset(indices, profile = 'paint') {212    this.pulseProfile = profile;213    this.replacePulse = true;214    this.selectionDirty = true;215    this.selection = new Set(indices);216    this.onSelection(this.selection.size);217    this.pulse();218  }219  clear() {220    this.pulseProfile = 'paint';221    this.replacePulse = false;222    this.selectionDirty = true;223    this.selection.clear();224    this.onSelection(0);225  }226  result(indices, counts, tick) {227    this.tick = tick;228    this.lastTickAt = performance.now();229    const c = this.fc;230    c.clearRect(0, 0, this.w, this.h);231    c.fillStyle = '#ffd180';232    for (let bucket = 0; bucket < 3; bucket++) {233      c.globalAlpha = [0.55, 0.8, 1][bucket];234      c.beginPath();235      for (let j = 0; j < indices.length; j++) {236        if (Math.min(2, Math.floor((counts[j] - 1) / 3)) !== bucket) continue;237        const p = this.byIndex.get(indices[j]);238        if (!p) continue;239        c.moveTo(p.x + 1.4, p.y);240        c.arc(p.x, p.y, 1.4, 0, Math.PI * 2);241      }242      c.fill();243    }244    c.globalAlpha = 1;245    let halos = 0;246    for (let j = 0; j < indices.length && halos < 48; j++) {247      if (counts[j] < 2) continue;248      const p = this.byIndex.get(indices[j]);249      if (!p) continue;250      const radius = 5 + Math.min(6, counts[j]) * 1.4;251      c.drawImage(this.spark, p.x - radius, p.y - radius, radius * 2, radius * 2);252      halos++;253    }254  }255  animate(now) {256    if (this.disposed) return;257    this.frame = requestAnimationFrame(this.animate);258    if (document.hidden) return;259    const c = this.ctx;260    c.clearRect(0, 0, this.w, this.h);261    c.drawImage(this.base, 0, 0, this.w, this.h);262    const age = (now - (this.pulseAt ?? -10000)) / 1000,263      pulse =264        (this.pulseProfile === 'turn'265          ? Math.exp(266              -Math.max(0, (this.tick - (this.pulseTick ?? this.tick)) * 0.0001 - 0.65) / 0.4,267            )268          : Math.exp(-age * 1.7)) *269        (1 + 0.18 * Math.sin(age * 11));270    if (this.selectionDirty) {271      const s = this.sc;272      s.clearRect(0, 0, this.w, this.h);273      s.fillStyle = '#bba0ff';274      s.beginPath();275      for (const i of this.selection) {276        const p = this.byIndex.get(i);277        if (!p) continue;278        s.moveTo(p.x + 1, p.y);279        s.arc(p.x, p.y, 1, 0, Math.PI * 2);280      }281      s.fill();282      this.selectionDirty = false;283    }284    c.globalAlpha = 0.34 + 0.66 * pulse;285    c.drawImage(this.selectedLayer, 0, 0, this.w, this.h);286    c.globalAlpha = Math.max(0, 1 - (now - this.lastTickAt) / 700);287    c.drawImage(this.fireLayer, 0, 0, this.w, this.h);288    c.globalAlpha = 1;289    if (this.hover) {290      c.strokeStyle = this.mode === 'erase' ? '#eab2b2bb' : '#b696ffdb';291      c.lineWidth = 1.6;292      c.beginPath();293      c.arc(this.hover.x, this.hover.y, this.brush, 0, Math.PI * 2);294      c.stroke();295      c.fillStyle = this.mode === 'erase' ? '#ffc1c109' : '#ab83ff0d';296      c.fill();297    }298  }299  reset() {300    this.fc.clearRect(0, 0, this.w, this.h);301    this.tick = 0;302    this.clear();303  }304  dispose() {305    this.disposed = true;306    cancelAnimationFrame(this.frame);307    this.observer.disconnect();308    for (const [type, fn] of [309      ['pointerdown', this.down],310      ['pointermove', this.move],311      ['pointerup', this.up],312      ['pointercancel', this.up],313      ['pointerleave', this.leave],314    ])315      this.canvas.removeEventListener(type, fn);316  }317}318