{"path":"visualization/atlas.js","url":"/source/visualization/atlas.js.txt","kind":"js","bytes":68407,"sha256":"49b64981dcd0c49e021b8eb040b217ea49a71b9ac631bb5a079e30eaf882cace","local_paths_redacted":false,"start":1,"end":120,"total_lines":932,"content":"/* Living Systems — no runtime dependencies or external data requests. */\n(function () {\n  \"use strict\";\n  const D = window.ATLAS_DATA, M = window.AtlasModel;\n  const $ = id => document.getElementById(id);\n  const canvas = $(\"world\"), ctx = canvas.getContext(\"2d\", { alpha: false });\n  if (!ctx || !D || !M) { $(\"world-title\").textContent = \"Canvas unavailable\"; return; }\n  const motionPreference = matchMedia(\"(prefers-reduced-motion: reduce)\");\n  const S = { scene: \"thermal\", playing: !motionPreference.matches, clock: 0, temperature: 30,\n    curve: \"chlorella_tpc:49\", model: \"sharpeschoolhigh_1981\", uncertainty: true,\n    cell: 0, sweep: 0, time: 0.24, speed: 0.25, passive: false,\n    firing: D.neural.reference.firing, release: D.neural.reference.release,\n    overlap: D.neural.reference.overlap, housekeeping: true, process: \"all\", scale: \"shared\", verticalZoom: 1,\n    compareModels: false, residuals: false, membraneMode: \"recorded\", hhCurrent: 7, sodium: 120, potassium: 36,\n    window: 0, audioMode: \"spikes\", compareBudget: true, thermalAmplitude: 0,\n    hhTemperature: 6.3, hhProtocol: \"step\", pulseInterval: 12,\n    depletion: false, recovery: 0.8, resource: 1, resourceTime: 0 };\n  const audio = new window.AtlasAudio();\n  let hh = M.hhTrace();\n  let currentDrag = null;\n  const channelRestore = { sodium: 120, potassium: 36 };\n  const TAU = Math.PI * 2;\n  const colors = { thermal: \"#dce8a0\", membrane: \"#cbbdee\", neural: \"#f6c085\" };\n  const flows = [\n    { key: \"postsynaptic receptors\", label: \"Postsynaptic\", short: \"RECEPTORS\", color: \"#d3b0df\" },\n    { key: \"action potentials\", label: \"Action potentials\", short: \"SPIKES\", color: \"#edbd7c\" },\n    { key: \"resting potentials\", label: \"Resting potentials\", short: \"RESTING\", color: \"#8dc6b7\" },\n    { key: \"presynaptic Ca2+ and vesicles\", label: \"Presynaptic\", short: \"VESICLES\", color: \"#df937d\" },\n    { key: \"glutamate recycling\", label: \"Glutamate recycling\", short: \"RECYCLING\", color: \"#b5ca86\" },\n    { key: \"housekeeping\", label: \"Housekeeping\", short: \"MAINTENANCE\", color: \"#a5b3c5\" },\n  ];\n  const componentLabels = { postsynaptic_ampa: \"AMPA\", postsynaptic_nmda: \"NMDA\", postsynaptic_mglur: \"mGluR\",\n    action_potential_axon: \"Axon\", action_potential_soma: \"Soma\", action_potential_dendrite: \"Dendrites\",\n    resting_potential_neuron: \"Neuron\", resting_potential_glia: \"Glia\", presynaptic: \"Ca²⁺ / vesicles\",\n    glutamate_recycling: \"Glutamate\", housekeeping: \"Housekeeping\" };\n  const referenceBudget = M.budget(D.neural, 4, 0.25, 1.24, true);\n  const thermalTemperature = () => S.temperature + S.thermalAmplitude * Math.sin(S.clock * TAU / 12);\n  const effectiveRelease = () => S.release * (S.depletion ? S.resource : 1);\n  const currentBudget = () => M.budget(D.neural, S.firing, effectiveRelease(), S.overlap, S.housekeeping);\n  const currentAt = (trace, t) => (trace.pulses || [[trace.on, trace.off]]).some(([a,b]) => t >= a && t < b) ? trace.amplitude_pa : 0;\n  const regenerateHH = () => { hh = M.hhTrace(S.hhCurrent, S.sodium, S.potassium,\n    { temperature: S.hhTemperature, protocol: S.hhProtocol, interval: S.pulseInterval }); audio.resetClock(); };\n  let cycleCache = null;\n  function thermalCycle() {\n    const key = [S.curve, S.model, S.temperature, S.thermalAmplitude].join(\"/\");\n    if (cycleCache?.key !== key) cycleCache = { key, value: M.cycleAverage(D.thermal.curves.find(c => c.id === S.curve).models[S.model], S.temperature, S.thermalAmplitude) };\n    return cycleCache.value;\n  }\n  function cycleBounds() {\n    const low = 11 + S.thermalAmplitude, high = 54 - S.thermalAmplitude;\n    S.temperature = clamp(S.temperature, low, high); $(\"temperature\").min = low; $(\"temperature\").max = high;\n    const ends = $(\"temperature\").parentElement.querySelectorAll(\".range-ends span\");\n    ends[0].textContent = `${low} °C`; ends[1].textContent = `${high} °C`;\n  }\n  let width = 0, height = 0, dpr = 1, raf = 0, last = 0, lastUI = 0, dragging = false;\n  let hitAreas = [], geometry = {}, liveBudget = null;\n  const seeded = n => { const x = Math.sin(n * 127.17 + 19.7) * 43758.5453; return x - Math.floor(x); };\n  const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x));\n  const mix = (a, b, t) => a + (b - a) * t;\n  const rgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));\n  const blend = (a, b, f) => \"#\" + rgb(a).map((v, i) => Math.round(mix(v, rgb(b)[i], clamp(f, 0, 1))).toString(16).padStart(2, \"0\")).join(\"\");\n  const alpha = (hex, a) => `rgba(${rgb(hex).join(\",\")},${a})`;\n  function text(str, x, y, { color = \"#c5d4c9\", size = 9, align = \"left\", serif = false } = {}) {\n    ctx.fillStyle = color; ctx.font = `${size}px ${serif ? \"Georgia,serif\" : \"Menlo,Consolas,monospace\"}`;\n    ctx.textAlign = align; ctx.fillText(str, x, y); ctx.textAlign = \"left\";\n  }\n  function line(x1, y1, x2, y2, color, thickness = 1) {\n    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = color;\n    ctx.lineWidth = thickness; ctx.stroke();\n  }\n  function dot(x, y, r, color) { ctx.beginPath(); ctx.arc(x, y, r, 0, TAU); ctx.fillStyle = color; ctx.fill(); }\n  function glow(x, y, r, color, strength = 0.2) {\n    const g = ctx.createRadialGradient(x, y, 0, x, y, r);\n    g.addColorStop(0, alpha(color, strength)); g.addColorStop(1, alpha(color, 0));\n    ctx.fillStyle = g; ctx.fillRect(x - r, y - r, r * 2, r * 2);\n  }\n  function cubic(p, t) {\n    const u = 1 - t;\n    return { x: u ** 3 * p[0].x + 3 * u ** 2 * t * p[1].x + 3 * u * t ** 2 * p[2].x + t ** 3 * p[3].x,\n      y: u ** 3 * p[0].y + 3 * u ** 2 * t * p[1].y + 3 * u * t ** 2 * p[2].y + t ** 3 * p[3].y };\n  }\n  function strokeCubic(p, color, w, dash = []) {\n    ctx.beginPath(); ctx.moveTo(p[0].x, p[0].y);\n    ctx.bezierCurveTo(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y);\n    ctx.strokeStyle = color; ctx.lineWidth = w; ctx.setLineDash(dash); ctx.stroke(); ctx.setLineDash([]);\n  }\n  function background() {\n    const base = S.scene === \"thermal\" ? \"#102a2b\" : S.scene === \"membrane\" ? \"#17252e\" : \"#292925\";\n    ctx.fillStyle = base; ctx.fillRect(0, 0, width, height);\n    glow(width * 0.67, height * 0.48, width * 0.55, colors[S.scene], 0.05);\n    ctx.strokeStyle = \"#d2e7d007\"; ctx.lineWidth = 0.7;\n    for (let x = 24; x < width; x += 48) for (let y = 20; y < height; y += 48) {\n      line(x - 2, y, x + 2, y, \"#d2e7d016\", 0.7); line(x, y - 2, x, y + 2, \"#d2e7d016\", 0.7);\n    }\n    for (let i = 0; i < 42; i++) {\n      const x = seeded(i + 13) * width, y = seeded(i + 67) * height;\n      const offset = S.clock * (2 + seeded(i) * 3);\n      dot((x + offset) % width, y, seeded(i + 25) * 1.2, \"#d2e7d020\");\n    }\n    hitAreas = [];\n  }\n  function thermal() {\n    const mobile = width < 700;\n    const selected = D.thermal.curves.find(c => c.id === S.curve);\n    const curves = D.thermal.curves.filter(c => c.trait === selected.trait);\n    const xs = D.thermal.temperatures;\n    const x0 = mobile ? 49 : 90;\n    const x1 = width - (mobile ? 65 : 130), span = x1 - x0;\n    const yBase = height - (mobile ? 204 : 170);\n    const amp = mobile ? 170 : height * 0.42;\n    const step = mobile ? 19 : 32;\n    const shift = mobile ? 4 : 18;\n    const maxRate = Math.max(...curves.flatMap(c => c.models[S.model].rate));\n    const sharedMax = maxRate * 1.1 / S.verticalZoom;\n    const pal = [\"#dce8a0\", \"#b9d7a0\", \"#8dc7ab\", \"#6eb5af\", \"#63a0aa\"];\n    const xT = t => x0 + (t - 11) / 43 * span;\n    geometry = { x0, x1, yBase, xT, mode: \"thermal\" };\n    // The selected-temperature plane ties the five independent curves together.\n    const temperature = thermalTemperature(), scanX = xT(temperature);\n    const plane = ctx.createLinearGradient(scanX - 10, 0, scanX + 10, 0);","offset":0,"next_offset":null,"next_start":121}