Files
Learn_System/frontend/js/labs/gas.js
T
Maxim Dolgolyov ae31e4c4e8 refactor: distribute lab-init.js into 34 engine files
lab-init.js: 4098 -> 543 lines (infrastructure + THEORY only)

Each sim's _open*() + UI helpers moved to its engine file:
graph.js, projectile.js, collision.js, magnetic.js, triangle.js,
geometry.js, trigcircle.js, gas.js (molphys), coulomb.js, circuit.js,
reactions.js (chemistry), newton.js (dynamics), chemsandbox.js,
celldivision.js, photosynthesis.js, angrybirds.js, quadratic.js,
normaldist.js, graphtransform.js, pendulum.js, equilibrium.js,
thinlens.js, mirror.js, isoprocess.js, titration.js, refraction.js,
probability.js, bohratom.js, electrolysis.js, waves.js,
crystal.js, orbitals.js, stereo.js, hydrostatics.js

All 34 engine files syntax-checked OK.
2026-05-08 14:54:54 +03:00

727 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
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() {
this._step();
this._step();
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);
}
_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 (_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 (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 ? '‖ Раздел' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Раздел снят';
}
function diffPartitionBtn() {
if (!diffSim) return;
const on = diffSim.partitionOn;
document.getElementById('diffusion-part-btn').innerHTML = on ? '‖ Раздел' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Раздел снят';
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;
}
/* ════════════════════════════════
ЗАКОН КУЛОНА
════════════════════════════════ */