'use strict'; // Lab simulation initializers — extracted from lab.html // Depends on engine files in /js/labs/ and shared globals from lab.html /* ════════════════════════════════ GRAPH SIMULATOR ════════════════════════════════ */ var FN_COLORS = ['#9B5DE5', '#06D6E0', '#F15BB5']; var gSim = null; var pSim = null; var cSim = null; var tSim = null; var mSim = null; var gasSim = null; var brownSim = null; var statesSim = null; var diffSim = null; var rdxSim = null; var ioxSim = null; var chemSandSim = null; var cellDivSim = null; var photosynSim = null; var quadSim = null; var eqSim = null; var lensSim = null; var titrSim = null; var refrSim = null; var probSim = null; var bohrSim = null; var elecSim = null; var wavesSim = null; var geomSim = null; var ALL_SIM_BODIES = ['sim-graph','sim-proj','sim-coll','sim-tri','sim-trigcircle','sim-mag', 'sim-molphys', 'sim-coulomb','sim-circuit','sim-chemistry','sim-dynamics', 'sim-crystal','sim-orbitals','sim-stereo','sim-chemsandbox', 'sim-celldivision','sim-photosynthesis','sim-angrybirds', 'sim-quadratic','sim-normaldist','sim-graphtransform', 'sim-pendulum','sim-equilibrium','sim-thinlens','sim-titration', 'sim-refraction','sim-mirrors','sim-isoprocess','sim-probability','sim-bohratom','sim-electrolysis', 'sim-waves','sim-hydro','sim-geometry']; var ALL_CTRL_BARS = ['ctrl-graph','ctrl-proj','ctrl-coll','ctrl-tri','ctrl-trigcircle','ctrl-mag', 'ctrl-molphys', 'ctrl-coulomb','ctrl-circuit','ctrl-chemistry','ctrl-dynamics','ctrl-chemsandbox', 'ctrl-celldivision','ctrl-photosynthesis','ctrl-angrybirds','ctrl-waves','ctrl-hydro', 'ctrl-geometry']; /* ── sim routing ── */ function openSim(id) { if (_disabledSimIds.has(id.split(':')[0])) return; document.getElementById('lab-home').style.display = 'none'; document.getElementById('lab-sim').classList.add('open'); // hide all inner bodies + controls ALL_SIM_BODIES.forEach(bid => document.getElementById(bid).style.display = 'none'); ALL_CTRL_BARS.forEach(bid => document.getElementById(bid).style.display = 'none'); // load theory for this sim loadTheory(id.includes(':') ? id.split(':')[0] : id); if (id === 'graph') _openGraph(); if (id === 'projectile') _openProjectile(); if (id === 'collision') _openCollision(); if (id === 'triangle') _openTriangle(); if (id === 'trigcircle') _openTrigCircle(); if (id === 'magnetic') _openMagnetic(); if (id === 'molphys') _openMolPhys(); if (id.startsWith('molphys:')) { _openMolPhys(id.split(':')[1]); } if (id === 'coulomb') _openCoulomb(); if (id === 'circuit') _openCircuit(); if (id === 'chemistry') _openChemistry(); if (id.startsWith('chemistry:')) { _openChemistry(id.split(':')[1]); } if (id === 'dynamics') _openDynamics(); if (id.startsWith('dynamics:')) { _openDynamics(id.split(':')[1]); } if (id === 'crystal') _openCrystal(); if (id === 'orbitals') _openOrbitals(); if (id === 'stereo') _openStereo(); if (id === 'chemsandbox') _openChemSandbox(); if (id === 'celldivision') _openCellDivision(); if (id === 'photosynthesis') _openPhotosynthesis(); if (id === 'angrybirds') _openAngryBirds(); if (id === 'quadratic') _openQuadratic(); if (id === 'normaldist') _openNormalDist(); if (id === 'graphtransform') _openGraphTransform(); if (id === 'pendulum') _openPendulum(); if (id === 'equilibrium') _openEquilibrium(); if (id === 'thinlens') _openThinLens(); if (id === 'mirrors') _openMirror(); if (id === 'isoprocess') _openIsoprocess(); if (id === 'titration') _openTitration(); if (id === 'refraction') _openRefraction(); if (id === 'probability') _openProbability(); if (id === 'bohratom') _openBohrAtom(); if (id === 'electrolysis') _openElectrolysis(); if (id === 'waves') _openWaves(); if (id === 'hydrostatics') _openHydro(); if (id.startsWith('hydrostatics:')) _openHydro(id.split(':')[1]); if (id === 'geometry') _openGeometry(); } function _simShow(elId) { // restore display:flex (overrides the display:none set above) document.getElementById(elId).style.display = 'flex'; } /* ── Touch-to-mouse bridge + ResizeObserver for canvas simulations ── */ function _addTouchSupport(canvas, sim) { let _tx0 = 0, _ty0 = 0, _tyLast = 0, _isScroll = false; function _syn(t) { return { clientX: t.clientX, clientY: t.clientY, button: 0 }; } canvas.addEventListener('touchstart', function(e) { e.preventDefault(); const t = e.changedTouches[0]; _tx0 = t.clientX; _ty0 = t.clientY; _tyLast = t.clientY; _isScroll = false; if (sim.handleMouseDown) sim.handleMouseDown(_syn(t)); // if no drag started (touched empty area), treat as scroll gesture if (!sim._drag) _isScroll = true; }, { passive: false }); canvas.addEventListener('touchmove', function(e) { e.preventDefault(); const t = e.changedTouches[0]; if (_isScroll && sim.handleWheel) { const dy = _tyLast - t.clientY; sim.handleWheel({ clientY: t.clientY, deltaY: dy * 2, preventDefault: function(){} }); } else if (sim.handleMouseMove) { sim.handleMouseMove(_syn(t)); } _tyLast = t.clientY; }, { passive: false }); canvas.addEventListener('touchend', function(e) { e.preventDefault(); const t = e.changedTouches[0]; const dist = Math.hypot(t.clientX - _tx0, t.clientY - _ty0); if (sim.handleMouseUp) sim.handleMouseUp(_syn(t)); if (dist < 10 && sim.handleClick) sim.handleClick(_syn(t)); _isScroll = false; }, { passive: false }); canvas.addEventListener('touchcancel', function(e) { if (e.changedTouches[0] && sim.handleMouseUp) sim.handleMouseUp(_syn(e.changedTouches[0])); _isScroll = false; }, { passive: false }); // ResizeObserver: refit canvas on orientation change / resize if (window.ResizeObserver && sim.fit) { const ro = new ResizeObserver(function() { sim.fit(); if (sim.draw) sim.draw(); }); ro.observe(canvas.parentElement || canvas); } } function closeSim() { if (pSim) pSim.pause(); if (cSim) cSim.pause(); if (mSim && mSim.particleOn) mSim.toggleParticle(); if (gasSim) gasSim.stop(); if (brownSim) brownSim.stop(); if (statesSim) statesSim.stop(); if (diffSim) diffSim.stop(); if (cirSim) cirSim.destroy(); if (reacSim) reacSim.stop(); if (flaskSim) flaskSim.stop(); if (rdxSim) rdxSim.stop(); if (ioxSim) ioxSim.stop(); if (newtonSim) newtonSim.stop(); if (sandboxSim) sandboxSim.destroy(); if (crystalSim) crystalSim.stop(); if (orbitalsSim) orbitalsSim.stop(); if (stereoSim) stereoSim.stop(); if (chemSandSim) chemSandSim.stop(); if (cellDivSim) cellDivSim.stop(); if (photosynSim) photosynSim.stop(); if (angryBirdsSim) angryBirdsSim.stop(); if (trigSim) trigSim.stop(); if (pendSim) pendSim.stop(); if (eqSim) eqSim.stop(); if (titrSim) titrSim.stop(); if (probSim) probSim.stop(); if (bohrSim) bohrSim.stop(); if (elecSim) elecSim.stop(); if (wavesSim) wavesSim.stop(); // tSim, csSim, quadSim, ndSim, gtSim, lensSim, refrSim have no animation loops — nothing to stop document.getElementById('stereo-stats').style.display = 'none'; document.getElementById('lab-sim').classList.remove('open'); document.getElementById('lab-home').style.display = ''; // close theory panel _theoryOpen = false; document.getElementById('theory-panel').classList.remove('open'); if (window.lucide) lucide.createIcons(); } /* ── 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: { title: 'График функции', sections: [ { head: 'Линейная функция', formula: 'y = kx + b', text: 'k — угловой коэффициент (наклон), b — свободный член (сдвиг по оси Y).' }, { head: 'Квадратичная функция', formula: 'y = ax^2 + bx + c', text: 'Парабола. Ветви вверх при a>0, вниз при a<0. Вершина: x = -b/(2a).' }, { head: 'Тригонометрия', formula: 'y = A\\sin(\\omega x + \\varphi)', vars: [['A','амплитуда'],['ω','частота'],['φ','начальная фаза']] }, ] }, projectile: { title: 'Бросок тела', sections: [ { head: 'Координаты', formula: 'x = v_0 \\cos\\alpha \\cdot t', text: '' }, { formula: 'y = h_0 + v_0 \\sin\\alpha \\cdot t - \\frac{g t^2}{2}' }, { head: 'Дальность', formula: 'L = \\frac{v_0^2 \\sin 2\\alpha}{g}', text: 'Максимальная дальность при α = 45° (без воздуха).' }, { head: 'Макс. высота', formula: 'H = h_0 + \\frac{v_0^2 \\sin^2\\alpha}{2g}' }, { head: 'Сила сопротивления', formula: 'F_{drag} = \\frac{1}{2} C_d \\rho A v^2', vars: [['Cd','коэф. лобового сопротивления'],['ρ','плотность воздуха, 1.225 кг/м³'],['A','площадь сечения'],['v','скорость']] }, { text: 'С воздухом траектория асимметрична: снижение дальности, более крутой спуск.' }, { head: 'Переменные', vars: [['v₀','начальная скорость, м/с'],['α','угол броска'],['h₀','начальная высота, м'],['g','ускорение свободного падения, 9.81 м/с²']] }, ] }, collision: { title: 'Столкновение шаров', sections: [ { head: 'Закон сохранения импульса', formula: 'm_1 v_1 + m_2 v_2 = m_1 v_1\' + m_2 v_2\'' }, { head: 'Закон сохранения энергии (упругий)', formula: '\\frac{m_1 v_1^2}{2} + \\frac{m_2 v_2^2}{2} = \\frac{m_1 v_1\'^2}{2} + \\frac{m_2 v_2\'^2}{2}' }, { head: 'Коэффициент восстановления', formula: 'e = \\frac{v_2\' - v_1\'}{v_1 - v_2}', text: 'e=1 — упругий, e=0 — абсолютно неупругий удар.' }, ] }, magnetic: { title: 'Магнитное поле', sections: [ { head: 'Поле прямого тока', formula: 'B = \\frac{\\mu_0 I}{2\\pi r}', vars: [['μ₀','4π·10⁻⁷ Тл·м/А'],['I','сила тока, А'],['r','расстояние от провода, м']] }, { head: 'Суперпозиция', formula: '\\vec{B} = \\sum_i \\vec{B}_i', text: 'Результирующее поле — векторная сумма полей всех проводов.' }, { head: 'Сила Лоренца', formula: '\\vec{F} = q\\vec{v} \\times \\vec{B}', text: 'Заряженная частица движется по окружности в однородном поле.' }, ] }, coulomb: { title: 'Закон Кулона', sections: [ { head: 'Сила взаимодействия', formula: 'F = k \\frac{|q_1 q_2|}{r^2}', vars: [['k','8.99·10⁹ Н·м²/Кл²'],['q','заряд, Кл'],['r','расстояние, м']] }, { head: 'Напряжённость поля', formula: '\\vec{E} = k \\frac{q}{r^2} \\hat{r}', text: 'Вектор направлен от «+» и к «−» заряду.' }, { head: 'Потенциал', formula: '\\varphi = k \\frac{q}{r}', text: 'Эквипотенциальные линии — окружности вокруг заряда.' }, ] }, circuit: { title: 'Электрические цепи', sections: [ { head: 'Закон Ома', formula: 'I = \\frac{U}{R}', vars: [['I','ток, А'],['U','напряжение, В'],['R','сопротивление, Ом']] }, { head: 'Последовательное', formula: 'R_{\\Sigma} = R_1 + R_2 + \\ldots' }, { head: 'Параллельное', formula: '\\frac{1}{R_{\\Sigma}} = \\frac{1}{R_1} + \\frac{1}{R_2} + \\ldots' }, { head: 'Закон Кирхгофа (токи)', formula: '\\sum I_{вх} = \\sum I_{вых}', text: 'Алгебраическая сумма токов в узле равна нулю.' }, { head: 'Ёмкость конденсатора', formula: 'Q = CU', vars: [['C','ёмкость, Ф'],['Q','заряд, Кл']] }, ] }, dynamics: { title: 'Динамика', sections: [ { head: 'I закон Ньютона (инерция)', text: 'Тело сохраняет состояние покоя или прямолинейного движения, пока на него не действуют внешние силы.' }, { head: 'II закон Ньютона', formula: '\\vec{F} = m\\vec{a}', text: 'Ускорение тела прямо пропорционально силе и обратно пропорционально массе.' }, { head: 'III закон Ньютона', formula: '\\vec{F}_{12} = -\\vec{F}_{21}', text: 'Тела действуют друг на друга с силами, равными по модулю и противоположными по направлению.' }, { head: 'Импульс', formula: '\\vec{p} = m\\vec{v}', text: 'Закон сохранения: суммарный импульс замкнутой системы постоянен.' }, { head: 'Сила трения', formula: 'F_{\\text{тр}} = \\mu N', text: 'Направлена против движения. N — сила нормальной реакции опоры.' }, { head: 'Кинетическая энергия', formula: 'E_к = \\frac{1}{2}mv^2', text: 'Энергия движущегося тела.' }, { head: 'Потенциальная энергия', formula: 'E_п = mgh', text: 'Энергия тела в поле тяжести относительно опоры.' }, { head: 'Закон сохранения энергии', formula: 'E_к + E_п + Q = \\text{const}', text: 'Полная энергия системы сохраняется. Q — потери на трение и неупругие удары.' }, { head: 'Наклонная плоскость', formula: 'a = g(\\sin\\alpha - \\mu\\cos\\alpha)', text: 'Тело скользит вниз, если mg·sinα > μ·mg·cosα. Иначе трение удерживает.' }, { head: 'Разложение сил на горке', formula: 'F_{\\parallel} = mg\\sin\\alpha,\\quad N = mg\\cos\\alpha', text: 'Сила тяжести раскладывается на составляющую вдоль склона и нормальную.' }, ] }, triangle: { title: 'Геометрия треугольника', sections: [ { head: 'Медиана', text: 'Отрезок от вершины до середины противоположной стороны. Три медианы пересекаются в центроиде (делят друг друга 2:1).' }, { head: 'Высота', text: 'Перпендикуляр из вершины к противоположной стороне. Пересечение — ортоцентр.' }, { head: 'Описанная окружность', formula: 'R = \\frac{abc}{4S}', text: 'Проходит через все три вершины. Центр — пересечение серединных перпендикуляров.' }, { head: 'Вписанная окружность', formula: 'r = \\frac{S}{p}', vars: [['S','площадь'],['p','полупериметр']] }, { head: 'Теорема синусов', formula: '\\frac{a}{\\sin A} = \\frac{b}{\\sin B} = \\frac{c}{\\sin C} = 2R', text: 'Отношение стороны к синусу противолежащего угла одинаково и равно диаметру описанной окружности.' }, { head: 'Теорема косинусов', formula: 'c^2 = a^2 + b^2 - 2ab\\cos C', text: 'Обобщение теоремы Пифагора на произвольный треугольник.' }, { head: 'Теорема Пифагора', formula: 'a^2 + b^2 = c^2', text: 'В прямоугольном треугольнике квадрат гипотенузы равен сумме квадратов катетов.' }, ] }, molphys: { title: 'Молекулярная физика', sections: [ { head: 'Уравнение состояния', formula: 'PV = nRT', vars: [['P','давление, Па'],['V','объём, м³'],['n','количество вещества, моль'],['R','8.314 Дж/(моль·К)'],['T','температура, К']] }, { head: 'Средняя кинетическая энергия', formula: '\\langle E_к \\rangle = \\frac{3}{2} k_B T', text: 'kB = 1.38·10⁻²³ Дж/К — постоянная Больцмана.' }, { head: 'Распределение Максвелла', text: 'С ростом T максимум кривой распределения скоростей сдвигается вправо и уширяется.' }, { head: 'Среднеквадратичное смещение', formula: '\\langle r^2 \\rangle = 2dDt', vars: [['d','размерность (2 для 2D)'],['D','коэф. диффузии'],['t','время']] }, { head: 'Формула Эйнштейна', formula: 'D = \\frac{k_B T}{6\\pi \\eta R}', vars: [['η','вязкость среды'],['R','радиус частицы']] }, { head: 'Потенциал Леннарда-Джонса', formula: 'U(r) = 4\\varepsilon \\left[\\left(\\frac{\\sigma}{r}\\right)^{12} - \\left(\\frac{\\sigma}{r}\\right)^{6}\\right]', text: 'ε — глубина ямы, σ — эффективный размер частицы.' }, { head: 'Фазовые переходы', text: 'При повышении T: кристалл жидкость (плавление) газ (испарение). Обратно — конденсация, кристаллизация.' }, { head: 'Закон Фика', formula: 'J = -D \\frac{\\partial c}{\\partial x}', vars: [['J','поток вещества'],['D','коэф. диффузии'],['c','концентрация']] }, { head: 'Энтропия', formula: 'S = k_B \\ln W', text: 'Смешивание газов — необратимый процесс, энтропия растёт.' }, ] }, chemistry: { title: 'Химические реакции', sections: [ { head: 'Закон действующих масс', formula: 'v = k [A]^a [B]^b', vars: [['k','константа скорости'],['[A],[B]','концентрации'],['a,b','порядки реакции']] }, { head: 'Уравнение Аррениуса', formula: 'k = A \\cdot e^{-E_a / RT}', vars: [['Eₐ','энергия активации, Дж/моль'],['A','предэкспоненциальный множитель']] }, { head: 'Реакция металл + кислота', formula: 'Zn + 2HCl \\to ZnCl_2 + H_2\\uparrow' }, { head: 'Ряд активности', text: 'Li > K > Ca > Na > Mg > Al > Zn > Fe > Ni > Sn > Pb > H₂ > Cu > Ag > Au' }, { head: 'Окисление', formula: 'Red \\to Ox + ne^-', text: 'Восстановитель отдаёт электроны, степень окисления растёт.' }, { head: 'Восстановление', formula: 'Ox + ne^- \\to Red', text: 'Окислитель принимает электроны, степень окисления падает.' }, { head: 'Электронный баланс', text: 'Число отданных e⁻ = числу принятых e⁻.' }, { head: 'Ионный обмен', text: 'Реакция идёт до конца, если образуется: осадок (), газ () или слабый электролит (H₂O).' }, { head: 'Полное ионное уравнение', text: 'Все сильные электролиты записываются в виде ионов. Краткое — без ионов-наблюдателей.' }, ] }, crystal: { title: 'Кристаллическая решётка', sections: [ { head: 'Ионная решётка (NaCl)', text: 'В узлах — катионы Na⁺ и анионы Cl⁻. Электростатическое притяжение. Высокая температура плавления.' }, { head: 'Ковалентная (алмаз)', text: 'Каждый атом C связан с четырьмя соседями sp³-гибридизацией. Самый твёрдый минерал.' }, { head: 'ОЦК (металл)', text: 'Объёмно-центрированная кубическая. 8 атомов в вершинах + 1 в центре куба. Fe, Cr, W.' }, { head: 'ГЦК (металл)', text: 'Гранецентрированная кубическая. 8 в вершинах + 6 в центрах граней. Cu, Al, Au, Ag.' }, { head: 'Координационное число', vars: [['NaCl','6'],['Алмаз','4'],['ОЦК','8'],['ГЦК','12']] }, ] }, orbitals: { title: 'Молекулярные орбитали', sections: [ { head: 's-орбиталь', text: 'Сферическая форма. Электрон с равной вероятностью находится на любом расстоянии от ядра.' }, { head: 'p-орбитали', text: 'Три гантелеобразные орбитали (px, py, pz) взаимно перпендикулярны. В каждой — до 2 электронов.' }, { head: 'd-орбитали', text: 'Пять орбиталей сложной формы (четырёхлепестковые и с «поясом»). Заполняются в d-элементах.' }, { head: 'σ-связь', formula: '\\psi_{\\sigma} = c_1 \\psi_A + c_2 \\psi_B', text: 'Перекрывание орбиталей вдоль линии связи. H₂ — простейший пример.' }, { head: 'Молекула H₂O', text: 'Угол связи 104.5°. Кислород: 2 связывающие пары (O-H) и 2 неподелённые пары.' }, ] }, stereo: { title: 'Стереометрия', sections: [ { head: 'Куб', formula: 'V = a^3,\\; S = 6a^2', text: 'Все грани — квадраты, все рёбра равны. Диагональ: d = a√3.' }, { head: 'Параллелепипед', formula: 'V = abc,\\; S = 2(ab+bc+ac)', text: 'Три измерения a, b, c. Диагональ: d = √(a²+b²+c²).' }, { head: 'Пирамида', formula: 'V = \\frac{1}{3} S_{\\text{осн}} \\cdot h', text: 'Объём — треть произведения площади основания на высоту.' }, { head: 'Тетраэдр', formula: 'V = \\frac{a^3\\sqrt{2}}{12}', text: 'Правильный тетраэдр: все 4 грани — равносторонние треугольники.' }, { head: 'Цилиндр', formula: 'V = \\pi r^2 h,\\; S_{\\text{бок}} = 2\\pi r h', text: 'Боковая поверхность при развёртке — прямоугольник.' }, { head: 'Конус', formula: 'V = \\frac{1}{3}\\pi r^2 h,\\; l = \\sqrt{r^2+h^2}', text: 'l — образующая. Боковая поверхность: πrl.' }, { head: 'Сечение', text: 'Плоскость пересекает тело, образуя многоугольник. Площадь сечения зависит от положения секущей плоскости.' }, { head: 'Сфера', formula: 'V = \\frac{4}{3}\\pi R^3,\\; S = 4\\pi R^2', text: 'Вписанная сфера касается всех граней, описанная проходит через все вершины.' }, ] }, pendulum: { title: 'Маятник', sections: [ { head: 'Уравнение движения', formula: '\\ddot{\\theta} = -\\frac{g}{L}\\sin\\theta', text: 'Нелинейное уравнение. Для малых углов sin θ ≈ θ — гармонические колебания.' }, { head: 'Период (малые θ)', formula: 'T = 2\\pi\\sqrt{\\frac{L}{g}}', text: 'Не зависит от амплитуды и массы (при малых углах).' }, { head: 'Кинетическая энергия', formula: 'E_к = \\frac{1}{2}mL^2\\dot{\\theta}^2', text: 'Максимальна в нижней точке.' }, { head: 'Потенциальная энергия', formula: 'E_п = mgL(1 - \\cos\\theta)', text: 'Максимальна в крайних точках.' }, { head: 'Затухание', formula: '\\ddot{\\theta} = -\\frac{g}{L}\\sin\\theta - \\gamma\\dot{\\theta}', text: 'γ — коэффициент затухания. Амплитуда экспоненциально убывает.' }, ] }, graphtransform: { title: 'Трансформации графиков', sections: [ { head: 'Вертикальное растяжение', formula: 'y = a \\cdot f(x)', text: '|a| > 1 — растяжение, 0 < |a| < 1 — сжатие по вертикали. a < 0 — отражение относительно оси x.' }, { head: 'Горизонтальное сжатие', formula: 'y = f(kx)', text: '|k| > 1 — сжатие, 0 < |k| < 1 — растяжение по горизонтали. k < 0 — отражение относительно оси y.' }, { head: 'Горизонтальный сдвиг', formula: 'y = f(x + b)', text: 'b > 0 — сдвиг влево, b < 0 — сдвиг вправо. Противоинтуитивно: знак b противоположен направлению сдвига.' }, { head: 'Вертикальный сдвиг', formula: 'y = f(x) + c', text: 'c > 0 — сдвиг вверх, c < 0 — сдвиг вниз.' }, { head: 'Общая формула', formula: 'y = a \\cdot f(k(x - x_0)) + y_0', text: 'Порядок преобразований: сначала горизонтальные (внутри аргумента), затем вертикальные (снаружи).' }, ] }, normaldist: { title: 'Нормальное распределение', sections: [ { head: 'Плотность', formula: 'f(x) = \\frac{1}{\\sigma\\sqrt{2\\pi}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}', vars: [['μ','математическое ожидание'],['σ','стандартное отклонение']] }, { head: 'Правило трёх сигм', text: '68.27% значений лежат в μ ± 1σ, 95.45% в μ ± 2σ, 99.73% в μ ± 3σ.' }, { head: 'Z-оценка', formula: 'z = \\frac{x - \\mu}{\\sigma}', text: 'Стандартизованное отклонение от среднего. Z = 0 в точке μ.' }, { head: 'Дисперсия', formula: 'D = \\sigma^2 = \\frac{1}{n}\\sum(x_i - \\mu)^2' }, { head: 'Свойства', text: 'Симметрична относительно μ. Площадь под всей кривой = 1. Максимум в точке x = μ.' }, ] }, quadratic: { title: 'Квадратное уравнение', sections: [ { head: 'Общий вид', formula: 'ax^2 + bx + c = 0', text: 'a ≠ 0 — старший коэффициент, b — средний, c — свободный член.' }, { head: 'Дискриминант', formula: 'D = b^2 - 4ac', text: 'D > 0 — два корня, D = 0 — один корень, D < 0 — нет действительных корней.' }, { head: 'Формула корней', formula: 'x_{1,2} = \\frac{-b \\pm \\sqrt{D}}{2a}' }, { head: 'Теорема Виета', formula: 'x_1 + x_2 = -\\frac{b}{a},\\quad x_1 \\cdot x_2 = \\frac{c}{a}' }, { head: 'Вершина параболы', formula: 'x_в = -\\frac{b}{2a},\\quad y_в = -\\frac{D}{4a}', text: 'При a > 0 — минимум, при a < 0 — максимум.' }, { head: 'Ось симметрии', formula: 'x = -\\frac{b}{2a}', text: 'Парабола симметрична относительно вертикальной прямой через вершину.' }, ] }, trigcircle: { title: 'Тригонометрическая окружность', sections: [ { head: 'Единичная окружность', formula: 'x^2 + y^2 = 1', text: 'Окружность радиуса 1 с центром в начале координат. Точка на окружности: (cos α, sin α).' }, { head: 'Синус и косинус', formula: '\\sin\\alpha = y,\\quad \\cos\\alpha = x', text: 'Синус — ордината, косинус — абсцисса точки на единичной окружности.' }, { head: 'Тангенс и котангенс', formula: '\\tan\\alpha = \\frac{\\sin\\alpha}{\\cos\\alpha},\\quad \\cot\\alpha = \\frac{\\cos\\alpha}{\\sin\\alpha}' }, { head: 'Основное тождество', formula: '\\sin^2\\alpha + \\cos^2\\alpha = 1' }, { head: 'Формулы приведения', text: 'sin(π−α) = sin α, cos(π−α) = −cos α. Функция «меняется» при π/2 ± α, «не меняется» при π ± α.' }, { head: 'Чётность', text: 'cos(−α) = cos α (чётная), sin(−α) = −sin α (нечётная), tan(−α) = −tan α (нечётная).' }, { head: 'Период', formula: 'T_{\\sin,\\cos} = 2\\pi,\\quad T_{\\tan,\\cot} = \\pi' }, ] }, celldivision: { title: 'Деление клетки', sections: [ { head: 'Клеточный цикл', text: 'G₁ S (репликация ДНК) G₂ M (митоз). Интерфаза = G₁ + S + G₂ — подготовка к делению.' }, { head: 'Митоз', text: 'Профаза Метафаза Анафаза Телофаза. Результат: 2 дочерние клетки с идентичным набором хромосом (2n).' }, { head: 'Профаза', text: 'Хромосомы конденсируются, ядерная оболочка разрушается, формируется веретено деления.' }, { head: 'Метафаза', text: 'Хромосомы выстраиваются в экваториальной плоскости. Кинетохоры присоединяются к нитям веретена.' }, { head: 'Анафаза', text: 'Центромеры делятся, хроматиды расходятся к полюсам клетки.' }, { head: 'Мейоз', text: 'Два последовательных деления. Результат: 4 гаплоидные клетки (n). Кроссинговер обеспечивает генетическое разнообразие.' }, { head: 'Формула', formula: '2n \\xrightarrow{\\text{мейоз I}} n \\xrightarrow{\\text{мейоз II}} n', text: 'Первое деление — редукционное (уменьшение числа хромосом вдвое).' }, ] }, photosynthesis: { title: 'Фотосинтез и дыхание', sections: [ { head: 'Суммарное уравнение', formula: '6CO_2 + 6H_2O \\xrightarrow{h\\nu} C_6H_{12}O_6 + 6O_2' }, { head: 'Световая фаза', text: 'Происходит в тилакоидах. Фотосистемы I и II поглощают свет, расщепляют воду (фотолиз), выделяют O₂. Образуются АТФ и НАДФН.' }, { head: 'Темновая фаза (цикл Кальвина)', text: 'В строме хлоропласта. CO₂ фиксируется ферментом РуБисКО. АТФ и НАДФН восстанавливают C₃ до Г3Ф глюкоза.' }, { head: 'Клеточное дыхание', formula: 'C_6H_{12}O_6 + 6O_2 \\to 6CO_2 + 6H_2O + 38\\text{АТФ}' }, { head: 'Гликолиз', text: 'Цитоплазма. Глюкоза 2 пирувата + 2 АТФ + 2 НАДН. Анаэробный процесс.' }, { head: 'Цикл Кребса', text: 'Матрикс митохондрий. Ацетил-КоА CO₂ + НАДН + ФАДН₂ + ГТФ.' }, { head: 'Окислительное фосфорилирование', text: 'Электрон-транспортная цепь на внутренней мембране митохондрий. Основной выход АТФ (~34).' }, ] }, chemsandbox: { title: 'Химическая песочница', sections: [ { head: 'Реакция нейтрализации', formula: '\\text{Кислота} + \\text{Основание} \\to \\text{Соль} + H_2O', text: 'Экзотермическая реакция. pH раствора стремится к 7.' }, { head: 'Осадок ()', text: 'Нерастворимое вещество выпадает из раствора. Правила растворимости: все нитраты растворимы, хлориды — кроме AgCl, PbCl₂.' }, { head: 'Газовыделение ()', text: 'Признак реакции: карбонаты + кислота CO₂, активные металлы + кислота H₂.' }, { head: 'Ряд активности металлов', text: 'Li K Ca Na Mg Al Zn Fe Ni Sn Pb (H₂) Cu Hg Ag Pt Au. Металл вытесняет из раствора все металлы правее него.' }, { head: 'Индикаторы', text: 'Фенолфталеин: бесцветный малиновый в щёлочи. Лакмус: красный в кислоте, синий в щёлочи.' }, ] }, angrybirds: { title: 'Физика полёта', sections: [ { head: 'Баллистическая траектория', formula: 'y = x\\tan\\alpha - \\frac{gx^2}{2v_0^2\\cos^2\\alpha}', text: 'Параболическая траектория без сопротивления воздуха.' }, { head: 'Дальность полёта', formula: 'L = \\frac{v_0^2 \\sin 2\\alpha}{g}', text: 'Максимум при α = 45°.' }, { head: 'Импульс', formula: '\\vec{p} = m\\vec{v}', text: 'При ударе передаётся импульс. Чем больше масса и скорость, тем сильнее удар.' }, { head: 'Кинетическая энергия', formula: 'E_к = \\frac{1}{2}mv^2', text: 'Энергия разрушения зависит от скорости в момент столкновения.' }, { head: 'Сопротивление воздуха', formula: 'F_{\\text{drag}} = \\frac{1}{2}C_d \\rho A v^2', text: 'Снижает дальность полёта. Ветер изменяет горизонтальную составляющую.' }, ] }, equilibrium: { title: 'Химическое равновесие', sections: [ { head: 'Закон действующих масс', formula: 'K_{eq} = \\frac{[C]^c[D]^d}{[A]^a[B]^b}', text: 'Константа равновесия — отношение произведений концентраций продуктов к реагентам.' }, { head: 'Коэффициент реакции', formula: 'Q = \\frac{[C][D]}{[A][B]}', text: 'Q < Keq — реакция идёт вправо, Q > Keq — влево, Q = Keq — равновесие.' }, { head: 'Принцип Ле Шателье', text: 'Если внешнее воздействие выводит систему из равновесия, система смещается так, чтобы ослабить это воздействие.' }, { head: 'Влияние температуры', text: 'Повышение T сдвигает равновесие в сторону эндотермической реакции. Понижение — в сторону экзотермической.' }, { head: 'Энергия активации', formula: 'k = A \\cdot e^{-E_a / RT}', text: 'Уравнение Аррениуса. Чем ниже Ea, тем быстрее реакция.' }, ] }, thinlens: { title: 'Тонкая линза', sections: [ { head: 'Формула тонкой линзы', formula: '\\frac{1}{f} = \\frac{1}{d} + \\frac{1}{d\'}', vars: [['f','фокусное расстояние'],['d','расстояние до предмета'],["d'",'расстояние до изображения']] }, { head: 'Увеличение', formula: 'M = -\\frac{d\'}{d} = \\frac{h\'}{h}', text: '|M| > 1 — увеличенное, |M| < 1 — уменьшенное. M < 0 — перевёрнутое.' }, { head: 'Собирающая линза (f > 0)', text: 'd > 2f — уменьшенное действительное. d = 2f — равное. f < d < 2f — увеличенное действительное. d < f — увеличенное мнимое.' }, { head: 'Рассеивающая линза (f < 0)', text: 'Всегда даёт уменьшенное мнимое прямое изображение.' }, { head: 'Оптическая сила', formula: 'D = \\frac{1}{f}\\text{ (дптр)}', text: 'Измеряется в диоптриях. D > 0 — собирающая, D < 0 — рассеивающая.' }, ] }, titration: { title: 'Титрование и pH', sections: [ { head: 'Водородный показатель', formula: 'pH = -\\lg[H^+]', text: 'pH < 7 — кислая среда, pH = 7 — нейтральная, pH > 7 — щелочная.' }, { head: 'Сильная кислота + сильное основание', formula: 'HCl + NaOH \\to NaCl + H_2O', text: 'Точка эквивалентности при pH = 7. Резкий скачок pH вблизи неё.' }, { head: 'Слабая кислота', formula: 'pH = pK_a + \\lg\\frac{[A^-]}{[HA]}', text: 'Уравнение Хендерсона — Хассельбальха. В точке полунейтрализации pH = pKa.' }, { head: 'Точка эквивалентности', formula: 'V_{экв} = \\frac{C_к \\cdot V_к}{C_о}', text: 'Объём основания, при котором кислота полностью нейтрализована.' }, { head: 'Индикаторы', text: 'Фенолфталеин: бесцветный малиновый (pH 8.2–10). Метилоранж: красный жёлтый (pH 3.1–4.4). Лакмус: красный синий (pH 5–8).' }, ] }, isoprocess: { title: 'Изопроцессы', sections: [ { head: 'Уравнение состояния', formula: 'PV = nRT', vars: [['P','давление (Па)'],['V','объём (м³)'],['T','температура (К)'],['n','количество вещества'],['R','8.314 Дж/(моль·К)']] }, { head: 'Изотермический (T=const)', formula: 'P_1V_1 = P_2V_2', text: 'Закон Бойля — Мариотта. ΔU = 0. Работа W = nRT·ln(V₂/V₁) = Q.' }, { head: 'Изохорный (V=const)', formula: '\\frac{P_1}{T_1} = \\frac{P_2}{T_2}', text: 'Закон Гей-Люссака (второй). W = 0. Q = ΔU = νCᵥΔT.' }, { head: 'Изобарный (P=const)', formula: '\\frac{V_1}{T_1} = \\frac{V_2}{T_2}', text: 'Закон Гей-Люссака (первый). W = PΔV. Q = νCpΔT = ΔU + W.' }, { head: 'Адиабатный (Q=0)', formula: 'PV^\\gamma = \\text{const}', text: 'Показатель γ = Cp/Cv: 5/3 — одноатомный газ, 7/5 — двухатомный. Q = 0, W = −ΔU.' }, { head: 'Начало термодинамики', formula: 'Q = \\Delta U + W', text: 'Теплота, сообщённая газу, расходуется на увеличение внутренней энергии и совершение работы.' }, ] }, mirrors: { title: 'Зеркала', sections: [ { head: 'Формула зеркала', formula: '\\frac{1}{f} = \\frac{1}{d} + \\frac{1}{d\'}', vars: [['f','фокусное расстояние'],['d','расстояние от предмета до зеркала'],["d'",'расстояние до изображения']] }, { head: 'Увеличение', formula: 'M = -\\frac{d\'}{d} = \\frac{h\'}{h}', text: 'M < 0 — перевёрнутое (действительное). |M| > 1 — увеличенное, |M| < 1 — уменьшенное.' }, { head: 'Вогнутое зеркало (f > 0)', text: 'd > 2f: уменьшенное действительное. d = 2f: равное действительное. f < d < 2f: увеличенное действительное. d < f: увеличенное мнимое (прямое).' }, { head: 'Выпуклое зеркало (f < 0)', text: 'Всегда даёт уменьшенное мнимое прямое изображение. Широкий угол обзора — применяется в автомобилях и видеонаблюдении.' }, { head: 'Плоское зеркало (f = ∞)', formula: "d' = -d,\\quad M = +1", text: 'Изображение мнимое, прямое, равное предмету — расположено на таком же расстоянии за зеркалом.' }, ] }, refraction: { title: 'Преломление света', sections: [ { head: 'Закон Снеллиуса', formula: 'n_1 \\sin\\theta_1 = n_2 \\sin\\theta_2', text: 'Угол преломления зависит от соотношения показателей преломления двух сред.' }, { head: 'Показатель преломления', formula: 'n = \\frac{c}{v}', text: 'Отношение скорости света в вакууме к скорости в среде. Воздух ≈ 1, вода = 1.33, стекло ≈ 1.5, алмаз = 2.42.' }, { head: 'Полное внутреннее отражение', formula: '\\theta_c = \\arcsin\\frac{n_2}{n_1}', text: 'Возникает при переходе из оптически более плотной среды в менее плотную (n₁ > n₂) при θ > θc.' }, { head: 'Коэффициент отражения', formula: 'R = \\left(\\frac{n_1\\cos\\theta_1 - n_2\\cos\\theta_2}{n_1\\cos\\theta_1 + n_2\\cos\\theta_2}\\right)^2', text: 'Формула Френеля (s-поляризация). Определяет долю отражённой интенсивности.' }, { head: 'Дисперсия', text: 'Показатель преломления зависит от длины волны. Фиолетовый свет преломляется сильнее красного разложение белого света в спектр.' }, ] }, probability: { title: 'Теория вероятностей', sections: [ { head: 'Вероятность', formula: 'P(A) = \\frac{m}{n}', text: 'm — число благоприятных исходов, n — общее число равновозможных исходов.' }, { head: 'Закон больших чисел', text: 'При большом числе испытаний частота события стремится к его вероятности: f(A) P(A) при n ∞.' }, { head: 'Биномиальное распределение', formula: 'P(k) = C_n^k p^k (1-p)^{n-k}', text: 'Вероятность ровно k успехов в n независимых испытаниях с вероятностью p.' }, { head: 'Матожидание', formula: 'M(X) = np', text: 'Среднее число успехов в n испытаниях.' }, { head: 'Критерий χ²', formula: '\\chi^2 = \\sum\\frac{(O_i - E_i)^2}{E_i}', text: 'Мера отклонения наблюдаемых частот O от ожидаемых E. Чем меньше χ², тем лучше согласие.' }, ] }, bohratom: { title: 'Атом Бора', sections: [ { head: 'Энергия уровня', formula: 'E_n = -\\frac{13.6}{n^2}\\text{ эВ}', text: 'n = 1 — основное состояние (-13.6 эВ), n ∞ — ионизация (0 эВ).' }, { head: 'Энергия фотона', formula: '\\Delta E = |E_f - E_i| = h\\nu', text: 'При переходе электрона между уровнями излучается или поглощается фотон.' }, { head: 'Длина волны', formula: '\\lambda = \\frac{hc}{\\Delta E} = \\frac{1240}{\\Delta E\\text{ (эВ)}}\\text{ нм}' }, { head: 'Серия Лаймана', text: 'Переходы на n=1. УФ-излучение (λ < 122 нм).' }, { head: 'Серия Бальмера', text: 'Переходы на n=2. Видимый свет: Hα=656нм (красный), Hβ=486нм (голубой), Hγ=434нм (фиолетовый).' }, { head: 'Серия Пашена', text: 'Переходы на n=3. Инфракрасное излучение.' }, ] }, electrolysis: { title: 'Электролиз', sections: [ { head: 'Первый закон Фарадея', formula: 'm = \\frac{M \\cdot I \\cdot t}{n \\cdot F}', vars: [['M','молярная масса'],['I','сила тока'],['t','время'],['n','число электронов'],['F','96485 Кл/моль']] }, { head: 'Катод (−)', text: 'Восстановление: катионы принимают электроны. Cu²⁺ + 2e⁻ Cu. 2H⁺ + 2e⁻ H₂.' }, { head: 'Анод (+)', text: 'Окисление: анионы отдают электроны. 2Cl⁻ − 2e⁻ Cl₂. 2H₂O − 4e⁻ O₂ + 4H⁺.' }, { head: 'Электролит NaCl', text: 'Катод: 2H₂O + 2e⁻ H₂ + 2OH⁻. Анод: 2Cl⁻ − 2e⁻ Cl₂.' }, { head: 'Электролит CuSO₄', text: 'Катод: Cu²⁺ + 2e⁻ Cu (осадок). Анод: 2H₂O − 4e⁻ O₂ + 4H⁺.' }, ] }, waves: { title: 'Волны и звук', sections: [ { head: 'Уравнение бегущей волны', formula: 'y(x,t) = A\\sin(\\omega t - kx)', vars: [['A','амплитуда (м)'],['\\omega = 2\\pi f','циклическая частота (рад/с)'],['k = 2\\pi/\\lambda','волновое число (1/м)']] }, { head: 'Связь параметров волны', formula: 'v = \\lambda f = \\frac{\\omega}{k}', vars: [['v','фазовая скорость'],['\\lambda','длина волны'],['f','частота (Гц)'],['T = 1/f','период (с)']] }, { head: 'Стоячая волна', formula: 'y = 2A\\sin(kx)\\cos(\\omega t)', text: 'Возникает при сложении двух волн одинаковой частоты, распространяющихся навстречу. Узлы — y\u22610 всегда. Пучности — |y|=max.' }, { head: 'Гармоники струны', formula: '\\lambda_n = \\frac{2L}{n},\\quad f_n = n\\frac{v}{2L}', text: 'Для струны длиной L, закреплённой на концах: n=1 — основной тон (1 пучность), n=2,3,... — обертоны.' }, { head: 'Принцип суперпозиции', text: 'При наложении волн смещения складываются: y = y\u2081 + y\u2082. Конструктивная интерференция (\u0394\u03c6=0): A = A\u2081+A\u2082. Деструктивная (\u0394\u03c6=\u03c0): A = |A\u2081\u2212A\u2082|.' }, { head: 'Биения', text: 'Если f\u2081 \u2260 f\u2082, результирующая амплитуда периодически меняется с частотой |f\u2081\u2212f\u2082|. Применяется в акустике для настройки инструментов.' }, ] }, }; /* ══════════════════════════════════════════════ 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'; } }