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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ReactionSim — Chemical reaction kinetics simulation.
|
||||
@@ -616,3 +616,272 @@ class ReactionSim {
|
||||
ctx.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── lab UI init ─────────────────────────────────── */
|
||||
function _openChemistry(mode) {
|
||||
document.getElementById('sim-topbar-title').textContent = 'Химические реакции';
|
||||
_simShow('sim-chemistry');
|
||||
_simShow('ctrl-chemistry');
|
||||
if (mode) _chemMode = mode;
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
chemMode(_chemMode);
|
||||
}));
|
||||
}
|
||||
|
||||
function chemMode(mode, btn) {
|
||||
_chemMode = mode;
|
||||
const MODES = ['kinetics', 'flask', 'redox', 'ionex'];
|
||||
const CANVASES = { kinetics: 'reactions-canvas', flask: 'flask-canvas', redox: 'redox-canvas', ionex: 'ionexchange-canvas' };
|
||||
|
||||
// toggle mode buttons
|
||||
document.querySelectorAll('.chem-mode').forEach(b => b.classList.remove('active'));
|
||||
const mb = document.getElementById('chem-mode-' + mode);
|
||||
if (mb) mb.classList.add('active');
|
||||
|
||||
// toggle panels
|
||||
MODES.forEach(m => {
|
||||
const p = document.getElementById('chem-panel-' + m);
|
||||
if (p) p.style.display = m === mode ? '' : 'none';
|
||||
});
|
||||
|
||||
// toggle canvases
|
||||
Object.entries(CANVASES).forEach(([m, cid]) => {
|
||||
document.getElementById(cid).style.display = m === mode ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// toggle topbar tool groups
|
||||
const modeToCtrl = { kinetics:'kin', flask:'flask', redox:'redox', ionex:'ionex' };
|
||||
['kin', 'flask', 'redox', 'ionex'].forEach(k => {
|
||||
const el = document.getElementById('ctrl-chem-' + k);
|
||||
if (el) el.style.display = k === modeToCtrl[mode] ? 'contents' : 'none';
|
||||
});
|
||||
|
||||
// stop all sims
|
||||
if (reacSim) reacSim.stop();
|
||||
if (flaskSim) flaskSim.stop();
|
||||
if (rdxSim) rdxSim.stop();
|
||||
if (ioxSim) ioxSim.stop();
|
||||
|
||||
// start the active one
|
||||
if (mode === 'kinetics') {
|
||||
const c = document.getElementById('reactions-canvas');
|
||||
if (!reacSim) { reacSim = new ReactionSim(c); reacSim.onUpdate = _reacUpdateUI; }
|
||||
reacSim.fit(); reacSim.start();
|
||||
_reacUpdateUI(reacSim.info());
|
||||
} else if (mode === 'flask') {
|
||||
const c = document.getElementById('flask-canvas');
|
||||
if (!flaskSim) { flaskSim = new FlaskSim(c); flaskSim.onUpdate = _flaskUpdateUI; }
|
||||
flaskSim.fit(); flaskSim.start();
|
||||
_flaskUpdateUI(flaskSim.info());
|
||||
} else if (mode === 'redox') {
|
||||
const c = document.getElementById('redox-canvas');
|
||||
if (!rdxSim) { rdxSim = new RedoxSim(c); rdxSim.onUpdate = _redoxUpdateUI; }
|
||||
rdxSim.fit(); rdxSim.draw();
|
||||
_redoxUpdateUI(rdxSim.info());
|
||||
} else if (mode === 'ionex') {
|
||||
const c = document.getElementById('ionexchange-canvas');
|
||||
if (!ioxSim) { ioxSim = new IonExSim(c); ioxSim.onUpdate = _ionexUpdateUI; }
|
||||
ioxSim.fit(); ioxSim.draw();
|
||||
_ionexUpdateUI(ioxSim.info());
|
||||
}
|
||||
}
|
||||
|
||||
function chemReset() {
|
||||
if (_chemMode === 'kinetics' && reacSim) reacSim.reset();
|
||||
if (_chemMode === 'flask' && flaskSim) flaskSim.reset();
|
||||
if (_chemMode === 'redox') redoxReset();
|
||||
if (_chemMode === 'ionex') ionexReset();
|
||||
}
|
||||
|
||||
// _openReactions is now handled by _openChemistry + chemMode
|
||||
|
||||
function reacNChange() {
|
||||
const v = +document.getElementById('sl-reacN').value;
|
||||
document.getElementById('reac-N-val').textContent = v;
|
||||
if (reacSim) reacSim.setN(v);
|
||||
}
|
||||
|
||||
function reacTChange() {
|
||||
const raw = +document.getElementById('sl-reacT').value;
|
||||
const t = (raw / 10).toFixed(1);
|
||||
document.getElementById('reac-T-val').textContent = t;
|
||||
if (reacSim) reacSim.setT(+t);
|
||||
}
|
||||
|
||||
function reacEaChange() {
|
||||
const raw = +document.getElementById('sl-reacEa').value;
|
||||
const ea = (raw / 10).toFixed(1);
|
||||
document.getElementById('reac-Ea-val').textContent = ea;
|
||||
if (reacSim) reacSim.setEa(+ea);
|
||||
}
|
||||
|
||||
function reacMode(mode, el) {
|
||||
if (reacSim) reacSim.setMode(mode);
|
||||
document.querySelectorAll('.reac-mode-btn').forEach(b => b.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
}
|
||||
|
||||
function reacPreset(name) {
|
||||
if (!reacSim) return;
|
||||
reacSim.preset(name);
|
||||
// Sync sliders and mode buttons
|
||||
document.getElementById('sl-reacN').value = reacSim.N;
|
||||
document.getElementById('reac-N-val').textContent = reacSim.N;
|
||||
document.getElementById('sl-reacT').value = Math.round(reacSim.T * 10);
|
||||
document.getElementById('reac-T-val').textContent = reacSim.T.toFixed(1);
|
||||
document.getElementById('sl-reacEa').value = Math.round(reacSim.Ea * 10);
|
||||
document.getElementById('reac-Ea-val').textContent = reacSim.Ea.toFixed(1);
|
||||
document.querySelectorAll('.reac-mode-btn').forEach(b => b.classList.remove('active'));
|
||||
const mBtn = document.getElementById('rmode-' + reacSim.mode);
|
||||
if (mBtn) mBtn.classList.add('active');
|
||||
_reacUpdateUI(reacSim.info());
|
||||
}
|
||||
|
||||
function reacTogglePause() {
|
||||
if (!reacSim) return;
|
||||
reacSim.toggleReaction();
|
||||
const btn = document.getElementById('reac-pause-btn');
|
||||
btn.innerHTML = reacSim.reactionOn ? '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Реакции';
|
||||
}
|
||||
|
||||
function _reacUpdateUI(info) {
|
||||
if (!info) return;
|
||||
document.getElementById('chbar-l1').textContent = 'A молекул';
|
||||
document.getElementById('chbar-v1').textContent = info.nA;
|
||||
document.getElementById('chbar-l2').textContent = 'B молекул';
|
||||
document.getElementById('chbar-v2').textContent = info.nB;
|
||||
document.getElementById('chbar-l3').textContent = 'C продукт';
|
||||
document.getElementById('chbar-v3').textContent = info.nC;
|
||||
document.getElementById('chbar-l4').textContent = 'Реакций';
|
||||
document.getElementById('chbar-v4').textContent = info.reactions;
|
||||
document.getElementById('chbar-l5').textContent = 'Скорость';
|
||||
document.getElementById('chbar-v5').textContent = info.rate > 0
|
||||
? (info.rate * 30).toFixed(1) + '/с' : '—';
|
||||
}
|
||||
|
||||
// _openFlask is now handled by _openChemistry('flask')
|
||||
|
||||
function flaskMetal(type, el) {
|
||||
if (flaskSim) { flaskSim.setMetal(type); flaskSim.reset(); }
|
||||
document.querySelectorAll('.flask-metal-btn').forEach(b => b.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
}
|
||||
|
||||
function flaskAcid(type, el) {
|
||||
if (flaskSim) flaskSim.setAcid(type);
|
||||
document.querySelectorAll('.flask-acid-btn').forEach(b => b.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
}
|
||||
|
||||
function flaskConcChange() {
|
||||
const v = +document.getElementById('sl-flask-conc').value;
|
||||
document.getElementById('flask-conc-val').textContent = v + '%';
|
||||
if (flaskSim) flaskSim.setConc(v / 100);
|
||||
}
|
||||
|
||||
function flaskTempChange() {
|
||||
const v = +document.getElementById('sl-flask-temp').value;
|
||||
document.getElementById('flask-temp-val').textContent = v + '°C';
|
||||
if (flaskSim) flaskSim.setEnvTemp(v);
|
||||
}
|
||||
|
||||
function flaskToggleFlame() {
|
||||
if (!flaskSim) return;
|
||||
flaskSim.toggleFlame();
|
||||
const active = flaskSim._flameOn;
|
||||
document.getElementById('flask-flame-btn').style.opacity = active ? '1' : '0.5';
|
||||
document.getElementById('flask-flame-panel').style.opacity = active ? '1' : '0.5';
|
||||
document.getElementById('flask-flame-panel').style.background = active ? 'rgba(239,71,111,0.22)' : '';
|
||||
}
|
||||
|
||||
function flaskTogglePause() {
|
||||
if (!flaskSim) return;
|
||||
flaskSim.togglePause();
|
||||
document.getElementById('flask-pause-btn').innerHTML = flaskSim._paused ? '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>' : '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
|
||||
}
|
||||
|
||||
function _flaskUpdateUI(info) {
|
||||
if (!info) return;
|
||||
document.getElementById('chbar-l1').textContent = 'Металл';
|
||||
document.getElementById('chbar-v1').textContent = info.metal;
|
||||
document.getElementById('chbar-l2').textContent = 'Масса';
|
||||
document.getElementById('chbar-v2').textContent = info.mass + ' г';
|
||||
document.getElementById('chbar-l3').textContent = 'T (°C)';
|
||||
document.getElementById('chbar-v3').textContent = info.temp + '°C';
|
||||
document.getElementById('chbar-l4').textContent = 'pH';
|
||||
document.getElementById('chbar-v4').textContent = info.pH;
|
||||
document.getElementById('chbar-l5').textContent = 'H₂ (%)';
|
||||
document.getElementById('chbar-v5').textContent = info.h2pct + '%';
|
||||
}
|
||||
|
||||
// _openRedox is now handled by _openChemistry('redox')
|
||||
|
||||
function redoxRxn(id, el) {
|
||||
document.querySelectorAll('.redox-rxn-btn').forEach(b => b.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
if (rdxSim) { rdxSim.setReaction(id); }
|
||||
}
|
||||
|
||||
function redoxStart() {
|
||||
if (rdxSim) rdxSim.start();
|
||||
}
|
||||
|
||||
function redoxReset() {
|
||||
if (rdxSim) rdxSim.reset();
|
||||
}
|
||||
|
||||
function _redoxUpdateUI(info) {
|
||||
if (!info) return;
|
||||
const phaseMap = { idle: 'ожидание', mixing: 'смешивание', reacting: 'реакция', done: 'завершена' };
|
||||
document.getElementById('chbar-l1').textContent = 'Реакция';
|
||||
document.getElementById('chbar-v1').textContent = info.rxn || '—';
|
||||
document.getElementById('chbar-l2').textContent = 'Фаза';
|
||||
document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
|
||||
document.getElementById('chbar-l3').textContent = 'Прогресс';
|
||||
document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
|
||||
document.getElementById('chbar-l4').textContent = 'Электронов';
|
||||
document.getElementById('chbar-v4').textContent = info.e + ' e⁻';
|
||||
document.getElementById('chbar-l5').textContent = 'Тип';
|
||||
document.getElementById('chbar-v5').innerHTML = info.phase === 'done' ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : '—';
|
||||
}
|
||||
|
||||
// _openIonExchange is now handled by _openChemistry('ionex')
|
||||
|
||||
function ionexRxn(id, el) {
|
||||
document.querySelectorAll('.ionex-rxn-btn').forEach(b => b.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
if (ioxSim) { ioxSim.setReaction(id); }
|
||||
}
|
||||
|
||||
function ionexStart() {
|
||||
if (ioxSim) ioxSim.start();
|
||||
}
|
||||
|
||||
function ionexReset() {
|
||||
if (ioxSim) ioxSim.reset();
|
||||
}
|
||||
|
||||
function _ionexUpdateUI(info) {
|
||||
if (!info) return;
|
||||
const phaseMap = { idle: 'ожидание', mixing: 'смешивание', pairing: 'реакция', done: 'завершена' };
|
||||
const rxn = IonExSim.RXN[ioxSim.rxnId];
|
||||
document.getElementById('chbar-l1').textContent = 'Реакция';
|
||||
document.getElementById('chbar-v1').textContent = info.rxn || '—';
|
||||
document.getElementById('chbar-l2').textContent = 'Фаза';
|
||||
document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
|
||||
document.getElementById('chbar-l3').textContent = 'Прогресс';
|
||||
document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
|
||||
document.getElementById('chbar-l4').textContent = 'Осадок';
|
||||
document.getElementById('chbar-v4').textContent = info.precip > 0 ? info.precip + ' ч.' : '—';
|
||||
document.getElementById('chbar-l5').textContent = 'Продукт';
|
||||
document.getElementById('chbar-v5').textContent = rxn ? (rxn.sign || '—') : '—';
|
||||
}
|
||||
|
||||
/* ════════════════════════════════
|
||||
ЗАКОНЫ НЬЮТОНА
|
||||
════════════════════════════════ */
|
||||
|
||||
/* ══════════════════════════════
|
||||
DYNAMICS (unified Newton + Sandbox)
|
||||
══════════════════════════════ */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user