Files
Maxim Dolgolyov eca68e1a28 feat(labs): Фаза2 — измерительные инструменты (линейка + угломер)
LabMeasure (_measure.js): SVG-оверлей поверх сцены с pointer-events:none
(симуляция остаётся интерактивной), перетаскиваемые ручки. Линейка — длина
px + ≈ метры (PX_PER_M) + угол; угломер — угол при вершине с дугой.
Кнопка-тумблер в топбаре лаборатории. Самодостаточно, симуляции не трогает.
Этим Фаза 2 закрыта.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:13:41 +03:00

539 lines
42 KiB
HTML
Raw Permalink 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" />
<link rel="stylesheet" href="/css/lab.css" />
<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:var(--violet);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>
<!-- emfield controls -->
<div id="ctrl-emfield" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="em-sign-pos" onclick="emSign(1)" title="Добавлять + заряды" style="font-size:1.1rem;font-weight:900;color:#EF476F">+</button>
<button class="zoom-btn" id="em-sign-neg" onclick="emSign(-1)" title="Добавлять − заряды" style="font-size:1.1rem;font-weight:900;color:#4CC9F0"></button>
<button class="zoom-btn" id="em-dir-out" onclick="emWireDir('out')" title="Провод • (ток на нас)" style="font-size:1rem"></button>
<button class="zoom-btn" id="em-dir-in" onclick="emWireDir('in')" title="Провод × (ток от нас)" style="font-size:1rem">×</button>
<button class="zoom-btn" onclick="emSim && emSim.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="var _as=_activeSim&&_activeSim();if(_as)_as.reset();_collSyncBtn();" 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 merged into ctrl-emfield) -->
<!-- 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-inductor" onclick="circTool('inductor',this)" title="Катушка индуктивности (I)" style="font-size:.65rem"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 12 Q4 8 6 12 Q8 8 10 12 Q12 8 14 12"/><line x1="14" y1="12" x2="22" y2="12"/><line x1="2" y1="12" x2="2" y2="12"/></svg></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"><svg class="ic" viewBox="0 0 24 24"><line x1="3" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="21" y2="12"/><line x1="9" y1="12" x2="17" y2="6"/></svg></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 style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 2px"></div>
<button class="zoom-btn" id="ctool-heat" onclick="circToggleHeat()" title="Тепловая карта мощности" style="font-size:.6rem;font-weight:700">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 2C8 6 6 10 8 14c1 2 3 4 4 6"/><path d="M16 6c-2 3-3 6-1 9 1 1.5 1 3 0 4"/><path d="M8 6C6 9 5 12 7 15"/></svg>
</button>
<button class="zoom-btn" id="btn-osc-toggle" onclick="circToggleOsc()" title="Осциллограф U(t) / I(t)" style="font-size:.6rem;font-weight:700">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="6 16 8 10 11 14 13 8 16 16"/></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"><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="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:.78rem;font-weight:700"><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:.78rem;font-weight:700"><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:.78rem;font-weight:700"><svg class="ic" viewBox="0 0 24 24" fill="none"><path d="M3 12 L6 8 L9 16 L12 8 L15 16 L18 8 L21 12"/></svg> Пружина</button>
<button class="zoom-btn sb-tool-btn" id="sbt-rope" onclick="sbTool('rope',this)" style="font-size:.78rem;font-weight:700">— Нить</button>
<button class="zoom-btn sb-tool-btn" id="sbt-anchor" onclick="sbTool('anchor',this)" style="font-size:.78rem;font-weight:700"><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:.78rem;font-weight:700"><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:.82rem;font-weight:700;padding:4px 12px">A</button>
<button class="zoom-btn nscene-btn" id="nscn-B" onclick="newtonScene('B',this)" style="font-size:.82rem;font-weight:700;padding:4px 12px">B</button>
<button class="zoom-btn nscene-btn" id="nscn-C" onclick="newtonScene('C',this)" style="font-size:.82rem;font-weight:700;padding:4px 12px">C</button>
<!-- classic scenes (law 4, hidden by default) -->
<button class="zoom-btn nscene-btn cl-scene-btn" id="nscn-cl-atwood" data-scene="atwood" onclick="classicScene('atwood')" style="display:none;font-size:.78rem;font-weight:700">Атвуд</button>
<button class="zoom-btn nscene-btn cl-scene-btn" id="nscn-cl-ramp" data-scene="ramp" onclick="classicScene('ramp')" style="display:none;font-size:.78rem;font-weight:700">Наклон</button>
<button class="zoom-btn nscene-btn cl-scene-btn" id="nscn-cl-roll" data-scene="roll" onclick="classicScene('roll')" style="display:none;font-size:.78rem;font-weight:700">Качение</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:.78rem;font-weight:700;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:var(--violet)" 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:var(--violet);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>
<!-- radioactive controls -->
<div id="ctrl-radioactive" class="sim-zoom-btns" style="display:none">
<button class="zoom-btn" id="rd-ctrl-play" onclick="radioactivePlay()" title="Старт / Пауза">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polygon points="5,3 19,12 5,21"/></svg>
</button>
<button class="zoom-btn" onclick="radioactiveReset()" 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>
<!-- 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>
<!-- save screenshot to «Мои материалы» -->
<button class="zoom-btn" id="lab-save-btn" onclick="labSaveToMaterials(this)" title="Сохранить кадр в «Мои материалы»">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
</button>
<!-- download PNG -->
<button class="zoom-btn" id="lab-png-btn" onclick="labDownloadPng()" title="Скачать кадр (PNG)">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
<!-- measurement tools (ruler / angle) -->
<button class="zoom-btn" id="lab-measure-btn" onclick="window.LabMeasure&&LabMeasure.toggle()" title="Измерения: линейка и угломер">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.3 8.7 8.7 21.3a1 1 0 0 1-1.4 0l-4.6-4.6a1 1 0 0 1 0-1.4L15.3 2.7a1 1 0 0 1 1.4 0l4.6 4.6a1 1 0 0 1 0 1.4Z"/><path d="m7.5 10.5 2 2M10.5 7.5l2 2M13.5 4.5l2 2M4.5 13.5l2 2"/></svg>
</button>
<!-- sound toggle -->
<button class="zoom-btn" id="labfx-sound-btn" onclick="(function(){var e=window.LabFX&&window.LabFX.sound;if(!e)return;e.setEnabled(!e.isEnabled());document.getElementById('labfx-sound-btn').setAttribute('aria-pressed',e.isEnabled());document.getElementById('labfx-sound-icon-on').style.display=e.isEnabled()?'':'none';document.getElementById('labfx-sound-icon-off').style.display=e.isEnabled()?'none':'';})()" title="Звук симуляций" style="position:relative" aria-pressed="true">
<!-- speaker on -->
<svg id="labfx-sound-icon-on" class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
</svg>
<!-- speaker off (muted) -->
<svg id="labfx-sound-icon-off" class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<line x1="23" y1="9" x2="17" y2="15"/>
<line x1="17" y1="9" x2="23" y2="15"/>
</svg>
</button>
<!-- economy / reduced-motion toggle -->
<button class="zoom-btn" id="labfx-eco-btn" onclick="(function(){var f=window.LabFX;if(!f)return;f.setEconomy(!f.reduced);var on=!!f.reduced;document.getElementById('labfx-eco-btn').setAttribute('aria-pressed',on);document.getElementById('labfx-eco-on').style.display=on?'':'none';document.getElementById('labfx-eco-off').style.display=on?'none':'';})()" title="Эконом-режим: меньше анимаций (для слабых устройств)" style="position:relative" aria-pressed="false">
<!-- economy ON (effects reduced) — leaf -->
<svg id="labfx-eco-on" class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/>
<path d="M2 21c0-3 1.85-5.36 5.08-6"/>
</svg>
<!-- economy OFF (full effects) — zap -->
<svg id="labfx-eco-off" class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>
</button>
</div>
<!-- Sim bodies вынесены в /labs-bodies.html (content-engine, Phase 2).
Синхронная инъекция во время парсинга: тела присутствуют до DOMContentLoaded,
что сохраняет обработчики (geometry.js) и порядок инициализации. -->
<div id="sim-bodies-host"></div>
<script>
(function () {
var host = document.getElementById("sim-bodies-host");
if (!host) return;
try {
var x = new XMLHttpRequest();
x.open("GET", "/labs-bodies.html?v=1", false);
x.send(null);
if (x.status >= 200 && x.status < 300) {
host.insertAdjacentHTML("beforebegin", x.responseText);
host.parentNode.removeChild(host);
} else {
if (window.console) console.error("[lab] sim bodies HTTP " + x.status);
}
} catch (e) {
if (window.console) console.error("[lab] sim bodies load failed", e);
}
})();
</script>
<!-- ── 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/material-save.js"></script>
<script src="/js/sidebar.js"></script>
<!-- ════════════════════════════════════════════════════════════════════════
Контент-движок, Фаза 3 — ЛЕНИВАЯ ЗАГРУЗКА КОДА СИМУЛЯЦИЙ.
На старте грузится только КАРКАС (~360 КБ): реестр, загрузчик, манифест,
fx-движки, общие визуалы (_phys_visuals/_chem_visuals/_graph_panel/_util),
graph.js (предоставляет GRID для 15 симуляций), lab-init/glue/register-all.
Код конкретной симуляции (~2.5 МБ суммарно) и three.js (~600 КБ) грузятся
по клику через LabLoader (см. _loader.js + _sim_deps.js). three.js — только
для 3D-симуляций (crystal/orbitals/stereo/periodic).
════════════════════════════════════════════════════════════════════════ -->
<script src="/js/labs/_registry.js"></script>
<script src="/js/labs/_loader.js"></script>
<script src="/js/labs/_sim_deps.js"></script>
<script src="/js/labs/_palette.js"></script>
<script src="/js/labs/_simbase.js"></script>
<script src="/js/labs/_fx_core.js"></script>
<script src="/js/labs/_fx_particles.js"></script>
<script src="/js/labs/_fx_motion.js"></script>
<script src="/js/labs/_fx_sound.js"></script>
<script src="/js/labs/_graph_panel.js"></script>
<!-- Конструктор симуляций (Фаза 0): движок выражений + рантайм + адаптер LabRegistry.
Лёгкие модули каркаса (~30 КБ), грузятся eager как _registry/_loader. -->
<script src="/js/labs/_sim_expr.js"></script>
<script src="/js/labs/_sim_engine.js"></script>
<script src="/js/labs/_sim_adapter.js"></script>
<script src="/js/labs/_tasks.js"></script>
<script src="/js/labs/_measure.js"></script>
<script src="/js/labs/_phys_visuals.js"></script>
<script src="/js/labs/_chem_visuals.js"></script>
<script src="/js/labs/graph.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
<script src="/js/labs/lab-init.js"></script>
<script src="/js/lab-previews.js"></script>
<script src="/js/labs/lab-glue.js"></script>
<script src="/js/labs/_register-all.js"></script>
<!-- Конструктор симуляций (Фаза 0): демо-спека за флагом (?simdemo=1). Грузится
после _register-all, чтобы LabRegistry/registerSpecSim уже существовали. -->
<script src="/js/labs/_sim_demo.js"></script>
<script>
/* Sync sound toggle button icon with localStorage state on load */
(function() {
var stored = localStorage.getItem('labfx-sound');
var on = stored === null ? true : stored === 'true';
var iconOn = document.getElementById('labfx-sound-icon-on');
var iconOff = document.getElementById('labfx-sound-icon-off');
var btn = document.getElementById('labfx-sound-btn');
if (!iconOn || !iconOff || !btn) return;
iconOn.style.display = on ? '' : 'none';
iconOff.style.display = on ? 'none' : '';
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
})();
/* Sync economy-mode toggle with LabFX.reduced (prefers-reduced-motion + localStorage) */
(function() {
var eco = !!(window.LabFX && window.LabFX.reduced);
var on = document.getElementById('labfx-eco-on');
var off = document.getElementById('labfx-eco-off');
var btn = document.getElementById('labfx-eco-btn');
if (!on || !off || !btn) return;
on.style.display = eco ? '' : 'none';
off.style.display = eco ? 'none' : '';
btn.setAttribute('aria-pressed', eco ? 'true' : 'false');
})();
/* ── Снимок симуляции: «В мои материалы» / «Скачать PNG» (Фаза 2) ──
Берём самый крупный видимый canvas в области симуляции. Для 3D (WebGL)
кадр может выйти пустым без preserveDrawingBuffer — допустимо для v1. */
function _labSimTitle() {
var t = document.getElementById('sim-topbar-title');
return t ? (t.textContent || '').trim() : '';
}
function _labActiveCanvas() {
var best = null, bestArea = 0;
document.querySelectorAll('canvas').forEach(function (c) {
if (c.offsetParent === null) return; // скрытый
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 60) return; // мелкие (иконки/спарклайны)
var area = r.width * r.height;
if (area > bestArea) { bestArea = area; best = c; }
});
return best;
}
function labDownloadPng() {
var c = _labActiveCanvas();
if (!c) { if (window.LS && LS.toast) LS.toast('Нет изображения', 'warn'); return; }
try {
var a = document.createElement('a');
a.href = c.toDataURL('image/png');
a.download = (_labSimTitle() || 'simulation') + '.png';
document.body.appendChild(a); a.click(); a.remove();
} catch (e) { if (window.LS && LS.toast) LS.toast('Не удалось сохранить кадр', 'error'); }
}
function labSaveToMaterials(btn) {
var c = _labActiveCanvas();
if (!c) { if (window.LS && LS.toast) LS.toast('Нет изображения', 'warn'); return; }
if (!window.MaterialSave) { if (window.LS && LS.toast) LS.toast('Модуль сохранения не загружен', 'error'); return; }
try {
c.toBlob(function (blob) {
if (!blob) { if (window.LS && LS.toast) LS.toast('Не удалось снять кадр', 'error'); return; }
MaterialSave.image({ blob: blob, title: _labSimTitle() || 'Симуляция', name: 'sim.png', sourceTitle: 'Лаборатория' }, btn);
}, 'image/png');
} catch (e) { if (window.LS && LS.toast) LS.toast('Не удалось снять кадр', 'error'); }
}
</script>
</body>
</html>