diff --git a/frontend/js/labs/angrybirds.js b/frontend/js/labs/angrybirds.js index f3005f7..94d1b41 100644 --- a/frontend/js/labs/angrybirds.js +++ b/frontend/js/labs/angrybirds.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════════════════════════ AngryBirdsSim — Angry Birds Physics @@ -853,3 +853,52 @@ class AngryBirdsSim { return `rgb(${r},${g},${b})`; } } + +/* ─── lab UI init ─────────────────────────────────── */ + var angryBirdsSim = null; + + function _openAngryBirds() { + document.getElementById('sim-topbar-title').textContent = 'Angry Birds Physics'; + _simShow('sim-angrybirds'); + _simShow('ctrl-angrybirds'); + requestAnimationFrame(() => requestAnimationFrame(() => { + const c = document.getElementById('angrybirds-canvas'); + if (!angryBirdsSim) { + angryBirdsSim = new AngryBirdsSim(c); + angryBirdsSim.onUpdate = _abUpdateUI; + c.addEventListener('mousedown', e => angryBirdsSim.handleMouseDown(e)); + c.addEventListener('mousemove', e => angryBirdsSim.handleMouseMove(e)); + c.addEventListener('mouseup', e => angryBirdsSim.handleMouseUp(e)); + c.addEventListener('mouseleave', e => angryBirdsSim.handleMouseUp(e)); + _addTouchSupport(c, angryBirdsSim); + } + angryBirdsSim.fit(); + angryBirdsSim.start(); + })); + } + + function abLevel(n, btn) { + document.querySelectorAll('.ab-lvl-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (angryBirdsSim) angryBirdsSim.loadLevel(n); + } + + function angryBirdsRestart() { + if (angryBirdsSim) angryBirdsSim.restart(); + } + + function _abUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('abbar-v1', info.level); + v('abbar-v2', info.birds); + v('abbar-v3', info.pigs); + v('abbar-v4', info.score.toLocaleString('ru')); + v('abbar-v5', info.planet); + /* sync level button highlight */ + document.querySelectorAll('.ab-lvl-btn').forEach((b, i) => { + b.classList.toggle('active', i === (info.level - 1)); + }); + } + + /* ── quadratic ── */ + diff --git a/frontend/js/labs/bohratom.js b/frontend/js/labs/bohratom.js index 9e365ad..b5c78cf 100644 --- a/frontend/js/labs/bohratom.js +++ b/frontend/js/labs/bohratom.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ BohrAtomSim — Bohr atomic model simulation (hydrogen) E_n = −13.6 / n² eV λ = 1240 / ΔE nm @@ -638,3 +638,43 @@ class BohrAtomSim { }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openBohrAtom() { + document.getElementById('sim-topbar-title').textContent = 'Атом Бора'; + _simShow('sim-bohratom'); + _registerSimState('bohratom', () => bohrSim?.getParams(), st => bohrSim?.setParams(st)); + if (_embedMode) _startStateEmit('bohratom'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!bohrSim) { + bohrSim = new BohrAtomSim(document.getElementById('bohratom-canvas')); + bohrSim.onUpdate = _bohrUpdateUI; + } + bohrSim.fit(); + bohrSim.play(); + })); + } + + function bohrLevel(n) { + if (bohrSim) { + const from = bohrSim.info().level; + if (from !== n) bohrSim.transition(from, n); + } + } + + function bohrTransition(from, to) { + if (bohrSim) bohrSim.transition(from, to); + } + + function _bohrUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('bohrbar-v1', info.level); + v('bohrbar-v2', info.energy.toFixed(2)); + if (info.lastTransition) { + v('bohrbar-v3', info.lastTransition.wavelength.toFixed(0)); + v('bohrbar-v4', info.lastTransition.series || '—'); + } + } + + /* ── electrolysis ── */ + diff --git a/frontend/js/labs/celldivision.js b/frontend/js/labs/celldivision.js index 6a76b7a..3df2436 100644 --- a/frontend/js/labs/celldivision.js +++ b/frontend/js/labs/celldivision.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ════════════════════════════════════════════════════════════════ CellDivisionSim v2 — интерактивное деление клетки Митоз и мейоз · анимация · частицы · скрабинг · клик @@ -813,3 +813,80 @@ function _cdRRect(ctx, x, y, w, h, r) { ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openCellDivision(mode) { + document.getElementById('sim-topbar-title').textContent = 'Деление клетки'; + _simShow('sim-celldivision'); + _simShow('ctrl-celldivision'); + requestAnimationFrame(() => requestAnimationFrame(() => { + const canvas = document.getElementById('celldiv-canvas'); + if (!cellDivSim) { + cellDivSim = new CellDivisionSim(canvas); + cellDivSim.onUpdate = _cdUpdateUI; + } + cellDivSim.fit(); + cellDivSim.setMode(mode || 'mitosis'); + cellDivSim.start(); + _cdBuildDots(cellDivSim._phaseIdx); + // sync auto button state + const autoBtn = document.getElementById('cd-auto-btn'); + if (autoBtn) { autoBtn.innerHTML = cellDivSim._autoPlay ? ' Пауза' : ' Авто'; } + _cdUpdateUI(cellDivSim.info()); + })); + } + + function _cdBuildDots(activeIdx) { + const box = document.getElementById('cd-phase-dots'); + if (!box || !cellDivSim) return; + const phases = cellDivSim._phases(); + box.innerHTML = phases.map((p, i) => + `
` + ).join(''); + } + + function cdSetMode(mode, btn) { + document.querySelectorAll('.cd-mode-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (!cellDivSim) return; + cellDivSim.setMode(mode); + _cdBuildDots(cellDivSim._phaseIdx); + _cdUpdateUI(cellDivSim.info()); + } + + function cdAutoPlay(btn) { + if (!cellDivSim) return; + cellDivSim.toggleAutoPlay(); + btn.classList.toggle('active', cellDivSim._autoPlay); + btn.innerHTML = cellDivSim._autoPlay ? ' Пауза' : ' Авто'; + } + + function cdPrevPhase() { + if (!cellDivSim) return; + cellDivSim.prevPhase(); + _cdBuildDots(cellDivSim._phaseIdx); + } + + function cdNextPhase() { + if (!cellDivSim) return; + cellDivSim.nextPhase(); + _cdBuildDots(cellDivSim._phaseIdx); + } + + function cdJumpPhase(idx) { + if (!cellDivSim) return; + cellDivSim.jumpToPhase(idx); + _cdBuildDots(idx); + } + + function _cdUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('cdbar-v1', info.phase || '—'); + v('cdbar-v2', info.chromN || '—'); + v('cdbar-v3', info.dna || '—'); + v('cdbar-v4', (info.index + 1) + ' / ' + info.total); + v('cdbar-v5', info.mode === 'mitosis' ? 'Митоз' : 'Мейоз'); + _cdBuildDots(info.index); + } + + /* ── Photosynthesis / Respiration ── */ diff --git a/frontend/js/labs/chemsandbox.js b/frontend/js/labs/chemsandbox.js index 3e07ef1..bf1eeaa 100644 --- a/frontend/js/labs/chemsandbox.js +++ b/frontend/js/labs/chemsandbox.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* Strip SVG markup for canvas fillText — replaces icon SVGs with Unicode */ function _csClean(s) { @@ -1680,3 +1680,158 @@ class ChemSandboxSim { if (this.onUpdate) this.onUpdate(this.info()); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openChemSandbox() { + document.getElementById('sim-topbar-title').textContent = 'Химическая песочница'; + _simShow('sim-chemsandbox'); + _simShow('ctrl-chemsandbox'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + const c = document.getElementById('chemsandbox-canvas'); + if (!chemSandSim) { + chemSandSim = new ChemSandboxSim(c); + chemSandSim.onUpdate = _chemSandUpdateUI; + chemSandSim.onQuizUpdate = _chemSandQuizUI; + c.addEventListener('click', e => chemSandSim.handleClick(e)); + c.addEventListener('mousedown', e => chemSandSim.handleMouseDown(e)); + c.addEventListener('mousemove', e => chemSandSim.handleMouseMove(e)); + c.addEventListener('mouseup', e => chemSandSim.handleMouseUp(e)); + c.addEventListener('wheel', e => chemSandSim.handleWheel(e), { passive: false }); + c.addEventListener('contextmenu', e => chemSandSim.handleContextMenu(e)); + _addTouchSupport(c, chemSandSim); + _chemSandBuildReagents('all'); + } + chemSandSim.fit(); + chemSandSim.start(); + chemSandSim.draw(); + })); + } + + function chemSandCat(cat, el) { + document.querySelectorAll('.chemsand-cat').forEach(b => b.classList.remove('active')); + el.classList.add('active'); + if (chemSandSim) chemSandSim.setCategory(cat); + _chemSandBuildReagents(cat); + if (chemSandSim) chemSandSim.draw(); + } + + function chemSandPreset(name) { if (chemSandSim) { chemSandSim.preset(name); _chemSandBuildReagents(chemSandSim.filterCat); } } + function chemSandReset() { if (chemSandSim) { chemSandSim.reset(); _chemSandBuildReagents(chemSandSim.filterCat); } } + function chemSandResetReaction() { if (chemSandSim) { chemSandSim.resetReaction(); _chemSandBuildReagents(chemSandSim.filterCat); } } + + function chemSandConcChange() { + const v = +document.getElementById('sl-csand-conc').value; + document.getElementById('csand-conc-val').textContent = v + '%'; + } + function chemSandTempChange() { + const v = +document.getElementById('sl-csand-temp').value; + document.getElementById('csand-temp-val').textContent = v + '°C'; + } + + function chemSandAdd(formula) { + if (!chemSandSim) return; + // toggle: if already in mix — remove, else add + if (chemSandSim.mixContents.includes(formula)) { + chemSandSim.removeFromMix(formula); + } else { + chemSandSim.addToMix(formula); + } + _chemSandBuildReagents(chemSandSim.filterCat); + } + + function _chemSandBuildReagents(cat) { + const box = document.getElementById('chemsand-reagents'); + if (!box) return; + const subs = ChemSandboxSim.SUBSTANCES; + const keys = Object.keys(subs).filter(k => cat === 'all' || subs[k].cat === cat); + const inMix = chemSandSim ? chemSandSim.mixContents : []; + box.innerHTML = keys.map(k => { + const s = subs[k]; + const active = inMix.includes(k); + const cls = active ? 'proj-preset-chip reac-mode-btn active' : 'proj-preset-chip reac-mode-btn'; + const sf = chemSandSim ? chemSandSim._shortFormula(k) : k; + const removeHint = active ? ' (клик — убрать)' : ''; + return ``; + }).join(''); + } + + function chemSandSetMode(mode, el) { + document.querySelectorAll('.chemsand-mode').forEach(b => b.classList.remove('active')); + if (el) el.classList.add('active'); + if (!chemSandSim) return; + if (mode === 'quiz') { + if (window._simQuizAllowed === false) { + LS.toast('Режим заданий недоступен — администратор ограничил доступ', 'error'); + // revert button state + document.querySelectorAll('.chemsand-mode').forEach(b => b.classList.remove('active')); + document.getElementById('csand-mode-free')?.classList.add('active'); + return; + } + chemSandSim.startQuiz(); + // reset category filter to 'all' so all reagents are accessible + document.querySelectorAll('.chemsand-cat').forEach(b => b.classList.remove('active')); + const allBtn = document.querySelector('.chemsand-cat'); + if (allBtn) allBtn.classList.add('active'); + _chemSandBuildReagents('all'); + } else { + chemSandSim.stopQuiz(); + document.getElementById('csand-quiz-question').style.display = 'none'; + document.getElementById('csand-quiz-result').style.display = 'none'; + document.getElementById('csand-quiz-next').style.display = 'none'; + document.getElementById('csand-quiz-score').textContent = ''; + } + } + + function chemSandQuizNext() { + if (chemSandSim && chemSandSim._quizMode) { + chemSandSim._nextQuizTask(); + _chemSandBuildReagents(chemSandSim.filterCat); + } + } + + function _chemSandQuizUI(qi) { + const qEl = document.getElementById('csand-quiz-question'); + const rEl = document.getElementById('csand-quiz-result'); + const nEl = document.getElementById('csand-quiz-next'); + const sEl = document.getElementById('csand-quiz-score'); + if (!qi.active) { + qEl.style.display = 'none'; rEl.style.display = 'none'; nEl.style.display = 'none'; + sEl.textContent = ''; + return; + } + qEl.style.display = 'block'; + qEl.textContent = qi.question || ''; + sEl.textContent = qi.total > 0 ? `${qi.score}/${qi.total}` : ''; + if (qi.result) { + rEl.style.display = 'block'; + rEl.style.color = qi.result === 'correct' ? '#7BF5A4' : '#EF476F'; + rEl.textContent = qi.result === 'correct' ? 'Верно!' : 'Неверно — ' + (qi.answer || ''); + nEl.style.display = qi.result === 'wrong' ? 'inline-block' : 'none'; + } else { + rEl.style.display = 'none'; nEl.style.display = 'none'; + } + } + + let _lastReportedEquation = null; + function _chemSandUpdateUI(info) { + document.getElementById('csbar-v1').textContent = info.mixed; + document.getElementById('csbar-v3').textContent = info.type || '—'; + const eqEl = document.getElementById('csbar-v4'); + eqEl.innerHTML = info.equation || '—'; + eqEl.title = (info.equation || '').replace(/<[^>]*>/g, ''); + document.getElementById('csbar-v5').textContent = info.products || '—'; + const ionEl = document.getElementById('csbar-v6'); + ionEl.innerHTML = info.ionNet || '—'; + ionEl.title = (info.ionNet || '').replace(/<[^>]*>/g, ''); + // rebuild reagent buttons to reflect active state + _chemSandBuildReagents(chemSandSim ? chemSandSim.filterCat : 'all'); + // Report lab activity for gamification (once per unique reaction) + if (info.reaction && info.equation && info.equation !== _lastReportedEquation) { + _lastReportedEquation = info.equation; + if (window.LS?.reportLabActivity) LS.reportLabActivity(1).catch(() => {}); + } + } + + /* ── Cell Division ── */ diff --git a/frontend/js/labs/circuit.js b/frontend/js/labs/circuit.js index f0dac00..dfbc359 100644 --- a/frontend/js/labs/circuit.js +++ b/frontend/js/labs/circuit.js @@ -1,4 +1,4 @@ -/** +/** * CircuitSim — Enhanced Electric Circuits Simulation v2 * MNA solver · L-shape wires · Drag · Undo/Redo · Tooltip * New: Capacitor · Diode · LED · AC source · Junction dots @@ -1306,3 +1306,88 @@ class CircuitSim { if (this.onUpdate) this.onUpdate(this.info()); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var cirSim = null; + var reacSim = null; + var flaskSim = null; + + function _openCircuit() { + document.getElementById('sim-topbar-title').textContent = 'Электрические цепи'; + _simShow('sim-circuit'); + _simShow('ctrl-circuit'); + requestAnimationFrame(() => requestAnimationFrame(() => { + const canvas = document.getElementById('circuit-canvas'); + if (!cirSim) { + cirSim = new CircuitSim(canvas); + cirSim.onUpdate = _circUpdateUI; + cirSim.onModeChange = (mode) => { + document.querySelectorAll('.circ-tool-btn').forEach(b => { + b.classList.toggle('active', b.dataset.tool === mode); + }); + document.querySelectorAll('.circ-top-btn').forEach(b => { + b.classList.toggle('active', b.id === 'ctool-' + mode); + }); + }; + } else { + cirSim.stop(); + } + cirSim.fit(); + if (cirSim.components.length === 0) cirSim.preset('serial'); + cirSim.start(); + _circUpdateUI(cirSim.info()); + })); + } + + function circTool(tool, el) { + if (cirSim) cirSim.addMode = tool; + document.querySelectorAll('.circ-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === tool)); + document.querySelectorAll('.circ-top-btn').forEach(b => b.classList.toggle('active', b.id === 'ctool-' + tool)); + } + + function circPreset(name) { + if (!cirSim) return; + cirSim.preset(name); + } + + function circRChange() { + const v = +document.getElementById('sl-circR').value; + document.getElementById('circ-R-val').textContent = v + ' Ω'; + if (cirSim) cirSim.R_value = v; + } + + function circUChange() { + const v = +document.getElementById('sl-circU').value; + document.getElementById('circ-U-val').textContent = v + ' В'; + if (cirSim) cirSim.U_value = v; + } + + function circCChange() { + const v = +document.getElementById('sl-circC').value; + document.getElementById('circ-C-val').textContent = v + ' µF'; + if (cirSim) cirSim.C_value = v; + } + + function circFChange() { + const v = +document.getElementById('sl-circF').value; + document.getElementById('circ-F-val').textContent = v + ' Гц'; + if (cirSim) cirSim.acFreq = v; + } + + function _circUpdateUI(info) { + if (!info) return; + document.getElementById('cirbar-comps').textContent = info.components; + document.getElementById('cirbar-U').textContent = info.voltage ? info.voltage + ' В' : '—'; + document.getElementById('cirbar-I').textContent = info.current ? info.current + ' А' : '—'; + document.getElementById('cirbar-P').textContent = info.power ? info.power + ' Вт' : '—'; + const st = document.getElementById('cirbar-status'); + st.textContent = info.solved ? 'Замкнута' : 'Разомкнута'; + st.style.color = info.solved ? '#7BF5A4' : '#EF476F'; + } + + /* ════════════════════════════════ + ХИМИЯ (unified: кинетика + колба + ОВР + ионный обмен) + ════════════════════════════════ */ + + let _chemMode = 'kinetics'; // 'kinetics' | 'flask' | 'redox' | 'ionex' + diff --git a/frontend/js/labs/collision.js b/frontend/js/labs/collision.js index d49bda6..df07e31 100644 --- a/frontend/js/labs/collision.js +++ b/frontend/js/labs/collision.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════ CollisionSim — 2D elastic/inelastic ball collision @@ -1008,3 +1008,123 @@ function _roundRect(ctx, x, y, w, h, r) { ctx.lineTo(x, y + r); ctx.arcTo(x, y, x+r, y, r); ctx.closePath(); } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openCollision() { + document.getElementById('sim-topbar-title').textContent = 'Столкновение шаров'; + _simShow('sim-coll'); + _simShow('ctrl-coll'); + _registerSimState('collision', () => cSim?.getParams(), st => cSim?.setParams(st)); + if (_embedMode) _startStateEmit('collision'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!cSim) { + cSim = new CollisionSim(document.getElementById('coll-canvas')); + cSim.onUpdate = _collUpdateUI; + cSim.onPlayPause = collPlayPause; + } + cSim.fit(); + cSim.setSpeed(+document.getElementById('sl-speed').value); + collParam(); + cSim.draw(); + _collUpdateUI(cSim.stats()); + })); + } + + function collPlayPause() { + if (!cSim) return; + if (cSim.playing) { cSim.pause(); } else { cSim.play(); } + _collSyncBtn(); + } + + function _collSyncBtn() { + const tb = document.getElementById('coll-play-btn'); + const lb = document.getElementById('coll-launch-main'); + const lbl = document.getElementById('coll-launch-label'); + const lic = document.getElementById('coll-launch-icon'); + if (!cSim) return; + const playing = cSim.playing; + + if (tb) { + tb.innerHTML = playing + ? '' + : ''; + tb.title = playing ? 'Пауза' : 'Запустить'; + tb.classList.toggle('active', playing); + } + + if (lb && lbl && lic) { + lb.classList.toggle('paused', playing); + lb.classList.remove('done'); + if (playing) { + lic.innerHTML = ''; + lbl.textContent = 'Пауза'; + } else { + lic.innerHTML = ''; + lbl.textContent = 'Запустить'; + } + } + } + + function collParam() { + const m1 = +document.getElementById('sl-m1').value; + const m2 = +document.getElementById('sl-m2').value; + const v1 = +document.getElementById('sl-cv1').value; + const v2 = +document.getElementById('sl-cv2').value; + const angle = +document.getElementById('sl-cangle').value; + const e = +document.getElementById('sl-e').value; + const spd = +document.getElementById('sl-speed').value; + + document.getElementById('c-m1').textContent = m1 + ' кг'; + document.getElementById('c-m2').textContent = m2 + ' кг'; + document.getElementById('c-v1').textContent = v1 + ' м/с'; + document.getElementById('c-v2').textContent = v2 + ' м/с'; + document.getElementById('c-angle').textContent = angle + '°'; + document.getElementById('c-e').textContent = e.toFixed(2); + document.getElementById('c-speed').textContent = spd.toFixed(2) + '×'; + + if (cSim) { + /* speed change doesn't require a reset */ + const speedChanged = Math.abs(cSim.speed - spd) > 0.001; + if (speedChanged) cSim.setSpeed(spd); + + const physChanged = cSim.m1 !== m1 || cSim.m2 !== m2 || + cSim.v1 !== v1 || cSim.v2 !== v2 || + cSim.angle !== angle || cSim.e !== e; + if (physChanged) cSim.setParams({ m1, m2, v1, v2, angle, e }); + _collSyncBtn(); + } + } + + function collPreset(m1, m2, v1, v2, angle, e) { + document.getElementById('sl-m1').value = m1; + document.getElementById('sl-m2').value = m2; + document.getElementById('sl-cv1').value = v1; + document.getElementById('sl-cv2').value = v2; + document.getElementById('sl-cangle').value = angle; + document.getElementById('sl-e').value = e; + collParam(); + } + + function _collUpdateUI(s) { + // before/after are arrays [{m, vx, vy, ke}, ...] + function snapKE(arr) { return arr ? arr.reduce((t, b) => t + b.ke, 0) : null; } + function snapP(arr) { + if (!arr) return null; + return Math.hypot(arr.reduce((t, b) => t + b.m * b.vx, 0), + arr.reduce((t, b) => t + b.m * b.vy, 0)); + } + const bKE = snapKE(s.before), bP = snapP(s.before); + const aKE = snapKE(s.after), aP = snapP(s.after); + const f2 = v => v !== null ? v.toFixed(2) : '—'; + + document.getElementById('cs-pbefore').textContent = bP !== null ? f2(bP) + ' кг·м/с' : '—'; + document.getElementById('cs-pafter').textContent = aP !== null ? f2(aP) + ' кг·м/с' : '—'; + document.getElementById('cs-kebefore').textContent = bKE !== null ? f2(bKE) + ' Дж' : '—'; + document.getElementById('cs-keafter').textContent = aKE !== null ? f2(aKE) + ' Дж' : '—'; + document.getElementById('cs-count').textContent = s.colCount; + _collSyncBtn(); + } + + /* ── magnetic ── */ + diff --git a/frontend/js/labs/coulomb.js b/frontend/js/labs/coulomb.js index 53c1367..82c9452 100644 --- a/frontend/js/labs/coulomb.js +++ b/frontend/js/labs/coulomb.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════ CoulombSim — Coulomb's Law interactive simulation • Click canvas to place charge (+ or −) @@ -746,3 +746,67 @@ class CoulombSim { }, { passive: false }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var csSim = null; + + function _openCoulomb() { + document.getElementById('sim-topbar-title').textContent = 'Закон Кулона'; + _simShow('sim-coulomb'); + _simShow('ctrl-coulomb'); + requestAnimationFrame(() => requestAnimationFrame(() => { + const canvas = document.getElementById('coulomb-canvas'); + if (!csSim) { + csSim = new CoulombSim(canvas); + csSim.onUpdate = _coulombUpdateUI; + } + csSim.fit(); + if (csSim.charges.length === 0) csSim.preset('dipole'); + _coulombUpdateUI(csSim.info()); + })); + } + + function coulombSign(s) { + if (!csSim) return; + csSim.setSign(s); + document.getElementById('cbtn-pos').classList.toggle('active', s > 0); + document.getElementById('cbtn-neg').classList.toggle('active', s < 0); + document.getElementById('csign-pos').style.opacity = s > 0 ? '1' : '0.45'; + document.getElementById('csign-neg').style.opacity = s < 0 ? '1' : '0.45'; + } + + function coulombLayer(name, rowEl) { + if (!csSim) return; + csSim.toggleLayer(name); + const on = csSim.layers[name]; + rowEl.classList.toggle('active', on); + const tog = rowEl.querySelector('.tri-toggle'); + if (tog) { + tog.style.background = on ? 'var(--violet)' : 'rgba(255,255,255,0.12)'; + const dot = tog.querySelector('span'); + if (dot) dot.style.marginLeft = on ? '14px' : '2px'; + } + csSim.draw(); + } + + function coulombPreset(name) { + if (!csSim) return; + csSim.preset(name); + } + + function _coulombUpdateUI(info) { + if (!info) return; + document.getElementById('cs-total').textContent = info.total; + document.getElementById('cs-curE').textContent = info.cursorE; + document.getElementById('cs-curV').textContent = info.cursorV; + document.getElementById('csbar-total').textContent = info.total; + document.getElementById('csbar-pos').textContent = info.positive; + document.getElementById('csbar-neg').textContent = info.negative; + document.getElementById('csbar-maxE').textContent = info.maxE; + document.getElementById('csbar-curE').textContent = info.cursorE; + } + + /* ════════════════════════════════ + ЭЛЕКТРИЧЕСКИЕ ЦЕПИ + ════════════════════════════════ */ + diff --git a/frontend/js/labs/crystal.js b/frontend/js/labs/crystal.js index 93d91cf..1cc8cf7 100644 --- a/frontend/js/labs/crystal.js +++ b/frontend/js/labs/crystal.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════ CrystalSim — 3D crystal lattice (Three.js) @@ -313,3 +313,26 @@ class CrystalSim { this.renderer.render(this.scene, this.camera); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var crystalSim = null; + function _openCrystal() { + document.getElementById('sim-topbar-title').textContent = 'Кристаллическая решётка'; + _simShow('sim-crystal'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!crystalSim) { + crystalSim = new CrystalSim(document.getElementById('crystal-container')); + } else { + crystalSim.fit(); + crystalSim.play(); + } + })); + } + function setCrystal(type, btn) { + document.querySelectorAll('.crystal-type-btn').forEach(b => { b.classList.remove('active'); b.style.borderColor = ''; b.style.color = ''; }); + btn.classList.add('active'); + btn.style.borderColor = '#9B5DE5'; btn.style.color = '#9B5DE5'; + if (crystalSim) crystalSim.setLattice(type); + } + + /* ── molecular orbitals (3D) ── */ diff --git a/frontend/js/labs/electrolysis.js b/frontend/js/labs/electrolysis.js index 404b92d..c6133fd 100644 --- a/frontend/js/labs/electrolysis.js +++ b/frontend/js/labs/electrolysis.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /** * ElectrolysisSim v2 — Электролиз водных растворов * Закон Фарадея: m = M·I·t / (n·F), F = 96485 Кл/моль @@ -539,3 +539,45 @@ class ElectrolysisSim { } if (typeof module !== 'undefined') module.exports = ElectrolysisSim; + +/* ─── lab UI init ─────────────────────────────────── */ + function _openElectrolysis() { + document.getElementById('sim-topbar-title').textContent = 'Электролиз'; + _simShow('sim-electrolysis'); + _registerSimState('electrolysis', () => elecSim?.getParams(), st => elecSim?.setParams(st)); + if (_embedMode) _startStateEmit('electrolysis'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!elecSim) { + elecSim = new ElectrolysisSim(document.getElementById('electrolysis-canvas')); + elecSim.onUpdate = _elecUpdateUI; + } + elecSim.fit(); + elecSim.reset(); + elecSim.play(); + })); + } + + function elecParam(name, val) { + const v = parseFloat(val); + if (name === 'voltage') document.getElementById('elec-V-val').textContent = v; + if (elecSim) elecSim.setParams({ [name]: v }); + } + + function elecPreset(name, btn) { + document.querySelectorAll('.elec-type-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + const voltages = { nacl: 6, cuso4: 4, h2so4: 3 }; + const vt = voltages[name] || 6; + document.getElementById('sl-elec-V').value = vt; document.getElementById('elec-V-val').textContent = vt; + if (elecSim) { elecSim.setParams({ electrolyte: name, voltage: vt }); elecSim.reset(); elecSim.play(); } + } + + function _elecUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('elecbar-v1', typeof info.current === 'number' ? info.current.toFixed(2) : '—'); + v('elecbar-v2', typeof info.massDeposited === 'number' ? info.massDeposited.toFixed(3) + ' г' : '—'); + v('elecbar-v3', typeof info.gasVolume === 'number' ? info.gasVolume.toFixed(1) : '—'); + v('elecbar-v4', typeof info.time === 'number' ? info.time.toFixed(0) + ' с' : '—'); + } + + /* ── waves ── */ diff --git a/frontend/js/labs/equilibrium.js b/frontend/js/labs/equilibrium.js index 2009caf..99dcc2f 100644 --- a/frontend/js/labs/equilibrium.js +++ b/frontend/js/labs/equilibrium.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /** * EquilibriumSim — Chemical equilibrium simulation. @@ -474,3 +474,48 @@ class EquilibriumSim { } if (typeof module !== 'undefined') module.exports = EquilibriumSim; + +/* ─── lab UI init ─────────────────────────────────── */ + function _openEquilibrium() { + document.getElementById('sim-topbar-title').textContent = 'Химическое равновесие'; + _simShow('sim-equilibrium'); + _registerSimState('equilibrium', () => eqSim?.getParams(), st => eqSim?.setParams(st)); + if (_embedMode) _startStateEmit('equilibrium'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!eqSim) { + eqSim = new EquilibriumSim(document.getElementById('equilibrium-canvas')); + eqSim.onUpdate = _eqUpdateUI; + } + eqSim.fit(); + eqSim.reset(); + eqSim.play(); + })); + } + + function eqParam(name, val) { + const v = parseFloat(val); + const ids = { T: 'eq-T-val', Ea_f: 'eq-Eaf-val', Ea_r: 'eq-Ear-val' }; + const el = document.getElementById(ids[name]); + if (el) el.textContent = v; + if (eqSim) eqSim.setParams({ [name]: v }); + } + + function eqPreset(name) { + if (eqSim) { eqSim.preset(name); eqSim.play(); } + const defs = { default: [300,50,55], exothermic: [280,35,65], endothermic: [350,65,35], excess_A: [300,50,55] }; + const d = defs[name] || defs.default; + document.getElementById('sl-eq-T').value = d[0]; document.getElementById('eq-T-val').textContent = d[0]; + document.getElementById('sl-eq-Eaf').value = d[1]; document.getElementById('eq-Eaf-val').textContent = d[1]; + document.getElementById('sl-eq-Ear').value = d[2]; document.getElementById('eq-Ear-val').textContent = d[2]; + } + + function _eqUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('eqbar-v1', info.keq); + v('eqbar-v2', info.Q); + v('eqbar-v3', info.direction); + v('eqbar-v4', info.nA + '|' + info.nB + '|' + info.nC + '|' + info.nD); + } + + /* ── thin lens ── */ + diff --git a/frontend/js/labs/gas.js b/frontend/js/labs/gas.js index 84c5546..4f080c0 100644 --- a/frontend/js/labs/gas.js +++ b/frontend/js/labs/gas.js @@ -1,4 +1,4 @@ -/** +/** * GasSim v2 — Ideal Gas simulation (PV=nRT, Maxwell-Boltzmann distribution) * v2: hover inspector, velocity vectors, movable piston, v_mp/v_rms markers. */ @@ -460,3 +460,267 @@ class GasSim { 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 ? '‖ Раздел' : ' Раздел снят'; + } + + 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; + } + + /* ════════════════════════════════ + ЗАКОН КУЛОНА + ════════════════════════════════ */ + diff --git a/frontend/js/labs/geometry.js b/frontend/js/labs/geometry.js index 54dab81..55813f0 100644 --- a/frontend/js/labs/geometry.js +++ b/frontend/js/labs/geometry.js @@ -1,4 +1,4 @@ -/* ═══════════════════════════════════════════════════════════════════════ +/* ═══════════════════════════════════════════════════════════════════════ geometry.js — Интерактивная планиметрия для LearnSpace Phase 1: точки, отрезки, прямые, лучи, окружности, многоугольники Phase 2: инструменты построения (середина, биссектрисы, параллельные, @@ -2581,3 +2581,150 @@ class GeoSim { }, 'image/png'); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function geoSetTool(name, btnEl) { + if (!geomSim) return; + geomSim.setTool(name); + document.querySelectorAll('.geo-tool-btn').forEach(b => b.classList.remove('active')); + if (btnEl) btnEl.classList.add('active'); + _geoShowHint(name); + } + + const _GEO_PHASE_HINTS = { + parallel_2: 'Теперь кликни на точку — через неё проведём прямую', + perpendicular_2: 'Теперь кликни на точку — через неё проведём перпендикуляр', + intersect_2: 'Теперь кликни на вторую прямую', + foot_2: 'Теперь кликни на точку — найдём основание перпендикуляра', + reflect_2: 'Теперь кликни на точку — получишь её симметричное отражение', + tangent_2: 'Теперь кликни на внешнюю точку — получишь две касательные', + translate_2: 'Теперь кликни конец вектора B', + translate_3: 'Теперь кликни точку P — она будет перенесена', + midline_2: 'Кликни вершину B (конец первой стороны)', + midline_3: 'Кликни вершину C (конец второй стороны) — построим среднюю линию', + parallelogram_2: 'Кликни вершину B (смежная с A)', + parallelogram_3: 'Кликни вершину C — построим параллелограмм ABCD', + scale_2: 'Кликни точку P — построим P\' = O + k·(P − O)', + thales_2: 'Кликни точку A (на первом луче)', + thales_3: 'Кликни точку B (на втором луче) — построим A\'B\' ∥ AB', + }; + + function _geoShowHint(name, phase) { + const hint = document.getElementById('geo-hint'); + if (!hint) return; + if (phase && phase > 1) { + hint.textContent = _GEO_PHASE_HINTS[`${name}_${phase}`] || _GEO_HINTS[name] || ''; + } else { + hint.textContent = _GEO_HINTS[name] || ''; + } + } + + function geoNgonN(delta) { + if (!geomSim) return; + geomSim.setNgonSides(geomSim._ngonSides + delta); + const el = document.getElementById('geo-ngon-n'); + if (el) el.textContent = geomSim._ngonSides; + } + + function geoScaleK(delta) { + if (!geomSim) return; + const k = Math.round((geomSim._scaleK + delta) * 10) / 10; + if (k < 0.1) return; + geomSim.setScaleK(k); + const el = document.getElementById('geo-scale-k'); + if (el) el.textContent = k; + } + + function geoToggle(prop, rowEl) { + if (!geomSim) return; + geomSim[prop] = !geomSim[prop]; + const tog = rowEl.querySelector('.geo-toggle'); + if (tog) tog.classList.toggle('on', geomSim[prop]); + geomSim.render(); + } + + function _geoUpdateStats() { + if (!geomSim) return; + const s = geomSim.getStats(); + document.getElementById('geo-st-pts').textContent = s.pts; + document.getElementById('geo-st-segs').textContent = s.segs; + document.getElementById('geo-st-circs').textContent = s.circs; + document.getElementById('geo-st-polys').textContent = s.polys; + const cEl = document.getElementById('geo-st-constr'); + if (cEl) cEl.textContent = s.constructions || 0; + } + + /* Диалог подтверждения удаления объекта с зависимыми */ + let _geoDelSoftFn = null, _geoDelHardFn = null; + function _geoShowDeleteConfirm(obj, deps, softFn, hardFn) { + const panel = document.getElementById('geo-del-confirm'); + const msg = document.getElementById('geo-del-msg'); + if (!panel || !msg) { hardFn(); return; } + const names = { point:'точка', segment:'отрезок', line:'прямая', ray:'луч', + circle:'окружность', polygon:'многоугольник', derived_line:'построение' }; + const n = names[obj.type] || 'объект'; + msg.textContent = `Удалить ${n}? Зависимых: ${deps.length}.`; + _geoDelSoftFn = softFn; + _geoDelHardFn = hardFn; + panel.classList.add('visible'); + } + function _geoHideDeleteConfirm() { + document.getElementById('geo-del-confirm')?.classList.remove('visible'); + _geoDelSoftFn = _geoDelHardFn = null; + } + // Кнопки диалога — подключаем после DOM ready + document.addEventListener('DOMContentLoaded', () => { + document.getElementById('geo-del-soft')?.addEventListener('click', () => { + _geoDelSoftFn?.(); _geoHideDeleteConfirm(); _geoUpdateStats(); + }); + document.getElementById('geo-del-hard')?.addEventListener('click', () => { + _geoDelHardFn?.(); _geoHideDeleteConfirm(); _geoUpdateStats(); + }); + document.getElementById('geo-del-cancel')?.addEventListener('click', _geoHideDeleteConfirm); + }); + + function _openGeometry() { + document.getElementById('sim-topbar-title').textContent = 'Планиметрия'; + _simShow('sim-geometry'); + _simShow('ctrl-geometry'); + + _registerSimState( + 'geometry', + () => geomSim?.exportState(), + st => { if (geomSim && st) { geomSim.importState(st); _geoUpdateStats(); } } + ); + if (_embedMode) _startStateEmit('geometry'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + const canvas = document.getElementById('geo-canvas'); + if (!geomSim) { + geomSim = new GeoSim(canvas); + geomSim.onUpdate = _geoUpdateStats; + geomSim.onHintChange = (tool, phase) => _geoShowHint(tool, phase); + geomSim.onDeleteRequest = _geoShowDeleteConfirm; + + // keyboard shortcuts + canvas.setAttribute('tabindex', '0'); + canvas.addEventListener('keydown', e => { + if (!geomSim) return; + if (e.key === 'Escape') { geoSetTool('select', document.getElementById('geo-btn-select')); } + if ((e.ctrlKey||e.metaKey) && e.key === 'z') { e.preventDefault(); geomSim.undo(); _geoUpdateStats(); } + if ((e.ctrlKey||e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key==='z'))) { e.preventDefault(); geomSim.redo(); _geoUpdateStats(); } + if (e.key === 'Delete' || e.key === 'Backspace') { geomSim.deleteSelected(); _geoUpdateStats(); } + if (e.key === 'Enter') { geomSim._finishPolygon?.(); _geoUpdateStats(); } + }); + } + geomSim.fit(); + geomSim.render(); + _geoUpdateStats(); + + // sync toggle UI to current state + ['showGrid','showAxes','showLabels','showLengths','showAngles'].forEach(p => { + const el = document.getElementById('geo-tog-' + p); + if (el) el.classList.toggle('on', !!geomSim[p]); + }); + })); + } + + /* ── trig circle ── */ + diff --git a/frontend/js/labs/graph.js b/frontend/js/labs/graph.js index edc42f4..4828aa0 100644 --- a/frontend/js/labs/graph.js +++ b/frontend/js/labs/graph.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════ GraphSim — interactive function plotter @@ -491,3 +491,147 @@ class GraphSim { this.onHover(this.hx, vals); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openGraph() { + document.getElementById('sim-topbar-title').textContent = 'График функции'; + _simShow('sim-graph'); + _simShow('ctrl-graph'); + + _registerSimState('graph', + () => ({ + fns: [0,1,2].map(i => ({ expr: document.getElementById(`fn${i}`)?.value || '', color: FN_COLORS[i] })) + }), + (st) => { + if (!Array.isArray(st.fns)) return; + st.fns.forEach((fn, i) => { + const el = document.getElementById(`fn${i}`); + if (el) { el.value = fn.expr; } + if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]); + }); + } + ); + if (_embedMode) _startStateEmit('graph'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!gSim) { + gSim = new GraphSim(document.getElementById('graph-canvas')); + gSim.onHover = updateInfoBar; + if (!document.getElementById('fn0').value.trim()) { + document.getElementById('fn0').value = 'sin(x)'; + renderPreview(0); + gSim.fit(); + gSim.setFn(0, 'sin(x)', FN_COLORS[0]); + return; + } + } + gSim.fit(); + gSim.draw(); + })); + } + + /* ── projectile ── */ + + function toLatex(expr) { + if (!expr) return ''; + return expr + // strip leading y= if typed + .replace(/^\s*y\s*=\s*/i, '') + // inverse trig (before sin/cos/tan) + .replace(/\barcsin\b/g, '\\arcsin').replace(/\barccos\b/g, '\\arccos') + .replace(/\b(arctan|arctg|atan|acos|asin)\b/g, (_, w) => + w === 'asin' ? '\\arcsin' : w === 'acos' ? '\\arccos' : '\\arctan') + // trig + .replace(/\bctg\b/g, '\\cot').replace(/\btg\b/g, '\\tan') + .replace(/\b(sin|cos|tan)\b/g, '\\$1') + // log / exp + .replace(/\bln\b/g, '\\ln').replace(/\blog2\b/g, '\\log_2') + .replace(/\blog\b/g, '\\log').replace(/\bexp\b/g, '\\exp') + // special functions: f(inner) LaTeX form + .replace(/\bsqrt\(([^()]*)\)/g, '\\sqrt{$1}') + .replace(/\babs\(([^()]*)\)/g, '\\left|$1\\right|') + .replace(/\bfloor\(([^()]*)\)/g, '\\lfloor $1 \\rfloor') + .replace(/\bceil\(([^()]*)\)/g, '\\lceil $1 \\rceil') + .replace(/\b(round|sign)\b/g, '\\operatorname{$1}') + // constants + .replace(/\bpi\b/gi, '\\pi') + // power: wrap exponent in braces for multi-char + .replace(/\^(-?\d{2,})/g, '^{$1}') + // clean up multiplication + .replace(/([0-9])\s*\*\s*([a-zA-Z\\])/g, '$1\\,$2') + .replace(/\*/g, '\\cdot '); + } + + function renderPreview(idx) { + const inp = document.getElementById('fn' + idx); + const prev = document.getElementById('fn' + idx + '-prev'); + const raw = inp?.value?.trim() || ''; + if (!raw || typeof katex === 'undefined') { + prev.innerHTML = ''; prev.classList.remove('has-content'); return; + } + try { + prev.innerHTML = katex.renderToString(toLatex(raw), { + throwOnError: false, strict: false, displayMode: false, + }); + prev.classList.add('has-content'); + } catch { prev.innerHTML = ''; prev.classList.remove('has-content'); } + } + + /* debounced formula update */ + const _debounce = {}; + function updateFn(idx) { + clearTimeout(_debounce[idx]); + renderPreview(idx); // instant preview + _debounce[idx] = setTimeout(() => { + if (!gSim) return; + const raw = document.getElementById('fn' + idx).value; + const val = raw.replace(/^\s*y\s*=\s*/i, ''); + const err = gSim.setFn(idx, val, FN_COLORS[idx]); + const errEl = document.getElementById('fn' + idx + '-err'); + errEl.classList.toggle('show', !!err && !!val.trim()); + }, 350); + } + + function applyPreset(expr) { + for (let i = 0; i < 3; i++) { + const inp = document.getElementById('fn' + i); + if (!inp.value.trim()) { + inp.value = expr; updateFn(i); inp.focus(); return; + } + } + document.getElementById('fn0').value = expr; updateFn(0); + } + + function clearAll() { + for (let i = 0; i < 3; i++) { + document.getElementById('fn' + i).value = ''; + document.getElementById('fn' + i + '-prev').innerHTML = ''; + document.getElementById('fn' + i + '-prev').classList.remove('has-content'); + document.getElementById('fn' + i + '-err').classList.remove('show'); + if (gSim) gSim.setFn(i, '', FN_COLORS[i]); + } + } + + /* hover info bar */ + function fmtVal(v) { + if (v === null || v === undefined) return '—'; + if (!isFinite(v)) return '∞'; + const abs = Math.abs(v); + if (abs === 0) return '0'; + if (abs < 0.001 || abs >= 1e6) return v.toExponential(3); + return parseFloat(v.toPrecision(6)).toString(); + } + + function updateInfoBar(mx, vals) { + document.getElementById('info-x').textContent = mx !== null ? fmtVal(mx) : '—'; + document.getElementById('info-y0').textContent = vals ? fmtVal(vals[0]) : '—'; + document.getElementById('info-y1').textContent = vals ? fmtVal(vals[1]) : '—'; + document.getElementById('info-y2').textContent = vals ? fmtVal(vals[2]) : '—'; + } + + /* ════════════════════════════════ + МОЛЕКУЛЯРНАЯ ФИЗИКА (unified: gas + brownian + states + diffusion) + ════════════════════════════════ */ + + let _molMode = 'gas'; // 'gas' | 'brownian' | 'states' | 'diffusion' + diff --git a/frontend/js/labs/graphtransform.js b/frontend/js/labs/graphtransform.js index 9c900d4..afd82d7 100644 --- a/frontend/js/labs/graphtransform.js +++ b/frontend/js/labs/graphtransform.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ GraphTransformSim — graph transformations explorer y = a·f(k·x + b) + c with sliders for a, k, b, c @@ -354,3 +354,53 @@ class GraphTransformSim { cv.addEventListener('touchend', () => { t0 = null; }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var gtSim = null; + + function _openGraphTransform() { + document.getElementById('sim-topbar-title').textContent = 'Трансформации графиков'; + _simShow('sim-graphtransform'); + _registerSimState('graphtransform', () => gtSim?.getParams(), st => gtSim?.setParams(st)); + if (_embedMode) _startStateEmit('graphtransform'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!gtSim) { + gtSim = new GraphTransformSim(document.getElementById('graphtransform-canvas')); + gtSim.onUpdate = _gtUpdateUI; + } + gtSim.fit(); + gtSim.draw(); + gtSim._emit(); + })); + } + + function gtParam(name, val) { + const v = parseFloat(val); + document.getElementById('gt-' + name + '-val').textContent = v % 1 === 0 ? v : v.toFixed(1); + if (gtSim) gtSim.setParams({ [name]: v }); + } + + function gtBase(name, btn) { + document.querySelectorAll('.gt-base-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (gtSim) gtSim.setBase(name); + } + + function gtEffect(a, k, b, c) { + document.getElementById('sl-gt-a').value = a; document.getElementById('gt-a-val').textContent = a; + document.getElementById('sl-gt-k').value = k; document.getElementById('gt-k-val').textContent = k; + document.getElementById('sl-gt-b').value = b; document.getElementById('gt-b-val').textContent = b; + document.getElementById('sl-gt-c').value = c; document.getElementById('gt-c-val').textContent = c; + if (gtSim) gtSim.setParams({ a, k, b, c }); + } + + function _gtUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('gtbar-v1', info.base); + v('gtbar-v2', info.a); + v('gtbar-v3', info.k); + v('gtbar-v4', info.b); + v('gtbar-v5', info.c); + } + + /* ── pendulum ── */ diff --git a/frontend/js/labs/hydrostatics.js b/frontend/js/labs/hydrostatics.js index f3e3d0d..c34ce58 100644 --- a/frontend/js/labs/hydrostatics.js +++ b/frontend/js/labs/hydrostatics.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════════════════════════ HydroSim v2 — Гидростатика Модули: давление · поверхностное натяжение · сообщающиеся сосуды · Архимед @@ -1348,3 +1348,119 @@ class HydroSim { } _notify() { if (this.onUpdate) try { this.onUpdate(this.getInfo()); } catch {} } } + +/* ─── lab UI init ─────────────────────────────────── */ + var hydroSim = null; + let _hydroValveOpen = true; + + function _openHydro(preset) { + document.getElementById('sim-topbar-title').textContent = 'Гидростатика'; + _simShow('sim-hydro'); + document.getElementById('ctrl-hydro').style.display = ''; + _registerSimState('hydrostatics', + () => ({ mode: hydroSim?.mode, liq: hydroSim?.liquidKey }), + st => { if (st?.mode && hydroSim) hydroMode(st.mode); }); + if (_embedMode) _startStateEmit('hydrostatics'); + window.addEventListener('load', () => {}, { once: true }); + requestAnimationFrame(() => requestAnimationFrame(() => { + const canvas = document.getElementById('hydro-canvas'); + const mode = preset || 'pressure'; + if (!hydroSim) { + hydroSim = new HydroSim(canvas, mode); + hydroSim.onUpdate = _hydroUpdateUI; + } else { + hydroSim.fit(); + hydroSim.play(); + } + hydroMode(mode); + })); + } + + function hydroMode(mode) { + if (!hydroSim) return; + hydroSim.setMode(mode); + const sel = document.getElementById('hydro-mode-sel'); + if (sel) sel.value = mode; + // show/hide sub-controls + ['arch','comm','surf','mat'].forEach(k => { + const el = document.getElementById('hydro-panel-' + k); + const el2 = document.getElementById('hydro-' + k + '-ctrl'); + if (el) el.style.display = 'none'; + if (el2) el2.style.display = 'none'; + }); + if (mode === 'archimedes') { + const a = document.getElementById('hydro-panel-mat'); + const b = document.getElementById('hydro-arch-ctrl'); + if (a) a.style.display = ''; + if (b) b.style.display = 'flex'; + } + if (mode === 'surface') { + const a = document.getElementById('hydro-panel-theta'); + const b = document.getElementById('hydro-surf-ctrl'); + if (a) a.style.display = ''; + if (b) b.style.display = 'flex'; + } + if (mode === 'communicating') { + const a = document.getElementById('hydro-panel-comm'); + const b = document.getElementById('hydro-comm-ctrl'); + if (a) a.style.display = ''; + if (b) b.style.display = 'flex'; + } + } + + function hydroToggleSurface() { + if (!hydroSim) return; + const next = hydroSim._stMode === 'capillary' ? 'drop' : 'capillary'; + hydroSim._stMode = next; + const label = next === 'capillary' ? '\u041A\u0430\u043F\u0438\u043B\u043B\u044F\u0440\u044B' : '\u041A\u0430\u043F\u043B\u044F'; + ['hydro-surf-toggle','hydro-surf-toggle-panel'].forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = label; + }); + } + + function hydroToggleValve() { + if (!hydroSim) return; + _hydroValveOpen = !_hydroValveOpen; + hydroSim.setValve(_hydroValveOpen); + const label = _hydroValveOpen ? 'Кран: открыт' : 'Кран: закрыт'; + const color = _hydroValveOpen ? '#06D6A0' : '#F15BB5'; + ['hydro-valve-btn','hydro-valve-panel-btn'].forEach(id => { + const el = document.getElementById(id); + if (el) { el.textContent = label; el.style.color = color; el.style.borderColor = _hydroValveOpen ? 'rgba(6,214,160,.3)' : 'rgba(241,91,181,.3)'; } + }); + } + + function hydroSetVessels(n, btn) { + if (hydroSim) hydroSim.setNumVessels(n); + document.querySelectorAll('.hydro-nv').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + } + + function _hydroUpdateUI(info) { + if (!info) return; + const el = document.getElementById('hydro-formulas'); + if (!el) return; + const lines = []; + if (info.formula) lines.push(`${info.formula}`); + if (info.liqName) lines.push(`Жидкость: ${info.liqName}${info.rho ? ' (ρ=' + info.rho + ')' : ''}`); + if (info.matName) lines.push(`Материал: ${info.matName}`); + if (info.FA) lines.push(`F_A = ${info.FA} Н`); + if (info.mg) lines.push(`mg = ${info.mg} Н`); + if (info.sigma) lines.push(`σ = ${info.sigma} Н/м, θ = ${info.theta}°`); + if (info.h && !info.FA) lines.push(`h_подъём = ${info.h} мм`); + el.innerHTML = lines.join('
'); + // result badge + const rb = document.getElementById('hydro-result'); + if (rb && info.state) { + const colors = { 'ВСПЛЫВАЕТ': '#06D6A0', 'ТОНЕТ': '#F15BB5', 'ВЗВЕШЕНО': '#FFD166' }; + rb.style.display = ''; + rb.style.color = colors[info.state] || '#fff'; + rb.style.background = (colors[info.state] || '#9B5DE5') + '18'; + rb.style.border = '1px solid ' + (colors[info.state] || '#9B5DE5') + '44'; + rb.textContent = info.state; + } else if (rb) { + rb.style.display = 'none'; + } + } + diff --git a/frontend/js/labs/isoprocess.js b/frontend/js/labs/isoprocess.js index 04c96aa..a35dd84 100644 --- a/frontend/js/labs/isoprocess.js +++ b/frontend/js/labs/isoprocess.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ IsoprocessSim — PV-diagram for 4 ideal-gas isoprocesses n = 1, R = 0.0821 L·atm/mol·K; energies in Joules @@ -462,3 +462,74 @@ class IsoprocessSim { }); } } + +/* ─── 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 ── */ + diff --git a/frontend/js/labs/lab-init.js b/frontend/js/labs/lab-init.js index 834e3af..4a4987a 100644 --- a/frontend/js/labs/lab-init.js +++ b/frontend/js/labs/lab-init.js @@ -192,3447 +192,6 @@ /* ── graph ── */ - function _openGraph() { - document.getElementById('sim-topbar-title').textContent = 'График функции'; - _simShow('sim-graph'); - _simShow('ctrl-graph'); - - _registerSimState('graph', - () => ({ - fns: [0,1,2].map(i => ({ expr: document.getElementById(`fn${i}`)?.value || '', color: FN_COLORS[i] })) - }), - (st) => { - if (!Array.isArray(st.fns)) return; - st.fns.forEach((fn, i) => { - const el = document.getElementById(`fn${i}`); - if (el) { el.value = fn.expr; } - if (gSim) gSim.setFn(i, fn.expr, FN_COLORS[i]); - }); - } - ); - if (_embedMode) _startStateEmit('graph'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!gSim) { - gSim = new GraphSim(document.getElementById('graph-canvas')); - gSim.onHover = updateInfoBar; - if (!document.getElementById('fn0').value.trim()) { - document.getElementById('fn0').value = 'sin(x)'; - renderPreview(0); - gSim.fit(); - gSim.setFn(0, 'sin(x)', FN_COLORS[0]); - return; - } - } - gSim.fit(); - gSim.draw(); - })); - } - - /* ── projectile ── */ - - function _openProjectile() { - document.getElementById('sim-topbar-title').textContent = 'Бросок тела'; - _simShow('sim-proj'); - _simShow('ctrl-proj'); - _registerSimState('projectile', () => pSim?.getParams(), st => pSim?.setParams(st)); - if (_embedMode) _startStateEmit('projectile'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!pSim) { - pSim = new ProjectileSim(document.getElementById('proj-canvas')); - pSim.onUpdate = _projUpdateUI; - pSim.onPlayPause = projPlayPause; - } - pSim.fit(); - projParam(); // sync sliders sim - pSim.draw(); - _projUpdateUI(pSim.stats()); - })); - } - - function projPlayPause() { - if (!pSim) return; - if (pSim.playing) { - pSim.pause(); - } else { - pSim.play(); - } - _projSyncPlayBtn(); - } - - function _projSyncPlayBtn() { - /* small topbar button */ - const tb = document.getElementById('proj-play-btn'); - /* big launch button */ - const lb = document.getElementById('proj-launch-main'); - const lbl = document.getElementById('proj-launch-label'); - const lic = document.getElementById('proj-launch-icon'); - if (!pSim) return; - - const tf = pSim._curTFlight(); - const done = !pSim.playing && pSim.t >= tf && pSim.t > 0; - const playing = pSim.playing; - - /* topbar */ - if (tb) { - tb.innerHTML = playing - ? '' - : ''; - tb.title = playing ? 'Пауза' : 'Запустить'; - tb.classList.toggle('active', playing); - } - - /* big button */ - if (lb && lbl && lic) { - lb.classList.toggle('paused', playing); - lb.classList.toggle('done', done && !playing); - if (playing) { - lic.innerHTML = ''; - lbl.textContent = 'Пауза'; - } else if (done) { - lic.innerHTML = ''; - lbl.textContent = 'Повторить'; - } else { - lic.innerHTML = ''; - lbl.textContent = 'Запустить'; - } - } - } - - function projParam() { - const v0 = +document.getElementById('sl-v0').value; - const angle = +document.getElementById('sl-angle').value; - const h0 = +document.getElementById('sl-h0').value; - const g = +document.getElementById('sl-g').value; - - document.getElementById('p-v0').textContent = v0 + ' м/с'; - document.getElementById('p-angle').textContent = angle + '°'; - document.getElementById('p-h0').textContent = h0 + ' м'; - document.getElementById('p-g').textContent = g.toFixed(2) + ' м/с²'; - - if (pSim) { pSim.setParams({ v0, angle, h0, g }); _projSyncPlayBtn(); } - } - - function projPreset(v0, angle, h0, g) { - document.getElementById('sl-v0').value = v0; - document.getElementById('sl-angle').value = angle; - document.getElementById('sl-h0').value = h0; - document.getElementById('sl-g').value = g; - projParam(); - } - - function projToggleDrag(rowEl) { - if (!pSim) return; - pSim.drag = !pSim.drag; - const on = pSim.drag; - rowEl.classList.toggle('active', on); - const tog = document.getElementById('drag-toggle'); - tog.style.background = on ? 'var(--violet)' : 'rgba(255,255,255,0.12)'; - tog.querySelector('span').style.marginLeft = on ? '14px' : '2px'; - document.getElementById('drag-params').style.display = on ? '' : 'none'; - document.getElementById('ps-loss-wrap').style.display = on ? '' : 'none'; - if (on) { - const cd = +document.getElementById('sl-cd').value / 100; - const mass = +document.getElementById('sl-mass').value; - pSim.setParams({ drag: true, Cd: cd, mass }); - } else { - pSim.setParams({ drag: false }); - } - } - - function projCdChange() { - const cd = +document.getElementById('sl-cd').value / 100; - document.getElementById('p-cd').textContent = cd.toFixed(2); - if (pSim) pSim.setParams({ Cd: cd }); - } - - function projMassChange() { - const mass = +document.getElementById('sl-mass').value; - document.getElementById('p-mass').textContent = mass + ' кг'; - if (pSim) pSim.setParams({ mass }); - } - - function projWindChange() { - const wind = +document.getElementById('sl-wind').value; - const label = wind === 0 ? '0 м/с' : (wind > 0 ? ' +' : ' ') + Math.abs(wind) + ' м/с'; - document.getElementById('p-wind').textContent = label; - document.getElementById('ps-loss-wrap').style.display = wind !== 0 ? '' : (pSim && pSim.drag ? '' : 'none'); - if (pSim) { pSim.setParams({ wind }); _projSyncPlayBtn(); } - } - - function projToggleBounce(rowEl) { - if (!pSim) return; - pSim.bounce = !pSim.bounce; - const on = pSim.bounce; - rowEl.classList.toggle('active', on); - const tog = document.getElementById('bounce-toggle'); - tog.style.background = on ? 'rgba(123,245,164,0.8)' : 'rgba(255,255,255,0.12)'; - tog.querySelector('span').style.marginLeft = on ? '14px' : '2px'; - document.getElementById('bounce-params').style.display = on ? '' : 'none'; - const e = +document.getElementById('sl-restitution').value / 100; - pSim.setParams({ bounce: on, restitution: e }); - } - - function projRestitutionChange() { - const e = +document.getElementById('sl-restitution').value / 100; - document.getElementById('p-restitution').textContent = e.toFixed(2); - if (pSim) pSim.setParams({ restitution: e }); - } - - function projSetSpeed(s, el) { - if (pSim) pSim.setSpeed(s); - document.querySelectorAll('.proj-speed').forEach(b => b.classList.remove('active')); - if (el) el.classList.add('active'); - } - - function projSaveGhost() { - if (pSim) pSim.saveGhost(); - } - - function projClearGhosts() { - if (pSim) pSim.clearGhosts(); - } - - function _projUpdateUI(s) { - const fmt = (n, unit) => n < 10000 ? n.toFixed(2) + ' ' + unit : (n/1000).toFixed(2) + ' к' + unit; - document.getElementById('ps-range').textContent = fmt(s.range, 'м'); - document.getElementById('ps-hmax').textContent = fmt(s.hMax, 'м'); - document.getElementById('ps-tf').textContent = s.tf.toFixed(2) + ' с'; - document.getElementById('ps-vland').textContent = fmt(s.vLand, 'м/с'); - document.getElementById('ps-t').textContent = s.t.toFixed(2) + ' с'; - const laEl = document.getElementById('ps-land-angle'); - if (laEl) laEl.textContent = s.landAngle > 0.5 ? s.landAngle.toFixed(1) + '°' : '—'; - if (s.hasMod) { - const lossEl = document.getElementById('ps-loss'); - if (lossEl) { - const sign = s.rangeLoss > 0 ? '+' : ''; - lossEl.textContent = s.rangeLoss !== 0 ? sign + s.rangeLoss + '%' : '0%'; - lossEl.style.color = s.rangeLoss < 0 ? '#EF476F' : '#7BF5A4'; - } - } - _projSyncPlayBtn(); - } - - /* ── collision ── */ - - function _openCollision() { - document.getElementById('sim-topbar-title').textContent = 'Столкновение шаров'; - _simShow('sim-coll'); - _simShow('ctrl-coll'); - _registerSimState('collision', () => cSim?.getParams(), st => cSim?.setParams(st)); - if (_embedMode) _startStateEmit('collision'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!cSim) { - cSim = new CollisionSim(document.getElementById('coll-canvas')); - cSim.onUpdate = _collUpdateUI; - cSim.onPlayPause = collPlayPause; - } - cSim.fit(); - cSim.setSpeed(+document.getElementById('sl-speed').value); - collParam(); - cSim.draw(); - _collUpdateUI(cSim.stats()); - })); - } - - function collPlayPause() { - if (!cSim) return; - if (cSim.playing) { cSim.pause(); } else { cSim.play(); } - _collSyncBtn(); - } - - function _collSyncBtn() { - const tb = document.getElementById('coll-play-btn'); - const lb = document.getElementById('coll-launch-main'); - const lbl = document.getElementById('coll-launch-label'); - const lic = document.getElementById('coll-launch-icon'); - if (!cSim) return; - const playing = cSim.playing; - - if (tb) { - tb.innerHTML = playing - ? '' - : ''; - tb.title = playing ? 'Пауза' : 'Запустить'; - tb.classList.toggle('active', playing); - } - - if (lb && lbl && lic) { - lb.classList.toggle('paused', playing); - lb.classList.remove('done'); - if (playing) { - lic.innerHTML = ''; - lbl.textContent = 'Пауза'; - } else { - lic.innerHTML = ''; - lbl.textContent = 'Запустить'; - } - } - } - - function collParam() { - const m1 = +document.getElementById('sl-m1').value; - const m2 = +document.getElementById('sl-m2').value; - const v1 = +document.getElementById('sl-cv1').value; - const v2 = +document.getElementById('sl-cv2').value; - const angle = +document.getElementById('sl-cangle').value; - const e = +document.getElementById('sl-e').value; - const spd = +document.getElementById('sl-speed').value; - - document.getElementById('c-m1').textContent = m1 + ' кг'; - document.getElementById('c-m2').textContent = m2 + ' кг'; - document.getElementById('c-v1').textContent = v1 + ' м/с'; - document.getElementById('c-v2').textContent = v2 + ' м/с'; - document.getElementById('c-angle').textContent = angle + '°'; - document.getElementById('c-e').textContent = e.toFixed(2); - document.getElementById('c-speed').textContent = spd.toFixed(2) + '×'; - - if (cSim) { - /* speed change doesn't require a reset */ - const speedChanged = Math.abs(cSim.speed - spd) > 0.001; - if (speedChanged) cSim.setSpeed(spd); - - const physChanged = cSim.m1 !== m1 || cSim.m2 !== m2 || - cSim.v1 !== v1 || cSim.v2 !== v2 || - cSim.angle !== angle || cSim.e !== e; - if (physChanged) cSim.setParams({ m1, m2, v1, v2, angle, e }); - _collSyncBtn(); - } - } - - function collPreset(m1, m2, v1, v2, angle, e) { - document.getElementById('sl-m1').value = m1; - document.getElementById('sl-m2').value = m2; - document.getElementById('sl-cv1').value = v1; - document.getElementById('sl-cv2').value = v2; - document.getElementById('sl-cangle').value = angle; - document.getElementById('sl-e').value = e; - collParam(); - } - - function _collUpdateUI(s) { - // before/after are arrays [{m, vx, vy, ke}, ...] - function snapKE(arr) { return arr ? arr.reduce((t, b) => t + b.ke, 0) : null; } - function snapP(arr) { - if (!arr) return null; - return Math.hypot(arr.reduce((t, b) => t + b.m * b.vx, 0), - arr.reduce((t, b) => t + b.m * b.vy, 0)); - } - const bKE = snapKE(s.before), bP = snapP(s.before); - const aKE = snapKE(s.after), aP = snapP(s.after); - const f2 = v => v !== null ? v.toFixed(2) : '—'; - - document.getElementById('cs-pbefore').textContent = bP !== null ? f2(bP) + ' кг·м/с' : '—'; - document.getElementById('cs-pafter').textContent = aP !== null ? f2(aP) + ' кг·м/с' : '—'; - document.getElementById('cs-kebefore').textContent = bKE !== null ? f2(bKE) + ' Дж' : '—'; - document.getElementById('cs-keafter').textContent = aKE !== null ? f2(aKE) + ' Дж' : '—'; - document.getElementById('cs-count').textContent = s.colCount; - _collSyncBtn(); - } - - /* ── magnetic ── */ - - function _openMagnetic() { - document.getElementById('sim-topbar-title').textContent = 'Магнитное поле токов'; - _simShow('sim-mag'); - _simShow('ctrl-mag'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!mSim) { - mSim = new MagneticSim(document.getElementById('mag-canvas')); - mSim.onUpdate = _magUpdateUI; - } - mSim.fit(); - // default preset on first open - if (mSim.sources.length === 0) mSim.preset('anti'); - _magUpdateUI(mSim.info()); - })); - } - - function magMode(dir) { - if (!mSim) return; - mSim.addMode = dir; - document.getElementById('mag-add-out').classList.toggle('active', dir === 'out'); - document.getElementById('mag-add-in').classList.toggle('active', dir === 'in'); - document.getElementById('mag-mode-out').classList.toggle('active', dir === 'out'); - document.getElementById('mag-mode-in').classList.toggle('active', dir === 'in'); - } - - function magCurrentChange() { - const I = +document.getElementById('sl-curI').value; - document.getElementById('m-curI').textContent = I + ' А'; - document.getElementById('mbar-I').textContent = I + ' А'; - if (mSim) mSim.setCurrentAll(I); - } - - function magLayer(name, rowEl) { - if (!mSim) return; - mSim.layers[name] = !mSim.layers[name]; - rowEl.classList.toggle('active', mSim.layers[name]); - mSim._invalidateCache(); - mSim.draw(); - } - - function magParticle(rowEl) { - if (!mSim) return; - mSim.toggleParticle(); - rowEl.classList.toggle('active', mSim.particleOn); - _magUpdateUI(mSim.info()); - } - - function magCondToggle(rowEl) { - if (!mSim) return; - mSim.toggleConductor(); - const on = mSim._cond.on; - rowEl.classList.toggle('active', on); - document.getElementById('cond-I-block').style.display = on ? '' : 'none'; - _magUpdateUI(mSim.info()); - } - - function magCondCurrentChange() { - if (!mSim) return; - const I = parseFloat(document.getElementById('sl-condI').value); - document.getElementById('m-condI').textContent = I + ' А'; - mSim.setConductorI(I); - } - - function magFluxToggle(rowEl) { - if (!mSim) return; - mSim.toggleFlux(); - rowEl.classList.toggle('active', mSim._flux.on); - _magUpdateUI(mSim.info()); - } - - function _magUpdateUI(info) { - document.getElementById('ms-out').textContent = info.out; - document.getElementById('ms-in').textContent = info.inn; - document.getElementById('mbar-total').textContent = info.total; - document.getElementById('mbar-out').textContent = info.out; - document.getElementById('mbar-in').textContent = info.inn; - document.getElementById('mbar-particle').textContent = info.particleOn ? 'вкл' : 'выкл'; - document.getElementById('mbar-particle').style.color = info.particleOn ? '#ffff50' : ''; - // Ampere force - const fEl = document.getElementById('mbar-ampere'); - if (info.condOn && info.Fz !== 0) { - const dir = info.Fz > 0 ? '⊙' : '⊗'; - fEl.textContent = dir + ' ' + Math.abs(info.Fz).toFixed(3); - fEl.style.color = '#fbbf24'; - } else { - fEl.textContent = '—'; - fEl.style.color = '#fbbf24'; - } - // Flux - const phEl = document.getElementById('mbar-flux'); - if (info.fluxOn) { - phEl.textContent = info.flux.toExponential(2) + ' Вб'; - phEl.style.color = '#34d399'; - } else { - phEl.textContent = '—'; - phEl.style.color = '#34d399'; - } - } - - /* ── triangle ── */ - - function _openTriangle() { - document.getElementById('sim-topbar-title').textContent = 'Геометрия треугольника'; - _simShow('sim-tri'); - _simShow('ctrl-tri'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!tSim) { - tSim = new TriangleSim(document.getElementById('tri-canvas')); - tSim.onUpdate = _triUpdateUI; - } - tSim.fit(); - tSim.draw(); - _triUpdateUI(tSim.stats()); - })); - } - - function triToggle(layer, rowEl) { - if (!tSim) return; - tSim.toggleLayer(layer); - rowEl.classList.toggle('active', tSim.layers[layer]); - } - - function _triUpdateUI(s) { - const f2 = v => v.toFixed(2); - const deg = v => v.toFixed(1) + '°'; - const unit = v => f2(v) + ' ед'; - - // panel - document.getElementById('ts-a').textContent = unit(s.a); - document.getElementById('ts-b').textContent = unit(s.b); - document.getElementById('ts-c').textContent = unit(s.c); - document.getElementById('ts-A').textContent = deg(s.A); - document.getElementById('ts-B').textContent = deg(s.B); - document.getElementById('ts-C').textContent = deg(s.C); - document.getElementById('ts-S').textContent = f2(s.S) + ' ед²'; - document.getElementById('ts-P').textContent = unit(s.perim); - document.getElementById('ts-R').textContent = unit(s.R); - document.getElementById('ts-r').textContent = unit(s.r); - document.getElementById('ts-type').textContent = s.type; - - // stats bar - document.getElementById('tbar-a').textContent = unit(s.a); - document.getElementById('tbar-b').textContent = unit(s.b); - document.getElementById('tbar-c').textContent = unit(s.c); - document.getElementById('tbar-S').textContent = f2(s.S) + ' ед²'; - document.getElementById('tbar-P').textContent = unit(s.perim); - document.getElementById('tbar-Rr').textContent = f2(s.R) + ' / ' + f2(s.r); - } - - /* ── geometry (planimetry) ── */ - - const _GEO_HINTS = { - select: 'Клик — выбрать объект, перетащи точку для перемещения', - point: 'Клик — поставить точку', - segment: 'Кликни 2 точки для отрезка', - line: 'Кликни 2 точки для прямой', - ray: 'Кликни: начало, затем направление', - circle: 'Клик — центр; второй клик — радиус', - triangle: 'Кликни 3 точки для треугольника', - quad: 'Кликни 4 точки для четырёхугольника', - polygon: 'Кликай точки; двойной клик или Enter — завершить', - midpoint: 'Кликни 2 точки — получи середину отрезка', - perpbisect: 'Кликни 2 точки — получи серединный перпендикуляр', - anglebisect: 'Кликни: точку A, затем вершину угла, затем точку B', - parallel: 'Сначала кликни на прямую/отрезок, затем на точку', - perpendicular:'Сначала кликни на прямую/отрезок, затем на точку', - intersect: 'Кликни на первую прямую, затем на вторую', - foot: 'Сначала кликни на прямую/отрезок', - circumcircle: 'Кликни 3 точки треугольника — получи описанную окружность', - incircle: 'Кликни 3 точки треугольника — получи вписанную окружность', - reflect: 'Сначала кликни на ось симметрии (прямую/отрезок)', - ngon: 'Клик — центр правильного многоугольника; второй клик — вершина', - tangent: 'Кликни на окружность — построим касательные', - translate: 'Кликни начало вектора A', - tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)', - arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)', - parallelmark: 'Кликни на отрезок или сторону — добавить метку параллельности (1–2; ещё раз — убрать)', - altitude: 'Кликни на вершину треугольника — построим высоту из неё', - median: 'Кликни на вершину треугольника — построим медиану из неё', - centroid: 'Кликни на треугольник или внутри него — построим все 3 медианы и центроид G', - orthocenter: 'Кликни на треугольник или внутри него — построим все 3 высоты и ортоцентр H', - thales: 'Кликни центр подобия O (начало лучей)', - midline: 'Кликни вершину A треугольника', - parallelogram:'Кликни вершину A параллелограмма', - diagonal: 'Кликни внутри четырёхугольника — построим диагонали', - scale: 'Кликни центр подобия O', - }; - - function geoSetTool(name, btnEl) { - if (!geomSim) return; - geomSim.setTool(name); - document.querySelectorAll('.geo-tool-btn').forEach(b => b.classList.remove('active')); - if (btnEl) btnEl.classList.add('active'); - _geoShowHint(name); - } - - const _GEO_PHASE_HINTS = { - parallel_2: 'Теперь кликни на точку — через неё проведём прямую', - perpendicular_2: 'Теперь кликни на точку — через неё проведём перпендикуляр', - intersect_2: 'Теперь кликни на вторую прямую', - foot_2: 'Теперь кликни на точку — найдём основание перпендикуляра', - reflect_2: 'Теперь кликни на точку — получишь её симметричное отражение', - tangent_2: 'Теперь кликни на внешнюю точку — получишь две касательные', - translate_2: 'Теперь кликни конец вектора B', - translate_3: 'Теперь кликни точку P — она будет перенесена', - midline_2: 'Кликни вершину B (конец первой стороны)', - midline_3: 'Кликни вершину C (конец второй стороны) — построим среднюю линию', - parallelogram_2: 'Кликни вершину B (смежная с A)', - parallelogram_3: 'Кликни вершину C — построим параллелограмм ABCD', - scale_2: 'Кликни точку P — построим P\' = O + k·(P − O)', - thales_2: 'Кликни точку A (на первом луче)', - thales_3: 'Кликни точку B (на втором луче) — построим A\'B\' ∥ AB', - }; - - function _geoShowHint(name, phase) { - const hint = document.getElementById('geo-hint'); - if (!hint) return; - if (phase && phase > 1) { - hint.textContent = _GEO_PHASE_HINTS[`${name}_${phase}`] || _GEO_HINTS[name] || ''; - } else { - hint.textContent = _GEO_HINTS[name] || ''; - } - } - - function geoNgonN(delta) { - if (!geomSim) return; - geomSim.setNgonSides(geomSim._ngonSides + delta); - const el = document.getElementById('geo-ngon-n'); - if (el) el.textContent = geomSim._ngonSides; - } - - function geoScaleK(delta) { - if (!geomSim) return; - const k = Math.round((geomSim._scaleK + delta) * 10) / 10; - if (k < 0.1) return; - geomSim.setScaleK(k); - const el = document.getElementById('geo-scale-k'); - if (el) el.textContent = k; - } - - function geoToggle(prop, rowEl) { - if (!geomSim) return; - geomSim[prop] = !geomSim[prop]; - const tog = rowEl.querySelector('.geo-toggle'); - if (tog) tog.classList.toggle('on', geomSim[prop]); - geomSim.render(); - } - - function _geoUpdateStats() { - if (!geomSim) return; - const s = geomSim.getStats(); - document.getElementById('geo-st-pts').textContent = s.pts; - document.getElementById('geo-st-segs').textContent = s.segs; - document.getElementById('geo-st-circs').textContent = s.circs; - document.getElementById('geo-st-polys').textContent = s.polys; - const cEl = document.getElementById('geo-st-constr'); - if (cEl) cEl.textContent = s.constructions || 0; - } - - /* Диалог подтверждения удаления объекта с зависимыми */ - let _geoDelSoftFn = null, _geoDelHardFn = null; - function _geoShowDeleteConfirm(obj, deps, softFn, hardFn) { - const panel = document.getElementById('geo-del-confirm'); - const msg = document.getElementById('geo-del-msg'); - if (!panel || !msg) { hardFn(); return; } - const names = { point:'точка', segment:'отрезок', line:'прямая', ray:'луч', - circle:'окружность', polygon:'многоугольник', derived_line:'построение' }; - const n = names[obj.type] || 'объект'; - msg.textContent = `Удалить ${n}? Зависимых: ${deps.length}.`; - _geoDelSoftFn = softFn; - _geoDelHardFn = hardFn; - panel.classList.add('visible'); - } - function _geoHideDeleteConfirm() { - document.getElementById('geo-del-confirm')?.classList.remove('visible'); - _geoDelSoftFn = _geoDelHardFn = null; - } - // Кнопки диалога — подключаем после DOM ready - document.addEventListener('DOMContentLoaded', () => { - document.getElementById('geo-del-soft')?.addEventListener('click', () => { - _geoDelSoftFn?.(); _geoHideDeleteConfirm(); _geoUpdateStats(); - }); - document.getElementById('geo-del-hard')?.addEventListener('click', () => { - _geoDelHardFn?.(); _geoHideDeleteConfirm(); _geoUpdateStats(); - }); - document.getElementById('geo-del-cancel')?.addEventListener('click', _geoHideDeleteConfirm); - }); - - function _openGeometry() { - document.getElementById('sim-topbar-title').textContent = 'Планиметрия'; - _simShow('sim-geometry'); - _simShow('ctrl-geometry'); - - _registerSimState( - 'geometry', - () => geomSim?.exportState(), - st => { if (geomSim && st) { geomSim.importState(st); _geoUpdateStats(); } } - ); - if (_embedMode) _startStateEmit('geometry'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - const canvas = document.getElementById('geo-canvas'); - if (!geomSim) { - geomSim = new GeoSim(canvas); - geomSim.onUpdate = _geoUpdateStats; - geomSim.onHintChange = (tool, phase) => _geoShowHint(tool, phase); - geomSim.onDeleteRequest = _geoShowDeleteConfirm; - - // keyboard shortcuts - canvas.setAttribute('tabindex', '0'); - canvas.addEventListener('keydown', e => { - if (!geomSim) return; - if (e.key === 'Escape') { geoSetTool('select', document.getElementById('geo-btn-select')); } - if ((e.ctrlKey||e.metaKey) && e.key === 'z') { e.preventDefault(); geomSim.undo(); _geoUpdateStats(); } - if ((e.ctrlKey||e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key==='z'))) { e.preventDefault(); geomSim.redo(); _geoUpdateStats(); } - if (e.key === 'Delete' || e.key === 'Backspace') { geomSim.deleteSelected(); _geoUpdateStats(); } - if (e.key === 'Enter') { geomSim._finishPolygon?.(); _geoUpdateStats(); } - }); - } - geomSim.fit(); - geomSim.render(); - _geoUpdateStats(); - - // sync toggle UI to current state - ['showGrid','showAxes','showLabels','showLengths','showAngles'].forEach(p => { - const el = document.getElementById('geo-tog-' + p); - if (el) el.classList.toggle('on', !!geomSim[p]); - }); - })); - } - - /* ── trig circle ── */ - - var trigSim = null; - - function _openTrigCircle() { - document.getElementById('sim-topbar-title').textContent = 'Тригонометрическая окружность'; - _simShow('sim-trigcircle'); - _simShow('ctrl-trigcircle'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!trigSim) { - trigSim = new TrigCircleSim(document.getElementById('trigcircle-canvas')); - trigSim.onUpdate = _trigUpdateUI; - } - trigSim.fit(); - trigSim.start(); - _trigUpdateUI(trigSim.stats()); - })); - } - - function trigToggle(layer, rowEl) { - if (!trigSim) return; - const isActive = rowEl.classList.toggle('active'); - trigSim.toggleLayer(layer, isActive); - } - - function trigSetGraphFn(fn, el) { - if (!trigSim) return; - document.querySelectorAll('.trig-fn-btn').forEach(b => b.classList.remove('active')); - el.classList.add('active'); - trigSim.setGraphFn(fn); - } - - function trigGoTo(rad) { - if (!trigSim) return; - trigSim.goToAngle(rad); - } - - function trigReset() { - if (!trigSim) return; - trigSim.setAngle(Math.PI / 4); - } - - function _trigUpdateUI(s) { - const _f = v => { - if (v === undefined) return '—'; - const a = Math.abs(v), sg = v < 0 ? '−' : ''; - if (a < 5e-4) return '0'; - if (Math.abs(a - 0.5) < 1e-3) return sg + '½'; - if (Math.abs(a - 1) < 1e-3) return sg + '1'; - if (Math.abs(a - Math.SQRT2/2) < 1e-3) return sg + '√2/2'; - if (Math.abs(a - Math.sqrt(3)/2) < 1e-3) return sg + '√3/2'; - if (Math.abs(a - Math.sqrt(3)/3) < 1e-3) return sg + '√3/3'; - if (Math.abs(a - Math.sqrt(3)) < 1e-3) return sg + '√3'; - return v.toFixed(4); - }; - const degStr = s.deg.toFixed(1) + '°'; - - // Panel values (nice fractions) - document.getElementById('trig-v-sin').textContent = _f(s.sin); - document.getElementById('trig-v-cos').textContent = _f(s.cos); - document.getElementById('trig-v-tan').textContent = _f(s.tan); - document.getElementById('trig-v-cot').textContent = _f(s.cot); - - // Angle badge - document.getElementById('trig-angle-badge').innerHTML = - `${degStr} = ${s.radLabel}
${s.angle.toFixed(4)} рад`; - - // Stats bar (nice fractions) - document.getElementById('trigbar-angle').textContent = degStr; - document.getElementById('trigbar-sin').textContent = _f(s.sin); - document.getElementById('trigbar-cos').textContent = _f(s.cos); - document.getElementById('trigbar-tan').textContent = _f(s.tan); - document.getElementById('trigbar-cot').textContent = _f(s.cot); - document.getElementById('trigbar-quad').textContent = ['I', 'II', 'III', 'IV'][s.quadrant - 1]; - } - - /* ── KaTeX live preview ── */ - - /** Convert user ascii expression LaTeX string for KaTeX preview */ - function toLatex(expr) { - if (!expr) return ''; - return expr - // strip leading y= if typed - .replace(/^\s*y\s*=\s*/i, '') - // inverse trig (before sin/cos/tan) - .replace(/\barcsin\b/g, '\\arcsin').replace(/\barccos\b/g, '\\arccos') - .replace(/\b(arctan|arctg|atan|acos|asin)\b/g, (_, w) => - w === 'asin' ? '\\arcsin' : w === 'acos' ? '\\arccos' : '\\arctan') - // trig - .replace(/\bctg\b/g, '\\cot').replace(/\btg\b/g, '\\tan') - .replace(/\b(sin|cos|tan)\b/g, '\\$1') - // log / exp - .replace(/\bln\b/g, '\\ln').replace(/\blog2\b/g, '\\log_2') - .replace(/\blog\b/g, '\\log').replace(/\bexp\b/g, '\\exp') - // special functions: f(inner) LaTeX form - .replace(/\bsqrt\(([^()]*)\)/g, '\\sqrt{$1}') - .replace(/\babs\(([^()]*)\)/g, '\\left|$1\\right|') - .replace(/\bfloor\(([^()]*)\)/g, '\\lfloor $1 \\rfloor') - .replace(/\bceil\(([^()]*)\)/g, '\\lceil $1 \\rceil') - .replace(/\b(round|sign)\b/g, '\\operatorname{$1}') - // constants - .replace(/\bpi\b/gi, '\\pi') - // power: wrap exponent in braces for multi-char - .replace(/\^(-?\d{2,})/g, '^{$1}') - // clean up multiplication - .replace(/([0-9])\s*\*\s*([a-zA-Z\\])/g, '$1\\,$2') - .replace(/\*/g, '\\cdot '); - } - - function renderPreview(idx) { - const inp = document.getElementById('fn' + idx); - const prev = document.getElementById('fn' + idx + '-prev'); - const raw = inp?.value?.trim() || ''; - if (!raw || typeof katex === 'undefined') { - prev.innerHTML = ''; prev.classList.remove('has-content'); return; - } - try { - prev.innerHTML = katex.renderToString(toLatex(raw), { - throwOnError: false, strict: false, displayMode: false, - }); - prev.classList.add('has-content'); - } catch { prev.innerHTML = ''; prev.classList.remove('has-content'); } - } - - /* debounced formula update */ - const _debounce = {}; - function updateFn(idx) { - clearTimeout(_debounce[idx]); - renderPreview(idx); // instant preview - _debounce[idx] = setTimeout(() => { - if (!gSim) return; - const raw = document.getElementById('fn' + idx).value; - const val = raw.replace(/^\s*y\s*=\s*/i, ''); - const err = gSim.setFn(idx, val, FN_COLORS[idx]); - const errEl = document.getElementById('fn' + idx + '-err'); - errEl.classList.toggle('show', !!err && !!val.trim()); - }, 350); - } - - function applyPreset(expr) { - for (let i = 0; i < 3; i++) { - const inp = document.getElementById('fn' + i); - if (!inp.value.trim()) { - inp.value = expr; updateFn(i); inp.focus(); return; - } - } - document.getElementById('fn0').value = expr; updateFn(0); - } - - function clearAll() { - for (let i = 0; i < 3; i++) { - document.getElementById('fn' + i).value = ''; - document.getElementById('fn' + i + '-prev').innerHTML = ''; - document.getElementById('fn' + i + '-prev').classList.remove('has-content'); - document.getElementById('fn' + i + '-err').classList.remove('show'); - if (gSim) gSim.setFn(i, '', FN_COLORS[i]); - } - } - - /* hover info bar */ - function fmtVal(v) { - if (v === null || v === undefined) return '—'; - if (!isFinite(v)) return '∞'; - const abs = Math.abs(v); - if (abs === 0) return '0'; - if (abs < 0.001 || abs >= 1e6) return v.toExponential(3); - return parseFloat(v.toPrecision(6)).toString(); - } - - function updateInfoBar(mx, vals) { - document.getElementById('info-x').textContent = mx !== null ? fmtVal(mx) : '—'; - document.getElementById('info-y0').textContent = vals ? fmtVal(vals[0]) : '—'; - document.getElementById('info-y1').textContent = vals ? fmtVal(vals[1]) : '—'; - document.getElementById('info-y2').textContent = vals ? fmtVal(vals[2]) : '—'; - } - - /* ════════════════════════════════ - МОЛЕКУЛЯРНАЯ ФИЗИКА (unified: gas + brownian + states + diffusion) - ════════════════════════════════ */ - - let _molMode = 'gas'; // 'gas' | 'brownian' | 'states' | 'diffusion' - - 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 ? '‖ Раздел' : ' Раздел снят'; - } - - 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; - } - - /* ════════════════════════════════ - ЗАКОН КУЛОНА - ════════════════════════════════ */ - - var csSim = null; - - function _openCoulomb() { - document.getElementById('sim-topbar-title').textContent = 'Закон Кулона'; - _simShow('sim-coulomb'); - _simShow('ctrl-coulomb'); - requestAnimationFrame(() => requestAnimationFrame(() => { - const canvas = document.getElementById('coulomb-canvas'); - if (!csSim) { - csSim = new CoulombSim(canvas); - csSim.onUpdate = _coulombUpdateUI; - } - csSim.fit(); - if (csSim.charges.length === 0) csSim.preset('dipole'); - _coulombUpdateUI(csSim.info()); - })); - } - - function coulombSign(s) { - if (!csSim) return; - csSim.setSign(s); - document.getElementById('cbtn-pos').classList.toggle('active', s > 0); - document.getElementById('cbtn-neg').classList.toggle('active', s < 0); - document.getElementById('csign-pos').style.opacity = s > 0 ? '1' : '0.45'; - document.getElementById('csign-neg').style.opacity = s < 0 ? '1' : '0.45'; - } - - function coulombLayer(name, rowEl) { - if (!csSim) return; - csSim.toggleLayer(name); - const on = csSim.layers[name]; - rowEl.classList.toggle('active', on); - const tog = rowEl.querySelector('.tri-toggle'); - if (tog) { - tog.style.background = on ? 'var(--violet)' : 'rgba(255,255,255,0.12)'; - const dot = tog.querySelector('span'); - if (dot) dot.style.marginLeft = on ? '14px' : '2px'; - } - csSim.draw(); - } - - function coulombPreset(name) { - if (!csSim) return; - csSim.preset(name); - } - - function _coulombUpdateUI(info) { - if (!info) return; - document.getElementById('cs-total').textContent = info.total; - document.getElementById('cs-curE').textContent = info.cursorE; - document.getElementById('cs-curV').textContent = info.cursorV; - document.getElementById('csbar-total').textContent = info.total; - document.getElementById('csbar-pos').textContent = info.positive; - document.getElementById('csbar-neg').textContent = info.negative; - document.getElementById('csbar-maxE').textContent = info.maxE; - document.getElementById('csbar-curE').textContent = info.cursorE; - } - - /* ════════════════════════════════ - ЭЛЕКТРИЧЕСКИЕ ЦЕПИ - ════════════════════════════════ */ - - var cirSim = null; - var reacSim = null; - var flaskSim = null; - - function _openCircuit() { - document.getElementById('sim-topbar-title').textContent = 'Электрические цепи'; - _simShow('sim-circuit'); - _simShow('ctrl-circuit'); - requestAnimationFrame(() => requestAnimationFrame(() => { - const canvas = document.getElementById('circuit-canvas'); - if (!cirSim) { - cirSim = new CircuitSim(canvas); - cirSim.onUpdate = _circUpdateUI; - cirSim.onModeChange = (mode) => { - document.querySelectorAll('.circ-tool-btn').forEach(b => { - b.classList.toggle('active', b.dataset.tool === mode); - }); - document.querySelectorAll('.circ-top-btn').forEach(b => { - b.classList.toggle('active', b.id === 'ctool-' + mode); - }); - }; - } else { - cirSim.stop(); - } - cirSim.fit(); - if (cirSim.components.length === 0) cirSim.preset('serial'); - cirSim.start(); - _circUpdateUI(cirSim.info()); - })); - } - - function circTool(tool, el) { - if (cirSim) cirSim.addMode = tool; - document.querySelectorAll('.circ-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === tool)); - document.querySelectorAll('.circ-top-btn').forEach(b => b.classList.toggle('active', b.id === 'ctool-' + tool)); - } - - function circPreset(name) { - if (!cirSim) return; - cirSim.preset(name); - } - - function circRChange() { - const v = +document.getElementById('sl-circR').value; - document.getElementById('circ-R-val').textContent = v + ' Ω'; - if (cirSim) cirSim.R_value = v; - } - - function circUChange() { - const v = +document.getElementById('sl-circU').value; - document.getElementById('circ-U-val').textContent = v + ' В'; - if (cirSim) cirSim.U_value = v; - } - - function circCChange() { - const v = +document.getElementById('sl-circC').value; - document.getElementById('circ-C-val').textContent = v + ' µF'; - if (cirSim) cirSim.C_value = v; - } - - function circFChange() { - const v = +document.getElementById('sl-circF').value; - document.getElementById('circ-F-val').textContent = v + ' Гц'; - if (cirSim) cirSim.acFreq = v; - } - - function _circUpdateUI(info) { - if (!info) return; - document.getElementById('cirbar-comps').textContent = info.components; - document.getElementById('cirbar-U').textContent = info.voltage ? info.voltage + ' В' : '—'; - document.getElementById('cirbar-I').textContent = info.current ? info.current + ' А' : '—'; - document.getElementById('cirbar-P').textContent = info.power ? info.power + ' Вт' : '—'; - const st = document.getElementById('cirbar-status'); - st.textContent = info.solved ? 'Замкнута' : 'Разомкнута'; - st.style.color = info.solved ? '#7BF5A4' : '#EF476F'; - } - - /* ════════════════════════════════ - ХИМИЯ (unified: кинетика + колба + ОВР + ионный обмен) - ════════════════════════════════ */ - - let _chemMode = 'kinetics'; // 'kinetics' | 'flask' | 'redox' | 'ionex' - - 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 ? ' Пауза' : ' Реакции'; - } - - 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 ? '' : ''; - } - - 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' ? '' : '—'; - } - - // _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) - ══════════════════════════════ */ - - var newtonSim = null; - var sandboxSim = null; - let _dynMode = 'sandbox'; // current mode: 'sandbox' | 'law1' | 'law2' | 'law3' - - function _openDynamics(preset) { - document.getElementById('sim-topbar-title').textContent = 'Динамика'; - _simShow('sim-dynamics'); - _simShow('ctrl-dynamics'); - requestAnimationFrame(() => requestAnimationFrame(() => { - // init sandbox - const sbCanvas = document.getElementById('sandbox-canvas'); - if (!sandboxSim) { - sandboxSim = new ForceSandboxSim(sbCanvas); - sandboxSim.onUpdate = _sbUpdateUI; - } - // init newton - const nwCanvas = document.getElementById('newton-canvas'); - if (!newtonSim) { - newtonSim = new NewtonSim(nwCanvas); - newtonSim.onUpdate = _newtonUpdateUI; - } - // activate current mode - dynMode(_dynMode); - if (preset) setTimeout(() => sbPreset(preset), 120); - })); - } - - function dynMode(mode, btn) { - _dynMode = mode; - const isSandbox = mode === 'sandbox'; - - // toggle mode buttons - document.querySelectorAll('.dyn-mode').forEach(b => b.classList.remove('active')); - const modeBtn = document.getElementById('dyn-mode-' + mode); - if (modeBtn) modeBtn.classList.add('active'); - - // toggle panels - document.getElementById('dyn-sandbox-panel').style.display = isSandbox ? '' : 'none'; - document.getElementById('dyn-newton-panel').style.display = isSandbox ? 'none' : ''; - - // toggle canvases - document.getElementById('sandbox-canvas').style.display = isSandbox ? 'block' : 'none'; - document.getElementById('newton-canvas').style.display = isSandbox ? 'none' : 'block'; - - // toggle topbar tool groups - document.getElementById('ctrl-dyn-sb').style.display = isSandbox ? 'contents' : 'none'; - document.getElementById('ctrl-dyn-nw').style.display = isSandbox ? 'none' : 'contents'; - - if (isSandbox) { - // stop newton, start sandbox - if (newtonSim) newtonSim.stop(); - if (sandboxSim) { sandboxSim.fit(); sandboxSim.start(); } - _sbUpdateUI(sandboxSim ? sandboxSim.info() : null); - } else { - // stop sandbox, switch newton law - if (sandboxSim) sandboxSim.stop(); - const lawN = mode === 'law1' ? 1 : mode === 'law2' ? 2 : 3; - if (newtonSim) { - newtonSim.setLaw(lawN); - newtonSim.fit(); - newtonSim.start(); - _newtonSyncUI(); - _newtonUpdateUI(newtonSim.info()); - } - } - } - - function dynPause() { - if (_dynMode === 'sandbox') { - if (sandboxSim) sandboxSim.togglePause(); - } else { - if (newtonSim) newtonSim.togglePause(); - } - } - - function dynReset() { - if (_dynMode === 'sandbox') { - sbReset(); - } else { - _resetNewtonScene(); - } - } - - const _NEWTON_SCENES = { - 1: { - A: { desc: 'Закон инерции: тело скользит по поверхности. Нажми на canvas — толкни блок.', action: null }, - B: { desc: 'Инерция в орбите: шар вращается на нити. Отруби нить — полетит по касательной!', action: ' Отрубить нить' }, - C: { desc: 'Инерция в космосе: тело движется равномерно, нет сил — нет ускорения.', action: null }, - }, - 2: { - A: { desc: 'Второй закон: F = ma. Прикладывай силу и следи за ускорением и скоростью.', action: ' Запустить' }, - B: { desc: 'Два тела, разные массы — одинаковая сила. Сравни ускорения!', action: ' Запустить' }, - C: { desc: 'Второй закон: изменяй силу и массу ползунками, наблюдай в реальном времени.', action: ' Запустить' }, - }, - 3: { - A: { desc: 'Третий закон: пушка выстрелила — отдача. Импульс сохраняется!', action: 'Выстрел' }, - B: { desc: 'Третий закон: два шара сталкиваются — силы равны и противоположны.', action: ' Столкнуть' }, - C: { desc: 'Реактивное движение: ракета выбрасывает газ — летит в обратную сторону.', action: 'Двигатель' }, - }, - }; - - const _NEWTON_PRESETS = { - 1: [ - { label: 'Космос', fn: 'space' }, - { label: 'Лёд', fn: 'ice' }, - { label: 'Асфальт', fn: 'asphalt' }, - { label: 'Резина', fn: 'rubber' }, - ], - 2: [ - { label: 'Лёгкий', fn: 'light' }, - { label: 'Тяжёлый', fn: 'heavy' }, - { label: 'Сравнить', fn: 'compare' }, - ], - 3: [ - { label: 'Большая пушка', fn: 'big_cannon' }, - { label: 'Маленькая', fn: 'small_cannon' }, - { label: 'Равные шары', fn: 'equal_balls' }, - ], - }; - - // _openNewton is now handled by _openDynamics + dynMode - - // newtonLaw is now handled by dynMode('law1'/'law2'/'law3') - - function newtonScene(s, topBtn, panelBtn) { - if (!newtonSim) return; - newtonSim.setScene(s); - document.querySelectorAll('.nscene-btn').forEach(b => { - b.classList.toggle('active', b.id === 'nscn-' + s || b.id === 'nscn-panel-' + s); - }); - _newtonSyncUI(); - _newtonUpdateUI(newtonSim.info()); - } - - function _newtonSyncUI() { - if (!newtonSim) return; - const law = newtonSim.law; - const scene = newtonSim.scene; - const sceneData = (_NEWTON_SCENES[law] || {})[scene] || {}; - - // description - const desc = document.getElementById('newton-scene-desc'); - if (desc) desc.textContent = sceneData.desc || ''; - - // action button label - const lbl = sceneData.action || (law === 1 ? ' Нить' : ' Действие'); - document.getElementById('newton-action-label').textContent = lbl; - document.getElementById('newton-action-top').textContent = lbl; - - // show/hide sliders - document.getElementById('newton-mu-block').style.display = law === 1 && scene === 'A' ? '' : 'none'; - document.getElementById('newton-mass1-block').style.display = (law === 2 || law === 3) ? '' : 'none'; - document.getElementById('newton-mass2-block').style.display = law === 3 ? '' : 'none'; - document.getElementById('newton-force-block').style.display = law === 2 ? '' : 'none'; - - // sync slider values from sim - document.getElementById('sl-newton-mu').value = newtonSim.mu; - document.getElementById('newton-mu-val').textContent = newtonSim.mu.toFixed(2); - document.getElementById('sl-newton-m1').value = newtonSim.mass1; - document.getElementById('newton-m1-val').textContent = newtonSim.mass1 + ' кг'; - document.getElementById('sl-newton-m2').value = newtonSim.mass2; - document.getElementById('newton-m2-val').textContent = newtonSim.mass2 + ' кг'; - document.getElementById('sl-newton-F').value = newtonSim.force; - document.getElementById('newton-F-val').textContent = newtonSim.force + ' Н'; - - // sync scene highlight buttons in both topbar and panel - ['A','B','C'].forEach(s => { - const tb = document.getElementById('nscn-' + s); - const pb = document.getElementById('nscn-panel-' + s); - const on = s === scene; - if (tb) tb.classList.toggle('active', on); - if (pb) pb.classList.toggle('active', on); - }); - - // presets - const presetsEl = document.getElementById('newton-presets'); - const presets = _NEWTON_PRESETS[law] || []; - presetsEl.innerHTML = presets.map(p => - `` - ).join(''); - - // scene B/C visibility for law I (B = orbital, C = space — but law I only has A,B) - // scene C doesn't exist for law I/II panel scene picker visibility - const cBtn = document.getElementById('nscn-panel-C'); - const cTopBtn = document.getElementById('nscn-C'); - const showC = law === 3; - if (cBtn) cBtn.style.display = showC ? '' : 'none'; - if (cTopBtn) cTopBtn.style.display = showC ? '' : 'none'; - const bBtn = document.getElementById('nscn-panel-B'); - const bTopBtn = document.getElementById('nscn-B'); - const showB = law !== 2 || true; // law 2 has compare scene B - if (bBtn) bBtn.style.display = ''; - if (bTopBtn) bTopBtn.style.display = ''; - } - - function newtonAction() { - if (!newtonSim) return; - const law = newtonSim.law; - const scene = newtonSim.scene; - if (law === 1 && scene === 'B') newtonSim.cutString(); - else if (law === 2) newtonSim.startL2(); - else if (law === 3 && scene === 'A') newtonSim.fireCannon(); - else if (law === 3 && scene === 'B') newtonSim._reset3B ? newtonSim._reset3B() : null; - else if (law === 3 && scene === 'C') newtonSim.toggleRocket(); - _newtonUpdateUI(newtonSim.info()); - } - - function _resetNewtonScene() { - if (!newtonSim) return; - const law = newtonSim.law; - const scene = newtonSim.scene; - if (law === 1 && scene === 'A') newtonSim.preset('ice'); - else if (law === 1) newtonSim.setScene(scene); - else if (law === 2) newtonSim.resetL2 ? newtonSim.resetL2() : newtonSim.setScene(scene); - else newtonSim.setScene(scene); - _newtonUpdateUI(newtonSim.info()); - } - - function newtonMuChange() { - const v = +document.getElementById('sl-newton-mu').value; - document.getElementById('newton-mu-val').textContent = v.toFixed(2); - if (newtonSim) newtonSim.setMu(v); - } - - function newtonMass1Change() { - const v = +document.getElementById('sl-newton-m1').value; - document.getElementById('newton-m1-val').textContent = v + ' кг'; - if (newtonSim) newtonSim.setMass1(v); - } - - function newtonMass2Change() { - const v = +document.getElementById('sl-newton-m2').value; - document.getElementById('newton-m2-val').textContent = v + ' кг'; - if (newtonSim) newtonSim.setMass2(v); - } - - function newtonForceChange() { - const v = +document.getElementById('sl-newton-F').value; - document.getElementById('newton-F-val').textContent = v + ' Н'; - if (newtonSim) newtonSim.setForce(v); - } - - function newtonPreset(name) { - if (!newtonSim) return; - newtonSim.preset(name); - _newtonSyncUI(); - _newtonUpdateUI(newtonSim.info()); - } - - function _newtonUpdateUI(info) { - if (!info) return; - const law = info.law; - const scene = info.scene; - - if (law === 1 && scene === 'A') { - document.getElementById('dbar-l1').textContent = 'Закон I-A'; - document.getElementById('dbar-v1').textContent = 'Скольжение'; - document.getElementById('dbar-l2').textContent = 'Скорость'; - document.getElementById('dbar-v2').textContent = info.v + ' м/с'; - document.getElementById('dbar-l3').textContent = 'Сила трения'; - document.getElementById('dbar-v3').textContent = info.fFr + ' Н'; - document.getElementById('dbar-l4').textContent = 'Масса'; - document.getElementById('dbar-v4').textContent = info.m + ' кг'; - document.getElementById('dbar-l5').textContent = 'μ'; - document.getElementById('dbar-v5').textContent = info.mu; - } else if (law === 1) { - document.getElementById('dbar-l1').textContent = 'Закон I-B'; - document.getElementById('dbar-v1').textContent = info.cut ? 'Нить срублена' : 'Вращение'; - document.getElementById('dbar-l2').textContent = 'Скорость'; - document.getElementById('dbar-v2').textContent = info.v + ' м/с'; - document.getElementById('dbar-l3').textContent = ''; - document.getElementById('dbar-v3').textContent = '—'; - document.getElementById('dbar-l4').textContent = ''; - document.getElementById('dbar-v4').textContent = '—'; - document.getElementById('dbar-l5').textContent = ''; - document.getElementById('dbar-v5').textContent = '—'; - } else if (law === 2) { - document.getElementById('dbar-l1').textContent = 'Закон II'; - document.getElementById('dbar-v1').textContent = 'F = ma'; - document.getElementById('dbar-l2').textContent = 'Сила F'; - document.getElementById('dbar-v2').textContent = info.F + ' Н'; - document.getElementById('dbar-l3').textContent = 'Масса m'; - document.getElementById('dbar-v3').textContent = info.m + ' кг'; - document.getElementById('dbar-l4').textContent = 'Ускор. a'; - document.getElementById('dbar-v4').textContent = info.a + ' м/с²'; - document.getElementById('dbar-l5').textContent = 'Скорость'; - document.getElementById('dbar-v5').textContent = info.v + ' м/с'; - } else if (scene === 'A') { - document.getElementById('dbar-l1').textContent = 'Закон III-A'; - document.getElementById('dbar-v1').textContent = 'Пушка'; - document.getElementById('dbar-l2').textContent = 'v снаряда'; - document.getElementById('dbar-v2').textContent = info.vBall !== '—' ? info.vBall + ' м/с' : '—'; - document.getElementById('dbar-l3').textContent = 'v пушки'; - document.getElementById('dbar-v3').textContent = info.vCannon + ' м/с'; - document.getElementById('dbar-l4').textContent = 'm снаряда'; - document.getElementById('dbar-v4').textContent = info.m1 + ' кг'; - document.getElementById('dbar-l5').textContent = 'm пушки'; - document.getElementById('dbar-v5').textContent = info.m2 + ' кг'; - } else if (scene === 'B') { - document.getElementById('dbar-l1').textContent = 'Закон III-B'; - document.getElementById('dbar-v1').textContent = 'Удар'; - document.getElementById('dbar-l2').textContent = 'p₁'; - document.getElementById('dbar-v2').textContent = info.p1 + ' кг·м/с'; - document.getElementById('dbar-l3').textContent = 'p₂'; - document.getElementById('dbar-v3').textContent = info.p2 + ' кг·м/с'; - document.getElementById('dbar-l4').textContent = 'p суммарный'; - document.getElementById('dbar-v4').textContent = info.pt + ' кг·м/с'; - document.getElementById('dbar-l5').textContent = ''; - document.getElementById('dbar-v5').textContent = '—'; - } else { - document.getElementById('dbar-l1').textContent = 'Закон III-C'; - document.getElementById('dbar-v1').textContent = 'Ракета'; - document.getElementById('dbar-l2').textContent = 'Ускорение'; - document.getElementById('dbar-v2').textContent = info.a + ' м/с²'; - document.getElementById('dbar-l3').textContent = 'Скорость'; - document.getElementById('dbar-v3').textContent = info.v + ' м/с'; - document.getElementById('dbar-l4').textContent = 'Масса'; - document.getElementById('dbar-v4').textContent = info.m + ' кг'; - document.getElementById('dbar-l5').textContent = 'Топливо'; - document.getElementById('dbar-v5').textContent = info.fuel + '%'; - } - } - - // _openSandbox is now handled by _openDynamics + dynMode - - function sbTool(t, btn) { - if (!sandboxSim) return; - sandboxSim.tool = t; - sandboxSim._springStart = null; - sandboxSim._ropeStart = null; - document.querySelectorAll('.sb-tool-btn').forEach(b => b.classList.toggle('active', b.id === 'sbt-' + t)); - document.querySelectorAll('.sb-panel-tool').forEach(b => b.classList.toggle('active', b.id === 'sbpt-' + t)); - const canvas = document.getElementById('sandbox-canvas'); - canvas.style.cursor = t === 'erase' ? 'not-allowed' - : (t === 'spring' || t === 'rope') ? 'cell' - : t === 'anchor' ? 'copy' - : 'crosshair'; - document.getElementById('sb-spring-block').style.display = t === 'spring' ? '' : 'none'; - } - - function sbSpringKChange() { - const v = +document.getElementById('sl-sb-springk').value; - document.getElementById('sb-springk-val').textContent = v + ' Н/м'; - if (sandboxSim) sandboxSim.newSpringK = v; - } - - function sbForceMode(m, btn) { - if (!sandboxSim) return; - sandboxSim.forceMode = m; - document.querySelectorAll('.sb-fmode').forEach(b => b.classList.toggle('active', b.id === 'sbfm-' + m)); - } - - function sbMassChange() { - const v = +document.getElementById('sl-sb-mass').value; - document.getElementById('sb-mass-val').textContent = v + ' кг'; - if (sandboxSim) sandboxSim.newMass = v; - } - - function sbRestChange() { - const v = +document.getElementById('sl-sb-rest').value; - document.getElementById('sb-rest-val').textContent = v.toFixed(2); - if (sandboxSim) sandboxSim.newRestitution = v; - } - - function sbFloorMuChange() { - const v = +document.getElementById('sl-sb-floormu').value; - document.getElementById('sb-floormu-val').textContent = v.toFixed(2); - if (sandboxSim) sandboxSim.floorMu = v; - } - - function sbWorldToggle() { - if (!sandboxSim) return; - sandboxSim.gravity = document.getElementById('sb-gravity').checked; - sandboxSim.hasFloor = document.getElementById('sb-floor').checked; - sandboxSim.hasWalls = document.getElementById('sb-walls').checked; - sandboxSim.airDrag = document.getElementById('sb-airdrag').checked; - } - - function sbRampToggle() { - if (!sandboxSim) return; - const on = document.getElementById('sb-ramp').checked; - sandboxSim.setRamp(on); - document.getElementById('sb-ramp-block').style.display = on ? '' : 'none'; - } - - function sbAngleChange() { - const v = +document.getElementById('sl-sb-angle').value; - document.getElementById('sb-angle-val').textContent = v + '°'; - if (sandboxSim) sandboxSim.setRampAngle(v); - } - - function sbRampMuChange() { - const v = +document.getElementById('sl-sb-rampmu').value; - document.getElementById('sb-rampmu-val').textContent = v.toFixed(2); - if (sandboxSim) sandboxSim.setRampMu(v); - } - - function sbDecompToggle() { - if (!sandboxSim) return; - sandboxSim.showDecomp = document.getElementById('sb-decomp').checked; - } - - function sbDisplayToggle() { - if (!sandboxSim) return; - sandboxSim.showForces = document.getElementById('sb-forces').checked; - sandboxSim.showVelocity = document.getElementById('sb-vel').checked; - sandboxSim.showFBD = document.getElementById('sb-fbd').checked; - sandboxSim.showEnergy = document.getElementById('sb-energy').checked; - sandboxSim.showTrail = document.getElementById('sb-trail').checked; - } - - function sbTimeScale(v, btn) { - if (!sandboxSim) return; - sandboxSim.timeScale = v; - document.querySelectorAll('.sb-time').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - } - - function sbPreset(name) { - if (!sandboxSim) return; - sandboxSim.preset(name); - // sync world checkboxes - document.getElementById('sb-gravity').checked = sandboxSim.gravity; - document.getElementById('sb-floor').checked = sandboxSim.hasFloor; - document.getElementById('sb-walls').checked = sandboxSim.hasWalls; - document.getElementById('sb-airdrag').checked = sandboxSim.airDrag; - document.getElementById('sl-sb-floormu').value = sandboxSim.floorMu; - document.getElementById('sb-floormu-val').textContent = sandboxSim.floorMu.toFixed(2); - // sync ramp - document.getElementById('sb-ramp').checked = sandboxSim.ramp; - document.getElementById('sb-ramp-block').style.display = sandboxSim.ramp ? '' : 'none'; - document.getElementById('sl-sb-angle').value = sandboxSim.rampAngle; - document.getElementById('sb-angle-val').textContent = sandboxSim.rampAngle + '°'; - document.getElementById('sl-sb-rampmu').value = sandboxSim.rampMu; - document.getElementById('sb-rampmu-val').textContent = sandboxSim.rampMu.toFixed(2); - _sbUpdateUI(sandboxSim.info()); - } - - function sbReset() { - if (!sandboxSim) return; - sandboxSim.reset(); - _sbUpdateUI(sandboxSim.info()); - } - - function _sbUpdateUI(info) { - if (!info) return; - document.getElementById('dbar-l1').textContent = 'Тел / связей'; - document.getElementById('dbar-v1').textContent = info.bodies + ' / ' + (info.springs + info.ropes); - document.getElementById('dbar-l2').textContent = 'KE (Дж)'; - document.getElementById('dbar-v2').textContent = info.KE; - document.getElementById('dbar-l3').textContent = 'PE (Дж)'; - document.getElementById('dbar-v3').textContent = info.PE; - document.getElementById('dbar-l4').textContent = 'ΣF'; - document.getElementById('dbar-v4').textContent = info.netF; - document.getElementById('dbar-l5').textContent = 'Время'; - document.getElementById('dbar-v5').textContent = info.time + ' с'; - } - - /* ── chem sandbox ── */ - - function _openChemSandbox() { - document.getElementById('sim-topbar-title').textContent = 'Химическая песочница'; - _simShow('sim-chemsandbox'); - _simShow('ctrl-chemsandbox'); - - requestAnimationFrame(() => requestAnimationFrame(() => { - const c = document.getElementById('chemsandbox-canvas'); - if (!chemSandSim) { - chemSandSim = new ChemSandboxSim(c); - chemSandSim.onUpdate = _chemSandUpdateUI; - chemSandSim.onQuizUpdate = _chemSandQuizUI; - c.addEventListener('click', e => chemSandSim.handleClick(e)); - c.addEventListener('mousedown', e => chemSandSim.handleMouseDown(e)); - c.addEventListener('mousemove', e => chemSandSim.handleMouseMove(e)); - c.addEventListener('mouseup', e => chemSandSim.handleMouseUp(e)); - c.addEventListener('wheel', e => chemSandSim.handleWheel(e), { passive: false }); - c.addEventListener('contextmenu', e => chemSandSim.handleContextMenu(e)); - _addTouchSupport(c, chemSandSim); - _chemSandBuildReagents('all'); - } - chemSandSim.fit(); - chemSandSim.start(); - chemSandSim.draw(); - })); - } - - function chemSandCat(cat, el) { - document.querySelectorAll('.chemsand-cat').forEach(b => b.classList.remove('active')); - el.classList.add('active'); - if (chemSandSim) chemSandSim.setCategory(cat); - _chemSandBuildReagents(cat); - if (chemSandSim) chemSandSim.draw(); - } - - function chemSandPreset(name) { if (chemSandSim) { chemSandSim.preset(name); _chemSandBuildReagents(chemSandSim.filterCat); } } - function chemSandReset() { if (chemSandSim) { chemSandSim.reset(); _chemSandBuildReagents(chemSandSim.filterCat); } } - function chemSandResetReaction() { if (chemSandSim) { chemSandSim.resetReaction(); _chemSandBuildReagents(chemSandSim.filterCat); } } - - function chemSandConcChange() { - const v = +document.getElementById('sl-csand-conc').value; - document.getElementById('csand-conc-val').textContent = v + '%'; - } - function chemSandTempChange() { - const v = +document.getElementById('sl-csand-temp').value; - document.getElementById('csand-temp-val').textContent = v + '°C'; - } - - function chemSandAdd(formula) { - if (!chemSandSim) return; - // toggle: if already in mix — remove, else add - if (chemSandSim.mixContents.includes(formula)) { - chemSandSim.removeFromMix(formula); - } else { - chemSandSim.addToMix(formula); - } - _chemSandBuildReagents(chemSandSim.filterCat); - } - - function _chemSandBuildReagents(cat) { - const box = document.getElementById('chemsand-reagents'); - if (!box) return; - const subs = ChemSandboxSim.SUBSTANCES; - const keys = Object.keys(subs).filter(k => cat === 'all' || subs[k].cat === cat); - const inMix = chemSandSim ? chemSandSim.mixContents : []; - box.innerHTML = keys.map(k => { - const s = subs[k]; - const active = inMix.includes(k); - const cls = active ? 'proj-preset-chip reac-mode-btn active' : 'proj-preset-chip reac-mode-btn'; - const sf = chemSandSim ? chemSandSim._shortFormula(k) : k; - const removeHint = active ? ' (клик — убрать)' : ''; - return ``; - }).join(''); - } - - function chemSandSetMode(mode, el) { - document.querySelectorAll('.chemsand-mode').forEach(b => b.classList.remove('active')); - if (el) el.classList.add('active'); - if (!chemSandSim) return; - if (mode === 'quiz') { - if (window._simQuizAllowed === false) { - LS.toast('Режим заданий недоступен — администратор ограничил доступ', 'error'); - // revert button state - document.querySelectorAll('.chemsand-mode').forEach(b => b.classList.remove('active')); - document.getElementById('csand-mode-free')?.classList.add('active'); - return; - } - chemSandSim.startQuiz(); - // reset category filter to 'all' so all reagents are accessible - document.querySelectorAll('.chemsand-cat').forEach(b => b.classList.remove('active')); - const allBtn = document.querySelector('.chemsand-cat'); - if (allBtn) allBtn.classList.add('active'); - _chemSandBuildReagents('all'); - } else { - chemSandSim.stopQuiz(); - document.getElementById('csand-quiz-question').style.display = 'none'; - document.getElementById('csand-quiz-result').style.display = 'none'; - document.getElementById('csand-quiz-next').style.display = 'none'; - document.getElementById('csand-quiz-score').textContent = ''; - } - } - - function chemSandQuizNext() { - if (chemSandSim && chemSandSim._quizMode) { - chemSandSim._nextQuizTask(); - _chemSandBuildReagents(chemSandSim.filterCat); - } - } - - function _chemSandQuizUI(qi) { - const qEl = document.getElementById('csand-quiz-question'); - const rEl = document.getElementById('csand-quiz-result'); - const nEl = document.getElementById('csand-quiz-next'); - const sEl = document.getElementById('csand-quiz-score'); - if (!qi.active) { - qEl.style.display = 'none'; rEl.style.display = 'none'; nEl.style.display = 'none'; - sEl.textContent = ''; - return; - } - qEl.style.display = 'block'; - qEl.textContent = qi.question || ''; - sEl.textContent = qi.total > 0 ? `${qi.score}/${qi.total}` : ''; - if (qi.result) { - rEl.style.display = 'block'; - rEl.style.color = qi.result === 'correct' ? '#7BF5A4' : '#EF476F'; - rEl.textContent = qi.result === 'correct' ? 'Верно!' : 'Неверно — ' + (qi.answer || ''); - nEl.style.display = qi.result === 'wrong' ? 'inline-block' : 'none'; - } else { - rEl.style.display = 'none'; nEl.style.display = 'none'; - } - } - - let _lastReportedEquation = null; - function _chemSandUpdateUI(info) { - document.getElementById('csbar-v1').textContent = info.mixed; - document.getElementById('csbar-v3').textContent = info.type || '—'; - const eqEl = document.getElementById('csbar-v4'); - eqEl.innerHTML = info.equation || '—'; - eqEl.title = (info.equation || '').replace(/<[^>]*>/g, ''); - document.getElementById('csbar-v5').textContent = info.products || '—'; - const ionEl = document.getElementById('csbar-v6'); - ionEl.innerHTML = info.ionNet || '—'; - ionEl.title = (info.ionNet || '').replace(/<[^>]*>/g, ''); - // rebuild reagent buttons to reflect active state - _chemSandBuildReagents(chemSandSim ? chemSandSim.filterCat : 'all'); - // Report lab activity for gamification (once per unique reaction) - if (info.reaction && info.equation && info.equation !== _lastReportedEquation) { - _lastReportedEquation = info.equation; - if (window.LS?.reportLabActivity) LS.reportLabActivity(1).catch(() => {}); - } - } - - /* ── Cell Division ── */ - function _openCellDivision(mode) { - document.getElementById('sim-topbar-title').textContent = 'Деление клетки'; - _simShow('sim-celldivision'); - _simShow('ctrl-celldivision'); - requestAnimationFrame(() => requestAnimationFrame(() => { - const canvas = document.getElementById('celldiv-canvas'); - if (!cellDivSim) { - cellDivSim = new CellDivisionSim(canvas); - cellDivSim.onUpdate = _cdUpdateUI; - } - cellDivSim.fit(); - cellDivSim.setMode(mode || 'mitosis'); - cellDivSim.start(); - _cdBuildDots(cellDivSim._phaseIdx); - // sync auto button state - const autoBtn = document.getElementById('cd-auto-btn'); - if (autoBtn) { autoBtn.innerHTML = cellDivSim._autoPlay ? ' Пауза' : ' Авто'; } - _cdUpdateUI(cellDivSim.info()); - })); - } - - function _cdBuildDots(activeIdx) { - const box = document.getElementById('cd-phase-dots'); - if (!box || !cellDivSim) return; - const phases = cellDivSim._phases(); - box.innerHTML = phases.map((p, i) => - `
` - ).join(''); - } - - function cdSetMode(mode, btn) { - document.querySelectorAll('.cd-mode-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (!cellDivSim) return; - cellDivSim.setMode(mode); - _cdBuildDots(cellDivSim._phaseIdx); - _cdUpdateUI(cellDivSim.info()); - } - - function cdAutoPlay(btn) { - if (!cellDivSim) return; - cellDivSim.toggleAutoPlay(); - btn.classList.toggle('active', cellDivSim._autoPlay); - btn.innerHTML = cellDivSim._autoPlay ? ' Пауза' : ' Авто'; - } - - function cdPrevPhase() { - if (!cellDivSim) return; - cellDivSim.prevPhase(); - _cdBuildDots(cellDivSim._phaseIdx); - } - - function cdNextPhase() { - if (!cellDivSim) return; - cellDivSim.nextPhase(); - _cdBuildDots(cellDivSim._phaseIdx); - } - - function cdJumpPhase(idx) { - if (!cellDivSim) return; - cellDivSim.jumpToPhase(idx); - _cdBuildDots(idx); - } - - function _cdUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('cdbar-v1', info.phase || '—'); - v('cdbar-v2', info.chromN || '—'); - v('cdbar-v3', info.dna || '—'); - v('cdbar-v4', (info.index + 1) + ' / ' + info.total); - v('cdbar-v5', info.mode === 'mitosis' ? 'Митоз' : 'Мейоз'); - _cdBuildDots(info.index); - } - - /* ── Photosynthesis / Respiration ── */ - function _openPhotosynthesis(mode) { - document.getElementById('sim-topbar-title').textContent = 'Фотосинтез и дыхание'; - _simShow('sim-photosynthesis'); - _simShow('ctrl-photosynthesis'); - requestAnimationFrame(() => requestAnimationFrame(() => { - const canvas = document.getElementById('photosyn-canvas'); - if (!photosynSim) { - photosynSim = new PhotosynthesisSim(canvas); - photosynSim.onUpdate = _psUpdateUI; - } - photosynSim.fit(); - photosynSim.setMode(mode || 'photo'); - photosynSim.start(); - })); - } - - function psSetMode(mode, btn) { - document.querySelectorAll('.ps-mode-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (photosynSim) photosynSim.setMode(mode); - } - - function psLightChange() { - const v = +document.getElementById('sl-ps-light').value; - document.getElementById('ps-light-val').textContent = v + '%'; - if (photosynSim) photosynSim.setLightIntensity(v); - } - - function psCO2Change() { - const v = +document.getElementById('sl-ps-co2').value; - document.getElementById('ps-co2-val').textContent = v + '%'; - if (photosynSim) photosynSim.setCO2(v); - } - - function psReset() { - if (photosynSim) photosynSim.reset(); - } - - function _psUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('psbar-v1', info.atpRate || '0'); - v('psbar-v2', info.o2 || '0'); - v('psbar-v3', info.co2 || '0'); - v('psbar-v4', info.efficiency ? info.efficiency + '%' : '—'); - v('psbar-v5', info.mode === 'photo' ? 'Фотосинтез' : 'Дыхание'); - } - - /* ── Angry Birds ── */ - var angryBirdsSim = null; - - function _openAngryBirds() { - document.getElementById('sim-topbar-title').textContent = 'Angry Birds Physics'; - _simShow('sim-angrybirds'); - _simShow('ctrl-angrybirds'); - requestAnimationFrame(() => requestAnimationFrame(() => { - const c = document.getElementById('angrybirds-canvas'); - if (!angryBirdsSim) { - angryBirdsSim = new AngryBirdsSim(c); - angryBirdsSim.onUpdate = _abUpdateUI; - c.addEventListener('mousedown', e => angryBirdsSim.handleMouseDown(e)); - c.addEventListener('mousemove', e => angryBirdsSim.handleMouseMove(e)); - c.addEventListener('mouseup', e => angryBirdsSim.handleMouseUp(e)); - c.addEventListener('mouseleave', e => angryBirdsSim.handleMouseUp(e)); - _addTouchSupport(c, angryBirdsSim); - } - angryBirdsSim.fit(); - angryBirdsSim.start(); - })); - } - - function abLevel(n, btn) { - document.querySelectorAll('.ab-lvl-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (angryBirdsSim) angryBirdsSim.loadLevel(n); - } - - function angryBirdsRestart() { - if (angryBirdsSim) angryBirdsSim.restart(); - } - - function _abUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('abbar-v1', info.level); - v('abbar-v2', info.birds); - v('abbar-v3', info.pigs); - v('abbar-v4', info.score.toLocaleString('ru')); - v('abbar-v5', info.planet); - /* sync level button highlight */ - document.querySelectorAll('.ab-lvl-btn').forEach((b, i) => { - b.classList.toggle('active', i === (info.level - 1)); - }); - } - - /* ── quadratic ── */ - - function _openQuadratic() { - document.getElementById('sim-topbar-title').textContent = 'Корни квадратного уравнения'; - _simShow('sim-quadratic'); - _registerSimState('quadratic', () => quadSim?.getParams(), st => quadSim?.setParams(st)); - if (_embedMode) _startStateEmit('quadratic'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!quadSim) { - quadSim = new QuadraticSim(document.getElementById('quadratic-canvas')); - quadSim.onUpdate = _quadUpdateUI; - } - quadSim.fit(); - quadSim.draw(); - quadSim._emit(); - })); - } - - function quadParam(name, val) { - const v = parseFloat(val); - document.getElementById('quad-' + name + '-val').textContent = v % 1 === 0 ? v : v.toFixed(1); - if (quadSim) quadSim.setParams({ [name]: v }); - } - - function quadPreset(a, b, c) { - document.getElementById('sl-quad-a').value = a; document.getElementById('quad-a-val').textContent = a; - document.getElementById('sl-quad-b').value = b; document.getElementById('quad-b-val').textContent = b; - document.getElementById('sl-quad-c').value = c; document.getElementById('quad-c-val').textContent = c; - if (quadSim) quadSim.setParams({ a, b, c }); - } - - function _quadUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('qbar-v1', 'D = ' + info.D); - v('qbar-v2', info.roots); - v('qbar-v3', info.vertex); - v('qbar-v4', info.equation); - } - - /* ── normal distribution ── */ - var ndSim = null; - - function _openNormalDist() { - document.getElementById('sim-topbar-title').textContent = 'Нормальное распределение'; - _simShow('sim-normaldist'); - _registerSimState('normaldist', () => ndSim?.getParams(), st => ndSim?.setParams(st)); - if (_embedMode) _startStateEmit('normaldist'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!ndSim) { - ndSim = new NormalDistSim(document.getElementById('normaldist-canvas')); - ndSim.onUpdate = _ndUpdateUI; - } - ndSim.fit(); - ndSim.draw(); - ndSim._emit(); - })); - } - - function ndParam(name, val) { - const v = parseFloat(val); - const elId = name === 'mu' ? 'nd-mu-val' : 'nd-sigma-val'; - document.getElementById(elId).textContent = v % 1 === 0 ? v : v.toFixed(1); - if (ndSim) ndSim.setParams({ [name]: v }); - } - - function ndShade(mode, btn) { - document.querySelectorAll('.nd-shade-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (ndSim) ndSim.setParams({ shade: mode }); - } - - function ndPreset(mu, sigma) { - document.getElementById('sl-nd-mu').value = mu; document.getElementById('nd-mu-val').textContent = mu; - document.getElementById('sl-nd-sigma').value = sigma; document.getElementById('nd-sigma-val').textContent = sigma; - if (ndSim) ndSim.setParams({ mu, sigma }); - } - - function _ndUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('ndbar-v1', info.mu); - v('ndbar-v2', info.sigma); - v('ndbar-v3', info.peak); - v('ndbar-v4', info.area); - } - - /* ── graph transform ── */ - var gtSim = null; - - function _openGraphTransform() { - document.getElementById('sim-topbar-title').textContent = 'Трансформации графиков'; - _simShow('sim-graphtransform'); - _registerSimState('graphtransform', () => gtSim?.getParams(), st => gtSim?.setParams(st)); - if (_embedMode) _startStateEmit('graphtransform'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!gtSim) { - gtSim = new GraphTransformSim(document.getElementById('graphtransform-canvas')); - gtSim.onUpdate = _gtUpdateUI; - } - gtSim.fit(); - gtSim.draw(); - gtSim._emit(); - })); - } - - function gtParam(name, val) { - const v = parseFloat(val); - document.getElementById('gt-' + name + '-val').textContent = v % 1 === 0 ? v : v.toFixed(1); - if (gtSim) gtSim.setParams({ [name]: v }); - } - - function gtBase(name, btn) { - document.querySelectorAll('.gt-base-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (gtSim) gtSim.setBase(name); - } - - function gtEffect(a, k, b, c) { - document.getElementById('sl-gt-a').value = a; document.getElementById('gt-a-val').textContent = a; - document.getElementById('sl-gt-k').value = k; document.getElementById('gt-k-val').textContent = k; - document.getElementById('sl-gt-b').value = b; document.getElementById('gt-b-val').textContent = b; - document.getElementById('sl-gt-c').value = c; document.getElementById('gt-c-val').textContent = c; - if (gtSim) gtSim.setParams({ a, k, b, c }); - } - - function _gtUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('gtbar-v1', info.base); - v('gtbar-v2', info.a); - v('gtbar-v3', info.k); - v('gtbar-v4', info.b); - v('gtbar-v5', info.c); - } - - /* ── pendulum ── */ - var pendSim = null; - - function _openPendulum() { - document.getElementById('sim-topbar-title').textContent = 'Маятник'; - _simShow('sim-pendulum'); - _registerSimState('pendulum', () => pendSim?.getParams(), st => pendSim?.setParams(st)); - if (_embedMode) _startStateEmit('pendulum'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!pendSim) { - pendSim = new PendulumSim(document.getElementById('pendulum-canvas')); - pendSim.onUpdate = _pendUpdateUI; - } - pendSim.fit(); - pendSim.play(); - })); - } - - function pendParam(name, val) { - const v = parseFloat(val); - const ids = { theta: 'pend-theta-val', L: 'pend-L-val', g: 'pend-g-val', damping: 'pend-damp-val' }; - const el = document.getElementById(ids[name]); - if (el) el.textContent = v % 1 === 0 ? v : v.toFixed(name === 'g' ? 2 : 1); - if (pendSim) pendSim.setParams({ [name]: v }); - } - - function pendPreset(theta, L, g, damp) { - document.getElementById('sl-pend-theta').value = theta; document.getElementById('pend-theta-val').textContent = theta; - document.getElementById('sl-pend-L').value = L; document.getElementById('pend-L-val').textContent = L; - document.getElementById('sl-pend-g').value = g; document.getElementById('pend-g-val').textContent = g; - document.getElementById('sl-pend-damp').value = damp; document.getElementById('pend-damp-val').textContent = damp; - if (pendSim) { - pendSim.setParams({ theta, L, g, damping: damp }); - pendSim.play(); - } - } - - function _pendUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('pendbar-v1', info.angle); - v('pendbar-v2', info.omega); - v('pendbar-v3', info.period); - v('pendbar-v4', info.energy); - } - - /* ── equilibrium ── */ - - function _openEquilibrium() { - document.getElementById('sim-topbar-title').textContent = 'Химическое равновесие'; - _simShow('sim-equilibrium'); - _registerSimState('equilibrium', () => eqSim?.getParams(), st => eqSim?.setParams(st)); - if (_embedMode) _startStateEmit('equilibrium'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!eqSim) { - eqSim = new EquilibriumSim(document.getElementById('equilibrium-canvas')); - eqSim.onUpdate = _eqUpdateUI; - } - eqSim.fit(); - eqSim.reset(); - eqSim.play(); - })); - } - - function eqParam(name, val) { - const v = parseFloat(val); - const ids = { T: 'eq-T-val', Ea_f: 'eq-Eaf-val', Ea_r: 'eq-Ear-val' }; - const el = document.getElementById(ids[name]); - if (el) el.textContent = v; - if (eqSim) eqSim.setParams({ [name]: v }); - } - - function eqPreset(name) { - if (eqSim) { eqSim.preset(name); eqSim.play(); } - const defs = { default: [300,50,55], exothermic: [280,35,65], endothermic: [350,65,35], excess_A: [300,50,55] }; - const d = defs[name] || defs.default; - document.getElementById('sl-eq-T').value = d[0]; document.getElementById('eq-T-val').textContent = d[0]; - document.getElementById('sl-eq-Eaf').value = d[1]; document.getElementById('eq-Eaf-val').textContent = d[1]; - document.getElementById('sl-eq-Ear').value = d[2]; document.getElementById('eq-Ear-val').textContent = d[2]; - } - - function _eqUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('eqbar-v1', info.keq); - v('eqbar-v2', info.Q); - v('eqbar-v3', info.direction); - v('eqbar-v4', info.nA + '|' + info.nB + '|' + info.nC + '|' + info.nD); - } - - /* ── thin lens ── */ - - function _openThinLens() { - document.getElementById('sim-topbar-title').textContent = 'Тонкая линза'; - _simShow('sim-thinlens'); - _registerSimState('thinlens', () => lensSim?.getParams(), st => lensSim?.setParams(st)); - if (_embedMode) _startStateEmit('thinlens'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!lensSim) { - lensSim = new ThinLensSim(document.getElementById('thinlens-canvas')); - lensSim.onUpdate = _lensUpdateUI; - } - lensSim.fit(); - lensSim.draw(); - lensSim._emit(); - })); - } - - function lensParam(name, val) { - const v = parseFloat(val); - const ids = { f: 'lens-f-val', d: 'lens-d-val', h: 'lens-h-val' }; - const el = document.getElementById(ids[name]); - if (el) el.textContent = v; - if (lensSim) lensSim.setParams({ [name]: v }); - } - - function lensPreset(f, d, h) { - document.getElementById('sl-lens-f').value = f; document.getElementById('lens-f-val').textContent = f; - document.getElementById('sl-lens-d').value = d; document.getElementById('lens-d-val').textContent = d; - document.getElementById('sl-lens-h').value = h; document.getElementById('lens-h-val').textContent = h; - if (lensSim) lensSim.setParams({ f, d, h }); - } - - function _lensUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('lensbar-v1', info.f); - v('lensbar-v2', info.dPrime === Infinity ? '∞' : info.dPrime); - v('lensbar-v3', info.M === Infinity ? '∞' : info.M); - v('lensbar-v4', info.imageType); - } - - /* ── mirrors ── */ - - var mirrorSim = null; - - function _openMirror() { - document.getElementById('sim-topbar-title').textContent = 'Зеркала'; - _simShow('sim-mirrors'); - _registerSimState('mirrors', () => mirrorSim?.getParams(), st => mirrorSim?.setParams(st)); - if (_embedMode) _startStateEmit('mirrors'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!mirrorSim) { - mirrorSim = new MirrorSim(document.getElementById('mirror-canvas')); - mirrorSim.onUpdate = _mirrorUpdateUI; - mirrorSim.onAnimate = (d) => { - const sl = document.getElementById('sl-mirror-d'); - const lbl = document.getElementById('mirror-d-val'); - if (sl) sl.value = Math.round(d); - if (lbl) lbl.textContent = Math.round(d); - }; - } - mirrorSim.fit(); - mirrorSim.draw(); - mirrorSim._emit(); - if (mirrorSim._showPhotons && !mirrorSim._photonRaf) mirrorSim._startPhotons(); - })); - } - - function mirrorType(type, el) { - document.querySelectorAll('.mirror-type-btn').forEach(b => b.classList.remove('active')); - if (el) el.classList.add('active'); - const fRow = document.getElementById('mirror-f-row'); - if (fRow) fRow.style.display = type === 'flat' ? 'none' : 'flex'; - if (mirrorSim) mirrorSim.setType(type); - const pb = document.getElementById('mirror-play-btn'); - if (pb) { pb.textContent = '▶ Анимация'; } - const sl = document.getElementById('sl-mirror-d'); - if (sl) sl.disabled = false; - } - - function mirrorParam(name, val) { - const v = parseFloat(val); - const ids = { f: 'mirror-f-val', d: 'mirror-d-val', h: 'mirror-h-val' }; - const el = document.getElementById(ids[name]); - if (el) el.textContent = v; - if (mirrorSim) mirrorSim.setParams({ [name]: v }); - } - - function mirrorPreset(name) { - const P = { - flat: { type: 'flat', f: 120, d: 200, h: 60 }, - far: { type: 'concave', f: 100, d: 280, h: 60 }, - '2f': { type: 'concave', f: 100, d: 200, h: 60 }, - between: { type: 'concave', f: 100, d: 140, h: 60 }, - near: { type: 'concave', f: 100, d: 60, h: 60 }, - convex: { type: 'convex', f: 100, d: 200, h: 60 }, - }; - const p = P[name]; if (!p) return; - document.querySelectorAll('.mirror-type-btn').forEach(b => b.classList.remove('active')); - const tb = document.getElementById(`mtype-${p.type}`); - if (tb) tb.classList.add('active'); - const fRow = document.getElementById('mirror-f-row'); - if (fRow) fRow.style.display = p.type === 'flat' ? 'none' : 'flex'; - document.getElementById('sl-mirror-f').value = p.f; document.getElementById('mirror-f-val').textContent = p.f; - document.getElementById('sl-mirror-d').value = p.d; document.getElementById('mirror-d-val').textContent = p.d; - document.getElementById('sl-mirror-h').value = p.h; document.getElementById('mirror-h-val').textContent = p.h; - if (mirrorSim) { mirrorSim.setType(p.type); mirrorSim.setParams({ f: p.f, d: p.d, h: p.h }); } - } - - function mirrorTogglePlay(btn) { - if (!mirrorSim) return; - mirrorSim.togglePlay(); - const playing = mirrorSim._playing; - if (btn) btn.textContent = playing ? '⏸ Стоп' : '▶ Анимация'; - const sl = document.getElementById('sl-mirror-d'); - if (sl) sl.disabled = playing; - } - - function mirrorSetSpeed(val) { if (mirrorSim) mirrorSim.setAnimSpeed(parseFloat(val)); } - function mirrorToggle(name, val) { if (mirrorSim) mirrorSim.setToggle(name, val); } - function mirrorStepNext() { if (mirrorSim) mirrorSim.stepNext(); } - function mirrorStepReset() { if (mirrorSim) mirrorSim.stepReset(); } - function mirrorSetPointMode(val) { if (mirrorSim) mirrorSim.setPointMode(val); } - - function _mirrorUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('mirrorbar-v1', info.f); - v('mirrorbar-v5', Math.round(info.d)); - v('mirrorbar-v2', info.dPrime === Infinity ? '∞' : info.dPrime); - v('mirrorbar-v3', info.M === Infinity ? '∞' : info.M); - v('mirrorbar-v4', info.imageType); - } - - /* ── isoprocesses ── */ - - 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 ── */ - - function _openTitration() { - document.getElementById('sim-topbar-title').textContent = 'pH и кривая титрования'; - _simShow('sim-titration'); - _registerSimState('titration', () => titrSim?.getParams(), st => titrSim?.setParams(st)); - if (_embedMode) _startStateEmit('titration'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!titrSim) { - titrSim = new TitrationSim(document.getElementById('titration-canvas')); - titrSim.onUpdate = _titrUpdateUI; - } - titrSim.fit(); - titrSim.reset(); - titrSim.play(); - })); - } - - function titrParam(name, val) { - const v = parseFloat(val); - const ids = { acidConc: 'titr-ac-val', baseConc: 'titr-bc-val', acidVol: 'titr-vol-val' }; - const el = document.getElementById(ids[name]); - if (el) el.textContent = name === 'acidVol' ? v : v.toFixed(2); - if (titrSim) titrSim.setParams({ [name]: v }); - } - - function titrIndicator(name, btn) { - document.querySelectorAll('.titr-ind-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (titrSim) titrSim.setParams({ indicator: name }); - } - - function titrPreset(name) { - if (titrSim) { titrSim.preset(name); titrSim.play(); } - const defs = { strong_strong: [0.1,0.1,50], weak_strong: [0.1,0.1,50], concentrated: [0.5,0.5,25] }; - const d = defs[name] || defs.strong_strong; - document.getElementById('sl-titr-ac').value = d[0]; document.getElementById('titr-ac-val').textContent = d[0].toFixed(2); - document.getElementById('sl-titr-bc').value = d[1]; document.getElementById('titr-bc-val').textContent = d[1].toFixed(2); - document.getElementById('sl-titr-vol').value = d[2]; document.getElementById('titr-vol-val').textContent = d[2]; - } - - function _titrUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('titrbar-v1', info.pH); - v('titrbar-v2', info.baseAdded + ' мл'); - v('titrbar-v3', info.eqPoint + ' мл'); - const indNames = { phenolphthalein: 'Фенолф.', methyl_orange: 'Метилор.', litmus: 'Лакмус' }; - v('titrbar-v4', indNames[info.indicator] || info.indicator); - } - - /* ── refraction ── */ - - function _openRefraction() { - document.getElementById('sim-topbar-title').textContent = 'Преломление света'; - _simShow('sim-refraction'); - _registerSimState('refraction', () => refrSim?.getParams(), st => refrSim?.setParams(st)); - if (_embedMode) _startStateEmit('refraction'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!refrSim) { - refrSim = new RefractionSim(document.getElementById('refraction-canvas')); - refrSim.onUpdate = _refrUpdateUI; - } - refrSim.fit(); - refrSim.draw(); - refrSim._emit(); - })); - } - - function refrParam(name, val) { - const v = parseFloat(val); - const ids = { n1: 'refr-n1-val', n2: 'refr-n2-val', angle: 'refr-angle-val' }; - const el = document.getElementById(ids[name]); - if (el) el.textContent = name === 'angle' ? v : v.toFixed(2); - if (refrSim) refrSim.setParams({ [name]: v }); - } - - function refrPreset(n1, n2, angle) { - document.getElementById('sl-refr-n1').value = n1; document.getElementById('refr-n1-val').textContent = n1.toFixed(2); - document.getElementById('sl-refr-n2').value = n2; document.getElementById('refr-n2-val').textContent = n2.toFixed(2); - document.getElementById('sl-refr-angle').value = angle; document.getElementById('refr-angle-val').textContent = angle; - if (refrSim) refrSim.setParams({ n1, n2, angle }); - } - - function _refrUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('refrbar-v1', info.angle1 + '°'); - v('refrbar-v2', info.isTIR ? 'ПВО' : info.angle2 + '°'); - v('refrbar-v3', info.criticalAngle !== null ? info.criticalAngle + '°' : '—'); - v('refrbar-v4', info.isTIR ? 'Да' : 'Нет'); - } - - /* ── probability ── */ - - function _openProbability() { - document.getElementById('sim-topbar-title').textContent = 'Теория вероятностей'; - _simShow('sim-probability'); - _registerSimState('probability', () => probSim?.getParams(), st => probSim?.setParams(st)); - if (_embedMode) _startStateEmit('probability'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!probSim) { - probSim = new ProbabilitySim(document.getElementById('probability-canvas')); - probSim.onUpdate = _probUpdateUI; - } - probSim.fit(); - probSim.reset(); - probSim.play(); - })); - } - - function probMode(mode, btn) { - document.querySelectorAll('.prob-mode-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (probSim) { probSim.setParams({ mode }); probSim.reset(); probSim.play(); } - } - - function probPreset(mode, trials) { - document.querySelectorAll('.prob-mode-btn').forEach(b => { - b.classList.toggle('active', b.textContent.toLowerCase().includes(mode === 'coin' ? 'монет' : mode === 'dice2' ? '2 куб' : 'кубик')); - }); - if (probSim) { probSim.setParams({ mode, trials }); probSim.reset(); probSim.play(); } - } - - function _probUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('probbar-v1', info.totalTrials); - v('probbar-v2', typeof info.maxDeviation === 'number' ? (info.maxDeviation * 100).toFixed(1) + '%' : '—'); - v('probbar-v3', typeof info.chiSquare === 'number' ? info.chiSquare.toFixed(2) : '—'); - const modeNames = { coin: 'Монета', dice: 'Кубик', dice2: '2 кубика' }; - v('probbar-v4', modeNames[info.mode] || info.mode); - } - - /* ── bohr atom ── */ - - function _openBohrAtom() { - document.getElementById('sim-topbar-title').textContent = 'Атом Бора'; - _simShow('sim-bohratom'); - _registerSimState('bohratom', () => bohrSim?.getParams(), st => bohrSim?.setParams(st)); - if (_embedMode) _startStateEmit('bohratom'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!bohrSim) { - bohrSim = new BohrAtomSim(document.getElementById('bohratom-canvas')); - bohrSim.onUpdate = _bohrUpdateUI; - } - bohrSim.fit(); - bohrSim.play(); - })); - } - - function bohrLevel(n) { - if (bohrSim) { - const from = bohrSim.info().level; - if (from !== n) bohrSim.transition(from, n); - } - } - - function bohrTransition(from, to) { - if (bohrSim) bohrSim.transition(from, to); - } - - function _bohrUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('bohrbar-v1', info.level); - v('bohrbar-v2', info.energy.toFixed(2)); - if (info.lastTransition) { - v('bohrbar-v3', info.lastTransition.wavelength.toFixed(0)); - v('bohrbar-v4', info.lastTransition.series || '—'); - } - } - - /* ── electrolysis ── */ - - function _openElectrolysis() { - document.getElementById('sim-topbar-title').textContent = 'Электролиз'; - _simShow('sim-electrolysis'); - _registerSimState('electrolysis', () => elecSim?.getParams(), st => elecSim?.setParams(st)); - if (_embedMode) _startStateEmit('electrolysis'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!elecSim) { - elecSim = new ElectrolysisSim(document.getElementById('electrolysis-canvas')); - elecSim.onUpdate = _elecUpdateUI; - } - elecSim.fit(); - elecSim.reset(); - elecSim.play(); - })); - } - - function elecParam(name, val) { - const v = parseFloat(val); - if (name === 'voltage') document.getElementById('elec-V-val').textContent = v; - if (elecSim) elecSim.setParams({ [name]: v }); - } - - function elecPreset(name, btn) { - document.querySelectorAll('.elec-type-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - const voltages = { nacl: 6, cuso4: 4, h2so4: 3 }; - const vt = voltages[name] || 6; - document.getElementById('sl-elec-V').value = vt; document.getElementById('elec-V-val').textContent = vt; - if (elecSim) { elecSim.setParams({ electrolyte: name, voltage: vt }); elecSim.reset(); elecSim.play(); } - } - - function _elecUpdateUI(info) { - const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; - v('elecbar-v1', typeof info.current === 'number' ? info.current.toFixed(2) : '—'); - v('elecbar-v2', typeof info.massDeposited === 'number' ? info.massDeposited.toFixed(3) + ' г' : '—'); - v('elecbar-v3', typeof info.gasVolume === 'number' ? info.gasVolume.toFixed(1) : '—'); - v('elecbar-v4', typeof info.time === 'number' ? info.time.toFixed(0) + ' с' : '—'); - } - - /* ── waves ── */ - function _openWaves() { - document.getElementById('sim-topbar-title').textContent = 'Волны и звук'; - document.getElementById('ctrl-waves').style.display = ''; - _simShow('sim-waves'); - _registerSimState('waves', () => wavesSim?.getParams(), - st => { if (wavesSim) { if (st.mode) wavesSim.setMode(st.mode); wavesSim.setParams(st); } }); - if (_embedMode) _startStateEmit('waves'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!wavesSim) { - wavesSim = new WavesSim(document.getElementById('waves-canvas')); - wavesSim.onUpdate = _wavesUpdateUI; - } - wavesSim.fit(); - wavesSim.reset(); - wavesSim.play(); - _wavesUpdateUI(wavesSim.info()); - })); - } - - function wavesMode(mode, btn) { - document.querySelectorAll('.wave-mode-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - document.getElementById('waves-w2-section').style.display = mode === 'superposition' ? '' : 'none'; - document.getElementById('waves-n-section').style.display = mode === 'standing' ? '' : 'none'; - if (wavesSim) wavesSim.setMode(mode); - } - - function wavesParam(name, val) { - const v = parseFloat(val); - const el = (id, txt) => { const e = document.getElementById(id); if (e) e.textContent = txt; }; - if (name === 'A1') el('waves-A1-val', v); - if (name === 'f1') el('waves-f1-val', v.toFixed(1) + ' Гц'); - if (name === 'phi1') el('waves-phi1-val', v.toFixed(1)); - if (name === 'A2') el('waves-A2-val', v); - if (name === 'f2') el('waves-f2-val', v.toFixed(1) + ' Гц'); - if (name === 'phi2') el('waves-phi2-val', v.toFixed(1)); - if (name === 'speed') el('waves-speed-val', '\u00d7' + v.toFixed(1)); - if (wavesSim) wavesSim.setParams({ [name]: v }); - } - - function wavesN(n, btn) { - document.querySelectorAll('.wave-n-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (wavesSim) wavesSim.setParams({ n }); - } - - function wavesPreset(name) { - const presets = { - constructive: { A1: 50, f1: 1.0, phi1: 0, A2: 50, f2: 1.0, phi2: 0 }, - destructive: { A1: 50, f1: 1.0, phi1: 0, A2: 50, f2: 1.0, phi2: 3.14 }, - beats: { A1: 50, f1: 1.0, phi1: 0, A2: 50, f2: 1.3, phi2: 0 }, - }; - const p = presets[name]; if (!p) return; - document.getElementById('sl-waves-A1').value = p.A1; - document.getElementById('sl-waves-f1').value = p.f1; - document.getElementById('sl-waves-phi1').value = p.phi1; - document.getElementById('sl-waves-A2').value = p.A2; - document.getElementById('sl-waves-f2').value = p.f2; - document.getElementById('sl-waves-phi2').value = p.phi2; - document.getElementById('waves-A1-val').textContent = p.A1; - document.getElementById('waves-f1-val').textContent = p.f1.toFixed(1) + ' Гц'; - document.getElementById('waves-phi1-val').textContent = p.phi1.toFixed(1); - document.getElementById('waves-A2-val').textContent = p.A2; - document.getElementById('waves-f2-val').textContent = p.f2.toFixed(1) + ' Гц'; - document.getElementById('waves-phi2-val').textContent = p.phi2.toFixed(1); - if (wavesSim) wavesSim.setParams({ A1: p.A1, f1: p.f1, phi1: p.phi1, A2: p.A2, f2: p.f2, phi2: p.phi2 }); - } - - function wavesPlayPause() { - if (!wavesSim) return; - const btn = document.getElementById('waves-play-btn'); - if (wavesSim._paused) { - wavesSim.play(); - btn.innerHTML = ''; - } else { - wavesSim.pause(); - btn.innerHTML = ''; - } - } - - function _wavesUpdateUI(info) { - const v = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; }; - v('wavesbar-T', info.T); - v('wavesbar-lam', info.lambda); - v('wavesbar-v', info.v); - v('wavesbar-f', (+info.f1).toFixed(1)); - } - - /* ── crystal lattice (3D) ── */ - var crystalSim = null; - function _openCrystal() { - document.getElementById('sim-topbar-title').textContent = 'Кристаллическая решётка'; - _simShow('sim-crystal'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!crystalSim) { - crystalSim = new CrystalSim(document.getElementById('crystal-container')); - } else { - crystalSim.fit(); - crystalSim.play(); - } - })); - } - function setCrystal(type, btn) { - document.querySelectorAll('.crystal-type-btn').forEach(b => { b.classList.remove('active'); b.style.borderColor = ''; b.style.color = ''; }); - btn.classList.add('active'); - btn.style.borderColor = '#9B5DE5'; btn.style.color = '#9B5DE5'; - if (crystalSim) crystalSim.setLattice(type); - } - - /* ── molecular orbitals (3D) ── */ - var orbitalsSim = null; - function _openOrbitals() { - document.getElementById('sim-topbar-title').textContent = 'Молекулярные орбитали'; - _simShow('sim-orbitals'); - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!orbitalsSim) { - orbitalsSim = new OrbitalsSim(document.getElementById('orbitals-container')); - } else { - orbitalsSim.fit(); - orbitalsSim.play(); - } - })); - } - function setOrbital(mode, btn) { - document.querySelectorAll('.orbital-mode-btn').forEach(b => { b.classList.remove('active'); b.style.borderColor = ''; b.style.color = ''; }); - btn.classList.add('active'); - btn.style.borderColor = '#9B5DE5'; btn.style.color = '#9B5DE5'; - if (orbitalsSim) orbitalsSim.setMode(mode); - } - - /* ── stereometry 3D ── */ - var stereoSim = null; - - // which params are relevant per figure type - const STEREO_PARAM_MAP = { - cube: ['a'], - parallelepiped: ['a','b','c'], - pyramid: ['a','n','h'], - tetrahedron: ['a'], - cylinder: ['r','h'], - cone: ['r','h'], - trunccone: ['R','r','h'], - sphere: ['r'], - prism: ['a','n','h'], - truncpyramid: ['a','b','n','h'], - octahedron: ['a'], - icosahedron: ['a'], - dodecahedron: ['a'], - }; - - function _openStereo() { - document.getElementById('sim-topbar-title').textContent = 'Стереометрия 3D'; - _simShow('sim-stereo'); - document.getElementById('stereo-stats').style.display = ''; - requestAnimationFrame(() => requestAnimationFrame(() => { - if (!stereoSim) { - stereoSim = new StereoSim(document.getElementById('stereo-container')); - stereoSim.onUpdate = _stereoUpdateUI; - } else { - stereoSim.fit(); - stereoSim.play(); - } - _stereoShowParams(stereoSim.figureType || 'cube'); - _stereoUpdateUI(stereoSim.info()); - _stereoUpdateFormulas(); - })); - } - - function setStereoFigure(type, btn) { - document.querySelectorAll('.stereo-fig-btn').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - if (stereoSim) { - stereoSim.setFigure(type); - _stereoShowParams(type); - _stereoUpdateFormulas(); - // reset toggles and tool buttons - document.getElementById('sect-toggle').classList.remove('active'); - document.getElementById('stereo-unfold-btn').classList.remove('active'); - document.getElementById('stereo-measure-btn').classList.remove('active'); - // reset element toggles - ['stg-height','stg-apothem','stg-diagonals','stg-midpoints','stg-inscribed','stg-circumscribed','stg-edgelengths'].forEach(id => { - document.getElementById(id)?.classList.remove('on'); - }); - _stereoDeactivateTools(); - } - } - - function _stereoShowParams(type) { - const show = STEREO_PARAM_MAP[type] || ['a']; - ['a','b','c','h','r','R','n'].forEach(k => { - document.getElementById('sp-' + k + '-row').style.display = show.includes(k) ? '' : 'none'; - }); - } - - function stereoParamChange(key, val) { - val = +val; - const label = document.getElementById('sp-' + key + '-val'); - if (label) label.textContent = val; - if (stereoSim) { - stereoSim.setParam(key, val); - _stereoUpdateFormulas(); - } - } - - function stereoOpacityChange(val) { - val = +val; - document.getElementById('sp-opacity-val').textContent = val.toFixed(2); - if (stereoSim) stereoSim.setOpacity(val); - } - - // legacy (used nowhere now but kept for safety) - function stereoToggle(layer, btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (!stereoSim) return; - if (layer === 'edges') stereoSim.toggleEdges(on); - if (layer === 'vertices') stereoSim.toggleVertices(on); - if (layer === 'labels') stereoSim.toggleLabels(on); - if (layer === 'axes') stereoSim.toggleAxes(on); - if (layer === 'grid') stereoSim.toggleGrid(on); - } - - // new toggle-row style - function stereoToggleSt(layer, toggle) { - const on = !toggle.classList.contains('on'); - toggle.classList.toggle('on', on); - if (!stereoSim) return; - if (layer === 'edges') stereoSim.toggleEdges(on); - if (layer === 'vertices') stereoSim.toggleVertices(on); - if (layer === 'labels') stereoSim.toggleLabels(on); - if (layer === 'axes') stereoSim.toggleAxes(on); - if (layer === 'grid') stereoSim.toggleGrid(on); - } - - function stereoToggleElem(layer, toggle) { - const on = !toggle.classList.contains('on'); - toggle.classList.toggle('on', on); - if (!stereoSim) return; - if (layer === 'height') stereoSim.toggleHeight(on); - if (layer === 'apothem') stereoSim.toggleApothem(on); - if (layer === 'diagonals') stereoSim.toggleDiagonals(on); - if (layer === 'midpoints') stereoSim.toggleMidpoints(on); - if (layer === 'inscribed') stereoSim.toggleInscribed(on); - if (layer === 'circumscribed') stereoSim.toggleCircumscribed(on); - if (layer === 'edgelengths') stereoSim.toggleEdgeLengths(on); - } - - // n-stepper for prism/pyramid - function stereoNChange(delta) { - if (!stereoSim) return; - const cur = stereoSim.params.n || 4; - const nv = Math.max(3, Math.min(12, cur + delta)); - document.getElementById('sp-n-val').textContent = nv; - stereoSim.setParam('n', nv); - _stereoUpdateFormulas(); - } - - function stereoSectionToggle(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleSection(on); - } - - function stereoSectionType(t, btn) { - document.querySelectorAll('.stereo-sect-type').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - // Show/hide angle slider for diagonal - document.getElementById('sp-angle-row').style.display = t === 'diagonal' ? '' : 'none'; - if (stereoSim) stereoSim.setSectionType(t); - } - - function stereoSectionHeight(val) { - document.getElementById('sp-sect-val').textContent = val + '%'; - if (stereoSim) stereoSim.setSectionHeight(+val / 100); - } - - function stereoSectionAngle(val) { - document.getElementById('sp-angle-val').textContent = val + '%'; - if (stereoSim) stereoSim.setSectionAngle(+val / 100); - } - - function stereoUnfold(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleUnfold(on); - } - - function _stereoDeactivateTools() { - ['stereo-measure-btn','stereo-point-btn','stereo-connect-btn', - 'stereo-angle-edge-btn','stereo-angle-lp-btn','stereo-angle-dih-btn','stereo-angle-pp-btn','stereo-angle-skew-btn', - 'stereo-mark-tick-btn','stereo-mark-par-btn', - 'stereo-derive-mid-btn','stereo-derive-fc-btn','stereo-derive-alt-btn','stereo-derive-cen-btn'].forEach(id => { - document.getElementById(id)?.classList.remove('active'); - }); - if (stereoSim) { - stereoSim.toggleMeasure(false); - stereoSim.togglePointMode(false); - stereoSim.toggleConnectMode(false); - stereoSim.setAngleMode(null); - stereoSim.setMarkMode(null); - stereoSim.setDeriveMode(null); - } - const hint = document.getElementById('angle-hint'); - if (hint) hint.textContent = ''; - } - - function stereoMeasure(btn) { - const on = !btn.classList.contains('active'); - _stereoDeactivateTools(); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleMeasure(on); - } - - function stereoMeasureUndo() { - if (stereoSim) stereoSim.removeLastMeasurement(); - } - - function stereoMeasureClear() { - if (stereoSim) stereoSim.clearMeasurements(); - } - - function stereoToggleHeight(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleHeight(on); - } - - function stereoToggleApothem(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleApothem(on); - } - - function stereoToggleDiag(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleDiagonals(on); - } - - function stereoToggleMid(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleMidpoints(on); - } - - const ANGLE_HINTS = { - edge: 'Кликните 3 точки: A, B (вершина угла), C', - linePlane: 'Кликните 2 точки (прямая), затем — грань', - dihedral: 'Кликните 2 точки общего ребра двух граней', - pointPlane: 'Кликните точку, затем — грань', - skewLines: 'P1, P2 (прямая 1) → P3, P4 (прямая 2): угол и расстояние', - }; - - function stereoAngleMode(mode, btn) { - const on = !btn.classList.contains('active'); - _stereoDeactivateTools(); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.setAngleMode(on ? mode : null); - const hint = document.getElementById('angle-hint'); - if (hint) hint.textContent = on ? ANGLE_HINTS[mode] : ''; - } - - function stereoAngleClear() { - _stereoDeactivateTools(); - if (stereoSim) { - stereoSim.setAngleMode(null); - stereoSim._clearGroup(stereoSim._angleGroup); - } - } - - /* ── Edge marks ── */ - function stereoMarkMode(mode, btn) { - const on = !btn.classList.contains('active'); - _stereoDeactivateTools(); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.setMarkMode(on ? mode : null); - } - - function stereoMarkClear() { - _stereoDeactivateTools(); - if (stereoSim) stereoSim.clearMarks(); - } - - function stereoToggleEdgeLengths(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleEdgeLengths(on); - } - - /* ── Derived points ── */ - function stereoDerive(mode, btn) { - const on = !btn.classList.contains('active'); - _stereoDeactivateTools(); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.setDeriveMode(on ? mode : null); - } - - function stereoDeriveUndo() { - if (stereoSim) stereoSim.removeLastDerived(); - } - - function stereoDeriveClear() { - _stereoDeactivateTools(); - if (stereoSim) stereoSim.clearDerived(); - } - - function stereoPointMode(btn) { - const on = !btn.classList.contains('active'); - _stereoDeactivateTools(); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.togglePointMode(on); - } - - function stereoConnectMode(btn) { - const on = !btn.classList.contains('active'); - _stereoDeactivateTools(); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleConnectMode(on); - } - - function stereoUndoPoint() { - if (stereoSim) stereoSim.removeLastPoint(); - } - - function stereoClearPoints() { - if (stereoSim) stereoSim.clearCustomPoints(); - _stereoUpdatePointsInfo(); - } - - function stereoInscribed(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleInscribed(on); - } - - function stereoCircumscribed(btn) { - const on = !btn.classList.contains('active'); - btn.classList.toggle('active', on); - if (stereoSim) stereoSim.toggleCircumscribed(on); - } - - function _stereoUpdateFormulas() { - if (!stereoSim) return; - const f = stereoSim.getFormulas(); - const el = document.getElementById('stereo-formulas'); - if (!f || !f.formulas) { el.innerHTML = ''; return; } - const colors = ['#7BF5A4','#60a5fa','#c4b5fd','#fbbf24','#f9a8d4','#F59E0B','#EF476F']; - el.innerHTML = f.formulas.map((s, i) => - '
' + s + '
' - ).join(''); - } - - function _stereoUpdateUI(info) { - if (!info) return; - document.getElementById('stbar-vol').textContent = info.V !== undefined ? info.V.toFixed(2) : '—'; - document.getElementById('stbar-area').textContent = info.S !== undefined ? info.S.toFixed(2) : '—'; - document.getElementById('stbar-side').textContent = info.S_side !== undefined ? info.S_side.toFixed(2) : '—'; - document.getElementById('stbar-h').textContent = info.h !== undefined ? info.h.toFixed(2) : '—'; - document.getElementById('stbar-d').textContent = info.d !== undefined && info.d > 0 ? info.d.toFixed(2) : '—'; - - // Section area - const sectEl = document.getElementById('sect-area-display'); - if (info.sectionArea && info.sectionArea > 0) { - sectEl.style.display = ''; - sectEl.textContent = 'S сечения = ' + info.sectionArea.toFixed(2); - } else { - sectEl.style.display = 'none'; - } - - // Inscribed / Circumscribed radius info - const rInfo = document.getElementById('sphere-radius-info'); - if (rInfo) { - const parts = []; - if (info.inscribedR != null) parts.push('r_вп = ' + info.inscribedR.toFixed(2)); - if (info.circumscribedR != null) parts.push('R_оп = ' + info.circumscribedR.toFixed(2)); - rInfo.textContent = parts.join(' · '); - rInfo.style.display = parts.length ? '' : 'none'; - } - - // Points info - _stereoUpdatePointsInfo(info); - } - - function _stereoUpdatePointsInfo(info) { - const el = document.getElementById('points-info'); - if (!el) return; - if (!info) info = stereoSim?.info(); - if (!info) { el.textContent = ''; return; } - let txt = ''; - if (info.customPoints > 0) txt += `Точек: ${info.customPoints}`; - if (info.connections > 0) txt += ` · Линий: ${info.connections}`; - el.textContent = txt; - } - /* ── theory panel ── */ const THEORY = { graph: { @@ -3982,117 +541,3 @@ /* ══════════════════════════════════════════════ HYDROSTATICS ══════════════════════════════════════════════ */ - var hydroSim = null; - let _hydroValveOpen = true; - - function _openHydro(preset) { - document.getElementById('sim-topbar-title').textContent = 'Гидростатика'; - _simShow('sim-hydro'); - document.getElementById('ctrl-hydro').style.display = ''; - _registerSimState('hydrostatics', - () => ({ mode: hydroSim?.mode, liq: hydroSim?.liquidKey }), - st => { if (st?.mode && hydroSim) hydroMode(st.mode); }); - if (_embedMode) _startStateEmit('hydrostatics'); - window.addEventListener('load', () => {}, { once: true }); - requestAnimationFrame(() => requestAnimationFrame(() => { - const canvas = document.getElementById('hydro-canvas'); - const mode = preset || 'pressure'; - if (!hydroSim) { - hydroSim = new HydroSim(canvas, mode); - hydroSim.onUpdate = _hydroUpdateUI; - } else { - hydroSim.fit(); - hydroSim.play(); - } - hydroMode(mode); - })); - } - - function hydroMode(mode) { - if (!hydroSim) return; - hydroSim.setMode(mode); - const sel = document.getElementById('hydro-mode-sel'); - if (sel) sel.value = mode; - // show/hide sub-controls - ['arch','comm','surf','mat'].forEach(k => { - const el = document.getElementById('hydro-panel-' + k); - const el2 = document.getElementById('hydro-' + k + '-ctrl'); - if (el) el.style.display = 'none'; - if (el2) el2.style.display = 'none'; - }); - if (mode === 'archimedes') { - const a = document.getElementById('hydro-panel-mat'); - const b = document.getElementById('hydro-arch-ctrl'); - if (a) a.style.display = ''; - if (b) b.style.display = 'flex'; - } - if (mode === 'surface') { - const a = document.getElementById('hydro-panel-theta'); - const b = document.getElementById('hydro-surf-ctrl'); - if (a) a.style.display = ''; - if (b) b.style.display = 'flex'; - } - if (mode === 'communicating') { - const a = document.getElementById('hydro-panel-comm'); - const b = document.getElementById('hydro-comm-ctrl'); - if (a) a.style.display = ''; - if (b) b.style.display = 'flex'; - } - } - - function hydroToggleSurface() { - if (!hydroSim) return; - const next = hydroSim._stMode === 'capillary' ? 'drop' : 'capillary'; - hydroSim._stMode = next; - const label = next === 'capillary' ? '\u041A\u0430\u043F\u0438\u043B\u043B\u044F\u0440\u044B' : '\u041A\u0430\u043F\u043B\u044F'; - ['hydro-surf-toggle','hydro-surf-toggle-panel'].forEach(id => { - const el = document.getElementById(id); - if (el) el.textContent = label; - }); - } - - function hydroToggleValve() { - if (!hydroSim) return; - _hydroValveOpen = !_hydroValveOpen; - hydroSim.setValve(_hydroValveOpen); - const label = _hydroValveOpen ? 'Кран: открыт' : 'Кран: закрыт'; - const color = _hydroValveOpen ? '#06D6A0' : '#F15BB5'; - ['hydro-valve-btn','hydro-valve-panel-btn'].forEach(id => { - const el = document.getElementById(id); - if (el) { el.textContent = label; el.style.color = color; el.style.borderColor = _hydroValveOpen ? 'rgba(6,214,160,.3)' : 'rgba(241,91,181,.3)'; } - }); - } - - function hydroSetVessels(n, btn) { - if (hydroSim) hydroSim.setNumVessels(n); - document.querySelectorAll('.hydro-nv').forEach(b => b.classList.remove('active')); - if (btn) btn.classList.add('active'); - } - - function _hydroUpdateUI(info) { - if (!info) return; - const el = document.getElementById('hydro-formulas'); - if (!el) return; - const lines = []; - if (info.formula) lines.push(`${info.formula}`); - if (info.liqName) lines.push(`Жидкость: ${info.liqName}${info.rho ? ' (ρ=' + info.rho + ')' : ''}`); - if (info.matName) lines.push(`Материал: ${info.matName}`); - if (info.FA) lines.push(`F_A = ${info.FA} Н`); - if (info.mg) lines.push(`mg = ${info.mg} Н`); - if (info.sigma) lines.push(`σ = ${info.sigma} Н/м, θ = ${info.theta}°`); - if (info.h && !info.FA) lines.push(`h_подъём = ${info.h} мм`); - el.innerHTML = lines.join('
'); - // result badge - const rb = document.getElementById('hydro-result'); - if (rb && info.state) { - const colors = { 'ВСПЛЫВАЕТ': '#06D6A0', 'ТОНЕТ': '#F15BB5', 'ВЗВЕШЕНО': '#FFD166' }; - rb.style.display = ''; - rb.style.color = colors[info.state] || '#fff'; - rb.style.background = (colors[info.state] || '#9B5DE5') + '18'; - rb.style.border = '1px solid ' + (colors[info.state] || '#9B5DE5') + '44'; - rb.textContent = info.state; - } else if (rb) { - rb.style.display = 'none'; - } - } - diff --git a/frontend/js/labs/magnetic.js b/frontend/js/labs/magnetic.js index b2e3cae..ced9fda 100644 --- a/frontend/js/labs/magnetic.js +++ b/frontend/js/labs/magnetic.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════ MagneticSim — magnetic field of current-carrying wires • Click canvas to place wire (• out / × in) @@ -1053,3 +1053,107 @@ class MagneticSim { ctx.restore(); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openMagnetic() { + document.getElementById('sim-topbar-title').textContent = 'Магнитное поле токов'; + _simShow('sim-mag'); + _simShow('ctrl-mag'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!mSim) { + mSim = new MagneticSim(document.getElementById('mag-canvas')); + mSim.onUpdate = _magUpdateUI; + } + mSim.fit(); + // default preset on first open + if (mSim.sources.length === 0) mSim.preset('anti'); + _magUpdateUI(mSim.info()); + })); + } + + function magMode(dir) { + if (!mSim) return; + mSim.addMode = dir; + document.getElementById('mag-add-out').classList.toggle('active', dir === 'out'); + document.getElementById('mag-add-in').classList.toggle('active', dir === 'in'); + document.getElementById('mag-mode-out').classList.toggle('active', dir === 'out'); + document.getElementById('mag-mode-in').classList.toggle('active', dir === 'in'); + } + + function magCurrentChange() { + const I = +document.getElementById('sl-curI').value; + document.getElementById('m-curI').textContent = I + ' А'; + document.getElementById('mbar-I').textContent = I + ' А'; + if (mSim) mSim.setCurrentAll(I); + } + + function magLayer(name, rowEl) { + if (!mSim) return; + mSim.layers[name] = !mSim.layers[name]; + rowEl.classList.toggle('active', mSim.layers[name]); + mSim._invalidateCache(); + mSim.draw(); + } + + function magParticle(rowEl) { + if (!mSim) return; + mSim.toggleParticle(); + rowEl.classList.toggle('active', mSim.particleOn); + _magUpdateUI(mSim.info()); + } + + function magCondToggle(rowEl) { + if (!mSim) return; + mSim.toggleConductor(); + const on = mSim._cond.on; + rowEl.classList.toggle('active', on); + document.getElementById('cond-I-block').style.display = on ? '' : 'none'; + _magUpdateUI(mSim.info()); + } + + function magCondCurrentChange() { + if (!mSim) return; + const I = parseFloat(document.getElementById('sl-condI').value); + document.getElementById('m-condI').textContent = I + ' А'; + mSim.setConductorI(I); + } + + function magFluxToggle(rowEl) { + if (!mSim) return; + mSim.toggleFlux(); + rowEl.classList.toggle('active', mSim._flux.on); + _magUpdateUI(mSim.info()); + } + + function _magUpdateUI(info) { + document.getElementById('ms-out').textContent = info.out; + document.getElementById('ms-in').textContent = info.inn; + document.getElementById('mbar-total').textContent = info.total; + document.getElementById('mbar-out').textContent = info.out; + document.getElementById('mbar-in').textContent = info.inn; + document.getElementById('mbar-particle').textContent = info.particleOn ? 'вкл' : 'выкл'; + document.getElementById('mbar-particle').style.color = info.particleOn ? '#ffff50' : ''; + // Ampere force + const fEl = document.getElementById('mbar-ampere'); + if (info.condOn && info.Fz !== 0) { + const dir = info.Fz > 0 ? '⊙' : '⊗'; + fEl.textContent = dir + ' ' + Math.abs(info.Fz).toFixed(3); + fEl.style.color = '#fbbf24'; + } else { + fEl.textContent = '—'; + fEl.style.color = '#fbbf24'; + } + // Flux + const phEl = document.getElementById('mbar-flux'); + if (info.fluxOn) { + phEl.textContent = info.flux.toExponential(2) + ' Вб'; + phEl.style.color = '#34d399'; + } else { + phEl.textContent = '—'; + phEl.style.color = '#34d399'; + } + } + + /* ── triangle ── */ + diff --git a/frontend/js/labs/mirror.js b/frontend/js/labs/mirror.js index 34b5062..e9fbfe6 100644 --- a/frontend/js/labs/mirror.js +++ b/frontend/js/labs/mirror.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ MirrorSim v3 Flat / Concave / Convex · 1/f = 1/d + 1/d' · M = -d'/d @@ -1003,3 +1003,97 @@ class MirrorSim { cv.addEventListener('touchend', () => { this._drag=null; }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var mirrorSim = null; + + function _openMirror() { + document.getElementById('sim-topbar-title').textContent = 'Зеркала'; + _simShow('sim-mirrors'); + _registerSimState('mirrors', () => mirrorSim?.getParams(), st => mirrorSim?.setParams(st)); + if (_embedMode) _startStateEmit('mirrors'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!mirrorSim) { + mirrorSim = new MirrorSim(document.getElementById('mirror-canvas')); + mirrorSim.onUpdate = _mirrorUpdateUI; + mirrorSim.onAnimate = (d) => { + const sl = document.getElementById('sl-mirror-d'); + const lbl = document.getElementById('mirror-d-val'); + if (sl) sl.value = Math.round(d); + if (lbl) lbl.textContent = Math.round(d); + }; + } + mirrorSim.fit(); + mirrorSim.draw(); + mirrorSim._emit(); + if (mirrorSim._showPhotons && !mirrorSim._photonRaf) mirrorSim._startPhotons(); + })); + } + + function mirrorType(type, el) { + document.querySelectorAll('.mirror-type-btn').forEach(b => b.classList.remove('active')); + if (el) el.classList.add('active'); + const fRow = document.getElementById('mirror-f-row'); + if (fRow) fRow.style.display = type === 'flat' ? 'none' : 'flex'; + if (mirrorSim) mirrorSim.setType(type); + const pb = document.getElementById('mirror-play-btn'); + if (pb) { pb.textContent = '▶ Анимация'; } + const sl = document.getElementById('sl-mirror-d'); + if (sl) sl.disabled = false; + } + + function mirrorParam(name, val) { + const v = parseFloat(val); + const ids = { f: 'mirror-f-val', d: 'mirror-d-val', h: 'mirror-h-val' }; + const el = document.getElementById(ids[name]); + if (el) el.textContent = v; + if (mirrorSim) mirrorSim.setParams({ [name]: v }); + } + + function mirrorPreset(name) { + const P = { + flat: { type: 'flat', f: 120, d: 200, h: 60 }, + far: { type: 'concave', f: 100, d: 280, h: 60 }, + '2f': { type: 'concave', f: 100, d: 200, h: 60 }, + between: { type: 'concave', f: 100, d: 140, h: 60 }, + near: { type: 'concave', f: 100, d: 60, h: 60 }, + convex: { type: 'convex', f: 100, d: 200, h: 60 }, + }; + const p = P[name]; if (!p) return; + document.querySelectorAll('.mirror-type-btn').forEach(b => b.classList.remove('active')); + const tb = document.getElementById(`mtype-${p.type}`); + if (tb) tb.classList.add('active'); + const fRow = document.getElementById('mirror-f-row'); + if (fRow) fRow.style.display = p.type === 'flat' ? 'none' : 'flex'; + document.getElementById('sl-mirror-f').value = p.f; document.getElementById('mirror-f-val').textContent = p.f; + document.getElementById('sl-mirror-d').value = p.d; document.getElementById('mirror-d-val').textContent = p.d; + document.getElementById('sl-mirror-h').value = p.h; document.getElementById('mirror-h-val').textContent = p.h; + if (mirrorSim) { mirrorSim.setType(p.type); mirrorSim.setParams({ f: p.f, d: p.d, h: p.h }); } + } + + function mirrorTogglePlay(btn) { + if (!mirrorSim) return; + mirrorSim.togglePlay(); + const playing = mirrorSim._playing; + if (btn) btn.textContent = playing ? '⏸ Стоп' : '▶ Анимация'; + const sl = document.getElementById('sl-mirror-d'); + if (sl) sl.disabled = playing; + } + + function mirrorSetSpeed(val) { if (mirrorSim) mirrorSim.setAnimSpeed(parseFloat(val)); } + function mirrorToggle(name, val) { if (mirrorSim) mirrorSim.setToggle(name, val); } + function mirrorStepNext() { if (mirrorSim) mirrorSim.stepNext(); } + function mirrorStepReset() { if (mirrorSim) mirrorSim.stepReset(); } + function mirrorSetPointMode(val) { if (mirrorSim) mirrorSim.setPointMode(val); } + + function _mirrorUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('mirrorbar-v1', info.f); + v('mirrorbar-v5', Math.round(info.d)); + v('mirrorbar-v2', info.dPrime === Infinity ? '∞' : info.dPrime); + v('mirrorbar-v3', info.M === Infinity ? '∞' : info.M); + v('mirrorbar-v4', info.imageType); + } + + /* ── isoprocesses ── */ + diff --git a/frontend/js/labs/newton.js b/frontend/js/labs/newton.js index d82c7cf..eb4d453 100644 --- a/frontend/js/labs/newton.js +++ b/frontend/js/labs/newton.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ════════════════════════════════════════════════════════════════ NewtonSim — три закона Ньютона Закон I : A — скользящий блок, B — шар на нити @@ -1202,3 +1202,465 @@ function _nwt_lighten(hex, d) { const c = v => Math.max(0, Math.min(255, v)); return `rgb(${c((n >> 16) + d)},${c(((n >> 8) & 255) + d)},${c((n & 255) + d)})`; } + +/* ─── lab UI init ─────────────────────────────────── */ + var newtonSim = null; + var sandboxSim = null; + let _dynMode = 'sandbox'; // current mode: 'sandbox' | 'law1' | 'law2' | 'law3' + + function _openDynamics(preset) { + document.getElementById('sim-topbar-title').textContent = 'Динамика'; + _simShow('sim-dynamics'); + _simShow('ctrl-dynamics'); + requestAnimationFrame(() => requestAnimationFrame(() => { + // init sandbox + const sbCanvas = document.getElementById('sandbox-canvas'); + if (!sandboxSim) { + sandboxSim = new ForceSandboxSim(sbCanvas); + sandboxSim.onUpdate = _sbUpdateUI; + } + // init newton + const nwCanvas = document.getElementById('newton-canvas'); + if (!newtonSim) { + newtonSim = new NewtonSim(nwCanvas); + newtonSim.onUpdate = _newtonUpdateUI; + } + // activate current mode + dynMode(_dynMode); + if (preset) setTimeout(() => sbPreset(preset), 120); + })); + } + + function dynMode(mode, btn) { + _dynMode = mode; + const isSandbox = mode === 'sandbox'; + + // toggle mode buttons + document.querySelectorAll('.dyn-mode').forEach(b => b.classList.remove('active')); + const modeBtn = document.getElementById('dyn-mode-' + mode); + if (modeBtn) modeBtn.classList.add('active'); + + // toggle panels + document.getElementById('dyn-sandbox-panel').style.display = isSandbox ? '' : 'none'; + document.getElementById('dyn-newton-panel').style.display = isSandbox ? 'none' : ''; + + // toggle canvases + document.getElementById('sandbox-canvas').style.display = isSandbox ? 'block' : 'none'; + document.getElementById('newton-canvas').style.display = isSandbox ? 'none' : 'block'; + + // toggle topbar tool groups + document.getElementById('ctrl-dyn-sb').style.display = isSandbox ? 'contents' : 'none'; + document.getElementById('ctrl-dyn-nw').style.display = isSandbox ? 'none' : 'contents'; + + if (isSandbox) { + // stop newton, start sandbox + if (newtonSim) newtonSim.stop(); + if (sandboxSim) { sandboxSim.fit(); sandboxSim.start(); } + _sbUpdateUI(sandboxSim ? sandboxSim.info() : null); + } else { + // stop sandbox, switch newton law + if (sandboxSim) sandboxSim.stop(); + const lawN = mode === 'law1' ? 1 : mode === 'law2' ? 2 : 3; + if (newtonSim) { + newtonSim.setLaw(lawN); + newtonSim.fit(); + newtonSim.start(); + _newtonSyncUI(); + _newtonUpdateUI(newtonSim.info()); + } + } + } + + function dynPause() { + if (_dynMode === 'sandbox') { + if (sandboxSim) sandboxSim.togglePause(); + } else { + if (newtonSim) newtonSim.togglePause(); + } + } + + function dynReset() { + if (_dynMode === 'sandbox') { + sbReset(); + } else { + _resetNewtonScene(); + } + } + + const _NEWTON_SCENES = { + 1: { + A: { desc: 'Закон инерции: тело скользит по поверхности. Нажми на canvas — толкни блок.', action: null }, + B: { desc: 'Инерция в орбите: шар вращается на нити. Отруби нить — полетит по касательной!', action: ' Отрубить нить' }, + C: { desc: 'Инерция в космосе: тело движется равномерно, нет сил — нет ускорения.', action: null }, + }, + 2: { + A: { desc: 'Второй закон: F = ma. Прикладывай силу и следи за ускорением и скоростью.', action: ' Запустить' }, + B: { desc: 'Два тела, разные массы — одинаковая сила. Сравни ускорения!', action: ' Запустить' }, + C: { desc: 'Второй закон: изменяй силу и массу ползунками, наблюдай в реальном времени.', action: ' Запустить' }, + }, + 3: { + A: { desc: 'Третий закон: пушка выстрелила — отдача. Импульс сохраняется!', action: 'Выстрел' }, + B: { desc: 'Третий закон: два шара сталкиваются — силы равны и противоположны.', action: ' Столкнуть' }, + C: { desc: 'Реактивное движение: ракета выбрасывает газ — летит в обратную сторону.', action: 'Двигатель' }, + }, + }; + + const _NEWTON_PRESETS = { + 1: [ + { label: 'Космос', fn: 'space' }, + { label: 'Лёд', fn: 'ice' }, + { label: 'Асфальт', fn: 'asphalt' }, + { label: 'Резина', fn: 'rubber' }, + ], + 2: [ + { label: 'Лёгкий', fn: 'light' }, + { label: 'Тяжёлый', fn: 'heavy' }, + { label: 'Сравнить', fn: 'compare' }, + ], + 3: [ + { label: 'Большая пушка', fn: 'big_cannon' }, + { label: 'Маленькая', fn: 'small_cannon' }, + { label: 'Равные шары', fn: 'equal_balls' }, + ], + }; + + // _openNewton is now handled by _openDynamics + dynMode + + // newtonLaw is now handled by dynMode('law1'/'law2'/'law3') + + function newtonScene(s, topBtn, panelBtn) { + if (!newtonSim) return; + newtonSim.setScene(s); + document.querySelectorAll('.nscene-btn').forEach(b => { + b.classList.toggle('active', b.id === 'nscn-' + s || b.id === 'nscn-panel-' + s); + }); + _newtonSyncUI(); + _newtonUpdateUI(newtonSim.info()); + } + + function _newtonSyncUI() { + if (!newtonSim) return; + const law = newtonSim.law; + const scene = newtonSim.scene; + const sceneData = (_NEWTON_SCENES[law] || {})[scene] || {}; + + // description + const desc = document.getElementById('newton-scene-desc'); + if (desc) desc.textContent = sceneData.desc || ''; + + // action button label + const lbl = sceneData.action || (law === 1 ? ' Нить' : ' Действие'); + document.getElementById('newton-action-label').textContent = lbl; + document.getElementById('newton-action-top').textContent = lbl; + + // show/hide sliders + document.getElementById('newton-mu-block').style.display = law === 1 && scene === 'A' ? '' : 'none'; + document.getElementById('newton-mass1-block').style.display = (law === 2 || law === 3) ? '' : 'none'; + document.getElementById('newton-mass2-block').style.display = law === 3 ? '' : 'none'; + document.getElementById('newton-force-block').style.display = law === 2 ? '' : 'none'; + + // sync slider values from sim + document.getElementById('sl-newton-mu').value = newtonSim.mu; + document.getElementById('newton-mu-val').textContent = newtonSim.mu.toFixed(2); + document.getElementById('sl-newton-m1').value = newtonSim.mass1; + document.getElementById('newton-m1-val').textContent = newtonSim.mass1 + ' кг'; + document.getElementById('sl-newton-m2').value = newtonSim.mass2; + document.getElementById('newton-m2-val').textContent = newtonSim.mass2 + ' кг'; + document.getElementById('sl-newton-F').value = newtonSim.force; + document.getElementById('newton-F-val').textContent = newtonSim.force + ' Н'; + + // sync scene highlight buttons in both topbar and panel + ['A','B','C'].forEach(s => { + const tb = document.getElementById('nscn-' + s); + const pb = document.getElementById('nscn-panel-' + s); + const on = s === scene; + if (tb) tb.classList.toggle('active', on); + if (pb) pb.classList.toggle('active', on); + }); + + // presets + const presetsEl = document.getElementById('newton-presets'); + const presets = _NEWTON_PRESETS[law] || []; + presetsEl.innerHTML = presets.map(p => + `` + ).join(''); + + // scene B/C visibility for law I (B = orbital, C = space — but law I only has A,B) + // scene C doesn't exist for law I/II panel scene picker visibility + const cBtn = document.getElementById('nscn-panel-C'); + const cTopBtn = document.getElementById('nscn-C'); + const showC = law === 3; + if (cBtn) cBtn.style.display = showC ? '' : 'none'; + if (cTopBtn) cTopBtn.style.display = showC ? '' : 'none'; + const bBtn = document.getElementById('nscn-panel-B'); + const bTopBtn = document.getElementById('nscn-B'); + const showB = law !== 2 || true; // law 2 has compare scene B + if (bBtn) bBtn.style.display = ''; + if (bTopBtn) bTopBtn.style.display = ''; + } + + function newtonAction() { + if (!newtonSim) return; + const law = newtonSim.law; + const scene = newtonSim.scene; + if (law === 1 && scene === 'B') newtonSim.cutString(); + else if (law === 2) newtonSim.startL2(); + else if (law === 3 && scene === 'A') newtonSim.fireCannon(); + else if (law === 3 && scene === 'B') newtonSim._reset3B ? newtonSim._reset3B() : null; + else if (law === 3 && scene === 'C') newtonSim.toggleRocket(); + _newtonUpdateUI(newtonSim.info()); + } + + function _resetNewtonScene() { + if (!newtonSim) return; + const law = newtonSim.law; + const scene = newtonSim.scene; + if (law === 1 && scene === 'A') newtonSim.preset('ice'); + else if (law === 1) newtonSim.setScene(scene); + else if (law === 2) newtonSim.resetL2 ? newtonSim.resetL2() : newtonSim.setScene(scene); + else newtonSim.setScene(scene); + _newtonUpdateUI(newtonSim.info()); + } + + function newtonMuChange() { + const v = +document.getElementById('sl-newton-mu').value; + document.getElementById('newton-mu-val').textContent = v.toFixed(2); + if (newtonSim) newtonSim.setMu(v); + } + + function newtonMass1Change() { + const v = +document.getElementById('sl-newton-m1').value; + document.getElementById('newton-m1-val').textContent = v + ' кг'; + if (newtonSim) newtonSim.setMass1(v); + } + + function newtonMass2Change() { + const v = +document.getElementById('sl-newton-m2').value; + document.getElementById('newton-m2-val').textContent = v + ' кг'; + if (newtonSim) newtonSim.setMass2(v); + } + + function newtonForceChange() { + const v = +document.getElementById('sl-newton-F').value; + document.getElementById('newton-F-val').textContent = v + ' Н'; + if (newtonSim) newtonSim.setForce(v); + } + + function newtonPreset(name) { + if (!newtonSim) return; + newtonSim.preset(name); + _newtonSyncUI(); + _newtonUpdateUI(newtonSim.info()); + } + + function _newtonUpdateUI(info) { + if (!info) return; + const law = info.law; + const scene = info.scene; + + if (law === 1 && scene === 'A') { + document.getElementById('dbar-l1').textContent = 'Закон I-A'; + document.getElementById('dbar-v1').textContent = 'Скольжение'; + document.getElementById('dbar-l2').textContent = 'Скорость'; + document.getElementById('dbar-v2').textContent = info.v + ' м/с'; + document.getElementById('dbar-l3').textContent = 'Сила трения'; + document.getElementById('dbar-v3').textContent = info.fFr + ' Н'; + document.getElementById('dbar-l4').textContent = 'Масса'; + document.getElementById('dbar-v4').textContent = info.m + ' кг'; + document.getElementById('dbar-l5').textContent = 'μ'; + document.getElementById('dbar-v5').textContent = info.mu; + } else if (law === 1) { + document.getElementById('dbar-l1').textContent = 'Закон I-B'; + document.getElementById('dbar-v1').textContent = info.cut ? 'Нить срублена' : 'Вращение'; + document.getElementById('dbar-l2').textContent = 'Скорость'; + document.getElementById('dbar-v2').textContent = info.v + ' м/с'; + document.getElementById('dbar-l3').textContent = ''; + document.getElementById('dbar-v3').textContent = '—'; + document.getElementById('dbar-l4').textContent = ''; + document.getElementById('dbar-v4').textContent = '—'; + document.getElementById('dbar-l5').textContent = ''; + document.getElementById('dbar-v5').textContent = '—'; + } else if (law === 2) { + document.getElementById('dbar-l1').textContent = 'Закон II'; + document.getElementById('dbar-v1').textContent = 'F = ma'; + document.getElementById('dbar-l2').textContent = 'Сила F'; + document.getElementById('dbar-v2').textContent = info.F + ' Н'; + document.getElementById('dbar-l3').textContent = 'Масса m'; + document.getElementById('dbar-v3').textContent = info.m + ' кг'; + document.getElementById('dbar-l4').textContent = 'Ускор. a'; + document.getElementById('dbar-v4').textContent = info.a + ' м/с²'; + document.getElementById('dbar-l5').textContent = 'Скорость'; + document.getElementById('dbar-v5').textContent = info.v + ' м/с'; + } else if (scene === 'A') { + document.getElementById('dbar-l1').textContent = 'Закон III-A'; + document.getElementById('dbar-v1').textContent = 'Пушка'; + document.getElementById('dbar-l2').textContent = 'v снаряда'; + document.getElementById('dbar-v2').textContent = info.vBall !== '—' ? info.vBall + ' м/с' : '—'; + document.getElementById('dbar-l3').textContent = 'v пушки'; + document.getElementById('dbar-v3').textContent = info.vCannon + ' м/с'; + document.getElementById('dbar-l4').textContent = 'm снаряда'; + document.getElementById('dbar-v4').textContent = info.m1 + ' кг'; + document.getElementById('dbar-l5').textContent = 'm пушки'; + document.getElementById('dbar-v5').textContent = info.m2 + ' кг'; + } else if (scene === 'B') { + document.getElementById('dbar-l1').textContent = 'Закон III-B'; + document.getElementById('dbar-v1').textContent = 'Удар'; + document.getElementById('dbar-l2').textContent = 'p₁'; + document.getElementById('dbar-v2').textContent = info.p1 + ' кг·м/с'; + document.getElementById('dbar-l3').textContent = 'p₂'; + document.getElementById('dbar-v3').textContent = info.p2 + ' кг·м/с'; + document.getElementById('dbar-l4').textContent = 'p суммарный'; + document.getElementById('dbar-v4').textContent = info.pt + ' кг·м/с'; + document.getElementById('dbar-l5').textContent = ''; + document.getElementById('dbar-v5').textContent = '—'; + } else { + document.getElementById('dbar-l1').textContent = 'Закон III-C'; + document.getElementById('dbar-v1').textContent = 'Ракета'; + document.getElementById('dbar-l2').textContent = 'Ускорение'; + document.getElementById('dbar-v2').textContent = info.a + ' м/с²'; + document.getElementById('dbar-l3').textContent = 'Скорость'; + document.getElementById('dbar-v3').textContent = info.v + ' м/с'; + document.getElementById('dbar-l4').textContent = 'Масса'; + document.getElementById('dbar-v4').textContent = info.m + ' кг'; + document.getElementById('dbar-l5').textContent = 'Топливо'; + document.getElementById('dbar-v5').textContent = info.fuel + '%'; + } + } + + // _openSandbox is now handled by _openDynamics + dynMode + + function sbTool(t, btn) { + if (!sandboxSim) return; + sandboxSim.tool = t; + sandboxSim._springStart = null; + sandboxSim._ropeStart = null; + document.querySelectorAll('.sb-tool-btn').forEach(b => b.classList.toggle('active', b.id === 'sbt-' + t)); + document.querySelectorAll('.sb-panel-tool').forEach(b => b.classList.toggle('active', b.id === 'sbpt-' + t)); + const canvas = document.getElementById('sandbox-canvas'); + canvas.style.cursor = t === 'erase' ? 'not-allowed' + : (t === 'spring' || t === 'rope') ? 'cell' + : t === 'anchor' ? 'copy' + : 'crosshair'; + document.getElementById('sb-spring-block').style.display = t === 'spring' ? '' : 'none'; + } + + function sbSpringKChange() { + const v = +document.getElementById('sl-sb-springk').value; + document.getElementById('sb-springk-val').textContent = v + ' Н/м'; + if (sandboxSim) sandboxSim.newSpringK = v; + } + + function sbForceMode(m, btn) { + if (!sandboxSim) return; + sandboxSim.forceMode = m; + document.querySelectorAll('.sb-fmode').forEach(b => b.classList.toggle('active', b.id === 'sbfm-' + m)); + } + + function sbMassChange() { + const v = +document.getElementById('sl-sb-mass').value; + document.getElementById('sb-mass-val').textContent = v + ' кг'; + if (sandboxSim) sandboxSim.newMass = v; + } + + function sbRestChange() { + const v = +document.getElementById('sl-sb-rest').value; + document.getElementById('sb-rest-val').textContent = v.toFixed(2); + if (sandboxSim) sandboxSim.newRestitution = v; + } + + function sbFloorMuChange() { + const v = +document.getElementById('sl-sb-floormu').value; + document.getElementById('sb-floormu-val').textContent = v.toFixed(2); + if (sandboxSim) sandboxSim.floorMu = v; + } + + function sbWorldToggle() { + if (!sandboxSim) return; + sandboxSim.gravity = document.getElementById('sb-gravity').checked; + sandboxSim.hasFloor = document.getElementById('sb-floor').checked; + sandboxSim.hasWalls = document.getElementById('sb-walls').checked; + sandboxSim.airDrag = document.getElementById('sb-airdrag').checked; + } + + function sbRampToggle() { + if (!sandboxSim) return; + const on = document.getElementById('sb-ramp').checked; + sandboxSim.setRamp(on); + document.getElementById('sb-ramp-block').style.display = on ? '' : 'none'; + } + + function sbAngleChange() { + const v = +document.getElementById('sl-sb-angle').value; + document.getElementById('sb-angle-val').textContent = v + '°'; + if (sandboxSim) sandboxSim.setRampAngle(v); + } + + function sbRampMuChange() { + const v = +document.getElementById('sl-sb-rampmu').value; + document.getElementById('sb-rampmu-val').textContent = v.toFixed(2); + if (sandboxSim) sandboxSim.setRampMu(v); + } + + function sbDecompToggle() { + if (!sandboxSim) return; + sandboxSim.showDecomp = document.getElementById('sb-decomp').checked; + } + + function sbDisplayToggle() { + if (!sandboxSim) return; + sandboxSim.showForces = document.getElementById('sb-forces').checked; + sandboxSim.showVelocity = document.getElementById('sb-vel').checked; + sandboxSim.showFBD = document.getElementById('sb-fbd').checked; + sandboxSim.showEnergy = document.getElementById('sb-energy').checked; + sandboxSim.showTrail = document.getElementById('sb-trail').checked; + } + + function sbTimeScale(v, btn) { + if (!sandboxSim) return; + sandboxSim.timeScale = v; + document.querySelectorAll('.sb-time').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + } + + function sbPreset(name) { + if (!sandboxSim) return; + sandboxSim.preset(name); + // sync world checkboxes + document.getElementById('sb-gravity').checked = sandboxSim.gravity; + document.getElementById('sb-floor').checked = sandboxSim.hasFloor; + document.getElementById('sb-walls').checked = sandboxSim.hasWalls; + document.getElementById('sb-airdrag').checked = sandboxSim.airDrag; + document.getElementById('sl-sb-floormu').value = sandboxSim.floorMu; + document.getElementById('sb-floormu-val').textContent = sandboxSim.floorMu.toFixed(2); + // sync ramp + document.getElementById('sb-ramp').checked = sandboxSim.ramp; + document.getElementById('sb-ramp-block').style.display = sandboxSim.ramp ? '' : 'none'; + document.getElementById('sl-sb-angle').value = sandboxSim.rampAngle; + document.getElementById('sb-angle-val').textContent = sandboxSim.rampAngle + '°'; + document.getElementById('sl-sb-rampmu').value = sandboxSim.rampMu; + document.getElementById('sb-rampmu-val').textContent = sandboxSim.rampMu.toFixed(2); + _sbUpdateUI(sandboxSim.info()); + } + + function sbReset() { + if (!sandboxSim) return; + sandboxSim.reset(); + _sbUpdateUI(sandboxSim.info()); + } + + function _sbUpdateUI(info) { + if (!info) return; + document.getElementById('dbar-l1').textContent = 'Тел / связей'; + document.getElementById('dbar-v1').textContent = info.bodies + ' / ' + (info.springs + info.ropes); + document.getElementById('dbar-l2').textContent = 'KE (Дж)'; + document.getElementById('dbar-v2').textContent = info.KE; + document.getElementById('dbar-l3').textContent = 'PE (Дж)'; + document.getElementById('dbar-v3').textContent = info.PE; + document.getElementById('dbar-l4').textContent = 'ΣF'; + document.getElementById('dbar-v4').textContent = info.netF; + document.getElementById('dbar-l5').textContent = 'Время'; + document.getElementById('dbar-v5').textContent = info.time + ' с'; + } + + /* ── chem sandbox ── */ + diff --git a/frontend/js/labs/normaldist.js b/frontend/js/labs/normaldist.js index 3d6bd6e..dc63ff5 100644 --- a/frontend/js/labs/normaldist.js +++ b/frontend/js/labs/normaldist.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /** * NormalDistSim v2 — интерактивное нормальное распределение * μ, σ · правило 68-95-99.7 · Z-score · закрашивание области @@ -392,3 +392,51 @@ class NormalDistSim { cv.addEventListener('touchend', () => { this.hx = null; this.draw(); }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var ndSim = null; + + function _openNormalDist() { + document.getElementById('sim-topbar-title').textContent = 'Нормальное распределение'; + _simShow('sim-normaldist'); + _registerSimState('normaldist', () => ndSim?.getParams(), st => ndSim?.setParams(st)); + if (_embedMode) _startStateEmit('normaldist'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!ndSim) { + ndSim = new NormalDistSim(document.getElementById('normaldist-canvas')); + ndSim.onUpdate = _ndUpdateUI; + } + ndSim.fit(); + ndSim.draw(); + ndSim._emit(); + })); + } + + function ndParam(name, val) { + const v = parseFloat(val); + const elId = name === 'mu' ? 'nd-mu-val' : 'nd-sigma-val'; + document.getElementById(elId).textContent = v % 1 === 0 ? v : v.toFixed(1); + if (ndSim) ndSim.setParams({ [name]: v }); + } + + function ndShade(mode, btn) { + document.querySelectorAll('.nd-shade-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (ndSim) ndSim.setParams({ shade: mode }); + } + + function ndPreset(mu, sigma) { + document.getElementById('sl-nd-mu').value = mu; document.getElementById('nd-mu-val').textContent = mu; + document.getElementById('sl-nd-sigma').value = sigma; document.getElementById('nd-sigma-val').textContent = sigma; + if (ndSim) ndSim.setParams({ mu, sigma }); + } + + function _ndUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('ndbar-v1', info.mu); + v('ndbar-v2', info.sigma); + v('ndbar-v3', info.peak); + v('ndbar-v4', info.area); + } + + /* ── graph transform ── */ diff --git a/frontend/js/labs/orbitals.js b/frontend/js/labs/orbitals.js index 1d7d2ab..2048934 100644 --- a/frontend/js/labs/orbitals.js +++ b/frontend/js/labs/orbitals.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════ OrbitalsSim — 3D molecular orbitals (Three.js) @@ -340,3 +340,26 @@ class OrbitalsSim { this.renderer.render(this.scene, this.camera); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var orbitalsSim = null; + function _openOrbitals() { + document.getElementById('sim-topbar-title').textContent = 'Молекулярные орбитали'; + _simShow('sim-orbitals'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!orbitalsSim) { + orbitalsSim = new OrbitalsSim(document.getElementById('orbitals-container')); + } else { + orbitalsSim.fit(); + orbitalsSim.play(); + } + })); + } + function setOrbital(mode, btn) { + document.querySelectorAll('.orbital-mode-btn').forEach(b => { b.classList.remove('active'); b.style.borderColor = ''; b.style.color = ''; }); + btn.classList.add('active'); + btn.style.borderColor = '#9B5DE5'; btn.style.color = '#9B5DE5'; + if (orbitalsSim) orbitalsSim.setMode(mode); + } + + /* ── stereometry 3D ── */ diff --git a/frontend/js/labs/pendulum.js b/frontend/js/labs/pendulum.js index 88f337a..4c64a01 100644 --- a/frontend/js/labs/pendulum.js +++ b/frontend/js/labs/pendulum.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ PendulumSim — simple pendulum simulation θ'' = -(g/L)sin(θ) − γ·θ' @@ -402,3 +402,51 @@ class PendulumSim { }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var pendSim = null; + + function _openPendulum() { + document.getElementById('sim-topbar-title').textContent = 'Маятник'; + _simShow('sim-pendulum'); + _registerSimState('pendulum', () => pendSim?.getParams(), st => pendSim?.setParams(st)); + if (_embedMode) _startStateEmit('pendulum'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!pendSim) { + pendSim = new PendulumSim(document.getElementById('pendulum-canvas')); + pendSim.onUpdate = _pendUpdateUI; + } + pendSim.fit(); + pendSim.play(); + })); + } + + function pendParam(name, val) { + const v = parseFloat(val); + const ids = { theta: 'pend-theta-val', L: 'pend-L-val', g: 'pend-g-val', damping: 'pend-damp-val' }; + const el = document.getElementById(ids[name]); + if (el) el.textContent = v % 1 === 0 ? v : v.toFixed(name === 'g' ? 2 : 1); + if (pendSim) pendSim.setParams({ [name]: v }); + } + + function pendPreset(theta, L, g, damp) { + document.getElementById('sl-pend-theta').value = theta; document.getElementById('pend-theta-val').textContent = theta; + document.getElementById('sl-pend-L').value = L; document.getElementById('pend-L-val').textContent = L; + document.getElementById('sl-pend-g').value = g; document.getElementById('pend-g-val').textContent = g; + document.getElementById('sl-pend-damp').value = damp; document.getElementById('pend-damp-val').textContent = damp; + if (pendSim) { + pendSim.setParams({ theta, L, g, damping: damp }); + pendSim.play(); + } + } + + function _pendUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('pendbar-v1', info.angle); + v('pendbar-v2', info.omega); + v('pendbar-v3', info.period); + v('pendbar-v4', info.energy); + } + + /* ── equilibrium ── */ + diff --git a/frontend/js/labs/photosynthesis.js b/frontend/js/labs/photosynthesis.js index 70352a0..37248d1 100644 --- a/frontend/js/labs/photosynthesis.js +++ b/frontend/js/labs/photosynthesis.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ════════════════════════════════════════════════════════════════ PhotosynthesisSim — Фотосинтез и клеточное дыхание Световые реакции · цикл Кальвина · митохондриальное дыхание @@ -809,3 +809,53 @@ function _psRRect(ctx, x, y, w, h, r) { ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openPhotosynthesis(mode) { + document.getElementById('sim-topbar-title').textContent = 'Фотосинтез и дыхание'; + _simShow('sim-photosynthesis'); + _simShow('ctrl-photosynthesis'); + requestAnimationFrame(() => requestAnimationFrame(() => { + const canvas = document.getElementById('photosyn-canvas'); + if (!photosynSim) { + photosynSim = new PhotosynthesisSim(canvas); + photosynSim.onUpdate = _psUpdateUI; + } + photosynSim.fit(); + photosynSim.setMode(mode || 'photo'); + photosynSim.start(); + })); + } + + function psSetMode(mode, btn) { + document.querySelectorAll('.ps-mode-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (photosynSim) photosynSim.setMode(mode); + } + + function psLightChange() { + const v = +document.getElementById('sl-ps-light').value; + document.getElementById('ps-light-val').textContent = v + '%'; + if (photosynSim) photosynSim.setLightIntensity(v); + } + + function psCO2Change() { + const v = +document.getElementById('sl-ps-co2').value; + document.getElementById('ps-co2-val').textContent = v + '%'; + if (photosynSim) photosynSim.setCO2(v); + } + + function psReset() { + if (photosynSim) photosynSim.reset(); + } + + function _psUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('psbar-v1', info.atpRate || '0'); + v('psbar-v2', info.o2 || '0'); + v('psbar-v3', info.co2 || '0'); + v('psbar-v4', info.efficiency ? info.efficiency + '%' : '—'); + v('psbar-v5', info.mode === 'photo' ? 'Фотосинтез' : 'Дыхание'); + } + + /* ── Angry Birds ── */ diff --git a/frontend/js/labs/probability.js b/frontend/js/labs/probability.js index 45455e3..7e84e17 100644 --- a/frontend/js/labs/probability.js +++ b/frontend/js/labs/probability.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ ProbabilitySim — probability & law of large numbers coin flip · single die · two-dice sum @@ -569,3 +569,45 @@ class ProbabilitySim { } if (typeof module !== 'undefined') module.exports = ProbabilitySim; + +/* ─── lab UI init ─────────────────────────────────── */ + function _openProbability() { + document.getElementById('sim-topbar-title').textContent = 'Теория вероятностей'; + _simShow('sim-probability'); + _registerSimState('probability', () => probSim?.getParams(), st => probSim?.setParams(st)); + if (_embedMode) _startStateEmit('probability'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!probSim) { + probSim = new ProbabilitySim(document.getElementById('probability-canvas')); + probSim.onUpdate = _probUpdateUI; + } + probSim.fit(); + probSim.reset(); + probSim.play(); + })); + } + + function probMode(mode, btn) { + document.querySelectorAll('.prob-mode-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (probSim) { probSim.setParams({ mode }); probSim.reset(); probSim.play(); } + } + + function probPreset(mode, trials) { + document.querySelectorAll('.prob-mode-btn').forEach(b => { + b.classList.toggle('active', b.textContent.toLowerCase().includes(mode === 'coin' ? 'монет' : mode === 'dice2' ? '2 куб' : 'кубик')); + }); + if (probSim) { probSim.setParams({ mode, trials }); probSim.reset(); probSim.play(); } + } + + function _probUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('probbar-v1', info.totalTrials); + v('probbar-v2', typeof info.maxDeviation === 'number' ? (info.maxDeviation * 100).toFixed(1) + '%' : '—'); + v('probbar-v3', typeof info.chiSquare === 'number' ? info.chiSquare.toFixed(2) : '—'); + const modeNames = { coin: 'Монета', dice: 'Кубик', dice2: '2 кубика' }; + v('probbar-v4', modeNames[info.mode] || info.mode); + } + + /* ── bohr atom ── */ + diff --git a/frontend/js/labs/projectile.js b/frontend/js/labs/projectile.js index b9cfd9a..aa1be0c 100644 --- a/frontend/js/labs/projectile.js +++ b/frontend/js/labs/projectile.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════════════════════════ ProjectileSim v2 — physics simulation @@ -1061,3 +1061,190 @@ function _projArrow(ctx, x1, y1, x2, y2, color, lw) { ctx.closePath(); ctx.fill(); ctx.restore(); } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openProjectile() { + document.getElementById('sim-topbar-title').textContent = 'Бросок тела'; + _simShow('sim-proj'); + _simShow('ctrl-proj'); + _registerSimState('projectile', () => pSim?.getParams(), st => pSim?.setParams(st)); + if (_embedMode) _startStateEmit('projectile'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!pSim) { + pSim = new ProjectileSim(document.getElementById('proj-canvas')); + pSim.onUpdate = _projUpdateUI; + pSim.onPlayPause = projPlayPause; + } + pSim.fit(); + projParam(); // sync sliders sim + pSim.draw(); + _projUpdateUI(pSim.stats()); + })); + } + + function projPlayPause() { + if (!pSim) return; + if (pSim.playing) { + pSim.pause(); + } else { + pSim.play(); + } + _projSyncPlayBtn(); + } + + function _projSyncPlayBtn() { + /* small topbar button */ + const tb = document.getElementById('proj-play-btn'); + /* big launch button */ + const lb = document.getElementById('proj-launch-main'); + const lbl = document.getElementById('proj-launch-label'); + const lic = document.getElementById('proj-launch-icon'); + if (!pSim) return; + + const tf = pSim._curTFlight(); + const done = !pSim.playing && pSim.t >= tf && pSim.t > 0; + const playing = pSim.playing; + + /* topbar */ + if (tb) { + tb.innerHTML = playing + ? '' + : ''; + tb.title = playing ? 'Пауза' : 'Запустить'; + tb.classList.toggle('active', playing); + } + + /* big button */ + if (lb && lbl && lic) { + lb.classList.toggle('paused', playing); + lb.classList.toggle('done', done && !playing); + if (playing) { + lic.innerHTML = ''; + lbl.textContent = 'Пауза'; + } else if (done) { + lic.innerHTML = ''; + lbl.textContent = 'Повторить'; + } else { + lic.innerHTML = ''; + lbl.textContent = 'Запустить'; + } + } + } + + function projParam() { + const v0 = +document.getElementById('sl-v0').value; + const angle = +document.getElementById('sl-angle').value; + const h0 = +document.getElementById('sl-h0').value; + const g = +document.getElementById('sl-g').value; + + document.getElementById('p-v0').textContent = v0 + ' м/с'; + document.getElementById('p-angle').textContent = angle + '°'; + document.getElementById('p-h0').textContent = h0 + ' м'; + document.getElementById('p-g').textContent = g.toFixed(2) + ' м/с²'; + + if (pSim) { pSim.setParams({ v0, angle, h0, g }); _projSyncPlayBtn(); } + } + + function projPreset(v0, angle, h0, g) { + document.getElementById('sl-v0').value = v0; + document.getElementById('sl-angle').value = angle; + document.getElementById('sl-h0').value = h0; + document.getElementById('sl-g').value = g; + projParam(); + } + + function projToggleDrag(rowEl) { + if (!pSim) return; + pSim.drag = !pSim.drag; + const on = pSim.drag; + rowEl.classList.toggle('active', on); + const tog = document.getElementById('drag-toggle'); + tog.style.background = on ? 'var(--violet)' : 'rgba(255,255,255,0.12)'; + tog.querySelector('span').style.marginLeft = on ? '14px' : '2px'; + document.getElementById('drag-params').style.display = on ? '' : 'none'; + document.getElementById('ps-loss-wrap').style.display = on ? '' : 'none'; + if (on) { + const cd = +document.getElementById('sl-cd').value / 100; + const mass = +document.getElementById('sl-mass').value; + pSim.setParams({ drag: true, Cd: cd, mass }); + } else { + pSim.setParams({ drag: false }); + } + } + + function projCdChange() { + const cd = +document.getElementById('sl-cd').value / 100; + document.getElementById('p-cd').textContent = cd.toFixed(2); + if (pSim) pSim.setParams({ Cd: cd }); + } + + function projMassChange() { + const mass = +document.getElementById('sl-mass').value; + document.getElementById('p-mass').textContent = mass + ' кг'; + if (pSim) pSim.setParams({ mass }); + } + + function projWindChange() { + const wind = +document.getElementById('sl-wind').value; + const label = wind === 0 ? '0 м/с' : (wind > 0 ? ' +' : ' ') + Math.abs(wind) + ' м/с'; + document.getElementById('p-wind').textContent = label; + document.getElementById('ps-loss-wrap').style.display = wind !== 0 ? '' : (pSim && pSim.drag ? '' : 'none'); + if (pSim) { pSim.setParams({ wind }); _projSyncPlayBtn(); } + } + + function projToggleBounce(rowEl) { + if (!pSim) return; + pSim.bounce = !pSim.bounce; + const on = pSim.bounce; + rowEl.classList.toggle('active', on); + const tog = document.getElementById('bounce-toggle'); + tog.style.background = on ? 'rgba(123,245,164,0.8)' : 'rgba(255,255,255,0.12)'; + tog.querySelector('span').style.marginLeft = on ? '14px' : '2px'; + document.getElementById('bounce-params').style.display = on ? '' : 'none'; + const e = +document.getElementById('sl-restitution').value / 100; + pSim.setParams({ bounce: on, restitution: e }); + } + + function projRestitutionChange() { + const e = +document.getElementById('sl-restitution').value / 100; + document.getElementById('p-restitution').textContent = e.toFixed(2); + if (pSim) pSim.setParams({ restitution: e }); + } + + function projSetSpeed(s, el) { + if (pSim) pSim.setSpeed(s); + document.querySelectorAll('.proj-speed').forEach(b => b.classList.remove('active')); + if (el) el.classList.add('active'); + } + + function projSaveGhost() { + if (pSim) pSim.saveGhost(); + } + + function projClearGhosts() { + if (pSim) pSim.clearGhosts(); + } + + function _projUpdateUI(s) { + const fmt = (n, unit) => n < 10000 ? n.toFixed(2) + ' ' + unit : (n/1000).toFixed(2) + ' к' + unit; + document.getElementById('ps-range').textContent = fmt(s.range, 'м'); + document.getElementById('ps-hmax').textContent = fmt(s.hMax, 'м'); + document.getElementById('ps-tf').textContent = s.tf.toFixed(2) + ' с'; + document.getElementById('ps-vland').textContent = fmt(s.vLand, 'м/с'); + document.getElementById('ps-t').textContent = s.t.toFixed(2) + ' с'; + const laEl = document.getElementById('ps-land-angle'); + if (laEl) laEl.textContent = s.landAngle > 0.5 ? s.landAngle.toFixed(1) + '°' : '—'; + if (s.hasMod) { + const lossEl = document.getElementById('ps-loss'); + if (lossEl) { + const sign = s.rangeLoss > 0 ? '+' : ''; + lossEl.textContent = s.rangeLoss !== 0 ? sign + s.rangeLoss + '%' : '0%'; + lossEl.style.color = s.rangeLoss < 0 ? '#EF476F' : '#7BF5A4'; + } + } + _projSyncPlayBtn(); + } + + /* ── collision ── */ + diff --git a/frontend/js/labs/quadratic.js b/frontend/js/labs/quadratic.js index 99ecda2..e70152a 100644 --- a/frontend/js/labs/quadratic.js +++ b/frontend/js/labs/quadratic.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ QuadraticSim — interactive quadratic equation explorer y = ax² + bx + c · discriminant, roots, vertex @@ -432,3 +432,43 @@ class QuadraticSim { cv.addEventListener('touchend', () => { t0 = null; }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openQuadratic() { + document.getElementById('sim-topbar-title').textContent = 'Корни квадратного уравнения'; + _simShow('sim-quadratic'); + _registerSimState('quadratic', () => quadSim?.getParams(), st => quadSim?.setParams(st)); + if (_embedMode) _startStateEmit('quadratic'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!quadSim) { + quadSim = new QuadraticSim(document.getElementById('quadratic-canvas')); + quadSim.onUpdate = _quadUpdateUI; + } + quadSim.fit(); + quadSim.draw(); + quadSim._emit(); + })); + } + + function quadParam(name, val) { + const v = parseFloat(val); + document.getElementById('quad-' + name + '-val').textContent = v % 1 === 0 ? v : v.toFixed(1); + if (quadSim) quadSim.setParams({ [name]: v }); + } + + function quadPreset(a, b, c) { + document.getElementById('sl-quad-a').value = a; document.getElementById('quad-a-val').textContent = a; + document.getElementById('sl-quad-b').value = b; document.getElementById('quad-b-val').textContent = b; + document.getElementById('sl-quad-c').value = c; document.getElementById('quad-c-val').textContent = c; + if (quadSim) quadSim.setParams({ a, b, c }); + } + + function _quadUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('qbar-v1', 'D = ' + info.D); + v('qbar-v2', info.roots); + v('qbar-v3', info.vertex); + v('qbar-v4', info.equation); + } + + /* ── normal distribution ── */ diff --git a/frontend/js/labs/reactions.js b/frontend/js/labs/reactions.js index 0945c40..7f89eba 100644 --- a/frontend/js/labs/reactions.js +++ b/frontend/js/labs/reactions.js @@ -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 ? ' Пауза' : ' Реакции'; + } + + 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 ? '' : ''; + } + + 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' ? '' : '—'; + } + + // _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) + ══════════════════════════════ */ + diff --git a/frontend/js/labs/refraction.js b/frontend/js/labs/refraction.js index 77babb3..f503c43 100644 --- a/frontend/js/labs/refraction.js +++ b/frontend/js/labs/refraction.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ RefractionSim — light refraction simulation (Snell's law) n₁·sin(θ₁) = n₂·sin(θ₂) @@ -496,3 +496,46 @@ class RefractionSim { }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openRefraction() { + document.getElementById('sim-topbar-title').textContent = 'Преломление света'; + _simShow('sim-refraction'); + _registerSimState('refraction', () => refrSim?.getParams(), st => refrSim?.setParams(st)); + if (_embedMode) _startStateEmit('refraction'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!refrSim) { + refrSim = new RefractionSim(document.getElementById('refraction-canvas')); + refrSim.onUpdate = _refrUpdateUI; + } + refrSim.fit(); + refrSim.draw(); + refrSim._emit(); + })); + } + + function refrParam(name, val) { + const v = parseFloat(val); + const ids = { n1: 'refr-n1-val', n2: 'refr-n2-val', angle: 'refr-angle-val' }; + const el = document.getElementById(ids[name]); + if (el) el.textContent = name === 'angle' ? v : v.toFixed(2); + if (refrSim) refrSim.setParams({ [name]: v }); + } + + function refrPreset(n1, n2, angle) { + document.getElementById('sl-refr-n1').value = n1; document.getElementById('refr-n1-val').textContent = n1.toFixed(2); + document.getElementById('sl-refr-n2').value = n2; document.getElementById('refr-n2-val').textContent = n2.toFixed(2); + document.getElementById('sl-refr-angle').value = angle; document.getElementById('refr-angle-val').textContent = angle; + if (refrSim) refrSim.setParams({ n1, n2, angle }); + } + + function _refrUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('refrbar-v1', info.angle1 + '°'); + v('refrbar-v2', info.isTIR ? 'ПВО' : info.angle2 + '°'); + v('refrbar-v3', info.criticalAngle !== null ? info.criticalAngle + '°' : '—'); + v('refrbar-v4', info.isTIR ? 'Да' : 'Нет'); + } + + /* ── probability ── */ + diff --git a/frontend/js/labs/stereo.js b/frontend/js/labs/stereo.js index 43fa899..d336e93 100644 --- a/frontend/js/labs/stereo.js +++ b/frontend/js/labs/stereo.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════════════════ StereoSim — 3D Stereometry (Three.js) @@ -3028,3 +3028,368 @@ class StereoSim { this.renderer.render(this.scene, this.camera); } } + +/* ─── lab UI init ─────────────────────────────────── */ + var stereoSim = null; + + // which params are relevant per figure type + const STEREO_PARAM_MAP = { + cube: ['a'], + parallelepiped: ['a','b','c'], + pyramid: ['a','n','h'], + tetrahedron: ['a'], + cylinder: ['r','h'], + cone: ['r','h'], + trunccone: ['R','r','h'], + sphere: ['r'], + prism: ['a','n','h'], + truncpyramid: ['a','b','n','h'], + octahedron: ['a'], + icosahedron: ['a'], + dodecahedron: ['a'], + }; + + function _openStereo() { + document.getElementById('sim-topbar-title').textContent = 'Стереометрия 3D'; + _simShow('sim-stereo'); + document.getElementById('stereo-stats').style.display = ''; + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!stereoSim) { + stereoSim = new StereoSim(document.getElementById('stereo-container')); + stereoSim.onUpdate = _stereoUpdateUI; + } else { + stereoSim.fit(); + stereoSim.play(); + } + _stereoShowParams(stereoSim.figureType || 'cube'); + _stereoUpdateUI(stereoSim.info()); + _stereoUpdateFormulas(); + })); + } + + function setStereoFigure(type, btn) { + document.querySelectorAll('.stereo-fig-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (stereoSim) { + stereoSim.setFigure(type); + _stereoShowParams(type); + _stereoUpdateFormulas(); + // reset toggles and tool buttons + document.getElementById('sect-toggle').classList.remove('active'); + document.getElementById('stereo-unfold-btn').classList.remove('active'); + document.getElementById('stereo-measure-btn').classList.remove('active'); + // reset element toggles + ['stg-height','stg-apothem','stg-diagonals','stg-midpoints','stg-inscribed','stg-circumscribed','stg-edgelengths'].forEach(id => { + document.getElementById(id)?.classList.remove('on'); + }); + _stereoDeactivateTools(); + } + } + + function _stereoShowParams(type) { + const show = STEREO_PARAM_MAP[type] || ['a']; + ['a','b','c','h','r','R','n'].forEach(k => { + document.getElementById('sp-' + k + '-row').style.display = show.includes(k) ? '' : 'none'; + }); + } + + function stereoParamChange(key, val) { + val = +val; + const label = document.getElementById('sp-' + key + '-val'); + if (label) label.textContent = val; + if (stereoSim) { + stereoSim.setParam(key, val); + _stereoUpdateFormulas(); + } + } + + function stereoOpacityChange(val) { + val = +val; + document.getElementById('sp-opacity-val').textContent = val.toFixed(2); + if (stereoSim) stereoSim.setOpacity(val); + } + + // legacy (used nowhere now but kept for safety) + function stereoToggle(layer, btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (!stereoSim) return; + if (layer === 'edges') stereoSim.toggleEdges(on); + if (layer === 'vertices') stereoSim.toggleVertices(on); + if (layer === 'labels') stereoSim.toggleLabels(on); + if (layer === 'axes') stereoSim.toggleAxes(on); + if (layer === 'grid') stereoSim.toggleGrid(on); + } + + // new toggle-row style + function stereoToggleSt(layer, toggle) { + const on = !toggle.classList.contains('on'); + toggle.classList.toggle('on', on); + if (!stereoSim) return; + if (layer === 'edges') stereoSim.toggleEdges(on); + if (layer === 'vertices') stereoSim.toggleVertices(on); + if (layer === 'labels') stereoSim.toggleLabels(on); + if (layer === 'axes') stereoSim.toggleAxes(on); + if (layer === 'grid') stereoSim.toggleGrid(on); + } + + function stereoToggleElem(layer, toggle) { + const on = !toggle.classList.contains('on'); + toggle.classList.toggle('on', on); + if (!stereoSim) return; + if (layer === 'height') stereoSim.toggleHeight(on); + if (layer === 'apothem') stereoSim.toggleApothem(on); + if (layer === 'diagonals') stereoSim.toggleDiagonals(on); + if (layer === 'midpoints') stereoSim.toggleMidpoints(on); + if (layer === 'inscribed') stereoSim.toggleInscribed(on); + if (layer === 'circumscribed') stereoSim.toggleCircumscribed(on); + if (layer === 'edgelengths') stereoSim.toggleEdgeLengths(on); + } + + // n-stepper for prism/pyramid + function stereoNChange(delta) { + if (!stereoSim) return; + const cur = stereoSim.params.n || 4; + const nv = Math.max(3, Math.min(12, cur + delta)); + document.getElementById('sp-n-val').textContent = nv; + stereoSim.setParam('n', nv); + _stereoUpdateFormulas(); + } + + function stereoSectionToggle(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleSection(on); + } + + function stereoSectionType(t, btn) { + document.querySelectorAll('.stereo-sect-type').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + // Show/hide angle slider for diagonal + document.getElementById('sp-angle-row').style.display = t === 'diagonal' ? '' : 'none'; + if (stereoSim) stereoSim.setSectionType(t); + } + + function stereoSectionHeight(val) { + document.getElementById('sp-sect-val').textContent = val + '%'; + if (stereoSim) stereoSim.setSectionHeight(+val / 100); + } + + function stereoSectionAngle(val) { + document.getElementById('sp-angle-val').textContent = val + '%'; + if (stereoSim) stereoSim.setSectionAngle(+val / 100); + } + + function stereoUnfold(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleUnfold(on); + } + + function _stereoDeactivateTools() { + ['stereo-measure-btn','stereo-point-btn','stereo-connect-btn', + 'stereo-angle-edge-btn','stereo-angle-lp-btn','stereo-angle-dih-btn','stereo-angle-pp-btn','stereo-angle-skew-btn', + 'stereo-mark-tick-btn','stereo-mark-par-btn', + 'stereo-derive-mid-btn','stereo-derive-fc-btn','stereo-derive-alt-btn','stereo-derive-cen-btn'].forEach(id => { + document.getElementById(id)?.classList.remove('active'); + }); + if (stereoSim) { + stereoSim.toggleMeasure(false); + stereoSim.togglePointMode(false); + stereoSim.toggleConnectMode(false); + stereoSim.setAngleMode(null); + stereoSim.setMarkMode(null); + stereoSim.setDeriveMode(null); + } + const hint = document.getElementById('angle-hint'); + if (hint) hint.textContent = ''; + } + + function stereoMeasure(btn) { + const on = !btn.classList.contains('active'); + _stereoDeactivateTools(); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleMeasure(on); + } + + function stereoMeasureUndo() { + if (stereoSim) stereoSim.removeLastMeasurement(); + } + + function stereoMeasureClear() { + if (stereoSim) stereoSim.clearMeasurements(); + } + + function stereoToggleHeight(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleHeight(on); + } + + function stereoToggleApothem(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleApothem(on); + } + + function stereoToggleDiag(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleDiagonals(on); + } + + function stereoToggleMid(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleMidpoints(on); + } + + const ANGLE_HINTS = { + edge: 'Кликните 3 точки: A, B (вершина угла), C', + linePlane: 'Кликните 2 точки (прямая), затем — грань', + dihedral: 'Кликните 2 точки общего ребра двух граней', + pointPlane: 'Кликните точку, затем — грань', + skewLines: 'P1, P2 (прямая 1) → P3, P4 (прямая 2): угол и расстояние', + }; + + function stereoAngleMode(mode, btn) { + const on = !btn.classList.contains('active'); + _stereoDeactivateTools(); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.setAngleMode(on ? mode : null); + const hint = document.getElementById('angle-hint'); + if (hint) hint.textContent = on ? ANGLE_HINTS[mode] : ''; + } + + function stereoAngleClear() { + _stereoDeactivateTools(); + if (stereoSim) { + stereoSim.setAngleMode(null); + stereoSim._clearGroup(stereoSim._angleGroup); + } + } + + /* ── Edge marks ── */ + function stereoMarkMode(mode, btn) { + const on = !btn.classList.contains('active'); + _stereoDeactivateTools(); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.setMarkMode(on ? mode : null); + } + + function stereoMarkClear() { + _stereoDeactivateTools(); + if (stereoSim) stereoSim.clearMarks(); + } + + function stereoToggleEdgeLengths(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleEdgeLengths(on); + } + + /* ── Derived points ── */ + function stereoDerive(mode, btn) { + const on = !btn.classList.contains('active'); + _stereoDeactivateTools(); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.setDeriveMode(on ? mode : null); + } + + function stereoDeriveUndo() { + if (stereoSim) stereoSim.removeLastDerived(); + } + + function stereoDeriveClear() { + _stereoDeactivateTools(); + if (stereoSim) stereoSim.clearDerived(); + } + + function stereoPointMode(btn) { + const on = !btn.classList.contains('active'); + _stereoDeactivateTools(); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.togglePointMode(on); + } + + function stereoConnectMode(btn) { + const on = !btn.classList.contains('active'); + _stereoDeactivateTools(); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleConnectMode(on); + } + + function stereoUndoPoint() { + if (stereoSim) stereoSim.removeLastPoint(); + } + + function stereoClearPoints() { + if (stereoSim) stereoSim.clearCustomPoints(); + _stereoUpdatePointsInfo(); + } + + function stereoInscribed(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleInscribed(on); + } + + function stereoCircumscribed(btn) { + const on = !btn.classList.contains('active'); + btn.classList.toggle('active', on); + if (stereoSim) stereoSim.toggleCircumscribed(on); + } + + function _stereoUpdateFormulas() { + if (!stereoSim) return; + const f = stereoSim.getFormulas(); + const el = document.getElementById('stereo-formulas'); + if (!f || !f.formulas) { el.innerHTML = ''; return; } + const colors = ['#7BF5A4','#60a5fa','#c4b5fd','#fbbf24','#f9a8d4','#F59E0B','#EF476F']; + el.innerHTML = f.formulas.map((s, i) => + '
' + s + '
' + ).join(''); + } + + function _stereoUpdateUI(info) { + if (!info) return; + document.getElementById('stbar-vol').textContent = info.V !== undefined ? info.V.toFixed(2) : '—'; + document.getElementById('stbar-area').textContent = info.S !== undefined ? info.S.toFixed(2) : '—'; + document.getElementById('stbar-side').textContent = info.S_side !== undefined ? info.S_side.toFixed(2) : '—'; + document.getElementById('stbar-h').textContent = info.h !== undefined ? info.h.toFixed(2) : '—'; + document.getElementById('stbar-d').textContent = info.d !== undefined && info.d > 0 ? info.d.toFixed(2) : '—'; + + // Section area + const sectEl = document.getElementById('sect-area-display'); + if (info.sectionArea && info.sectionArea > 0) { + sectEl.style.display = ''; + sectEl.textContent = 'S сечения = ' + info.sectionArea.toFixed(2); + } else { + sectEl.style.display = 'none'; + } + + // Inscribed / Circumscribed radius info + const rInfo = document.getElementById('sphere-radius-info'); + if (rInfo) { + const parts = []; + if (info.inscribedR != null) parts.push('r_вп = ' + info.inscribedR.toFixed(2)); + if (info.circumscribedR != null) parts.push('R_оп = ' + info.circumscribedR.toFixed(2)); + rInfo.textContent = parts.join(' · '); + rInfo.style.display = parts.length ? '' : 'none'; + } + + // Points info + _stereoUpdatePointsInfo(info); + } + + function _stereoUpdatePointsInfo(info) { + const el = document.getElementById('points-info'); + if (!el) return; + if (!info) info = stereoSim?.info(); + if (!info) { el.textContent = ''; return; } + let txt = ''; + if (info.customPoints > 0) txt += `Точек: ${info.customPoints}`; + if (info.connections > 0) txt += ` · Линий: ${info.connections}`; + el.textContent = txt; + } + diff --git a/frontend/js/labs/thinlens.js b/frontend/js/labs/thinlens.js index b384099..3986102 100644 --- a/frontend/js/labs/thinlens.js +++ b/frontend/js/labs/thinlens.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ ThinLensSim — thin lens ray tracing simulation 1/f = 1/d + 1/d' M = -d'/d @@ -444,3 +444,46 @@ class ThinLensSim { }); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openThinLens() { + document.getElementById('sim-topbar-title').textContent = 'Тонкая линза'; + _simShow('sim-thinlens'); + _registerSimState('thinlens', () => lensSim?.getParams(), st => lensSim?.setParams(st)); + if (_embedMode) _startStateEmit('thinlens'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!lensSim) { + lensSim = new ThinLensSim(document.getElementById('thinlens-canvas')); + lensSim.onUpdate = _lensUpdateUI; + } + lensSim.fit(); + lensSim.draw(); + lensSim._emit(); + })); + } + + function lensParam(name, val) { + const v = parseFloat(val); + const ids = { f: 'lens-f-val', d: 'lens-d-val', h: 'lens-h-val' }; + const el = document.getElementById(ids[name]); + if (el) el.textContent = v; + if (lensSim) lensSim.setParams({ [name]: v }); + } + + function lensPreset(f, d, h) { + document.getElementById('sl-lens-f').value = f; document.getElementById('lens-f-val').textContent = f; + document.getElementById('sl-lens-d').value = d; document.getElementById('lens-d-val').textContent = d; + document.getElementById('sl-lens-h').value = h; document.getElementById('lens-h-val').textContent = h; + if (lensSim) lensSim.setParams({ f, d, h }); + } + + function _lensUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('lensbar-v1', info.f); + v('lensbar-v2', info.dPrime === Infinity ? '∞' : info.dPrime); + v('lensbar-v3', info.M === Infinity ? '∞' : info.M); + v('lensbar-v4', info.imageType); + } + + /* ── mirrors ── */ + diff --git a/frontend/js/labs/titration.js b/frontend/js/labs/titration.js index d97be34..95655fa 100644 --- a/frontend/js/labs/titration.js +++ b/frontend/js/labs/titration.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════════════ TitrationSim — acid-base titration simulation Strong acid (HCl) / weak acid (CH₃COOH) + strong base (NaOH) @@ -656,3 +656,55 @@ class TitrationSim { } if (typeof module !== 'undefined') module.exports = TitrationSim; + +/* ─── lab UI init ─────────────────────────────────── */ + function _openTitration() { + document.getElementById('sim-topbar-title').textContent = 'pH и кривая титрования'; + _simShow('sim-titration'); + _registerSimState('titration', () => titrSim?.getParams(), st => titrSim?.setParams(st)); + if (_embedMode) _startStateEmit('titration'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!titrSim) { + titrSim = new TitrationSim(document.getElementById('titration-canvas')); + titrSim.onUpdate = _titrUpdateUI; + } + titrSim.fit(); + titrSim.reset(); + titrSim.play(); + })); + } + + function titrParam(name, val) { + const v = parseFloat(val); + const ids = { acidConc: 'titr-ac-val', baseConc: 'titr-bc-val', acidVol: 'titr-vol-val' }; + const el = document.getElementById(ids[name]); + if (el) el.textContent = name === 'acidVol' ? v : v.toFixed(2); + if (titrSim) titrSim.setParams({ [name]: v }); + } + + function titrIndicator(name, btn) { + document.querySelectorAll('.titr-ind-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (titrSim) titrSim.setParams({ indicator: name }); + } + + function titrPreset(name) { + if (titrSim) { titrSim.preset(name); titrSim.play(); } + const defs = { strong_strong: [0.1,0.1,50], weak_strong: [0.1,0.1,50], concentrated: [0.5,0.5,25] }; + const d = defs[name] || defs.strong_strong; + document.getElementById('sl-titr-ac').value = d[0]; document.getElementById('titr-ac-val').textContent = d[0].toFixed(2); + document.getElementById('sl-titr-bc').value = d[1]; document.getElementById('titr-bc-val').textContent = d[1].toFixed(2); + document.getElementById('sl-titr-vol').value = d[2]; document.getElementById('titr-vol-val').textContent = d[2]; + } + + function _titrUpdateUI(info) { + const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; + v('titrbar-v1', info.pH); + v('titrbar-v2', info.baseAdded + ' мл'); + v('titrbar-v3', info.eqPoint + ' мл'); + const indNames = { phenolphthalein: 'Фенолф.', methyl_orange: 'Метилор.', litmus: 'Лакмус' }; + v('titrbar-v4', indNames[info.indicator] || info.indicator); + } + + /* ── refraction ── */ + diff --git a/frontend/js/labs/triangle.js b/frontend/js/labs/triangle.js index 574aa9c..82a34ad 100644 --- a/frontend/js/labs/triangle.js +++ b/frontend/js/labs/triangle.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ══════════════════════════════════════════════════════ TriangleSim — interactive triangle geometry simulation Draggable vertices A / B / C, toggleable layers: @@ -959,3 +959,93 @@ class TriangleSim { ctx.restore(); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openTriangle() { + document.getElementById('sim-topbar-title').textContent = 'Геометрия треугольника'; + _simShow('sim-tri'); + _simShow('ctrl-tri'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!tSim) { + tSim = new TriangleSim(document.getElementById('tri-canvas')); + tSim.onUpdate = _triUpdateUI; + } + tSim.fit(); + tSim.draw(); + _triUpdateUI(tSim.stats()); + })); + } + + function triToggle(layer, rowEl) { + if (!tSim) return; + tSim.toggleLayer(layer); + rowEl.classList.toggle('active', tSim.layers[layer]); + } + + function _triUpdateUI(s) { + const f2 = v => v.toFixed(2); + const deg = v => v.toFixed(1) + '°'; + const unit = v => f2(v) + ' ед'; + + // panel + document.getElementById('ts-a').textContent = unit(s.a); + document.getElementById('ts-b').textContent = unit(s.b); + document.getElementById('ts-c').textContent = unit(s.c); + document.getElementById('ts-A').textContent = deg(s.A); + document.getElementById('ts-B').textContent = deg(s.B); + document.getElementById('ts-C').textContent = deg(s.C); + document.getElementById('ts-S').textContent = f2(s.S) + ' ед²'; + document.getElementById('ts-P').textContent = unit(s.perim); + document.getElementById('ts-R').textContent = unit(s.R); + document.getElementById('ts-r').textContent = unit(s.r); + document.getElementById('ts-type').textContent = s.type; + + // stats bar + document.getElementById('tbar-a').textContent = unit(s.a); + document.getElementById('tbar-b').textContent = unit(s.b); + document.getElementById('tbar-c').textContent = unit(s.c); + document.getElementById('tbar-S').textContent = f2(s.S) + ' ед²'; + document.getElementById('tbar-P').textContent = unit(s.perim); + document.getElementById('tbar-Rr').textContent = f2(s.R) + ' / ' + f2(s.r); + } + + /* ── geometry (planimetry) ── */ + + const _GEO_HINTS = { + select: 'Клик — выбрать объект, перетащи точку для перемещения', + point: 'Клик — поставить точку', + segment: 'Кликни 2 точки для отрезка', + line: 'Кликни 2 точки для прямой', + ray: 'Кликни: начало, затем направление', + circle: 'Клик — центр; второй клик — радиус', + triangle: 'Кликни 3 точки для треугольника', + quad: 'Кликни 4 точки для четырёхугольника', + polygon: 'Кликай точки; двойной клик или Enter — завершить', + midpoint: 'Кликни 2 точки — получи середину отрезка', + perpbisect: 'Кликни 2 точки — получи серединный перпендикуляр', + anglebisect: 'Кликни: точку A, затем вершину угла, затем точку B', + parallel: 'Сначала кликни на прямую/отрезок, затем на точку', + perpendicular:'Сначала кликни на прямую/отрезок, затем на точку', + intersect: 'Кликни на первую прямую, затем на вторую', + foot: 'Сначала кликни на прямую/отрезок', + circumcircle: 'Кликни 3 точки треугольника — получи описанную окружность', + incircle: 'Кликни 3 точки треугольника — получи вписанную окружность', + reflect: 'Сначала кликни на ось симметрии (прямую/отрезок)', + ngon: 'Клик — центр правильного многоугольника; второй клик — вершина', + tangent: 'Кликни на окружность — построим касательные', + translate: 'Кликни начало вектора A', + tick: 'Кликни на отрезок или сторону — добавить штрих (1–3; ещё раз — убрать)', + arcmark: 'Кликни на вершину полигона — добавить дугу (1–3; ещё раз — убрать)', + parallelmark: 'Кликни на отрезок или сторону — добавить метку параллельности (1–2; ещё раз — убрать)', + altitude: 'Кликни на вершину треугольника — построим высоту из неё', + median: 'Кликни на вершину треугольника — построим медиану из неё', + centroid: 'Кликни на треугольник или внутри него — построим все 3 медианы и центроид G', + orthocenter: 'Кликни на треугольник или внутри него — построим все 3 высоты и ортоцентр H', + thales: 'Кликни центр подобия O (начало лучей)', + midline: 'Кликни вершину A треугольника', + parallelogram:'Кликни вершину A параллелограмма', + diagonal: 'Кликни внутри четырёхугольника — построим диагонали', + scale: 'Кликни центр подобия O', + }; + diff --git a/frontend/js/labs/trigcircle.js b/frontend/js/labs/trigcircle.js index c51b339..86151c1 100644 --- a/frontend/js/labs/trigcircle.js +++ b/frontend/js/labs/trigcircle.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════════════════════════════════ TrigCircleSim — premium interactive unit-circle + graph visualisation @@ -967,3 +967,83 @@ class TrigCircleSim { } if (typeof window !== 'undefined') window.TrigCircleSim = TrigCircleSim; + +/* ─── lab UI init ─────────────────────────────────── */ + var trigSim = null; + + function _openTrigCircle() { + document.getElementById('sim-topbar-title').textContent = 'Тригонометрическая окружность'; + _simShow('sim-trigcircle'); + _simShow('ctrl-trigcircle'); + + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!trigSim) { + trigSim = new TrigCircleSim(document.getElementById('trigcircle-canvas')); + trigSim.onUpdate = _trigUpdateUI; + } + trigSim.fit(); + trigSim.start(); + _trigUpdateUI(trigSim.stats()); + })); + } + + function trigToggle(layer, rowEl) { + if (!trigSim) return; + const isActive = rowEl.classList.toggle('active'); + trigSim.toggleLayer(layer, isActive); + } + + function trigSetGraphFn(fn, el) { + if (!trigSim) return; + document.querySelectorAll('.trig-fn-btn').forEach(b => b.classList.remove('active')); + el.classList.add('active'); + trigSim.setGraphFn(fn); + } + + function trigGoTo(rad) { + if (!trigSim) return; + trigSim.goToAngle(rad); + } + + function trigReset() { + if (!trigSim) return; + trigSim.setAngle(Math.PI / 4); + } + + function _trigUpdateUI(s) { + const _f = v => { + if (v === undefined) return '—'; + const a = Math.abs(v), sg = v < 0 ? '−' : ''; + if (a < 5e-4) return '0'; + if (Math.abs(a - 0.5) < 1e-3) return sg + '½'; + if (Math.abs(a - 1) < 1e-3) return sg + '1'; + if (Math.abs(a - Math.SQRT2/2) < 1e-3) return sg + '√2/2'; + if (Math.abs(a - Math.sqrt(3)/2) < 1e-3) return sg + '√3/2'; + if (Math.abs(a - Math.sqrt(3)/3) < 1e-3) return sg + '√3/3'; + if (Math.abs(a - Math.sqrt(3)) < 1e-3) return sg + '√3'; + return v.toFixed(4); + }; + const degStr = s.deg.toFixed(1) + '°'; + + // Panel values (nice fractions) + document.getElementById('trig-v-sin').textContent = _f(s.sin); + document.getElementById('trig-v-cos').textContent = _f(s.cos); + document.getElementById('trig-v-tan').textContent = _f(s.tan); + document.getElementById('trig-v-cot').textContent = _f(s.cot); + + // Angle badge + document.getElementById('trig-angle-badge').innerHTML = + `${degStr} = ${s.radLabel}
${s.angle.toFixed(4)} рад`; + + // Stats bar (nice fractions) + document.getElementById('trigbar-angle').textContent = degStr; + document.getElementById('trigbar-sin').textContent = _f(s.sin); + document.getElementById('trigbar-cos').textContent = _f(s.cos); + document.getElementById('trigbar-tan').textContent = _f(s.tan); + document.getElementById('trigbar-cot').textContent = _f(s.cot); + document.getElementById('trigbar-quad').textContent = ['I', 'II', 'III', 'IV'][s.quadrant - 1]; + } + + /* ── KaTeX live preview ── */ + + /** Convert user ascii expression LaTeX string for KaTeX preview */ diff --git a/frontend/js/labs/waves.js b/frontend/js/labs/waves.js index c2be230..1e597e0 100644 --- a/frontend/js/labs/waves.js +++ b/frontend/js/labs/waves.js @@ -1,4 +1,4 @@ -'use strict'; +'use strict'; /* ═══════════════════════════════════════════ WavesSim v2 — Волны и звук Modes: transverse | longitudinal | superposition | standing @@ -454,3 +454,94 @@ class WavesSim { _emit() { if (this.onUpdate) this.onUpdate(this.info()); } } + +/* ─── lab UI init ─────────────────────────────────── */ + function _openWaves() { + document.getElementById('sim-topbar-title').textContent = 'Волны и звук'; + document.getElementById('ctrl-waves').style.display = ''; + _simShow('sim-waves'); + _registerSimState('waves', () => wavesSim?.getParams(), + st => { if (wavesSim) { if (st.mode) wavesSim.setMode(st.mode); wavesSim.setParams(st); } }); + if (_embedMode) _startStateEmit('waves'); + requestAnimationFrame(() => requestAnimationFrame(() => { + if (!wavesSim) { + wavesSim = new WavesSim(document.getElementById('waves-canvas')); + wavesSim.onUpdate = _wavesUpdateUI; + } + wavesSim.fit(); + wavesSim.reset(); + wavesSim.play(); + _wavesUpdateUI(wavesSim.info()); + })); + } + + function wavesMode(mode, btn) { + document.querySelectorAll('.wave-mode-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + document.getElementById('waves-w2-section').style.display = mode === 'superposition' ? '' : 'none'; + document.getElementById('waves-n-section').style.display = mode === 'standing' ? '' : 'none'; + if (wavesSim) wavesSim.setMode(mode); + } + + function wavesParam(name, val) { + const v = parseFloat(val); + const el = (id, txt) => { const e = document.getElementById(id); if (e) e.textContent = txt; }; + if (name === 'A1') el('waves-A1-val', v); + if (name === 'f1') el('waves-f1-val', v.toFixed(1) + ' Гц'); + if (name === 'phi1') el('waves-phi1-val', v.toFixed(1)); + if (name === 'A2') el('waves-A2-val', v); + if (name === 'f2') el('waves-f2-val', v.toFixed(1) + ' Гц'); + if (name === 'phi2') el('waves-phi2-val', v.toFixed(1)); + if (name === 'speed') el('waves-speed-val', '\u00d7' + v.toFixed(1)); + if (wavesSim) wavesSim.setParams({ [name]: v }); + } + + function wavesN(n, btn) { + document.querySelectorAll('.wave-n-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + if (wavesSim) wavesSim.setParams({ n }); + } + + function wavesPreset(name) { + const presets = { + constructive: { A1: 50, f1: 1.0, phi1: 0, A2: 50, f2: 1.0, phi2: 0 }, + destructive: { A1: 50, f1: 1.0, phi1: 0, A2: 50, f2: 1.0, phi2: 3.14 }, + beats: { A1: 50, f1: 1.0, phi1: 0, A2: 50, f2: 1.3, phi2: 0 }, + }; + const p = presets[name]; if (!p) return; + document.getElementById('sl-waves-A1').value = p.A1; + document.getElementById('sl-waves-f1').value = p.f1; + document.getElementById('sl-waves-phi1').value = p.phi1; + document.getElementById('sl-waves-A2').value = p.A2; + document.getElementById('sl-waves-f2').value = p.f2; + document.getElementById('sl-waves-phi2').value = p.phi2; + document.getElementById('waves-A1-val').textContent = p.A1; + document.getElementById('waves-f1-val').textContent = p.f1.toFixed(1) + ' Гц'; + document.getElementById('waves-phi1-val').textContent = p.phi1.toFixed(1); + document.getElementById('waves-A2-val').textContent = p.A2; + document.getElementById('waves-f2-val').textContent = p.f2.toFixed(1) + ' Гц'; + document.getElementById('waves-phi2-val').textContent = p.phi2.toFixed(1); + if (wavesSim) wavesSim.setParams({ A1: p.A1, f1: p.f1, phi1: p.phi1, A2: p.A2, f2: p.f2, phi2: p.phi2 }); + } + + function wavesPlayPause() { + if (!wavesSim) return; + const btn = document.getElementById('waves-play-btn'); + if (wavesSim._paused) { + wavesSim.play(); + btn.innerHTML = ''; + } else { + wavesSim.pause(); + btn.innerHTML = ''; + } + } + + function _wavesUpdateUI(info) { + const v = (id, val) => { const e = document.getElementById(id); if (e) e.textContent = val; }; + v('wavesbar-T', info.T); + v('wavesbar-lam', info.lambda); + v('wavesbar-v', info.v); + v('wavesbar-f', (+info.f1).toFixed(1)); + } + + /* ── crystal lattice (3D) ── */