Files
Learn_System/frontend/lab.html
T

9041 lines
552 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Лаборатория — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<style>
/* ── page fill so sim canvas can go full-height ── */
.app-layout { height: 100vh; overflow: hidden; }
.sb-content { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
/* ════════════════════════════════
HOME VIEW
════════════════════════════════ */
#lab-home { flex: 1; overflow-y: auto; padding: 36px 28px 80px; }
.lab-hero {
display: flex; align-items: center; gap: 20px;
margin-bottom: 36px;
}
.lab-hero-icon {
width: 64px; height: 64px; border-radius: 20px; flex-shrink: 0;
background: linear-gradient(135deg, rgba(155,93,229,.35), rgba(6,214,224,.25));
border: 1.5px solid rgba(255,255,255,.12);
display: flex; align-items: center; justify-content: center;
}
.lab-hero-icon svg { width: 30px; height: 30px; stroke: #9B5DE5; stroke-width: 1.5; }
.lab-hero-title {
font-family: 'Unbounded', sans-serif; font-size: 1.55rem; font-weight: 800;
letter-spacing: -0.02em; margin-bottom: 5px;
}
.lab-hero-sub { font-size: 0.9rem; color: var(--text-2); font-weight: 500; }
/* category filter */
.lab-filters {
display: flex; gap: 6px; margin-bottom: 28px; flex-wrap: wrap;
}
.lab-filter {
padding: 6px 18px; border-radius: 99px;
border: 1.5px solid var(--border-h);
background: var(--surface); color: var(--text-2);
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
cursor: pointer; transition: all .16s;
}
.lab-filter:hover { border-color: rgba(155,93,229,.4); color: var(--violet); }
.lab-filter.active { background: #0F172A; color: #fff; border-color: #0F172A; }
/* sim grid */
.sim-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 20px;
}
.sim-card {
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 20px;
overflow: hidden;
cursor: pointer;
transition: border-color .18s, box-shadow .18s, transform .18s;
position: relative;
}
.sim-card:hover {
border-color: var(--violet);
box-shadow: 0 8px 32px rgba(155,93,229,.18);
transform: translateY(-2px);
}
.sim-card.soon { cursor: default; opacity: .65; }
.sim-card.soon:hover { transform: none; box-shadow: none; border-color: var(--border); }
.sim-preview {
width: 100%; height: 140px; display: block;
background: #0D0D1A;
}
.sim-body { padding: 18px 20px 20px; }
.sim-cat {
display: inline-flex; align-items: center; gap: 5px;
font-size: 0.68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
padding: 3px 10px; border-radius: 99px; margin-bottom: 10px;
}
.sim-cat.math { background: rgba(155,93,229,.12); color: var(--violet); }
.sim-cat.phys { background: rgba(6,214,224,.1); color: #06D6E0; }
.sim-title {
font-family: 'Unbounded', sans-serif; font-size: 0.9rem; font-weight: 800;
margin-bottom: 6px; letter-spacing: -.01em;
}
.sim-desc { font-size: 0.82rem; color: var(--text-2); line-height: 1.55; }
.sim-soon-badge {
position: absolute; top: 12px; right: 12px;
background: rgba(15,23,42,.7); color: rgba(255,255,255,.55);
font-size: 0.65rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
padding: 3px 9px; border-radius: 99px; backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,.1);
}
/* ════════════════════════════════
SIM VIEW (graph)
════════════════════════════════ */
#lab-sim {
display: none;
flex: 1; min-height: 0;
flex-direction: column;
}
#lab-sim.open { display: flex; }
/* top bar */
.sim-topbar {
flex-shrink: 0;
display: flex; align-items: center; gap: 12px;
padding: 10px 18px;
background: var(--surface);
border-bottom: 1.5px solid var(--border);
}
.sim-back {
display: flex; align-items: center; gap: 6px;
padding: 6px 14px; border-radius: 99px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-2);
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
cursor: pointer; transition: all .15s;
}
.sim-back:hover { border-color: var(--violet); color: var(--violet); }
.sim-back svg { width: 14px; height: 14px; stroke: currentColor; stroke-width: 2.2; flex-shrink: 0; }
.sim-topbar-title {
font-family: 'Unbounded', sans-serif; font-size: 0.88rem; font-weight: 800;
flex: 1;
}
.sim-zoom-btns { display: flex; gap: 4px; }
.zoom-btn {
min-width: 32px; width: auto; height: 32px; border-radius: 10px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-2);
cursor: pointer; font-size: .8rem; font-weight: 700;
padding: 0 9px; white-space: nowrap;
display: flex; align-items: center; justify-content: center; gap: 4px;
transition: all .15s;
}
.zoom-btn:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.07); }
.zoom-btn svg { width: 15px; height: 15px; stroke: currentColor; stroke-width: 2.2; }
/* sim body */
.sim-body-wrap {
flex: 1; min-height: 0;
display: flex;
}
/* left panel */
.graph-panel {
width: 280px; flex-shrink: 0;
background: var(--surface);
border-right: 1.5px solid var(--border);
display: flex; flex-direction: column;
overflow-y: auto;
padding: 16px 14px;
gap: 6px;
}
.gp-section-title {
font-family: 'Unbounded', sans-serif; font-size: 0.62rem; font-weight: 800;
color: var(--text-3); text-transform: uppercase; letter-spacing: .08em;
display: flex; align-items: center; gap: 8px; margin: 4px 0 8px;
}
.gp-section-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* function rows */
.fn-row {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 12px;
border: 1.5px solid var(--border);
background: rgba(15,23,42,.03);
transition: border-color .15s;
}
.fn-label {
font-family: 'Manrope', monospace; font-size: 0.85rem; font-weight: 700;
color: var(--fn-color, #9B5DE5); flex-shrink: 0; letter-spacing: .01em;
}
.fn-row:focus-within { border-color: var(--fn-color, #9B5DE5); }
.fn-dot {
width: 12px; height: 12px; border-radius: 50%;
flex-shrink: 0;
background: var(--fn-color, #9B5DE5);
box-shadow: 0 0 6px var(--fn-color, #9B5DE5);
}
.fn-input {
flex: 1; border: none; outline: none; background: transparent;
font-family: 'Manrope', monospace; font-size: 0.88rem; font-weight: 600;
color: var(--text); padding: 0; min-width: 0;
}
.fn-input::placeholder { color: var(--text-3); font-weight: 500; }
/* KaTeX live preview */
.fn-preview {
min-height: 20px; padding: 3px 4px 3px 36px;
font-size: 0.82rem; line-height: 1.5;
color: rgba(255,255,255,.65);
overflow: hidden; display: none;
}
.fn-preview.has-content { display: block; }
.fn-preview .katex { color: rgba(255,255,255,.8); font-size: 1em; }
.fn-err {
font-size: 0.68rem; color: var(--pink); font-weight: 600;
padding: 2px 0 0 22px; display: none;
}
.fn-err.show { display: block; }
/* presets */
.presets-wrap { display: flex; flex-wrap: wrap; gap: 5px; }
.preset-btn {
padding: 4px 11px; border-radius: 8px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-2);
font-family: 'Manrope', monospace; font-size: 0.75rem; font-weight: 700;
cursor: pointer; transition: all .14s;
}
.preset-btn:hover {
border-color: var(--violet); color: var(--violet);
background: rgba(155,93,229,.07);
}
/* info / actions */
.gp-btn {
display: flex; align-items: center; justify-content: center; gap: 7px;
padding: 9px 14px; border-radius: 12px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-2);
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 700;
cursor: pointer; transition: all .15s; width: 100%;
}
.gp-btn:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.06); }
.gp-btn svg { width: 14px; height: 14px; stroke: currentColor; stroke-width: 2; flex-shrink: 0; }
/* stereo param sliders */
.stereo-sl-row { margin-bottom: 6px; }
.stereo-sl-row label { display: flex; justify-content: space-between; font-size: 0.72rem; color: var(--text-2); margin-bottom: 2px; }
.stereo-sl-row label span { color: var(--violet); font-weight: 700; }
.stereo-sl-row input[type=range] { width: 100%; accent-color: var(--violet); }
.stereo-fig-btn.active, .stereo-toggle.active, .stereo-sect-btn.active, .stereo-sect-type.active {
border-color: var(--violet) !important; color: var(--violet) !important; background: rgba(155,93,229,.1) !important;
}
#stereo-unfold-btn.active, #stereo-measure-btn.active, #stereo-inscribed-btn.active, #stereo-circumscribed-btn.active {
border-color: var(--violet) !important; color: var(--violet) !important; background: rgba(155,93,229,.1) !important;
}
.gp-preset-group { margin-bottom: 8px; }
.gp-preset-label {
font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: var(--text-3);
margin: 6px 0 4px;
}
/* canvas area */
.graph-canvas-outer {
flex: 1; min-width: 0; min-height: 0;
display: flex; flex-direction: column;
}
.graph-canvas-wrap {
flex: 1; min-height: 0; position: relative;
}
#graph-canvas { display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/* info bar */
.graph-info-bar {
flex-shrink: 0;
display: flex; align-items: center; gap: 20px;
padding: 8px 18px;
background: #0D0D1A;
border-top: 1px solid rgba(255,255,255,.07);
font-family: 'Manrope', monospace; font-size: 0.8rem;
color: rgba(255,255,255,.4);
min-height: 36px;
}
.info-coord { display: flex; align-items: center; gap: 6px; }
.info-coord .ic-label { color: rgba(255,255,255,.25); }
.info-coord .ic-val { color: rgba(255,255,255,.7); font-weight: 700; min-width: 52px; }
.info-fn-val { display: flex; align-items: center; gap: 6px; }
.info-fn-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.info-fn-val .ic-val { color: rgba(255,255,255,.7); font-weight: 700; min-width: 60px; }
.info-hint { margin-left: auto; font-size: 0.72rem; color: rgba(255,255,255,.2); }
/* ════════════════════════════════
PROJECTILE SIM
════════════════════════════════ */
.sim-proj-wrap {
flex: 1; min-height: 0;
display: flex; flex-direction: column; overflow: hidden;
}
/* left panel (shared base with .graph-panel) */
.proj-panel {
width: 260px; flex-shrink: 0;
background: var(--surface);
border-right: 1.5px solid var(--border);
display: flex; flex-direction: column;
overflow-y: auto; padding: 16px 14px; gap: 6px;
}
/* canvas */
.proj-canvas-outer {
flex: 1; min-width: 0; position: relative;
}
.proj-canvas-outer canvas {
display: block; position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
}
/* sliders */
.param-block { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.param-header { display: flex; justify-content: space-between; align-items: baseline; }
.param-name { font-size: 0.8rem; font-weight: 700; color: var(--text-2); }
.param-val {
font-family: 'Manrope', monospace; font-size: 0.82rem; font-weight: 800;
color: var(--violet); min-width: 70px; text-align: right;
}
.param-slider {
-webkit-appearance: none; appearance: none;
width: 100%; height: 4px; border-radius: 4px;
background: var(--border-h); outline: none; cursor: pointer;
}
.param-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%;
background: var(--violet); box-shadow: 0 0 6px rgba(155,93,229,.5);
cursor: pointer;
}
.param-slider::-moz-range-thumb {
width: 16px; height: 16px; border-radius: 50%; border: none;
background: var(--violet); cursor: pointer;
}
/* preset chips for projectile */
.proj-preset-chip {
padding: 5px 10px; border-radius: 8px; font-size: 0.75rem; font-weight: 700;
border: 1.5px solid var(--border-h); background: transparent;
color: var(--text-2); cursor: pointer; transition: all .15s; white-space: nowrap;
}
.proj-preset-chip:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.06); }
/* stats bar */
.proj-stats-bar {
flex-shrink: 0; display: flex; align-items: stretch;
background: #0D0D1A; border-top: 1px solid rgba(255,255,255,.07);
min-height: 48px;
}
.pstat {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 2px; padding: 6px 8px;
border-right: 1px solid rgba(255,255,255,.06);
}
.pstat:last-child { border-right: none; }
.pstat-label { font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: rgba(255,255,255,.3); }
.pstat-val { font-family: 'Manrope', monospace; font-size: 0.9rem; font-weight: 800; color: rgba(255,255,255,.85); }
#csbar-v4, #csbar-v6 { font-size: 0.60rem; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* play button highlight */
.zoom-btn.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.12); }
/* ── launch button ── */
.proj-launch-btn {
width: 100%; padding: 14px 18px;
border-radius: 16px; border: none;
background: linear-gradient(135deg, #7c3aed 0%, #9B5DE5 45%, #F15BB5 100%);
color: #fff;
font-family: 'Unbounded', sans-serif; font-size: 0.8rem; font-weight: 800;
letter-spacing: .04em; text-transform: uppercase;
cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 10px;
box-shadow: 0 4px 24px rgba(155,93,229,.45), 0 0 0 0 rgba(155,93,229,.3);
transition: transform .15s, box-shadow .15s;
position: relative; overflow: hidden;
}
.proj-launch-btn::before {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,.15) 0%, transparent 60%);
pointer-events: none;
}
.proj-launch-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(155,93,229,.6), 0 0 0 3px rgba(155,93,229,.15);
}
.proj-launch-btn:active { transform: translateY(0); }
.proj-launch-btn.paused {
background: linear-gradient(135deg, #0891b2 0%, #06D6E0 100%);
box-shadow: 0 4px 24px rgba(6,214,224,.35);
}
.proj-launch-btn.done {
background: linear-gradient(135deg, #15803d 0%, #22c55e 100%);
box-shadow: 0 4px 24px rgba(34,197,94,.35);
}
.proj-reset-btn {
width: 100%; padding: 8px;
border-radius: 10px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-3);
font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 700;
cursor: pointer; transition: all .15s;
display: flex; align-items: center; justify-content: center; gap: 7px;
}
.proj-reset-btn:hover { border-color: var(--violet); color: var(--violet); }
/* speed slider — cyan thumb */
#sl-speed::-webkit-slider-thumb {
background: #06D6E0;
box-shadow: 0 0 6px rgba(6,214,224,.5);
}
#sl-speed::-moz-range-thumb { background: #06D6E0; }
/* magnetic canvas */
#mag-canvas {
display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
cursor: crosshair;
}
/* mode buttons */
.mag-mode-btn {
flex: 1; padding: 8px 6px; border-radius: 10px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-2);
font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 700;
cursor: pointer; transition: all .15s;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
.mag-mode-btn:hover { border-color: var(--violet); color: var(--text); }
.mag-mode-btn.active {
background: rgba(155,93,229,.15); border-color: var(--violet); color: #fff;
}
/* triangle canvas */
#tri-canvas {
display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
/* molecular physics canvases */
#gas-canvas, #brownian-canvas, #states-canvas, #diffusion-canvas, #reactions-canvas {
display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
/* chemistry sim-cat badge */
.sim-cat.chem { background: rgba(52,211,153,.1); color: #34d399; }
/* biology sim-cat badge */
.sim-cat.bio { background: rgba(34,211,153,.1); color: #22d399; }
/* phase dot navigator */
.cd-phase-nav { display:flex; align-items:center; justify-content:center; gap:6px; padding:6px 0; flex-wrap:wrap; }
.cd-phase-dot { width:8px; height:8px; border-radius:50%; background:var(--border-h,rgba(255,255,255,.15)); cursor:pointer; transition:background .2s,transform .2s; flex-shrink:0; }
.cd-phase-dot.active { background:#22d399; transform:scale(1.5); }
/* ── triangle panel components ── */
.tri-layer-row {
display: flex; align-items: center; gap: 8px;
padding: 7px 10px; border-radius: 10px;
border: 1.5px solid var(--border);
background: rgba(15,23,42,.03);
cursor: pointer; user-select: none;
transition: border-color .15s, background .15s;
}
.tri-layer-row:hover { background: rgba(155,93,229,.05); border-color: rgba(155,93,229,.3); }
.tri-layer-row.active { background: rgba(155,93,229,.08); border-color: rgba(155,93,229,.4); }
.tri-dot {
width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
}
.tri-layer-name {
font-size: 0.78rem; font-weight: 700; color: var(--text); flex: 1;
}
.tri-layer-hint {
font-size: 0.68rem; font-weight: 700; opacity: 0.8;
}
.tri-toggle {
width: 28px; height: 16px; border-radius: 99px;
background: var(--border-h); flex-shrink: 0;
position: relative; transition: background .2s;
}
.tri-toggle::after {
content: ''; position: absolute; top: 2px; left: 2px;
width: 12px; height: 12px; border-radius: 50%;
background: #fff; transition: transform .2s;
}
.tri-layer-row.active .tri-toggle { background: var(--violet); }
.tri-layer-row.active .tri-toggle::after { transform: translateX(12px); }
.tri-stats-grid {
display: grid; grid-template-columns: 26px 1fr; gap: 3px 8px;
padding: 0 4px;
}
.tri-stat-k {
font-family: 'Manrope', monospace; font-size: 0.78rem; font-weight: 800;
display: flex; align-items: center;
}
.tri-stat-v {
font-family: 'Manrope', monospace; font-size: 0.82rem; font-weight: 700;
color: var(--text); padding: 2px 6px;
border-radius: 6px; background: rgba(15,23,42,.08);
}
/* ── trig circle buttons ── */
.trig-fn-btn {
padding: 6px 14px; border-radius: 8px;
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.10);
color: #aaa; font-size: 13px; font-weight: 700; cursor: pointer;
font-family: 'Manrope', sans-serif; transition: .15s;
}
.trig-fn-btn:hover { background: rgba(var(--fc-rgb,155,93,229),0.15); border-color: var(--fc,#9B5DE5); color: var(--fc,#9B5DE5); }
.trig-fn-btn.active { background: rgba(var(--fc-rgb,155,93,229),0.18); border-color: var(--fc,#9B5DE5); color: var(--fc,#9B5DE5); box-shadow: 0 0 8px rgba(var(--fc-rgb,155,93,229),0.3); }
/* ── responsive ── */
@media (max-width: 768px) {
#lab-home { padding: 20px 16px 60px; }
.sim-grid { grid-template-columns: 1fr 1fr; gap: 14px; }
.graph-panel { width: 220px; }
/* Touch-friendly button targets (min 44px) */
.zoom-btn { width: auto; min-width: 44px; height: 44px; padding: 0 12px; border-radius: 12px; }
.sim-back { padding: 10px 16px; min-height: 44px; }
.theory-toggle-btn { width: 44px; height: 44px; border-radius: 12px; }
}
@media (max-width: 540px) {
.sim-grid { grid-template-columns: 1fr; }
.sim-body-wrap { flex-direction: column; }
.graph-panel { width: 100%; height: auto; border-right: none; border-bottom: 1.5px solid var(--border); max-height: 220px; }
}
/* Circuit tool buttons */
.circ-tool-btn.active {
background: rgba(155,93,229,0.25) !important;
border-color: #9B5DE5 !important;
color: #c4b5fd !important;
}
.circ-top-btn.active { background: rgba(155,93,229,0.35) !important; color: #c4b5fd !important; }
/* Flask selection buttons */
.flask-metal-btn.active, .flask-acid-btn.active {
background: rgba(75,205,155,0.20) !important;
border-color: #4BCD9B !important;
color: #7BF5A4 !important;
}
/* Reaction mode buttons */
.reac-mode-btn.active {
background: rgba(6,214,224,0.18) !important;
border-color: #06D6E0 !important;
color: #06D6E0 !important;
}
/* Newton law/scene buttons */
.nlaw-btn.active {
background: rgba(6,214,224,0.18) !important;
border-color: #06D6E0 !important;
color: #06D6E0 !important;
}
.nscene-btn.active {
background: rgba(241,91,181,0.15) !important;
border-color: #F15BB5 !important;
color: #F15BB5 !important;
}
/* ── wave mode buttons ── */
.wave-mode-btn {
flex: 1; padding: 7px 4px; border-radius: 10px;
border: 1.5px solid var(--border-h);
background: transparent; color: var(--text-2);
font-family: 'Manrope', sans-serif; font-size: 0.72rem; font-weight: 700;
cursor: pointer; transition: all .15s; text-align: center;
}
.wave-mode-btn:hover { border-color: var(--violet); color: var(--text); }
.wave-mode-btn.active {
background: rgba(155,93,229,.15) !important;
border-color: var(--violet) !important; color: #fff !important;
}
.wave-n-btn.active {
background: rgba(255,209,102,.15) !important;
border-color: #FFD166 !important; color: #FFD166 !important;
}
/* ── theory panel (overlay right sidebar) ── */
#lab-sim { position: relative; }
.theory-panel {
position: absolute; right: 0; top: 0; bottom: 0; z-index: 55;
width: 320px;
background: var(--surface);
border-left: 1.5px solid var(--border);
transform: translateX(100%);
transition: transform 0.25s cubic-bezier(.4,0,.2,1);
display: flex; flex-direction: column;
box-shadow: -4px 0 24px rgba(0,0,0,0.12);
}
.theory-panel.open { transform: translateX(0); }
.theory-panel-inner {
padding: 20px 16px; overflow-y: auto; flex: 1;
display: flex; flex-direction: column; gap: 16px;
}
.tp-title {
font-family: 'Unbounded', sans-serif; font-size: 0.72rem; font-weight: 800;
color: var(--text-3); text-transform: uppercase; letter-spacing: .06em;
display: flex; align-items: center; gap: 8px;
}
.tp-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.tp-section { margin-bottom: 4px; }
.tp-section-head {
font-size: 0.76rem; font-weight: 800; color: var(--violet);
margin-bottom: 6px; display: flex; align-items: center; gap: 6px;
}
.tp-formula {
background: rgba(155,93,229,0.06); border: 1px solid rgba(155,93,229,0.12);
border-radius: 10px; padding: 10px 12px; margin-bottom: 8px;
font-size: 0.88rem; text-align: center;
}
.tp-formula .katex { font-size: 1em; }
.tp-text {
font-size: 0.78rem; color: var(--text-2); line-height: 1.6; margin-bottom: 8px;
}
.tp-var-list { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
.tp-var {
font-size: 0.74rem; color: var(--text-2); display: flex; gap: 6px;
padding: 3px 0; border-bottom: 1px dashed rgba(255,255,255,0.04);
}
.tp-var b { color: var(--text); font-weight: 700; min-width: 24px; }
.theory-toggle-btn {
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
width: 32px; height: 32px; border-radius: 10px;
border: 1.5px solid var(--border-h); background: transparent; color: var(--text-2);
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: all 0.15s; z-index: 2;
}
.theory-toggle-btn:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.07); }
.theory-toggle-btn svg { width: 15px; height: 15px; stroke: currentColor; stroke-width: 2.2; }
@media (max-width: 768px) {
.theory-panel.open { width: 100%; position: absolute; right: 0; top: 0; bottom: 0; z-index: 60; }
}
/* ── embed mode (loaded in iframe, no sidebar/topbar) ── */
.embed-mode { height: 100vh; }
.embed-mode .sb-content { height: 100vh; margin-left: 0 !important; }
.embed-mode #lab-sim { flex: 1; }
.embed-mode .sim-body-wrap { height: 100vh; }
.embed-mode .graph-panel { max-height: 100vh; }
/* ════════════════════════════════
GEOMETRY SIM STYLES
════════════════════════════════ */
.geo-panel {
width: 210px; flex-shrink: 0;
background: var(--surface);
border-right: 1.5px solid var(--border);
display: flex; flex-direction: column;
overflow-y: auto; padding: 12px 10px; gap: 4px;
}
.geo-tool-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
margin-bottom: 4px;
}
.geo-tool-btn {
display: flex; align-items: center; gap: 6px;
padding: 7px 9px; border-radius: 10px;
border: 1.5px solid var(--border);
background: transparent; color: var(--text-2);
font-family: 'Manrope', sans-serif; font-size: 0.73rem; font-weight: 700;
cursor: pointer; transition: all .14s; white-space: nowrap;
}
.geo-tool-btn svg { width: 13px; height: 13px; stroke: currentColor; stroke-width: 2.2; flex-shrink: 0; }
.geo-tool-btn:hover { border-color: rgba(155,93,229,.4); color: var(--violet); background: rgba(155,93,229,.06); }
.geo-tool-btn.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.12); }
.geo-tool-wide {
grid-column: span 2;
}
.geo-ngon-ctrl {
display: flex; align-items: center; justify-content: center; gap: 6px;
border: 1.5px solid var(--border); border-radius: 10px;
padding: 4px 6px;
}
.geo-ngon-btn {
width: 22px; height: 22px; border-radius: 6px;
border: 1px solid var(--border-h);
background: transparent; color: var(--text-2);
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background .12s;
}
.geo-ngon-btn svg { width: 12px; height: 12px; stroke: currentColor; }
.geo-ngon-btn:hover { background: rgba(155,93,229,.1); }
#geo-ngon-n {
font-size: 0.78rem; font-weight: 700; color: var(--text);
min-width: 18px; text-align: center;
}
.geo-toggle-row {
display: flex; align-items: center; justify-content: space-between;
padding: 5px 4px; border-radius: 8px; cursor: pointer;
transition: background .13s;
}
.geo-toggle-row:hover { background: rgba(255,255,255,.04); }
.geo-toggle-label {
font-size: 0.73rem; font-weight: 600; color: var(--text-2);
display: flex; align-items: center; gap: 6px;
}
.geo-toggle-label svg { width: 12px; height: 12px; stroke: currentColor; stroke-width: 2; opacity: .7; }
.geo-toggle {
width: 28px; height: 16px; border-radius: 8px;
background: rgba(255,255,255,.1); border: 1.5px solid var(--border-h);
position: relative; transition: background .15s; flex-shrink: 0;
}
.geo-toggle::after {
content: ''; position: absolute; left: 2px; top: 50%;
transform: translateY(-50%);
width: 9px; height: 9px; border-radius: 50%;
background: rgba(255,255,255,.4); transition: all .15s;
}
.geo-toggle.on { background: var(--violet); border-color: var(--violet); }
.geo-toggle.on::after { left: calc(100% - 11px); background: #fff; }
.geo-info-box {
background: rgba(155,93,229,.07); border: 1px solid rgba(155,93,229,.15);
border-radius: 10px; padding: 8px 10px; margin-top: 4px;
font-size: 0.72rem; line-height: 1.55; color: var(--text-2);
}
.geo-info-box .geo-info-key { color: var(--violet); font-weight: 700; }
.geo-stat-row {
display: flex; justify-content: space-between; align-items: center;
font-size: 0.7rem; padding: 2px 0;
border-bottom: 1px dashed rgba(255,255,255,.05);
color: var(--text-2);
}
.geo-stat-row:last-child { border: none; }
.geo-stat-row b { color: var(--text); font-weight: 700; }
.geo-canvas-outer {
flex: 1; min-width: 0; position: relative; background: #0a0718;
}
.geo-canvas-outer canvas {
display: block; position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
}
.geo-hint-bar {
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
background: rgba(10,7,24,.82); border: 1px solid rgba(255,255,255,.1);
border-radius: 20px; padding: 4px 14px;
font-size: 0.7rem; color: rgba(255,255,255,.5);
pointer-events: none; white-space: nowrap;
backdrop-filter: blur(6px);
}
.geo-del-confirm {
display: none; position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
gap: 8px; align-items: center; white-space: nowrap;
background: rgba(18,10,32,.96); border: 1px solid rgba(155,93,229,.35);
border-radius: 10px; padding: 8px 12px; z-index: 20;
backdrop-filter: blur(8px); box-shadow: 0 4px 20px rgba(0,0,0,.6);
font-size: 0.72rem; color: rgba(255,255,255,.8);
}
.geo-del-confirm.visible { display: flex; }
.geo-del-confirm span { margin-right: 2px; }
.geo-del-btn {
padding: 3px 9px; border-radius: 6px; border: 1px solid;
font-size: 0.7rem; cursor: pointer; font-family: inherit;
transition: background .15s;
}
.geo-del-btn-soft { border-color: rgba(74,222,128,.4); color: #4ADE80; background: rgba(74,222,128,.08); }
.geo-del-btn-soft:hover { background: rgba(74,222,128,.18); }
.geo-del-btn-hard { border-color: rgba(248,113,113,.4); color: #f87171; background: rgba(248,113,113,.08); }
.geo-del-btn-hard:hover { background: rgba(248,113,113,.18); }
.geo-del-btn-cancel{ border-color: rgba(255,255,255,.15); color: rgba(255,255,255,.5); background: transparent; }
.geo-del-btn-cancel:hover{ background: rgba(255,255,255,.06); }
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<!-- ══════════ HOME VIEW ══════════ -->
<div id="lab-home">
<div class="lab-hero">
<div class="lab-hero-icon">
<i data-lucide="atom" style="width:30px;height:30px;stroke:#9B5DE5;stroke-width:1.5"></i>
</div>
<div>
<div class="lab-hero-title">Лаборатория</div>
<div class="lab-hero-sub">Интерактивные симуляции по математике и физике</div>
</div>
</div>
<div class="lab-filters">
<button class="lab-filter active" onclick="filterSims('all',this)">Все</button>
<button class="lab-filter" onclick="filterSims('math',this)">
<i data-lucide="sigma" style="width:12px;height:12px;vertical-align:-2px;margin-right:4px"></i>Математика
</button>
<button class="lab-filter" onclick="filterSims('phys',this)">
<i data-lucide="zap" style="width:12px;height:12px;vertical-align:-2px;margin-right:4px"></i>Физика
</button>
<button class="lab-filter" onclick="filterSims('chem',this)">
<i data-lucide="flask-conical" style="width:12px;height:12px;vertical-align:-2px;margin-right:4px"></i>Химия
</button>
<button class="lab-filter" onclick="filterSims('bio',this)">
<i data-lucide="dna" style="width:12px;height:12px;vertical-align:-2px;margin-right:4px"></i>Биология
</button>
<button class="lab-filter" onclick="filterSims('game',this)">
<i data-lucide="gamepad-2" style="width:12px;height:12px;vertical-align:-2px;margin-right:4px"></i>Игры
</button>
</div>
<div class="sim-grid" id="sim-grid"></div>
</div>
<!-- ══════════ SIM VIEW ══════════ -->
<div id="lab-sim">
<!-- top bar -->
<div class="sim-topbar">
<button class="sim-back" onclick="closeSim()">
<svg viewBox="0 0 24 24" fill="none"><polyline points="15 18 9 12 15 6"/></svg>
Назад
</button>
<div class="sim-topbar-title" id="sim-topbar-title">График функции</div>
<!-- graph controls -->
<div id="ctrl-graph" class="sim-zoom-btns">
<button class="zoom-btn" onclick="gSim.zoomIn()" title="Приблизить">
<svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
</button>
<button class="zoom-btn" onclick="gSim.zoomOut()" title="Отдалить">
<svg viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
</button>
<button class="zoom-btn" onclick="gSim.resetView()" title="Сброс вида">
<svg viewBox="0 0 24 24" fill="none"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- projectile controls -->
<div id="ctrl-proj" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="proj-play-btn" onclick="projPlayPause()" title="Запустить">
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button class="zoom-btn" onclick="pSim && pSim.reset(); _projSyncPlayBtn()" title="Сброс">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 2px"></div>
<button class="zoom-btn" onclick="projSaveGhost()" title="Зафиксировать траекторию" style="font-size:.65rem;font-weight:800"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M12 2a7 7 0 00-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 00-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg></button>
<button class="zoom-btn" onclick="projClearGhosts()" title="Очистить следы" style="font-size:.65rem"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
<!-- magnetic controls -->
<div id="ctrl-mag" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="mag-add-out" onclick="magMode('out')" title="Добавить провод • (ток на нас)" style="font-size:1rem"></button>
<button class="zoom-btn" id="mag-add-in" onclick="magMode('in')" title="Добавить провод × (ток от нас)" style="font-size:1rem">×</button>
<button class="zoom-btn" onclick="mSim && mSim.clearAll()" title="Очистить">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
</button>
</div>
<!-- triangle controls -->
<div id="ctrl-tri" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="tSim && tSim.reset()" title="Сбросить треугольник">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- geometry controls -->
<div id="ctrl-geometry" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="geomSim&&geomSim.undo()" title="Отменить (Ctrl+Z)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 7v6h6"/><path d="M3 13A9 9 0 1 0 6 6.3L3 7"/></svg>
</button>
<button class="zoom-btn" onclick="geomSim&&geomSim.redo()" title="Повторить (Ctrl+Y)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 7v6h-6"/><path d="M21 13A9 9 0 1 1 18 6.3L21 7"/></svg>
</button>
<button class="zoom-btn" onclick="geomSim&&geomSim.deleteSelected()" title="Удалить выбранное">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
</button>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.1);margin:0 2px"></div>
<button class="zoom-btn" onclick="geomSim&&geomSim.resetView()" title="Сброс вида">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
<button class="zoom-btn" onclick="geomSim&&geomSim.exportPNG()" title="Экспорт PNG">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>
</div>
<!-- trig circle controls -->
<div id="ctrl-trigcircle" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="trigReset()" title="Сбросить на 45°">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- collision controls -->
<div id="ctrl-coll" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="coll-play-btn" onclick="collPlayPause()" title="Запустить">
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button class="zoom-btn" onclick="cSim && cSim.reset()" title="Сброс">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- molphys controls (unified: gas + brownian + states + diffusion) -->
<div id="ctrl-molphys" class="sim-zoom-btns" style="display:none">
<!-- diffusion-only: partition button -->
<span id="ctrl-mol-diff" style="display:none">
<button class="zoom-btn" onclick="diffSim && diffSim.togglePartition(); diffPartitionBtn()" title="Снять/поставить раздел" style="font-size:0.72rem;font-weight:800;font-family:Manrope,sans-serif" id="diffusion-part-btn">
‖ Раздел
</button>
</span>
<button class="zoom-btn" onclick="molReset()" title="Сброс">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- coulomb controls -->
<div id="ctrl-coulomb" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="csign-pos" onclick="coulombSign(1)" title="Добавить + заряд" style="font-size:1.1rem;font-weight:900;color:#EF476F">+</button>
<button class="zoom-btn" id="csign-neg" onclick="coulombSign(-1)" title="Добавить − заряд" style="font-size:1.1rem;font-weight:900;color:#4CC9F0"></button>
<button class="zoom-btn" onclick="csSim && csSim.reset(); csSim && csSim.draw(); _coulombUpdateUI(csSim&&csSim.info())" title="Очистить">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
</button>
</div>
<!-- circuit controls -->
<div id="ctrl-circuit" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn circ-top-btn active" id="ctool-wire" onclick="circTool('wire',this)" title="Провод (W)" style="font-size:.7rem;font-weight:800">~</button>
<button class="zoom-btn circ-top-btn" id="ctool-resistor" onclick="circTool('resistor',this)" title="Резистор (R)" style="font-size:.6rem;font-weight:800">R</button>
<button class="zoom-btn circ-top-btn" id="ctool-battery" onclick="circTool('battery',this)" title="Батарея (B)" style="font-size:.6rem;font-weight:800">U</button>
<button class="zoom-btn circ-top-btn" id="ctool-capacitor" onclick="circTool('capacitor',this)" title="Конденсатор (C)" style="font-size:.6rem;font-weight:800">C</button>
<button class="zoom-btn circ-top-btn" id="ctool-diode" onclick="circTool('diode',this)" title="Диод (D)" style="font-size:.75rem"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>|</button>
<button class="zoom-btn circ-top-btn" id="ctool-led" onclick="circTool('led',this)" title="LED" style="font-size:.6rem;font-weight:800">LED</button>
<button class="zoom-btn circ-top-btn" id="ctool-ac" onclick="circTool('ac',this)" title="AC источник" style="font-size:.65rem;font-weight:800">AC</button>
<button class="zoom-btn circ-top-btn" id="ctool-switch" onclick="circTool('switch',this)" title="Выключатель (S)" style="font-size:.7rem"></button>
<button class="zoom-btn circ-top-btn" id="ctool-lamp" onclick="circTool('lamp',this)" title="Лампа (L)" style="font-size:.75rem"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="zoom-btn circ-top-btn" id="ctool-ammeter" onclick="circTool('ammeter',this)" title="Амперметр (A)" style="font-size:.6rem;font-weight:800">А</button>
<button class="zoom-btn circ-top-btn" id="ctool-voltmeter" onclick="circTool('voltmeter',this)" title="Вольтметр (V)" style="font-size:.6rem;font-weight:800">V</button>
<button class="zoom-btn circ-top-btn" id="ctool-erase" onclick="circTool('erase',this)" title="Ластик (E)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M20 20H7L3 16l11.5-11.5a2 2 0 0 1 2.83 0l3.17 3.17a2 2 0 0 1 0 2.83L13 18"/><line x1="6" y1="14" x2="18" y2="2"/></svg>
</button>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 2px"></div>
<button class="zoom-btn" onclick="cirSim&&cirSim.undo()" title="Отменить (Ctrl+Z)" style="font-size:.65rem"><svg class="ic" viewBox="0 0 24 24"><polyline points="9 14 4 9 9 4"/><path d="M20 20v-7a4 4 0 0 0-4-4H4"/></svg></button>
<button class="zoom-btn" onclick="cirSim&&cirSim.redo()" title="Повторить (Ctrl+Y)" style="font-size:.65rem"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 14 20 9 15 4"/><path d="M4 20v-7a4 4 0 0 1 4-4h12"/></svg></button>
<button class="zoom-btn" onclick="cirSim&&cirSim.preset('clear')" title="Очистить">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
</button>
</div>
<!-- reactions controls -->
<!-- chemistry controls (unified) -->
<div id="ctrl-chemistry" class="sim-zoom-btns" style="display:none">
<!-- kinetics tools -->
<span id="ctrl-chem-kin" style="display:contents">
<button class="zoom-btn" id="reac-pause-btn" onclick="reacTogglePause()" title="Пауза реакций" style="font-size:.68rem;font-weight:800;font-family:Manrope,sans-serif"><svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза</button>
</span>
<!-- flask tools -->
<span id="ctrl-chem-flask" style="display:none">
<button class="zoom-btn" onclick="flaskSim && flaskSim.dropMetal()" title="Бросить металл" style="font-size:.65rem;font-weight:800">⬇ Металл</button>
<button class="zoom-btn" id="flask-flame-btn" onclick="flaskToggleFlame()" title="Поджечь H₂" style="font-size:.75rem"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M12 2c.5 3.5-1.5 6-1.5 6 1 1.5 3 2 3 5a4 4 0 01-8 0c0-2 .5-3 1.5-4.5C8.5 6.5 7 4.5 7 4.5S9.5 2 12 2z"/></svg></button>
<button class="zoom-btn" id="flask-pause-btn" onclick="flaskTogglePause()" title="Пауза" style="font-size:.68rem;font-weight:800;font-family:Manrope,sans-serif"><svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg></button>
</span>
<!-- redox tools -->
<span id="ctrl-chem-redox" style="display:none">
<button class="zoom-btn" onclick="redoxStart()" title="Начать" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Старт</button>
</span>
<!-- ionex tools -->
<span id="ctrl-chem-ionex" style="display:none">
<button class="zoom-btn" onclick="ionexStart()" title="Смешать" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Смешать</button>
</span>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 3px"></div>
<button class="zoom-btn" onclick="chemReset()" title="Сброс">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- newton controls -->
<!-- dynamics controls (unified newton + sandbox) -->
<div id="ctrl-dynamics" class="sim-zoom-btns" style="display:none">
<!-- sandbox tools (shown in sandbox mode) -->
<span id="ctrl-dyn-sb" style="display:contents">
<button class="zoom-btn sb-tool-btn active" id="sbt-box" onclick="sbTool('box',this)" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/></svg> Блок</button>
<button class="zoom-btn sb-tool-btn" id="sbt-ball" onclick="sbTool('ball',this)" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/></svg> Шар</button>
<button class="zoom-btn sb-tool-btn" id="sbt-spring" onclick="sbTool('spring',this)" style="font-size:.65rem;font-weight:800">〜 Пружина</button>
<button class="zoom-btn sb-tool-btn" id="sbt-rope" onclick="sbTool('rope',this)" style="font-size:.65rem;font-weight:800">— Нить</button>
<button class="zoom-btn sb-tool-btn" id="sbt-anchor" onclick="sbTool('anchor',this)" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><path d="M12 2 2 12 12 22 22 12Z"/></svg> Якорь</button>
<button class="zoom-btn sb-tool-btn" id="sbt-erase" onclick="sbTool('erase',this)" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Ластик</button>
</span>
<!-- newton tools (shown in law modes) -->
<span id="ctrl-dyn-nw" style="display:none">
<button class="zoom-btn nscene-btn active" id="nscn-A" onclick="newtonScene('A',this)" style="font-size:.65rem;font-weight:800">A</button>
<button class="zoom-btn nscene-btn" id="nscn-B" onclick="newtonScene('B',this)" style="font-size:.65rem;font-weight:800">B</button>
<button class="zoom-btn nscene-btn" id="nscn-C" onclick="newtonScene('C',this)" style="font-size:.65rem;font-weight:800">C</button>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 3px"></div>
<button class="zoom-btn" id="newton-action-top" onclick="newtonAction()" style="font-size:.65rem;font-weight:800;font-family:Manrope,sans-serif"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Действие</button>
</span>
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 3px"></div>
<button class="zoom-btn" onclick="dynPause()" title="Пауза" style="font-size:.75rem"><svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg></button>
<button class="zoom-btn" onclick="dynReset()" title="Сброс">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- chemsandbox controls -->
<div id="ctrl-chemsandbox" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="chemSandResetReaction()" title="Сбросить реакцию" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сброс реакции</button>
<button class="zoom-btn" onclick="chemSandReset()" title="Очистить всё" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Очистить</button>
</div>
<!-- celldivision controls -->
<div id="ctrl-celldivision" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="cdPrevPhase()" title="Предыдущая фаза" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><polygon points="19 20 9 12 19 4 19 20"/></svg> Назад</button>
<button class="zoom-btn" onclick="cdNextPhase()" title="Следующая фаза" style="font-size:.65rem;font-weight:800">Далее <svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>
<button class="zoom-btn" id="ctrl-cd-auto" onclick="cdAutoPlay(document.getElementById('cd-auto-btn'))" title="Авто" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Авто</button>
</div>
<!-- photosynthesis controls -->
<div id="ctrl-photosynthesis" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="psReset()" title="Сброс" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сброс</button>
</div>
<div id="ctrl-angrybirds" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" onclick="angryBirdsRestart()" title="Начать уровень заново" style="font-size:.65rem;font-weight:800"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сначала</button>
</div>
<!-- waves controls -->
<div id="ctrl-waves" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="waves-play-btn" onclick="wavesPlayPause()" title="Пауза/Старт">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
</button>
<button class="zoom-btn" onclick="wavesSim && wavesSim.reset()" title="Сброс">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
</button>
</div>
<!-- hydrostatics controls -->
<div id="ctrl-hydro" class="sim-zoom-btns" style="display:none">
<select id="hydro-mode-sel" onchange="hydroMode(this.value)" style="background:#1a1030;color:#f0e8ff;border:1px solid rgba(255,255,255,.15);border-radius:7px;padding:3px 8px;font-size:.72rem;cursor:pointer">
<option value="pressure">Давление P=ρgh</option>
<option value="surface">Пов. натяжение</option>
<option value="communicating">Сообщ. сосуды</option>
<option value="archimedes">Архимед</option>
</select>
<select id="hydro-liq-sel" onchange="hydroSim&&hydroSim.setLiquid(this.value)" style="background:#1a1030;color:#f0e8ff;border:1px solid rgba(255,255,255,.15);border-radius:7px;padding:3px 8px;font-size:.72rem;cursor:pointer">
<option value="water">Вода</option>
<option value="saltwater">Солёная вода</option>
<option value="oil">Масло</option>
<option value="alcohol">Спирт</option>
<option value="glycerin">Глицерин</option>
<option value="mercury">Ртуть</option>
</select>
<div id="hydro-arch-ctrl" style="display:none;gap:4px;align-items:center">
<select id="hydro-mat-sel" onchange="hydroSim&&hydroSim.setMaterial(this.value)" style="background:#1a1030;color:#f0e8ff;border:1px solid rgba(255,255,255,.15);border-radius:7px;padding:3px 8px;font-size:.72rem;cursor:pointer">
<option value="styrofoam">Пенопласт</option>
<option value="cork">Пробка</option>
<option value="wood">Дерево</option>
<option value="ice">Лёд</option>
<option value="plastic">Пластик</option>
<option value="glass">Стекло</option>
<option value="aluminum">Алюминий</option>
<option value="iron">Железо</option>
<option value="gold">Золото</option>
</select>
<button class="zoom-btn" onclick="hydroSim&&hydroSim.addBody()" title="Добавить тело">+ Тело</button>
<button class="zoom-btn" onclick="hydroSim&&hydroSim.clearBodies()" title="Очистить">Очистить</button>
</div>
<div id="hydro-comm-ctrl" style="display:none;gap:4px;align-items:center">
<label style="font-size:.72rem;color:rgba(255,255,255,.5)">Сосудов:</label>
<select onchange="hydroSim&&hydroSim.setNumVessels(+this.value)" style="background:#1a1030;color:#f0e8ff;border:1px solid rgba(255,255,255,.15);border-radius:7px;padding:3px 6px;font-size:.72rem;cursor:pointer">
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<button class="zoom-btn" id="hydro-valve-btn" onclick="hydroToggleValve()" title="Кран">Кран: откр.</button>
</div>
<div id="hydro-surf-ctrl" style="display:none;gap:4px;align-items:center">
<label style="font-size:.72rem;color:rgba(255,255,255,.5);white-space:nowrap">θ:</label>
<input type="range" min="0" max="160" value="20" step="5" style="width:72px;accent-color:#9B5DE5" oninput="hydroSim&&hydroSim.setContactAngle(+this.value);document.getElementById('hydro-theta-val').textContent=this.value+'\u00B0';document.getElementById('hydro-theta-lbl').textContent=this.value+'\u00B0';document.querySelector('#hydro-panel-theta input[type=range]').value=this.value">
<span id="hydro-theta-val" style="font-size:.72rem;color:#9B5DE5;min-width:28px;white-space:nowrap">20°</span>
<button class="zoom-btn" id="hydro-surf-toggle" onclick="hydroToggleSurface()" title="Переключить: капилляры / капля" style="white-space:nowrap">Капилляры</button>
</div>
</div>
<!-- theory toggle (all sims) -->
<button class="zoom-btn" id="theory-toggle" onclick="toggleTheory()" title="Теория и формулы" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>
</button>
</div>
<!-- ── GRAPH sim body ── -->
<div id="sim-graph" class="sim-body-wrap">
<!-- left panel -->
<div class="graph-panel">
<div class="gp-section-title">Функции</div>
<!-- fn 1 -->
<div style="--fn-color:#9B5DE5">
<div class="fn-row">
<div class="fn-dot"></div>
<span class="fn-label">y =</span>
<input class="fn-input" id="fn0" placeholder="sin(x)" autocomplete="off" spellcheck="false" oninput="updateFn(0)" />
</div>
<div class="fn-preview" id="fn0-prev"></div>
<div class="fn-err" id="fn0-err">Синтаксическая ошибка</div>
</div>
<!-- fn 2 -->
<div style="--fn-color:#06D6E0">
<div class="fn-row">
<div class="fn-dot"></div>
<span class="fn-label">y =</span>
<input class="fn-input" id="fn1" placeholder="x^2 - 4" autocomplete="off" spellcheck="false" oninput="updateFn(1)" />
</div>
<div class="fn-preview" id="fn1-prev"></div>
<div class="fn-err" id="fn1-err">Синтаксическая ошибка</div>
</div>
<!-- fn 3 -->
<div style="--fn-color:#F15BB5">
<div class="fn-row">
<div class="fn-dot"></div>
<span class="fn-label">y =</span>
<input class="fn-input" id="fn2" placeholder="tg(x)" autocomplete="off" spellcheck="false" oninput="updateFn(2)" />
</div>
<div class="fn-preview" id="fn2-prev"></div>
<div class="fn-err" id="fn2-err">Синтаксическая ошибка</div>
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title">Примеры</div>
<div class="gp-preset-group">
<div class="gp-preset-label">Линейные / степенные</div>
<div class="presets-wrap">
<button class="preset-btn" onclick="applyPreset('2x-1')">2x1</button>
<button class="preset-btn" onclick="applyPreset('x^2')"></button>
<button class="preset-btn" onclick="applyPreset('x^2-4')">x²−4</button>
<button class="preset-btn" onclick="applyPreset('x^3-3x')">x³−3x</button>
<button class="preset-btn" onclick="applyPreset('x^4-4x^2+3')">x⁴−4x²+3</button>
</div>
</div>
<div class="gp-preset-group">
<div class="gp-preset-label">Тригонометрия</div>
<div class="presets-wrap">
<button class="preset-btn" onclick="applyPreset('sin(x)')">sin x</button>
<button class="preset-btn" onclick="applyPreset('cos(x)')">cos x</button>
<button class="preset-btn" onclick="applyPreset('tg(x)')">tg x</button>
<button class="preset-btn" onclick="applyPreset('sin(2x)')">sin 2x</button>
<button class="preset-btn" onclick="applyPreset('x*sin(x)')">x·sin x</button>
<button class="preset-btn" onclick="applyPreset('sin(x)/x')">sin(x)/x</button>
</div>
</div>
<div class="gp-preset-group">
<div class="gp-preset-label">Показательные / логарифмы</div>
<div class="presets-wrap">
<button class="preset-btn" onclick="applyPreset('exp(x)')"></button>
<button class="preset-btn" onclick="applyPreset('2^x')"></button>
<button class="preset-btn" onclick="applyPreset('ln(x)')">ln x</button>
<button class="preset-btn" onclick="applyPreset('log(x)')">log x</button>
</div>
</div>
<div class="gp-preset-group">
<div class="gp-preset-label">Прочие</div>
<div class="presets-wrap">
<button class="preset-btn" onclick="applyPreset('sqrt(x)')">√x</button>
<button class="preset-btn" onclick="applyPreset('1/x')">1/x</button>
<button class="preset-btn" onclick="applyPreset('abs(x)')">|x|</button>
<button class="preset-btn" onclick="applyPreset('floor(x)')">⌊x⌋</button>
<button class="preset-btn" onclick="applyPreset('1/(1+exp(-x))')">σ(x)</button>
</div>
</div>
<div style="margin-top:auto;padding-top:14px"></div>
<button class="gp-btn" onclick="clearAll()">
<svg viewBox="0 0 24 24" fill="none"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
Очистить всё
</button>
</div>
<!-- canvas area -->
<div class="graph-canvas-outer">
<div class="graph-canvas-wrap">
<canvas id="graph-canvas"></canvas>
</div>
<div class="graph-info-bar" id="graph-info-bar">
<div class="info-coord">
<span class="ic-label">x =</span>
<span class="ic-val" id="info-x"></span>
</div>
<div class="info-fn-val">
<div class="info-fn-dot" style="background:#9B5DE5;box-shadow:0 0 5px #9B5DE5"></div>
<span class="ic-label">y₁ =</span>
<span class="ic-val" id="info-y0"></span>
</div>
<div class="info-fn-val">
<div class="info-fn-dot" style="background:#06D6E0;box-shadow:0 0 5px #06D6E0"></div>
<span class="ic-label">y₂ =</span>
<span class="ic-val" id="info-y1"></span>
</div>
<div class="info-fn-val">
<div class="info-fn-dot" style="background:#F15BB5;box-shadow:0 0 5px #F15BB5"></div>
<span class="ic-label">y₃ =</span>
<span class="ic-val" id="info-y2"></span>
</div>
<div class="info-hint">Скролл — зум · Перетащи — панорама</div>
</div>
</div>
</div><!-- /#sim-graph -->
<!-- ── MAGNETIC sim body ── -->
<div id="sim-mag" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:248px;gap:0">
<!-- Mode -->
<div class="gp-section-title" style="margin-bottom:8px">Режим добавления</div>
<div style="display:flex;gap:6px;margin-bottom:12px">
<button id="mag-mode-out" class="mag-mode-btn active" onclick="magMode('out')">
<span style="font-size:1.2rem;font-weight:900;color:#06D6E0"></span>
Ток на нас
</button>
<button id="mag-mode-in" class="mag-mode-btn" onclick="magMode('in')">
<span style="font-size:1.1rem;font-weight:900;color:#F15BB5">×</span>
Ток от нас
</button>
</div>
<!-- Current -->
<div class="param-block">
<div class="param-header">
<span class="param-name">Сила тока I</span>
<span class="param-val" id="m-curI">6 А</span>
</div>
<input type="range" class="param-slider" id="sl-curI" min="1" max="20" value="6" oninput="magCurrentChange()">
</div>
<!-- Layers -->
<div class="gp-section-title" style="margin-top:4px;margin-bottom:8px">Визуализация</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px">
<label class="tri-layer-row active" id="ml-colormap" onclick="magLayer('colormap',this)">
<span class="tri-dot" style="background:linear-gradient(90deg,#9B5DE5,#06D6E0,#F15BB5)"></span>
<span class="tri-layer-name">Карта поля</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,.4)">hue = направление</span>
<span class="tri-toggle" style="background:var(--violet)"><span style="content:'';display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:14px"></span></span>
</label>
<label class="tri-layer-row active" id="ml-fieldlines" onclick="magLayer('fieldlines',this)">
<span class="tri-dot" style="background:#06D6E0;box-shadow:0 0 5px #06D6E0"></span>
<span class="tri-layer-name">Силовые линии</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,.4)">+ стрелки</span>
<span class="tri-toggle" style="background:var(--violet)"><span style="content:'';display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:14px"></span></span>
</label>
<label class="tri-layer-row" id="ml-vectors" onclick="magLayer('vectors',this)">
<span class="tri-dot" style="background:#9B5DE5;box-shadow:0 0 5px #9B5DE5"></span>
<span class="tri-layer-name">Векторное поле</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,.4)">сетка стрелок</span>
<span class="tri-toggle"></span>
</label>
</div>
<!-- Particle -->
<div class="gp-section-title" style="margin-bottom:8px">Частица</div>
<label class="tri-layer-row" id="ml-particle" onclick="magParticle(this)" style="margin-bottom:10px">
<span class="tri-dot" style="background:#ffff50;box-shadow:0 0 5px #ffff50"></span>
<span class="tri-layer-name">Заряженная частица</span>
<span class="tri-layer-hint" style="color:#ffff50">Сила Лоренца</span>
<span class="tri-toggle" id="ml-particle-toggle"></span>
</label>
<!-- Conductor -->
<div class="gp-section-title" style="margin-bottom:8px">Проводник в поле</div>
<label class="tri-layer-row" id="ml-cond" onclick="magCondToggle(this)" style="margin-bottom:6px">
<span class="tri-dot" style="background:#fbbf24;box-shadow:0 0 5px #fbbf24"></span>
<span class="tri-layer-name">Проводник (Ампер)</span>
<span class="tri-layer-hint" style="color:#fbbf24">F = I·L×B</span>
<span class="tri-toggle" id="ml-cond-toggle"></span>
</label>
<div class="param-block" id="cond-I-block" style="display:none;margin-bottom:10px">
<div class="param-header">
<span class="param-name">Ток проводника I꜀</span>
<span class="param-val" id="m-condI">8 А</span>
</div>
<input type="range" class="param-slider" id="sl-condI" min="1" max="20" value="8" oninput="magCondCurrentChange()">
</div>
<!-- Flux -->
<div class="gp-section-title" style="margin-bottom:8px">Магнитный поток</div>
<label class="tri-layer-row" id="ml-flux" onclick="magFluxToggle(this)" style="margin-bottom:10px">
<span class="tri-dot" style="background:#34d399;box-shadow:0 0 5px #34d399"></span>
<span class="tri-layer-name">Индикатор потока</span>
<span class="tri-layer-hint" style="color:#34d399">Φ = B·S</span>
<span class="tri-toggle" id="ml-flux-toggle"></span>
</label>
<!-- Presets -->
<div class="gp-section-title" style="margin-bottom:8px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="mSim && mSim.preset('single')">Один провод</button>
<button class="proj-preset-chip" onclick="mSim && mSim.preset('parallel')">Параллельные <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg><svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
<button class="proj-preset-chip" onclick="mSim && mSim.preset('anti')">Антипараллельные <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg><svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
<button class="proj-preset-chip" onclick="mSim && mSim.preset('solenoid')">Соленоид</button>
<button class="proj-preset-chip" onclick="mSim && mSim.preset('quadrupole')">Квадруполь</button>
<button class="proj-preset-chip" onclick="mSim && mSim.preset('ring')">Кольцо</button>
<button class="proj-preset-chip" onclick="mSim && mSim.preset('dipole')">Диполь</button>
</div>
<!-- Stats -->
<div style="margin-top:auto;padding-top:6px;display:flex;flex-direction:column;gap:5px">
<div class="tri-stats-grid" style="grid-template-columns:1fr 1fr">
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Провода •</div>
<div class="tri-stat-v" id="ms-out" style="text-align:center;color:#06D6E0">0</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Провода ×</div>
<div class="tri-stat-v" id="ms-in" style="text-align:center;color:#F15BB5">0</div>
</div>
<div style="font-size:0.68rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:4px">
Клик — добавить &nbsp;·&nbsp; ПКМ / 2×клик — удалить<br>
Перетащи провод для перемещения
</div>
</div>
</div><!-- /.proj-panel -->
<div class="proj-canvas-outer">
<canvas id="mag-canvas"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<div class="proj-stats-bar">
<div class="pstat">
<div class="pstat-label">Проводов</div>
<div class="pstat-val" id="mbar-total">0</div>
</div>
<div class="pstat">
<div class="pstat-label">• Ток на нас</div>
<div class="pstat-val" id="mbar-out" style="color:#06D6E0">0</div>
</div>
<div class="pstat">
<div class="pstat-label">× Ток от нас</div>
<div class="pstat-val" id="mbar-in" style="color:#F15BB5">0</div>
</div>
<div class="pstat">
<div class="pstat-label">Ток I</div>
<div class="pstat-val" id="mbar-I">6 А</div>
</div>
<div class="pstat">
<div class="pstat-label">Частица</div>
<div class="pstat-val" id="mbar-particle">выкл</div>
</div>
<div class="pstat">
<div class="pstat-label">Сила Ампера</div>
<div class="pstat-val" id="mbar-ampere" style="color:#fbbf24"></div>
</div>
<div class="pstat">
<div class="pstat-label">Поток Φ</div>
<div class="pstat-val" id="mbar-flux" style="color:#34d399"></div>
</div>
</div>
</div><!-- /#sim-mag -->
<!-- ── TRIANGLE sim body ── -->
<div id="sim-tri" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<!-- left panel -->
<div class="proj-panel" style="width:240px;gap:0">
<!-- Layer toggles -->
<div class="gp-section-title" style="margin-bottom:10px">Слои</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px">
<label class="tri-layer-row" onclick="triToggle('medians',this)">
<span class="tri-dot" style="background:#22d55e;box-shadow:0 0 5px #22d55e"></span>
<span class="tri-layer-name">Медианы</span>
<span class="tri-layer-hint" style="color:#22d55e">G — центроид</span>
<span class="tri-toggle" id="tl-medians"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('altitudes',this)">
<span class="tri-dot" style="background:#f59e0b;box-shadow:0 0 5px #f59e0b"></span>
<span class="tri-layer-name">Высоты</span>
<span class="tri-layer-hint" style="color:#f59e0b">H — ортоцентр</span>
<span class="tri-toggle" id="tl-altitudes"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('bisectors',this)">
<span class="tri-dot" style="background:#ec4899;box-shadow:0 0 5px #ec4899"></span>
<span class="tri-layer-name">Биссектрисы</span>
<span class="tri-layer-hint" style="color:#ec4899">I — инцентр</span>
<span class="tri-toggle" id="tl-bisectors"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('circumcircle',this)">
<span class="tri-dot" style="background:#F15BB5;box-shadow:0 0 5px #F15BB5"></span>
<span class="tri-layer-name">Описанная окружность</span>
<span class="tri-layer-hint" style="color:#F15BB5">O, R</span>
<span class="tri-toggle" id="tl-circumcircle"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('incircle',this)">
<span class="tri-dot" style="background:#06D6E0;box-shadow:0 0 5px #06D6E0"></span>
<span class="tri-layer-name">Вписанная окружность</span>
<span class="tri-layer-hint" style="color:#06D6E0">I, r</span>
<span class="tri-toggle" id="tl-incircle"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('eulerLine',this)">
<span class="tri-dot" style="background:rgba(255,255,100,0.8);box-shadow:0 0 5px yellow"></span>
<span class="tri-layer-name">Прямая Эйлера</span>
<span class="tri-layer-hint" style="color:rgba(255,255,100,0.7)">O<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>G<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>H</span>
<span class="tri-toggle" id="tl-eulerLine"></span>
</label>
</div>
<div class="gp-section-title" style="margin-bottom:10px">Теоремы</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px">
<label class="tri-layer-row" onclick="triToggle('sineLaw',this)">
<span class="tri-dot" style="background:#60a5fa;box-shadow:0 0 5px #60a5fa"></span>
<span class="tri-layer-name">Теорема синусов</span>
<span class="tri-layer-hint" style="color:#60a5fa">a/sinA = 2R</span>
<span class="tri-toggle" id="tl-sineLaw"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('cosineLaw',this)">
<span class="tri-dot" style="background:#fbbf24;box-shadow:0 0 5px #fbbf24"></span>
<span class="tri-layer-name">Теорема косинусов</span>
<span class="tri-layer-hint" style="color:#fbbf24">c²=a²+b²−2ab·cosC</span>
<span class="tri-toggle" id="tl-cosineLaw"></span>
</label>
<label class="tri-layer-row" onclick="triToggle('pythagorean',this)">
<span class="tri-dot" style="background:#EF476F;box-shadow:0 0 5px #EF476F"></span>
<span class="tri-layer-name">Теорема Пифагора</span>
<span class="tri-layer-hint" style="color:#EF476F">a²+b² = c²</span>
<span class="tri-toggle" id="tl-pythagorean"></span>
</label>
</div>
<!-- Stats -->
<div class="gp-section-title" style="margin-bottom:8px">Стороны</div>
<div class="tri-stats-grid" style="margin-bottom:10px">
<span class="tri-stat-k" style="color:#9B5DE5">a</span><span class="tri-stat-v" id="ts-a"></span>
<span class="tri-stat-k" style="color:#06D6E0">b</span><span class="tri-stat-v" id="ts-b"></span>
<span class="tri-stat-k" style="color:#F15BB5">c</span><span class="tri-stat-v" id="ts-c"></span>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Углы</div>
<div class="tri-stats-grid" style="margin-bottom:10px">
<span class="tri-stat-k" style="color:#9B5DE5">∠A</span><span class="tri-stat-v" id="ts-A"></span>
<span class="tri-stat-k" style="color:#06D6E0">∠B</span><span class="tri-stat-v" id="ts-B"></span>
<span class="tri-stat-k" style="color:#F15BB5">∠C</span><span class="tri-stat-v" id="ts-C"></span>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Вычисляемые</div>
<div class="tri-stats-grid" style="margin-bottom:10px">
<span class="tri-stat-k">S</span><span class="tri-stat-v" id="ts-S"></span>
<span class="tri-stat-k">P</span><span class="tri-stat-v" id="ts-P"></span>
<span class="tri-stat-k">R</span><span class="tri-stat-v" id="ts-R"></span>
<span class="tri-stat-k">r</span><span class="tri-stat-v" id="ts-r"></span>
</div>
<!-- Type badge -->
<div style="margin-top:auto;padding-top:10px">
<div style="font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3);margin-bottom:6px">Тип</div>
<div id="ts-type" style="
padding:8px 14px;border-radius:12px;
background:rgba(155,93,229,0.12);border:1.5px solid rgba(155,93,229,0.25);
font-family:'Manrope',sans-serif;font-size:0.82rem;font-weight:700;
color:#9B5DE5;text-align:center;
"></div>
<div style="margin-top:8px;font-size:0.7rem;color:var(--text-3);text-align:center;line-height:1.5">
Перетащи вершины<br>A, B, C для изменения
</div>
</div>
</div><!-- /.proj-panel -->
<!-- canvas -->
<div class="proj-canvas-outer">
<canvas id="tri-canvas"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<!-- stats bar -->
<div class="proj-stats-bar">
<div class="pstat">
<div class="pstat-label">Сторона a</div>
<div class="pstat-val" id="tbar-a"></div>
</div>
<div class="pstat">
<div class="pstat-label">Сторона b</div>
<div class="pstat-val" id="tbar-b"></div>
</div>
<div class="pstat">
<div class="pstat-label">Сторона c</div>
<div class="pstat-val" id="tbar-c"></div>
</div>
<div class="pstat">
<div class="pstat-label">Площадь S</div>
<div class="pstat-val" id="tbar-S"></div>
</div>
<div class="pstat">
<div class="pstat-label">Периметр P</div>
<div class="pstat-val" id="tbar-P"></div>
</div>
<div class="pstat">
<div class="pstat-label">R / r</div>
<div class="pstat-val" id="tbar-Rr"></div>
</div>
</div>
</div><!-- /#sim-tri -->
<!-- ══════════════════════════════════════════════
ТРИГОНОМЕТРИЧЕСКАЯ ОКРУЖНОСТЬ
══════════════════════════════════════════════ -->
<div id="sim-trigcircle" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<!-- left panel -->
<div class="proj-panel" style="width:240px;gap:0">
<!-- Function toggles -->
<div class="gp-section-title" style="margin-bottom:10px">Отрезки</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:14px">
<label class="tri-layer-row active" onclick="trigToggle('sin',this)">
<span class="tri-dot" style="background:#EF476F;box-shadow:0 0 5px #EF476F"></span>
<span class="tri-layer-name">sin</span>
<span class="tri-layer-hint" style="color:#EF476F">вертикаль</span>
<span class="tri-toggle" id="trig-tl-sin"></span>
</label>
<label class="tri-layer-row active" onclick="trigToggle('cos',this)">
<span class="tri-dot" style="background:#06D6E0;box-shadow:0 0 5px #06D6E0"></span>
<span class="tri-layer-name">cos</span>
<span class="tri-layer-hint" style="color:#06D6E0">горизонталь</span>
<span class="tri-toggle" id="trig-tl-cos"></span>
</label>
<label class="tri-layer-row" onclick="trigToggle('tan',this)">
<span class="tri-dot" style="background:#FFD166;box-shadow:0 0 5px #FFD166"></span>
<span class="tri-layer-name">tg</span>
<span class="tri-layer-hint" style="color:#FFD166">касательная</span>
<span class="tri-toggle" id="trig-tl-tan"></span>
</label>
<label class="tri-layer-row" onclick="trigToggle('cot',this)">
<span class="tri-dot" style="background:#7BF5A4;box-shadow:0 0 5px #7BF5A4"></span>
<span class="tri-layer-name">ctg</span>
<span class="tri-layer-hint" style="color:#7BF5A4">кокасательная</span>
<span class="tri-toggle" id="trig-tl-cot"></span>
</label>
</div>
<!-- Graph function selector -->
<div class="gp-section-title" style="margin-bottom:8px">График</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:14px">
<button class="trig-fn-btn active" onclick="trigSetGraphFn('sin',this)" style="--fc:#EF476F">sin</button>
<button class="trig-fn-btn" onclick="trigSetGraphFn('cos',this)" style="--fc:#06D6E0">cos</button>
<button class="trig-fn-btn" onclick="trigSetGraphFn('tan',this)" style="--fc:#FFD166">tg</button>
<button class="trig-fn-btn" onclick="trigSetGraphFn('cot',this)" style="--fc:#7BF5A4">ctg</button>
</div>
<!-- Values display -->
<div class="gp-section-title" style="margin-bottom:8px">Значения</div>
<div class="tri-stats-grid" style="margin-bottom:14px">
<span class="tri-stat-k" style="color:#EF476F">sin</span><span class="tri-stat-v" id="trig-v-sin"></span>
<span class="tri-stat-k" style="color:#06D6E0">cos</span><span class="tri-stat-v" id="trig-v-cos"></span>
<span class="tri-stat-k" style="color:#FFD166">tg</span><span class="tri-stat-v" id="trig-v-tan"></span>
<span class="tri-stat-k" style="color:#7BF5A4">ctg</span><span class="tri-stat-v" id="trig-v-cot"></span>
</div>
<!-- Notable angles -->
<div class="gp-section-title" style="margin-bottom:8px">Табличные углы</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:14px">
<button class="preset-btn" onclick="trigGoTo(0)"></button>
<button class="preset-btn" onclick="trigGoTo(Math.PI/6)">30°</button>
<button class="preset-btn" onclick="trigGoTo(Math.PI/4)">45°</button>
<button class="preset-btn" onclick="trigGoTo(Math.PI/3)">60°</button>
<button class="preset-btn" onclick="trigGoTo(Math.PI/2)">90°</button>
<button class="preset-btn" onclick="trigGoTo(2*Math.PI/3)">120°</button>
<button class="preset-btn" onclick="trigGoTo(Math.PI)">180°</button>
<button class="preset-btn" onclick="trigGoTo(3*Math.PI/2)">270°</button>
</div>
<!-- Angle info -->
<div style="margin-top:auto;padding-top:10px">
<div style="font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3);margin-bottom:6px">Угол</div>
<div id="trig-angle-badge" style="
padding:10px 14px;border-radius:12px;
background:rgba(155,93,229,0.12);border:1.5px solid rgba(155,93,229,0.25);
font-family:'Manrope',sans-serif;font-size:0.85rem;font-weight:700;
color:#9B5DE5;text-align:center;line-height:1.6;
">45° = π/4</div>
<div style="margin-top:8px;font-size:0.7rem;color:var(--text-3);text-align:center;line-height:1.5">
Перетащи точку по окружности<br>или выбери табличный угол
</div>
</div>
</div><!-- /.proj-panel -->
<!-- canvas -->
<div class="proj-canvas-outer">
<canvas id="trigcircle-canvas"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<!-- stats bar -->
<div class="proj-stats-bar">
<div class="pstat">
<div class="pstat-label">Угол</div>
<div class="pstat-val" id="trigbar-angle" style="color:#9B5DE5">45°</div>
</div>
<div class="pstat">
<div class="pstat-label">sin</div>
<div class="pstat-val" id="trigbar-sin" style="color:#EF476F"></div>
</div>
<div class="pstat">
<div class="pstat-label">cos</div>
<div class="pstat-val" id="trigbar-cos" style="color:#06D6E0"></div>
</div>
<div class="pstat">
<div class="pstat-label">tg</div>
<div class="pstat-val" id="trigbar-tan" style="color:#FFD166"></div>
</div>
<div class="pstat">
<div class="pstat-label">ctg</div>
<div class="pstat-val" id="trigbar-cot" style="color:#7BF5A4"></div>
</div>
<div class="pstat">
<div class="pstat-label">Четверть</div>
<div class="pstat-val" id="trigbar-quad" style="color:#9B5DE5"></div>
</div>
</div>
</div><!-- /#sim-trigcircle -->
<!-- ══════════════════════════════════════════════
ИДЕАЛЬНЫЙ ГАЗ
══════════════════════════════════════════════ -->
<!-- ══════════════════════════════════════════════
МОЛЕКУЛЯРНАЯ ФИЗИКА (газ + броуновское + состояния + диффузия)
══════════════════════════════════════════════ -->
<div id="sim-molphys" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:248px;gap:0">
<!-- Mode selector -->
<div style="display:flex;gap:3px;margin-bottom:12px;padding:3px;background:rgba(255,255,255,0.04);border-radius:10px;border:1px solid var(--border)">
<button class="mag-mode-btn mol-mode active" id="mol-mode-gas" onclick="molMode('gas',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Газ</button>
<button class="mag-mode-btn mol-mode" id="mol-mode-brownian" onclick="molMode('brownian',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Броуновское</button>
<button class="mag-mode-btn mol-mode" id="mol-mode-states" onclick="molMode('states',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Фазы</button>
<button class="mag-mode-btn mol-mode" id="mol-mode-diffusion" onclick="molMode('diffusion',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Диффузия</button>
</div>
<!-- ── Gas panel ── -->
<div id="mol-panel-gas">
<div class="gp-section-title" style="margin-bottom:8px">Параметры газа</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Число молекул N</span><span class="param-val" id="g-N">80</span></div>
<input type="range" class="param-slider" id="sl-gN" min="20" max="200" value="80" oninput="gasNChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Температура T</span><span class="param-val" id="g-T">1.0 у.е.</span></div>
<input type="range" class="param-slider" id="sl-gT" min="2" max="30" value="10" oninput="gasTChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Поршень (объём)</span><span class="param-val" id="g-piston">100%</span></div>
<input type="range" class="param-slider" id="sl-gPiston" min="30" max="100" value="100" oninput="gasPistonChange()">
</div>
<div style="margin-bottom:10px">
<button class="proj-preset-chip" id="gas-vec-btn" onclick="gasToggleVectors(this)" style="width:100%">Векторы скоростей: Выкл</button>
</div>
<div class="gp-section-title" style="margin-top:4px;margin-bottom:8px">Состояние</div>
<div class="tri-stats-grid" style="grid-template-columns:1fr 1fr;margin-bottom:8px">
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Давление P</div>
<div class="tri-stat-v" id="gstat-P" style="text-align:center;color:#9B5DE5"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Объём V</div>
<div class="tri-stat-v" id="gstat-V" style="text-align:center;color:#06D6E0"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">PV</div>
<div class="tri-stat-v" id="gstat-PV" style="text-align:center;color:#34d399"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">⟨v⟩ средн.</div>
<div class="tri-stat-v" id="gstat-v" style="text-align:center;color:#FFD166"></div>
</div>
<div style="font-size:.65rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:auto">Стенки светятся по P · Поршень перетащи мышью</div>
</div>
<!-- ── Brownian panel ── -->
<div id="mol-panel-brownian" style="display:none">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Молекул газа N</span><span class="param-val" id="br-N">120</span></div>
<input type="range" class="param-slider" id="sl-brN" min="30" max="300" value="120" oninput="brownNChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Температура T</span><span class="param-val" id="br-T">1.0 у.е.</span></div>
<input type="range" class="param-slider" id="sl-brT" min="2" max="20" value="10" oninput="brownTChange()">
</div>
<div class="gp-section-title" style="margin-top:10px;margin-bottom:8px">Статистика частицы B</div>
<div class="tri-stats-grid" style="grid-template-columns:1fr 1fr;margin-bottom:8px">
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">|Δr| смещение</div>
<div class="tri-stat-v" id="brstat-dr" style="text-align:center;color:#FFD166">0</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">MSD</div>
<div class="tri-stat-v" id="brstat-msd" style="text-align:center;color:#9B5DE5">0</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Скорость v</div>
<div class="tri-stat-v" id="brstat-v" style="text-align:center;color:#06D6E0">0</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Шагов</div>
<div class="tri-stat-v" id="brstat-steps" style="text-align:center;color:#34d399">0</div>
</div>
<div style="margin-bottom:10px">
<button class="proj-preset-chip" onclick="if(brownSim) brownSim.resetOrigin()" style="width:100%">Сбросить отсчёт (Origin)</button>
</div>
<div style="font-size:.65rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:auto">График MSD нарастает линейно — закон диффузии</div>
</div>
<!-- ── States panel ── -->
<div id="mol-panel-states" style="display:none">
<div class="gp-section-title" style="margin-bottom:8px">Управление</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Температура T</span><span class="param-val" id="st-T">0.15</span></div>
<input type="range" class="param-slider" id="sl-stT" min="1" max="100" value="15" oninput="statesTChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Частиц N</span><span class="param-val" id="st-N">64</span></div>
<input type="range" class="param-slider" id="sl-stN" min="16" max="100" step="4" value="64" oninput="statesNChange()">
</div>
<div style="display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap">
<button class="proj-preset-chip" onclick="statesPreset(0.05)">Твёрдое</button>
<button class="proj-preset-chip" onclick="statesPreset(0.30)">Жидкость</button>
<button class="proj-preset-chip" onclick="statesPreset(0.75)">Газ</button>
</div>
<button id="states-vec-btn" class="proj-preset-chip" style="margin-bottom:10px;width:100%" onclick="statesToggleVectors(this)">Векторы скоростей: Выкл</button>
<div class="gp-section-title" style="margin-bottom:8px">Фаза и энергия</div>
<div class="tri-stats-grid" style="grid-template-columns:1fr 1fr;margin-bottom:8px">
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Фаза</div>
<div class="tri-stat-v" id="ststat-phase" style="text-align:center"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Кин. энергия</div>
<div class="tri-stat-v" id="ststat-KE" style="text-align:center;color:#FFD166"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Пот. энергия</div>
<div class="tri-stat-v" id="ststat-PE" style="text-align:center;color:#9B5DE5"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Давление</div>
<div class="tri-stat-v" id="ststat-P" style="text-align:center;color:#EF476F"></div>
</div>
<div style="font-size:.65rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:auto">LJ потенциал · g(r) — структура · цвет = скорость</div>
</div>
<!-- ── Diffusion panel ── -->
<div id="mol-panel-diffusion" style="display:none">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Молекул каждого вида</span><span class="param-val" id="df-N">60</span></div>
<input type="range" class="param-slider" id="sl-dfN" min="20" max="120" value="60" oninput="diffNChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Температура T</span><span class="param-val" id="df-T">1.0 у.е.</span></div>
<input type="range" class="param-slider" id="sl-dfT" min="2" max="20" value="10" oninput="diffTChange()">
</div>
<label class="tri-layer-row active" id="df-part-row" onclick="diffPartitionToggle(this)" style="margin-bottom:6px">
<span class="tri-dot" style="background:rgba(255,255,255,0.5)"></span>
<span class="tri-layer-name">Перегородка</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,.4)"></span>
<span class="tri-toggle" id="df-part-toggle" style="background:var(--violet)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:14px"></span></span>
</label>
<label class="tri-layer-row" id="df-pore-row" onclick="diffPoreToggle(this)" style="margin-bottom:10px">
<span class="tri-dot" style="background:rgba(255,179,71,0.5)"></span>
<span class="tri-layer-name">Пора (щель)</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,.4)"></span>
<span class="tri-toggle" id="df-pore-toggle" style="background:rgba(255,255,255,0.15)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px"></span></span>
</label>
<div class="gp-section-title" style="margin-bottom:8px">Концентрации</div>
<div class="tri-stats-grid" style="grid-template-columns:1fr 1fr;margin-bottom:8px">
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#06D6E0">Лево A</div>
<div class="tri-stat-v" id="dfstat-LA" style="text-align:center;color:#06D6E0"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#F15BB5">Лево B</div>
<div class="tri-stat-v" id="dfstat-LB" style="text-align:center;color:#F15BB5"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#06D6E0">Право A</div>
<div class="tri-stat-v" id="dfstat-RA" style="text-align:center;color:#06D6E0"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#F15BB5">Право B</div>
<div class="tri-stat-v" id="dfstat-RB" style="text-align:center;color:#F15BB5"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Смешивание</div>
<div class="tri-stat-v" id="dfstat-mix" style="text-align:center;color:#34d399"></div>
</div>
<div style="font-size:.65rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:auto">A (cyan) — лево · B (розовый) — право</div>
</div>
</div><!-- /.proj-panel -->
<div class="proj-canvas-outer">
<canvas id="gas-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
<canvas id="brownian-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
<canvas id="states-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
<canvas id="diffusion-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<div class="proj-stats-bar" id="mol-stats-bar">
<div class="pstat"><div class="pstat-label" id="mpbar-l1"></div><div class="pstat-val" id="mpbar-v1" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label" id="mpbar-l2"></div><div class="pstat-val" id="mpbar-v2" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label" id="mpbar-l3"></div><div class="pstat-val" id="mpbar-v3" style="color:#9B5DE5"></div></div>
<div class="pstat"><div class="pstat-label" id="mpbar-l4"></div><div class="pstat-val" id="mpbar-v4" style="color:#34d399"></div></div>
<div class="pstat"><div class="pstat-label" id="mpbar-l5"></div><div class="pstat-val" id="mpbar-v5"></div></div>
</div>
</div><!-- /#sim-molphys -->
<!-- ══════════════════════════════════════════════
ЗАКОН КУЛОНА
══════════════════════════════════════════════ -->
<div id="sim-coulomb" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:240px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Знак заряда</div>
<div style="display:flex;gap:6px;margin-bottom:12px">
<button class="mag-mode-btn active" id="cbtn-pos" onclick="coulombSign(1)" style="flex:1" title="Добавить положительный заряд">
<span style="font-size:1.3rem;font-weight:900;color:#EF476F">+</span> Положит.
</button>
<button class="mag-mode-btn" id="cbtn-neg" onclick="coulombSign(-1)" style="flex:1" title="Добавить отрицательный заряд">
<span style="font-size:1.3rem;font-weight:900;color:#4CC9F0"></span> Отрицат.
</button>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Слои</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px">
<label class="tri-layer-row active" id="cl-colormap" onclick="coulombLayer('colormap',this)">
<span class="tri-dot" style="background:#9B5DE5;box-shadow:0 0 5px #9B5DE5"></span>
<span class="tri-layer-name">Карта потенциала</span>
<span class="tri-layer-hint" style="color:#9B5DE5">V</span>
<span class="tri-toggle" style="background:var(--violet)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:14px"></span></span>
</label>
<label class="tri-layer-row active" id="cl-fieldlines" onclick="coulombLayer('fieldlines',this)">
<span class="tri-dot" style="background:rgba(255,255,255,0.8);box-shadow:0 0 5px rgba(255,255,255,0.6)"></span>
<span class="tri-layer-name">Линии поля</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,0.5)">E</span>
<span class="tri-toggle" style="background:var(--violet)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:14px"></span></span>
</label>
<label class="tri-layer-row" id="cl-vectors" onclick="coulombLayer('vectors',this)">
<span class="tri-dot" style="background:rgba(255,255,255,0.4)"></span>
<span class="tri-layer-name">Векторы E</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,0.3)"><svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></span>
<span class="tri-toggle"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px"></span></span>
</label>
<label class="tri-layer-row active" id="cl-equipotentials" onclick="coulombLayer('equipotentials',this)">
<span class="tri-dot" style="background:rgba(255,255,255,0.5)"></span>
<span class="tri-layer-name">Эквипотенциали</span>
<span class="tri-layer-hint" style="color:rgba(255,255,255,0.3)">V=const</span>
<span class="tri-toggle" style="background:var(--violet)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:14px"></span></span>
</label>
<label class="tri-layer-row" id="cl-forces" onclick="coulombLayer('forces',this)">
<span class="tri-dot" style="background:#FFD166;box-shadow:0 0 5px #FFD166"></span>
<span class="tri-layer-name">Силы Кулона</span>
<span class="tri-layer-hint" style="color:#FFD166">F</span>
<span class="tri-toggle"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px"></span></span>
</label>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="coulombPreset('dipole')">Диполь ±</button>
<button class="proj-preset-chip" onclick="coulombPreset('equal')">Два + заряда</button>
<button class="proj-preset-chip" onclick="coulombPreset('quadrupole')">Квадруполь</button>
<button class="proj-preset-chip" onclick="coulombPreset('ring')">Кольцо</button>
</div>
<div style="margin-top:auto;display:flex;flex-direction:column;gap:5px">
<div class="tri-stats-grid" style="grid-template-columns:auto 1fr">
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Зарядов</div>
<div class="tri-stat-v" id="cs-total" style="color:#9B5DE5">0</div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Курсор E</div>
<div class="tri-stat-v" id="cs-curE" style="color:rgba(255,255,255,0.6)"></div>
<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3)">Курсор V</div>
<div class="tri-stat-v" id="cs-curV" style="color:rgba(255,255,255,0.5)"></div>
</div>
<div style="font-size:0.68rem;color:var(--text-3);text-align:center;line-height:1.6;margin-top:4px">
Клик — добавить &nbsp;·&nbsp; ПКМ — удалить<br>
Перетащи заряд для перемещения
</div>
</div>
</div><!-- /.proj-panel -->
<div class="proj-canvas-outer">
<canvas id="coulomb-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<div class="proj-stats-bar">
<div class="pstat"><div class="pstat-label">Зарядов</div><div class="pstat-val" id="csbar-total">0</div></div>
<div class="pstat"><div class="pstat-label">+ Позитивных</div><div class="pstat-val" id="csbar-pos" style="color:#EF476F">0</div></div>
<div class="pstat"><div class="pstat-label"> Негативных</div><div class="pstat-val" id="csbar-neg" style="color:#4CC9F0">0</div></div>
<div class="pstat"><div class="pstat-label">max |E|</div><div class="pstat-val" id="csbar-maxE"></div></div>
<div class="pstat"><div class="pstat-label">E курсора</div><div class="pstat-val" id="csbar-curE" style="color:rgba(255,255,255,0.7)"></div></div>
</div>
</div><!-- /#sim-coulomb -->
<!-- ══════════════════════════════════════════════
ЭЛЕКТРИЧЕСКИЕ ЦЕПИ
══════════════════════════════════════════════ -->
<div id="sim-circuit" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:240px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Инструмент</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip circ-tool-btn active" id="ptool-wire" onclick="circTool('wire',this)" data-tool="wire">Провод</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-resistor" onclick="circTool('resistor',this)" data-tool="resistor">Резистор</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-battery" onclick="circTool('battery',this)" data-tool="battery">Батарея</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-capacitor" onclick="circTool('capacitor',this)" data-tool="capacitor">Конденсатор</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-diode" onclick="circTool('diode',this)" data-tool="diode">Диод</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-led" onclick="circTool('led',this)" data-tool="led">LED</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-ac" onclick="circTool('ac',this)" data-tool="ac">AC источник</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-switch" onclick="circTool('switch',this)" data-tool="switch">Выключатель</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-lamp" onclick="circTool('lamp',this)" data-tool="lamp">Лампа</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-ammeter" onclick="circTool('ammeter',this)" data-tool="ammeter">Амперметр</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-voltmeter" onclick="circTool('voltmeter',this)" data-tool="voltmeter">Вольтметр</button>
<button class="proj-preset-chip circ-tool-btn" id="ptool-erase" onclick="circTool('erase',this)" data-tool="erase">Ластик</button>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Сопротивление R</span>
<span class="param-val" id="circ-R-val">10 Ω</span>
</div>
<input type="range" class="param-slider" id="sl-circR" min="1" max="100" value="10" oninput="circRChange()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Напряжение U</span>
<span class="param-val" id="circ-U-val">9 В</span>
</div>
<input type="range" class="param-slider" id="sl-circU" min="1" max="24" value="9" oninput="circUChange()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Ёмкость C</span>
<span class="param-val" id="circ-C-val">100 µF</span>
</div>
<input type="range" class="param-slider" id="sl-circC" min="10" max="1000" value="100" step="10" oninput="circCChange()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота AC</span>
<span class="param-val" id="circ-F-val">2 Гц</span>
</div>
<input type="range" class="param-slider" id="sl-circF" min="1" max="20" value="2" oninput="circFChange()">
</div>
<div class="gp-section-title" style="margin-bottom:8px;margin-top:4px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="circPreset('serial')">Последовательное</button>
<button class="proj-preset-chip" onclick="circPreset('parallel')">Параллельное</button>
<button class="proj-preset-chip" onclick="circPreset('lamp')">Лампа + выкл</button>
<button class="proj-preset-chip" onclick="circPreset('divider')">Делитель V</button>
<button class="proj-preset-chip" onclick="circPreset('bridge')">Мост Уитстона</button>
<button class="proj-preset-chip" onclick="circPreset('diode')">Диод</button>
<button class="proj-preset-chip" onclick="circPreset('led')">LED</button>
<button class="proj-preset-chip" onclick="circPreset('rc')">RC-цепь</button>
<button class="proj-preset-chip" onclick="circPreset('ac')">AC-цепь</button>
</div>
<div style="margin-top:auto;font-size:0.68rem;color:var(--text-3);text-align:center;line-height:1.7;padding-top:4px">
Тяни узлы для рисования · ПКМ — удалить<br>
2×клик по выключателю — вкл/выкл · Ctrl+Z отмена
</div>
</div><!-- /.proj-panel -->
<div class="proj-canvas-outer">
<canvas id="circuit-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<div class="proj-stats-bar">
<div class="pstat"><div class="pstat-label">Компонентов</div><div class="pstat-val" id="cirbar-comps">0</div></div>
<div class="pstat"><div class="pstat-label">Напряжение U</div><div class="pstat-val" id="cirbar-U" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">Ток I</div><div class="pstat-val" id="cirbar-I" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Мощность P</div><div class="pstat-val" id="cirbar-P" style="color:#7BF5A4"></div></div>
<div class="pstat"><div class="pstat-label">Статус</div><div class="pstat-val" id="cirbar-status"></div></div>
</div>
</div><!-- /#sim-circuit -->
<!-- ══════════════════════════════════════════════
КИНЕТИКА РЕАКЦИЙ
══════════════════════════════════════════════ -->
<!-- ══════════════════════════════════════════════
ХИМИЯ (unified: кинетика + колба + ОВР + ионный обмен)
══════════════════════════════════════════════ -->
<div id="sim-chemistry" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:248px;gap:0">
<!-- Mode selector -->
<div style="display:flex;gap:3px;margin-bottom:12px;padding:3px;background:rgba(255,255,255,0.04);border-radius:10px;border:1px solid var(--border)">
<button class="mag-mode-btn chem-mode active" id="chem-mode-kinetics" onclick="chemMode('kinetics',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Кинетика</button>
<button class="mag-mode-btn chem-mode" id="chem-mode-flask" onclick="chemMode('flask',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Колба</button>
<button class="mag-mode-btn chem-mode" id="chem-mode-redox" onclick="chemMode('redox',this)" style="flex:1;font-size:.63rem;padding:5px 2px">ОВР</button>
<button class="mag-mode-btn chem-mode" id="chem-mode-ionex" onclick="chemMode('ionex',this)" style="flex:1;font-size:.63rem;padding:5px 2px">Ионный</button>
</div>
<!-- ── Kinetics panel ── -->
<div id="chem-panel-kinetics">
<div class="gp-section-title" style="margin-bottom:6px">Параметры</div>
<div class="param-block" style="margin-bottom:7px">
<div class="param-header"><span class="param-name">Молекул N</span><span class="param-val" id="reac-N-val">28</span></div>
<input type="range" class="param-slider" id="sl-reacN" min="5" max="60" value="28" oninput="reacNChange()">
</div>
<div class="param-block" style="margin-bottom:7px">
<div class="param-header"><span class="param-name">Температура T</span><span class="param-val" id="reac-T-val">1.2</span></div>
<input type="range" class="param-slider" id="sl-reacT" min="2" max="40" value="12" oninput="reacTChange()">
</div>
<div class="param-block" style="margin-bottom:7px">
<div class="param-header"><span class="param-name">Энергия активации Ea</span><span class="param-val" id="reac-Ea-val">2.0</span></div>
<input type="range" class="param-slider" id="sl-reacEa" min="5" max="50" value="20" oninput="reacEaChange()">
</div>
<div class="gp-section-title" style="margin-bottom:6px">Режим</div>
<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:8px">
<button class="proj-preset-chip reac-mode-btn active" id="rmode-forward" onclick="reacMode('forward',this)" style="text-align:left"><svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Прямая (A+B<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>C)</button>
<button class="proj-preset-chip reac-mode-btn" id="rmode-reversible" onclick="reacMode('reversible',this)" style="text-align:left">⇌ Обратимая</button>
<button class="proj-preset-chip reac-mode-btn" id="rmode-chain" onclick="reacMode('chain',this)" style="text-align:left"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg> Цепная</button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:5px">
<button class="proj-preset-chip" onclick="reacPreset('simple')">Простая</button>
<button class="proj-preset-chip" onclick="reacPreset('reversible')">Равновесие</button>
<button class="proj-preset-chip" onclick="reacPreset('hot')">Горячая</button>
<button class="proj-preset-chip" onclick="reacPreset('cold')">Холодная</button>
<button class="proj-preset-chip" onclick="reacPreset('chain')">Цепная</button>
</div>
</div><!-- /chem-panel-kinetics -->
<!-- ── Flask panel ── -->
<div id="chem-panel-flask" style="display:none">
<div class="gp-section-title" style="margin-bottom:8px">Металл</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip flask-metal-btn active" id="fmetal-Zn" onclick="flaskMetal('Zn',this)">Zn</button>
<button class="proj-preset-chip flask-metal-btn" id="fmetal-Fe" onclick="flaskMetal('Fe',this)">Fe</button>
<button class="proj-preset-chip flask-metal-btn" id="fmetal-Mg" onclick="flaskMetal('Mg',this)">Mg</button>
<button class="proj-preset-chip flask-metal-btn" id="fmetal-Cu" onclick="flaskMetal('Cu',this)">Cu</button>
<button class="proj-preset-chip flask-metal-btn" id="fmetal-Na" onclick="flaskMetal('Na',this)">Na</button>
<button class="proj-preset-chip flask-metal-btn" id="fmetal-Al" onclick="flaskMetal('Al',this)">Al</button>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Реагент</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip flask-acid-btn active" id="facid-HCl" onclick="flaskAcid('HCl',this)">HCl</button>
<button class="proj-preset-chip flask-acid-btn" id="facid-H2SO4" onclick="flaskAcid('H2SO4',this)">H₂SO₄</button>
<button class="proj-preset-chip flask-acid-btn" id="facid-H2O" onclick="flaskAcid('H2O',this)">H₂O</button>
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Концентрация</span><span class="param-val" id="flask-conc-val">35%</span></div>
<input type="range" class="param-slider" id="sl-flask-conc" min="5" max="98" value="35" oninput="flaskConcChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Темп. окружения</span><span class="param-val" id="flask-temp-val">20°C</span></div>
<input type="range" class="param-slider" id="sl-flask-temp" min="5" max="80" value="20" oninput="flaskTempChange()">
</div>
<div style="display:flex;gap:6px;margin-bottom:10px;margin-top:2px">
<button class="proj-preset-chip" style="flex:1;background:rgba(239,71,111,0.18);border-color:rgba(239,71,111,0.4)"
onclick="flaskSim && flaskSim.dropMetal()">⬇ Бросить металл</button>
<button class="proj-preset-chip" id="flask-flame-panel" style="flex:0 0 auto"
onclick="flaskToggleFlame()"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M12 2c.5 3.5-1.5 6-1.5 6 1 1.5 3 2 3 5a4 4 0 01-8 0c0-2 .5-3 1.5-4.5C8.5 6.5 7 4.5 7 4.5S9.5 2 12 2z"/></svg></button>
</div>
<div style="font-size:.65rem;color:var(--text-3);text-align:center;line-height:1.7;margin-top:auto;padding-top:4px">
Выбери металл + кислоту<br>H₂ накапливается — поднеси огонь!
</div>
</div><!-- /chem-panel-flask -->
<!-- ── Redox panel ── -->
<div id="chem-panel-redox" style="display:none">
<div class="gp-section-title" style="margin-bottom:8px">Реакция</div>
<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:12px">
<button class="proj-preset-chip redox-rxn-btn active" id="rdxrxn-fe_cu" onclick="redoxRxn('fe_cu',this)" style="text-align:left;font-size:.72rem">Fe + CuSO₄ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> FeSO₄ + Cu<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
<button class="proj-preset-chip redox-rxn-btn" id="rdxrxn-zn_hcl" onclick="redoxRxn('zn_hcl',this)" style="text-align:left;font-size:.72rem">Zn + 2HCl <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ZnCl₂ + H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
<button class="proj-preset-chip redox-rxn-btn" id="rdxrxn-cl2_ki" onclick="redoxRxn('cl2_ki',this)" style="text-align:left;font-size:.72rem">Cl₂ + 2KI <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> I₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg> + 2KCl</button>
<button class="proj-preset-chip redox-rxn-btn" id="rdxrxn-kmno4" onclick="redoxRxn('kmno4',this)" style="text-align:left;font-size:.72rem">KMnO₄ + FeSO₄ (кислая)</button>
<button class="proj-preset-chip redox-rxn-btn" id="rdxrxn-cu_fecl3" onclick="redoxRxn('cu_fecl3',this)" style="text-align:left;font-size:.72rem">Cu + 2FeCl₃ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> CuCl₂ + 2FeCl₂</button>
</div>
<button class="proj-launch-btn" style="margin-bottom:8px" onclick="redoxStart()"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Начать реакцию</button>
<button class="proj-reset-btn" onclick="redoxReset()"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сброс</button>
<div style="margin-top:auto;padding-top:6px;font-size:.65rem;color:var(--text-3);line-height:1.7;text-align:center">
Синие e⁻ — перенос электронов<br>Цифры — степень окисления
</div>
</div><!-- /chem-panel-redox -->
<!-- ── Ion exchange panel ── -->
<div id="chem-panel-ionex" style="display:none">
<div class="gp-section-title" style="margin-bottom:8px">Реакция</div>
<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:12px">
<button class="proj-preset-chip ionex-rxn-btn active" id="ioxrxn-ba_so4" onclick="ionexRxn('ba_so4',this)" style="text-align:left;font-size:.72rem">BaCl₂ + Na₂SO₄ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> BaSO₄<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
<button class="proj-preset-chip ionex-rxn-btn" id="ioxrxn-ag_cl" onclick="ionexRxn('ag_cl',this)" style="text-align:left;font-size:.72rem">AgNO₃ + NaCl <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> AgCl<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
<button class="proj-preset-chip ionex-rxn-btn" id="ioxrxn-co3_hcl" onclick="ionexRxn('co3_hcl',this)" style="text-align:left;font-size:.72rem">Na₂CO₃ + 2HCl <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> CO₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
<button class="proj-preset-chip ionex-rxn-btn" id="ioxrxn-pb_i" onclick="ionexRxn('pb_i',this)" style="text-align:left;font-size:.72rem">Pb(NO₃)₂ + 2KI <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> PbI₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
<button class="proj-preset-chip ionex-rxn-btn" id="ioxrxn-ca_co3" onclick="ionexRxn('ca_co3',this)" style="text-align:left;font-size:.72rem">CaCl₂ + Na₂CO₃ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> CaCO₃<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
</div>
<button class="proj-launch-btn" style="margin-bottom:8px" onclick="ionexStart()"><svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Смешать растворы</button>
<button class="proj-reset-btn" onclick="ionexReset()"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сброс</button>
<div style="margin-top:auto;padding-top:6px;font-size:.65rem;color:var(--text-3);line-height:1.7;text-align:center">
Яркие ионы = реагируют<br>Тусклые = зрители · <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg> осадок · <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg> газ
</div>
</div><!-- /chem-panel-ionex -->
</div><!-- /.proj-panel -->
<div class="proj-canvas-outer">
<canvas id="reactions-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
<canvas id="flask-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%;cursor:default"></canvas>
<canvas id="redox-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%;cursor:default"></canvas>
<canvas id="ionexchange-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%;cursor:default"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<div class="proj-stats-bar" id="chem-stats-bar">
<div class="pstat"><div class="pstat-label" id="chbar-l1"></div><div class="pstat-val" id="chbar-v1" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label" id="chbar-l2"></div><div class="pstat-val" id="chbar-v2" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label" id="chbar-l3"></div><div class="pstat-val" id="chbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label" id="chbar-l4"></div><div class="pstat-val" id="chbar-v4"></div></div>
<div class="pstat"><div class="pstat-label" id="chbar-l5"></div><div class="pstat-val" id="chbar-v5" style="color:#7BF5A4"></div></div>
</div>
</div><!-- /#sim-chemistry -->
<!-- ── DYNAMICS sim body (Newton + Sandbox unified) ── -->
<div id="sim-dynamics" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:248px;gap:0">
<!-- ══ Mode selector ══ -->
<div style="display:flex;gap:4px;margin-bottom:12px;padding:3px;background:rgba(255,255,255,0.04);border-radius:10px;border:1px solid var(--border)">
<button class="mag-mode-btn dyn-mode active" id="dyn-mode-sandbox" onclick="dynMode('sandbox',this)" style="flex:1;font-size:.68rem;padding:5px 0">Песочница</button>
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law1" onclick="dynMode('law1',this)" style="flex:1;font-size:.68rem;padding:5px 0">I закон</button>
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law2" onclick="dynMode('law2',this)" style="flex:1;font-size:.68rem;padding:5px 0">II закон</button>
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law3" onclick="dynMode('law3',this)" style="flex:1;font-size:.68rem;padding:5px 0">III закон</button>
</div>
<!-- ══ Newton controls (shown in law modes) ══ -->
<div id="dyn-newton-panel" style="display:none">
<!-- Scene selector -->
<div class="gp-section-title" style="margin-bottom:8px">Сцена</div>
<div style="display:flex;gap:5px;margin-bottom:12px" id="newton-scene-row">
<button class="mag-mode-btn nscene-btn active" id="nscn-panel-A" onclick="newtonScene('A',null,this)" style="flex:1;font-size:.72rem">A</button>
<button class="mag-mode-btn nscene-btn" id="nscn-panel-B" onclick="newtonScene('B',null,this)" style="flex:1;font-size:.72rem">B</button>
<button class="mag-mode-btn nscene-btn" id="nscn-panel-C" onclick="newtonScene('C',null,this)" style="flex:1;font-size:.72rem">C</button>
</div>
<!-- Scene description -->
<div id="newton-scene-desc" style="font-size:0.71rem;color:var(--text-3);line-height:1.6;margin-bottom:10px;padding:6px 8px;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid var(--border)">
Закон инерции: тело движется равномерно при отсутствии сил.
</div>
<!-- Parameter sliders -->
<div id="newton-mu-block" class="param-block">
<div class="param-header"><span class="param-name">Коэфф. трения μ</span><span class="param-val" id="newton-mu-val">0.20</span></div>
<input type="range" class="param-slider" id="sl-newton-mu" min="0" max="1" step="0.01" value="0.20" oninput="newtonMuChange()">
</div>
<div id="newton-mass1-block" class="param-block" style="display:none">
<div class="param-header"><span class="param-name">Масса m₁ (кг)</span><span class="param-val" id="newton-m1-val">5 кг</span></div>
<input type="range" class="param-slider" id="sl-newton-m1" min="1" max="20" value="5" oninput="newtonMass1Change()">
</div>
<div id="newton-mass2-block" class="param-block" style="display:none">
<div class="param-header"><span class="param-name">Масса m₂ (кг)</span><span class="param-val" id="newton-m2-val">5 кг</span></div>
<input type="range" class="param-slider" id="sl-newton-m2" min="1" max="20" value="5" oninput="newtonMass2Change()">
</div>
<div id="newton-force-block" class="param-block" style="display:none">
<div class="param-header"><span class="param-name">Сила F (Н)</span><span class="param-val" id="newton-F-val">20 Н</span></div>
<input type="range" class="param-slider" id="sl-newton-F" min="1" max="60" value="20" oninput="newtonForceChange()">
</div>
<!-- Presets -->
<div class="gp-section-title" style="margin-top:6px">Пресеты</div>
<div id="newton-presets" style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="newtonPreset('space')">Космос</button>
<button class="proj-preset-chip" onclick="newtonPreset('ice')">Лёд</button>
<button class="proj-preset-chip" onclick="newtonPreset('asphalt')">Асфальт</button>
<button class="proj-preset-chip" onclick="newtonPreset('rubber')">Резина</button>
</div>
<!-- Action -->
<div style="margin-top:auto;padding-top:10px;display:flex;flex-direction:column;gap:6px">
<button class="proj-launch-btn" id="newton-action-main" onclick="newtonAction()">
<span id="newton-action-label"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Действие</span>
</button>
<button class="proj-reset-btn" onclick="_resetNewtonScene()">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
Сброс сцены
</button>
</div>
</div><!-- /#dyn-newton-panel -->
<!-- ══ Sandbox controls (shown in sandbox mode) ══ -->
<div id="dyn-sandbox-panel">
<div class="gp-section-title" style="margin-bottom:8px">Инструмент</div>
<div style="display:flex;gap:5px;margin-bottom:4px">
<button class="mag-mode-btn sb-panel-tool active" id="sbpt-box" onclick="sbTool('box',this)" style="flex:1;font-size:.72rem"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/></svg> Блок</button>
<button class="mag-mode-btn sb-panel-tool" id="sbpt-ball" onclick="sbTool('ball',this)" style="flex:1;font-size:.72rem"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/></svg> Шар</button>
<button class="mag-mode-btn sb-panel-tool" id="sbpt-erase" onclick="sbTool('erase',this)" style="flex:1;font-size:.72rem"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
<div style="display:flex;gap:5px;margin-bottom:10px">
<button class="mag-mode-btn sb-panel-tool" id="sbpt-spring" onclick="sbTool('spring',this)" style="flex:1;font-size:.72rem">〜 Пружина</button>
<button class="mag-mode-btn sb-panel-tool" id="sbpt-rope" onclick="sbTool('rope',this)" style="flex:1;font-size:.72rem">— Нить</button>
<button class="mag-mode-btn sb-panel-tool" id="sbpt-anchor" onclick="sbTool('anchor',this)" style="flex:1;font-size:.72rem"><svg class="ic" viewBox="0 0 24 24"><path d="M12 2 2 12 12 22 22 12Z"/></svg> Якорь</button>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Режим силы</div>
<div style="display:flex;gap:5px;margin-bottom:10px">
<button class="mag-mode-btn sb-fmode active" id="sbfm-constant" onclick="sbForceMode('constant',this)" style="flex:1;font-size:.72rem">Постоянная</button>
<button class="mag-mode-btn sb-fmode" id="sbfm-impulse" onclick="sbForceMode('impulse',this)" style="flex:1;font-size:.72rem">Импульс</button>
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Масса нового тела</span><span class="param-val" id="sb-mass-val">5 кг</span></div>
<input type="range" class="param-slider" id="sl-sb-mass" min="1" max="30" value="5" oninput="sbMassChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Упругость</span><span class="param-val" id="sb-rest-val">0.65</span></div>
<input type="range" class="param-slider" id="sl-sb-rest" min="0" max="1" step="0.05" value="0.65" oninput="sbRestChange()">
</div>
<div class="param-block" id="sb-spring-block" style="display:none">
<div class="param-header"><span class="param-name">Жёсткость пружины k</span><span class="param-val" id="sb-springk-val">120 Н</span></div>
<input type="range" class="param-slider" id="sl-sb-springk" min="10" max="600" step="10" value="120" oninput="sbSpringKChange()">
</div>
<div class="gp-section-title" style="margin-top:6px;margin-bottom:6px">Мир</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px">
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-gravity" checked onchange="sbWorldToggle()"> Гравитация</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-floor" checked onchange="sbWorldToggle()"> Пол</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-walls" checked onchange="sbWorldToggle()"> Стенки</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-airdrag" onchange="sbWorldToggle()"> Сопротивление воздуха</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-ramp" onchange="sbRampToggle()"> Наклонная плоскость</label>
</div>
<div id="sb-ramp-block" style="display:none">
<div class="param-block">
<div class="param-header"><span class="param-name">Угол α</span><span class="param-val" id="sb-angle-val">30°</span></div>
<input type="range" class="param-slider" id="sl-sb-angle" min="5" max="75" value="30" oninput="sbAngleChange()">
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Трение горки μ</span><span class="param-val" id="sb-rampmu-val">0.20</span></div>
<input type="range" class="param-slider" id="sl-sb-rampmu" min="0" max="1" step="0.01" value="0.20" oninput="sbRampMuChange()">
</div>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer;margin-bottom:6px"><input type="checkbox" id="sb-decomp" checked onchange="sbDecompToggle()"> Разложение сил</label>
</div>
<div class="param-block">
<div class="param-header"><span class="param-name">Трение пола μ</span><span class="param-val" id="sb-floormu-val">0.30</span></div>
<input type="range" class="param-slider" id="sl-sb-floormu" min="0" max="1" step="0.01" value="0.30" oninput="sbFloorMuChange()">
</div>
<div class="gp-section-title" style="margin-top:6px;margin-bottom:6px">Отображение</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:10px">
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-forces" checked onchange="sbDisplayToggle()"> Силы</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-vel" checked onchange="sbDisplayToggle()"> Скорости</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-fbd" onchange="sbDisplayToggle()"> FBD-диаграмма</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-energy" checked onchange="sbDisplayToggle()"> Энергия</label>
<label style="display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-2);cursor:pointer"><input type="checkbox" id="sb-trail" checked onchange="sbDisplayToggle()"> Стробоскоп</label>
</div>
<div class="gp-section-title" style="margin-top:2px;margin-bottom:6px">Время</div>
<div style="display:flex;gap:5px;margin-bottom:10px">
<button class="mag-mode-btn sb-time" onclick="sbTimeScale(0.25,this)" style="flex:1;font-size:.68rem">×0.25</button>
<button class="mag-mode-btn sb-time" onclick="sbTimeScale(0.5,this)" style="flex:1;font-size:.68rem">×0.5</button>
<button class="mag-mode-btn sb-time active" onclick="sbTimeScale(1,this)" style="flex:1;font-size:.68rem">×1</button>
<button class="mag-mode-btn sb-time" onclick="sbTimeScale(2,this)" style="flex:1;font-size:.68rem">×2</button>
</div>
<div class="gp-section-title" style="margin-top:2px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="sbPreset('freefall')">Падение</button>
<button class="proj-preset-chip" onclick="sbPreset('collision')">Столкновение</button>
<button class="proj-preset-chip" onclick="sbPreset('friction')">Трение</button>
<button class="proj-preset-chip" onclick="sbPreset('tug')">Перетягивание</button>
<button class="proj-preset-chip" onclick="sbPreset('balance')">Равновесие</button>
<button class="proj-preset-chip" onclick="sbPreset('ramp_slide')">Горка</button>
<button class="proj-preset-chip" onclick="sbPreset('ramp_angle')"><svg class="ic" viewBox="0 0 24 24"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg> Крутой спуск</button>
<button class="proj-preset-chip" onclick="sbPreset('ramp_friction')"><svg class="ic" viewBox="0 0 24 24"><rect width="20" height="5" x="2" y="3" rx="1"/><rect width="8" height="5" x="2" y="11" rx="1"/><rect width="8" height="5" x="14" y="11" rx="1"/><rect width="20" height="5" x="2" y="19" rx="1"/></svg> Трение на горке</button>
<button class="proj-preset-chip" onclick="sbPreset('spring_bounce')">〜 Пружина</button>
<button class="proj-preset-chip" onclick="sbPreset('spring_chain')">〜 Цепочка</button>
<button class="proj-preset-chip" onclick="sbPreset('pendulum')">⬤ Маятник</button>
<button class="proj-preset-chip" onclick="sbPreset('atwood')"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93 17.66 6.34M21 12h-2M19.07 19.07l-1.41-1.41M12 21v-2M6.34 17.66 4.93 19.07M3 12h2M4.93 4.93l1.41 1.41M12 3v2"/><circle cx="12" cy="12" r="7"/></svg> Машина Атвуда</button>
<button class="proj-preset-chip" onclick="sbPreset('two_body')"><svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="21" x2="12" y2="3"/><polyline points="7 8 12 3 17 8"/><polyline points="17 16 12 21 7 16"/></svg> Два тела</button>
<button class="proj-preset-chip" onclick="sbPreset('elastic_collision')"><svg class="ic" viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Упругий удар</button>
<button class="proj-preset-chip" onclick="sbPreset('inelastic_collision')"><svg class="ic" viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg> Неупругий</button>
<button class="proj-preset-chip" onclick="sbPreset('newton_cradle')"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93 17.66 6.34M21 12h-2M19.07 19.07l-1.41-1.41M12 21v-2M6.34 17.66 4.93 19.07M3 12h2M4.93 4.93l1.41 1.41M12 3v2"/><circle cx="12" cy="12" r="7"/></svg> Колыбель Ньютона</button>
<button class="proj-preset-chip" onclick="sbPreset('harmonic_oscillator')">〜 Осциллятор</button>
<button class="proj-preset-chip" onclick="sbPreset('double_pendulum')">⬤⬤ Двойной маятник</button>
<button class="proj-preset-chip" onclick="sbPreset('coupled_oscillators')">〜〜 Связанные</button>
<button class="proj-preset-chip" onclick="sbPreset('stacked_boxes')"><svg class="ic" viewBox="0 0 24 24"><rect width="20" height="5" x="2" y="3" rx="1"/><rect width="8" height="5" x="2" y="11" rx="1"/><rect width="8" height="5" x="14" y="11" rx="1"/><rect width="20" height="5" x="2" y="19" rx="1"/></svg> Стопка</button>
<button class="proj-preset-chip" onclick="sbPreset('pulley_ramp')"><svg class="ic" viewBox="0 0 24 24"><path d="m8 3 4 8 5-5 5 15H2L8 3z"/></svg> Горка+блок</button>
<button class="proj-preset-chip" onclick="sbPreset('circular_motion')">⭕ Круговое</button>
<button class="proj-preset-chip" onclick="sbPreset('projectile_angle')"><svg class="ic" viewBox="0 0 24 24"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg> Снаряд 45°</button>
</div>
<div style="font-size:.65rem;color:var(--text-3);line-height:1.5;margin-top:auto;padding:6px 8px;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid var(--border)">
ЛКМ — создать тело · Drag — сила<br>
Shift+drag — импульс · ПКМ — удалить<br>
DblClick — закрепить / открепить<br>
Пружина / Нить — кликни 2 тела
</div>
</div><!-- /#dyn-sandbox-panel -->
</div><!-- /.proj-panel -->
<div class="proj-canvas-outer">
<canvas id="newton-canvas" style="display:none;position:absolute;top:0;left:0;width:100%;height:100%;cursor:default"></canvas>
<canvas id="sandbox-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<div class="proj-stats-bar" id="dyn-stats-bar">
<div class="pstat"><div class="pstat-label" id="dbar-l1">Тел</div><div class="pstat-val" id="dbar-v1" style="color:#06D6E0">0</div></div>
<div class="pstat"><div class="pstat-label" id="dbar-l2">KE (Дж)</div><div class="pstat-val" id="dbar-v2" style="color:#4CC9F0">0</div></div>
<div class="pstat"><div class="pstat-label" id="dbar-l3">PE (Дж)</div><div class="pstat-val" id="dbar-v3" style="color:#7BF5A4">0</div></div>
<div class="pstat"><div class="pstat-label" id="dbar-l4">ΣF</div><div class="pstat-val" id="dbar-v4" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label" id="dbar-l5">Время</div><div class="pstat-val" id="dbar-v5" style="color:#FFD166">0 с</div></div>
</div>
</div><!-- /#sim-dynamics -->
<!-- ── PROJECTILE sim body ── -->
<div id="sim-proj" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<!-- controls panel -->
<div class="proj-panel">
<div class="gp-section-title">Параметры</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Скорость v₀</span>
<span class="param-val" id="p-v0">20 м/с</span>
</div>
<input type="range" class="param-slider" id="sl-v0" min="1" max="100" value="20" oninput="projParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Угол θ</span>
<span class="param-val" id="p-angle">45°</span>
</div>
<input type="range" class="param-slider" id="sl-angle" min="0" max="90" value="45" oninput="projParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Высота h₀</span>
<span class="param-val" id="p-h0">2 м</span>
</div>
<input type="range" class="param-slider" id="sl-h0" min="0" max="50" value="2" oninput="projParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Сила тяжести g</span>
<span class="param-val" id="p-g">9.81 м/с²</span>
</div>
<input type="range" class="param-slider" id="sl-g" min="1" max="25" step="0.01" value="9.81" oninput="projParam()">
</div>
<!-- Air resistance -->
<div class="gp-section-title" style="margin-top:6px">Сопротивление воздуха</div>
<label class="tri-layer-row" id="drag-row" onclick="projToggleDrag(this)" style="margin-bottom:6px">
<span class="tri-dot" style="background:rgba(239,71,111,0.5)"></span>
<span class="tri-layer-name">Воздух</span>
<span class="tri-toggle" id="drag-toggle" style="background:rgba(255,255,255,0.12)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px"></span></span>
</label>
<div id="drag-params" style="display:none">
<div class="param-block">
<div class="param-header">
<span class="param-name">Коэф. Cd</span>
<span class="param-val" id="p-cd">0.30</span>
</div>
<input type="range" class="param-slider" id="sl-cd" min="1" max="100" value="30" oninput="projCdChange()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Масса тела m</span>
<span class="param-val" id="p-mass">1 кг</span>
</div>
<input type="range" class="param-slider" id="sl-mass" min="1" max="20" value="1" oninput="projMassChange()">
</div>
</div>
<!-- Wind -->
<div class="gp-section-title" style="margin-top:6px">Ветер</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Скорость ветра</span>
<span class="param-val" id="p-wind">0 м/с</span>
</div>
<input type="range" class="param-slider" id="sl-wind" min="-20" max="20" value="0" step="1" oninput="projWindChange()">
</div>
<!-- Bounce -->
<div class="gp-section-title" style="margin-top:6px">Отскок</div>
<label class="tri-layer-row" id="bounce-row" onclick="projToggleBounce(this)" style="margin-bottom:6px">
<span class="tri-dot" style="background:rgba(123,245,164,0.5)"></span>
<span class="tri-layer-name">Отскок при ударе</span>
<span class="tri-toggle" id="bounce-toggle" style="background:rgba(255,255,255,0.12)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px;transition:margin-left .15s"></span></span>
</label>
<div id="bounce-params" style="display:none">
<div class="param-block">
<div class="param-header">
<span class="param-name">Коэф. упругости e</span>
<span class="param-val" id="p-restitution">0.70</span>
</div>
<input type="range" class="param-slider" id="sl-restitution" min="0" max="100" value="70" oninput="projRestitutionChange()">
</div>
</div>
<div class="gp-section-title" style="margin-top:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">
<button class="proj-preset-chip" onclick="projPreset(20,45,0,9.81)">Земля 45°</button>
<button class="proj-preset-chip" onclick="projPreset(20,30,0,9.81)">30°</button>
<button class="proj-preset-chip" onclick="projPreset(20,60,0,9.81)">60°</button>
<button class="proj-preset-chip" onclick="projPreset(20,45,15,9.81)">С высоты</button>
<button class="proj-preset-chip" onclick="projPreset(30,45,0,1.62)">Луна</button>
<button class="proj-preset-chip" onclick="projPreset(30,45,0,3.72)">Марс</button>
<button class="proj-preset-chip" onclick="projPreset(50,45,0,9.81)">Дальний</button>
<button class="proj-preset-chip" onclick="projPreset(20,90,0,9.81)">Вверх</button>
</div>
<!-- Speed -->
<div class="gp-section-title" style="margin-top:8px">Скорость симуляции</div>
<div style="display:flex;gap:5px;margin-bottom:8px">
<button class="mag-mode-btn proj-speed" onclick="projSetSpeed(0.25,this)" style="flex:1;font-size:.68rem">×0.25</button>
<button class="mag-mode-btn proj-speed" onclick="projSetSpeed(0.5,this)" style="flex:1;font-size:.68rem">×0.5</button>
<button class="mag-mode-btn proj-speed active" onclick="projSetSpeed(1,this)" style="flex:1;font-size:.68rem">×1</button>
<button class="mag-mode-btn proj-speed" onclick="projSetSpeed(2,this)" style="flex:1;font-size:.68rem">×2</button>
<button class="mag-mode-btn proj-speed" onclick="projSetSpeed(4,this)" style="flex:1;font-size:.68rem">×4</button>
</div>
<!-- Ghost trails -->
<div class="gp-section-title" style="margin-top:10px">Сравнение</div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="proj-preset-chip" onclick="projSaveGhost()" style="border-color:rgba(255,214,102,.4);color:#FFD166">Зафиксировать</button>
<button class="proj-preset-chip" onclick="projClearGhosts()" style="border-color:rgba(255,255,255,.15)"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Очистить следы</button>
</div>
<div style="font-size:.6rem;color:var(--text-3);margin-top:4px">Сохрани траекторию, измени параметры и сравни</div>
<!-- LAUNCH BUTTON -->
<div style="margin-top:auto; padding-top:16px; display:flex; flex-direction:column; gap:8px;">
<button class="proj-launch-btn" id="proj-launch-main" onclick="projPlayPause()">
<svg id="proj-launch-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
<span id="proj-launch-label">Запустить</span>
</button>
<button class="proj-reset-btn" onclick="pSim && pSim.reset(); _projSyncPlayBtn()">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>
</svg>
Сброс
</button>
</div>
</div>
<!-- canvas -->
<div class="proj-canvas-outer">
<canvas id="proj-canvas"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<!-- stats bar -->
<div class="proj-stats-bar">
<div class="pstat">
<div class="pstat-label">Дальность</div>
<div class="pstat-val" id="ps-range"></div>
</div>
<div class="pstat">
<div class="pstat-label">Макс. высота</div>
<div class="pstat-val" id="ps-hmax"></div>
</div>
<div class="pstat">
<div class="pstat-label">Время полёта</div>
<div class="pstat-val" id="ps-tf"></div>
</div>
<div class="pstat">
<div class="pstat-label">Скор. удара</div>
<div class="pstat-val" id="ps-vland"></div>
</div>
<div class="pstat">
<div class="pstat-label">Текущее t</div>
<div class="pstat-val" id="ps-t">0.00 с</div>
</div>
<div class="pstat">
<div class="pstat-label">Угол посадки</div>
<div class="pstat-val" id="ps-land-angle"></div>
</div>
<div class="pstat" id="ps-loss-wrap" style="display:none">
<div class="pstat-label">Δ дальность</div>
<div class="pstat-val" id="ps-loss"></div>
</div>
</div>
</div><!-- /#sim-proj -->
<!-- ── COLLISION sim body ── -->
<div id="sim-coll" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<!-- controls panel -->
<div class="proj-panel">
<div class="gp-section-title">Параметры</div>
<div class="param-block">
<div class="param-header">
<span class="param-name" style="color:#9B5DE5">Масса m₁</span>
<span class="param-val" id="c-m1">4 кг</span>
</div>
<input type="range" class="param-slider" id="sl-m1" min="1" max="20" value="4" oninput="collParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name" style="color:#06D6E0">Масса m₂</span>
<span class="param-val" id="c-m2">4 кг</span>
</div>
<input type="range" class="param-slider" id="sl-m2" min="1" max="20" value="4" oninput="collParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name" style="color:#9B5DE5">Скорость v₁</span>
<span class="param-val" id="c-v1">8 м/с</span>
</div>
<input type="range" class="param-slider" id="sl-cv1" min="0" max="30" value="8" oninput="collParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name" style="color:#06D6E0">Скорость v₂</span>
<span class="param-val" id="c-v2">8 м/с</span>
</div>
<input type="range" class="param-slider" id="sl-cv2" min="0" max="30" value="8" oninput="collParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Угол v₂</span>
<span class="param-val" id="c-angle"></span>
</div>
<input type="range" class="param-slider" id="sl-cangle" min="-60" max="60" value="0" oninput="collParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Упругость e</span>
<span class="param-val" id="c-e">1.00</span>
</div>
<input type="range" class="param-slider" id="sl-e" min="0" max="1" step="0.01" value="1" oninput="collParam()">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name"><svg class="ic" viewBox="0 0 24 24"><polygon points="13 19 22 12 13 5 13 19"/><polygon points="2 19 11 12 2 5 2 19"/></svg> Скорость</span>
<span class="param-val" id="c-speed" style="color:#06D6E0">1.00×</span>
</div>
<input type="range" class="param-slider" id="sl-speed" min="0.1" max="4" step="0.05" value="1"
oninput="collParam()" style="--track-color:#06D6E0">
</div>
<div class="gp-section-title" style="margin-top:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">
<button class="proj-preset-chip" onclick="collPreset(4,4,8,8,0,1)">Упругий 1:1</button>
<button class="proj-preset-chip" onclick="collPreset(4,4,8,8,0,0)">Абс. неупругий</button>
<button class="proj-preset-chip" onclick="collPreset(4,4,8,8,0,0.5)">e = 0.5</button>
<button class="proj-preset-chip" onclick="collPreset(4,4,12,0,0,1)">Бильярд</button>
<button class="proj-preset-chip" onclick="collPreset(2,8,14,0,0,1)">Лёгкий<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>тяжёлый</button>
<button class="proj-preset-chip" onclick="collPreset(8,2,6,0,0,1)">Тяжёлый<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>лёгкий</button>
<button class="proj-preset-chip" onclick="collPreset(2,14,18,0,0,1)">Большая разница</button>
<button class="proj-preset-chip" onclick="collPreset(5,5,8,8,30,1)">Косой 30°</button>
<button class="proj-preset-chip" onclick="collPreset(5,5,8,8,50,0.8)">Скользящий</button>
<button class="proj-preset-chip" onclick="collPreset(6,6,10,10,0,0)">Слипание</button>
</div>
<!-- launch button -->
<div style="margin-top:auto; padding-top:16px; display:flex; flex-direction:column; gap:8px;">
<button class="proj-launch-btn" id="coll-launch-main" onclick="collPlayPause()">
<svg id="coll-launch-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
<span id="coll-launch-label">Запустить</span>
</button>
<button class="proj-reset-btn" onclick="cSim && cSim.reset(); _collSyncBtn()">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>
</svg>
Сброс
</button>
</div>
</div>
<!-- canvas -->
<div class="proj-canvas-outer">
<canvas id="coll-canvas"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
<!-- stats bar -->
<div class="proj-stats-bar">
<div class="pstat">
<div class="pstat-label">Импульс до</div>
<div class="pstat-val" id="cs-pbefore"></div>
</div>
<div class="pstat">
<div class="pstat-label">Импульс после</div>
<div class="pstat-val" id="cs-pafter"></div>
</div>
<div class="pstat">
<div class="pstat-label">КЭ до</div>
<div class="pstat-val" id="cs-kebefore"></div>
</div>
<div class="pstat">
<div class="pstat-label">КЭ после</div>
<div class="pstat-val" id="cs-keafter"></div>
</div>
<div class="pstat">
<div class="pstat-label">Столкновений</div>
<div class="pstat-val" id="cs-count">0</div>
</div>
</div>
</div><!-- /#sim-coll -->
<!-- ── CRYSTAL sim body ── -->
<div id="sim-crystal" class="sim-body-wrap" style="display:none">
<div class="graph-panel">
<div class="gp-section-title">Тип решётки</div>
<button class="gp-btn crystal-type-btn active" id="crys-nacl" onclick="setCrystal('nacl',this)" style="margin-bottom:6px"><svg class="ic" viewBox="0 0 24 24"><path d="M3 18a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7H3z"/><path d="M3 7V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2"/><path d="M12 11v4M10 13h4"/></svg> NaCl (ионная)</button>
<button class="gp-btn crystal-type-btn" id="crys-diamond" onclick="setCrystal('diamond',this)" style="margin-bottom:6px"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle"><path d="M6 3l-3 7 9 11 9-11-3-7H6z"/><path d="M3 10h18"/><path d="M12 21L6 3"/><path d="M12 21l6-18"/></svg> Алмаз (ковалентная)</button>
<button class="gp-btn crystal-type-btn" id="crys-bcc" onclick="setCrystal('bcc',this)" style="margin-bottom:6px">ОЦК (металл)</button>
<button class="gp-btn crystal-type-btn" id="crys-fcc" onclick="setCrystal('fcc',this)" style="margin-bottom:6px">ГЦК (металл)</button>
<div class="gp-section-title" style="margin-top:12px">Управление</div>
<div class="tp-text" style="font-size:0.72rem">Вращение: зажмите и тяните<br>Зум: колёсико мыши</div>
</div>
<div class="graph-canvas-outer">
<div class="graph-canvas-wrap" id="crystal-container"></div>
</div>
</div>
<!-- ── ORBITALS sim body ── -->
<div id="sim-orbitals" class="sim-body-wrap" style="display:none">
<div class="graph-panel">
<div class="gp-section-title">Орбиталь</div>
<button class="gp-btn orbital-mode-btn active" id="orb-s" onclick="setOrbital('s',this)" style="margin-bottom:6px"<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#4CC9F0;margin-right:4px"></span>s-орбиталь</button>
<button class="gp-btn orbital-mode-btn" id="orb-p" onclick="setOrbital('p',this)" style="margin-bottom:6px"<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#06D6A0;margin-right:4px"></span>p-орбитали</button>
<button class="gp-btn orbital-mode-btn" id="orb-d" onclick="setOrbital('d',this)" style="margin-bottom:6px"<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#FFD166;margin-right:4px"></span>d-орбитали</button>
<div class="gp-section-title" style="margin-top:12px">Молекулы</div>
<button class="gp-btn orbital-mode-btn" id="orb-h2" onclick="setOrbital('h2',this)" style="margin-bottom:6px">H₂ (σ-связь)</button>
<button class="gp-btn orbital-mode-btn" id="orb-h2o" onclick="setOrbital('h2o',this)" style="margin-bottom:6px">H₂O (угловая)</button>
<div class="gp-section-title" style="margin-top:12px">Управление</div>
<div class="tp-text" style="font-size:0.72rem">Вращение: зажмите и тяните<br>Зум: колёсико мыши</div>
</div>
<div class="graph-canvas-outer">
<div class="graph-canvas-wrap" id="orbitals-container"></div>
</div>
</div>
<!-- ── CHEM SANDBOX sim body ── -->
<div id="sim-chemsandbox" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<!-- Category filter -->
<div class="gp-section-title" style="margin-bottom:6px">Реагенты</div>
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:10px">
<button class="proj-preset-chip reac-mode-btn chemsand-cat active" onclick="chemSandCat('all',this)">Все</button>
<button class="proj-preset-chip reac-mode-btn chemsand-cat" onclick="chemSandCat('acid',this)">Кислоты</button>
<button class="proj-preset-chip reac-mode-btn chemsand-cat" onclick="chemSandCat('base',this)">Основания</button>
<button class="proj-preset-chip reac-mode-btn chemsand-cat" onclick="chemSandCat('salt',this)">Соли</button>
<button class="proj-preset-chip reac-mode-btn chemsand-cat" onclick="chemSandCat('metal',this)">Металлы</button>
<button class="proj-preset-chip reac-mode-btn chemsand-cat" onclick="chemSandCat('indicator',this)">Индикаторы</button>
<button class="proj-preset-chip reac-mode-btn chemsand-cat" onclick="chemSandCat('other',this)">Другое</button>
</div>
<!-- Reagent list -->
<div id="chemsand-reagents" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;max-height:180px;overflow-y:auto"></div>
<!-- Presets -->
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="proj-preset-chip" onclick="chemSandPreset('neutralization')">Нейтрализация</button>
<button class="proj-preset-chip" onclick="chemSandPreset('gas_evolution')">Газ CO₂</button>
<button class="proj-preset-chip" onclick="chemSandPreset('precipitate')">Осадок AgCl</button>
<button class="proj-preset-chip" onclick="chemSandPreset('displacement')">Замещение Cu</button>
<button class="proj-preset-chip" onclick="chemSandPreset('indicator')">Индикатор</button>
<button class="proj-preset-chip" onclick="chemSandPreset('violent')">Na + H₂O</button>
<button class="proj-preset-chip" onclick="chemSandPreset('yellow_precip')">PbCrO₄<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
<button class="proj-preset-chip" onclick="chemSandPreset('blue_precip')">Cu(OH)₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></button>
</div>
<!-- Sliders -->
<div class="gp-section-title" style="margin-bottom:4px;margin-top:4px">Условия</div>
<div class="param-block" style="margin-bottom:5px">
<div class="param-header"><span class="param-name" style="font-size:.7rem">Концентрация</span><span class="param-val" id="csand-conc-val" style="font-size:.7rem;min-width:40px">35%</span></div>
<input type="range" class="param-slider" id="sl-csand-conc" min="5" max="95" value="35" oninput="chemSandConcChange()">
</div>
<div class="param-block" style="margin-bottom:5px">
<div class="param-header"><span class="param-name" style="font-size:.7rem">Температура</span><span class="param-val" id="csand-temp-val" style="font-size:.7rem;min-width:40px">20°C</span></div>
<input type="range" class="param-slider" id="sl-csand-temp" min="0" max="100" value="20" oninput="chemSandTempChange()">
</div>
<!-- Quiz mode -->
<div class="pp-group" style="margin-top:8px">
<div class="pp-label" style="font-size:.65rem">Режим</div>
<div style="display:flex;gap:6px;align-items:center">
<button class="proj-preset-chip reac-mode-btn chemsand-mode active" id="csand-mode-free" onclick="chemSandSetMode('free',this)">Свободный</button>
<button class="proj-preset-chip reac-mode-btn chemsand-mode" id="csand-mode-quiz" onclick="chemSandSetMode('quiz',this)">Задания</button>
<span id="csand-quiz-score" style="font-size:.7rem;color:rgba(255,255,255,.4);margin-left:8px"></span>
</div>
<div id="csand-quiz-question" style="display:none;margin-top:6px;padding:6px 10px;border-radius:8px;background:rgba(155,93,229,.10);border:1px solid rgba(155,93,229,.25);font-size:.72rem;color:#C9A0FF;line-height:1.3"></div>
<div id="csand-quiz-result" style="display:none;margin-top:4px;font-size:.72rem;font-weight:700"></div>
<button id="csand-quiz-next" style="display:none;margin-top:4px" class="proj-preset-chip" onclick="chemSandQuizNext()">Следующее задание</button>
</div>
<!-- Hint -->
<div class="tp-text" style="font-size:.65rem;opacity:.4;line-height:1.3">Клик на панели — добавить/убрать · Drag с полки в колбу · ПКМ — сбросить</div>
</div>
<div class="proj-canvas-outer">
<canvas id="chemsandbox-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:default"></canvas>
</div>
</div>
<!-- Stats bar -->
<div class="proj-stats-bar" id="chemsand-stats-bar">
<div class="pstat"><div class="pstat-label">В зоне</div><div class="pstat-val" id="csbar-v1" style="color:#06D6E0">0</div></div>
<div class="pstat"><div class="pstat-label">Тип</div><div class="pstat-val" id="csbar-v3" style="color:#7BF5A4"></div></div>
<div class="pstat"><div class="pstat-label">Уравнение</div><div class="pstat-val" id="csbar-v4" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">Сокр. ионное</div><div class="pstat-val" id="csbar-v6" style="color:#9BD4FF"></div></div>
<div class="pstat"><div class="pstat-label">Продукты</div><div class="pstat-val" id="csbar-v5" style="color:#4CC9F0"></div></div>
</div>
</div>
<!-- ── CELL DIVISION sim body ── -->
<div id="sim-celldivision" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:200px;gap:0">
<div class="gp-section-title" style="margin-bottom:6px">Режим</div>
<div style="display:flex;gap:4px;margin-bottom:12px">
<button class="proj-preset-chip reac-mode-btn cd-mode-btn active" onclick="cdSetMode('mitosis',this)">Митоз</button>
<button class="proj-preset-chip reac-mode-btn cd-mode-btn" onclick="cdSetMode('meiosis',this)">Мейоз</button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Фазы</div>
<div id="cd-phase-dots" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px"></div>
<div style="display:flex;gap:4px;margin-bottom:12px">
<button class="gp-btn" onclick="cdPrevPhase()" style="flex:1"><svg class="ic" viewBox="0 0 24 24"><polygon points="19 20 9 12 19 4 19 20"/></svg> Назад</button>
<button class="gp-btn" onclick="cdNextPhase()" style="flex:1">Далее <svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>
</div>
<button class="gp-btn" id="cd-auto-btn" onclick="cdAutoPlay(this)" style="width:100%;margin-bottom:10px"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Авто</button>
<div class="pp-hint">Нажми фазу или используй кнопки для пошагового просмотра</div>
</div>
<div class="proj-canvas-outer">
<canvas id="celldiv-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="cdbar">
<div class="pstat"><div class="pstat-label">Фаза</div><div class="pstat-val" id="cdbar-v1" style="color:#7BF5A4"></div></div>
<div class="pstat"><div class="pstat-label">Хромосомы</div><div class="pstat-val" id="cdbar-v2" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">ДНК</div><div class="pstat-val" id="cdbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Шаг</div><div class="pstat-val" id="cdbar-v4" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">Режим</div><div class="pstat-val" id="cdbar-v5" style="color:#9B5DE5"></div></div>
</div>
</div>
<!-- ── PHOTOSYNTHESIS sim body ── -->
<div id="sim-photosynthesis" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:200px;gap:0">
<div class="gp-section-title" style="margin-bottom:6px">Процесс</div>
<div style="display:flex;gap:4px;margin-bottom:12px">
<button class="proj-preset-chip reac-mode-btn ps-mode-btn active" onclick="psSetMode('photo',this)">Фотосинтез</button>
<button class="proj-preset-chip reac-mode-btn ps-mode-btn" onclick="psSetMode('resp',this)">Дыхание</button>
</div>
<div class="gp-section-title" style="margin-bottom:4px">Интенсивность света</div>
<div class="sl-row" style="margin-bottom:10px">
<input type="range" id="sl-ps-light" min="0" max="100" value="70" oninput="psLightChange()" style="flex:1">
<span class="sl-val" id="ps-light-val">70%</span>
</div>
<div class="gp-section-title" style="margin-bottom:4px">Концентрация CO₂</div>
<div class="sl-row" style="margin-bottom:10px">
<input type="range" id="sl-ps-co2" min="0" max="100" value="50" oninput="psCO2Change()" style="flex:1">
<span class="sl-val" id="ps-co2-val">50%</span>
</div>
<button class="gp-btn" onclick="psReset()" style="width:100%;margin-top:4px"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сброс</button>
<div class="pp-hint" style="margin-top:10px">Меняй параметры и наблюдай за скоростью реакций</div>
</div>
<div class="proj-canvas-outer">
<canvas id="photosyn-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="psbar">
<div class="pstat"><div class="pstat-label">АТФ/с</div><div class="pstat-val" id="psbar-v1" style="color:#FFD166">0</div></div>
<div class="pstat"><div class="pstat-label">O₂ выд.</div><div class="pstat-val" id="psbar-v2" style="color:#7BF5A4">0</div></div>
<div class="pstat"><div class="pstat-label">CO₂ усв.</div><div class="pstat-val" id="psbar-v3" style="color:#06D6E0">0</div></div>
<div class="pstat"><div class="pstat-label">КПД</div><div class="pstat-val" id="psbar-v4" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">Режим</div><div class="pstat-val" id="psbar-v5" style="color:#9B5DE5"></div></div>
</div>
</div>
<!-- ── ANGRY BIRDS sim body ── -->
<div id="sim-angrybirds" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:200px;gap:0">
<div class="gp-section-title" style="margin-bottom:6px">Уровень</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px" id="ab-level-btns">
<button class="proj-preset-chip reac-mode-btn ab-lvl-btn active" onclick="abLevel(0,this)">1</button>
<button class="proj-preset-chip reac-mode-btn ab-lvl-btn" onclick="abLevel(1,this)">2</button>
<button class="proj-preset-chip reac-mode-btn ab-lvl-btn" onclick="abLevel(2,this)">3</button>
<button class="proj-preset-chip reac-mode-btn ab-lvl-btn" onclick="abLevel(3,this)">4</button>
<button class="proj-preset-chip reac-mode-btn ab-lvl-btn" onclick="abLevel(4,this)">5</button>
<button class="proj-preset-chip reac-mode-btn ab-lvl-btn" onclick="abLevel(5,this)">6</button>
</div>
<button onclick="angryBirdsRestart()" style="width:100%;padding:6px 10px;border-radius:8px;border:1.5px solid rgba(255,255,255,.15);background:rgba(255,255,255,.06);color:#e0e0e0;font-size:.76rem;font-weight:700;cursor:pointer;margin-bottom:12px" title="Начать уровень заново"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сначала</button>
<div class="gp-section-title" style="margin-bottom:6px">Планеты</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:14px">
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="font-size:.75rem;color:#e0e0e0"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> Земля</span><span style="font-size:.72rem;color:#06D6E0;font-weight:700">9.81 м/с²</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="font-size:.75rem;color:#e0e0e0"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="currentColor" stroke="none"/></svg> Луна</span><span style="font-size:.72rem;color:#06D6E0;font-weight:700">1.62 м/с²</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="font-size:.75rem;color:#e0e0e0"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="currentColor" stroke="none"/></svg> Марс</span><span style="font-size:.72rem;color:#06D6E0;font-weight:700">3.71 м/с²</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="font-size:.75rem;color:#e0e0e0"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="currentColor" stroke="none"/></svg> Юпитер</span><span style="font-size:.72rem;color:#06D6E0;font-weight:700">24.8 м/с²</span>
</div>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Птицы</div>
<div style="display:flex;flex-direction:column;gap:5px;margin-bottom:14px">
<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="width:14px;height:14px;border-radius:50%;background:#e63946;flex-shrink:0;display:inline-block"></span>
<span style="font-size:.73rem;color:#e0e0e0">Красная — обычная</span>
</div>
<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="width:14px;height:14px;border-radius:50%;background:#888;flex-shrink:0;display:inline-block"></span>
<span style="font-size:.73rem;color:#e0e0e0">Тяжёлая — высокий урон</span>
</div>
<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:7px;background:rgba(255,255,255,.06)">
<span style="width:14px;height:14px;border-radius:50%;background:#ffd166;flex-shrink:0;display:inline-block"></span>
<span style="font-size:.73rem;color:#e0e0e0">Жёлтая — быстрая</span>
</div>
</div>
<div class="pp-hint">Тяни птицу мышью · отпусти — выстрел</div>
</div>
<div class="proj-canvas-outer">
<canvas id="angrybirds-canvas" style="display:block;position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="abbar">
<div class="pstat"><div class="pstat-label">Уровень</div><div class="pstat-val" id="abbar-v1" style="color:#06D6E0">1</div></div>
<div class="pstat"><div class="pstat-label">Птиц</div><div class="pstat-val" id="abbar-v2" style="color:#ffd166">3</div></div>
<div class="pstat"><div class="pstat-label">Свиней</div><div class="pstat-val" id="abbar-v3" style="color:#7bf5a4">1</div></div>
<div class="pstat"><div class="pstat-label">Очки</div><div class="pstat-val" id="abbar-v4" style="color:#ef476f">0</div></div>
<div class="pstat"><div class="pstat-label">Планета</div><div class="pstat-val" id="abbar-v5" style="color:#9b5de5">Земля <svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div></div>
</div>
</div>
<!-- ── QUADRATIC sim body ── -->
<div id="sim-quadratic" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Коэффициенты</div>
<div class="proj-slider-row" style="margin-bottom:10px">
<label style="font-size:.78rem;color:#ccc;width:60px">a = <span id="quad-a-val" style="color:#9B5DE5;font-weight:700">1</span></label>
<input type="range" id="sl-quad-a" min="-5" max="5" step="0.1" value="1" oninput="quadParam('a',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:10px">
<label style="font-size:.78rem;color:#ccc;width:60px">b = <span id="quad-b-val" style="color:#06D6E0;font-weight:700">0</span></label>
<input type="range" id="sl-quad-b" min="-10" max="10" step="0.1" value="0" oninput="quadParam('b',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:10px">
<label style="font-size:.78rem;color:#ccc;width:60px">c = <span id="quad-c-val" style="color:#F15BB5;font-weight:700">-1</span></label>
<input type="range" id="sl-quad-c" min="-10" max="10" step="0.1" value="-1" oninput="quadParam('c',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Примеры</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn" onclick="quadPreset(1,0,-4)">x²−4</button>
<button class="preset-btn" onclick="quadPreset(1,-2,1)">(x1)²</button>
<button class="preset-btn" onclick="quadPreset(1,0,1)">x²+1</button>
<button class="preset-btn" onclick="quadPreset(-1,0,4)">x²+4</button>
<button class="preset-btn" onclick="quadPreset(2,-3,-2)">2x²−3x2</button>
<button class="preset-btn" onclick="quadPreset(0.5,1,-3)">½x²+x3</button>
</div>
<div class="pp-hint">Скролл — зум · Тащи — панорама</div>
</div>
<div class="proj-canvas-outer">
<canvas id="quadratic-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="quadbar">
<div class="pstat"><div class="pstat-label">Дискриминант</div><div class="pstat-val" id="qbar-v1" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Корни</div><div class="pstat-val" id="qbar-v2" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">Вершина</div><div class="pstat-val" id="qbar-v3" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">Уравнение</div><div class="pstat-val" id="qbar-v4" style="color:#9B5DE5"></div></div>
</div>
</div>
<!-- ── NORMAL DISTRIBUTION sim body ── -->
<div id="sim-normaldist" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:10px">
<label style="font-size:.78rem;color:#ccc;width:60px">μ = <span id="nd-mu-val" style="color:#06D6E0;font-weight:700">0</span></label>
<input type="range" id="sl-nd-mu" min="-5" max="5" step="0.1" value="0" oninput="ndParam('mu',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:10px">
<label style="font-size:.78rem;color:#ccc;width:60px">σ = <span id="nd-sigma-val" style="color:#9B5DE5;font-weight:700">1</span></label>
<input type="range" id="sl-nd-sigma" min="0.2" max="4" step="0.1" value="1" oninput="ndParam('sigma',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Закрасить область</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn nd-shade-btn active" onclick="ndShade('1s',this)">μ ± 1σ</button>
<button class="preset-btn nd-shade-btn" onclick="ndShade('2s',this)">μ ± 2σ</button>
<button class="preset-btn nd-shade-btn" onclick="ndShade('3s',this)">μ ± 3σ</button>
<button class="preset-btn nd-shade-btn" onclick="ndShade('none',this)">Нет</button>
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn" onclick="ndPreset(0,1)">Стандартное</button>
<button class="preset-btn" onclick="ndPreset(0,0.5)">Узкое σ=0.5</button>
<button class="preset-btn" onclick="ndPreset(0,2)">Широкое σ=2</button>
<button class="preset-btn" onclick="ndPreset(3,1)">Сдвиг μ=3</button>
</div>
<div class="pp-hint">Наведи курсор — Z-score и плотность</div>
</div>
<div class="proj-canvas-outer">
<canvas id="normaldist-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="ndbar">
<div class="pstat"><div class="pstat-label">μ</div><div class="pstat-val" id="ndbar-v1" style="color:#06D6E0">0</div></div>
<div class="pstat"><div class="pstat-label">σ</div><div class="pstat-val" id="ndbar-v2" style="color:#9B5DE5">1</div></div>
<div class="pstat"><div class="pstat-label">Пик f(μ)</div><div class="pstat-val" id="ndbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Область</div><div class="pstat-val" id="ndbar-v4" style="color:#7BF5A4"></div></div>
</div>
</div>
<!-- ── GRAPH TRANSFORM sim body ── -->
<div id="sim-graphtransform" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:230px;gap:0">
<div class="gp-section-title" style="margin-bottom:6px">Базовая функция f(x)</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px">
<button class="preset-btn gt-base-btn active" onclick="gtBase('sin',this)">sin x</button>
<button class="preset-btn gt-base-btn" onclick="gtBase('cos',this)">cos x</button>
<button class="preset-btn gt-base-btn" onclick="gtBase('x^2',this)"></button>
<button class="preset-btn gt-base-btn" onclick="gtBase('x^3',this)"></button>
<button class="preset-btn gt-base-btn" onclick="gtBase('sqrt',this)">√x</button>
<button class="preset-btn gt-base-btn" onclick="gtBase('|x|',this)">|x|</button>
<button class="preset-btn gt-base-btn" onclick="gtBase('1/x',this)">1/x</button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">y = a · f(k·x + b) + c</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">a = <span id="gt-a-val" style="color:#9B5DE5;font-weight:700">1</span></label>
<input type="range" id="sl-gt-a" min="-3" max="3" step="0.1" value="1" oninput="gtParam('a',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">k = <span id="gt-k-val" style="color:#06D6E0;font-weight:700">1</span></label>
<input type="range" id="sl-gt-k" min="-3" max="3" step="0.1" value="1" oninput="gtParam('k',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">b = <span id="gt-b-val" style="color:#FFD166;font-weight:700">0</span></label>
<input type="range" id="sl-gt-b" min="-5" max="5" step="0.1" value="0" oninput="gtParam('b',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">c = <span id="gt-c-val" style="color:#EF476F;font-weight:700">0</span></label>
<input type="range" id="sl-gt-c" min="-5" max="5" step="0.1" value="0" oninput="gtParam('c',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Эффекты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="gtEffect(2,1,0,0)">Растяжение <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="21" x2="12" y2="3"/><polyline points="7 8 12 3 17 8"/><polyline points="17 16 12 21 7 16"/></svg></button>
<button class="preset-btn" onclick="gtEffect(1,2,0,0)">Сжатие <svg class="ic" viewBox="0 0 24 24"><line x1="3" y1="12" x2="21" y2="12"/><polyline points="8 17 3 12 8 7"/><polyline points="16 7 21 12 16 17"/></svg></button>
<button class="preset-btn" onclick="gtEffect(-1,1,0,0)">Отражение <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="21" x2="12" y2="3"/><polyline points="7 8 12 3 17 8"/><polyline points="17 16 12 21 7 16"/></svg></button>
<button class="preset-btn" onclick="gtEffect(1,-1,0,0)">Отражение <svg class="ic" viewBox="0 0 24 24"><line x1="3" y1="12" x2="21" y2="12"/><polyline points="8 17 3 12 8 7"/><polyline points="16 7 21 12 16 17"/></svg></button>
<button class="preset-btn" onclick="gtEffect(1,1,2,0)">Сдвиг <svg class="ic" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg></button>
<button class="preset-btn" onclick="gtEffect(1,1,0,3)">Сдвиг <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></button>
</div>
<button class="preset-btn" onclick="gtEffect(1,1,0,0)" style="width:100%;margin-top:4px">Сброс</button>
<div class="pp-hint">Скролл — зум · Тащи — панорама</div>
</div>
<div class="proj-canvas-outer">
<canvas id="graphtransform-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="gtbar">
<div class="pstat"><div class="pstat-label">f(x)</div><div class="pstat-val" id="gtbar-v1" style="color:#06D6E0">sin(x)</div></div>
<div class="pstat"><div class="pstat-label">a</div><div class="pstat-val" id="gtbar-v2" style="color:#9B5DE5">1</div></div>
<div class="pstat"><div class="pstat-label">k</div><div class="pstat-val" id="gtbar-v3" style="color:#06D6E0">1</div></div>
<div class="pstat"><div class="pstat-label">b</div><div class="pstat-val" id="gtbar-v4" style="color:#FFD166">0</div></div>
<div class="pstat"><div class="pstat-label">c</div><div class="pstat-val" id="gtbar-v5" style="color:#EF476F">0</div></div>
</div>
</div>
<!-- ── PENDULUM sim body ── -->
<div id="sim-pendulum" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">θ = <span id="pend-theta-val" style="color:#9B5DE5;font-weight:700">45</span>°</label>
<input type="range" id="sl-pend-theta" min="5" max="170" step="1" value="45" oninput="pendParam('theta',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">L = <span id="pend-L-val" style="color:#06D6E0;font-weight:700">200</span></label>
<input type="range" id="sl-pend-L" min="60" max="300" step="5" value="200" oninput="pendParam('L',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">g = <span id="pend-g-val" style="color:#FFD166;font-weight:700">9.81</span></label>
<input type="range" id="sl-pend-g" min="1" max="25" step="0.1" value="9.81" oninput="pendParam('g',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:70px">Затух. <span id="pend-damp-val" style="color:#EF476F;font-weight:700">0</span></label>
<input type="range" id="sl-pend-damp" min="0" max="2" step="0.05" value="0" oninput="pendParam('damping',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="pendPreset(45,200,9.81,0)">Земля</button>
<button class="preset-btn" onclick="pendPreset(45,200,1.62,0)">Луна</button>
<button class="preset-btn" onclick="pendPreset(170,200,9.81,0)">Большой θ</button>
<button class="preset-btn" onclick="pendPreset(45,200,9.81,0.5)">Затухание</button>
</div>
<div class="pp-hint">Тащи грузик мышью для установки угла</div>
</div>
<div class="proj-canvas-outer">
<canvas id="pendulum-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="pendbar">
<div class="pstat"><div class="pstat-label">Угол</div><div class="pstat-val" id="pendbar-v1" style="color:#9B5DE5">45°</div></div>
<div class="pstat"><div class="pstat-label">ω</div><div class="pstat-val" id="pendbar-v2" style="color:#06D6E0">0</div></div>
<div class="pstat"><div class="pstat-label">Период T</div><div class="pstat-val" id="pendbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Энергия</div><div class="pstat-val" id="pendbar-v4" style="color:#EF476F"></div></div>
</div>
</div>
<!-- ── EQUILIBRIUM sim body ── -->
<div id="sim-equilibrium" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">T = <span id="eq-T-val" style="color:#FFD166;font-weight:700">300</span> K</label>
<input type="range" id="sl-eq-T" min="200" max="500" step="10" value="300" oninput="eqParam('T',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">Ea<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> <span id="eq-Eaf-val" style="color:#7BF5A4;font-weight:700">50</span></label>
<input type="range" id="sl-eq-Eaf" min="20" max="80" step="1" value="50" oninput="eqParam('Ea_f',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">Ea<svg class="ic" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> <span id="eq-Ear-val" style="color:#EF476F;font-weight:700">55</span></label>
<input type="range" id="sl-eq-Ear" min="20" max="80" step="1" value="55" oninput="eqParam('Ea_r',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="eqPreset('default')">По умолч.</button>
<button class="preset-btn" onclick="eqPreset('exothermic')">Экзо</button>
<button class="preset-btn" onclick="eqPreset('endothermic')">Эндо</button>
<button class="preset-btn" onclick="eqPreset('excess_A')">Избыток A</button>
</div>
<div class="pp-hint">A + B ⇌ C + D — принцип Ле Шателье</div>
</div>
<div class="proj-canvas-outer">
<canvas id="equilibrium-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="eqbar">
<div class="pstat"><div class="pstat-label">Keq</div><div class="pstat-val" id="eqbar-v1" style="color:#7BF5A4"></div></div>
<div class="pstat"><div class="pstat-label">Q</div><div class="pstat-val" id="eqbar-v2" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Направление</div><div class="pstat-val" id="eqbar-v3" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">A|B|C|D</div><div class="pstat-val" id="eqbar-v4" style="color:#9B5DE5"></div></div>
</div>
</div>
<!-- ── THIN LENS sim body ── -->
<div id="sim-thinlens" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">f = <span id="lens-f-val" style="color:#06D6E0;font-weight:700">100</span></label>
<input type="range" id="sl-lens-f" min="-200" max="200" step="5" value="100" oninput="lensParam('f',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">d = <span id="lens-d-val" style="color:#9B5DE5;font-weight:700">200</span></label>
<input type="range" id="sl-lens-d" min="30" max="400" step="5" value="200" oninput="lensParam('d',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">h = <span id="lens-h-val" style="color:#EF476F;font-weight:700">50</span></label>
<input type="range" id="sl-lens-h" min="20" max="80" step="2" value="50" oninput="lensParam('h',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="lensPreset(100,200,50)">Собирающая</button>
<button class="preset-btn" onclick="lensPreset(-100,200,50)">Рассеивающая</button>
<button class="preset-btn" onclick="lensPreset(100,100,50)">d = f</button>
<button class="preset-btn" onclick="lensPreset(100,60,50)">d &lt; f</button>
</div>
<div class="pp-hint">Тащи стрелку-предмет или фокус мышью</div>
</div>
<div class="proj-canvas-outer">
<canvas id="thinlens-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="lensbar">
<div class="pstat"><div class="pstat-label">f</div><div class="pstat-val" id="lensbar-v1" style="color:#06D6E0">100</div></div>
<div class="pstat"><div class="pstat-label">d'</div><div class="pstat-val" id="lensbar-v2" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">M</div><div class="pstat-val" id="lensbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Тип</div><div class="pstat-val" id="lensbar-v4" style="color:#9B5DE5"></div></div>
</div>
</div>
<!-- ── MIRRORS sim body ── -->
<div id="sim-mirrors" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:264px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Тип зеркала</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn mirror-type-btn" id="mtype-flat" onclick="mirrorType('flat',this)" style="font-size:.72rem">Плоское</button>
<button class="preset-btn mirror-type-btn active" id="mtype-concave" onclick="mirrorType('concave',this)" style="font-size:.72rem">Вогнутое</button>
<button class="preset-btn mirror-type-btn" id="mtype-convex" onclick="mirrorType('convex',this)" style="font-size:.72rem">Выпуклое</button>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px" id="mirror-f-row">
<label style="font-size:.78rem;color:#ccc;width:60px">f = <span id="mirror-f-val" style="color:#06D6E0;font-weight:700">120</span></label>
<input type="range" id="sl-mirror-f" min="30" max="300" step="5" value="120" oninput="mirrorParam('f',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:60px">d = <span id="mirror-d-val" style="color:#9B5DE5;font-weight:700">240</span></label>
<input type="range" id="sl-mirror-d" min="30" max="500" step="5" value="240" oninput="mirrorParam('d',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:60px">h = <span id="mirror-h-val" style="color:#EF476F;font-weight:700">60</span></label>
<input type="range" id="sl-mirror-h" min="20" max="80" step="2" value="60" oninput="mirrorParam('h',this.value)" style="flex:1">
</div>
<div style="display:flex;gap:6px;margin-top:8px;margin-bottom:8px;align-items:center">
<button id="mirror-play-btn" onclick="mirrorTogglePlay(this)" style="flex:1;padding:6px 0;border-radius:8px;border:none;background:linear-gradient(135deg,#06D6E0,#9B5DE5);color:#fff;font-size:.78rem;font-weight:700;cursor:pointer">▶ Анимация</button>
<div style="display:flex;flex-direction:column;align-items:center;gap:2px">
<span style="font-size:.62rem;color:#888">скорость</span>
<select id="mirror-speed-sel" onchange="mirrorSetSpeed(this.value)" style="background:#1a1a2e;color:#ccc;border:1px solid #333;border-radius:4px;font-size:.7rem;padding:2px 4px">
<option value="0.25">×¼</option><option value="0.5">×½</option>
<option value="1" selected>×1</option><option value="2">×2</option>
</select>
</div>
</div>
<div style="display:flex;gap:4px;margin-bottom:10px">
<button onclick="mirrorStepNext()" style="flex:1;padding:5px 0;border-radius:6px;border:1px solid #333;background:#1a1a2e;color:#7BF5A4;font-size:.73rem;cursor:pointer" title="Показать следующий луч">① Пошагово</button>
<button onclick="mirrorStepReset()" style="padding:5px 9px;border-radius:6px;border:1px solid #333;background:#1a1a2e;color:#888;font-size:.78rem;cursor:pointer" title="Показать все лучи"></button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Отображение</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;margin-bottom:10px">
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer"><input type="checkbox" id="mtog-normals" checked onchange="mirrorToggle('normals',this.checked)"> Нормали</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer"><input type="checkbox" id="mtog-dims" checked onchange="mirrorToggle('dims',this.checked)"> Размеры</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer"><input type="checkbox" id="mtog-angles" checked onchange="mirrorToggle('angles',this.checked)"> Углы θ</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer"><input type="checkbox" id="mtog-photons" checked onchange="mirrorToggle('photons',this.checked)"> Фотоны</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer"><input type="checkbox" id="mtog-grid" onchange="mirrorToggle('grid',this.checked)"> Сетка</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer"><input type="checkbox" id="mtog-zones" checked onchange="mirrorToggle('zones',this.checked)"> Зоны</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#ccc;cursor:pointer;grid-column:span 2"><input type="checkbox" id="mtog-point" onchange="mirrorSetPointMode(this.checked)"> Точечный объект</label>
</div>
<button onclick="if(mirrorSim)mirrorSim.exportPng()" style="width:100%;padding:5px 0;border-radius:6px;border:1px solid #333;background:#1a1a2e;color:#888;font-size:.72rem;cursor:pointer;margin-bottom:8px">📷 Экспорт PNG</button>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="mirrorPreset('flat')">Плоское</button>
<button class="preset-btn" onclick="mirrorPreset('far')">d &gt; 2f</button>
<button class="preset-btn" onclick="mirrorPreset('2f')">d = 2f</button>
<button class="preset-btn" onclick="mirrorPreset('between')">f &lt; d &lt; 2f</button>
<button class="preset-btn" onclick="mirrorPreset('near')">d &lt; f</button>
<button class="preset-btn" onclick="mirrorPreset('convex')">Выпуклое</button>
</div>
<div class="pp-hint">Тащи предмет, фокус или изображение мышью</div>
</div>
<div class="proj-canvas-outer">
<canvas id="mirror-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="mirrorbar">
<div class="pstat"><div class="pstat-label">f</div><div class="pstat-val" id="mirrorbar-v1" style="color:#06D6E0">120</div></div>
<div class="pstat"><div class="pstat-label">d</div><div class="pstat-val" id="mirrorbar-v5" style="color:#9B5DE5">240</div></div>
<div class="pstat"><div class="pstat-label">d'</div><div class="pstat-val" id="mirrorbar-v2" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">M</div><div class="pstat-val" id="mirrorbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Тип изобр.</div><div class="pstat-val" id="mirrorbar-v4" style="color:#9B5DE5"></div></div>
</div>
</div>
<!-- ── ISOPROCESS sim body ── -->
<div id="sim-isoprocess" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Процесс</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn iso-proc-btn active" id="iproc-isothermal" onclick="isoProc('isothermal',this)" style="font-size:.72rem">Изотерма</button>
<button class="preset-btn iso-proc-btn" id="iproc-isochoric" onclick="isoProc('isochoric',this)" style="font-size:.72rem">Изохора</button>
<button class="preset-btn iso-proc-btn" id="iproc-isobaric" onclick="isoProc('isobaric',this)" style="font-size:.72rem">Изобара</button>
<button class="preset-btn iso-proc-btn" id="iproc-adiabatic" onclick="isoProc('adiabatic',this)" style="font-size:.72rem">Адиабата</button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Газ γ</div>
<div style="display:flex;gap:4px;margin-bottom:10px">
<button class="preset-btn iso-gamma-btn" id="igamma-14" onclick="isoGamma(1.4,this)" style="font-size:.72rem">Двухат. 1.4</button>
<button class="preset-btn iso-gamma-btn active" id="igamma-167" onclick="isoGamma(1.667,this)" style="font-size:.72rem">Одноат. 5/3</button>
</div>
<div class="gp-section-title" style="margin-bottom:8px">Начальное состояние</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:70px">P₁ = <span id="iso-p1-val" style="color:#9B5DE5;font-weight:700">3.0</span></label>
<input type="range" id="sl-iso-p1" min="0.5" max="8" step="0.1" value="3.0" oninput="isoParam('P1',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:70px">V₁ = <span id="iso-v1-val" style="color:#9B5DE5;font-weight:700">10</span></label>
<input type="range" id="sl-iso-v1" min="2" max="28" step="1" value="10" oninput="isoParam('V1',this.value)" style="flex:1">
</div>
<div class="gp-section-title" style="margin-bottom:8px">Конечное состояние</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:70px;white-space:nowrap">Сжат./расш.</label>
<input type="range" id="sl-iso-ratio" min="0.01" max="0.99" step="0.01" value="0.5" oninput="isoRatio(this.value)" style="flex:1">
</div>
<div style="margin-top:6px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="isoPreset('iso_expand')">Изотерма расш.</button>
<button class="preset-btn" onclick="isoPreset('iso_comp')">Изотерма сжат.</button>
<button class="preset-btn" onclick="isoPreset('heat_iso')">Изохора нагрев</button>
<button class="preset-btn" onclick="isoPreset('adiab_exp')">Адиабата расш.</button>
</div>
<div class="pp-hint">Тащи точки 1 и 2 по диаграмме</div>
</div>
<div class="proj-canvas-outer">
<canvas id="isoprocess-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="isobar">
<div class="pstat"><div class="pstat-label">T₁, K</div><div class="pstat-val" id="isobar-t1" style="color:#9B5DE5"></div></div>
<div class="pstat"><div class="pstat-label">T₂, K</div><div class="pstat-val" id="isobar-t2" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">W, Дж</div><div class="pstat-val" id="isobar-w" style="color:#7BF5A4"></div></div>
<div class="pstat"><div class="pstat-label">Q, Дж</div><div class="pstat-val" id="isobar-q" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">ΔU, Дж</div><div class="pstat-val" id="isobar-du" style="color:#EF476F"></div></div>
</div>
</div>
<!-- ── TITRATION sim body ── -->
<div id="sim-titration" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:75px">C(кисл) <span id="titr-ac-val" style="color:#EF476F;font-weight:700">0.10</span> М</label>
<input type="range" id="sl-titr-ac" min="0.05" max="1" step="0.05" value="0.1" oninput="titrParam('acidConc',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:75px">C(осн) <span id="titr-bc-val" style="color:#9B5DE5;font-weight:700">0.10</span> М</label>
<input type="range" id="sl-titr-bc" min="0.05" max="1" step="0.05" value="0.1" oninput="titrParam('baseConc',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:75px">V(кисл) <span id="titr-vol-val" style="color:#06D6E0;font-weight:700">50</span> мл</label>
<input type="range" id="sl-titr-vol" min="25" max="100" step="5" value="50" oninput="titrParam('acidVol',this.value)" style="flex:1">
</div>
<div style="margin-top:4px;margin-bottom:8px">
<div class="gp-section-title" style="margin-bottom:4px">Индикатор</div>
<div style="display:flex;flex-wrap:wrap;gap:4px">
<button class="preset-btn titr-ind-btn active" onclick="titrIndicator('phenolphthalein',this)" style="font-size:.72rem">Фенолф.</button>
<button class="preset-btn titr-ind-btn" onclick="titrIndicator('methyl_orange',this)" style="font-size:.72rem">Метилор.</button>
<button class="preset-btn titr-ind-btn" onclick="titrIndicator('litmus',this)" style="font-size:.72rem">Лакмус</button>
</div>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="titrPreset('strong_strong')">HCl+NaOH</button>
<button class="preset-btn" onclick="titrPreset('weak_strong')">CH₃COOH</button>
<button class="preset-btn" onclick="titrPreset('concentrated')">Конц.</button>
</div>
<div class="pp-hint">Нажми <svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> — основание добавляется каплями</div>
</div>
<div class="proj-canvas-outer">
<canvas id="titration-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="titrbar">
<div class="pstat"><div class="pstat-label">pH</div><div class="pstat-val" id="titrbar-v1" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">Добавлено</div><div class="pstat-val" id="titrbar-v2" style="color:#9B5DE5">0.0 мл</div></div>
<div class="pstat"><div class="pstat-label">Точка экв.</div><div class="pstat-val" id="titrbar-v3" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">Тип</div><div class="pstat-val" id="titrbar-v4" style="color:#FFD166"></div></div>
</div>
</div>
<!-- ── REFRACTION sim body ── -->
<div id="sim-refraction" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">n₁ = <span id="refr-n1-val" style="color:#9B5DE5;font-weight:700">1.00</span></label>
<input type="range" id="sl-refr-n1" min="1" max="2.5" step="0.01" value="1" oninput="refrParam('n1',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">n₂ = <span id="refr-n2-val" style="color:#06D6E0;font-weight:700">1.50</span></label>
<input type="range" id="sl-refr-n2" min="1" max="2.5" step="0.01" value="1.5" oninput="refrParam('n2',this.value)" style="flex:1">
</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">θ = <span id="refr-angle-val" style="color:#FFD166;font-weight:700">30</span>°</label>
<input type="range" id="sl-refr-angle" min="0" max="89" step="1" value="30" oninput="refrParam('angle',this.value)" style="flex:1">
</div>
<div style="margin-top:8px"></div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="refrPreset(1,1.5,30)">Воздух<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>Стекло</button>
<button class="preset-btn" onclick="refrPreset(1.5,1,30)">Стекло<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>Воздух</button>
<button class="preset-btn" onclick="refrPreset(1.33,1.5,30)">Вода<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>Стекло</button>
<button class="preset-btn" onclick="refrPreset(1,2.42,45)">Алмаз</button>
</div>
<div class="pp-hint">Тащи луч мышью для изменения угла</div>
</div>
<div class="proj-canvas-outer">
<canvas id="refraction-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="refrbar">
<div class="pstat"><div class="pstat-label">θ₁</div><div class="pstat-val" id="refrbar-v1" style="color:#9B5DE5">30°</div></div>
<div class="pstat"><div class="pstat-label">θ₂</div><div class="pstat-val" id="refrbar-v2" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">Крит. угол</div><div class="pstat-val" id="refrbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">ПВО</div><div class="pstat-val" id="refrbar-v4" style="color:#EF476F">Нет</div></div>
</div>
</div>
<!-- ── PROBABILITY sim body ── -->
<div id="sim-probability" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Режим</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn prob-mode-btn active" onclick="probMode('coin',this)">Монета</button>
<button class="preset-btn prob-mode-btn" onclick="probMode('dice',this)">Кубик</button>
<button class="preset-btn prob-mode-btn" onclick="probMode('dice2',this)">2 кубика</button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="probPreset('coin',100)">100 бросков</button>
<button class="preset-btn" onclick="probPreset('coin',1000)">1000</button>
<button class="preset-btn" onclick="probPreset('dice',100)">Кубик ×100</button>
<button class="preset-btn" onclick="probPreset('dice2',500)">2 куб. ×500</button>
</div>
<div class="pp-hint">Нажми <svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> — броски идут автоматически</div>
</div>
<div class="proj-canvas-outer">
<canvas id="probability-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="probbar">
<div class="pstat"><div class="pstat-label">Бросков</div><div class="pstat-val" id="probbar-v1" style="color:#9B5DE5">0</div></div>
<div class="pstat"><div class="pstat-label">Макс. отклон.</div><div class="pstat-val" id="probbar-v2" style="color:#EF476F"></div></div>
<div class="pstat"><div class="pstat-label">χ²</div><div class="pstat-val" id="probbar-v3" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">Режим</div><div class="pstat-val" id="probbar-v4" style="color:#FFD166">Монета</div></div>
</div>
</div>
<!-- ── BOHR ATOM sim body ── -->
<div id="sim-bohratom" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Уровень</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn" onclick="bohrLevel(1)">n=1</button>
<button class="preset-btn" onclick="bohrLevel(2)">n=2</button>
<button class="preset-btn" onclick="bohrLevel(3)">n=3</button>
<button class="preset-btn" onclick="bohrLevel(4)">n=4</button>
<button class="preset-btn" onclick="bohrLevel(5)">n=5</button>
<button class="preset-btn" onclick="bohrLevel(6)">n=6</button>
</div>
<div class="gp-section-title" style="margin-bottom:6px">Переходы</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="preset-btn" onclick="bohrTransition(2,1)">2<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>1 (УФ)</button>
<button class="preset-btn" onclick="bohrTransition(3,2)">3<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>2 (красн.)</button>
<button class="preset-btn" onclick="bohrTransition(4,2)">4<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>2 (голуб.)</button>
<button class="preset-btn" onclick="bohrTransition(4,3)">4<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>3 (ИК)</button>
<button class="preset-btn" onclick="bohrTransition(1,3)">1<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>3 (поглощ.)</button>
</div>
<div class="pp-hint">Кликни на уровень для перехода электрона</div>
</div>
<div class="proj-canvas-outer">
<canvas id="bohratom-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="bohrbar">
<div class="pstat"><div class="pstat-label">Уровень n</div><div class="pstat-val" id="bohrbar-v1" style="color:#9B5DE5">1</div></div>
<div class="pstat"><div class="pstat-label">E (эВ)</div><div class="pstat-val" id="bohrbar-v2" style="color:#06D6E0">-13.6</div></div>
<div class="pstat"><div class="pstat-label">λ (нм)</div><div class="pstat-val" id="bohrbar-v3" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Серия</div><div class="pstat-val" id="bohrbar-v4" style="color:#EF476F"></div></div>
</div>
</div>
<!-- ── ELECTROLYSIS sim body ── -->
<div id="sim-electrolysis" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:220px;gap:0">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<div class="proj-slider-row" style="margin-bottom:8px">
<label style="font-size:.78rem;color:#ccc;width:55px">U = <span id="elec-V-val" style="color:#FFD166;font-weight:700">6</span> В</label>
<input type="range" id="sl-elec-V" min="1" max="12" step="0.5" value="6" oninput="elecParam('voltage',this.value)" style="flex:1">
</div>
<div style="margin-top:4px;margin-bottom:8px">
<div class="gp-section-title" style="margin-bottom:4px">Электролит</div>
<div style="display:flex;flex-wrap:wrap;gap:4px">
<button class="preset-btn elec-type-btn active" onclick="elecPreset('nacl',this)">NaCl</button>
<button class="preset-btn elec-type-btn" onclick="elecPreset('cuso4',this)">CuSO₄</button>
<button class="preset-btn elec-type-btn" onclick="elecPreset('h2so4',this)">H₂SO₄</button>
</div>
</div>
<div class="pp-hint">Ионы движутся к электродам, на электродах — газ и осадок</div>
</div>
<div class="proj-canvas-outer">
<canvas id="electrolysis-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="elecbar">
<div class="pstat"><div class="pstat-label">I (А)</div><div class="pstat-val" id="elecbar-v1" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">Масса</div><div class="pstat-val" id="elecbar-v2" style="color:#9B5DE5"></div></div>
<div class="pstat"><div class="pstat-label">Газ (мл)</div><div class="pstat-val" id="elecbar-v3" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">Время</div><div class="pstat-val" id="elecbar-v4" style="color:#EF476F">0 с</div></div>
</div>
</div>
<!-- ── WAVES sim body ── -->
<div id="sim-waves" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<div class="proj-panel" style="width:228px;gap:0;overflow-y:auto">
<!-- Mode selector -->
<div class="gp-section-title" style="margin-bottom:6px">Режим</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:12px">
<button class="wave-mode-btn active" onclick="wavesMode('transverse',this)">Поперечная</button>
<button class="wave-mode-btn" onclick="wavesMode('longitudinal',this)">Продольная</button>
<button class="wave-mode-btn" onclick="wavesMode('superposition',this)">Суперпозиция</button>
<button class="wave-mode-btn" onclick="wavesMode('standing',this)">Стоячая</button>
</div>
<!-- Wave 1 -->
<div class="gp-section-title" style="margin-bottom:6px">Волна 1</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Амплитуда A₁</span>
<span class="param-val" id="waves-A1-val" style="color:#9B5DE5">50</span>
</div>
<input type="range" id="sl-waves-A1" class="param-slider" min="10" max="90" step="2" value="50" oninput="wavesParam('A1',this.value)">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота f₁</span>
<span class="param-val" id="waves-f1-val" style="color:#9B5DE5">1.0 Гц</span>
</div>
<input type="range" id="sl-waves-f1" class="param-slider" min="0.3" max="4" step="0.1" value="1.0" oninput="wavesParam('f1',this.value)">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Фаза φ₁</span>
<span class="param-val" id="waves-phi1-val" style="color:#9B5DE5">0</span>
</div>
<input type="range" id="sl-waves-phi1" class="param-slider" min="0" max="6.28" step="0.1" value="0" oninput="wavesParam('phi1',this.value)">
</div>
<!-- Wave 2 (superposition only) -->
<div id="waves-w2-section" style="display:none">
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Волна 2</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Амплитуда A₂</span>
<span class="param-val" id="waves-A2-val" style="color:#06D6E0">40</span>
</div>
<input type="range" id="sl-waves-A2" class="param-slider" min="10" max="90" step="2" value="40" oninput="wavesParam('A2',this.value)">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Частота f₂</span>
<span class="param-val" id="waves-f2-val" style="color:#06D6E0">1.5 Гц</span>
</div>
<input type="range" id="sl-waves-f2" class="param-slider" min="0.3" max="4" step="0.1" value="1.5" oninput="wavesParam('f2',this.value)">
</div>
<div class="param-block">
<div class="param-header">
<span class="param-name">Фаза φ₂</span>
<span class="param-val" id="waves-phi2-val" style="color:#06D6E0">0</span>
</div>
<input type="range" id="sl-waves-phi2" class="param-slider" min="0" max="6.28" step="0.1" value="0" oninput="wavesParam('phi2',this.value)">
</div>
<div class="pp-hint" style="margin-bottom:8px">φ₂=0: конструктивная · φ₂=π: деструктивная интерференция</div>
</div>
<!-- Standing wave harmonics -->
<div id="waves-n-section" style="display:none">
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Гармоника n</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn wave-n-btn active" onclick="wavesN(1,this)">n=1</button>
<button class="preset-btn wave-n-btn" onclick="wavesN(2,this)">n=2</button>
<button class="preset-btn wave-n-btn" onclick="wavesN(3,this)">n=3</button>
<button class="preset-btn wave-n-btn" onclick="wavesN(4,this)">n=4</button>
<button class="preset-btn wave-n-btn" onclick="wavesN(5,this)">n=5</button>
</div>
</div>
<!-- Presets -->
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Пресеты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:10px">
<button class="preset-btn" onclick="wavesPreset('constructive')">Конструктивная</button>
<button class="preset-btn" onclick="wavesPreset('destructive')">Деструктивная</button>
<button class="preset-btn" onclick="wavesPreset('beats')">Биения</button>
</div>
<!-- Speed -->
<div class="param-block" style="margin-top:4px">
<div class="param-header">
<span class="param-name">Скорость анимации</span>
<span class="param-val" id="waves-speed-val" style="color:#FFD166">×2.0</span>
</div>
<input type="range" id="sl-waves-speed" class="param-slider" min="0.3" max="5" step="0.1" value="2.0" oninput="wavesParam('speed',this.value)" style="accent-color:#FFD166">
</div>
<div class="pp-hint" style="margin-top:10px">v = λ·f — основное волновое уравнение</div>
</div>
<div class="proj-canvas-outer">
<canvas id="waves-canvas"></canvas>
</div>
</div>
<div class="proj-stats-bar" id="wavesbar">
<div class="pstat"><div class="pstat-label">T (с)</div><div class="pstat-val" id="wavesbar-T" style="color:#9B5DE5"></div></div>
<div class="pstat"><div class="pstat-label">λ (px)</div><div class="pstat-val" id="wavesbar-lam" style="color:#06D6E0"></div></div>
<div class="pstat"><div class="pstat-label">v (px/с)</div><div class="pstat-val" id="wavesbar-v" style="color:#FFD166"></div></div>
<div class="pstat"><div class="pstat-label">f (Гц)</div><div class="pstat-val" id="wavesbar-f" style="color:#F15BB5"></div></div>
</div>
</div>
<!-- ── STEREO sim body ── -->
<div id="sim-stereo" class="sim-body-wrap" style="display:none">
<div class="graph-panel" style="overflow-y:auto;max-height:100%">
<div class="gp-section-title">Фигура</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
<button class="gp-btn stereo-fig-btn active" onclick="setStereoFigure('cube',this)">Куб</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('parallelepiped',this)">Параллелепипед</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('pyramid',this)">Пирамида</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('tetrahedron',this)">Тетраэдр</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('cylinder',this)">Цилиндр</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('cone',this)">Конус</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('trunccone',this)">Усечённый конус</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('sphere',this)">Сфера</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('prism',this)">Призма</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('truncpyramid',this)">Усеч. пирамида</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('octahedron',this)">Октаэдр</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('icosahedron',this)">Икосаэдр</button>
<button class="gp-btn stereo-fig-btn" onclick="setStereoFigure('dodecahedron',this)">Додекаэдр</button>
</div>
<div class="gp-section-title">Параметры</div>
<div id="stereo-params">
<div class="stereo-sl-row" id="sp-a-row">
<label>a <span id="sp-a-val">4</span></label>
<input type="range" id="sl-sp-a" min="1" max="10" step="0.5" value="4" oninput="stereoParamChange('a',this.value)">
</div>
<div class="stereo-sl-row" id="sp-b-row" style="display:none">
<label>b <span id="sp-b-val">3</span></label>
<input type="range" id="sl-sp-b" min="1" max="10" step="0.5" value="3" oninput="stereoParamChange('b',this.value)">
</div>
<div class="stereo-sl-row" id="sp-c-row" style="display:none">
<label>c <span id="sp-c-val">5</span></label>
<input type="range" id="sl-sp-c" min="1" max="10" step="0.5" value="5" oninput="stereoParamChange('c',this.value)">
</div>
<div class="stereo-sl-row" id="sp-h-row" style="display:none">
<label>h <span id="sp-h-val">5</span></label>
<input type="range" id="sl-sp-h" min="1" max="12" step="0.5" value="5" oninput="stereoParamChange('h',this.value)">
</div>
<div class="stereo-sl-row" id="sp-r-row" style="display:none">
<label>r <span id="sp-r-val">2</span></label>
<input type="range" id="sl-sp-r" min="0.5" max="8" step="0.5" value="2" oninput="stereoParamChange('r',this.value)">
</div>
<div class="stereo-sl-row" id="sp-R-row" style="display:none">
<label>R <span id="sp-R-val">3</span></label>
<input type="range" id="sl-sp-R" min="0.5" max="8" step="0.5" value="3" oninput="stereoParamChange('R',this.value)">
</div>
<div class="stereo-sl-row" id="sp-n-row" style="display:none">
<label>n <span id="sp-n-val">4</span></label>
<input type="range" id="sl-sp-n" min="3" max="12" step="1" value="4" oninput="stereoParamChange('n',this.value)">
</div>
</div>
<div class="stereo-sl-row" style="margin-top:6px">
<label>Прозрачность <span id="sp-opacity-val">0.3</span></label>
<input type="range" id="sl-sp-opacity" min="0" max="1" step="0.05" value="0.3" oninput="stereoOpacityChange(this.value)">
</div>
<div class="gp-section-title" style="margin-top:10px">Отображение</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px">
<button class="gp-btn stereo-toggle active" data-layer="edges" onclick="stereoToggle('edges',this)">Рёбра</button>
<button class="gp-btn stereo-toggle active" data-layer="vertices" onclick="stereoToggle('vertices',this)">Вершины</button>
<button class="gp-btn stereo-toggle active" data-layer="labels" onclick="stereoToggle('labels',this)">Метки</button>
<button class="gp-btn stereo-toggle active" data-layer="axes" onclick="stereoToggle('axes',this)">Оси</button>
<button class="gp-btn stereo-toggle active" data-layer="grid" onclick="stereoToggle('grid',this)">Сетка</button>
</div>
<div class="gp-section-title" style="margin-top:8px">Сечение</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn stereo-sect-btn" id="sect-toggle" onclick="stereoSectionToggle(this)">Показать</button>
<button class="gp-btn stereo-sect-type" data-type="horizontal" onclick="stereoSectionType('horizontal',this)">Горизонт.</button>
<button class="gp-btn stereo-sect-type" data-type="diagonal" onclick="stereoSectionType('diagonal',this)">Диагональ</button>
<button class="gp-btn stereo-sect-type" data-type="custom" onclick="stereoSectionType('custom',this)">По точкам</button>
</div>
<div class="stereo-sl-row">
<label>Высота <span id="sp-sect-val">50%</span></label>
<input type="range" id="sl-sp-sect" min="0" max="100" step="1" value="50" oninput="stereoSectionHeight(this.value)">
</div>
<div class="stereo-sl-row" id="sp-angle-row" style="display:none">
<label>Наклон <span id="sp-angle-val">50%</span></label>
<input type="range" id="sl-sp-angle" min="0" max="100" step="1" value="50" oninput="stereoSectionAngle(this.value)">
</div>
<div id="sect-area-display" style="font-size:0.7rem;color:#06D6E0;margin-top:2px;display:none"></div>
<div id="sphere-radius-info" style="font-size:0.7rem;color:#F59E0B;margin-top:2px;display:none"></div>
<div class="gp-section-title" style="margin-top:8px">Точки и линии</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn" id="stereo-point-btn" onclick="stereoPointMode(this)">Поставить точку</button>
<button class="gp-btn" id="stereo-connect-btn" onclick="stereoConnectMode(this)">Соединить</button>
<button class="gp-btn" id="stereo-undo-pt-btn" onclick="stereoUndoPoint()">Отменить</button>
<button class="gp-btn" id="stereo-clear-pts-btn" onclick="stereoClearPoints()">Очистить</button>
</div>
<div id="points-info" style="font-size:0.68rem;color:rgba(255,255,255,0.5);margin-bottom:4px"></div>
<div class="gp-section-title" style="margin-top:8px">Элементы фигуры</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn" id="stereo-height-btn" onclick="stereoToggleHeight(this)">Высота</button>
<button class="gp-btn" id="stereo-apothem-btn" onclick="stereoToggleApothem(this)">Апофема</button>
<button class="gp-btn" id="stereo-diag-btn" onclick="stereoToggleDiag(this)">Диагонали</button>
<button class="gp-btn" id="stereo-mid-btn" onclick="stereoToggleMid(this)">Середины</button>
<button class="gp-btn" id="stereo-inscribed-btn" onclick="stereoInscribed(this)">Вписанная</button>
<button class="gp-btn" id="stereo-circumscribed-btn" onclick="stereoCircumscribed(this)">Описанная</button>
</div>
<div class="gp-section-title" style="margin-top:8px">Метки рёбер</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn" id="stereo-mark-tick-btn" onclick="stereoMarkMode('ticks',this)" title="Засечки равных рёбер — кликните на ребро (до 3 штрихов)">Засечки</button>
<button class="gp-btn" id="stereo-mark-par-btn" onclick="stereoMarkMode('parallel',this)" title="Метки параллельных рёбер — кликните на ребро (до 3 стрелок)">Параллельные</button>
<button class="gp-btn" id="stereo-edge-len-btn" onclick="stereoToggleEdgeLengths(this)" title="Показать длины всех рёбер">Длины рёбер</button>
<button class="gp-btn" onclick="stereoMarkClear()">Очистить</button>
</div>
<div class="gp-section-title" style="margin-top:8px">Производные точки</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn" id="stereo-derive-mid-btn" onclick="stereoDerive('midpoint',this)" title="Середина ребра — кликните на ребро">Середина ребра</button>
<button class="gp-btn" id="stereo-derive-fc-btn" onclick="stereoDerive('face_centroid',this)" title="Центр грани — кликните на грань">Центр грани</button>
<button class="gp-btn" id="stereo-derive-alt-btn" onclick="stereoDerive('alt_foot',this)" title="Основание высоты — кликните вершину, затем точку ребра">Осн. высоты</button>
<button class="gp-btn" id="stereo-derive-cen-btn" onclick="stereoDerive('solid_centroid',this)" title="Центроид тела — вставляется автоматически">Центроид тела</button>
<button class="gp-btn" onclick="stereoDeriveUndo()">Удалить послед.</button>
<button class="gp-btn" onclick="stereoDeriveClear()">Очистить</button>
</div>
<div class="gp-section-title" style="margin-top:8px">Инструменты</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn" id="stereo-unfold-btn" onclick="stereoUnfold(this)">Развёртка</button>
<button class="gp-btn" id="stereo-measure-btn" onclick="stereoMeasure(this)">Измерение</button>
<button class="gp-btn" onclick="stereoMeasureUndo()">Удалить посл.</button>
<button class="gp-btn" onclick="stereoMeasureClear()">Очистить</button>
</div>
<div class="gp-section-title" style="margin-top:8px">Углы и расстояния</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">
<button class="gp-btn" id="stereo-angle-edge-btn" onclick="stereoAngleMode('edge', this)">∠ рёбер</button>
<button class="gp-btn" id="stereo-angle-lp-btn" onclick="stereoAngleMode('linePlane', this)">∠ прям.–пл.</button>
<button class="gp-btn" id="stereo-angle-dih-btn" onclick="stereoAngleMode('dihedral', this)">∠ двугранный</button>
<button class="gp-btn" id="stereo-angle-pp-btn" onclick="stereoAngleMode('pointPlane', this)">d(т<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>пл)</button>
<button class="gp-btn" id="stereo-angle-skew-btn" onclick="stereoAngleMode('skewLines', this)">∠ скрещ. прям.</button>
<button class="gp-btn" id="stereo-angle-clear-btn" onclick="stereoAngleClear()">Очистить</button>
</div>
<div id="angle-hint" style="font-size:0.65rem;color:rgba(255,255,255,0.4);margin-bottom:4px"></div>
<div class="gp-section-title" style="margin-top:10px">Формулы</div>
<div id="stereo-formulas" style="font-size:0.72rem;color:rgba(255,255,255,0.7);line-height:1.5"></div>
<div class="gp-section-title" style="margin-top:10px">Управление</div>
<div class="tp-text" style="font-size:0.72rem">
Вращение: зажмите и тяните<br>
Зум: колёсико мыши<br>
Точка: включите режим, кликните на ребро<br>
Соединить: кликните 2 точки/вершины<br>
По точкам: поставьте 3+ точки <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> сечение<br>
∠ рёбер: 3 точки (A-B-C), угол в вершине B<br>
∠ прям.–пл.: 2 точки (прямая), затем грань<br>
∠ двугранный: 2 точки общего ребра<br>
d(т<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>пл): точка, затем грань — перпендикуляр<br>
∠ скрещ.: 4 точки — P1,P2 (пр.1), P3,P4 (пр.2)<br>
Засечки/Парал.: кликайте рёбра (0→1→2→3→0)<br>
Середина ребра: клик на ребро<br>
Центр грани: клик на грань<br>
Осн. высоты: вершина → точка/ребро<br>
Координаты: наведите на вершину
</div>
</div>
<div class="graph-canvas-outer">
<div class="graph-canvas-wrap" id="stereo-container"></div>
</div>
</div>
<!-- stats bar for stereo -->
<div class="proj-stats-bar" id="stereo-stats" style="display:none">
<div class="pstat"><div class="pstat-label">Объём</div><div class="pstat-val" id="stbar-vol"></div></div>
<div class="pstat"><div class="pstat-label">Полная S</div><div class="pstat-val" id="stbar-area"></div></div>
<div class="pstat"><div class="pstat-label">Бок. S</div><div class="pstat-val" id="stbar-side"></div></div>
<div class="pstat"><div class="pstat-label">Высота</div><div class="pstat-val" id="stbar-h"></div></div>
<div class="pstat"><div class="pstat-label">Диагональ</div><div class="pstat-val" id="stbar-d"></div></div>
</div>
<!-- ── HYDROSTATICS sim body ── -->
<div id="sim-hydro" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<!-- left panel -->
<div class="proj-panel" style="width:230px;gap:0;overflow-y:auto">
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
<!-- liquid -->
<div style="margin-bottom:10px">
<div style="font-size:.72rem;color:rgba(255,255,255,.4);margin-bottom:4px">Жидкость</div>
<select onchange="hydroSim&&hydroSim.setLiquid(this.value);document.getElementById('hydro-liq-sel').value=this.value" style="width:100%;background:#1a1030;color:#f0e8ff;border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:5px 8px;font-size:.78rem">
<option value="water">Вода (1000 кг/м³)</option>
<option value="saltwater">Солёная вода (1030)</option>
<option value="oil">Масло (900)</option>
<option value="alcohol">Спирт (790)</option>
<option value="glycerin">Глицерин (1260)</option>
<option value="mercury">Ртуть (13600)</option>
</select>
</div>
<!-- material (Archimedes only) -->
<div id="hydro-panel-mat" style="margin-bottom:10px;display:none">
<div style="font-size:.72rem;color:rgba(255,255,255,.4);margin-bottom:4px">Материал тела</div>
<select onchange="hydroSim&&hydroSim.setMaterial(this.value)" style="width:100%;background:#1a1030;color:#f0e8ff;border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:5px 8px;font-size:.78rem">
<option value="styrofoam">Пенопласт (30 кг/м³)</option>
<option value="cork">Пробка (120)</option>
<option value="wood">Дерево (500)</option>
<option value="ice">Лёд (900)</option>
<option value="plastic">Пластик (1100)</option>
<option value="glass">Стекло (2500)</option>
<option value="aluminum">Алюминий (2700)</option>
<option value="iron">Железо (7800)</option>
<option value="gold">Золото (19300)</option>
</select>
<div style="display:flex;gap:5px;margin-top:6px">
<button class="gp-btn" onclick="hydroSim&&hydroSim.addBody()" style="flex:1">+ Тело</button>
<button class="gp-btn" onclick="hydroSim&&hydroSim.clearBodies()" style="flex:1">Очистить</button>
</div>
</div>
<!-- contact angle (surface tension) -->
<div id="hydro-panel-theta" style="margin-bottom:10px;display:none">
<div style="display:flex;justify-content:space-between;font-size:.72rem;color:rgba(255,255,255,.4);margin-bottom:4px">
<span>Краевой угол θ</span>
<span id="hydro-theta-lbl" style="color:#9B5DE5">20°</span>
</div>
<input type="range" min="0" max="160" value="20" step="5" style="width:100%;accent-color:#9B5DE5" oninput="hydroSim&&hydroSim.setContactAngle(+this.value);document.getElementById('hydro-theta-lbl').textContent=this.value+'\u00B0';document.getElementById('hydro-theta-val').textContent=this.value+'\u00B0';document.querySelector('#hydro-surf-ctrl input[type=range]').value=this.value">
<div style="display:flex;justify-content:space-between;font-size:.65rem;color:rgba(255,255,255,.25);margin-top:2px">
<span>Смачивание</span><span>Несмачивание</span>
</div>
<div style="margin-top:6px">
<button class="gp-btn" id="hydro-surf-toggle-panel" onclick="hydroToggleSurface()" style="width:100%">Капилляры</button>
</div>
</div>
<!-- communicating vessels -->
<div id="hydro-panel-comm" style="margin-bottom:10px;display:none">
<div style="font-size:.72rem;color:rgba(255,255,255,.4);margin-bottom:4px">Сосудов</div>
<div style="display:flex;gap:5px">
<button class="gp-btn hydro-nv active" onclick="hydroSetVessels(2,this)" style="flex:1">2</button>
<button class="gp-btn hydro-nv" onclick="hydroSetVessels(3,this)" style="flex:1">3</button>
<button class="gp-btn hydro-nv" onclick="hydroSetVessels(4,this)" style="flex:1">4</button>
</div>
<div style="margin-top:8px">
<button class="gp-btn" id="hydro-valve-panel-btn" onclick="hydroToggleValve()" style="width:100%;color:#06D6A0;border-color:rgba(6,214,160,.3)">Кран: открыт</button>
</div>
<div style="margin-top:6px;display:flex;gap:5px">
<button class="gp-btn" onclick="hydroSim&&hydroSim.addLiquid(0)" style="flex:1">+ Жидкость</button>
<button class="gp-btn" onclick="hydroSim&&hydroSim.removeLiquid()" style="flex:1">- Жидкость</button>
</div>
</div>
<!-- formula display -->
<div class="gp-section-title" style="margin-top:4px;margin-bottom:6px">Формулы</div>
<div id="hydro-formulas" style="font-size:.72rem;font-family:'JetBrains Mono',monospace;color:rgba(255,255,255,.6);line-height:1.7;background:rgba(255,255,255,.03);border-radius:8px;padding:8px 10px;min-height:80px"></div>
<!-- result badge -->
<div id="hydro-result" style="margin-top:8px;font-size:.82rem;font-weight:700;text-align:center;padding:8px;border-radius:8px;display:none"></div>
</div><!-- /.proj-panel -->
<!-- canvas area -->
<div style="flex:1;min-width:0;position:relative">
<canvas id="hydro-canvas" style="width:100%;height:100%;display:block"></canvas>
</div>
</div><!-- /.sim-body-wrap -->
</div><!-- /#sim-hydro -->
<!-- ══════════════════════════════════════════════
ПЛАНИМЕТРИЯ
══════════════════════════════════════════════ -->
<div id="sim-geometry" class="sim-proj-wrap" style="display:none">
<div class="sim-body-wrap">
<!-- left panel -->
<div class="geo-panel">
<!-- Tool: select + point -->
<div class="gp-section-title">Инструмент</div>
<div class="geo-tool-grid">
<button id="geo-btn-select" class="geo-tool-btn active" onclick="geoSetTool('select',this)" title="Выделить / переместить (Esc)">
<svg viewBox="0 0 24 24" fill="none"><path d="M5 3l14 9-7 1-4 7z" stroke-width="2"/></svg>
Выбор
</button>
<button id="geo-btn-point" class="geo-tool-btn" onclick="geoSetTool('point',this)" title="Поставить точку">
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>
Точка
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Построения</div>
<div class="geo-tool-grid">
<button id="geo-btn-segment" class="geo-tool-btn" onclick="geoSetTool('segment',this)" title="Отрезок — 2 точки">
<svg viewBox="0 0 24 24" fill="none"><line x1="4" y1="20" x2="20" y2="4" stroke-width="2.5"/><circle cx="4" cy="20" r="2.5" fill="currentColor"/><circle cx="20" cy="4" r="2.5" fill="currentColor"/></svg>
Отрезок
</button>
<button id="geo-btn-line" class="geo-tool-btn" onclick="geoSetTool('line',this)" title="Прямая — 2 точки">
<svg viewBox="0 0 24 24" fill="none"><line x1="2" y1="22" x2="22" y2="2" stroke-width="2" stroke-dasharray="3,2"/></svg>
Прямая
</button>
<button id="geo-btn-ray" class="geo-tool-btn" onclick="geoSetTool('ray',this)" title="Луч — начало + направление">
<svg viewBox="0 0 24 24" fill="none"><line x1="4" y1="20" x2="22" y2="4" stroke-width="2"/><polyline points="17 4 22 4 22 9" stroke-width="2"/><circle cx="4" cy="20" r="2.5" fill="currentColor"/></svg>
Луч
</button>
<button id="geo-btn-circle" class="geo-tool-btn" onclick="geoSetTool('circle',this)" title="Окружность — центр + радиус">
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8" stroke-width="2"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>
Круг
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Фигуры</div>
<div class="geo-tool-grid">
<button id="geo-btn-triangle" class="geo-tool-btn" onclick="geoSetTool('triangle',this)" title="Треугольник — 3 точки">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 22,21 2,21" stroke-width="2"/></svg>
Треуг.
</button>
<button id="geo-btn-quad" class="geo-tool-btn" onclick="geoSetTool('quad',this)" title="Четырёхугольник — 4 точки">
<svg viewBox="0 0 24 24" fill="none"><polygon points="3,6 21,4 20,19 4,20" stroke-width="2"/></svg>
Четырёх.
</button>
<button id="geo-btn-polygon" class="geo-tool-btn geo-tool-wide" onclick="geoSetTool('polygon',this)" title="Многоугольник — N точек, Enter/двойной клик для завершения">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,2 22,8 19,21 5,21 2,8" stroke-width="2"/></svg>
Многоугольник
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Построения</div>
<div class="geo-tool-grid">
<button id="geo-btn-midpoint" class="geo-tool-btn" onclick="geoSetTool('midpoint',this)" title="Середина отрезка — 2 точки">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="12" x2="21" y2="12" stroke-width="2"/><circle cx="12" cy="12" r="3.5" fill="currentColor"/></svg>
Середина
</button>
<button id="geo-btn-perpbisect" class="geo-tool-btn" onclick="geoSetTool('perpbisect',this)" title="Серединный перпендикуляр — 2 точки">
<svg viewBox="0 0 24 24" fill="none"><line x1="4" y1="18" x2="20" y2="6" stroke-width="2"/><line x1="12" y1="2" x2="12" y2="22" stroke-width="1.5" stroke-dasharray="3,2"/><circle cx="12" cy="12" r="2.5" fill="currentColor"/></svg>
⊥ биссект.
</button>
<button id="geo-btn-anglebisect" class="geo-tool-btn" onclick="geoSetTool('anglebisect',this)" title="Биссектриса угла — 3 точки: A, вершина, B">
<svg viewBox="0 0 24 24" fill="none"><polyline points="4,20 12,4 20,20" stroke-width="2"/><line x1="12" y1="4" x2="12" y2="20" stroke-width="1.5" stroke-dasharray="3,2"/></svg>
∠ биссект.
</button>
<button id="geo-btn-parallel" class="geo-tool-btn" onclick="geoSetTool('parallel',this)" title="Параллельная прямая — клик на линию, затем на точку">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="8" x2="21" y2="8" stroke-width="2"/><line x1="3" y1="16" x2="21" y2="16" stroke-width="2" opacity=".5" stroke-dasharray="4,3"/></svg>
|| прямая
</button>
<button id="geo-btn-perpendicular" class="geo-tool-btn" onclick="geoSetTool('perpendicular',this)" title="Перпендикулярная прямая — клик на линию, затем на точку">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="12" x2="21" y2="12" stroke-width="2"/><line x1="12" y1="4" x2="12" y2="20" stroke-width="2" opacity=".5" stroke-dasharray="4,3"/></svg>
⊥ прямая
</button>
<button id="geo-btn-intersect" class="geo-tool-btn" onclick="geoSetTool('intersect',this)" title="Точка пересечения — клик на две прямые">
<svg viewBox="0 0 24 24" fill="none"><line x1="4" y1="20" x2="20" y2="4" stroke-width="2"/><line x1="4" y1="4" x2="20" y2="20" stroke-width="2"/><circle cx="12" cy="12" r="3.5" fill="currentColor"/></svg>
Пересеч.
</button>
<button id="geo-btn-foot" class="geo-tool-btn" onclick="geoSetTool('foot',this)" title="Основание перпендикуляра — клик на прямую, затем на точку">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="18" x2="21" y2="18" stroke-width="2"/><line x1="12" y1="18" x2="12" y2="4" stroke-width="1.5" stroke-dasharray="3,2"/><path d="M12 18 L15 18 L15 15" stroke-width="1.5" fill="none"/><circle cx="12" cy="4" r="2.5" fill="currentColor"/></svg>
Основание
</button>
<button id="geo-btn-circumcircle" class="geo-tool-btn geo-tool-wide" onclick="geoSetTool('circumcircle',this)" title="Описанная окружность — 3 точки треугольника">
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke-width="1.5" stroke-dasharray="4,3"/><polygon points="6,18 18,18 12,6" stroke-width="1.5" fill="none"/></svg>
Описанная ☉
</button>
<button id="geo-btn-incircle" class="geo-tool-btn geo-tool-wide" onclick="geoSetTool('incircle',this)" title="Вписанная окружность — 3 точки треугольника">
<svg viewBox="0 0 24 24" fill="none"><polygon points="4,20 20,20 12,4" stroke-width="1.5" fill="none"/><circle cx="12" cy="15" r="5" stroke-width="1.5" stroke-dasharray="4,3"/></svg>
Вписанная ○
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Преобразования</div>
<div class="geo-tool-grid">
<button id="geo-btn-reflect" class="geo-tool-btn" onclick="geoSetTool('reflect',this)" title="Симметрия точки относительно прямой — клик на ось, затем на точку">
<svg viewBox="0 0 24 24" fill="none"><line x1="12" y1="2" x2="12" y2="22" stroke-width="1.5" stroke-dasharray="3,2"/><circle cx="6" cy="12" r="3" stroke-width="1.5"/><circle cx="18" cy="12" r="2.5" stroke-width="1.5" opacity=".5"/><line x1="9" y1="12" x2="15" y2="12" stroke-width="1" opacity=".6"/></svg>
Симметрия
</button>
<button id="geo-btn-translate" class="geo-tool-btn" onclick="geoSetTool('translate',this)" title="Параллельный перенос — вектор AB, затем точка P">
<svg viewBox="0 0 24 24" fill="none"><circle cx="6" cy="18" r="2.5" stroke-width="1.5"/><circle cx="18" cy="6" r="2.5" stroke-width="1.5"/><line x1="6" y1="18" x2="18" y2="6" stroke-width="1.5" marker-end="url(#arrow)"/><circle cx="14" cy="18" r="2.5" stroke-width="1.5" opacity=".4"/><circle cx="21" cy="9" r="2" stroke-width="1.5" stroke-dasharray="3,2"/></svg>
Перенос
</button>
<button id="geo-btn-tangent" class="geo-tool-btn geo-tool-wide" onclick="geoSetTool('tangent',this)" title="Касательные из точки к окружности — клик на окружность, затем на точку">
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="7" stroke-width="1.5"/><line x1="4" y1="4" x2="19" y2="12" stroke-width="1.5" stroke-dasharray="4,3"/><line x1="4" y1="20" x2="19" y2="12" stroke-width="1.5" stroke-dasharray="4,3"/><circle cx="4" cy="12" r="2.5" fill="currentColor"/></svg>
Касательные
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Элементы треугольника</div>
<div class="geo-tool-grid">
<button id="geo-btn-altitude" class="geo-tool-btn" onclick="geoSetTool('altitude',this)" title="Высота — клик на сторону, затем на вершину">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="20" x2="21" y2="20" stroke-width="1.5"/><line x1="10" y1="4" x2="10" y2="20" stroke-width="1.5" stroke-dasharray="4,3"/><rect x="10" y="14" width="4" height="4" stroke-width="1.2"/><polygon points="10,4 3,20 21,20" stroke-width="1.5" fill="none"/></svg>
Высота
</button>
<button id="geo-btn-median" class="geo-tool-btn" onclick="geoSetTool('median',this)" title="Медиана — клик вершина A, B, C">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="20" stroke-width="1.5"/><circle cx="12" cy="20" r="2.5" fill="currentColor"/></svg>
Медиана
</button>
<button id="geo-btn-centroid" class="geo-tool-btn" onclick="geoSetTool('centroid',this)" title="Центроид — 3 точки треугольника, строит 3 медианы">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="20" stroke-width="1.2" opacity=".6"/><line x1="3" y1="20" x2="16.5" y2="11.5" stroke-width="1.2" opacity=".6"/><line x1="21" y1="20" x2="7.5" y2="11.5" stroke-width="1.2" opacity=".6"/><circle cx="12" cy="14.3" r="2.5" fill="currentColor"/></svg>
Центроид
</button>
<button id="geo-btn-orthocenter" class="geo-tool-btn" onclick="geoSetTool('orthocenter',this)" title="Ортоцентр — 3 точки треугольника, строит 3 высоты">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="12" y1="3" x2="12" y2="20" stroke-width="1.2" stroke-dasharray="3,2" opacity=".6"/><line x1="3" y1="20" x2="16" y2="12" stroke-width="1.2" stroke-dasharray="3,2" opacity=".6"/><line x1="21" y1="20" x2="8" y2="12" stroke-width="1.2" stroke-dasharray="3,2" opacity=".6"/><circle cx="12" cy="14" r="2.5" fill="currentColor"/></svg>
Ортоцентр
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Средняя линия и четырёхугольники</div>
<div class="geo-tool-grid">
<button id="geo-btn-midline" class="geo-tool-btn" onclick="geoSetTool('midline',this)" title="Средняя линия треугольника — 3 вершины A, B, C">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 3,20 21,20" stroke-width="1.5" fill="none"/><line x1="7.5" y1="11.5" x2="16.5" y2="11.5" stroke-width="2"/><circle cx="7.5" cy="11.5" r="2" fill="currentColor"/><circle cx="16.5" cy="11.5" r="2" fill="currentColor"/></svg>
Средняя линия
</button>
<button id="geo-btn-parallelogram" class="geo-tool-btn" onclick="geoSetTool('parallelogram',this)" title="Параллелограмм — 3 точки A, B, C → вычисляет D">
<svg viewBox="0 0 24 24" fill="none"><polygon points="6,19 3,5 18,5 21,19" stroke-width="1.5" fill="none"/></svg>
Параллелограмм
</button>
<button id="geo-btn-diagonal" class="geo-tool-btn" onclick="geoSetTool('diagonal',this)" title="Диагонали — клик внутри многоугольника (4+ вершин)">
<svg viewBox="0 0 24 24" fill="none"><rect x="3" y="3" width="18" height="18" stroke-width="1.5" fill="none"/><line x1="3" y1="3" x2="21" y2="21" stroke-width="1.5" stroke-dasharray="4,2"/><line x1="21" y1="3" x2="3" y2="21" stroke-width="1.5" stroke-dasharray="4,2"/></svg>
Диагонали
</button>
<button id="geo-btn-scale" class="geo-tool-btn" onclick="geoSetTool('scale',this)" title="Подобие — клик центр O, затем клик точку P → P&#39; = O + k·(P O)">
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="2" fill="currentColor"/><line x1="12" y1="12" x2="20" y2="6" stroke-width="1.5"/><circle cx="20" cy="6" r="2.5" stroke-width="1.5"/><line x1="12" y1="12" x2="17" y2="18" stroke-width="1.5" stroke-dasharray="3,2"/><circle cx="17" cy="18" r="2.5" stroke-width="1.5" stroke-dasharray="3,2"/></svg>
Подобие
</button>
</div>
<div class="geo-ngon-ctrl" id="geo-scale-ctrl" style="gap:6px;padding:2px 0 4px">
<span style="font-size:11px;opacity:.7">k =</span>
<button class="geo-ngon-btn" onclick="geoScaleK(-0.5)">
<svg viewBox="0 0 16 16" fill="none"><line x1="3" y1="8" x2="13" y2="8" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<span id="geo-scale-k">2</span>
<button class="geo-ngon-btn" onclick="geoScaleK(+0.5)">
<svg viewBox="0 0 16 16" fill="none"><line x1="8" y1="3" x2="8" y2="13" stroke-width="2" stroke-linecap="round"/><line x1="3" y1="8" x2="13" y2="8" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
<div class="gp-section-title" style="margin-top:4px">Правильный многоугольник</div>
<div class="geo-tool-grid">
<button id="geo-btn-ngon" class="geo-tool-btn" onclick="geoSetTool('ngon',this)" title="Правильный n-угольник — клик центр, клик вершина">
<svg viewBox="0 0 24 24" fill="none"><polygon points="12,3 20.2,8.5 17.3,18 6.7,18 3.8,8.5" stroke-width="1.5" fill="none"/></svg>
n-угольник
</button>
<div class="geo-ngon-ctrl" id="geo-ngon-ctrl">
<button class="geo-ngon-btn" onclick="geoNgonN(-1)">
<svg viewBox="0 0 16 16" fill="none"><line x1="3" y1="8" x2="13" y2="8" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<span id="geo-ngon-n">6</span>
<button class="geo-ngon-btn" onclick="geoNgonN(+1)">
<svg viewBox="0 0 16 16" fill="none"><line x1="8" y1="3" x2="8" y2="13" stroke-width="2" stroke-linecap="round"/><line x1="3" y1="8" x2="13" y2="8" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
</div>
<!-- Thales theorem -->
<div class="gp-section-title" style="margin-top:4px">Теорема Фалеса</div>
<div class="geo-tool-grid">
<button id="geo-btn-thales" class="geo-tool-btn" onclick="geoSetTool('thales',this)" title="Теорема Фалеса — клик O, затем A, затем B → A&#39;B&#39; ∥ AB">
<svg viewBox="0 0 24 24" fill="none"><circle cx="4" cy="20" r="2" fill="currentColor"/><line x1="4" y1="20" x2="22" y2="4" stroke-width="1.5"/><line x1="4" y1="20" x2="22" y2="12" stroke-width="1.5"/><line x1="10" y1="15" x2="13" y2="12" stroke-width="2" stroke-dasharray="0"/><line x1="17" y1="9" x2="20" y2="7" stroke-width="2" opacity=".6"/></svg>
Фалес
</button>
</div>
<!-- Mark tools -->
<div class="gp-section-title" style="margin-top:4px">Метки</div>
<div class="geo-tool-grid">
<button id="geo-btn-tick" class="geo-tool-btn" onclick="geoSetTool('tick',this)" title="Метки равных сторон — клик на отрезок или сторону (1–3 штриха)">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="20" x2="21" y2="4" stroke-width="1.5"/><line x1="11" y1="7" x2="8" y2="11" stroke-width="2" stroke-linecap="round"/><line x1="13" y1="9" x2="10" y2="13" stroke-width="2" stroke-linecap="round"/></svg>
Штрихи
</button>
<button id="geo-btn-arcmark" class="geo-tool-btn" onclick="geoSetTool('arcmark',this)" title="Метки равных углов — клик на вершину полигона (1–3 дуги)">
<svg viewBox="0 0 24 24" fill="none"><path d="M4 20 L20 20 L20 4" stroke-width="1.5" fill="none"/><path d="M8 20 A12 12 0 0 1 20 8" stroke-width="1.5" fill="none"/><path d="M11 20 A9 9 0 0 1 20 11" stroke-width="1.5" fill="none"/></svg>
Дуги
</button>
<button id="geo-btn-parallelmark" class="geo-tool-btn" onclick="geoSetTool('parallelmark',this)" title="Метки параллельных сторон — клик на отрезок (1–2 стрелки)">
<svg viewBox="0 0 24 24" fill="none"><line x1="3" y1="8" x2="21" y2="8" stroke-width="1.5"/><polyline points="9,5 13,8 9,11" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/><line x1="3" y1="16" x2="21" y2="16" stroke-width="1.5"/><polyline points="9,13 13,16 9,19" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Параллельность
</button>
</div>
<!-- Display options -->
<div class="gp-section-title" style="margin-top:6px">Параметры</div>
<label class="geo-toggle-row" onclick="geoToggle('showGrid',this)">
<span class="geo-toggle-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
Сетка
</span>
<div class="geo-toggle on" id="geo-tog-showGrid"></div>
</label>
<label class="geo-toggle-row" onclick="geoToggle('showAxes',this)">
<span class="geo-toggle-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="12" y1="20" x2="12" y2="4"/><line x1="4" y1="12" x2="20" y2="12"/></svg>
Оси
</span>
<div class="geo-toggle on" id="geo-tog-showAxes"></div>
</label>
<label class="geo-toggle-row" onclick="geoToggle('showLabels',this)">
<span class="geo-toggle-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
Метки
</span>
<div class="geo-toggle on" id="geo-tog-showLabels"></div>
</label>
<label class="geo-toggle-row" onclick="geoToggle('showLengths',this)">
<span class="geo-toggle-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="8" x2="4" y2="16"/><line x1="20" y1="8" x2="20" y2="16"/></svg>
Длины
</span>
<div class="geo-toggle" id="geo-tog-showLengths"></div>
</label>
<label class="geo-toggle-row" onclick="geoToggle('showAngles',this)">
<span class="geo-toggle-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 20 L20 20 L20 4"/><path d="M7 20 A13 13 0 0 1 20 9"/></svg>
Углы
</span>
<div class="geo-toggle" id="geo-tog-showAngles"></div>
</label>
<!-- Stats -->
<div class="gp-section-title" style="margin-top:6px">Объектов</div>
<div style="display:flex;flex-direction:column;gap:0">
<div class="geo-stat-row"><span>Точки</span><b id="geo-st-pts">0</b></div>
<div class="geo-stat-row"><span>Отрезки</span><b id="geo-st-segs">0</b></div>
<div class="geo-stat-row"><span>Окружности</span><b id="geo-st-circs">0</b></div>
<div class="geo-stat-row"><span>Многоугольники</span><b id="geo-st-polys">0</b></div>
<div class="geo-stat-row"><span>Построения</span><b id="geo-st-constr">0</b></div>
</div>
<!-- Actions -->
<div style="margin-top:auto;padding-top:8px;display:flex;flex-direction:column;gap:4px">
<button class="gp-btn" onclick="geomSim&&geomSim.reset()" title="Очистить всё">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/></svg>
Очистить
</button>
</div>
</div><!-- /.geo-panel -->
<!-- canvas area -->
<div class="geo-canvas-outer">
<canvas id="geo-canvas"></canvas>
<div class="geo-hint-bar" id="geo-hint">Кликни для добавления точки</div>
<div class="geo-del-confirm" id="geo-del-confirm">
<span id="geo-del-msg"></span>
<button class="geo-del-btn geo-del-btn-soft" id="geo-del-soft">Только этот</button>
<button class="geo-del-btn geo-del-btn-hard" id="geo-del-hard">Со всеми зависимыми</button>
<button class="geo-del-btn geo-del-btn-cancel" id="geo-del-cancel">Отмена</button>
</div>
</div>
</div><!-- /.sim-body-wrap -->
</div><!-- /#sim-geometry -->
<!-- ── Theory panel (overlay right) ── -->
<div class="theory-panel" id="theory-panel">
<div class="theory-panel-inner" id="theory-content"></div>
</div>
</div><!-- /#lab-sim -->
</div><!-- /.sb-content -->
</div><!-- /.app-layout -->
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js"></script>
<script src="/js/labs/graph.js"></script>
<script src="/js/labs/magnetic.js"></script>
<script src="/js/labs/triangle.js"></script>
<script src="/js/labs/projectile.js"></script>
<script src="/js/labs/collision.js"></script>
<script src="/js/labs/gas.js"></script>
<script src="/js/labs/states.js"></script>
<script src="/js/labs/brownian.js"></script>
<script src="/js/labs/diffusion.js"></script>
<script src="/js/labs/coulomb.js"></script>
<script src="/js/labs/circuit.js"></script>
<script src="/js/labs/reactions.js"></script>
<script src="/js/labs/flask.js"></script>
<script src="/js/labs/redox.js"></script>
<script src="/js/labs/ionexchange.js"></script>
<script src="/js/labs/stereo.js?v=2"></script>
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
<script>
const { user, isTeacher, isAdmin } = LS.initPage();
window._simQuizAllowed = true; // default; overridden after permission fetch for students
LS.showBoardIfAllowed();
/* ════════════════════════════════
SIM CATALOGUE (defined after P_* consts below)
════════════════════════════════ */
let _catFilter = 'all';
let _disabledSimIds = new Set();
let _simModuleDisabled = false;
function filterSims(cat, btn) {
_catFilter = cat;
document.querySelectorAll('.lab-filter').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderSims();
}
function renderSims() {
const base = _catFilter === 'all' ? SIMS : SIMS.filter(s => s.cat === _catFilter);
const list = base.filter(s => !s.id || !_disabledSimIds.has(s.id));
document.getElementById('sim-grid').innerHTML = list.map(s => `
<div class="sim-card ${s.id ? '' : 'soon'}" ${s.id ? `onclick="openSim('${s.id}')"` : ''}>
${s.preview}
<div class="sim-body">
<div class="sim-cat ${s.cat}">${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Химия' : s.cat === 'bio' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg> Биология' : s.cat === 'game' ? '<svg class="ic" viewBox="0 0 24 24"><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg> Игры' : LS.icon('zap',14) + ' Физика'}</div>
<div class="sim-title">${s.title}</div>
<div class="sim-desc">${s.desc}</div>
</div>
${!s.id ? '<div class="sim-soon-badge">Скоро</div>' : ''}
</div>`).join('');
if (window.lucide) lucide.createIcons();
}
/* ════════════════════════════════
CARD PREVIEW SVGs
════════════════════════════════ */
function _grid(fg='rgba(255,255,255,0.06)') {
return `<g stroke="${fg}" stroke-width="1">
<line x1="45" y1="0" x2="45" y2="140"/><line x1="90" y1="0" x2="90" y2="140"/>
<line x1="135" y1="0" x2="135" y2="140"/><line x1="180" y1="0" x2="180" y2="140"/>
<line x1="225" y1="0" x2="225" y2="140"/>
<line x1="0" y1="35" x2="270" y2="35"/><line x1="0" y1="70" x2="270" y2="70"/>
<line x1="0" y1="105" x2="270" y2="105"/>
</g>`;
}
function _axes() {
return `<line x1="0" y1="70" x2="262" y2="70" stroke="rgba(255,255,255,0.32)" stroke-width="1.5"/>
<line x1="135" y1="140" x2="135" y2="6" stroke="rgba(255,255,255,0.32)" stroke-width="1.5"/>
<polygon points="265,70 258,67 258,73" fill="rgba(255,255,255,0.32)"/>
<polygon points="135,4 132,11 138,11" fill="rgba(255,255,255,0.32)"/>`;
}
function _svg(body) {
return `<svg class="sim-preview" viewBox="0 0 270 140" xmlns="http://www.w3.org/2000/svg">
<rect width="270" height="140" fill="#0D0D1A"/>${body}</svg>`;
}
/* 1 — Graph */
const P_GRAPH = _svg(`${_grid()}${_axes()}
<path d="M 15,132 Q 135,20 255,132" stroke="#9B5DE5" stroke-width="2.5" fill="none"/>
<path d="M 0,70 C 34,30 56,30 90,70 C 124,110 146,110 180,70 C 214,30 236,30 270,70"
stroke="#06D6E0" stroke-width="2" fill="none" opacity="0.75"/>`);
/* 2 — Transform: three shifted/scaled sines */
const P_TRANSFORM = _svg(`${_grid()}${_axes()}
<path d="M 0,70 C 34,30 56,30 90,70 C 124,110 146,110 180,70 C 214,30 236,30 270,70"
stroke="#9B5DE5" stroke-width="2" fill="none" opacity="0.9"/>
<path d="M 0,53 C 22,24 42,24 67,53 C 92,82 112,82 135,53 C 158,24 178,24 202,53 C 227,82 248,82 270,53"
stroke="#06D6E0" stroke-width="2" fill="none" opacity="0.65"/>
<path d="M 0,85 C 45,36 80,36 135,85 C 190,134 225,134 270,85"
stroke="#F15BB5" stroke-width="2" fill="none" opacity="0.55"/>`);
/* 3 — Triangle geometry */
const P_TRIANGLE = _svg(`${_grid('rgba(255,255,255,0.04)')}
<polygon points="60,115 210,115 135,25" fill="rgba(155,93,229,0.1)" stroke="#9B5DE5" stroke-width="2"/>
<line x1="60" y1="115" x2="173" y2="70" stroke="rgba(6,214,224,0.5)" stroke-width="1.3" stroke-dasharray="4,3"/>
<line x1="210" y1="115" x2="98" y2="70" stroke="rgba(6,214,224,0.5)" stroke-width="1.3" stroke-dasharray="4,3"/>
<line x1="135" y1="25" x2="135" y2="115" stroke="rgba(6,214,224,0.5)" stroke-width="1.3" stroke-dasharray="4,3"/>
<circle cx="135" cy="78" r="3" fill="#06D6E0"/>
<rect x="131" y="111" width="8" height="8" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.2"/>`);
/* 4 — Inscribed/circumscribed circles */
const P_CIRCLES = _svg(`${_grid('rgba(255,255,255,0.04)')}
<polygon points="80,118 190,118 135,22" fill="rgba(155,93,229,0.08)" stroke="#9B5DE5" stroke-width="1.8"/>
<circle cx="135" cy="85" r="33" fill="none" stroke="#06D6E0" stroke-width="1.8" stroke-dasharray="5,3" opacity="0.7"/>
<circle cx="135" cy="55" r="52" fill="none" stroke="#F15BB5" stroke-width="1.5" stroke-dasharray="5,3" opacity="0.5"/>
<circle cx="135" cy="85" r="3" fill="#06D6E0" opacity="0.8"/>
<circle cx="135" cy="55" r="3" fill="#F15BB5" opacity="0.8"/>`);
/* 5 — Quadratic roots: parabola crossing x-axis */
const P_QUADRATIC = _svg(`${_grid()}${_axes()}
<path d="M 55,125 Q 135,8 215,125" stroke="#9B5DE5" stroke-width="2.5" fill="none"/>
<circle cx="85" cy="70" r="5" fill="#F15BB5" stroke="#fff" stroke-width="1.5"/>
<circle cx="185" cy="70" r="5" fill="#F15BB5" stroke="#fff" stroke-width="1.5"/>
<line x1="85" y1="68" x2="85" y2="125" stroke="rgba(241,91,181,0.35)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="185" y1="68" x2="185" y2="125" stroke="rgba(241,91,181,0.35)" stroke-width="1" stroke-dasharray="3,3"/>
<text x="135" y="136" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">D = b²− 4ac</text>`);
/* 6 — 3D geometry: isometric cube */
const P_3D = _svg(`${_grid('rgba(255,255,255,0.04)')}
<polygon points="135,20 210,58 210,115 135,77" fill="rgba(155,93,229,0.15)" stroke="#9B5DE5" stroke-width="1.8"/>
<polygon points="135,20 60,58 60,115 135,77" fill="rgba(155,93,229,0.08)" stroke="#9B5DE5" stroke-width="1.8"/>
<polygon points="60,58 135,20 210,58 135,96" fill="rgba(155,93,229,0.22)" stroke="#9B5DE5" stroke-width="1.8"/>
<line x1="135" y1="77" x2="135" y2="96" stroke="#9B5DE5" stroke-width="1.8"/>
<text x="135" y="132" font-size="9" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">V = a³</text>`);
/* 7 — Probability: histogram bars */
const P_PROB = _svg(`${_grid()}
<line x1="30" y1="15" x2="30" y2="118" stroke="rgba(255,255,255,0.35)" stroke-width="1.5"/>
<line x1="28" y1="118" x2="255" y2="118" stroke="rgba(255,255,255,0.35)" stroke-width="1.5"/>
<rect x="38" y="90" width="24" height="28" fill="rgba(155,93,229,0.6)" rx="2"/>
<rect x="68" y="72" width="24" height="46" fill="rgba(155,93,229,0.7)" rx="2"/>
<rect x="98" y="44" width="24" height="74" fill="rgba(155,93,229,0.85)" rx="2"/>
<rect x="128" y="32" width="24" height="86" fill="#9B5DE5" rx="2"/>
<rect x="158" y="50" width="24" height="68" fill="rgba(155,93,229,0.8)" rx="2"/>
<rect x="188" y="76" width="24" height="42" fill="rgba(155,93,229,0.65)" rx="2"/>
<rect x="218" y="96" width="24" height="22" fill="rgba(155,93,229,0.5)" rx="2"/>`);
/* 8 — Normal distribution: bell curve */
const P_NORMAL = _svg(`${_grid()}
<line x1="10" y1="118" x2="260" y2="118" stroke="rgba(255,255,255,0.32)" stroke-width="1.5"/>
<path d="M 10,116 C 50,115 80,110 100,90 C 115,72 125,35 135,22 C 145,35 155,72 170,90 C 190,110 220,115 260,116"
stroke="#9B5DE5" stroke-width="2.5" fill="none"/>
<path d="M 100,90 C 115,72 125,35 135,22 C 145,35 155,72 170,90 L 170,118 L 100,118 Z"
fill="rgba(155,93,229,0.15)"/>
<line x1="135" y1="22" x2="135" y2="118" stroke="rgba(255,255,255,0.2)" stroke-width="1" stroke-dasharray="3,3"/>
<text x="135" y="132" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">μ = 0, σ = 1</text>`);
/* 8b — Trig circle */
const P_TRIGCIRCLE = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="30" y1="70" x2="240" y2="70" stroke="rgba(255,255,255,0.25)" stroke-width="1.2"/>
<line x1="135" y1="8" x2="135" y2="132" stroke="rgba(255,255,255,0.25)" stroke-width="1.2"/>
<circle cx="135" cy="70" r="52" fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="1.8"/>
<line x1="135" y1="70" x2="172" y2="33" stroke="rgba(255,255,255,0.45)" stroke-width="1.3"/>
<line x1="135" y1="70" x2="172" y2="70" stroke="#06D6E0" stroke-width="2.5"/>
<line x1="172" y1="70" x2="172" y2="33" stroke="#EF476F" stroke-width="2.5"/>
<circle cx="172" cy="33" r="5" fill="#9B5DE5"/>
<path d="M 148,70 A 13,13 0 0,0 144,60" stroke="rgba(155,93,229,0.6)" stroke-width="1.5" fill="none"/>
<text x="135" y="136" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">sin · cos · tg · ctg</text>`);
/* 9 — Projectile motion */
const P_PROJECTILE = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="15" y1="118" x2="255" y2="118" stroke="rgba(255,255,255,0.32)" stroke-width="1.5"/>
<path d="M 20,118 Q 135,18 250,118" stroke="#06D6E0" stroke-width="2.5" fill="none"/>
<circle cx="20" cy="118" r="5" fill="#06D6E0"/>
<line x1="20" y1="118" x2="52" y2="80" stroke="rgba(6,214,224,0.8)" stroke-width="1.5"
marker-end="url(#arr)"/>
<defs><marker id="arr" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#06D6E0"/>
</marker></defs>
<line x1="135" y1="18" x2="135" y2="118" stroke="rgba(255,255,255,0.15)" stroke-width="1" stroke-dasharray="3,3"/>
<text x="135" y="132" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">x = v₀cos(α)·t</text>`);
/* 10 — Pendulum */
const P_PENDULUM = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="135" y1="15" x2="165" y2="95" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
<circle cx="165" cy="100" r="12" fill="rgba(6,214,224,0.25)" stroke="#06D6E0" stroke-width="2"/>
<line x1="135" y1="15" x2="95" y2="95" stroke="rgba(255,255,255,0.2)" stroke-width="1.5" stroke-dasharray="4,3"/>
<circle cx="95" cy="100" r="12" fill="none" stroke="rgba(6,214,224,0.3)" stroke-width="1.5" stroke-dasharray="3,3"/>
<path d="M 110,60 A 55,55 0 0 1 160,60" fill="none" stroke="rgba(6,214,224,0.4)" stroke-width="1.2" stroke-dasharray="3,3"/>
<circle cx="135" cy="15" r="4" fill="rgba(255,255,255,0.5)"/>
<text x="135" y="132" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">T = 2π√(l/g)</text>`);
/* 11 — Collision */
const P_COLLISION = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="15" y1="70" x2="255" y2="70" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>
<circle cx="70" cy="70" r="28" fill="rgba(6,214,224,0.15)" stroke="#06D6E0" stroke-width="2"/>
<text x="70" y="75" font-size="11" fill="#06D6E0" text-anchor="middle" font-family="Manrope,sans-serif" font-weight="700">m₁</text>
<line x1="100" y1="70" x2="120" y2="70" stroke="#06D6E0" stroke-width="2" marker-end="url(#a2)"/>
<circle cx="195" cy="70" r="20" fill="rgba(241,91,181,0.15)" stroke="#F15BB5" stroke-width="2"/>
<text x="195" y="75" font-size="11" fill="#F15BB5" text-anchor="middle" font-family="Manrope,sans-serif" font-weight="700">m₂</text>
<line x1="175" y1="70" x2="155" y2="70" stroke="#F15BB5" stroke-width="2" marker-end="url(#a3)"/>
<defs>
<marker id="a2" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#06D6E0"/></marker>
<marker id="a3" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#F15BB5"/></marker>
</defs>`);
/* 13 — Electric circuit */
const P_CIRCUIT = _svg(`${_grid('rgba(255,255,255,0.04)')}
<rect x="30" y="25" width="210" height="90" fill="none" stroke="rgba(255,255,255,0.25)" stroke-width="1.5" rx="4"/>
<line x1="30" y1="70" x2="70" y2="70" stroke="#06D6E0" stroke-width="2"/>
<rect x="70" y="58" width="36" height="24" fill="rgba(6,214,224,0.15)" stroke="#06D6E0" stroke-width="1.8" rx="3"/>
<text x="88" y="74" font-size="10" fill="#06D6E0" text-anchor="middle" font-family="Manrope,sans-serif" font-weight="700">R₁</text>
<line x1="106" y1="70" x2="130" y2="70" stroke="#06D6E0" stroke-width="2"/>
<rect x="130" y="58" width="36" height="24" fill="rgba(6,214,224,0.15)" stroke="#06D6E0" stroke-width="1.8" rx="3"/>
<text x="148" y="74" font-size="10" fill="#06D6E0" text-anchor="middle" font-family="Manrope,sans-serif" font-weight="700">R₂</text>
<line x1="166" y1="70" x2="190" y2="70" stroke="#06D6E0" stroke-width="2"/>
<rect x="190" y="56" width="18" height="28" fill="rgba(241,91,181,0.15)" stroke="#F15BB5" stroke-width="1.8" rx="3"/>
<line x1="208" y1="70" x2="240" y2="70" stroke="#06D6E0" stroke-width="2"/>
<text x="135" y="132" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">I = U/R</text>`);
/* 14 — Magnetic field */
const P_MAGNETIC = _svg(`
<rect width="270" height="140" fill="#05050F"/>
${_grid('rgba(155,93,229,0.06)')}
<defs>
<radialGradient id="mg1" cx="38%" cy="50%"><stop offset="0%" stop-color="#06D6E0" stop-opacity=".55"/><stop offset="100%" stop-color="#06D6E0" stop-opacity="0"/></radialGradient>
<radialGradient id="mg2" cx="62%" cy="50%"><stop offset="0%" stop-color="#F15BB5" stop-opacity=".55"/><stop offset="100%" stop-color="#F15BB5" stop-opacity="0"/></radialGradient>
</defs>
<rect width="270" height="140" fill="url(#mg1)" opacity=".7"/>
<rect width="270" height="140" fill="url(#mg2)" opacity=".7"/>
<ellipse cx="95" cy="70" rx="45" ry="45" fill="none" stroke="#06D6E0" stroke-width="1.4" stroke-dasharray="5,3" opacity=".6"/>
<ellipse cx="95" cy="70" rx="70" ry="60" fill="none" stroke="#06D6E0" stroke-width="1" stroke-dasharray="4,4" opacity=".3"/>
<ellipse cx="175" cy="70" rx="45" ry="45" fill="none" stroke="#F15BB5" stroke-width="1.4" stroke-dasharray="5,3" opacity=".6"/>
<ellipse cx="175" cy="70" rx="70" ry="60" fill="none" stroke="#F15BB5" stroke-width="1" stroke-dasharray="4,4" opacity=".3"/>
<path d="M95,30 C160,30 110,70 175,70" stroke="rgba(255,255,255,0.25)" stroke-width="1.2" fill="none"/>
<path d="M95,110 C160,110 110,70 175,70" stroke="rgba(255,255,255,0.25)" stroke-width="1.2" fill="none"/>
<circle cx="95" cy="70" r="11" fill="rgba(5,5,20,0.9)" stroke="#06D6E0" stroke-width="2.2"/>
<circle cx="95" cy="70" r="4" fill="#06D6E0"/>
<circle cx="175" cy="70" r="11" fill="rgba(5,5,20,0.9)" stroke="#F15BB5" stroke-width="2.2"/>
<line x1="170" y1="65" x2="180" y2="75" stroke="#F15BB5" stroke-width="2"/>
<line x1="180" y1="65" x2="170" y2="75" stroke="#F15BB5" stroke-width="2"/>
<text x="135" y="132" font-size="9" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">B = μ₀I / 2πr</text>`);
/* 14 — Electric field lines */
const P_FIELD = _svg(`${_grid('rgba(255,255,255,0.04)')}
<circle cx="135" cy="70" r="10" fill="rgba(155,93,229,0.3)" stroke="#9B5DE5" stroke-width="2"/>
<text x="135" y="74" font-size="10" fill="#9B5DE5" text-anchor="middle" font-family="Manrope,sans-serif" font-weight="800">+</text>
<g stroke="#9B5DE5" stroke-width="1.3" fill="none" opacity="0.6">
<path d="M135,60 L135,20"/><path d="M135,80 L135,120"/>
<path d="M125,63 L95,38"/><path d="M145,63 L175,38"/>
<path d="M125,77 L95,102"/><path d="M145,77 L175,102"/>
<path d="M122,70 L80,70"/><path d="M148,70 L190,70"/>
<path d="M125,64 L102,42"/><path d="M145,64 L168,42"/>
<path d="M125,76 L102,98"/><path d="M145,76 L168,98"/>
</g>
<circle cx="135" cy="20" r="2" fill="#9B5DE5" opacity="0.5"/>
<circle cx="135" cy="120" r="2" fill="#9B5DE5" opacity="0.5"/>
<circle cx="80" cy="70" r="2" fill="#9B5DE5" opacity="0.5"/>
<circle cx="190" cy="70" r="2" fill="#9B5DE5" opacity="0.5"/>`);
/* 15 — Thin lens */
const P_LENS = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="10" y1="70" x2="260" y2="70" stroke="rgba(255,255,255,0.25)" stroke-width="1"/>
<path d="M 135,20 Q 155,70 135,120 Q 115,70 135,20" fill="rgba(6,214,224,0.12)" stroke="#06D6E0" stroke-width="2"/>
<line x1="30" y1="45" x2="135" y2="45" stroke="#9B5DE5" stroke-width="1.8"/>
<line x1="135" y1="45" x2="230" y2="90" stroke="#9B5DE5" stroke-width="1.8"/>
<line x1="30" y1="70" x2="230" y2="70" stroke="#06D6E0" stroke-width="1.5" stroke-dasharray="3,3" opacity="0.5"/>
<line x1="30" y1="95" x2="135" y2="95" stroke="#F15BB5" stroke-width="1.8"/>
<line x1="135" y1="95" x2="230" y2="55" stroke="#F15BB5" stroke-width="1.8"/>
<circle cx="30" cy="70" r="5" fill="#9B5DE5" opacity="0.7"/>
<line x1="30" y1="40" x2="30" y2="100" stroke="rgba(255,255,255,0.4)" stroke-width="1.5"/>`);
/* 16 — Refraction */
const P_REFRACTION = _svg(`
<rect width="270" height="70" fill="#0D0D1A"/>
<rect y="70" width="270" height="70" fill="rgba(6,214,224,0.07)"/>
<line x1="0" y1="70" x2="270" y2="70" stroke="rgba(6,214,224,0.35)" stroke-width="1.5" stroke-dasharray="6,4"/>
<line x1="135" y1="10" x2="135" y2="130" stroke="rgba(255,255,255,0.15)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="60" y1="15" x2="135" y2="70" stroke="#9B5DE5" stroke-width="2.5"/>
<polygon points="135,70 127,50 143,50" fill="#9B5DE5" opacity="0.7"/>
<line x1="135" y1="70" x2="195" y2="125" stroke="#06D6E0" stroke-width="2.5"/>
<polygon points="195,125 183,112 196,107" fill="#06D6E0" opacity="0.7"/>
<path d="M 135,50 A 22,22 0 0 0 118,70" fill="none" stroke="rgba(155,93,229,0.5)" stroke-width="1.2"/>
<path d="M 135,90 A 28,28 0 0 1 157,70" fill="none" stroke="rgba(6,214,224,0.5)" stroke-width="1.2"/>
<text x="118" y="63" font-size="9" fill="rgba(155,93,229,0.8)" font-family="Manrope,sans-serif">α</text>
<text x="152" y="87" font-size="9" fill="rgba(6,214,224,0.8)" font-family="Manrope,sans-serif">β</text>
<text x="135" y="136" font-size="9" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">n₁sinα = n₂sinβ</text>`);
/* 17 — Mirrors */
const P_MIRROR = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="10" y1="70" x2="260" y2="70" stroke="rgba(255,255,255,0.25)" stroke-width="1"/>
<path d="M 200,15 Q 184,70 200,125" fill="none" stroke="#06D6E0" stroke-width="2.5"/>
<line x1="200" y1="20" x2="210" y2="30" stroke="rgba(6,214,224,0.25)" stroke-width="1.5"/>
<line x1="200" y1="45" x2="210" y2="55" stroke="rgba(6,214,224,0.25)" stroke-width="1.5"/>
<line x1="200" y1="70" x2="210" y2="80" stroke="rgba(6,214,224,0.25)" stroke-width="1.5"/>
<line x1="200" y1="95" x2="210" y2="105" stroke="rgba(6,214,224,0.25)" stroke-width="1.5"/>
<line x1="200" y1="118" x2="210" y2="128" stroke="rgba(6,214,224,0.25)" stroke-width="1.5"/>
<circle cx="130" cy="70" r="4" fill="#06D6E0" opacity="0.8"/>
<text x="130" y="84" text-anchor="middle" font-size="9" fill="#06D6E0" font-family="Manrope,sans-serif">F</text>
<line x1="50" y1="70" x2="50" y2="30" stroke="#9B5DE5" stroke-width="2"/>
<polygon points="50,30 44,42 56,42" fill="#9B5DE5"/>
<line x1="50" y1="30" x2="200" y2="30" stroke="#06D6E0" stroke-width="1.5"/>
<line x1="200" y1="30" x2="70" y2="105" stroke="#06D6E0" stroke-width="1.5"/>
<line x1="50" y1="30" x2="200" y2="70" stroke="#7BF5A4" stroke-width="1.5"/>
<line x1="200" y1="70" x2="70" y2="105" stroke="#7BF5A4" stroke-width="1.5"/>
<line x1="70" y1="70" x2="70" y2="105" stroke="#EF476F" stroke-width="2"/>
<polygon points="70,105 64,93 76,93" fill="#EF476F"/>`);
/* 18 — Isoprocesses */
const P_ISOPROCESS = _svg(`${_grid('rgba(255,255,255,0.04)')}
<line x1="30" y1="10" x2="30" y2="125" stroke="rgba(255,255,255,0.3)" stroke-width="1.5"/>
<line x1="30" y1="125" x2="265" y2="125" stroke="rgba(255,255,255,0.3)" stroke-width="1.5"/>
<path d="M 50,20 Q 140,60 240,110" fill="none" stroke="#EF476F" stroke-width="2" opacity="0.5" stroke-dasharray="4,3"/>
<path d="M 50,20 Q 130,80 230,118" fill="none" stroke="#FFD166" stroke-width="2" opacity="0.5" stroke-dasharray="4,3"/>
<line x1="50" y1="20" x2="50" y2="118" stroke="#06D6E0" stroke-width="2" opacity="0.5" stroke-dasharray="4,3"/>
<line x1="50" y1="20" x2="230" y2="20" stroke="#7BF5A4" stroke-width="2" opacity="0.5" stroke-dasharray="4,3"/>
<path d="M 50,20 Q 120,55 220,108" fill="none" stroke="#EF476F" stroke-width="2.5"/>
<circle cx="50" cy="20" r="5" fill="#9B5DE5"/>
<circle cx="220" cy="108" r="5" fill="#EF476F"/>
<text x="240" y="113" font-size="9" fill="#EF476F" font-family="Manrope,sans-serif">2</text>
<text x="40" y="16" font-size="9" fill="#9B5DE5" font-family="Manrope,sans-serif">1</text>
<text x="255" y="128" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">V</text>
<text x="18" y="12" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">P</text>`);
/* ── Chemistry / Molecular Physics previews ── */
const P_GAS = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
<rect x="6" y="6" width="258" height="128" rx="4" fill="none" stroke="rgba(155,93,229,0.4)" stroke-width="2"/>
${[
[40,30,'#4CC9F0'],[70,80,'#7BF5A4'],[110,25,'#EF476F'],[150,60,'#FFD166'],[190,30,'#4CC9F0'],
[220,90,'#EF476F'],[55,110,'#7BF5A4'],[95,65,'#4CC9F0'],[130,110,'#EF476F'],[170,40,'#FFD166'],
[210,115,'#4CC9F0'],[240,55,'#7BF5A4'],[30,70,'#FFD166'],[80,120,'#EF476F'],[165,95,'#4CC9F0']
].map(([x,y,c])=>`<circle cx="${x}" cy="${y}" r="5" fill="${c}" opacity="0.85"/>`).join('')}
<rect x="6" y="105" width="258" height="29" rx="3" fill="rgba(0,0,0,0.55)"/>
<rect x="18" y="112" width="40" height="12" rx="2" fill="rgba(155,93,229,0.25)"/>
<rect x="18" y="112" width="14" height="12" rx="2" fill="rgba(155,93,229,0.6)"/>
<rect x="70" y="112" width="40" height="12" rx="2" fill="rgba(155,93,229,0.25)"/>
<rect x="70" y="112" width="22" height="12" rx="2" fill="#7BF5A4" opacity="0.7"/>
<rect x="122" y="112" width="40" height="12" rx="2" fill="rgba(155,93,229,0.25)"/>
<rect x="122" y="112" width="30" height="12" rx="2" fill="#EF476F" opacity="0.7"/>
<text x="202" y="121" font-size="8" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">PV=nRT</text>`);
/* ── Законы Ньютона ── */
const P_NEWTON = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
<line x1="0" y1="105" x2="270" y2="105" stroke="rgba(255,255,255,0.22)" stroke-width="2"/>
<rect x="80" y="75" width="50" height="30" rx="5" fill="rgba(6,214,224,0.18)" stroke="#06D6E0" stroke-width="2"/>
<line x1="130" y1="90" x2="175" y2="90" stroke="#EF476F" stroke-width="2.5" marker-end="url(#na)"/>
<defs><marker id="na" markerWidth="7" markerHeight="7" refX="5" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#EF476F"/></marker></defs>
<text x="153" y="84" font-size="9" fill="#EF476F" font-family="Manrope,sans-serif" font-weight="700">F</text>
<line x1="105" y1="75" x2="105" y2="55" stroke="rgba(255,255,255,0.3)" stroke-width="1.2" stroke-dasharray="3,2"/>
<line x1="105" y1="55" x2="175" y2="55" stroke="rgba(255,255,255,0.18)" stroke-width="1" stroke-dasharray="3,3"/>
<circle cx="65" cy="90" r="12" fill="rgba(155,93,229,0.2)" stroke="#9B5DE5" stroke-width="1.8"/>
<text x="65" y="94" font-size="9" fill="#9B5DE5" text-anchor="middle" font-family="Manrope,sans-serif" font-weight="700">m₂</text>
<line x1="195" y1="90" x2="220" y2="90" stroke="#9B5DE5" stroke-width="2" stroke-dasharray="4,3" marker-end="url(#nb)"/>
<defs><marker id="nb" markerWidth="7" markerHeight="7" refX="5" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#9B5DE5"/></marker></defs>
<text x="135" y="130" font-size="8" fill="rgba(255,255,255,0.4)" text-anchor="middle" font-family="Manrope,sans-serif">a = F/m · III законы Ньютона</text>`);
/* ── Песочница сил ── */
const P_SANDBOX = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
${_grid('rgba(255,255,255,0.03)')}
<line x1="0" y1="115" x2="270" y2="115" stroke="rgba(155,93,229,0.35)" stroke-width="2"/>
<rect x="55" y="82" width="44" height="33" rx="6" fill="rgba(239,71,111,0.22)" stroke="#EF476F" stroke-width="1.8"/>
<text x="77" y="103" font-size="8" fill="#fff" text-anchor="middle" font-family="monospace" font-weight="700">5кг</text>
<circle cx="180" cy="88" r="18" fill="rgba(76,201,240,0.18)" stroke="#4CC9F0" stroke-width="1.8"/>
<text x="180" y="92" font-size="8" fill="#fff" text-anchor="middle" font-family="monospace" font-weight="700">8кг</text>
<line x1="99" y1="95" x2="140" y2="95" stroke="#FFD166" stroke-width="2.2" marker-end="url(#sa)"/>
<line x1="198" y1="88" x2="238" y2="68" stroke="#7BF5A4" stroke-width="2.2" marker-end="url(#sb)"/>
<defs>
<marker id="sa" markerWidth="7" markerHeight="7" refX="5" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#FFD166"/></marker>
<marker id="sb" markerWidth="7" markerHeight="7" refX="5" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7 Z" fill="#7BF5A4"/></marker>
</defs>
<text x="120" y="87" font-size="8" fill="#FFD166" font-family="monospace">F₁</text>
<text x="225" y="63" font-size="8" fill="#7BF5A4" font-family="monospace">F₂</text>
<text x="135" y="133" font-size="8" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">Песочница сил · F = ma</text>`);
/* ── coming soon chem previews (simple) ── */
const P_KINETICS = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
${_grid()}
<path d="M 20,120 C 60,90 100,50 140,35 S 220,28 260,26" fill="none" stroke="#34d399" stroke-width="2" stroke-linecap="round"/>
<path d="M 20,30 C 60,55 100,100 140,112 S 220,118 260,120" fill="none" stroke="#EF476F" stroke-width="2" stroke-linecap="round"/>
<circle cx="140" cy="35" r="4" fill="#34d399"/>
<circle cx="140" cy="112" r="4" fill="#EF476F"/>
<text x="20" y="18" font-size="9" fill="rgba(52,211,153,0.8)" font-family="Manrope,sans-serif">[C] продукт</text>
<text x="180" y="130" font-size="9" fill="rgba(239,71,111,0.8)" font-family="Manrope,sans-serif">[A] реагент</text>`);
const P_EQUILIBRIUM = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
<text x="135" y="30" font-size="11" fill="rgba(255,255,255,0.7)" text-anchor="middle" font-family="Manrope,sans-serif">A + B ⇌ C + D</text>
<rect x="30" y="50" width="60" height="70" rx="4" fill="rgba(155,93,229,0.15)" stroke="rgba(155,93,229,0.4)" stroke-width="1.5"/>
<rect x="100" y="75" width="70" height="45" rx="4" fill="rgba(6,214,224,0.12)" stroke="rgba(6,214,224,0.35)" stroke-width="1.5"/>
<rect x="180" y="55" width="60" height="65" rx="4" fill="rgba(241,91,181,0.12)" stroke="rgba(241,91,181,0.35)" stroke-width="1.5"/>
<text x="60" y="90" font-size="9" fill="#9B5DE5" text-anchor="middle" font-family="Manrope,sans-serif">A,B</text>
<text x="135" y="101" font-size="8" fill="#06D6E0" text-anchor="middle" font-family="Manrope,sans-serif">K</text>
<text x="210" y="91" font-size="9" fill="#F15BB5" text-anchor="middle" font-family="Manrope,sans-serif">C,D</text>`);
const P_ELECTROLYSIS = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
<rect x="20" y="30" width="230" height="90" rx="6" fill="rgba(6,214,224,0.07)" stroke="rgba(6,214,224,0.2)" stroke-width="1.5"/>
<rect x="50" y="20" width="12" height="80" rx="3" fill="#9B5DE5" opacity="0.8"/>
<rect x="208" y="20" width="12" height="80" rx="3" fill="#EF476F" opacity="0.8"/>
${[55,58,61,64,67,70].map(x=>`<circle cx="${x}" cy="${110-Math.random()*20|0}" r="2.5" fill="rgba(155,93,229,0.6)"/>`).join('')}
${[210,214,218,222,226].map(x=>`<circle cx="${x}" cy="${100-Math.random()*15|0}" r="2.5" fill="rgba(239,71,111,0.6)"/>`).join('')}
<text x="56" y="15" font-size="8" fill="#9B5DE5" text-anchor="middle" font-family="Manrope,sans-serif"></text>
<text x="214" y="15" font-size="8" fill="#EF476F" text-anchor="middle" font-family="Manrope,sans-serif">+</text>`);
const P_BOHR = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
<circle cx="135" cy="70" r="8" fill="#FFD166" opacity="0.9"/>
<ellipse cx="135" cy="70" rx="30" ry="10" fill="none" stroke="rgba(155,93,229,0.4)" stroke-width="1.5"/>
<ellipse cx="135" cy="70" rx="55" ry="18" fill="none" stroke="rgba(6,214,224,0.3)" stroke-width="1.5"/>
<ellipse cx="135" cy="70" rx="80" ry="27" fill="none" stroke="rgba(241,91,181,0.25)" stroke-width="1.5"/>
<circle cx="165" cy="70" r="4" fill="#9B5DE5"/>
<circle cx="90" cy="70" r="4" fill="#06D6E0"/>
<circle cx="215" cy="70" r="4" fill="#F15BB5"/>
<line x1="165" y1="60" x2="190" y2="35" stroke="rgba(255,214,0,0.6)" stroke-width="1.5" stroke-dasharray="3,2"/>
<circle cx="190" cy="35" r="3" fill="#FFD166" opacity="0.8"/>`);
const P_ORBITALS = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
<ellipse cx="135" cy="70" rx="60" ry="25" fill="rgba(155,93,229,0.15)" stroke="rgba(155,93,229,0.5)" stroke-width="1.5"/>
<ellipse cx="135" cy="70" rx="25" ry="60" fill="rgba(6,214,224,0.1)" stroke="rgba(6,214,224,0.4)" stroke-width="1.5"/>
<circle cx="135" cy="70" r="6" fill="#FFD166" opacity="0.9"/>
<circle cx="95" cy="70" r="5" fill="#9B5DE5" opacity="0.8"/>
<circle cx="175" cy="70" r="5" fill="#9B5DE5" opacity="0.8"/>`);
const P_PH = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
${_grid()}
<path d="M 20,110 L 60,108 L 100,105 L 120,90 L 130,30 L 140,75 L 180,40 L 220,35 L 260,32"
fill="none" stroke="#34d399" stroke-width="2" stroke-linecap="round"/>
<line x1="20" y1="70" x2="260" y2="70" stroke="rgba(255,255,255,0.15)" stroke-width="1" stroke-dasharray="4,3"/>
<text x="20" y="18" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">pH</text>
<text x="240" y="130" font-size="9" fill="rgba(255,255,255,0.4)" font-family="Manrope,sans-serif">V</text>`);
const P_CHEMSANDBOX = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
${_grid()}
<rect x="85" y="20" width="100" height="70" rx="8" fill="none" stroke="rgba(75,205,155,0.4)" stroke-width="1.5"/>
<rect x="88" y="55" width="94" height="32" rx="4" fill="rgba(75,205,155,0.15)"/>
<circle cx="110" cy="71" r="4" fill="rgba(255,200,60,0.5)"/>
<circle cx="130" cy="68" r="3" fill="rgba(255,255,255,0.3)"/>
<circle cx="150" cy="73" r="3.5" fill="rgba(90,200,235,0.4)"/>
<text x="135" y="105" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif" text-anchor="middle">A + B <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> C + D</text>
<rect x="40" y="115" width="28" height="18" rx="4" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
<rect x="75" y="115" width="28" height="18" rx="4" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
<rect x="110" y="115" width="28" height="18" rx="4" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
<rect x="145" y="115" width="28" height="18" rx="4" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
<rect x="180" y="115" width="28" height="18" rx="4" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
<circle cx="54" cy="121" r="3" fill="#78D278"/><circle cx="89" cy="121" r="3" fill="#7BF5A4"/>
<circle cx="124" cy="121" r="3" fill="#4CC9F0"/><circle cx="159" cy="121" r="3" fill="#9BB8CC"/>
<circle cx="194" cy="121" r="3" fill="#FFD166"/>`);
const P_CRYSTAL = _svg(`
<rect width="270" height="140" fill="#0D0D1A"/>
${[
[80,40],[135,40],[190,40],
[55,75],[110,75],[165,75],[220,75],
[80,110],[135,110],[190,110]
].map(([x,y],i)=>`<circle cx="${x}" cy="${y}" r="${i%2===0?7:5}" fill="${i%2===0?'#9B5DE5':'#06D6E0'}" opacity="0.8"/>`).join('')}
<line x1="80" y1="40" x2="135" y2="40" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="135" y1="40" x2="190" y2="40" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="80" y1="40" x2="55" y2="75" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="135" y1="40" x2="110" y2="75" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="190" y1="40" x2="165" y2="75" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="190" y1="40" x2="220" y2="75" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="55" y1="75" x2="80" y2="110" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="110" y1="75" x2="135" y2="110" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="165" y1="75" x2="190" y2="110" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="80" y1="110" x2="135" y2="110" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<line x1="135" y1="110" x2="190" y2="110" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>`);
const P_CELLDIVISION = _svg(`
<rect width="270" height="140" fill="#0e0e18"/>
<ellipse cx="135" cy="70" rx="78" ry="54" fill="rgba(34,211,153,0.07)" stroke="rgba(34,211,153,0.5)" stroke-width="2"/>
<ellipse cx="135" cy="70" rx="44" ry="28" fill="rgba(155,93,229,0.08)" stroke="rgba(155,93,229,0.3)" stroke-width="1" stroke-dasharray="4,3"/>
<line x1="55" y1="70" x2="215" y2="70" stroke="rgba(255,214,0,0.35)" stroke-width="1.2" stroke-dasharray="3,2"/>
<rect x="98" y="57" width="9" height="15" rx="2" fill="#EF476F" opacity="0.9"/>
<rect x="112" y="57" width="9" height="15" rx="2" fill="#FF6B35" opacity="0.9"/>
<rect x="126" y="57" width="9" height="15" rx="2" fill="#9B5DE5" opacity="0.9"/>
<rect x="140" y="57" width="9" height="15" rx="2" fill="#FFD166" opacity="0.9"/>
<rect x="154" y="57" width="9" height="15" rx="2" fill="#EF476F" opacity="0.9"/>
<rect x="98" y="75" width="9" height="15" rx="2" fill="#EF476F" opacity="0.9"/>
<rect x="112" y="75" width="9" height="15" rx="2" fill="#FF6B35" opacity="0.9"/>
<rect x="126" y="75" width="9" height="15" rx="2" fill="#9B5DE5" opacity="0.9"/>
<rect x="140" y="75" width="9" height="15" rx="2" fill="#FFD166" opacity="0.9"/>
<rect x="154" y="75" width="9" height="15" rx="2" fill="#EF476F" opacity="0.9"/>
<line x1="135" y1="16" x2="114" y2="57" stroke="rgba(255,214,0,0.4)" stroke-width="1"/>
<line x1="135" y1="16" x2="135" y2="57" stroke="rgba(255,214,0,0.4)" stroke-width="1"/>
<line x1="135" y1="124" x2="114" y2="90" stroke="rgba(255,214,0,0.4)" stroke-width="1"/>
<line x1="135" y1="124" x2="135" y2="90" stroke="rgba(255,214,0,0.4)" stroke-width="1"/>
<circle cx="135" cy="15" r="5" fill="rgba(255,214,0,0.7)"/>
<circle cx="135" cy="125" r="5" fill="rgba(255,214,0,0.7)"/>
<text x="135" y="137" font-size="8" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">Метафаза · митоз</text>`);
const P_PHOTOSYNTHESIS = _svg(`
<rect width="270" height="140" fill="#0a0e14"/>
<ellipse cx="135" cy="72" rx="100" ry="48" fill="rgba(34,211,153,0.07)" stroke="rgba(34,211,153,0.45)" stroke-width="2"/>
<rect x="52" y="60" width="166" height="22" rx="7" fill="rgba(34,211,153,0.18)" stroke="rgba(34,211,153,0.5)" stroke-width="1.5"/>
<line x1="70" y1="12" x2="79" y2="59" stroke="#FFD166" stroke-width="1.8" stroke-dasharray="3,2" opacity="0.8"/>
<line x1="100" y1="8" x2="107" y2="59" stroke="#FFD166" stroke-width="1.8" stroke-dasharray="3,2" opacity="0.8"/>
<line x1="135" y1="6" x2="135" y2="59" stroke="#FFD166" stroke-width="1.8" stroke-dasharray="3,2" opacity="0.8"/>
<line x1="170" y1="8" x2="163" y2="59" stroke="#FFD166" stroke-width="1.8" stroke-dasharray="3,2" opacity="0.8"/>
<circle cx="70" cy="11" r="5" fill="#FFD166" opacity="0.9"/>
<circle cx="100" cy="7" r="5" fill="#FFD166" opacity="0.9"/>
<circle cx="135" cy="5" r="5" fill="#FFD166" opacity="0.9"/>
<circle cx="170" cy="7" r="5" fill="#FFD166" opacity="0.9"/>
<text x="52" y="98" font-size="8" fill="rgba(6,214,224,0.8)" font-family="Manrope,sans-serif">H₂O</text>
<text x="90" y="106" font-size="8" fill="rgba(255,255,255,0.45)" font-family="Manrope,sans-serif">CO₂</text>
<text x="168" y="52" font-size="8" fill="#9B5DE5" font-family="Manrope,sans-serif">ATP</text>
<text x="185" y="98" font-size="8" fill="#22d399" font-family="Manrope,sans-serif">G3P</text>
<text x="135" y="135" font-size="8" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">Световые реакции · цикл Кальвина</text>`);
const P_ANGRYBIRDS = _svg(`
<rect width="270" height="140" fill="#0f1923"/>
<rect x="0" y="108" width="270" height="32" fill="#3d6b47"/>
<line x1="0" y1="108" x2="270" y2="108" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<rect x="175" y="68" width="22" height="40" fill="#b5651d" stroke="#7a3f0a" stroke-width="1.5"/>
<rect x="158" y="56" width="56" height="14" fill="#b5651d" stroke="#7a3f0a" stroke-width="1.5"/>
<rect x="168" y="40" width="18" height="18" fill="#a8d8ea" stroke="#5badd4" stroke-width="1.5"/>
<rect x="202" y="78" width="16" height="30" fill="#7a7a7a" stroke="#444" stroke-width="1.5"/>
<circle cx="232" cy="100" r="10" fill="#22c55e" stroke="#166534" stroke-width="1.5"/>
<circle cx="215" cy="99" r="10" fill="#22c55e" stroke="#166534" stroke-width="1.5"/>
<path d="M 32,102 Q 90,38 148,98" stroke="#ef476f" stroke-width="2.5" fill="none" stroke-dasharray="4,3"/>
<circle cx="32" cy="102" r="9" fill="#e63946"/>
<circle cx="36" cy="98" r="2.5" fill="#fff"/>
<circle cx="37.5" cy="97.5" r="1.1" fill="#111"/>
<line x1="28" y1="94" x2="38" y2="98" stroke="#333" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="21" cy="106" r="6.5" fill="#888" opacity="0.7"/>
<circle cx="10" cy="106" r="5.5" fill="#ffd166" opacity="0.5"/>
<line x1="18" y1="93" x2="22" y2="80" stroke="rgba(255,255,255,0.18)" stroke-width="5" stroke-linecap="round"/>
<line x1="22" y1="80" x2="26" y2="93" stroke="rgba(255,255,255,0.18)" stroke-width="5" stroke-linecap="round"/>
<text x="135" y="130" font-size="8" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">Физика полёта · импульс · разрушение</text>`);
const P_WAVES = _svg(`${_grid()}
<line x1="0" y1="70" x2="270" y2="70" stroke="rgba(255,255,255,0.13)" stroke-width="1" stroke-dasharray="4,3"/>
<path d="M 0,70 C 17,26 33,26 67,70 C 101,114 117,114 135,70 C 153,26 169,26 202,70 C 236,114 252,114 270,70"
stroke="#9B5DE5" stroke-width="2" fill="none" opacity="0.7"/>
<path d="M 0,70 C 22,18 44,18 90,70 C 136,122 158,122 180,70 C 202,18 224,18 270,70"
stroke="#06D6E0" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 0,70 C 12,10 28,8 50,40 C 72,72 88,118 112,85 C 136,52 155,18 180,50 C 205,82 240,108 270,70"
stroke="#F15BB5" stroke-width="2.5" fill="none" opacity="0.9"/>
<text x="135" y="132" font-size="8" fill="rgba(255,255,255,0.35)" text-anchor="middle" font-family="Manrope,sans-serif">v = \u03bbf \u00b7 y = A sin(\u03c9t \u2212 kx) \u00b7 \u0441\u0442\u043e\u044f\u0447\u0438\u0435 \u0432\u043e\u043b\u043d\u044b</text>`);
/* Geometry (planimetry) preview */
const P_GEOMETRY = _svg(`${_grid('rgba(255,255,255,0.04)')}
<circle cx="135" cy="70" r="50" fill="rgba(155,93,229,0.07)" stroke="#9B5DE5" stroke-width="1.5"/>
<polygon points="85,99 185,99 135,20" fill="rgba(6,214,224,0.08)" stroke="#06D6E0" stroke-width="1.8"/>
<line x1="85" y1="99" x2="162" y2="57" stroke="rgba(241,91,181,0.45)" stroke-width="1.2" stroke-dasharray="4,3"/>
<line x1="185" y1="99" x2="109" y2="57" stroke="rgba(241,91,181,0.45)" stroke-width="1.2" stroke-dasharray="4,3"/>
<line x1="135" y1="20" x2="135" y2="99" stroke="rgba(241,91,181,0.45)" stroke-width="1.2" stroke-dasharray="4,3"/>
<circle cx="135" cy="64" r="4" fill="#06D6E0" opacity="0.9"/>
<circle cx="85" cy="99" r="4" fill="#9B5DE5"/>
<circle cx="185" cy="99" r="4" fill="#9B5DE5"/>
<circle cx="135" cy="20" r="4" fill="#9B5DE5"/>
<text x="78" y="111" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">A</text>
<text x="188" y="111" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">B</text>
<text x="131" y="16" font-size="9" fill="rgba(255,255,255,0.5)" font-family="Manrope,sans-serif">C</text>`);
const SIMS = [
/* ── Математика ── */
{ id: 'graph', cat: 'math',
title: 'График функции',
desc: 'Строй графики функций y = f(x) с параметрами, зумом и курсором координат.',
preview: P_GRAPH },
{ id: 'graphtransform', cat: 'math',
title: 'Трансформации графиков',
desc: 'Наблюдай, как сдвиги, растяжения и отражения меняют вид функции y = a·f(kx+b)+c.',
preview: P_TRANSFORM },
{ id: 'geometry', cat: 'math',
title: 'Планиметрия',
desc: 'Интерактивная среда построений: точки, отрезки, прямые, окружности, многоугольники. Полноценный чертёж с привязкой и измерениями.',
preview: P_GEOMETRY },
{ id: 'triangle', cat: 'math',
title: 'Геометрия треугольника',
desc: 'Интерактивный треугольник: медианы, высоты, биссектрисы, вписанная и описанная окружности.',
preview: P_TRIANGLE },
{ id: 'quadratic', cat: 'math',
title: 'Корни квадратного уравнения',
desc: 'Задай a, b, c ползунками — смотри дискриминант и корни анимированно на числовой оси.',
preview: P_QUADRATIC },
{ id: 'stereo', cat: 'math',
title: 'Стереометрия 3D',
desc: 'Вращаемые объёмные фигуры: куб, пирамида, цилиндр, конус с формулами объёма и площади. Сечения, развёртка, вписанные/описанные сферы.',
preview: P_3D },
{ id: 'probability', cat: 'math',
title: 'Теория вероятностей',
desc: 'Подброс монеты/кубика N раз — гистограмма частот и закон больших чисел в действии.',
preview: P_PROB },
{ id: 'trigcircle', cat: 'math',
title: 'Тригонометрическая окружность',
desc: 'Единичная окружность с sin, cos, tg, ctg. Перетаскивай точку — все функции обновляются мгновенно. График синхронизирован.',
preview: P_TRIGCIRCLE },
{ id: 'normaldist', cat: 'math',
title: 'Нормальное распределение',
desc: 'Двигай μ и σ ползунками — колокол Гаусса и площадь под кривой обновляются мгновенно.',
preview: P_NORMAL },
/* ── Физика ── */
{ id: 'projectile', cat: 'phys',
title: 'Бросок тела',
desc: 'Задай начальную скорость и угол — симулируй траекторию, дальность и высоту полёта.',
preview: P_PROJECTILE },
{ id: 'pendulum', cat: 'phys',
title: 'Маятник',
desc: 'Регулируй длину и угол отклонения — изучай период колебаний и затухание.',
preview: P_PENDULUM },
{ id: 'collision', cat: 'phys',
title: 'Столкновение шаров',
desc: 'Упругий и неупругий удар двух тел: законы сохранения импульса и энергии.',
preview: P_COLLISION },
{ id: 'magnetic', cat: 'phys',
title: 'Магнитное поле токов',
desc: 'Размести провода с током — наблюдай суперпозицию полей: карта, силовые линии, вектора. Заряженная частица в поле.',
preview: P_MAGNETIC },
{ id: 'circuit', cat: 'phys',
title: 'Электрические цепи',
desc: 'Конструктор цепей из резисторов и конденсаторов. Законы Ома и Кирхгофа наглядно.',
preview: P_CIRCUIT },
{ id: 'coulomb', cat: 'phys',
title: 'Закон Кулона',
desc: 'Силовые линии и эквипотенциальные поверхности для системы точечных зарядов.',
preview: P_FIELD },
{ id: 'hydrostatics', cat: 'phys',
title: 'Гидростатика',
desc: 'Давление жидкости P=ρgh, закон Архимеда, сообщающиеся сосуды, поверхностное натяжение и капиллярность.',
preview: P_SANDBOX },
{ id: 'dynamics', cat: 'phys',
title: 'Динамика',
desc: 'Законы Ньютона, песочница сил, наклонная плоскость — всё в одном интерактивном модуле.',
preview: P_SANDBOX },
{ id: 'thinlens', cat: 'phys',
title: 'Тонкая линза',
desc: 'Двигай объект относительно линзы — формула линзы, мнимое и действительное изображение.',
preview: P_LENS },
{ id: 'refraction', cat: 'phys',
title: 'Преломление света',
desc: 'Луч на границе двух сред: закон Снеллиуса, угол Брюстера, полное внутреннее отражение.',
preview: P_REFRACTION },
{ id: 'mirrors', cat: 'phys',
title: 'Зеркала',
desc: 'Плоское, вогнутое и выпуклое зеркало: построение изображения тремя главными лучами.',
preview: P_MIRROR },
{ id: 'isoprocess', cat: 'phys',
title: 'Изопроцессы',
desc: 'PV-диаграмма для четырёх изопроцессов идеального газа. Расчёт работы, теплоты и внутренней энергии.',
preview: P_ISOPROCESS },
/* ── Химия / Молекулярная физика ── */
{ id: 'molphys', cat: 'chem',
title: 'Молекулярная физика',
desc: 'Идеальный газ, броуновское движение, агрегатные состояния и диффузия — всё в одном модуле.',
preview: P_GAS },
{ id: 'chemistry', cat: 'chem',
title: 'Химические реакции',
desc: 'Кинетика реакций, металл + кислота в колбе, ОВР с переносом электронов, ионный обмен — всё в одном модуле.',
preview: P_KINETICS },
{ id: 'equilibrium', cat: 'chem',
title: 'Химическое равновесие',
desc: 'Прямая и обратная реакция, принцип Ле Шателье: изменяй T, P, концентрацию и наблюдай сдвиг.',
preview: P_EQUILIBRIUM },
{ id: 'electrolysis', cat: 'chem',
title: 'Электролиз',
desc: 'Катод и анод в растворе электролита: движение ионов, выделение газа, закон Фарадея.',
preview: P_ELECTROLYSIS },
/* ── Скоро: Атомная структура ── */
{ id: 'bohratom', cat: 'chem',
title: 'Атом Бора',
desc: 'Электроны на орбитах, квантование энергии, эмиссия и поглощение фотонов при переходах.',
preview: P_BOHR },
{ id: 'orbitals', cat: 'chem',
title: 'Молекулярные орбитали',
desc: 'H₂, H₂O — ковалентная связь, перекрывание орбиталей, 3D-визуализация электронных облаков.',
preview: P_ORBITALS },
/* ── Скоро: Визуальная химия ── */
{ id: 'titration', cat: 'chem',
title: 'pH и кривая титрования',
desc: 'Добавляй кислоту или щёлочь — наблюдай изменение pH, цвет раствора и кривую нейтрализации.',
preview: P_PH },
{ id: 'chemsandbox', cat: 'chem',
title: 'Химическая песочница',
desc: 'Смешивай реагенты, наблюдай реакции: осадки, газы, изменение цвета. Свободное экспериментирование.',
preview: P_CHEMSANDBOX },
{ id: 'crystal', cat: 'chem',
title: 'Кристаллическая решётка',
desc: 'NaCl, алмаз, металл — интерактивная 3D-решётка, типы связей, вращение структуры.',
preview: P_CRYSTAL },
/* ── Биология ── */
{ id: 'celldivision', cat: 'bio',
title: 'Деление клетки',
desc: 'Митоз и мейоз: анимированные фазы, хромосомы, веретено деления, ядерная оболочка.',
preview: P_CELLDIVISION },
{ id: 'photosynthesis', cat: 'bio',
title: 'Фотосинтез и дыхание',
desc: 'Световые реакции в тилакоидах, цикл Кальвина, митохондриальное дыхание — молекулярная анимация.',
preview: P_PHOTOSYNTHESIS },
/* ── Игры ── */
{ id: 'angrybirds', cat: 'game',
title: 'Angry Birds Physics',
desc: 'Запускай птиц из рогатки, разрушай блоки, побеждай свиней. Реальная физика: гравитация, ветер, импульс. 6 уровней.',
preview: P_ANGRYBIRDS },
/* ── Физика: Волны ── */
{ id: 'waves', cat: 'phys',
title: 'Волны и звук',
desc: 'Поперечные и продольные волны, суперпозиция, стоячие волны. Частота, амплитуда, фаза, гармоники.',
preview: P_WAVES },
];
/* ════════════════════════════════
GRAPH SIMULATOR
════════════════════════════════ */
const FN_COLORS = ['#9B5DE5', '#06D6E0', '#F15BB5'];
let gSim = null;
let pSim = null;
let cSim = null;
let tSim = null;
let mSim = null;
let gasSim = null;
let brownSim = null;
let statesSim = null;
let diffSim = null;
let rdxSim = null;
let ioxSim = null;
let chemSandSim = null;
let cellDivSim = null;
let photosynSim = null;
let quadSim = null;
let eqSim = null;
let lensSim = null;
let titrSim = null;
let refrSim = null;
let probSim = null;
let bohrSim = null;
let elecSim = null;
let wavesSim = null;
let geomSim = null;
const 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'];
const 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 <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 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
? '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>'
: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
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 = '<rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/>';
lbl.textContent = 'Пауза';
} else if (done) {
lic.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
lbl.textContent = 'Повторить';
} else {
lic.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
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 ? '<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> +' : '<svg class="ic" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> ') + 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
? '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>'
: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
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 = '<rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/>';
lbl.textContent = 'Пауза';
} else {
lic.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
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 ── */
let 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}<br><span style="font-size:0.72rem;opacity:0.6">${s.angle.toFixed(4)} рад</span>`;
// 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 <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 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) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 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 ? '‖ Раздел' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Раздел снят';
}
function diffPartitionBtn() {
if (!diffSim) return;
const on = diffSim.partitionOn;
document.getElementById('diffusion-part-btn').innerHTML = on ? '‖ Раздел' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Раздел снят';
document.getElementById('df-part-row').classList.toggle('active', on);
}
function diffPoreToggle(rowEl) {
if (!diffSim) return;
diffSim.togglePore();
const pore = diffSim._poreMode;
const on = diffSim.partitionOn;
rowEl.classList.toggle('active', pore);
const tog = document.getElementById('df-pore-toggle');
if (tog) tog.style.background = pore ? '#FFB347' : 'rgba(255,255,255,0.15)';
const span = tog && tog.querySelector('span');
if (span) span.style.marginLeft = pore ? '14px' : '2px';
// Also sync partition row
document.getElementById('df-part-row').classList.toggle('active', on);
}
function _diffUpdateUI(info) {
document.getElementById('dfstat-LA').textContent = info.leftA;
document.getElementById('dfstat-LB').textContent = info.leftB;
document.getElementById('dfstat-RA').textContent = info.rightA;
document.getElementById('dfstat-RB').textContent = info.rightB;
document.getElementById('dfstat-mix').textContent = info.mixed + '%';
document.getElementById('mpbar-l1').textContent = 'Смешивание';
document.getElementById('mpbar-v1').textContent = info.mixed + '%';
document.getElementById('mpbar-l2').textContent = 'Лево A/B';
document.getElementById('mpbar-v2').textContent = info.leftA + '/' + info.leftB;
document.getElementById('mpbar-l3').textContent = 'Право A/B';
document.getElementById('mpbar-v3').textContent = info.rightA + '/' + info.rightB;
document.getElementById('mpbar-l4').textContent = 'Раздел';
const partLabel = !info.partitionOn ? 'снят' : info.poreMode ? 'пора' : 'вкл';
document.getElementById('mpbar-v4').textContent = partLabel;
document.getElementById('mpbar-v4').style.color = !info.partitionOn ? '#34d399' : info.poreMode ? '#FFB347' : '#fff';
document.getElementById('mpbar-l5').textContent = 'Шагов';
document.getElementById('mpbar-v5').textContent = info.steps;
}
/* ════════════════════════════════
ЗАКОН КУЛОНА
════════════════════════════════ */
let 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;
}
/* ════════════════════════════════
ЭЛЕКТРИЧЕСКИЕ ЦЕПИ
════════════════════════════════ */
let cirSim = null;
let reacSim = null;
let 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 ? '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Реакции';
}
function _reacUpdateUI(info) {
if (!info) return;
document.getElementById('chbar-l1').textContent = 'A молекул';
document.getElementById('chbar-v1').textContent = info.nA;
document.getElementById('chbar-l2').textContent = 'B молекул';
document.getElementById('chbar-v2').textContent = info.nB;
document.getElementById('chbar-l3').textContent = 'C продукт';
document.getElementById('chbar-v3').textContent = info.nC;
document.getElementById('chbar-l4').textContent = 'Реакций';
document.getElementById('chbar-v4').textContent = info.reactions;
document.getElementById('chbar-l5').textContent = 'Скорость';
document.getElementById('chbar-v5').textContent = info.rate > 0
? (info.rate * 30).toFixed(1) + '/с' : '—';
}
// _openFlask is now handled by _openChemistry('flask')
function flaskMetal(type, el) {
if (flaskSim) { flaskSim.setMetal(type); flaskSim.reset(); }
document.querySelectorAll('.flask-metal-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
}
function flaskAcid(type, el) {
if (flaskSim) flaskSim.setAcid(type);
document.querySelectorAll('.flask-acid-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
}
function flaskConcChange() {
const v = +document.getElementById('sl-flask-conc').value;
document.getElementById('flask-conc-val').textContent = v + '%';
if (flaskSim) flaskSim.setConc(v / 100);
}
function flaskTempChange() {
const v = +document.getElementById('sl-flask-temp').value;
document.getElementById('flask-temp-val').textContent = v + '°C';
if (flaskSim) flaskSim.setEnvTemp(v);
}
function flaskToggleFlame() {
if (!flaskSim) return;
flaskSim.toggleFlame();
const active = flaskSim._flameOn;
document.getElementById('flask-flame-btn').style.opacity = active ? '1' : '0.5';
document.getElementById('flask-flame-panel').style.opacity = active ? '1' : '0.5';
document.getElementById('flask-flame-panel').style.background = active ? 'rgba(239,71,111,0.22)' : '';
}
function flaskTogglePause() {
if (!flaskSim) return;
flaskSim.togglePause();
document.getElementById('flask-pause-btn').innerHTML = flaskSim._paused ? '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>' : '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
}
function _flaskUpdateUI(info) {
if (!info) return;
document.getElementById('chbar-l1').textContent = 'Металл';
document.getElementById('chbar-v1').textContent = info.metal;
document.getElementById('chbar-l2').textContent = 'Масса';
document.getElementById('chbar-v2').textContent = info.mass + ' г';
document.getElementById('chbar-l3').textContent = 'T (°C)';
document.getElementById('chbar-v3').textContent = info.temp + '°C';
document.getElementById('chbar-l4').textContent = 'pH';
document.getElementById('chbar-v4').textContent = info.pH;
document.getElementById('chbar-l5').textContent = 'H₂ (%)';
document.getElementById('chbar-v5').textContent = info.h2pct + '%';
}
// _openRedox is now handled by _openChemistry('redox')
function redoxRxn(id, el) {
document.querySelectorAll('.redox-rxn-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
if (rdxSim) { rdxSim.setReaction(id); }
}
function redoxStart() {
if (rdxSim) rdxSim.start();
}
function redoxReset() {
if (rdxSim) rdxSim.reset();
}
function _redoxUpdateUI(info) {
if (!info) return;
const phaseMap = { idle: 'ожидание', mixing: 'смешивание', reacting: 'реакция', done: 'завершена' };
document.getElementById('chbar-l1').textContent = 'Реакция';
document.getElementById('chbar-v1').textContent = info.rxn || '—';
document.getElementById('chbar-l2').textContent = 'Фаза';
document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
document.getElementById('chbar-l3').textContent = 'Прогресс';
document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
document.getElementById('chbar-l4').textContent = 'Электронов';
document.getElementById('chbar-v4').textContent = info.e + ' e⁻';
document.getElementById('chbar-l5').textContent = 'Тип';
document.getElementById('chbar-v5').innerHTML = info.phase === 'done' ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : '—';
}
// _openIonExchange is now handled by _openChemistry('ionex')
function ionexRxn(id, el) {
document.querySelectorAll('.ionex-rxn-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
if (ioxSim) { ioxSim.setReaction(id); }
}
function ionexStart() {
if (ioxSim) ioxSim.start();
}
function ionexReset() {
if (ioxSim) ioxSim.reset();
}
function _ionexUpdateUI(info) {
if (!info) return;
const phaseMap = { idle: 'ожидание', mixing: 'смешивание', pairing: 'реакция', done: 'завершена' };
const rxn = IonExSim.RXN[ioxSim.rxnId];
document.getElementById('chbar-l1').textContent = 'Реакция';
document.getElementById('chbar-v1').textContent = info.rxn || '—';
document.getElementById('chbar-l2').textContent = 'Фаза';
document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
document.getElementById('chbar-l3').textContent = 'Прогресс';
document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
document.getElementById('chbar-l4').textContent = 'Осадок';
document.getElementById('chbar-v4').textContent = info.precip > 0 ? info.precip + ' ч.' : '—';
document.getElementById('chbar-l5').textContent = 'Продукт';
document.getElementById('chbar-v5').textContent = rxn ? (rxn.sign || '—') : '—';
}
/* ════════════════════════════════
ЗАКОНЫ НЬЮТОНА
════════════════════════════════ */
/* ══════════════════════════════
DYNAMICS (unified Newton + Sandbox)
══════════════════════════════ */
let newtonSim = null;
let 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: '<svg class="ic" viewBox="0 0 24 24"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> Отрубить нить' },
C: { desc: 'Инерция в космосе: тело движется равномерно, нет сил — нет ускорения.', action: null },
},
2: {
A: { desc: 'Второй закон: F = ma. Прикладывай силу и следи за ускорением и скоростью.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить' },
B: { desc: 'Два тела, разные массы — одинаковая сила. Сравни ускорения!', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить' },
C: { desc: 'Второй закон: изменяй силу и массу ползунками, наблюдай в реальном времени.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Запустить' },
},
3: {
A: { desc: 'Третий закон: пушка выстрелила — отдача. Импульс сохраняется!', action: 'Выстрел' },
B: { desc: 'Третий закон: два шара сталкиваются — силы равны и противоположны.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Столкнуть' },
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 ? '<svg class="ic" viewBox="0 0 24 24"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg> Нить' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Действие');
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 =>
`<button class="proj-preset-chip" onclick="newtonPreset('${p.fn}')">${p.label}</button>`
).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 `<button class="${cls}" onclick="chemSandAdd('${k}')" title="${s.name}${removeHint}" style="font-size:.68rem;padding:4px 7px">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${s.color};margin-right:3px;vertical-align:middle"></span>${sf}${active ? ' ×' : ''}</button>`;
}).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 ? '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Авто'; }
_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) =>
`<div class="cd-phase-dot${i === activeIdx ? ' active' : ''}" onclick="cdJumpPhase(${i})" title="${p.label}"></div>`
).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 ? '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Авто';
}
function 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 ── */
let 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 ── */
let 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 ── */
let 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 ── */
let 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 ── */
let 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 ── */
let 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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
} else {
wavesSim.pause();
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
}
}
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) ── */
let 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) ── */
let 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 ── */
let 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 section/unfold/sphere buttons
document.getElementById('sect-toggle').classList.remove('active');
document.getElementById('stereo-unfold-btn').classList.remove('active');
document.getElementById('stereo-measure-btn').classList.remove('active');
document.getElementById('stereo-inscribed-btn').classList.remove('active');
document.getElementById('stereo-circumscribed-btn').classList.remove('active');
document.getElementById('stereo-height-btn').classList.remove('active');
document.getElementById('stereo-apothem-btn').classList.remove('active');
document.getElementById('stereo-diag-btn').classList.remove('active');
document.getElementById('stereo-mid-btn').classList.remove('active');
_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);
}
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);
}
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) =>
'<div style="color:' + (colors[i % colors.length]) + '">' + s + '</div>'
).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: кристалл <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> жидкость (плавление) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> газ (испарение). Обратно — конденсация, кристаллизация.' },
{ 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: 'Реакция идёт до конца, если образуется: осадок (<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>), газ (<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>) или слабый электролит (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₁ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> S (репликация ДНК) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> G₂ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> M (митоз). Интерфаза = G₁ + S + G₂ — подготовка к делению.' },
{ head: 'Митоз', text: 'Профаза <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Метафаза <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Анафаза <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Телофаза. Результат: 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Ф <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> глюкоза.' },
{ head: 'Клеточное дыхание', formula: 'C_6H_{12}O_6 + 6O_2 \\to 6CO_2 + 6H_2O + 38\\text{АТФ}' },
{ head: 'Гликолиз', text: 'Цитоплазма. Глюкоза <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 2 пирувата + 2 АТФ + 2 НАДН. Анаэробный процесс.' },
{ head: 'Цикл Кребса', text: 'Матрикс митохондрий. Ацетил-КоА <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> CO₂ + НАДН + ФАДН₂ + ГТФ.' },
{ head: 'Окислительное фосфорилирование', text: 'Электрон-транспортная цепь на внутренней мембране митохондрий. Основной выход АТФ (~34).' },
]
},
chemsandbox: {
title: 'Химическая песочница',
sections: [
{ head: 'Реакция нейтрализации', formula: '\\text{Кислота} + \\text{Основание} \\to \\text{Соль} + H_2O', text: 'Экзотермическая реакция. pH раствора стремится к 7.' },
{ head: 'Осадок (<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>)', text: 'Нерастворимое вещество выпадает из раствора. Правила растворимости: все нитраты растворимы, хлориды — кроме AgCl, PbCl₂.' },
{ head: 'Газовыделение (<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>)', text: 'Признак реакции: карбонаты + кислота <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> CO₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>, активные металлы + кислота <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>.' },
{ head: 'Ряд активности металлов', text: 'Li K Ca Na Mg Al Zn Fe Ni Sn Pb (H₂) Cu Hg Ag Pt Au. Металл вытесняет из раствора все металлы правее него.' },
{ head: 'Индикаторы', text: 'Фенолфталеин: бесцветный <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> малиновый в щёлочи. Лакмус: красный в кислоте, синий в щёлочи.' },
]
},
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: 'Фенолфталеин: бесцветный <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> малиновый (pH 8.2–10). Метилоранж: красный <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> жёлтый (pH 3.1–4.4). Лакмус: красный <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> синий (pH 58).' },
]
},
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: 'Показатель преломления зависит от длины волны. Фиолетовый свет преломляется сильнее красного <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> разложение белого света в спектр.' },
]
},
probability: {
title: 'Теория вероятностей',
sections: [
{ head: 'Вероятность', formula: 'P(A) = \\frac{m}{n}', text: 'm — число благоприятных исходов, n — общее число равновозможных исходов.' },
{ head: 'Закон больших чисел', text: 'При большом числе испытаний частота события стремится к его вероятности: f(A) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> P(A) при n <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ∞.' },
{ 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 <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ∞ — ионизация (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⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Cu. 2H⁺ + 2e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>.' },
{ head: 'Анод (+)', text: 'Окисление: анионы отдают электроны. 2Cl⁻ 2e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Cl₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>. 2H₂O 4e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> O₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg> + 4H⁺.' },
{ head: 'Электролит NaCl', text: 'Катод: 2H₂O + 2e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> H₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg> + 2OH⁻. Анод: 2Cl⁻ 2e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Cl₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>.' },
{ head: 'Электролит CuSO₄', text: 'Катод: Cu²⁺ + 2e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> Cu<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg> (осадок). Анод: 2H₂O 4e⁻ <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> O₂<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg> + 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
══════════════════════════════════════════════ */
let 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(`<span style="color:#FFD166">${info.formula}</span>`);
if (info.liqName) lines.push(`Жидкость: ${info.liqName}${info.rho ? ' (ρ=' + info.rho + ')' : ''}`);
if (info.matName) lines.push(`Материал: ${info.matName}`);
if (info.FA) lines.push(`<span style="color:#06D6E0">F_A = ${info.FA} Н</span>`);
if (info.mg) lines.push(`<span style="color:#F15BB5">mg = ${info.mg} Н</span>`);
if (info.sigma) lines.push(`σ = ${info.sigma} Н/м, θ = ${info.theta}°`);
if (info.h && !info.FA) lines.push(`h_подъём = ${info.h} мм`);
el.innerHTML = lines.join('<br>');
// 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';
}
}
let _theoryOpen = false;
function toggleTheory() {
_theoryOpen = !_theoryOpen;
document.getElementById('theory-panel').classList.toggle('open', _theoryOpen);
const btn = document.getElementById('theory-toggle');
btn.style.background = _theoryOpen ? 'rgba(155,93,229,0.15)' : '';
btn.style.borderColor = _theoryOpen ? '#9B5DE5' : '';
btn.style.color = _theoryOpen ? '#9B5DE5' : '';
}
function loadTheory(simId) {
const t = THEORY[simId];
const el = document.getElementById('theory-content');
if (!t) { el.innerHTML = '<div class="tp-text" style="text-align:center;padding:40px 0;color:var(--text-3)">Теория для этой симуляции пока не добавлена</div>'; return; }
let html = `<div class="tp-title">${LS.icon('book-open',16)} ${t.title}</div>`;
for (const s of t.sections) {
html += '<div class="tp-section">';
if (s.head) html += `<div class="tp-section-head">${s.head}</div>`;
if (s.formula) html += `<div class="tp-formula" data-formula="${s.formula.replace(/"/g,'&quot;')}"></div>`;
if (s.text) html += `<div class="tp-text">${s.text}</div>`;
if (s.vars) html += `<div class="tp-var-list">${s.vars.map(([v,d]) => `<div class="tp-var"><b>${v}</b> — ${d}</div>`).join('')}</div>`;
html += '</div>';
}
el.innerHTML = html;
// render KaTeX formulas
el.querySelectorAll('.tp-formula[data-formula]').forEach(div => {
try { katex.render(div.dataset.formula, div, { displayMode: true, throwOnError: false }); }
catch(e) { div.textContent = div.dataset.formula; }
});
}
/* ── embed mode + auto-open from ?sim= ── */
const _qp = new URLSearchParams(location.search);
const _embedMode = _qp.get('embed') === '1';
const _autoSim = _qp.get('sim');
/* ── Sim state relay (embed mode only) ──────────────────────────────── */
// Map simId → { getState, applyState } registered by openSim handlers
const _simStateRegistry = {};
function _registerSimState(simId, getState, applyState) {
_simStateRegistry[simId] = { getState, applyState };
}
let _lastEmittedState = null;
let _stateEmitInterval = null;
function _startStateEmit(simId) {
if (_stateEmitInterval) clearInterval(_stateEmitInterval);
_lastEmittedState = null;
_stateEmitInterval = setInterval(() => {
const reg = _simStateRegistry[simId];
if (!reg) return;
try {
const state = reg.getState();
const json = JSON.stringify(state);
if (json === _lastEmittedState) return;
_lastEmittedState = json;
window.parent.postMessage({ type: 'sim_state', simId, state }, '*');
} catch {}
}, 400);
}
function _stopStateEmit() {
if (_stateEmitInterval) { clearInterval(_stateEmitInterval); _stateEmitInterval = null; }
_lastEmittedState = null;
}
// Receive apply_sim_state from parent (students)
window.addEventListener('message', e => {
if (!_embedMode) return;
const d = e.data;
if (!d || d.type !== 'apply_sim_state') return;
const reg = _simStateRegistry[_autoSim];
if (!reg) return;
try {
reg.applyState(d.state);
_lastEmittedState = JSON.stringify(d.state); // suppress echo
} catch {}
});
if (_embedMode) {
document.querySelector('.sidebar').style.display = 'none';
document.querySelector('.sb-content').style.marginLeft = '0';
document.querySelector('.app-layout').classList.add('embed-mode');
document.getElementById('lab-home').style.display = 'none';
document.getElementById('theory-toggle').style.display = 'none';
if (_autoSim) {
document.getElementById('lab-sim').classList.add('open');
document.querySelector('.sim-topbar').style.display = 'none';
// defer until all external scripts are loaded
window.addEventListener('load', () => openSim(_autoSim));
}
} else {
/* init — fetch sim settings + permissions in parallel, then render */
const _permFetch = (!isTeacher && !isAdmin)
? LS.api('/api/permissions/me').catch(() => null)
: Promise.resolve(null);
Promise.all([
LS.api('/api/settings/sims').catch(() => ({})),
_permFetch,
]).then(([cfg, permData]) => {
_simModuleDisabled = cfg.module_disabled || false;
_disabledSimIds = new Set(cfg.disabled_ids || []);
// check simulations.access for students
if (!isTeacher && !isAdmin && permData) {
const p = permData.permissions?.find(p => p.key === 'simulations.access');
if (p && p.effective === false) {
document.getElementById('sim-grid').innerHTML =
`<div style="grid-column:1/-1;padding:60px 0;text-align:center;color:#8898AA">
<div style="font-size:2rem;margin-bottom:12px"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div style="font-family:'Unbounded',sans-serif;font-size:1rem;font-weight:800;color:#0F172A;margin-bottom:6px">Доступ к симуляциям закрыт</div>
<div style="font-size:.88rem">Администратор ограничил доступ к лаборатории</div>
</div>`;
return;
}
// store quiz permission for later use
const qp = permData.permissions?.find(p => p.key === 'simulations.quiz');
window._simQuizAllowed = !qp || qp.effective !== false;
} else {
window._simQuizAllowed = true;
}
if (_simModuleDisabled) {
document.getElementById('sim-grid').innerHTML =
`<div style="grid-column:1/-1;padding:60px 0;text-align:center;color:#8898AA">
<div style="font-size:2rem;margin-bottom:12px"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div style="font-family:'Unbounded',sans-serif;font-size:1rem;font-weight:800;color:#0F172A;margin-bottom:6px">Модуль симуляций отключён</div>
<div style="font-size:.88rem">Администратор временно отключил лабораторию</div>
</div>`;
} else {
renderSims();
if (_autoSim) openSim(_autoSim);
}
});
lucide.createIcons();
LS.notif.init();
}
</script>
<script src="/js/labs/newton.js"></script>
<script src="/js/labs/forcesandbox.js"></script>
<script src="/js/labs/angrybirds.js"></script>
<script src="/js/labs/waves.js"></script>
<script src="/js/labs/chemsandbox.js"></script>
<script src="/js/labs/celldivision.js"></script>
<script src="/js/labs/photosynthesis.js"></script>
<script src="/js/labs/crystal.js"></script>
<script src="/js/labs/orbitals.js"></script>
<script src="/js/labs/trigcircle.js"></script>
<script src="/js/labs/_util.js"></script>
<script src="/js/labs/quadratic.js"></script>
<script src="/js/labs/normaldist.js"></script>
<script src="/js/labs/graphtransform.js"></script>
<script src="/js/labs/pendulum.js"></script>
<script src="/js/labs/equilibrium.js"></script>
<script src="/js/labs/thinlens.js"></script>
<script src="/js/labs/mirror.js"></script>
<script src="/js/labs/isoprocess.js"></script>
<script src="/js/labs/titration.js"></script>
<script src="/js/labs/refraction.js"></script>
<script src="/js/labs/probability.js"></script>
<script src="/js/labs/bohratom.js"></script>
<script src="/js/labs/electrolysis.js"></script>
<script src="/js/labs/hydrostatics.js"></script>
<script src="/js/labs/geometry.js"></script>
</body>
</html>