95cca89dfc
- GeoEngine: система производных объектов (recompute, propagateDeps), каскадная цепочка зависимостей при перемещении точек - 6 новых инструментов в GeoSim: * midpoint — середина отрезка (производная точка) * perpbisect — серединный перпендикуляр (derived_line) * anglebisect — биссектриса угла ABC (derived_line) * parallel — параллельная прямая через точку (derived_line) * perpendicular — перпендикуляр через точку (derived_line) * intersect — точка пересечения двух прямых (производная точка) - Производные объекты: пунктирный стиль, светящийся ободок, автообновление при перемещении родительских точек - Двухфазный UI для parallel/perpendicular/intersect: _pendingLineRef + _drawLineRefHighlight (подсветка первой линии) - lab.html: 6 новых кнопок в секции "Построения", счётчик построений, onHintChange callback для контекстных подсказок Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8713 lines
527 KiB
HTML
8713 lines
527 KiB
HTML
<!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-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);
|
||
}
|
||
</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')">2x−1</button>
|
||
<button class="preset-btn" onclick="applyPreset('x^2')">x²</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)')">eˣ</button>
|
||
<button class="preset-btn" onclick="applyPreset('2^x')">2ˣ</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">
|
||
Клик — добавить · ПКМ / 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)">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">
|
||
Клик — добавить · ПКМ — удалить<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">0°</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)">(x−1)²</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²−3x−2</button>
|
||
<button class="preset-btn" onclick="quadPreset(0.5,1,-3)">½x²+x−3</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)">x²</button>
|
||
<button class="preset-btn gt-base-btn" onclick="gtBase('x^3',this)">x³</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 < 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 > 2f</button>
|
||
<button class="preset-btn" onclick="mirrorPreset('2f')">d = 2f</button>
|
||
<button class="preset-btn" onclick="mirrorPreset('between')">f < d < 2f</button>
|
||
<button class="preset-btn" onclick="mirrorPreset('near')">d < 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>
|
||
</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-unfold-btn" onclick="stereoUnfold(this)">Развёртка</button>
|
||
<button class="gp-btn" id="stereo-measure-btn" onclick="stereoMeasure(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-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-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>
|
||
Координаты: наведите на вершину
|
||
</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>
|
||
</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>
|
||
|
||
</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: 'Кликни на первую прямую, затем на вторую',
|
||
};
|
||
|
||
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);
|
||
}
|
||
|
||
function _geoShowHint(name, phase2) {
|
||
const hint = document.getElementById('geo-hint');
|
||
if (!hint) return;
|
||
if (phase2) {
|
||
const phase2hints = {
|
||
parallel: 'Теперь кликни на точку — через неё проведём прямую',
|
||
perpendicular: 'Теперь кликни на точку — через неё проведём перпендикуляр',
|
||
intersect: 'Теперь кликни на вторую прямую',
|
||
};
|
||
hint.textContent = phase2hints[name] || _GEO_HINTS[name] || '';
|
||
} else {
|
||
hint.textContent = _GEO_HINTS[name] || '';
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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 > 1);
|
||
|
||
// 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','h'],
|
||
tetrahedron: ['a'],
|
||
cylinder: ['r','h'],
|
||
cone: ['r','h'],
|
||
trunccone: ['R','r','h'],
|
||
sphere: ['r'],
|
||
prism: ['a','n','h'],
|
||
};
|
||
|
||
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'].forEach(id => {
|
||
document.getElementById(id)?.classList.remove('active');
|
||
});
|
||
if (stereoSim) {
|
||
stereoSim.toggleMeasure(false);
|
||
stereoSim.togglePointMode(false);
|
||
stereoSim.toggleConnectMode(false);
|
||
stereoSim.setAngleMode(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 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: 'Кликните точку, затем — грань',
|
||
};
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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 5–8).' },
|
||
]
|
||
},
|
||
isoprocess: {
|
||
title: 'Изопроцессы',
|
||
sections: [
|
||
{ head: 'Уравнение состояния', formula: 'PV = nRT', vars: [['P','давление (Па)'],['V','объём (м³)'],['T','температура (К)'],['n','количество вещества'],['R','8.314 Дж/(моль·К)']] },
|
||
{ head: 'Изотермический (T=const)', formula: 'P_1V_1 = P_2V_2', text: 'Закон Бойля — Мариотта. ΔU = 0. Работа W = nRT·ln(V₂/V₁) = Q.' },
|
||
{ head: 'Изохорный (V=const)', formula: '\\frac{P_1}{T_1} = \\frac{P_2}{T_2}', text: 'Закон Гей-Люссака (второй). W = 0. Q = ΔU = νCᵥΔT.' },
|
||
{ head: 'Изобарный (P=const)', formula: '\\frac{V_1}{T_1} = \\frac{V_2}{T_2}', text: 'Закон Гей-Люссака (первый). W = PΔV. Q = νCpΔT = ΔU + W.' },
|
||
{ head: 'Адиабатный (Q=0)', formula: 'PV^\\gamma = \\text{const}', text: 'Показатель γ = Cp/Cv: 5/3 — одноатомный газ, 7/5 — двухатомный. Q = 0, W = −ΔU.' },
|
||
{ head: 'Начало термодинамики', formula: 'Q = \\Delta U + W', text: 'Теплота, сообщённая газу, расходуется на увеличение внутренней энергии и совершение работы.' },
|
||
]
|
||
},
|
||
mirrors: {
|
||
title: 'Зеркала',
|
||
sections: [
|
||
{ head: 'Формула зеркала', formula: '\\frac{1}{f} = \\frac{1}{d} + \\frac{1}{d\'}', vars: [['f','фокусное расстояние'],['d','расстояние от предмета до зеркала'],["d'",'расстояние до изображения']] },
|
||
{ head: 'Увеличение', formula: 'M = -\\frac{d\'}{d} = \\frac{h\'}{h}', text: 'M < 0 — перевёрнутое (действительное). |M| > 1 — увеличенное, |M| < 1 — уменьшенное.' },
|
||
{ head: 'Вогнутое зеркало (f > 0)', text: 'd > 2f: уменьшенное действительное. d = 2f: равное действительное. f < d < 2f: увеличенное действительное. d < f: увеличенное мнимое (прямое).' },
|
||
{ head: 'Выпуклое зеркало (f < 0)', text: 'Всегда даёт уменьшенное мнимое прямое изображение. Широкий угол обзора — применяется в автомобилях и видеонаблюдении.' },
|
||
{ head: 'Плоское зеркало (f = ∞)', formula: "d' = -d,\\quad M = +1", text: 'Изображение мнимое, прямое, равное предмету — расположено на таком же расстоянии за зеркалом.' },
|
||
]
|
||
},
|
||
refraction: {
|
||
title: 'Преломление света',
|
||
sections: [
|
||
{ head: 'Закон Снеллиуса', formula: 'n_1 \\sin\\theta_1 = n_2 \\sin\\theta_2', text: 'Угол преломления зависит от соотношения показателей преломления двух сред.' },
|
||
{ head: 'Показатель преломления', formula: 'n = \\frac{c}{v}', text: 'Отношение скорости света в вакууме к скорости в среде. Воздух ≈ 1, вода = 1.33, стекло ≈ 1.5, алмаз = 2.42.' },
|
||
{ head: 'Полное внутреннее отражение', formula: '\\theta_c = \\arcsin\\frac{n_2}{n_1}', text: 'Возникает при переходе из оптически более плотной среды в менее плотную (n₁ > n₂) при θ > θc.' },
|
||
{ head: 'Коэффициент отражения', formula: 'R = \\left(\\frac{n_1\\cos\\theta_1 - n_2\\cos\\theta_2}{n_1\\cos\\theta_1 + n_2\\cos\\theta_2}\\right)^2', text: 'Формула Френеля (s-поляризация). Определяет долю отражённой интенсивности.' },
|
||
{ head: 'Дисперсия', text: 'Показатель преломления зависит от длины волны. Фиолетовый свет преломляется сильнее красного <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,'"')}"></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>
|