ae31e4c4e8
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.
536 lines
20 KiB
JavaScript
536 lines
20 KiB
JavaScript
'use strict';
|
||
/* ══════════════════════════════════════════════════════════════
|
||
IsoprocessSim — PV-diagram for 4 ideal-gas isoprocesses
|
||
n = 1, R = 0.0821 L·atm/mol·K; energies in Joules
|
||
Isothermal PV = const ΔU=0, W=nRT·ln(V2/V1), Q=W
|
||
Isochoric V = const W=0, ΔU=νCvΔT, Q=ΔU
|
||
Isobaric P = const W=PΔV, ΔU=νCvΔT, Q=ΔU+W
|
||
Adiabatic PV^γ = const Q=0, ΔU=-W, W=PΔV/(γ-1)
|
||
══════════════════════════════════════════════════════════════ */
|
||
|
||
class IsoprocessSim {
|
||
constructor(canvas) {
|
||
this.canvas = canvas;
|
||
this.ctx = canvas.getContext('2d');
|
||
this.W = 0; this.H = 0;
|
||
|
||
/* physics */
|
||
this.n = 1;
|
||
this.R = 0.0821; // L·atm / mol·K
|
||
this.R_J = 8.314; // J / mol·K
|
||
this.gamma = 1.4; // 7/5 diatomic default
|
||
|
||
/* state */
|
||
this.P1 = 3.0; // atm
|
||
this.V1 = 10.0; // L
|
||
this._ratio = 0.5; // 0..1, maps end state position along process
|
||
|
||
/* process */
|
||
this.process = 'isothermal';
|
||
|
||
/* axis range */
|
||
this.Vmin = 1; this.Vmax = 33;
|
||
this.Pmin = 0.2; this.Pmax = 9.5;
|
||
|
||
/* margins */
|
||
this.ML = 52; this.MB = 46; this.MT = 20; this.MR = 18;
|
||
|
||
this._drag = null; // 'state1' | 'state2'
|
||
this.onUpdate = null;
|
||
|
||
this._bindEvents();
|
||
new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);
|
||
}
|
||
|
||
/* ── public API ─────────────────────────────── */
|
||
|
||
fit() {
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const w = this.canvas.offsetWidth || 600;
|
||
const h = this.canvas.offsetHeight || 400;
|
||
this.canvas.width = w * dpr;
|
||
this.canvas.height = h * dpr;
|
||
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
this.W = w; this.H = h;
|
||
}
|
||
|
||
setProcess(p) { this.process = p; this.draw(); this._emit(); }
|
||
setGamma(g) { this.gamma = +g; this.draw(); this._emit(); }
|
||
getParams() { return { P1: this.P1, V1: this.V1, process: this.process }; }
|
||
setParams({ P1, V1 } = {}) {
|
||
if (P1 !== undefined) this.P1 = Math.max(0.4, Math.min(8, +P1));
|
||
if (V1 !== undefined) this.V1 = Math.max(2, Math.min(28, +V1));
|
||
this.draw(); this._emit();
|
||
}
|
||
setRatio(r) { this._ratio = Math.max(0.01, Math.min(0.99, +r)); this.draw(); this._emit(); }
|
||
|
||
/* ── coordinate transforms ─────────────────── */
|
||
|
||
_pw() { return this.W - this.ML - this.MR; }
|
||
_ph() { return this.H - this.MT - this.MB; }
|
||
|
||
_vx(v) { return this.ML + (v - this.Vmin) / (this.Vmax - this.Vmin) * this._pw(); }
|
||
_py(p) { return this.MT + (1 - (p - this.Pmin) / (this.Pmax - this.Pmin)) * this._ph(); }
|
||
_xv(x) { return this.Vmin + (x - this.ML) / this._pw() * (this.Vmax - this.Vmin); }
|
||
_yp(y) { return this.Pmin + (1 - (y - this.MT) / this._ph()) * (this.Pmax - this.Pmin); }
|
||
|
||
/* ── physics ───────────────────────────────── */
|
||
|
||
_T(P, V) { return P * V / (this.n * this.R); }
|
||
|
||
_state2() {
|
||
const { P1, V1, _ratio, gamma } = this;
|
||
/* ratio in [0..1] → multiplier in [0.2..3.5] for V2/V1 or P2/P1 */
|
||
const mult = 0.2 + _ratio * 3.3;
|
||
const clampV = v => Math.max(this.Vmin + 0.5, Math.min(this.Vmax - 0.5, v));
|
||
const clampP = p => Math.max(this.Pmin + 0.05, Math.min(this.Pmax - 0.1, p));
|
||
|
||
switch (this.process) {
|
||
case 'isothermal': {
|
||
const V2 = clampV(V1 * mult);
|
||
return { P2: clampP(P1 * V1 / V2), V2 };
|
||
}
|
||
case 'isochoric': {
|
||
return { P2: clampP(P1 * mult), V2: V1 };
|
||
}
|
||
case 'isobaric': {
|
||
const V2 = clampV(V1 * mult);
|
||
return { P2: P1, V2 };
|
||
}
|
||
case 'adiabatic': {
|
||
const V2 = clampV(V1 * mult);
|
||
return { P2: clampP(P1 * Math.pow(V1 / V2, gamma)), V2 };
|
||
}
|
||
}
|
||
return { P2: P1, V2: V1 };
|
||
}
|
||
|
||
info() {
|
||
const { P1, V1, n, R_J, gamma } = this;
|
||
const T1 = this._T(P1, V1);
|
||
const { P2, V2 } = this._state2();
|
||
const T2 = this._T(P2, V2);
|
||
|
||
/* internal energy: ΔU = νCvΔT, Cv = R/(γ-1) */
|
||
const Cv_J = R_J / (gamma - 1);
|
||
const dU_J = n * Cv_J * (T2 - T1);
|
||
|
||
/* P in Pa = P_atm * 101325, V in m³ = V_L * 0.001 */
|
||
const P1Pa = P1 * 101325, P2Pa = P2 * 101325;
|
||
const V1m3 = V1 * 0.001, V2m3 = V2 * 0.001;
|
||
|
||
let W_J = 0, Q_J = 0;
|
||
switch (this.process) {
|
||
case 'isothermal':
|
||
W_J = n * R_J * T1 * Math.log(V2 / V1);
|
||
Q_J = W_J; break;
|
||
case 'isochoric':
|
||
W_J = 0; Q_J = dU_J; break;
|
||
case 'isobaric':
|
||
W_J = P1Pa * (V2m3 - V1m3);
|
||
Q_J = dU_J + W_J; break;
|
||
case 'adiabatic':
|
||
Q_J = 0;
|
||
W_J = -dU_J; break;
|
||
}
|
||
|
||
const fmt = x => (x >= 0 ? '+' : '') + Math.round(x);
|
||
return {
|
||
P1: P1.toFixed(2), V1: V1.toFixed(1), T1: Math.round(T1),
|
||
P2: P2.toFixed(2), V2: V2.toFixed(1), T2: Math.round(T2),
|
||
W: fmt(W_J), Q: fmt(Q_J), dU: fmt(Math.round(dU_J)),
|
||
W_raw: W_J, Q_raw: Q_J, dU_raw: dU_J,
|
||
process: this.process,
|
||
};
|
||
}
|
||
|
||
_emit() { if (this.onUpdate) this.onUpdate(this.info()); }
|
||
|
||
/* ── draw ──────────────────────────────────── */
|
||
|
||
draw() {
|
||
const { ctx, W, H } = this;
|
||
if (!W || !H) return;
|
||
ctx.fillStyle = '#0D0D1A';
|
||
ctx.fillRect(0, 0, W, H);
|
||
this._drawGrid(ctx);
|
||
this._drawBgCurves(ctx);
|
||
this._drawActiveCurve(ctx);
|
||
this._drawPoints(ctx);
|
||
this._drawInfoBox(ctx);
|
||
}
|
||
|
||
_drawGrid(ctx) {
|
||
const { ML, MT, MR, MB } = this;
|
||
const pw = this._pw(), ph = this._ph();
|
||
|
||
/* plot background */
|
||
ctx.fillStyle = 'rgba(255,255,255,0.018)';
|
||
ctx.fillRect(ML, MT, pw, ph);
|
||
|
||
/* grid */
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.055)';
|
||
ctx.lineWidth = 1; ctx.setLineDash([]);
|
||
for (let v = 5; v <= 30; v += 5) {
|
||
const x = this._vx(v);
|
||
ctx.beginPath(); ctx.moveTo(x, MT); ctx.lineTo(x, MT + ph); ctx.stroke();
|
||
}
|
||
for (let p = 1; p <= 9; p++) {
|
||
const y = this._py(p);
|
||
ctx.beginPath(); ctx.moveTo(ML, y); ctx.lineTo(ML + pw, y); ctx.stroke();
|
||
}
|
||
|
||
/* axes */
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.beginPath();
|
||
ctx.moveTo(ML, MT); ctx.lineTo(ML, MT + ph); ctx.lineTo(ML + pw, MT + ph);
|
||
ctx.stroke();
|
||
|
||
/* tick labels */
|
||
ctx.font = '11px Manrope, system-ui, sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||
ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
|
||
for (let p = 1; p <= 9; p++) {
|
||
const y = this._py(p);
|
||
if (y < MT + 2 || y > MT + ph - 2) continue;
|
||
ctx.fillText(p, ML - 6, y);
|
||
}
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||
for (let v = 5; v <= 30; v += 5) {
|
||
const x = this._vx(v);
|
||
ctx.fillText(v, x, MT + ph + 5);
|
||
}
|
||
|
||
/* axis titles */
|
||
ctx.fillStyle = 'rgba(255,255,255,0.55)';
|
||
ctx.font = '12px Manrope, system-ui, sans-serif';
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||
ctx.fillText('V, л', ML + pw / 2, MT + ph + 32);
|
||
ctx.save();
|
||
ctx.translate(13, MT + ph / 2);
|
||
ctx.rotate(-Math.PI / 2);
|
||
ctx.fillText('P, атм', 0, 0);
|
||
ctx.restore();
|
||
}
|
||
|
||
_COLORS = {
|
||
isothermal: '#EF476F',
|
||
isochoric: '#06D6E0',
|
||
isobaric: '#7BF5A4',
|
||
adiabatic: '#FFD166',
|
||
};
|
||
|
||
/* draw one process curve through (P1,V1) */
|
||
_curve(ctx, process, alpha, lw, dashed) {
|
||
const { P1, V1, gamma, Vmin, Vmax, Pmin, Pmax } = this;
|
||
ctx.save();
|
||
ctx.globalAlpha = alpha;
|
||
ctx.strokeStyle = this._COLORS[process];
|
||
ctx.lineWidth = lw;
|
||
ctx.setLineDash(dashed ? [5, 4] : []);
|
||
ctx.beginPath();
|
||
|
||
if (process === 'isochoric') {
|
||
const x = this._vx(V1);
|
||
ctx.moveTo(x, this._py(Pmax));
|
||
ctx.lineTo(x, this._py(Pmin));
|
||
} else {
|
||
let started = false;
|
||
const steps = 300;
|
||
for (let i = 0; i <= steps; i++) {
|
||
const v = Vmin + (Vmax - Vmin) * i / steps;
|
||
let p;
|
||
if (process === 'isothermal') p = P1 * V1 / v;
|
||
else if (process === 'isobaric') p = P1;
|
||
else p = P1 * Math.pow(V1 / v, gamma); // adiabatic
|
||
if (p < Pmin || p > Pmax + 0.1) { started = false; continue; }
|
||
const x = this._vx(v), y = this._py(Math.min(p, Pmax));
|
||
if (!started) { ctx.moveTo(x, y); started = true; }
|
||
else ctx.lineTo(x, y);
|
||
}
|
||
}
|
||
ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
ctx.restore();
|
||
}
|
||
|
||
_drawBgCurves(ctx) {
|
||
for (const p of ['isothermal', 'isochoric', 'isobaric', 'adiabatic']) {
|
||
if (p !== this.process) this._curve(ctx, p, 0.14, 1.2, true);
|
||
}
|
||
/* legend dots */
|
||
const names = { isothermal: 'Изотерма', isochoric: 'Изохора', isobaric: 'Изобара', adiabatic: 'Адиабата' };
|
||
ctx.font = '10px Manrope, system-ui, sans-serif';
|
||
let lx = this.ML + this._pw() - 8, ly = this.MT + 8;
|
||
ctx.textAlign = 'right'; ctx.textBaseline = 'top';
|
||
for (const [proc, label] of Object.entries(names)) {
|
||
const col = this._COLORS[proc];
|
||
const isCur = proc === this.process;
|
||
ctx.globalAlpha = isCur ? 0.85 : 0.3;
|
||
ctx.fillStyle = col;
|
||
ctx.beginPath(); ctx.arc(lx + 5, ly + 4, 4, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillText(label, lx - 3, ly);
|
||
ly += 16;
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
|
||
_drawActiveCurve(ctx) {
|
||
/* full curve dimmed */
|
||
this._curve(ctx, this.process, 0.3, 1.5, false);
|
||
|
||
/* highlighted segment state1 → state2 */
|
||
const { P1, V1, gamma } = this;
|
||
const { P2, V2 } = this._state2();
|
||
const color = this._COLORS[this.process];
|
||
|
||
ctx.save();
|
||
ctx.strokeStyle = color;
|
||
ctx.lineWidth = 2.8;
|
||
ctx.setLineDash([]);
|
||
|
||
const steps = 200;
|
||
const [Vs, Ve] = V2 >= V1 ? [V1, V2] : [V2, V1];
|
||
|
||
if (this.process === 'isochoric') {
|
||
const x = this._vx(V1);
|
||
const y1c = this._py(P1), y2c = this._py(P2);
|
||
ctx.beginPath(); ctx.moveTo(x, y1c); ctx.lineTo(x, y2c); ctx.stroke();
|
||
this._arrowHead(ctx, x, y1c, x, y2c, color);
|
||
} else {
|
||
ctx.beginPath();
|
||
let started = false;
|
||
for (let i = 0; i <= steps; i++) {
|
||
const v = Vs + (Ve - Vs) * i / steps;
|
||
let p;
|
||
if (this.process === 'isothermal') p = P1 * V1 / v;
|
||
else if (this.process === 'isobaric') p = P1;
|
||
else p = P1 * Math.pow(V1 / v, gamma);
|
||
const x = this._vx(v), y = this._py(p);
|
||
if (!started) { ctx.moveTo(x, y); started = true; }
|
||
else ctx.lineTo(x, y);
|
||
}
|
||
ctx.stroke();
|
||
|
||
/* arrow at ~80% of segment */
|
||
const vArr = Vs + (Ve - Vs) * 0.8;
|
||
const vArr2 = Vs + (Ve - Vs) * 0.82;
|
||
let p1a, p2a;
|
||
if (this.process === 'isothermal') { p1a = P1*V1/vArr; p2a = P1*V1/vArr2; }
|
||
else if (this.process === 'isobaric') { p1a = P1; p2a = P1; }
|
||
else { p1a = P1*Math.pow(V1/vArr,gamma); p2a = P1*Math.pow(V1/vArr2,gamma); }
|
||
/* ensure arrow points from 1→2 */
|
||
const dir = V2 > V1 ? 1 : -1;
|
||
this._arrowHead(ctx,
|
||
this._vx(vArr + dir*0), this._py(p1a + dir*0),
|
||
this._vx(vArr2 + dir*0), this._py(p2a + dir*0), color);
|
||
}
|
||
ctx.restore();
|
||
}
|
||
|
||
_arrowHead(ctx, x1, y1, x2, y2, color) {
|
||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||
const s = 10;
|
||
ctx.fillStyle = color;
|
||
ctx.beginPath();
|
||
ctx.moveTo(x2, y2);
|
||
ctx.lineTo(x2 - s * Math.cos(angle - 0.4), y2 - s * Math.sin(angle - 0.4));
|
||
ctx.lineTo(x2 - s * Math.cos(angle + 0.4), y2 - s * Math.sin(angle + 0.4));
|
||
ctx.closePath(); ctx.fill();
|
||
}
|
||
|
||
_drawPoints(ctx) {
|
||
const { P2, V2 } = this._state2();
|
||
const color = this._COLORS[this.process];
|
||
|
||
const dot = (x, y, fill, label, textX, textY) => {
|
||
ctx.fillStyle = fill;
|
||
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.55)'; ctx.lineWidth = 1.5;
|
||
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.font = 'bold 11px Manrope, system-ui, sans-serif';
|
||
ctx.fillStyle = fill; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||
ctx.fillText(label, textX, textY);
|
||
};
|
||
|
||
const x1 = this._vx(this.V1), y1 = this._py(this.P1);
|
||
const x2 = this._vx(V2), y2 = this._py(P2);
|
||
dot(x1, y1, '#9B5DE5', '1', x1 - 12, y1 - 4);
|
||
dot(x2, y2, color, '2', x2 + 12, y2 - 4);
|
||
}
|
||
|
||
_drawInfoBox(ctx) {
|
||
const info = this.info();
|
||
const color = this._COLORS[info.process];
|
||
const names = { isothermal:'Изотермический', isochoric:'Изохорный', isobaric:'Изобарный', adiabatic:'Адиабатический' };
|
||
const formulas = { isothermal:'PV = const', isochoric:'V = const', isobaric:'P = const', adiabatic:'PV^γ = const' };
|
||
|
||
const bx = this.ML + 6, by = this.MT + 6;
|
||
const boxW = 205, boxH = 98;
|
||
|
||
ctx.fillStyle = 'rgba(13,13,26,0.9)';
|
||
ctx.beginPath(); ctx.roundRect(bx, by, boxW, boxH, 8); ctx.fill();
|
||
|
||
ctx.font = 'bold 11px Manrope, system-ui, sans-serif';
|
||
ctx.fillStyle = color; ctx.textAlign = 'left'; ctx.textBaseline = 'top';
|
||
ctx.fillText(`${names[info.process]} ${formulas[info.process]}`, bx + 10, by + 8);
|
||
|
||
ctx.font = '11px Manrope, system-ui, sans-serif';
|
||
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
||
ctx.fillText(`T₁ = ${info.T1} K → T₂ = ${info.T2} K`, bx + 10, by + 28);
|
||
|
||
const wColor = info.W_raw > 0 ? '#7BF5A4' : info.W_raw < 0 ? '#EF476F' : 'rgba(255,255,255,0.4)';
|
||
const qColor = info.Q_raw > 0 ? '#FFD166' : info.Q_raw < 0 ? '#06D6E0' : 'rgba(255,255,255,0.4)';
|
||
|
||
ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText('W =', bx + 10, by + 48);
|
||
ctx.fillStyle = wColor; ctx.fillText(`${info.W} Дж`, bx + 38, by + 48);
|
||
|
||
ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText('Q =', bx + 10, by + 65);
|
||
ctx.fillStyle = qColor; ctx.fillText(`${info.Q} Дж`, bx + 38, by + 65);
|
||
|
||
ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.fillText('ΔU =', bx + 10, by + 82);
|
||
ctx.fillStyle = 'rgba(255,255,255,0.65)'; ctx.fillText(`${info.dU} Дж`, bx + 40, by + 82);
|
||
}
|
||
|
||
/* ── events ─────────────────────────────────── */
|
||
|
||
_bindEvents() {
|
||
const cv = this.canvas;
|
||
|
||
const pos = e => {
|
||
const r = cv.getBoundingClientRect();
|
||
const t = e.touches ? e.touches[0] : e;
|
||
return {
|
||
px: (t.clientX - r.left) * (this.W / r.width),
|
||
py: (t.clientY - r.top) * (this.H / r.height),
|
||
};
|
||
};
|
||
|
||
const hit = (px, py) => {
|
||
const x1 = this._vx(this.V1), y1 = this._py(this.P1);
|
||
if (Math.hypot(px - x1, py - y1) < 18) return 'state1';
|
||
const { P2, V2 } = this._state2();
|
||
const x2 = this._vx(V2), y2 = this._py(P2);
|
||
if (Math.hypot(px - x2, py - y2) < 18) return 'state2';
|
||
return null;
|
||
};
|
||
|
||
const clampV = v => Math.max(this.Vmin + 0.5, Math.min(this.Vmax - 0.5, v));
|
||
const clampP = p => Math.max(this.Pmin + 0.1, Math.min(this.Pmax - 0.1, p));
|
||
|
||
const onDown = e => { const { px, py } = pos(e); this._drag = hit(px, py); };
|
||
|
||
const onMove = e => {
|
||
if (!this._drag) return;
|
||
if (e.cancelable) e.preventDefault();
|
||
const { px, py } = pos(e);
|
||
const v = this._xv(px), p = this._yp(py);
|
||
|
||
if (this._drag === 'state1') {
|
||
this.V1 = clampV(v); this.P1 = clampP(p);
|
||
} else {
|
||
/* constrain state2 to current process curve */
|
||
switch (this.process) {
|
||
case 'isothermal': case 'isobaric': case 'adiabatic': {
|
||
const V2 = clampV(v);
|
||
this._ratio = Math.max(0.01, Math.min(0.99, (V2 / this.V1 - 0.2) / 3.3));
|
||
break;
|
||
}
|
||
case 'isochoric': {
|
||
const P2 = clampP(p);
|
||
this._ratio = Math.max(0.01, Math.min(0.99, (P2 / this.P1 - 0.2) / 3.3));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
this.draw(); this._emit();
|
||
};
|
||
|
||
const onUp = () => { this._drag = null; };
|
||
|
||
cv.addEventListener('mousedown', onDown);
|
||
window.addEventListener('mousemove', onMove);
|
||
window.addEventListener('mouseup', onUp);
|
||
cv.addEventListener('touchstart', e => { if (e.touches.length === 1) onDown(e); }, { passive: true });
|
||
cv.addEventListener('touchmove', e => onMove(e), { passive: false });
|
||
cv.addEventListener('touchend', onUp);
|
||
cv.addEventListener('mousemove', e => {
|
||
if (this._drag) { cv.style.cursor = 'grabbing'; return; }
|
||
const { px, py } = pos(e);
|
||
cv.style.cursor = hit(px, py) ? 'grab' : 'default';
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ─── lab UI init ─────────────────────────────────── */
|
||
var isoSim = null;
|
||
|
||
function _openIsoprocess() {
|
||
document.getElementById('sim-topbar-title').textContent = 'Изопроцессы';
|
||
_simShow('sim-isoprocess');
|
||
_registerSimState('isoprocess', () => isoSim?.getParams(),
|
||
st => { if (isoSim) { isoSim.setParams(st); if (st.process) isoSim.setProcess(st.process); } });
|
||
if (_embedMode) _startStateEmit('isoprocess');
|
||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||
if (!isoSim) {
|
||
isoSim = new IsoprocessSim(document.getElementById('isoprocess-canvas'));
|
||
isoSim.onUpdate = _isoUpdateUI;
|
||
isoSim.setGamma(1.667);
|
||
}
|
||
isoSim.fit();
|
||
isoSim.draw();
|
||
isoSim._emit();
|
||
}));
|
||
}
|
||
|
||
function isoProc(proc, el) {
|
||
document.querySelectorAll('.iso-proc-btn').forEach(b => b.classList.remove('active'));
|
||
if (el) el.classList.add('active');
|
||
if (isoSim) isoSim.setProcess(proc);
|
||
}
|
||
|
||
function isoGamma(g, el) {
|
||
document.querySelectorAll('.iso-gamma-btn').forEach(b => b.classList.remove('active'));
|
||
if (el) el.classList.add('active');
|
||
if (isoSim) isoSim.setGamma(g);
|
||
}
|
||
|
||
function isoParam(name, val) {
|
||
const v = parseFloat(val);
|
||
if (name === 'P1') { document.getElementById('iso-p1-val').textContent = v.toFixed(1); if (isoSim) isoSim.setParams({ P1: v }); }
|
||
if (name === 'V1') { document.getElementById('iso-v1-val').textContent = v; if (isoSim) isoSim.setParams({ V1: v }); }
|
||
}
|
||
|
||
function isoRatio(val) { if (isoSim) isoSim.setRatio(parseFloat(val)); }
|
||
|
||
function isoPreset(name) {
|
||
const P = {
|
||
iso_expand: { proc:'isothermal', P1:4, V1:8, ratio:0.75, gamma:1.4 },
|
||
iso_comp: { proc:'isothermal', P1:1.5, V1:20, ratio:0.25, gamma:1.4 },
|
||
heat_iso: { proc:'isochoric', P1:2, V1:10, ratio:0.72, gamma:1.667 },
|
||
adiab_exp: { proc:'adiabatic', P1:5, V1:6, ratio:0.7, gamma:1.667 },
|
||
};
|
||
const p = P[name]; if (!p) return;
|
||
document.querySelectorAll('.iso-proc-btn').forEach(b => b.classList.remove('active'));
|
||
const pb = document.getElementById(`iproc-${p.proc}`); if (pb) pb.classList.add('active');
|
||
document.querySelectorAll('.iso-gamma-btn').forEach(b => b.classList.remove('active'));
|
||
const gb = document.getElementById(p.gamma === 1.4 ? 'igamma-14' : 'igamma-167'); if (gb) gb.classList.add('active');
|
||
document.getElementById('sl-iso-p1').value = p.P1; document.getElementById('iso-p1-val').textContent = p.P1.toFixed(1);
|
||
document.getElementById('sl-iso-v1').value = p.V1; document.getElementById('iso-v1-val').textContent = p.V1;
|
||
document.getElementById('sl-iso-ratio').value = p.ratio;
|
||
if (isoSim) { isoSim.setGamma(p.gamma); isoSim.setProcess(p.proc); isoSim.setParams({ P1: p.P1, V1: p.V1 }); isoSim.setRatio(p.ratio); }
|
||
}
|
||
|
||
function _isoUpdateUI(info) {
|
||
const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
|
||
v('isobar-t1', info.T1);
|
||
v('isobar-t2', info.T2);
|
||
v('isobar-w', info.W);
|
||
v('isobar-q', info.Q);
|
||
v('isobar-du', info.dU);
|
||
}
|
||
|
||
/* ── titration ── */
|
||
|