feat(labs): универсальные инструменты для физических симуляций (Раунд 2)

ФУНДАМЕНТ — frontend/js/labs/_phys_visuals.js (новый файл, 871 строк):
- LSPhysFX.FORCE_COLORS: стандарт 10 цветов (mg красный, N циан, F_тр оранжевый,
  T фиолетовый, F_упр зелёный, F_сопр серый, F_прил жёлтый, импульс, v, a)
- drawVector / drawForceArrow / drawSpring / drawRope / drawSurface
- drawCoordSystem / drawScale / drawClock / drawAngleArc / drawPivot
- drawEnergyBars (KE/PE/W_тр/E_упр стеком с авто-масштабированием)
- LSTimeControl class (pause/play/0.1×-5×/scrubber для одного DOF)
- LSMotionTrail class (gradient line с alpha fade)
- LSBuildTimeControlUI helper для DOM-UI бара

ГРАФИКИ — frontend/js/labs/_graph_panel.js (новый файл, 415 строк):
- LSGraphPanel: 3 stacked time-series плота, авто-scale, freeze/export PNG
- LSGraphPanelUI: overlay-виджет 320×248 в углу canvas, body-selector,
  кнопки Сброс/Стоп/PNG download

FBD (свободные силовые диаграммы) интегрированы в:
- projectile.js: mg + drag + wind + elastic (bounce)
- pendulum.js: mg + T (math), mg + F_упр (spring vert/horiz)
- collision.js: стрелки скорости каждого шара + flash импульса
- newton.js: все сцены законов I-III + новые Атвуд/наклон/скатывание
- forcesandbox.js: gravity/N/friction/spring/applied на каждом теле

ENERGY BARS интегрированы в 5 сим с расчётами:
- projectile: ΔE_drag = F_d·v·dt (cumulative)
- pendulum: для math/spring/double/physical с учётом γ-затухания
- collision: KE loss при каждом столкновении
- newton: все 8 сцен включая Атвуд + наклон + скатывание (с моментом инерции)
- forcesandbox: + E_упр от пружин

GRAPHS PANEL — в 5 сим:
- pendulum: θ/ω/E (режим-aware)
- collision: |v₁|, |v₂|, v_цм
- newton: x/v/a (зависит от закона)
- forcesandbox: x/|v|/|a| выбранного тела
- hydrostatics: depth/vy/submergedFrac (только Архимед)

TIME CONTROL + MOTION TRAILS в 5 сим:
- pendulum (полный scrubber для math), collision, newton, forcesandbox (speed+pause)
- projectile (layered speed+pause, свой trail сохранён)
- LSMotionTrail на bob/балах/блоках с alpha gradient

Заменено рисование пружин на LSPhysFX.drawSpring везде.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-26 14:37:48 +03:00
parent e46548d06b
commit 7a323f8fe0
10 changed files with 2536 additions and 39 deletions
+58 -1
View File
@@ -1624,6 +1624,18 @@
</button>
</div>
<!-- Newton graphs toggle -->
<div style="margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.06)">
<button id="btn-newton-graphs" onclick="newtonToggleGraphs()" style="width:100%;font-size:.75rem;padding:5px;border-radius:8px;border:1px solid rgba(6,214,224,0.3);background:rgba(6,214,224,0.08);color:#06D6E0;cursor:pointer">&#1043;&#1088;&#1072;&#1092;&#1080;&#1082;&#1080; x/v/a</button>
</div>
<!-- Energy bars toggle — newton -->
<div class="gp-section-title" style="margin-top:10px">&#1047;&#1072;&#1082;&#1086;&#1085; &#1089;&#1086;&#1093;&#1088;&#1072;&#1085;&#1077;&#1085;&#1080;&#1103; &#1101;&#1085;&#1077;&#1088;&#1075;&#1080;&#1080;</div>
<button id="nwt-energy-btn" class="proj-preset-chip" onclick="dynToggleEnergy()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px">
<svg class="ic" viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>&#1069;&#1085;&#1077;&#1088;&#1075;&#1080;&#1103;: &#1042;&#1099;&#1082;&#1083;</span>
</button>
</div><!-- /#dyn-newton-panel -->
<!-- ══ Sandbox controls (shown in sandbox mode) ══ -->
@@ -1735,6 +1747,17 @@
Пружина / Нить — кликни 2 тела
</div>
<!-- Sandbox graphs toggle -->
<div style="margin-top:8px">
<button id="btn-sandbox-graphs" onclick="sandboxToggleGraphs()" style="width:100%;font-size:.75rem;padding:5px;border-radius:8px;border:1px solid rgba(6,214,224,0.3);background:rgba(6,214,224,0.08);color:#06D6E0;cursor:pointer">&#1043;&#1088;&#1072;&#1092;&#1080;&#1082;&#1080; &#1090;&#1077;&#1083;&#1072;</button>
</div>
<!-- Energy bars toggle — sandbox -->
<button id="fsb-energy-btn" class="proj-preset-chip" onclick="fsbToggleEnergy()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px;margin-top:6px">
<svg class="ic" viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>&#1069;&#1085;&#1077;&#1088;&#1075;&#1080;&#1103;+: &#1042;&#1099;&#1082;&#1083;</span>
</button>
</div><!-- /#dyn-sandbox-panel -->
</div><!-- /.proj-panel -->
@@ -2001,6 +2024,13 @@
<div id="proj-target-hud" style="font-size:.72rem;font-weight:700;color:#FFD166;margin-top:6px;text-align:center">Цели: 0/3 Попыток: 0</div>
</div>
<!-- Energy bars toggle -->
<div class="gp-section-title" style="margin-top:10px">Закон сохранения энергии</div>
<button id="proj-energy-btn" class="proj-preset-chip" onclick="projToggleEnergy()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px">
<svg class="ic" viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>Энергия: Выкл</span>
</button>
<!-- Feature 2: Graphs toggle -->
<div class="gp-section-title" style="margin-top:10px">Графики</div>
<button id="proj-graphs-btn" class="proj-preset-chip" onclick="projToggleGraphs()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px">
@@ -2189,6 +2219,18 @@
<button class="proj-preset-chip" onclick="collPreset(6,6,10,10,0,0)">Слипание</button>
</div>
<!-- graphs toggle -->
<div style="margin-top:10px">
<button id="btn-coll-graphs" onclick="collToggleGraphs()" style="width:100%;font-size:.75rem;padding:5px;border-radius:8px;border:1px solid rgba(6,214,224,0.3);background:rgba(6,214,224,0.08);color:#06D6E0;cursor:pointer">&#1043;&#1088;&#1072;&#1092;&#1080;&#1082;&#1080; &#1089;&#1082;&#1086;&#1088;&#1086;&#1089;&#1090;&#1077;&#1081;</button>
</div>
<!-- Energy bars toggle -->
<div class="gp-section-title" style="margin-top:10px">&#1047;&#1072;&#1082;&#1086;&#1085; &#1089;&#1086;&#1093;&#1088;&#1072;&#1085;&#1077;&#1085;&#1080;&#1103; &#1101;&#1085;&#1077;&#1088;&#1075;&#1080;&#1080;</div>
<button id="coll-energy-btn" class="proj-preset-chip" onclick="collToggleEnergy()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px">
<svg class="ic" viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>&#1069;&#1085;&#1077;&#1088;&#1075;&#1080;&#1103;: &#1042;&#1099;&#1082;&#1083;</span>
</button>
<!-- 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()">
@@ -2634,6 +2676,7 @@
<button class="pend-mode-btn" data-mode="foucault" onclick="pendSetMode('foucault')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">&#1060;&#1091;&#1082;&#1086;</button>
<button class="pend-mode-btn" data-mode="resonance" onclick="pendSetMode('resonance')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">&#1056;&#1077;&#1079;&#1086;&#1085;&#1072;&#1085;&#1089;</button>
<button id="btn-pend-phase" onclick="pendTogglePhase()" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer;margin-left:auto">&#1060;&#1072;&#1079;. &#1087;&#1086;&#1088;&#1090;&#1088;&#1077;&#1090;</button>
<button id="btn-pend-graphs" onclick="pendToggleGraphs()" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(6,214,224,0.3);background:rgba(6,214,224,0.08);color:#06D6E0;cursor:pointer">&#1043;&#1088;&#1072;&#1092;&#1080;&#1082;&#1080;</button>
</div>
<div class="sim-body-wrap">
<div class="proj-panel" style="width:230px;gap:0;overflow-y:auto;max-height:100%">
@@ -2795,6 +2838,13 @@
<div class="pp-hint">&#1050;&#1088;&#1072;&#1089;&#1085;&#1072;&#1103; &#1089;&#1090;&#1088;&#1077;&#1083;&#1082;&#1072;&#1074;&#1099;&#1085;&#1091;&#1078;&#1076;&#1072;&#1102;&#1097;&#1072;&#1103; &#1089;&#1080;&#1083;&#1072;. &#1055;&#1080;&#1082; &#1087;&#1088;&#1080; &#969; &#8776; &#969;&#8320;.</div>
</div>
<!-- Energy bars toggle (shared across all pendulum modes) -->
<div class="gp-section-title" style="margin-top:10px">&#1047;&#1072;&#1082;&#1086;&#1085; &#1089;&#1086;&#1093;&#1088;&#1072;&#1085;&#1077;&#1085;&#1080;&#1103; &#1101;&#1085;&#1077;&#1088;&#1075;&#1080;&#1080;</div>
<button id="pend-energy-btn" class="proj-preset-chip" onclick="pendToggleEnergy()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px">
<svg class="ic" viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>&#1069;&#1085;&#1077;&#1088;&#1075;&#1080;&#1103;: &#1042;&#1099;&#1082;&#1083;</span>
</button>
</div>
<div class="proj-canvas-outer">
<canvas id="pendulum-canvas"></canvas>
@@ -4052,10 +4102,15 @@
<!-- 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>
<!-- hydrostatics graphs toggle (Archimedes mode) -->
<div id="hydro-graphs-row" style="margin-top:8px;display:none">
<button id="btn-hydro-graphs" onclick="hydroToggleGraphs()" style="width:100%;font-size:.75rem;padding:5px;border-radius:8px;border:1px solid rgba(6,214,224,0.3);background:rgba(6,214,224,0.08);color:#06D6E0;cursor:pointer">&#1043;&#1088;&#1072;&#1092;&#1080;&#1082; &#1087;&#1086;&#1075;&#1088;&#1091;&#1078;&#1077;&#1085;&#1080;&#1103;</button>
</div>
</div><!-- /.proj-panel -->
<!-- canvas area -->
<div style="flex:1;min-width:0;position:relative">
<div style="flex:1;min-width:0;position:relative" id="hydro-canvas-wrap">
<canvas id="hydro-canvas" style="width:100%;height:100%;display:block"></canvas>
</div>
@@ -4612,8 +4667,10 @@
<script src="/js/labs/_fx_motion.js"></script>
<script src="/js/labs/_fx_sound.js"></script>
<script src="/js/labs/graph.js"></script>
<script src="/js/labs/_phys_visuals.js"></script>
<script src="/js/labs/emfield.js"></script>
<script src="/js/labs/triangle.js"></script>
<script src="/js/labs/_graph_panel.js"></script>
<script src="/js/labs/projectile.js"></script>
<script src="/js/labs/collision.js"></script>
<script src="/js/labs/gas.js"></script>