/** * GasSim v2 — Ideal Gas simulation (PV=nRT, Maxwell-Boltzmann distribution) * v2: hover inspector, velocity vectors, movable piston, v_mp/v_rms markers. */ class GasSim { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.W = 0; this.H = 0; this.particles = []; this.N = 80; this.T = 1.0; this._wallImpulse = 0; this._pressureSmooth = 0; this._raf = null; this._updateTick = 0; this.onUpdate = null; this._loop = this._loop.bind(this); // v2 this._showVectors = false; this._pistonFrac = 1.0; // fraction of W — right wall position this._hover = null; // hovered particle this._pistonDrag = false; // LabFX throttle this._fxPressureTimer = 0; this._fxLastT = 0; canvas.addEventListener('mousemove', e => this._onMouseMove(e)); canvas.addEventListener('mouseleave', () => { this._hover = null; this._pistonDrag = false; }); canvas.addEventListener('mousedown', e => this._onMouseDown(e)); canvas.addEventListener('mouseup', () => { this._pistonDrag = false; }); } // ── canvas coordinate helper ──────────────────────────────────────────────── _cp(e) { const r = this.canvas.getBoundingClientRect(); return { x: (e.clientX - r.left) * (this.W / r.width), y: (e.clientY - r.top) * (this.H / r.height), }; } _onMouseDown(e) { const { x } = this._cp(e); const px = this.W * this._pistonFrac; if (Math.abs(x - px) < 16) this._pistonDrag = true; } _onMouseMove(e) { const { x, y } = this._cp(e); if (this._pistonDrag) { this.setPiston(x / this.W); return; } // nearest particle within 28px let best = null, bestD = 28; for (const p of this.particles) { const d = Math.hypot(p.x - x, p.y - y); if (d < bestD) { bestD = d; best = p; } } this._hover = best; } // ── public API ────────────────────────────────────────────────────────────── fit() { this.W = this.canvas.offsetWidth; this.H = this.canvas.offsetHeight; this.canvas.width = this.W * devicePixelRatio; this.canvas.height = this.H * devicePixelRatio; this.ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); this.reset(); } reset() { this.particles = []; const px = this.W * this._pistonFrac; for (let i = 0; i < this.N; i++) { const a = Math.random() * Math.PI * 2; const s = this._maxwellSpeed(); this.particles.push({ x: 20 + Math.random() * (px - 40), y: 20 + Math.random() * (this.H - 40), vx: s * Math.cos(a), vy: s * Math.sin(a), r: 5, }); } this._wallImpulse = 0; this._pressureSmooth = 0; this._updateTick = 0; this._hover = null; } setN(n) { this.N = n; this.reset(); } setT(t) { const oldT = this.T; if (oldT <= 0) { this.T = t; this.reset(); return; } const f = Math.sqrt(t / oldT); for (const p of this.particles) { p.vx *= f; p.vy *= f; } this.T = t; } setPiston(frac) { this._pistonFrac = Math.max(0.3, Math.min(1.0, frac)); const px = this.W * this._pistonFrac; for (const p of this.particles) { if (p.x + p.r > px) { p.x = px - p.r; if (p.vx > 0) p.vx = -p.vx; } } } toggleVectors() { this._showVectors = !this._showVectors; } start() { if (!this._raf) this._raf = requestAnimationFrame(this._loop); } stop() { cancelAnimationFrame(this._raf); this._raf = null; } // ── simulation ────────────────────────────────────────────────────────────── _loop(now) { const dt = this._fxLastT ? Math.min(now - this._fxLastT, 80) : 16; this._fxLastT = now; this._step(); this._step(); if (window.LabFX) { LabFX.particles.update(dt); // throttled pressure tick sound (~every 150ms, proportional to pressure) this._fxPressureTimer += dt; if (this._fxPressureTimer >= 150) { this._fxPressureTimer = 0; const P = parseFloat(this.info().P); if (P > 5) LabFX.sound.play('tick', { volume: 0.05 }); } } this.draw(); this._raf = requestAnimationFrame(this._loop); } _maxwellSpeed() { const u1 = Math.max(1e-10, Math.random()); const sigma = this.T * 60; return Math.abs(Math.sqrt(-2 * Math.log(u1)) * Math.cos(Math.PI * 2 * Math.random()) * sigma + sigma); } _step() { const { W, H, particles } = this; const px = W * this._pistonFrac; for (const p of particles) { p.x += p.vx; p.y += p.vy; } for (const p of particles) { if (p.x < p.r) { p.x = p.r; p.vx = Math.abs(p.vx); this._wallImpulse += 2 * Math.abs(p.vx); } else if (p.x > px - p.r) { p.x = px - p.r; p.vx = -Math.abs(p.vx); this._wallImpulse += 2 * Math.abs(p.vx); } if (p.y < p.r) { p.y = p.r; p.vy = Math.abs(p.vy); this._wallImpulse += 2 * Math.abs(p.vy); } else if (p.y > H - p.r) { p.y = H - p.r; p.vy = -Math.abs(p.vy); this._wallImpulse += 2 * Math.abs(p.vy); } } // Spatial grid collision const cell = 14, cols = Math.ceil(W / cell), rows = Math.ceil(H / cell); const grid = new Map(); const key = (cx, cy) => cy * cols + cx; for (let i = 0; i < particles.length; i++) { const p = particles[i]; const k = key(Math.floor(p.x / cell), Math.floor(p.y / cell)); if (!grid.has(k)) grid.set(k, []); grid.get(k).push(i); } const checked = new Set(); for (let i = 0; i < particles.length; i++) { const p = particles[i]; const cx = Math.floor(p.x / cell); const cy = Math.floor(p.y / cell); for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { const nx = cx + dx, ny = cy + dy; if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue; const cell2 = grid.get(key(nx, ny)); if (!cell2) continue; for (const j of cell2) { if (j <= i) continue; const pk = i * 100000 + j; if (checked.has(pk)) continue; checked.add(pk); const q = particles[j]; const ddx = q.x - p.x, ddy = q.y - p.y; const d2 = ddx * ddx + ddy * ddy; const md = p.r + q.r; if (d2 < md * md && d2 > 0) { const d = Math.sqrt(d2), nx2 = ddx / d, ny2 = ddy / d; const dvn = (q.vx - p.vx) * nx2 + (q.vy - p.vy) * ny2; if (dvn >= 0) continue; p.vx += dvn * nx2; p.vy += dvn * ny2; q.vx -= dvn * nx2; q.vy -= dvn * ny2; const ov = (md - d) / 2; p.x -= ov * nx2; p.y -= ov * ny2; q.x += ov * nx2; q.y += ov * ny2; } } } } this._pressureSmooth = this._pressureSmooth * 0.92 + this._wallImpulse * 0.08; this._wallImpulse = 0; if (++this._updateTick % 30 === 0 && this.onUpdate) this.onUpdate(this.info()); } info() { const speeds = this.particles.map(p => Math.hypot(p.vx, p.vy)); const avgSpeed = speeds.length ? speeds.reduce((a, b) => a + b) / speeds.length : 0; const pf = this._pistonFrac; const P = this._pressureSmooth / (2 * (this.W * pf + this.H)) * 100; const V = (this.W * pf * this.H) / 10000; return { N: this.N, T: this.T, P: P.toFixed(1), V: V.toFixed(1), PV: (P * V).toFixed(1), avgSpeed: avgSpeed.toFixed(0), speedData: this._speedHistogram(speeds), }; } _speedHistogram(speeds) { const maxSpeed = this.T * 200; const numBins = 12; const binWidth = maxSpeed / numBins; const bins = new Array(numBins).fill(0); for (const s of speeds) { const idx = Math.floor(s / binWidth); if (idx >= 0 && idx < numBins) bins[idx]++; } return { bins, max: Math.max(...bins, 1), binWidth }; } _mbCurve(v) { const sigma = this.T * 60; return (v / (sigma * sigma)) * Math.exp(-v * v / (2 * sigma * sigma)); } // ── drawing ───────────────────────────────────────────────────────────────── draw() { const { ctx, W, H } = this; const pistonX = W * this._pistonFrac; // Background const bg = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.max(W, H) * 0.7); bg.addColorStop(0, '#080818'); bg.addColorStop(1, '#030308'); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); // Grid ctx.strokeStyle = 'rgba(255,255,255,0.03)'; ctx.lineWidth = 1; ctx.beginPath(); for (let x = 0; x <= W; x += 20) { ctx.moveTo(x, 0); ctx.lineTo(x, H); } for (let y = 0; y <= H; y += 20) { ctx.moveTo(0, y); ctx.lineTo(W, y); } ctx.stroke(); // Dead zone beyond piston if (this._pistonFrac < 0.99) { ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(pistonX, 0, W - pistonX, H); } // Pressure wall glow const P = parseFloat(this.info().P); const wi = Math.min(1, P / 50); if (wi > 0) { const a = wi * 0.3, gd = 30; const glows = [ [ctx.createLinearGradient(0, 0, gd, 0), 0, 0, gd, H], [ctx.createLinearGradient(pistonX, 0, pistonX - gd, 0), pistonX - gd, 0, gd, H], [ctx.createLinearGradient(0, 0, 0, gd), 0, 0, W, gd], [ctx.createLinearGradient(0, H, 0, H - gd), 0, H - gd, W, gd], ]; for (const [g, rx, ry, rw, rh] of glows) { g.addColorStop(0, `rgba(155,93,229,${a})`); g.addColorStop(1, 'rgba(155,93,229,0)'); ctx.fillStyle = g; ctx.fillRect(rx, ry, rw, rh); } } // Velocity vectors if (this._showVectors) { ctx.save(); for (const p of this.particles) { const scale = 3; const ex = p.x + p.vx * scale, ey = p.y + p.vy * scale; const ang = Math.atan2(p.vy, p.vx); ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(ex, ey); ctx.stroke(); const hl = 4; ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.beginPath(); ctx.moveTo(ex, ey); ctx.lineTo(ex - hl * Math.cos(ang - 0.4), ey - hl * Math.sin(ang - 0.4)); ctx.lineTo(ex - hl * Math.cos(ang + 0.4), ey - hl * Math.sin(ang + 0.4)); ctx.closePath(); ctx.fill(); } ctx.restore(); } // Particles for (const p of this.particles) { const spd = Math.hypot(p.vx, p.vy); const T = this.T; const color = spd < T * 40 ? '#4CC9F0' : spd < T * 80 ? '#7BF5A4' : spd < T * 120 ? '#FFD166' : '#EF476F'; const isH = this._hover === p; ctx.save(); ctx.shadowBlur = isH ? 20 : 8; ctx.shadowColor = color; ctx.beginPath(); ctx.arc(p.x, p.y, isH ? p.r + 2 : p.r, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); if (isH) { ctx.strokeStyle = 'rgba(255,255,255,0.6)'; ctx.lineWidth = 1.5; ctx.stroke(); } ctx.restore(); } // Piston this._drawPiston(ctx, pistonX, H); // Hover inspector if (this._hover) this._drawInspector(ctx, this._hover, W, H); // Histogram this._drawHistogram(ctx, W, H); if (window.LabFX) LabFX.particles.draw(ctx); } _drawPiston(ctx, pistonX, H) { if (this._pistonFrac >= 0.99) return; ctx.save(); const pw = 8; ctx.shadowBlur = 16; ctx.shadowColor = 'rgba(255,209,102,0.5)'; const g = ctx.createLinearGradient(pistonX - pw, 0, pistonX + pw, 0); g.addColorStop(0, 'rgba(255,209,102,0.4)'); g.addColorStop(0.5, 'rgba(255,209,102,0.9)'); g.addColorStop(1, 'rgba(255,209,102,0.3)'); ctx.fillStyle = g; ctx.fillRect(pistonX - pw / 2, 0, pw, H); // Handle const hh = 44, hw = 18, hx = pistonX - hw / 2, hy = H / 2 - hh / 2; ctx.shadowBlur = 0; ctx.fillStyle = 'rgba(255,209,102,0.88)'; ctx.beginPath(); ctx.roundRect(hx, hy, hw, hh, 4); ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 1.5; for (let i = 0; i < 3; i++) { const gy = hy + 10 + i * 10; ctx.beginPath(); ctx.moveTo(hx + 4, gy); ctx.lineTo(hx + hw - 4, gy); ctx.stroke(); } ctx.fillStyle = 'rgba(255,209,102,0.7)'; ctx.font = "bold 9px 'Manrope', sans-serif"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⇌', pistonX, hy - 12); ctx.restore(); } _drawInspector(ctx, p, W, H) { const spd = Math.hypot(p.vx, p.vy); const ang = Math.atan2(p.vy, p.vx) * 180 / Math.PI; const ke = 0.5 * spd * spd; const T = this.T; const clr = spd < T * 40 ? '#4CC9F0' : spd < T * 80 ? '#7BF5A4' : spd < T * 120 ? '#FFD166' : '#EF476F'; const rows = [ ['|v|', spd.toFixed(1) + ' у.е.'], ['vx', p.vx.toFixed(1)], ['vy', p.vy.toFixed(1)], ['KE', ke.toFixed(0) + ' у.е.'], ['угол', ang.toFixed(1) + '°'], ]; const tw = 132, th = 18 + rows.length * 17 + 8; let tx = p.x + 14, ty = p.y - th / 2; if (tx + tw > W - 10) tx = p.x - tw - 14; ty = Math.max(8, Math.min(H - th - 8, ty)); ctx.save(); ctx.fillStyle = 'rgba(6,8,28,0.92)'; ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.fill(); ctx.fillStyle = clr; ctx.beginPath(); ctx.roundRect(tx, ty, tw, 3, [8, 8, 0, 0]); ctx.fill(); ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.stroke(); ctx.beginPath(); ctx.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1; ctx.stroke(); ctx.font = "11px 'Manrope', monospace"; ctx.textBaseline = 'middle'; for (let i = 0; i < rows.length; i++) { const ry = ty + 18 + i * 17; ctx.fillStyle = 'rgba(255,255,255,0.42)'; ctx.textAlign = 'left'; ctx.fillText(rows[i][0], tx + 10, ry); ctx.fillStyle = 'rgba(255,255,255,0.92)'; ctx.textAlign = 'right'; ctx.fillText(rows[i][1], tx + tw - 10, ry); } ctx.restore(); } _drawHistogram(ctx, W, H) { const speeds = this.particles.map(p => Math.hypot(p.vx, p.vy)); const hist = this._speedHistogram(speeds); const hw = 204, hh = 102; const hx = W - hw - 12, hy = H - hh - 12; const pad = { l: 8, r: 8, t: 20, b: 18 }; const barW = (hw - pad.l - pad.r) / hist.bins.length; const barAreaH = hh - pad.t - pad.b; const maxV = this.T * 200; ctx.save(); ctx.fillStyle = 'rgba(0,0,0,0.58)'; ctx.beginPath(); ctx.roundRect(hx, hy, hw, hh, 6); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.72)'; ctx.font = '9px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Распределение скоростей', hx + hw / 2, hy + 11); ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.font = '8px sans-serif'; ctx.fillText('v (у.е.)', hx + hw / 2, hy + hh - 2); // Bars for (let i = 0; i < hist.bins.length; i++) { const ratio = hist.bins[i] / hist.max; const bh = ratio * barAreaH; const bx = hx + pad.l + i * barW; const by = hy + pad.t + barAreaH - bh; ctx.fillStyle = 'rgba(155,93,229,0.75)'; ctx.beginPath(); ctx.roundRect(bx + 0.5, by, barW - 1, bh, 2); ctx.fill(); } // MB theoretical curve ctx.strokeStyle = 'rgba(255,209,102,0.9)'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 3]); ctx.beginPath(); let first = true; for (let i = 0; i <= 80; i++) { const v = (i / 80) * maxV; const sc = this._mbCurve(v) * speeds.length * hist.binWidth / hist.max; const cx2 = hx + pad.l + (v / maxV) * (hw - pad.l - pad.r); const cy2 = hy + pad.t + barAreaH - sc * barAreaH; if (first) { ctx.moveTo(cx2, cy2); first = false; } else ctx.lineTo(cx2, cy2); } ctx.stroke(); ctx.setLineDash([]); // Characteristic speed lines const sigma = this.T * 60; const v_mp = sigma; // v most probable (mode) const v_rms = sigma * Math.sqrt(2); // v_rms in 2D = sqrt(2) * sigma const vline = (v, color, label) => { if (v > maxV) return; const vx2 = hx + pad.l + (v / maxV) * (hw - pad.l - pad.r); ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.setLineDash([2, 3]); ctx.beginPath(); ctx.moveTo(vx2, hy + pad.t); ctx.lineTo(vx2, hy + pad.t + barAreaH); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = color; ctx.font = '7px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(label, vx2, hy + pad.t - 3); }; vline(v_mp, 'rgba(76,201,240,0.9)', 'v_mp'); vline(v_rms, 'rgba(239,71,111,0.9)', 'v_rms'); ctx.restore(); } } /* ─── lab UI init ─────────────────────────────────── */ function _openMolPhys(mode) { document.getElementById('sim-topbar-title').textContent = 'Молекулярная физика'; _simShow('sim-molphys'); _simShow('ctrl-molphys'); requestAnimationFrame(() => requestAnimationFrame(() => { // lazy-init all sims if (!gasSim) { gasSim = new GasSim(document.getElementById('gas-canvas')); gasSim.onUpdate = _gasUpdateUI; } if (!brownSim) { brownSim = new BrownianSim(document.getElementById('brownian-canvas')); brownSim.onUpdate = _brownUpdateUI; } if (!statesSim) { statesSim = new StatesSim(document.getElementById('states-canvas')); statesSim.onUpdate = _statesUpdateUI; } if (!diffSim) { diffSim = new DiffusionSim(document.getElementById('diffusion-canvas')); diffSim.onUpdate = _diffUpdateUI; } molMode(mode || 'gas'); })); } function molMode(mode, btn) { _molMode = mode; // stop all if (gasSim) gasSim.stop(); if (brownSim) brownSim.stop(); if (statesSim) statesSim.stop(); if (diffSim) diffSim.stop(); // toggle mode buttons document.querySelectorAll('.mol-mode').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); else { const mb = document.getElementById('mol-mode-' + mode); if (mb) mb.classList.add('active'); } // toggle panels const panels = ['gas', 'brownian', 'states', 'diffusion']; panels.forEach(p => { document.getElementById('mol-panel-' + p).style.display = p === mode ? '' : 'none'; }); // toggle canvases document.getElementById('gas-canvas').style.display = mode === 'gas' ? 'block' : 'none'; document.getElementById('brownian-canvas').style.display = mode === 'brownian' ? 'block' : 'none'; document.getElementById('states-canvas').style.display = mode === 'states' ? 'block' : 'none'; document.getElementById('diffusion-canvas').style.display = mode === 'diffusion' ? 'block' : 'none'; // toggle topbar diffusion partition button document.getElementById('ctrl-mol-diff').style.display = mode === 'diffusion' ? 'contents' : 'none'; // start active sim const titles = { gas: 'Молекулярная физика — Газ', brownian: 'Молекулярная физика — Броуновское', states: 'Молекулярная физика — Фазы', diffusion: 'Молекулярная физика — Диффузия' }; document.getElementById('sim-topbar-title').textContent = titles[mode] || 'Молекулярная физика'; if (mode === 'gas') { gasSim.fit(); gasSim.start(); } if (mode === 'brownian') { brownSim.fit(); brownSim.start(); } if (mode === 'states') { statesSim.fit(); statesSim.start(); } if (mode === 'diffusion') { diffSim.fit(); diffSim.start(); } } function molReset() { if (window.LabFX) LabFX.sound.play('click'); if (_molMode === 'gas' && gasSim) { gasSim.reset(); document.getElementById('sl-gPiston').value = 100; document.getElementById('g-piston').textContent = '100%'; } if (_molMode === 'brownian' && brownSim) brownSim.reset(); if (_molMode === 'states' && statesSim) { statesSim.reset(); document.getElementById('sl-stN').value = 64; document.getElementById('st-N').textContent = '64'; const vBtn = document.getElementById('states-vec-btn'); if (vBtn) { vBtn.textContent = 'Векторы скоростей: Выкл'; vBtn.style.color = ''; } } if (_molMode === 'diffusion' && diffSim) { diffSim.reset(); document.getElementById('diffusion-part-btn').textContent = '‖ Раздел'; document.getElementById('df-part-row').classList.add('active'); document.getElementById('df-pore-row').classList.remove('active'); } } function gasNChange() { const n = +document.getElementById('sl-gN').value; document.getElementById('g-N').textContent = n; if (gasSim) { gasSim.setN(n); } } function gasTChange() { const raw = +document.getElementById('sl-gT').value; const t = raw / 10; document.getElementById('g-T').textContent = t.toFixed(1) + ' у.е.'; if (gasSim) gasSim.setT(t); } function gasPistonChange() { const v = +document.getElementById('sl-gPiston').value; document.getElementById('g-piston').textContent = v + '%'; if (gasSim) gasSim.setPiston(v / 100); } function gasToggleVectors(btn) { if (!gasSim) return; gasSim.toggleVectors(); btn.textContent = 'Векторы скоростей: ' + (gasSim._showVectors ? 'Вкл' : 'Выкл'); btn.style.color = gasSim._showVectors ? '#7BF5A4' : ''; } function _gasUpdateUI(info) { document.getElementById('gstat-P').textContent = info.P; document.getElementById('gstat-V').textContent = info.V; document.getElementById('gstat-PV').textContent = info.PV; document.getElementById('gstat-v').textContent = info.avgSpeed + ' у.е.'; document.getElementById('mpbar-l1').textContent = 'N'; document.getElementById('mpbar-v1').textContent = info.N; document.getElementById('mpbar-l2').textContent = 'T'; document.getElementById('mpbar-v2').textContent = info.T.toFixed(1); document.getElementById('mpbar-l3').textContent = 'P'; document.getElementById('mpbar-v3').textContent = info.P; document.getElementById('mpbar-l4').textContent = 'V'; document.getElementById('mpbar-v4').textContent = info.V; document.getElementById('mpbar-l5').textContent = 'PV'; document.getElementById('mpbar-v5').textContent = info.PV; } function brownNChange() { const n = +document.getElementById('sl-brN').value; document.getElementById('br-N').textContent = n; if (brownSim) brownSim.setN(n); } function brownTChange() { const t = +document.getElementById('sl-brT').value / 10; document.getElementById('br-T').textContent = t.toFixed(1) + ' у.е.'; if (brownSim) brownSim.setT(t); } function _brownUpdateUI(info) { document.getElementById('brstat-dr').textContent = info.displacement + ' px'; document.getElementById('brstat-msd').textContent = info.msd + ' px²'; document.getElementById('brstat-v').textContent = info.speed; document.getElementById('brstat-steps').textContent = info.steps; document.getElementById('mpbar-l1').textContent = 'Шагов'; document.getElementById('mpbar-v1').textContent = info.steps; document.getElementById('mpbar-l2').textContent = '|Δr|'; document.getElementById('mpbar-v2').textContent = info.displacement + ' px'; document.getElementById('mpbar-l3').textContent = 'MSD'; document.getElementById('mpbar-v3').textContent = info.msd + ' px²'; document.getElementById('mpbar-l4').textContent = 'v'; document.getElementById('mpbar-v4').textContent = info.speed; document.getElementById('mpbar-l5').textContent = 'N'; document.getElementById('mpbar-v5').textContent = info.N; } function statesTChange() { const raw = +document.getElementById('sl-stT').value; const t = raw / 100; document.getElementById('st-T').textContent = t.toFixed(2); if (statesSim) statesSim.setT(t); } function statesPreset(t) { document.getElementById('sl-stT').value = Math.round(t * 100); document.getElementById('st-T').textContent = t.toFixed(2); if (window.LabFX) { const stateIdx = t < 0.2 ? 0 : t < 0.5 ? 1 : 2; LabFX.sound.play('whoosh', { pitch: [0.7, 1.0, 1.3][stateIdx], volume: 0.3 }); } if (statesSim) statesSim.setT(t); } function statesNChange() { const n = +document.getElementById('sl-stN').value; document.getElementById('st-N').textContent = n; if (statesSim) statesSim.setN(n); } function statesToggleVectors(btn) { if (!statesSim) return; statesSim.toggleVectors(); btn.textContent = 'Векторы скоростей: ' + (statesSim._showVectors ? 'Вкл' : 'Выкл'); btn.style.color = statesSim._showVectors ? '#7BF5A4' : ''; } function _statesUpdateUI(info) { const phaseColors = { solid: '#4CC9F0', liquid: '#7BF5A4', gas: '#EF476F' }; const phaseLabels = { solid: 'Твёрдое', liquid: 'Жидкость', gas: 'Газ' }; const c = phaseColors[info.phase] || '#fff'; document.getElementById('ststat-phase').textContent = phaseLabels[info.phase] || info.phase; document.getElementById('ststat-phase').style.color = c; document.getElementById('ststat-KE').textContent = info.avgKE; document.getElementById('ststat-PE').textContent = info.avgPE; const pEl = document.getElementById('ststat-P'); if (pEl) pEl.textContent = info.P !== undefined ? info.P : '—'; document.getElementById('mpbar-l1').textContent = 'Фаза'; document.getElementById('mpbar-v1').textContent = phaseLabels[info.phase] || info.phase; document.getElementById('mpbar-v1').style.color = c; document.getElementById('mpbar-l2').textContent = 'T'; document.getElementById('mpbar-v2').textContent = info.T.toFixed(2); document.getElementById('mpbar-l3').textContent = 'KE'; document.getElementById('mpbar-v3').textContent = info.avgKE; document.getElementById('mpbar-l4').textContent = 'PE'; document.getElementById('mpbar-v4').textContent = info.avgPE; document.getElementById('mpbar-l5').textContent = 'P'; document.getElementById('mpbar-v5').textContent = info.P !== undefined ? info.P : '—'; } function diffNChange() { const n = +document.getElementById('sl-dfN').value; document.getElementById('df-N').textContent = n; if (diffSim) diffSim.setN(n); } function diffTChange() { const t = +document.getElementById('sl-dfT').value / 10; document.getElementById('df-T').textContent = t.toFixed(1) + ' у.е.'; if (diffSim) diffSim.setT(t); } function diffPartitionToggle(rowEl) { if (!diffSim) return; diffSim.togglePartition(); const on = diffSim.partitionOn; rowEl.classList.toggle('active', on); document.getElementById('diffusion-part-btn').innerHTML = on ? '‖ Раздел' : ' Раздел снят'; } function diffPartitionBtn() { if (!diffSim) return; const on = diffSim.partitionOn; document.getElementById('diffusion-part-btn').innerHTML = on ? '‖ Раздел' : ' Раздел снят'; document.getElementById('df-part-row').classList.toggle('active', on); } function diffPoreToggle(rowEl) { if (!diffSim) return; diffSim.togglePore(); const pore = diffSim._poreMode; const on = diffSim.partitionOn; rowEl.classList.toggle('active', pore); const tog = document.getElementById('df-pore-toggle'); if (tog) tog.style.background = pore ? '#FFB347' : 'rgba(255,255,255,0.15)'; const span = tog && tog.querySelector('span'); if (span) span.style.marginLeft = pore ? '14px' : '2px'; // Also sync partition row document.getElementById('df-part-row').classList.toggle('active', on); } function _diffUpdateUI(info) { document.getElementById('dfstat-LA').textContent = info.leftA; document.getElementById('dfstat-LB').textContent = info.leftB; document.getElementById('dfstat-RA').textContent = info.rightA; document.getElementById('dfstat-RB').textContent = info.rightB; document.getElementById('dfstat-mix').textContent = info.mixed + '%'; document.getElementById('mpbar-l1').textContent = 'Смешивание'; document.getElementById('mpbar-v1').textContent = info.mixed + '%'; document.getElementById('mpbar-l2').textContent = 'Лево A/B'; document.getElementById('mpbar-v2').textContent = info.leftA + '/' + info.leftB; document.getElementById('mpbar-l3').textContent = 'Право A/B'; document.getElementById('mpbar-v3').textContent = info.rightA + '/' + info.rightB; document.getElementById('mpbar-l4').textContent = 'Раздел'; const partLabel = !info.partitionOn ? 'снят' : info.poreMode ? 'пора' : 'вкл'; document.getElementById('mpbar-v4').textContent = partLabel; document.getElementById('mpbar-v4').style.color = !info.partitionOn ? '#34d399' : info.poreMode ? '#FFB347' : '#fff'; document.getElementById('mpbar-l5').textContent = 'Шагов'; document.getElementById('mpbar-v5').textContent = info.steps; } /* ════════════════════════════════ ЗАКОН КУЛОНА ════════════════════════════════ */