Files
Learn_System/frontend/js/labs/newton.js
T
Maxim Dolgolyov 7a323f8fe0 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>
2026-05-26 14:37:48 +03:00

2845 lines
111 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* ════════════════════════════════════════════════════════════════
NewtonSim — три закона Ньютона
Закон I : A — скользящий блок, B — шар на нити
Закон II : A — один блок F=ma, B — сравнение масс
Закон III: A — пушка + откат, B — столкновение шаров, C — ракета
════════════════════════════════════════════════════════════════ */
class NewtonSim {
static SCALE = 58; // px per metre (visual)
static G = 9.81; // m/s²
/* ── Конструктор ─────────────────────────────────────────── */
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
/* Пользовательские параметры */
this.law = 1;
this.scene = 'A';
this.mu = 0.20;
this.mass1 = 5; // кг — основной блок / шар / ядро
this.mass2 = 12; // кг — сравниваемый блок / пушка
this.force = 30; // Н — приложенная сила (закон II)
/* Классические задачи (law 4) */
this.atwM1 = 5; // кг
this.atwM2 = 8; // кг
this.atwMassive = false; // идеальный блок по умолчанию
this.rampAlpha = 30; // градусы
this.rampMu = 0.20;
this.rampForce = 0; // Н внешней силы вдоль горки
this.rollAlpha = 20; // градусы
this.rollFriction = false;
/* Состояние сцен */
this._1A = {};
this._1B = {};
this._2 = {};
this._3A = {};
this._3B = {};
this._3C = {};
/* Классические задачи */
this._atw = {};
this._ramp = {};
this._roll = {};
/* Петля */
this._raf = null;
this._last = 0;
this._paused = false;
/* Геометрия */
this.W = 0; this.H = 0;
this._g = {};
this.onUpdate = null;
this.onModeChange = null;
/* FBD toggle */
this._fbdOn = false;
/* ── Energy bars widget ── */
this._energyOn = false;
this._frictionWork = 0; // cumulative friction heat (J)
this._appliedWork = 0; // work done by applied force (J)
this._energyScale = 0;
/* -- GraphPanel widget -- */
this._graphsOn = false;
this._graphUI = null;
this._tSim = 0;
/* ── TimeControl ── */
this._tc = window.LSTimeControl ? new window.LSTimeControl(this) : null;
this._tcScale = 1; // separate from tc.scale so we can multiply with existing dt
this.fit();
this._bindEvents();
}
/* ── Геометрия ───────────────────────────────────────────── */
fit() {
const dpr = window.devicePixelRatio || 1;
const W = this.canvas.offsetWidth || 700;
const H = this.canvas.offsetHeight || 440;
this.canvas.width = Math.round(W * dpr);
this.canvas.height = Math.round(H * dpr);
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.W = W; this.H = H;
this._g = {
gY: H * 0.73,
cx: W * 0.50,
cy: H * 0.48,
orbitR: Math.min(W, H) * 0.255,
};
this._resetAll();
}
_resetAll() {
this._reset1A(); this._reset1B();
this._reset2();
this._reset3A(); this._reset3B(); this._reset3C();
this._resetAtwood(); this._resetRamp(); this._resetRoll();
}
/* ── Сброс каждой сцены ──────────────────────────────────── */
_reset1A() {
const { W, _g: g } = this;
this._1A = {
bx: W * 0.15, by: g.gY, bvx: 0, bvy: 0,
BW: 56, BH: 46,
trail: [], inAir: false,
};
}
_reset1B() {
const { _g: g } = this;
const omega = 1.35;
this._1B = {
angle: 0, omega,
cut: false, cutTimer: 0,
bx: g.cx + g.orbitR, by: g.cy,
bvx: 0, bvy: -g.orbitR * omega,
};
}
_reset2() {
const { W, _g: g } = this;
this._2 = {
b1x: W * 0.12, b1vx: 0,
b2x: W * 0.12, b2vx: 0,
history: [], t: 0, running: false,
flash: '', // 'fin' message
};
}
_reset3A() {
const { W, _g: g } = this;
this._3A = {
cx: W * 0.38, cvx: 0,
ball: null, fired: false,
sparks: [], forceFlash: 0,
};
}
_reset3B() {
const { W, _g: g } = this;
const r1 = 16 + this.mass1 * 1.1;
const r2 = 16 + this.mass2 * 1.1;
this._3B = {
b1: { x: W * 0.18, vx: 160, mass: this.mass1, r: r1, color: '#EF476F' },
b2: { x: W * 0.82, vx: -100, mass: this.mass2, r: r2, color: '#4CC9F0' },
colFlash: 0, done: false,
};
}
_reset3C() {
const { W, H } = this;
this._3C = {
ry: H * 0.78, rvy: 0,
rmass: 10, fuel: 1,
particles: [], running: false,
stopped: false,
};
}
/* ── Запуск / остановка ──────────────────────────────────── */
start() {
if (this._raf) return;
this._last = performance.now();
const loop = t => { this._raf = requestAnimationFrame(loop); this._tick(t); };
this._raf = requestAnimationFrame(loop);
}
stop() { cancelAnimationFrame(this._raf); this._raf = null; }
/* ── Публичный API ───────────────────────────────────────── */
setLaw(n) {
this.law = n;
this.scene = (n === 4) ? 'atwood' : 'A';
this._resetAll();
if (window.LabFX) LabFX.sound.play('click');
if (this.onModeChange) this.onModeChange();
}
setScene(s) { this.scene = s; this._resetAll(); if (window.LabFX) LabFX.sound.play('click'); }
setMu(v) { this.mu = v; }
setMass1(v) { this.mass1 = v; this._reset3B(); if (this.law === 4) { this._resetRamp(); } }
setMass2(v) { this.mass2 = v; this._reset3B(); }
setForce(v) { this.force = v; }
/* ── Сеттеры для классических задач ─────────────────────────── */
setAtwM1(v) { this.atwM1 = v; this._resetAtwood(); }
setAtwM2(v) { this.atwM2 = v; this._resetAtwood(); }
setAtwMassive(v) { this.atwMassive = v; this._resetAtwood(); }
setRampAlpha(v) { this.rampAlpha = v; this._resetRamp(); }
setRampMu(v) { this.rampMu = v; this._resetRamp(); }
setRampForce(v) { this.rampForce = v; this._resetRamp(); }
setRollAlpha(v) { this.rollAlpha = v; this._resetRoll(); }
setRollFriction(v) { this.rollFriction = v; this._resetRoll(); }
startAtwood() { if (this._atw) { this._atw.running = !this._atw.running; if (window.LabFX) LabFX.sound.play('tick'); } }
startRamp() { if (this._ramp) { this._ramp.running = !this._ramp.running; if (window.LabFX) LabFX.sound.play('tick'); } }
startRoll() { if (this._roll) { this._roll.running = !this._roll.running; if (window.LabFX) LabFX.sound.play('tick'); } }
cutString() {
this._1B.cut = true;
this._1B.bvx = -Math.sin(this._1B.angle) * this._g.orbitR * this._1B.omega;
this._1B.bvy = Math.cos(this._1B.angle) * this._g.orbitR * this._1B.omega;
}
startL2() { this._2.running = true; }
resetL2() { this._reset2(); }
fireCannon() {
if (this._3A.fired) { this._reset3A(); return; }
const { _g: g } = this;
const S = NewtonSim.SCALE;
const vBall = 360; // px/s
const vCannon = -(this.mass1 / this.mass2) * vBall;
this._3A.ball = { x: this._3A.cx + 68, y: g.gY - 22, vx: vBall, vy: -160 };
this._3A.cvx = vCannon;
this._3A.fired = true;
this._3A.forceFlash = 0.55;
for (let i = 0; i < 24; i++) {
const a = (Math.random() - 0.5) * 1.1 - 0.05;
this._3A.sparks.push({
x: this._3A.cx + 68, y: g.gY - 22,
vx: Math.cos(a) * (180 + Math.random() * 220),
vy: Math.sin(a) * 140 - 80 - Math.random() * 120,
life: 1,
});
}
}
toggleRocket() {
if (this._3C.fuel <= 0) { this._reset3C(); return; }
this._3C.running = !this._3C.running;
if (this.onModeChange) this.onModeChange();
}
togglePause() { this._paused = !this._paused; }
preset(name) {
switch (name) {
case 'space': this.mu = 0; this._reset1A(); break;
case 'ice': this.mu = 0.04; this._reset1A(); break;
case 'asphalt': this.mu = 0.38; this._reset1A(); break;
case 'rubber': this.mu = 0.72; this._reset1A(); break;
case 'light': this.mass1 = 2; this.force = 20; this._reset2(); break;
case 'heavy': this.mass1 = 18; this.force = 20; this._reset2(); break;
case 'compare': this.mass1 = 2; this.mass2 = 16; this.scene = 'B'; this._reset2(); break;
case 'big_cannon': this.mass2 = 50; this.mass1 = 1; this._reset3A(); break;
case 'small_cannon': this.mass2 = 5; this.mass1 = 4; this._reset3A(); break;
case 'equal_balls': this.mass1 = 8; this.mass2 = 8; this._reset3B(); break;
}
if (this.onUpdate) this.onUpdate(this.info());
}
/* ── Тик ──────────────────────────────────────────────────── */
_tick(now) {
const rawDt = Math.min((now - this._last) / 1000, 0.05);
this._last = now;
if (window.LabFX) LabFX.particles.update(rawDt);
/* TimeControl: pause / speed scale */
let dt = rawDt;
if (this._tc) {
dt = this._tc.advance(rawDt);
if (dt === 0) { this.draw(); if (this.onUpdate) this.onUpdate(this.info()); return; }
}
if (!this._paused) {
if (this.law === 1 && this.scene === 'A') this._step1A(dt);
else if (this.law === 1) this._step1B(dt);
else if (this.law === 2) this._step2(dt);
else if (this.law === 4 && this.scene === 'atwood') this._stepAtwood(dt);
else if (this.law === 4 && this.scene === 'ramp') this._stepRamp(dt);
else if (this.law === 4 && this.scene === 'roll') this._stepRoll(dt);
else if (this.scene === 'A') this._step3A(dt);
else if (this.scene === 'B') this._step3B(dt);
else this._step3C(dt);
}
this._tSim += dt;
this.draw();
if (this.onUpdate) this.onUpdate(this.info());
if (window.LSGraphPanel && this._graphsOn && this._graphUI && !this._paused) {
this._graphUI.push(this._tSim, this._newtonGraphValues());
}
}
/* ── Физика I-A : блок с трением ────────────────────────── */
_step1A(dt) {
const b = this._1A;
const { W, _g: g } = this;
const S = NewtonSim.SCALE;
const GV = NewtonSim.G * S;
/* Гравитация */
if (b.by < g.gY || b.bvy < 0) {
b.bvy += GV * dt;
b.inAir = true;
}
/* Интеграция */
b.bx += b.bvx * dt;
b.by += b.bvy * dt;
/* Приземление */
if (b.by >= g.gY) {
b.by = g.gY;
b.bvy = Math.abs(b.bvy) > 60 ? -b.bvy * 0.42 : 0;
b.inAir = false;
}
/* Трение (только на земле) */
if (!b.inAir) {
const speed = Math.abs(b.bvx);
if (speed > 1) {
const dec = this.mu * GV * dt;
if (dec >= speed) b.bvx = 0;
else b.bvx -= Math.sign(b.bvx) * dec;
}
}
/* Стены (упругий отскок) */
const hw = b.BW / 2;
if (b.bx < hw) { b.bx = hw; b.bvx = Math.abs(b.bvx) * 0.65; }
if (b.bx > W - hw) { b.bx = W - hw; b.bvx = -Math.abs(b.bvx) * 0.65; }
/* След */
const speed = Math.hypot(b.bvx, b.bvy);
if (speed > 15) {
b.trail.push({ x: b.bx, y: Math.min(b.by, g.gY) });
if (b.trail.length > 90) b.trail.shift();
} else if (b.trail.length > 0) {
b.trail.shift();
}
}
/* ── Физика I-B : орбита → прямолинейное движение ────────── */
_step1B(dt) {
const s = this._1B;
const { _g: g } = this;
if (!s.cut) {
s.angle += s.omega * dt;
s.bx = g.cx + Math.cos(s.angle) * g.orbitR;
s.by = g.cy + Math.sin(s.angle) * g.orbitR;
s.bvx = -Math.sin(s.angle) * g.orbitR * s.omega;
s.bvy = Math.cos(s.angle) * g.orbitR * s.omega;
} else {
s.bx += s.bvx * dt;
s.by += s.bvy * dt;
s.cutTimer += dt;
if (s.cutTimer > 4.5) this._reset1B();
}
}
/* ── Физика II : F = m·a ──────────────────────────────────── */
_step2(dt) {
if (!this._2.running) return;
const { W } = this;
const S = NewtonSim.SCALE;
const a1 = (this.force / this.mass1) * S;
const a2 = (this.force / this.mass2) * S;
this._2.b1vx += a1 * dt; this._2.b1x += this._2.b1vx * dt;
this._2.b2vx += a2 * dt; this._2.b2x += this._2.b2vx * dt;
this._2.t += dt;
/* История для графика (примерно 20 точек/с) */
if (this._2.t % 0.05 < dt) {
this._2.history.push({ v1: this._2.b1vx / S, v2: this._2.b2vx / S });
if (this._2.history.length > 130) this._2.history.shift();
}
/* Сброс при достижении правого края */
if (this._2.b1x > W * 0.89 || this._2.b2x > W * 0.89) {
this._reset2(); this._2.running = true;
}
}
/* ── Физика III-A : пушка ─────────────────────────────────── */
_step3A(dt) {
const s = this._3A;
const { W, _g: g } = this;
const S = NewtonSim.SCALE;
const GV = NewtonSim.G * S;
if (!s.fired) return;
if (this._3A.forceFlash > 0) this._3A.forceFlash -= dt;
/* Пушка тормозит */
if (Math.abs(s.cvx) > 2) {
const dec = this.mu * GV * dt;
if (dec >= Math.abs(s.cvx)) s.cvx = 0;
else s.cvx -= Math.sign(s.cvx) * dec;
} else { s.cvx = 0; }
s.cx = Math.max(60, Math.min(W - 80, s.cx + s.cvx * dt));
/* Ядро (баллистика) */
if (s.ball) {
s.ball.vy += GV * dt;
s.ball.x += s.ball.vx * dt;
s.ball.y += s.ball.vy * dt;
if (s.ball.y > g.gY + 40 || s.ball.x > W + 120 || s.ball.x < -120) s.ball = null;
}
/* Искры */
for (const sp of s.sparks) {
sp.x += sp.vx * dt; sp.y += sp.vy * dt;
sp.vy += GV * 0.6 * dt; sp.life -= dt * 2;
}
s.sparks = s.sparks.filter(sp => sp.life > 0);
}
/* ── Физика III-B : столкновение ─────────────────────────── */
_step3B(dt) {
const { b1, b2 } = this._3B;
const { W, _g: g } = this;
const S = NewtonSim.SCALE;
b1.x += b1.vx * dt;
b2.x += b2.vx * dt;
if (this._3B.colFlash > 0) this._3B.colFlash -= dt;
/* Упругое столкновение */
if (!this._3B.done) {
const dist = b2.x - b1.x;
if (dist < b1.r + b2.r) {
const m1 = b1.mass, m2 = b2.mass;
const v1 = b1.vx, v2 = b2.vx;
b1.vx = ((m1 - m2) * v1 + 2 * m2 * v2) / (m1 + m2);
b2.vx = ((m2 - m1) * v2 + 2 * m1 * v1) / (m1 + m2);
const overlap = b1.r + b2.r - dist;
b1.x -= overlap * 0.5; b2.x += overlap * 0.5;
this._3B.colFlash = 0.55;
this._3B.done = true;
}
}
/* Минимальное трение поверхности (сохраняет видимость закона импульса) */
if (this._3B.done) {
[b1, b2].forEach(b => { b.vx *= (1 - dt * 0.04); });
}
/* Авто-сброс */
if (b1.x < -120 || b2.x > W + 120 || (this._3B.done && Math.abs(b1.vx) < 2 && Math.abs(b2.vx) < 2)) {
setTimeout(() => this._reset3B(), 1800);
this._3B.done = true; // prevent double reset
}
}
/* ── Физика III-C : ракета ────────────────────────────────── */
_step3C(dt) {
const s = this._3C;
const { W, H } = this;
const g_vis = NewtonSim.G * 0.42; // visual gravity (px/s²)
/* Gravity always acts — even after fuel is out */
if (!s.running && s.fuel <= 0 && !s.stopped) {
s.rvy += g_vis * dt;
s.ry += s.rvy * dt;
if (s.ry >= H * 0.78) { s.ry = H * 0.78; s.rvy = 0; s.stopped = true; }
/* exhaust smoke fading */
for (const p of s.particles) { p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt * 1.6; }
s.particles = s.particles.filter(p => p.life > 0 && p.y < H + 20);
return;
}
if (!s.running || s.fuel <= 0) {
if (s.fuel <= 0 && !s.stopped) { s.running = false; }
return;
}
const dmdt = 0.025; // кг/с — сгорание топлива
const F_thr = 220; // Н — тяга (визуальная)
s.fuel = Math.max(0, s.fuel - dmdt * dt);
s.rmass = Math.max(2, 10 * s.fuel + 2);
/* F_net = F_thrust - m·g */
const a_thrust = F_thr / s.rmass;
const a_net = a_thrust - g_vis;
s.rvy -= a_net * dt;
s.ry += s.rvy * dt;
if (s.ry < H * 0.08) { s.ry = H * 0.08; s.rvy = 0; }
if (s.ry >= H * 0.78) { s.ry = H * 0.78; s.rvy = 0; }
/* Частицы выхлопа */
if (Math.random() < 0.55) {
s.particles.push({
x: W * 0.5 + (Math.random() - 0.5) * 16,
y: s.ry + 48,
vx: (Math.random() - 0.5) * 38,
vy: 130 + Math.random() * 170,
r: 2 + Math.random() * 4,
life: 1,
});
}
for (const p of s.particles) {
p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt * 1.6;
}
s.particles = s.particles.filter(p => p.life > 0 && p.y < H + 20);
/* LabFX: rocket flame trail at nozzle */
if (window.LabFX) {
const nozzleX = W * 0.5;
const nozzleY = s.ry + 22;
LabFX.particles.emit({
ctx: this.ctx, x: nozzleX, y: nozzleY,
count: 3, color: ['#FFD166', '#FF6B35', '#EF476F'],
speed: 80, spread: Math.PI / 6, angle: Math.PI / 2,
gravity: -50, life: 400, glow: true, shape: 'spark',
});
}
}
/* ── Мышь ─────────────────────────────────────────────────── */
_bindEvents() {
this.canvas.addEventListener('click', e => {
if (this.law !== 1 || this.scene !== 'A') return;
const r = this.canvas.getBoundingClientRect();
const x = e.clientX - r.left;
const y = e.clientY - r.top;
const b = this._1A;
const dx = x - b.bx, dy = y - b.by;
const d = Math.hypot(dx, dy);
if (d < 4) return;
const spd = 340;
b.bvx = (dx / d) * spd;
b.bvy = (dy / d) * spd;
});
}
/* ═══════════════════════════════════════════════════════════
РЕНДЕРИНГ
═══════════════════════════════════════════════════════════ */
draw() {
const ctx = this.ctx;
const { W, H } = this;
ctx.clearRect(0, 0, W, H);
/* Фон */
const bg = ctx.createRadialGradient(W / 2, H * 0.3, 0, W / 2, H / 2, W * 0.82);
bg.addColorStop(0, '#0d1320'); bg.addColorStop(1, '#050810');
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
/* Водяной знак — номер закона */
ctx.save();
ctx.font = 'bold 78px sans-serif';
ctx.fillStyle = 'rgba(255,255,255,0.023)';
ctx.textAlign = 'center';
const wmarks = ['', 'ЗАКОН I', 'ЗАКОН II', 'ЗАКОН III', 'КЛАССИКА'];
ctx.fillText(wmarks[this.law] || '', W / 2, H * 0.60);
ctx.textAlign = 'left';
ctx.restore();
if (this.law === 1 && this.scene === 'A') this._drawL1A(ctx);
else if (this.law === 1) this._drawL1B(ctx);
else if (this.law === 2) this._drawL2(ctx);
else if (this.law === 4 && this.scene === 'atwood') this._drawAtwood(ctx);
else if (this.law === 4 && this.scene === 'ramp') this._drawRamp(ctx);
else if (this.law === 4 && this.scene === 'roll') this._drawRoll(ctx);
else if (this.scene === 'A') this._drawL3A(ctx);
else if (this.scene === 'B') this._drawL3B(ctx);
else this._drawL3C(ctx);
/* ── Energy bars overlay ── */
if (this._energyOn && window.LSPhysFX) {
this._drawEnergyBarsNwt(ctx);
}
/* LabFX: particles overlay */
if (window.LabFX) LabFX.particles.draw(this.ctx);
}
/* ── Energy bars: newton ── */
_drawEnergyBarsNwt(ctx) {
var en = this._calcEnergiesNwt();
if (!en) return;
var tot = en.ke + en.pe + en.friction;
if (tot > this._energyScale) this._energyScale = tot;
var PW = 188, MARGIN = 12;
LSPhysFX.drawEnergyBars(ctx, this.W - PW - MARGIN, MARGIN, PW, 0,
{ ke: en.ke, pe: en.pe, friction: en.friction, total: this._energyScale }, {});
}
_calcEnergiesNwt() {
var ke = 0, pe = 0, fr = this._frictionWork;
var S = NewtonSim.SCALE, G = NewtonSim.G;
/* law 1A — sliding block */
if (this.law === 1 && this.scene === 'A') {
var b = this._1A;
var spd = Math.hypot(b.bvx || 0, b.bvy || 0) / S;
ke = 0.5 * this.mass1 * spd * spd;
} else if (this.law === 1) {
/* law 1B — projectile */
var b2 = this._1B;
if (b2) {
var spd2 = Math.hypot(b2.vx || 0, b2.vy || 0) / S;
var h2 = Math.max(0, (this.H * 0.6 - (b2.y || 0))) / S;
ke = 0.5 * this.mass1 * spd2 * spd2;
pe = this.mass1 * G * h2;
}
} else if (this.law === 4 && this.scene === 'atwood') {
var atw = this._atw;
var v = Math.abs(atw.vy || 0) / S;
ke = 0.5 * (this.atwM1 + this.atwM2) * v * v;
/* PE from starting positions */
var dy1 = ((atw.y1 || 0) - (this.H * 0.3)) / S;
var dy2 = ((atw.y2 || 0) - (this.H * 0.3)) / S;
pe = Math.max(0, this.atwM1 * G * (-dy1) + this.atwM2 * G * (-dy2));
} else if (this.law === 4 && this.scene === 'ramp') {
var rmp = this._ramp;
ke = 0.5 * this.mass1 * (rmp.bv || 0) * (rmp.bv || 0);
var h = (rmp.bx || 0) * Math.sin(rmp.alpha || 0);
pe = Math.max(0, this.mass1 * G * h);
} else if (this.law === 4 && this.scene === 'roll') {
/* rolling: use ball as representative */
var roll = this._roll;
var vb = roll.vBall || 0;
/* KE includes rotational: for solid ball k=2/5, cyl=1/2, hoop=1 */
ke = 0.5 * this.mass1 * vb * vb * (1 + 0.4) / 1; /* solid ball */
var hRoll = Math.max(0, (roll.L || 3) * Math.sin(roll.alpha || 0) - (roll.sBall || 0) * Math.sin(roll.alpha || 0));
pe = this.mass1 * G * hRoll;
} else if (this.law === 2) {
var b2s = this._2;
var spd3 = Math.abs(b2s.v || 0);
ke = 0.5 * this.mass1 * spd3 * spd3;
} else if (this.law === 3) {
var b3 = this.scene === 'A' ? this._3A : (this.scene === 'B' ? this._3B : this._3C);
if (b3) {
var v3a = Math.abs(b3.v1 || 0), v3b = Math.abs(b3.v2 || 0);
ke = 0.5 * this.mass1 * v3a * v3a + 0.5 * this.mass2 * v3b * v3b;
}
}
return { ke: Math.max(0, ke), pe: Math.max(0, pe), friction: Math.max(0, fr) };
}
/* ── Закон I — Сцена A ───────────────────────────────────── */
_drawL1A(ctx) {
const { W, H, _g: g } = this;
const b = this._1A;
const S = NewtonSim.SCALE;
const spd = Math.hypot(b.bvx, b.bvy);
/* Звёздный фон при μ≈0 */
if (this.mu < 0.02) {
this._stars(ctx);
ctx.font = 'bold 12px sans-serif';
ctx.fillStyle = '#7BF5A4';
ctx.textAlign = 'center';
ctx.fillText('Трения нет — тело движется вечно!', W / 2, H * 0.10);
ctx.textAlign = 'left';
} else {
this._ground(ctx, g.gY, W);
}
/* След */
if (b.trail.length > 2) {
for (let i = 2; i < b.trail.length; i++) {
const a = (i / b.trail.length) * 0.55;
ctx.strokeStyle = `rgba(255,209,102,${a})`;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(b.trail[i-1].x, b.trail[i-1].y - 2);
ctx.lineTo(b.trail[i].x, b.trail[i].y - 2);
ctx.stroke();
}
}
/* Блок */
const by = b.by - b.BH / 2;
this._block(ctx, b.bx, by, b.BW, b.BH, '#9B5DE5', `${this.mass1} кг`);
/* Вектор скорости */
if (spd > 8) {
const scale = 0.28;
this._arrow(ctx,
b.bx, by,
b.bx + b.bvx * scale, by + b.bvy * scale,
'#FFD166', 'v = ' + (spd / S).toFixed(1) + ' м/с', 2.5);
}
/* Сила трения (горизонтальная, только на земле) */
if (!b.inAir && Math.abs(b.bvx) > 8 && this.mu > 0.01) {
const fFr = this.mu * this.mass1 * NewtonSim.G;
this._arrow(ctx,
b.bx, by - 32,
b.bx - Math.sign(b.bvx) * 55, by - 32,
'#EF476F', `F тр = ${fFr.toFixed(1)} Н`, 2);
}
/* Подсказка */
if (spd < 4) {
ctx.font = '13px sans-serif'; ctx.fillStyle = 'rgba(185,210,255,0.38)';
ctx.textAlign = 'center';
ctx.fillText('Кликни куда угодно — придай импульс блоку', W / 2, g.gY + 28);
ctx.textAlign = 'left';
}
/* μ и формула */
ctx.font = '12px monospace'; ctx.fillStyle = 'rgba(185,210,255,0.75)';
ctx.fillText(`μ = ${this.mu.toFixed(2)} F тр = μ · m · g`, 18, 26);
/* FBD: mg, N for Law I scene A */
if (this._fbdOn && window.LSPhysFX) {
const by2 = b.by - b.BH / 2;
const mg2 = this.mass1 * NewtonSim.G;
LSPhysFX.drawForceArrow(ctx, b.bx, by2, 0, 50, 'gravity', 'mg=' + mg2.toFixed(0) + 'Н');
if (!b.inAir) {
LSPhysFX.drawForceArrow(ctx, b.bx, by2, 0, -50, 'normal', 'N=' + mg2.toFixed(0) + 'Н');
}
}
this._caption(ctx, 'Тело продолжает движение\nпока не подействует сила', W, H);
}
/* ── Закон I — Сцена B ───────────────────────────────────── */
_drawL1B(ctx) {
const { W, H, _g: g } = this;
const s = this._1B;
this._stars(ctx);
/* Орбита */
ctx.save();
ctx.setLineDash([5, 9]);
ctx.strokeStyle = 'rgba(100,165,255,0.20)'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(g.cx, g.cy, g.orbitR, 0, Math.PI * 2); ctx.stroke();
ctx.setLineDash([]); ctx.restore();
/* Центральное тело */
ctx.save();
ctx.shadowColor = '#FFD166'; ctx.shadowBlur = 22;
ctx.beginPath(); ctx.arc(g.cx, g.cy, 20, 0, Math.PI * 2);
const cg = ctx.createRadialGradient(g.cx - 5, g.cy - 5, 0, g.cx, g.cy, 20);
cg.addColorStop(0, '#FFEA70'); cg.addColorStop(1, '#FF9500');
ctx.fillStyle = cg; ctx.fill();
ctx.restore();
/* Нить */
if (!s.cut) {
ctx.strokeStyle = 'rgba(210,225,255,0.55)'; ctx.lineWidth = 1.8;
ctx.beginPath(); ctx.moveTo(g.cx, g.cy); ctx.lineTo(s.bx, s.by); ctx.stroke();
/* Стрелка натяжения (центростремительная) */
const len = g.orbitR;
const tx = (g.cx - s.bx) / len * 36;
const ty = (g.cy - s.by) / len * 36;
const F_c = (this.mass1 * g.orbitR * s.omega * s.omega * NewtonSim.SCALE / NewtonSim.SCALE).toFixed(1);
this._arrow(ctx, s.bx, s.by, s.bx + tx, s.by + ty, '#4CC9F0', `T = F ц`, 1.8);
} else {
ctx.font = 'bold 14px sans-serif'; ctx.fillStyle = '#7BF5A4';
ctx.textAlign = 'center';
ctx.fillText('✂ Нить разрезана — тело летит прямолинейно!', W / 2, H * 0.10);
ctx.textAlign = 'left';
/* Вектор скорости по касательной */
this._arrow(ctx, s.bx, s.by,
s.bx + s.bvx * 0.22, s.by + s.bvy * 0.22,
'#FFD166', 'v = const', 2.8);
}
/* Шар */
ctx.save();
ctx.shadowColor = '#4CC9F0'; ctx.shadowBlur = 14;
ctx.beginPath(); ctx.arc(s.bx, s.by, 16, 0, Math.PI * 2);
ctx.fillStyle = '#4CC9F0'; ctx.fill();
ctx.restore();
/* Вектор скорости (во время орбиты) */
if (!s.cut) {
this._arrow(ctx, s.bx, s.by,
s.bx + s.bvx * 0.18, s.by + s.bvy * 0.18,
'#FFD166', '', 2);
}
this._caption(ctx, 'Без силы тело движется прямолинейно\nравномерно (1-й закон Ньютона)', W, H);
}
/* ── Закон II ─────────────────────────────────────────────── */
_drawL2(ctx) {
const { W, H, _g: g } = this;
const S = NewtonSim.SCALE;
const a1 = this.force / this.mass1;
const a2 = this.force / this.mass2;
this._ground(ctx, g.gY, W);
/* Линия финиша */
ctx.strokeStyle = 'rgba(255,255,255,0.13)'; ctx.lineWidth = 1;
ctx.setLineDash([6, 7]);
ctx.beginPath(); ctx.moveTo(W * 0.89, 0); ctx.lineTo(W * 0.89, g.gY + 8); ctx.stroke();
ctx.setLineDash([]);
const BW = 58, BH = 48;
if (this.scene === 'A') {
/* ── Один блок ── */
const { b1x: bx, b1vx: bvx } = this._2;
const by = g.gY - BH / 2;
this._block(ctx, bx, by, BW, BH, '#EF476F', `${this.mass1} кг`);
/* Сила F */
const _fTipX = bx + BW / 2 + 48 + this.force * 0.9;
const _fTipY = by;
this._arrow(ctx, bx + BW / 2, by,
_fTipX, _fTipY,
'#EF476F', `F = ${this.force} Н`, 2.5);
/* LabFX: spark at force arrow tip (scene II A, running) */
if (window.LabFX && this._2.running && Math.random() < 0.25) {
LabFX.particles.emit({
ctx, x: _fTipX, y: _fTipY,
count: 5, color: '#FFD166', speed: 40,
spread: Math.PI / 2, angle: 0, life: 200,
glow: true, shape: 'spark',
});
}
/* Ускорение a */
const aLen = 32 + a1 * 5;
this._arrow(ctx, bx + BW / 2, by - 32,
bx + BW / 2 + aLen, by - 32,
'#7BF5A4', `a = ${a1.toFixed(1)} м/с²`, 2.5);
/* Скорость v */
if (bvx > 8) {
const v = bvx / S;
this._arrow(ctx, bx + BW / 2, by + 32,
bx + BW / 2 + bvx * 0.28, by + 32,
'#FFD166', `v = ${v.toFixed(1)} м/с`, 2);
}
/* Уравнение F = m·a */
this._fma(ctx, this.force, this.mass1, a1, W / 2, H * 0.12);
/* Мини-график v(t) */
if (this._2.history.length > 3) {
this._graph(ctx, this._2.history.map(h => h.v1),
W * 0.72, 14, 145, 62, '#FFD166', 'v(t) м/с');
}
if (!this._2.running) {
ctx.font = '13px sans-serif'; ctx.fillStyle = 'rgba(185,210,255,0.38)';
ctx.textAlign = 'center';
ctx.fillText('Нажмите «Старт» чтобы запустить', W / 2, g.gY + 28);
ctx.textAlign = 'left';
}
} else {
/* ── Сравнение двух масс ── */
const y1 = g.gY - BH - 6, y2 = g.gY + 4;
/* Разделитель дорожек */
ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1;
ctx.setLineDash([4, 7]);
ctx.beginPath(); ctx.moveTo(0, g.gY - BH / 2 - 4); ctx.lineTo(W, g.gY - BH / 2 - 4); ctx.stroke();
ctx.setLineDash([]);
const bx1 = this._2.b1x, bx2 = this._2.b2x;
this._block(ctx, bx1, y1, BW, BH, '#EF476F', `${this.mass1} кг`);
this._block(ctx, bx2, y2, BW, BH, '#4CC9F0', `${this.mass2} кг`);
/* Силы (одинаковые) */
const fLen = 40 + this.force * 0.9;
this._arrow(ctx, bx1 + BW / 2, y1 + BH / 2, bx1 + BW / 2 + fLen, y1 + BH / 2, '#EF476F', `F=${this.force}Н`, 2);
this._arrow(ctx, bx2 + BW / 2, y2 + BH / 2, bx2 + BW / 2 + fLen, y2 + BH / 2, '#4CC9F0', `F=${this.force}Н`, 2);
/* Ускорения */
ctx.font = 'bold 11px monospace';
ctx.fillStyle = '#7BF5A4'; ctx.fillText(`a₁=${a1.toFixed(1)} м/с²`, bx1 + 2, y1 - 8);
ctx.fillStyle = '#06D6E0'; ctx.fillText(`a₂=${a2.toFixed(1)} м/с²`, bx2 + 2, y2 - 8);
/* Вывод */
if (this._2.running && bx1 > bx2 + 20) {
ctx.font = 'bold 13px sans-serif'; ctx.fillStyle = '#7BF5A4';
ctx.textAlign = 'center';
ctx.fillText('Меньше масса → больше ускорение!', W / 2, g.gY + 26);
ctx.textAlign = 'left';
}
/* Графики */
if (this._2.history.length > 3) {
this._graph(ctx, this._2.history.map(h => h.v1), W * 0.68, 14, 130, 58, '#EF476F', 'v₁ м/с');
this._graph(ctx, this._2.history.map(h => h.v2), W * 0.68, 80, 130, 58, '#4CC9F0', 'v₂ м/с');
}
if (!this._2.running) {
ctx.font = '13px sans-serif'; ctx.fillStyle = 'rgba(185,210,255,0.38)';
ctx.textAlign = 'center';
ctx.fillText('Нажмите «Старт» чтобы запустить', W / 2, g.gY + 28);
ctx.textAlign = 'left';
}
}
/* FBD: F_applied, mg, N, F_friction for Law II scene A */
if (this._fbdOn && window.LSPhysFX && this.scene === 'A') {
const { b1x: bx2 } = this._2;
const BH2 = 48;
const by2 = g.gY - BH2 / 2;
const mg2 = this.mass1 * NewtonSim.G;
const fFr2 = this.mu ? this.mu * mg2 : 0;
LSPhysFX.drawForceArrow(ctx, bx2, by2, 0, 44, 'gravity', 'mg=' + mg2.toFixed(0) + 'Н');
LSPhysFX.drawForceArrow(ctx, bx2, by2, 0, -44, 'normal', 'N=' + mg2.toFixed(0) + 'Н');
LSPhysFX.drawForceArrow(ctx, bx2, by2, Math.min(55, this.force * 1.5), 0, 'applied', 'F=' + this.force + 'Н');
if (fFr2 > 0.5) {
LSPhysFX.drawForceArrow(ctx, bx2, by2, -Math.min(40, fFr2 * 1.5), 0, 'friction', 'Fтр=' + fFr2.toFixed(0) + 'Н');
}
}
this._caption(ctx, 'F = m · a', W, H);
}
/* ── Закон III — Сцена A : пушка ────────────────────────── */
_drawL3A(ctx) {
const { W, H, _g: g } = this;
const s = this._3A;
const S = NewtonSim.SCALE;
const CW = 124, CH = 42;
this._ground(ctx, g.gY, W);
/* Корпус пушки */
ctx.save();
ctx.shadowColor = '#9B5DE5'; ctx.shadowBlur = 14;
_nwt_rrect(ctx, s.cx - CW / 2, g.gY - CH - 4, CW, CH, 8);
const cg = ctx.createLinearGradient(0, g.gY - CH - 4, 0, g.gY - 4);
cg.addColorStop(0, '#5a3a7a'); cg.addColorStop(1, '#3a2260');
ctx.fillStyle = cg; ctx.fill();
ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1.8; ctx.stroke();
ctx.restore();
/* Ствол */
ctx.save();
ctx.shadowColor = '#9B5DE5'; ctx.shadowBlur = 7;
_nwt_rrect(ctx, s.cx + CW / 2 - 8, g.gY - CH / 2 - 8 - 4, 58, 16, 4);
ctx.fillStyle = '#7340a0'; ctx.fill();
ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.restore();
/* Колёса */
[s.cx - CW / 2 + 18, s.cx + CW / 2 - 18].forEach(wx => {
ctx.save(); ctx.shadowColor = '#9B5DE5'; ctx.shadowBlur = 6;
ctx.beginPath(); ctx.arc(wx, g.gY, 10, 0, Math.PI * 2);
ctx.fillStyle = '#4a2870'; ctx.fill();
ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.restore();
});
/* Масса пушки */
ctx.font = 'bold 11px monospace'; ctx.fillStyle = 'rgba(200,180,255,0.9)';
ctx.textAlign = 'center';
ctx.fillText(`M = ${this.mass2} кг`, s.cx, g.gY - CH - 16);
ctx.textAlign = 'left';
/* Ядро */
if (s.ball) {
ctx.save();
ctx.shadowColor = '#FFD166'; ctx.shadowBlur = 12;
ctx.beginPath(); ctx.arc(s.ball.x, s.ball.y, 12, 0, Math.PI * 2);
ctx.fillStyle = '#FFD166'; ctx.fill();
ctx.restore();
ctx.font = '10px monospace'; ctx.fillStyle = 'rgba(255,209,102,0.82)';
ctx.textAlign = 'center';
ctx.fillText(`m = ${this.mass1} кг`, s.ball.x, s.ball.y + 26);
ctx.textAlign = 'left';
}
/* Стрелки сил (сразу после выстрела) */
if (s.forceFlash > 0) {
const alpha = Math.min(1, s.forceFlash * 2.5);
const fScale = 72 * alpha;
const ny = g.gY - CH - 32;
/* Сила на ядро → вправо */
this._arrow(ctx, s.cx + CW / 2 + 20, ny, s.cx + CW / 2 + 20 + fScale, ny, '#EF476F', 'F→ядро', 2.5);
/* Реакция на пушку → влево */
this._arrow(ctx, s.cx - CW / 2 - 20, ny, s.cx - CW / 2 - 20 - fScale, ny, '#4CC9F0', 'F→пушка', 2.5);
ctx.save(); ctx.globalAlpha = alpha;
ctx.font = 'bold 12px sans-serif'; ctx.fillStyle = '#FFD166';
ctx.textAlign = 'center';
ctx.fillText('|F→ядро| = |F→пушка|', s.cx, ny - 22);
ctx.restore();
}
/* Скорости (после выстрела, когда искры погасли) */
if (s.fired && s.sparks.length === 0 && s.forceFlash <= 0) {
if (s.cvx !== 0) {
this._arrow(ctx, s.cx, g.gY - CH / 2 - 4, s.cx + s.cvx * 0.35, g.gY - CH / 2 - 4,
'#4CC9F0', `V₂=${(s.cvx/S).toFixed(1)}м/с`, 2);
}
if (s.ball) {
this._arrow(ctx, s.ball.x, s.ball.y, s.ball.x + s.ball.vx * 0.12, s.ball.y + s.ball.vy * 0.12,
'#EF476F', `V₁=${(s.ball.vx/S).toFixed(1)}м/с`, 2);
}
/* Сохранение импульса */
const p1 = (this.mass1 * 360 / S).toFixed(1);
const p2 = Math.abs(this.mass2 * (this.mass1 / this.mass2) * 360 / S).toFixed(1);
ctx.font = '12px monospace'; ctx.fillStyle = 'rgba(185,210,255,0.82)';
ctx.textAlign = 'center';
ctx.fillText(`p₁ = ${p1} кг·м/с p₂ = ${p2} кг·м/с Δp_total = 0`, W / 2, H * 0.11);
ctx.textAlign = 'left';
}
/* Искры */
for (const sp of s.sparks) {
ctx.save(); ctx.globalAlpha = sp.life;
ctx.shadowColor = '#FFD166'; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.arc(sp.x, sp.y, 3 * sp.life, 0, Math.PI * 2);
ctx.fillStyle = sp.life > 0.5 ? '#FFD166' : '#EF476F'; ctx.fill();
ctx.restore();
}
if (!s.fired) {
ctx.font = '13px sans-serif'; ctx.fillStyle = 'rgba(185,210,255,0.38)';
ctx.textAlign = 'center';
ctx.fillText('Нажмите «Выстрел!» чтобы запустить ядро', W / 2, g.gY + 28);
ctx.textAlign = 'left';
}
/* FBD: action/reaction via LSPhysFX for Law III scene A */
if (this._fbdOn && window.LSPhysFX && s.fired && s.ball) {
const ny3 = g.gY - CW - 50;
const S3 = NewtonSim.SCALE;
const fMag = Math.min(70, this.mass1 * 6);
/* force on ball → right */
LSPhysFX.drawForceArrow(ctx, s.cx + CW / 2 + 12, ny3,
fMag, 0, 'applied', 'F→ядро');
/* reaction on cannon → left */
LSPhysFX.drawForceArrow(ctx, s.cx - CW / 2 - 12, ny3,
-fMag, 0, 'impulse', 'F→пушка');
}
this._caption(ctx, 'Действие = Противодействие\nF₁ = −F₂', W, H);
}
/* ── Закон III — Сцена B : столкновение ─────────────────── */
_drawL3B(ctx) {
const { W, H, _g: g } = this;
const { b1, b2, colFlash } = this._3B;
const S = NewtonSim.SCALE;
const cy = g.cy + 15;
/* Дорожка */
ctx.fillStyle = 'rgba(60,90,160,0.07)';
ctx.fillRect(0, cy - 50, W, 100);
/* Тени-следы */
[b1, b2].forEach(b => {
if (Math.abs(b.vx) < 4) return;
ctx.save(); ctx.globalAlpha = 0.14;
for (let s2 = 1; s2 <= 3; s2++) {
ctx.beginPath();
ctx.arc(b.x - b.vx * 0.06 * s2, cy, b.r * (1 - s2 * 0.1), 0, Math.PI * 2);
ctx.fillStyle = b.color; ctx.fill();
}
ctx.restore();
});
/* Шары */
[b1, b2].forEach(b => {
ctx.save();
ctx.shadowColor = b.color; ctx.shadowBlur = 16;
ctx.beginPath(); ctx.arc(b.x, cy, b.r, 0, Math.PI * 2);
const bg2 = ctx.createRadialGradient(b.x - b.r * 0.3, cy - b.r * 0.3, 0, b.x, cy, b.r);
bg2.addColorStop(0, _nwt_lighten(b.color, 65));
bg2.addColorStop(1, b.color);
ctx.fillStyle = bg2; ctx.fill();
ctx.restore();
/* Масса и скорость */
ctx.font = 'bold 12px monospace'; ctx.fillStyle = 'rgba(225,235,255,0.9)';
ctx.textAlign = 'center';
ctx.fillText(`${b.mass} кг`, b.x, cy + b.r + 19);
if (Math.abs(b.vx) > 6) {
ctx.fillStyle = '#FFD166';
ctx.fillText(`v=${( b.vx / S ).toFixed(1)}`, b.x, cy - b.r - 8);
}
ctx.textAlign = 'left';
});
/* Вспышка сил при ударе */
if (colFlash > 0.06) {
const mx = (b1.x + b2.x) / 2;
const fY = cy - b1.r - 28;
const a = Math.min(1, colFlash * 2.5);
const len = 65 * a;
this._arrow(ctx, mx, fY, mx - len, fY, '#EF476F', 'F₁₂', 2.5);
this._arrow(ctx, mx, fY + 8, mx + len, fY + 8, '#4CC9F0', 'F₂₁', 2.5);
ctx.save(); ctx.globalAlpha = a;
ctx.font = 'bold 12px sans-serif'; ctx.fillStyle = '#FFD166';
ctx.textAlign = 'center'; ctx.fillText('|F₁₂| = |F₂₁|', mx, fY - 18); ctx.restore();
}
/* Импульс */
const p1 = (b1.mass * b1.vx / S).toFixed(2);
const p2 = (b2.mass * b2.vx / S).toFixed(2);
const pt = ((b1.mass * b1.vx + b2.mass * b2.vx) / S).toFixed(2);
ctx.font = '12px monospace'; ctx.fillStyle = 'rgba(185,210,255,0.82)';
ctx.textAlign = 'center';
ctx.fillText(`p₁ = ${p1} p₂ = ${p2} p(сумм) = ${pt} кг·м/с`, W / 2, H * 0.12);
ctx.textAlign = 'left';
this._caption(ctx, 'Δp₁ = −Δp₂ (импульс сохраняется)', W, H);
}
/* ── Закон III — Сцена C : ракета ───────────────────────── */
_drawL3C(ctx) {
const { W, H } = this;
const s = this._3C;
const S = NewtonSim.SCALE;
const rx = W / 2;
this._stars(ctx);
/* Частицы выхлопа */
for (const p of s.particles) {
ctx.save();
ctx.globalAlpha = p.life * 0.85;
const col = p.life > 0.6 ? '#FFD166' : p.life > 0.3 ? '#FF6B35' : '#EF476F';
ctx.shadowColor = col; ctx.shadowBlur = 7;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r * p.life, 0, Math.PI * 2);
ctx.fillStyle = col; ctx.fill();
ctx.restore();
}
/* Ракета */
const ry = s.ry;
ctx.save(); ctx.shadowColor = '#4CC9F0'; ctx.shadowBlur = 18;
/* Фюзеляж */
_nwt_rrect(ctx, rx - 17, ry - 50, 34, 62, 7);
const rg = ctx.createLinearGradient(rx - 17, 0, rx + 17, 0);
rg.addColorStop(0, '#1a3a5a'); rg.addColorStop(0.5, '#4CC9F0'); rg.addColorStop(1, '#1a3a5a');
ctx.fillStyle = rg; ctx.fill();
ctx.strokeStyle = '#4CC9F0'; ctx.lineWidth = 1.5; ctx.stroke();
/* Нос */
ctx.beginPath();
ctx.moveTo(rx, ry - 72); ctx.lineTo(rx - 15, ry - 50); ctx.lineTo(rx + 15, ry - 50);
ctx.closePath(); ctx.fillStyle = '#06D6E0'; ctx.fill();
/* Плавники */
[[-1], [1]].forEach(([dx]) => {
ctx.beginPath();
ctx.moveTo(rx + dx * 17, ry + 12);
ctx.lineTo(rx + dx * 32, ry + 32);
ctx.lineTo(rx + dx * 17, ry + 22);
ctx.closePath(); ctx.fillStyle = '#06D6E0'; ctx.fill();
});
/* Иллюминатор */
ctx.beginPath(); ctx.arc(rx, ry - 20, 7, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(200,240,255,0.25)'; ctx.fill();
ctx.strokeStyle = '#4CC9F0'; ctx.lineWidth = 1; ctx.stroke();
ctx.restore();
/* Стрелки сил */
if (s.running) {
const g_vis = NewtonSim.G * 0.42;
const a_thrust = 220 / s.rmass;
const a_net = (a_thrust - g_vis).toFixed(1);
this._arrow(ctx, rx, ry - 55, rx, ry - 55 - 52, '#7BF5A4', `F тяга`, 2.5);
this._arrow(ctx, rx - 38, ry, rx - 38, ry + 36, '#FFD166', 'mg', 1.8);
this._arrow(ctx, rx, ry + 25, rx, ry + 80, '#EF476F', 'F газ', 2.5);
ctx.font = '12px monospace'; ctx.fillStyle = '#7BF5A4';
ctx.textAlign = 'center';
ctx.fillText(`a = F/m g = ${a_net} м/с²`, rx, ry - 110);
ctx.textAlign = 'left';
}
/* Falling after fuel out — show gravity arrow */
if (s.fuel <= 0 && !s.stopped) {
this._arrow(ctx, rx, ry + 25, rx, ry + 65, '#EF476F', 'mg↓', 2.5);
ctx.font = 'bold 13px sans-serif'; ctx.fillStyle = '#EF476F';
ctx.textAlign = 'center'; ctx.fillText('Топливо кончилось — ракета падает!', W / 2, H * 0.15); ctx.textAlign = 'left';
}
/* Инфо */
ctx.font = '12px monospace'; ctx.fillStyle = 'rgba(185,210,255,0.82)';
ctx.textAlign = 'center';
ctx.fillText(`Масса: ${s.rmass.toFixed(1)} кг Топливо: ${(s.fuel * 100).toFixed(0)}%`, W / 2, H * 0.94);
ctx.textAlign = 'left';
if (s.fuel <= 0 && s.stopped) {
ctx.font = 'bold 13px sans-serif'; ctx.fillStyle = '#FFD166';
ctx.textAlign = 'center'; ctx.fillText('Ракета приземлилась. Нажмите «Запуск» для сброса.', W / 2, H * 0.15); ctx.textAlign = 'left';
} else if (!s.running && s.fuel > 0) {
ctx.font = '13px sans-serif'; ctx.fillStyle = 'rgba(185,210,255,0.38)';
ctx.textAlign = 'center'; ctx.fillText('Нажмите «Запуск» для включения двигателя', W / 2, H * 0.50); ctx.textAlign = 'left';
}
this._caption(ctx, 'Газ вниз → ракета вверх\n(3-й закон Ньютона)', W, H);
}
/* ── Вспомогательные рисовалки ──────────────────────────── */
_ground(ctx, gY, W) {
const mu = this.mu;
/* Поверхность */
const gg = ctx.createLinearGradient(0, gY, 0, gY + 42);
gg.addColorStop(0, mu < 0.1 ? '#182535' : mu < 0.45 ? '#1c1f2d' : '#201420');
gg.addColorStop(1, '#0c101a');
ctx.fillStyle = gg; ctx.fillRect(0, gY, W, 55);
/* Линия */
ctx.strokeStyle = mu < 0.1 ? 'rgba(76,201,240,0.42)' : mu < 0.45 ? 'rgba(155,93,229,0.42)' : 'rgba(239,71,111,0.42)';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, gY); ctx.lineTo(W, gY); ctx.stroke();
/* Штриховка */
ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1;
for (let x = 0; x < W; x += 22) {
ctx.beginPath(); ctx.moveTo(x, gY); ctx.lineTo(x + 12, gY + 12); ctx.stroke();
}
/* Шероховатость (при высоком трении) */
if (mu > 0.06) {
ctx.fillStyle = `rgba(255,255,255,${mu * 0.055})`;
for (let x = 9; x < W; x += 20) {
ctx.beginPath(); ctx.arc(x, gY + 5, 2.5, 0, Math.PI * 2); ctx.fill();
}
}
}
_stars(ctx) {
const { W, H } = this;
for (let i = 0; i < 65; i++) {
const x = ((i * 139.5 + 7) % W);
const y = ((i * 97.3 + 5) % (H * 0.88));
const r = i % 8 === 0 ? 1.6 : 0.9;
ctx.fillStyle = `rgba(255,255,255,${0.35 + (i % 3) * 0.18})`;
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
}
}
_block(ctx, cx, cy, w, h, color, label) {
ctx.save();
ctx.shadowColor = color; ctx.shadowBlur = 10;
_nwt_rrect(ctx, cx - w / 2, cy - h / 2, w, h, 7);
const bg = ctx.createLinearGradient(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
bg.addColorStop(0, _nwt_lighten(color, 45));
bg.addColorStop(1, color);
ctx.fillStyle = bg; ctx.fill();
ctx.strokeStyle = _nwt_lighten(color, 60); ctx.lineWidth = 1.5; ctx.stroke();
ctx.shadowBlur = 0;
ctx.font = 'bold 11px monospace'; ctx.fillStyle = '#fff';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(label, cx, cy);
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
ctx.restore();
}
_arrow(ctx, x1, y1, x2, y2, color, label, lw = 2) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.hypot(dx, dy);
if (len < 5) return;
const ux = dx / len, uy = dy / len;
const hw = 7, hl = 13;
ctx.save();
ctx.strokeStyle = color; ctx.lineWidth = lw;
ctx.shadowColor = color; ctx.shadowBlur = 6;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2 - ux * hl, y2 - uy * hl);
ctx.stroke();
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - ux * hl - uy * hw, y2 - uy * hl + ux * hw);
ctx.lineTo(x2 - ux * hl + uy * hw, y2 - uy * hl - ux * hw);
ctx.closePath(); ctx.fill();
if (label) {
ctx.shadowBlur = 0; ctx.font = '11px monospace'; ctx.fillStyle = color;
const lx = (x1 + x2) / 2 - uy * 16;
const ly = (y1 + y2) / 2 + ux * 16;
ctx.textAlign = 'center'; ctx.fillText(label, lx, ly); ctx.textAlign = 'left';
}
ctx.restore();
}
_newtonGraphValues() {
const S = NewtonSim.SCALE;
if (this.law === 1 && this.scene === 'A') {
const b = this._1A;
const x = b.bx ? b.bx / S : 0;
const v = Math.hypot(b.bvx || 0, b.bvy || 0) / S;
const a = this._paused ? 0 : (this.mu * NewtonSim.G);
return [x, v, -a];
}
if (this.law === 2) {
const b = this._2;
const x = (b.b1x || 0) / S;
const v = (b.b1vx || 0) / S;
const a = b.running ? (this.force / this.mass1) : 0;
return [x, v, a];
}
if (this.law === 4 && this.scene === 'atwood' && this._atw) {
const atw = this._atw;
const x = atw.y1 ? atw.y1 / S : 0;
const v = (atw.vy || 0) / S;
const a = atw.aPhys || 0;
return [x, v, a];
}
// default: use 1A block or zero
return [0, 0, 0];
}
_fma(ctx, F, m, a, cx, y) {
ctx.save();
ctx.font = 'bold 15px monospace';
ctx.textAlign = 'center';
ctx.shadowColor = '#7BF5A4'; ctx.shadowBlur = 14;
ctx.fillStyle = 'rgba(123,245,164,0.88)';
ctx.fillText(`F = m·a ${F} Н = ${m} кг × ${a.toFixed(1)} м/с²`, cx, y);
ctx.restore();
}
_graph(ctx, data, x, y, w, h, color, label) {
if (data.length < 2) return;
const max = Math.max(...data, 0.01);
_nwt_rrect(ctx, x, y, w, h, 4);
ctx.fillStyle = 'rgba(0,0,0,0.38)'; ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; ctx.stroke();
ctx.strokeStyle = color; ctx.lineWidth = 1.6;
ctx.beginPath();
data.forEach((v, i) => {
const px = x + (i / (data.length - 1)) * w;
const py = y + h - 3 - (v / max) * (h - 7);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
});
ctx.stroke();
ctx.font = '9px monospace'; ctx.fillStyle = color;
ctx.fillText(label, x + 3, y + 11);
ctx.fillText(max.toFixed(1), x + 3, y + h - 3);
}
_caption(ctx, text, W, H) {
ctx.save();
ctx.font = 'italic 12px sans-serif';
ctx.fillStyle = 'rgba(185,210,255,0.35)';
ctx.textAlign = 'right';
text.split('\n').forEach((line, i) => ctx.fillText(line, W - 16, H * 0.90 + i * 18));
ctx.textAlign = 'left';
ctx.restore();
}
/* ── Info ──────────────────────────────────────────────────── */
info() {
if (this.law === 4) return this._infoClassic();
const S = NewtonSim.SCALE;
const base = { law: this.law, scene: this.scene };
if (this.law === 1 && this.scene === 'A') {
const b = this._1A;
const spd = Math.hypot(b.bvx, b.bvy) / S;
const fFr = this.mu * this.mass1 * NewtonSim.G;
return { ...base, v: spd.toFixed(2), fFr: fFr.toFixed(2), mu: this.mu.toFixed(2), m: this.mass1 };
}
if (this.law === 1) {
const s = this._1B;
const spd = Math.hypot(s.bvx, s.bvy) / S;
return { ...base, v: spd.toFixed(2), cut: s.cut };
}
if (this.law === 2) {
const a = this.force / this.mass1;
const v = this._2.b1vx / S;
return { ...base, F: this.force, m: this.mass1, a: a.toFixed(2), v: v.toFixed(2) };
}
if (this.scene === 'A') {
const vBall = this._3A.ball ? (this._3A.ball.vx / S).toFixed(1) : '—';
const vCannon = (this._3A.cvx / S).toFixed(2);
return { ...base, vBall, vCannon, m1: this.mass1, m2: this.mass2 };
}
if (this.scene === 'B') {
const { b1, b2 } = this._3B;
return { ...base,
p1: (b1.mass * b1.vx / S).toFixed(2),
p2: (b2.mass * b2.vx / S).toFixed(2),
pt: ((b1.mass * b1.vx + b2.mass * b2.vx) / S).toFixed(2),
};
}
/* III-C rocket */
const s = this._3C;
const g_vis = NewtonSim.G * 0.42;
const a_net = s.running ? (220 / s.rmass - g_vis) : (s.stopped ? 0 : -g_vis);
return { ...base,
a: a_net.toFixed(1),
v: Math.abs(s.rvy / S).toFixed(2),
fuel: (s.fuel * 100).toFixed(0),
m: s.rmass.toFixed(1),
};
}
/* ═══════════════════════════════════════════════════════════════
КЛАССИЧЕСКИЕ ЗАДАЧИ (law === 4)
Сцены: 'atwood' | 'ramp' | 'roll'
═══════════════════════════════════════════════════════════════ */
/* ── Сброс сцен ─────────────────────────────────────────────── */
_resetAtwood() {
const { W, H } = this;
/* Параметры */
const m1 = this.atwM1; // кг
const m2 = this.atwM2; // кг
const G = NewtonSim.G;
const massive = this.atwMassive; // bool: массивный блок?
/* Формула: a = (m2-m1)*g / (m1+m2) для идеального блока
с массивным блоком (I = 0.5*M*R², M=2кг, R=0.04м):
a = (m2-m1)*g / (m1+m2 + I/R²) = (m2-m1)*g / (m1+m2+1) */
const denom = m1 + m2 + (massive ? 1 : 0);
const aPhys = (m2 - m1) * G / denom; // м/с² (may be negative)
const T = 2 * m1 * m2 * G / denom; // Н
this._atw = {
/* визуальные позиции грузов (px) — блок по центру сверху */
pulleyX: W * 0.50,
pulleyY: H * 0.14,
ropeLen: H * 0.60, // общая длина нити (px)
y1: H * 0.30, // верх груза 1 (px)
y2: H * 0.30, // верх груза 2 (px)
vy: 0, // скорость грузов (px/s, + вниз для груза 2)
running: false,
aPhys, T,
finished: false,
t: 0,
/* история скоростей */
history: [],
};
}
_resetRamp() {
const { W, H } = this;
this._ramp = {
/* угол горки в радианах */
alpha: this.rampAlpha * Math.PI / 180,
/* состояние */
bx: 0, // позиция по склону (м)
bv: 0, // скорость по склону (м/с)
running: false,
t: 0,
history: [], // v(t) — для графика
finished: false,
};
}
_resetRoll() {
const { W, H } = this;
this._roll = {
alpha: this.rollAlpha * Math.PI / 180,
L: 3.0, // длина горки, м
/* позиции трёх тел (по склону, м) */
sBall: 0, sCyl: 0, sHoop: 0,
/* скорости */
vBall: 0, vCyl: 0, vHoop: 0,
/* угловые скорости для вращения */
wBall: 0, wCyl: 0, wHoop: 0,
running: false,
t: 0,
winner: null, // 'ball'|'cyl'|'hoop'
finishTimes: {},
withFriction: this.rollFriction,
};
}
/* ── Шаг физики — Машина Атвуда ─────────────────────────────── */
_stepAtwood(dt) {
const s = this._atw;
if (!s.running || s.finished) return;
const S = NewtonSim.SCALE; // px/м
const { H } = this;
/* Интеграция */
s.vy += s.aPhys * S * dt; // px/s² → px/s
s.y1 -= s.vy * dt; // груз 1 движется вверх если vy>0
s.y2 += s.vy * dt; // груз 2 вниз
s.t += dt;
/* Ограничение хода */
const minY = s.pulleyY + 18;
const maxY = H * 0.88;
if (s.y1 < minY || s.y2 > maxY || s.y1 > maxY || s.y2 < minY) {
s.running = false;
s.finished = true;
if (window.LabFX) LabFX.sound.play('chime');
}
/* История для HUD */
if (s.t % 0.05 < dt + 0.001) {
const vMs = Math.abs(s.vy) / S;
s.history.push(vMs);
if (s.history.length > 120) s.history.shift();
}
}
/* ── Шаг физики — Наклонная плоскость ──────────────────────── */
_stepRamp(dt) {
const s = this._ramp;
if (!s.running || s.finished) return;
const G = NewtonSim.G;
const alpha = this.rampAlpha * Math.PI / 180;
const mu = this.rampMu;
const Fapp = this.rampForce;
const sinA = Math.sin(alpha);
const cosA = Math.cos(alpha);
const Fdrive = G * sinA + Fapp / (this.mass1); // Н/кг = м/с²
const Ffrmax = mu * G * cosA;
let accel = 0;
if (Math.abs(Fdrive) <= Ffrmax) {
/* Покоится */
accel = 0;
s.bv = 0;
} else {
accel = Fdrive - Math.sign(Fdrive) * Ffrmax;
}
s.bv += accel * dt;
/* Не скользить назад если нет внешней силы и v~0 */
if (s.bv < 0 && Fapp <= 0) s.bv = 0;
s.bx += s.bv * dt;
s.t += dt;
const Lramp = 3.5; // м
if (s.bx >= Lramp) {
s.bx = Lramp; s.bv = 0; s.running = false; s.finished = true;
if (window.LabFX) LabFX.sound.play('chime');
}
/* История v(t) */
if (s.t % 0.05 < dt + 0.001) {
s.history.push(s.bv);
if (s.history.length > 120) s.history.shift();
}
}
/* ── Шаг физики — Скатывание тел ──────────────────────────── */
_stepRoll(dt) {
const s = this._roll;
if (!s.running) return;
const G = NewtonSim.G;
const alpha = this.rollAlpha * Math.PI / 180;
const sinA = Math.sin(alpha);
const mu = 0.30; // коэффициент сцепления для проскальзывания
/* Моменты инерции: k = I/(mR²)
Шар: k=2/5, Цилиндр: k=1/2, Обруч: k=1 */
const bodies = [
{ key: 'Ball', k: 2/5, color: '#EF476F', label: 'Шар' },
{ key: 'Cyl', k: 1/2, color: '#4CC9F0', label: 'Цилиндр' },
{ key: 'Hoop', k: 1, color: '#FFD166', label: 'Обруч' },
];
s.t += dt;
for (const b of bodies) {
if (s.finishTimes[b.key] !== undefined) continue; // уже финишировал
let accel;
if (!s.withFriction) {
/* Чистое качение: a = g*sinα / (1 + k) */
accel = G * sinA / (1 + b.k);
} else {
/* С возможным проскальзыванием: ограничиваем трением */
const aRoll = G * sinA / (1 + b.k);
const aSlide = G * (sinA - mu * Math.cos(alpha));
accel = Math.max(0, Math.max(aRoll, aSlide));
}
s['v' + b.key] += accel * dt;
s['s' + b.key] += s['v' + b.key] * dt;
/* угловая скорость для рисовки */
s['w' + b.key] += (accel / 0.08) * dt; // R=0.08м
if (s['s' + b.key] >= s.L) {
s['s' + b.key] = s.L;
s.finishTimes[b.key] = s.t;
if (!s.winner) {
s.winner = b.key;
if (window.LabFX) LabFX.sound.play('chime');
}
}
}
/* Все финишировали */
if (Object.keys(s.finishTimes).length === 3) {
s.running = false;
}
}
/* ── Отрисовка — Машина Атвуда ─────────────────────────────── */
_drawAtwood(ctx) {
const { W, H } = this;
const s = this._atw;
const S = NewtonSim.SCALE;
const m1 = this.atwM1;
const m2 = this.atwM2;
/* Потолок */
ctx.fillStyle = '#2a3040';
ctx.fillRect(s.pulleyX - 50, 0, 100, s.pulleyY - 10);
ctx.fillStyle = '#3a4560';
ctx.fillRect(0, 0, W, 10);
/* Блок (шкив) */
ctx.save();
ctx.strokeStyle = '#8899cc';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(s.pulleyX, s.pulleyY, 14, 0, Math.PI * 2);
ctx.fillStyle = '#1e2840';
ctx.fill(); ctx.stroke();
ctx.restore();
if (this.atwMassive) {
ctx.save();
ctx.font = 'bold 9px Manrope,sans-serif';
ctx.fillStyle = '#aabbff';
ctx.textAlign = 'center';
ctx.fillText('M', s.pulleyX, s.pulleyY + 4);
ctx.restore();
}
/* Нити */
ctx.strokeStyle = '#c0c8e0';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(s.pulleyX - 12, s.pulleyY);
ctx.lineTo(s.pulleyX - 12, s.y1);
ctx.moveTo(s.pulleyX + 12, s.pulleyY);
ctx.lineTo(s.pulleyX + 12, s.y2);
ctx.stroke();
/* Вспомогательные размеры грузов */
const bw1 = 18 + m1 * 2.2;
const bh1 = 18 + m1 * 2.2;
const bw2 = 18 + m2 * 2.2;
const bh2 = 18 + m2 * 2.2;
/* Груз 1 */
_nwt_rrect(ctx, s.pulleyX - 12 - bw1 / 2, s.y1, bw1, bh1, 4);
ctx.fillStyle = '#EF476F';
ctx.fill();
ctx.strokeStyle = _nwt_lighten('#EF476F', 60);
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.min(12, bw1 * 0.5)}px Manrope,sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(m1 + ' кг', s.pulleyX - 12, s.y1 + bh1 * 0.62);
/* Груз 2 */
_nwt_rrect(ctx, s.pulleyX + 12 - bw2 / 2, s.y2, bw2, bh2, 4);
ctx.fillStyle = '#4CC9F0';
ctx.fill();
ctx.strokeStyle = _nwt_lighten('#4CC9F0', 60);
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.min(12, bw2 * 0.5)}px Manrope,sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(m2 + ' кг', s.pulleyX + 12, s.y2 + bh2 * 0.62);
ctx.textAlign = 'left';
/* HUD: расчёты */
const vMs = (Math.abs(s.vy) / S).toFixed(2);
const aDisp = s.aPhys.toFixed(3);
const TDisp = s.T.toFixed(2);
const eq = m1 === m2;
this._atwHUD(ctx, W, H, vMs, aDisp, TDisp, eq, s);
/* График скорости */
if (s.history.length > 2) {
this._graph(ctx, s.history, W - 155, H - 90, 140, 65, '#7BF5A4', 'v (м/с)');
}
}
_atwHUD(ctx, W, H, vMs, aDisp, TDisp, eq, s) {
const lines = eq
? ['Равновесие', `a = 0 м/с²`, `T = ${TDisp} Н`]
: [`a = ${aDisp} м/с²`, `T = ${TDisp} Н`, `v = ${vMs} м/с`];
ctx.save();
ctx.font = '13px Manrope,sans-serif';
ctx.fillStyle = 'rgba(0,0,0,0.55)';
_nwt_rrect(ctx, 14, H - 80, 160, 64, 8);
ctx.fill();
ctx.fillStyle = '#c8d8ff';
lines.forEach((l, i) => ctx.fillText(l, 22, H - 58 + i * 18));
ctx.restore();
}
/* ── Отрисовка — Наклонная плоскость ───────────────────────── */
_drawRamp(ctx) {
const { W, H } = this;
const s = this._ramp;
const S = NewtonSim.SCALE;
const alpha = this.rampAlpha * Math.PI / 180;
const mu = this.rampMu;
const m = this.mass1;
const Fapp = this.rampForce;
const G = NewtonSim.G;
/* Геометрия горки */
const baseX = W * 0.12;
const baseY = H * 0.84;
const Lpx = W * 0.78;
const Hpx = Lpx * Math.tan(alpha);
const topX = baseX;
const topY = baseY - Hpx;
/* Наклонная плоскость */
ctx.beginPath();
ctx.moveTo(baseX, baseY);
ctx.lineTo(baseX + Lpx, baseY);
ctx.lineTo(topX, topY);
ctx.closePath();
ctx.fillStyle = '#1e2840';
ctx.fill();
ctx.strokeStyle = '#3a4d7a';
ctx.lineWidth = 2;
ctx.stroke();
/* Отметки угла */
ctx.save();
ctx.strokeStyle = '#7BF5A4';
ctx.lineWidth = 1.5;
const arcR = 36;
ctx.beginPath();
ctx.arc(baseX + Lpx, baseY, arcR, Math.PI, Math.PI + alpha, false);
ctx.stroke();
ctx.fillStyle = '#7BF5A4';
ctx.font = '11px Manrope,sans-serif';
ctx.fillText(this.rampAlpha + '°', baseX + Lpx - arcR * 1.55, baseY - 8);
ctx.restore();
/* Поверхность горки */
ctx.save();
ctx.strokeStyle = 'rgba(200,220,255,0.18)';
ctx.lineWidth = 1;
const Nlines = 7;
for (let i = 1; i < Nlines; i++) {
const frac = i / Nlines;
const sx = topX + (baseX + Lpx - topX) * frac;
const sy = topY + (baseY - topY) * frac;
const nx = Math.sin(alpha) * 8;
const ny = -Math.cos(alpha) * 8;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + nx, sy + ny);
ctx.stroke();
}
ctx.restore();
/* Тело на горке */
const bSize = 28 + m * 0.8;
const Spos = s.bx * S; // px от начала горки
/* Вектор вдоль горки */
const ux = Math.cos(Math.PI - alpha);
const uy = -Math.sin(Math.PI - alpha);
const bCx = baseX + Lpx - Spos * Math.cos(alpha) - (bSize / 2) * Math.sin(alpha);
const bCy = baseY - Spos * Math.sin(alpha) + (bSize / 2) * Math.cos(alpha) - bSize / 2;
ctx.save();
ctx.translate(bCx + bSize / 2, bCy + bSize / 2);
ctx.rotate(-alpha);
_nwt_rrect(ctx, -bSize / 2, -bSize / 2, bSize, bSize, 5);
ctx.fillStyle = '#EF476F';
ctx.fill();
ctx.strokeStyle = _nwt_lighten('#EF476F', 50);
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px Manrope,sans-serif';
ctx.textAlign = 'center';
ctx.fillText(m + ' кг', 0, 4);
/* Векторы сил на теле */
const sinA = Math.sin(alpha), cosA = Math.cos(alpha);
const Fgrav = m * G;
const Fnorm = Fgrav * cosA;
const Fdrive = Fgrav * sinA;
const Ffrmax = mu * Fnorm;
const sliding = Fdrive > Ffrmax;
const Ffr = sliding ? Ffrmax : Fdrive;
const arrowScale = 2.5;
/* Сила тяжести (вниз) */
this._arrow(ctx, 0, 0, 0, Fgrav * arrowScale, '#FFD166', 'mg');
/* Нормальная реакция (перп. горке = вверх в ЛСО) */
this._arrow(ctx, 0, 0, 0, -Fnorm * arrowScale, '#4CC9F0', 'N');
/* Компонент по горке (вдоль горки = влево в ЛСО) */
this._arrow(ctx, 0, 0, -Fdrive * arrowScale, 0, '#EF476F', 'mg·sinα');
/* Трение (вправо вдоль горки если скользит) */
if (Ffr > 0.5) {
const frColor = sliding ? '#7BF5A4' : '#8899cc';
this._arrow(ctx, 0, 0, Ffr * arrowScale, 0, frColor, sliding ? 'F_тр' : 'F_ст');
}
if (Fapp > 0.5) {
this._arrow(ctx, 0, 0, Fapp * arrowScale / m, 0, '#FF9F1C', 'F_пр');
}
ctx.restore();
/* Состояние */
const state = sliding
? `Скользит a = ${(G * (sinA - mu * cosA)).toFixed(2)} м/с²`
: `Покоится F_ст = ${(m * G * sinA).toFixed(1)} Н`;
ctx.save();
ctx.font = '12px Manrope,sans-serif';
ctx.fillStyle = 'rgba(0,0,0,0.55)';
_nwt_rrect(ctx, 14, H - 100, 230, 82, 8);
ctx.fill();
ctx.fillStyle = '#c8d8ff';
ctx.fillText(state, 22, H - 79);
ctx.fillText(`mg·sinα = ${(m * G * sinA).toFixed(1)} Н`, 22, H - 61);
ctx.fillText(`μ·mg·cosα = ${(mu * m * G * cosA).toFixed(1)} Н`, 22, H - 43);
ctx.fillText(`v = ${s.bv.toFixed(2)} м/с t = ${s.t.toFixed(1)} с`, 22, H - 25);
ctx.restore();
/* График v(t) */
if (s.history.length > 2) {
this._graph(ctx, s.history, W - 155, H - 90, 140, 65, '#EF476F', 'v (м/с)');
}
}
/* ── Отрисовка — Скатывание тел ─────────────────────────────── */
_drawRoll(ctx) {
const { W, H } = this;
const s = this._roll;
const S = NewtonSim.SCALE;
const alpha = this.rollAlpha * Math.PI / 180;
const G = NewtonSim.G;
/* Геометрия горки */
const baseX = W * 0.10;
const baseY = H * 0.82;
const Lpx = W * 0.82;
const Hpx = Lpx * Math.tan(alpha);
ctx.beginPath();
ctx.moveTo(baseX, baseY);
ctx.lineTo(baseX + Lpx, baseY);
ctx.lineTo(baseX, baseY - Hpx);
ctx.closePath();
ctx.fillStyle = '#1a2035';
ctx.fill();
ctx.strokeStyle = '#3a4d6a';
ctx.lineWidth = 2;
ctx.stroke();
/* Отметка угла */
ctx.save();
ctx.strokeStyle = '#aabbcc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(baseX, baseY, 32, -alpha, 0, false);
ctx.stroke();
ctx.fillStyle = '#aabbcc';
ctx.font = '11px Manrope,sans-serif';
ctx.fillText(this.rollAlpha + '°', baseX + 38, baseY - 6);
ctx.restore();
/* Линия финиша */
const finishFrac = 0.96;
const finX = baseX + Lpx * (1 - finishFrac * Math.cos(alpha) * Math.cos(alpha));
const finY = baseY - Lpx * finishFrac * Math.sin(alpha) * Math.cos(alpha);
ctx.save();
ctx.strokeStyle = '#FFD16688';
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(finX, finY - 12);
ctx.lineTo(finX, finY + 12);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
/* Три тела */
const bodies = [
{ key: 'Ball', k: 2/5, color: '#EF476F', label: 'Шар', radius: 10 },
{ key: 'Cyl', k: 1/2, color: '#4CC9F0', label: 'Цилиндр', radius: 11 },
{ key: 'Hoop', k: 1, color: '#FFD166', label: 'Обруч', radius: 12 },
];
for (let i = 0; i < 3; i++) {
const b = bodies[i];
const sPos = s['s' + b.key]; // позиция по склону (м)
const wAng = s['w' + b.key]; // угол поворота
const Spos = sPos * S; // px
/* Центр тела вдоль наклонной */
const cx = baseX + Spos * Math.cos(alpha) + b.radius * Math.sin(alpha);
const cy = (baseY - Hpx) + Spos * Math.cos(alpha) * Math.tan(alpha)
- Spos * Math.sin(alpha) + (Hpx - Spos * Math.sin(alpha))
+ b.radius * Math.cos(alpha) - Hpx + Spos * Math.sin(alpha);
/* Пересчитаем проще: точка на поверхности горки */
const sx0 = baseX + Spos * Math.cos(alpha);
const sy0 = baseY - Spos * Math.sin(alpha);
/* Смещение от поверхности (нормаль к горке) */
const nx = -Math.sin(alpha);
const ny = -Math.cos(alpha);
const bx = sx0 + nx * (b.radius + i * 1.5);
const by = sy0 + ny * (b.radius + i * 1.5);
ctx.save();
ctx.translate(bx, by);
ctx.rotate(-alpha + wAng);
if (b.key === 'Hoop') {
/* Обруч — только контур */
ctx.beginPath();
ctx.arc(0, 0, b.radius, 0, Math.PI * 2);
ctx.strokeStyle = b.color;
ctx.lineWidth = 3;
ctx.stroke();
/* спицы */
ctx.strokeStyle = b.color + '80';
ctx.lineWidth = 1;
for (let a = 0; a < Math.PI * 2; a += Math.PI / 3) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(a) * b.radius, Math.sin(a) * b.radius);
ctx.stroke();
}
} else if (b.key === 'Cyl') {
/* Цилиндр — закрашенный круг + линия диаметра */
ctx.beginPath();
ctx.arc(0, 0, b.radius, 0, Math.PI * 2);
ctx.fillStyle = b.color + 'aa';
ctx.fill();
ctx.strokeStyle = b.color;
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-b.radius, 0); ctx.lineTo(b.radius, 0);
ctx.strokeStyle = '#ffffffaa';
ctx.lineWidth = 1;
ctx.stroke();
} else {
/* Шар — градиентная заливка */
const grad = ctx.createRadialGradient(-b.radius * 0.3, -b.radius * 0.3, 1,
0, 0, b.radius);
grad.addColorStop(0, _nwt_lighten(b.color, 60));
grad.addColorStop(1, b.color);
ctx.beginPath();
ctx.arc(0, 0, b.radius, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = _nwt_lighten(b.color, 40);
ctx.lineWidth = 1.5;
ctx.stroke();
}
ctx.restore();
/* Подпись */
ctx.fillStyle = b.color;
ctx.font = '9px Manrope,sans-serif';
ctx.textAlign = 'center';
ctx.fillText(b.label, bx, by - b.radius - 4);
}
ctx.textAlign = 'left';
/* HUD */
const sinA = Math.sin(alpha);
const aBall = (G * sinA / (1 + 2/5)).toFixed(3);
const aCyl = (G * sinA / (1 + 1/2)).toFixed(3);
const aHoop = (G * sinA / (1 + 1)).toFixed(3);
ctx.save();
ctx.font = '11px Manrope,sans-serif';
ctx.fillStyle = 'rgba(0,0,0,0.55)';
_nwt_rrect(ctx, 14, H - 102, 210, 88, 8);
ctx.fill();
const lines = [
['Шар (k=2/5):', `a=${aBall} м/с²`, '#EF476F'],
['Цилиндр (k=1/2):', `a=${aCyl} м/с²`, '#4CC9F0'],
['Обруч (k=1):', `a=${aHoop} м/с²`, '#FFD166'],
];
lines.forEach(([lbl, val, col], i) => {
ctx.fillStyle = col;
ctx.fillText(lbl, 22, H - 82 + i * 20);
ctx.fillStyle = '#e0e8ff';
ctx.fillText(val, 130, H - 82 + i * 20);
});
/* Победитель */
if (s.winner) {
const wLabel = bodies.find(b => b.key === s.winner)?.label || s.winner;
ctx.fillStyle = '#7BF5A4';
ctx.font = 'bold 12px Manrope,sans-serif';
ctx.fillText('Первым: ' + wLabel, 22, H - 18);
} else {
ctx.fillStyle = '#8899aa';
ctx.font = '11px Manrope,sans-serif';
ctx.fillText('t = ' + s.t.toFixed(1) + ' с', 22, H - 18);
}
ctx.restore();
}
/* ── info() расширение для law=4 ──────────────────────────── */
_infoClassic() {
const scene = this.scene;
const base = { law: 4, scene };
const G = NewtonSim.G;
const S = NewtonSim.SCALE;
if (scene === 'atwood') {
const s = this._atw;
return { ...base,
a: s.aPhys.toFixed(3),
T: s.T.toFixed(2),
v: (Math.abs(s.vy) / S).toFixed(2),
eq: this.atwM1 === this.atwM2,
m1: this.atwM1,
m2: this.atwM2,
};
}
if (scene === 'ramp') {
const s = this._ramp;
const alpha = this.rampAlpha * Math.PI / 180;
const sinA = Math.sin(alpha), cosA = Math.cos(alpha);
const m = this.mass1;
const Fdrive = m * G * sinA;
const Ffrmax = this.rampMu * m * G * cosA;
const sliding = (Fdrive + this.rampForce) > Ffrmax;
const accel = sliding
? G * (sinA - this.rampMu * cosA) + this.rampForce / m
: 0;
return { ...base,
alpha: this.rampAlpha,
accel: accel.toFixed(3),
v: s.bv.toFixed(2),
Fdrive: Fdrive.toFixed(1),
Ffrmax: Ffrmax.toFixed(1),
sliding,
};
}
/* roll */
const s = this._roll;
const alpha = this.rollAlpha * Math.PI / 180;
const sinA = Math.sin(alpha);
return { ...base,
aBall: (G * sinA / (1 + 2/5)).toFixed(3),
aCyl: (G * sinA / (1 + 1/2)).toFixed(3),
aHoop: (G * sinA / (1 + 1 )).toFixed(3),
winner: s.winner,
t: s.t.toFixed(1),
};
}
}
/* ── Утилиты ─────────────────────────────────────────────────── */
function _nwt_rrect(ctx, x, y, w, h, r) {
if (w <= 0 || h <= 0) return;
r = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function _nwt_lighten(hex, d) {
const n = parseInt(hex.slice(1), 16);
const c = v => Math.max(0, Math.min(255, v));
return `rgb(${c((n >> 16) + d)},${c(((n >> 8) & 255) + d)},${c((n & 255) + d)})`;
}
/* ─── GraphPanel helpers ─────────────────────────── */
function newtonToggleGraphs() {
const sim = typeof newtonSim !== 'undefined' ? newtonSim : null;
if (!sim || !window.LSGraphPanelUI) return;
sim._graphsOn = !sim._graphsOn;
if (sim._graphsOn) {
sim._tSim = 0;
const canvasOuter = document.querySelector('#sim-dynamics .proj-canvas-outer');
if (!canvasOuter) return;
const S = NewtonSim.SCALE;
sim._graphUI = new GraphPanelUI(canvasOuter, {
maxPoints: 400,
traces: ['x', 'v', 'a'],
labels: ['x', 'v', 'a'],
units: ['м', 'м/с', 'м/с²'],
colors: ['#06D6E0', '#FFD166', '#EF476F'],
toggleBtnId: 'btn-newton-graphs',
title: 'Графики x/v/a'
});
sim._graphUI.isOn = true;
sim._graphUI._build();
} else {
if (sim._graphUI) { sim._graphUI._destroy(); sim._graphUI = null; }
}
const btn = document.getElementById('btn-newton-graphs');
if (btn) btn.classList.toggle('active', sim._graphsOn);
}
/* ─── lab UI init ─────────────────────────────────── */
var newtonSim = null;
var sandboxSim = null;
let _dynMode = 'sandbox'; // current mode: 'sandbox' | 'law1' | 'law2' | 'law3'
function _openDynamics(preset) {
document.getElementById('sim-topbar-title').textContent = 'Динамика';
_simShow('sim-dynamics');
_simShow('ctrl-dynamics');
requestAnimationFrame(() => requestAnimationFrame(() => {
// init sandbox
const sbCanvas = document.getElementById('sandbox-canvas');
if (!sandboxSim) {
sandboxSim = new ForceSandboxSim(sbCanvas);
sandboxSim.onUpdate = _sbUpdateUI;
}
// init newton
const nwCanvas = document.getElementById('newton-canvas');
if (!newtonSim) {
newtonSim = new NewtonSim(nwCanvas);
newtonSim.onUpdate = _newtonUpdateUI;
_dynInjectTCBar();
}
// activate current mode
dynMode(_dynMode);
if (preset) setTimeout(() => sbPreset(preset), 120);
}));
}
function _dynInjectTCBar() {
if (!window.LSTimeControl || !window.LSBuildTimeControlUI) return;
var wrap = document.getElementById('sim-dynamics');
if (!wrap || wrap.querySelector('.tc-bar')) return;
/* proxy TC that routes to whichever sim is active */
var proxyTC = {
paused: false,
scale: 1,
advance: function(dt) { return this.paused ? 0 : dt * this.scale; },
setScale: function(s) {
this.scale = +s;
if (newtonSim && newtonSim._tc) newtonSim._tc.setScale(+s);
if (sandboxSim && sandboxSim._tc) sandboxSim._tc.setScale(+s);
},
togglePause: function() {
this.paused = !this.paused;
if (newtonSim && newtonSim._tc) newtonSim._tc.paused = this.paused;
if (sandboxSim && sandboxSim._tc) sandboxSim._tc.paused = this.paused;
return this.paused;
},
};
var simProxy = { draw: function() {
if (_dynMode === 'sandbox' && sandboxSim) sandboxSim.draw();
else if (newtonSim) newtonSim.draw();
}};
var tcBar = window.LSBuildTimeControlUI(simProxy, proxyTC, { scrubSupported: false });
/* Trails toggle (sandbox only — newton doesn't have generic bodies) */
var sep = document.createElement('div');
sep.style.cssText = 'width:1px;height:18px;background:rgba(255,255,255,0.12);flex-shrink:0';
var trailBtn = document.createElement('button');
trailBtn.className = 'zoom-btn';
trailBtn.style.cssText = 'display:inline-flex;align-items:center;gap:3px;padding:3px 8px;border:1px solid rgba(255,255,255,0.15);border-radius:7px;background:rgba(28,18,48,0.8);color:#ccc;font-size:.68rem;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;font-family:Manrope,sans-serif';
trailBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" width="13" height="13"><path d="M3 12 Q6 3 12 12 Q18 21 21 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> Следы';
trailBtn.title = 'Следы движения (Sandbox)';
trailBtn.addEventListener('click', function() {
var on = sandboxSim ? !sandboxSim.showTrail : false;
if (sandboxSim) sandboxSim.showTrail = on;
trailBtn.style.background = on ? 'rgba(6,214,224,0.2)' : 'rgba(28,18,48,0.8)';
trailBtn.style.color = on ? '#06D6E0' : '#ccc';
trailBtn.style.borderColor = on ? 'rgba(6,214,224,0.5)' : 'rgba(255,255,255,0.15)';
});
tcBar.appendChild(sep);
tcBar.appendChild(trailBtn);
var statsBar = wrap.querySelector('.proj-stats-bar');
if (statsBar) wrap.insertBefore(tcBar, statsBar);
else wrap.appendChild(tcBar);
}
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 : mode === 'law4' ? 4 : 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: 'Двигатель' },
},
4: {
atwood: { desc: 'Машина Атвуда: два груза на нити через блок. a = (m₂-m₁)g/(m₁+m₂).', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Старт' },
ramp: { desc: 'Наклонная плоскость: тело скользит если mg·sinα > μ·mg·cosα.', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Старт' },
roll: { desc: 'Скатывание тел: шар, цилиндр, обруч. a = g·sinα/(1+k). Кто быстрее?', action: '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Гонка' },
},
};
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').innerHTML = lbl;
document.getElementById('newton-action-top').innerHTML = lbl;
/* ── Показ/скрытие панелей ── */
const isClassic = (law === 4);
// classic panel
const classicPanel = document.getElementById('newton-classic-panel');
if (classicPanel) classicPanel.style.display = isClassic ? '' : 'none';
// standard scene row
const sceneTitle = document.getElementById('newton-scene-title');
const sceneRow = document.getElementById('newton-scene-row');
const presetsTitle = document.querySelector('#dyn-newton-panel .gp-section-title[style*="margin-top"]');
if (sceneTitle) sceneTitle.style.display = isClassic ? 'none' : '';
if (sceneRow) sceneRow.style.display = isClassic ? 'none' : '';
// show/hide standard sliders (hidden for law 4)
document.getElementById('newton-mu-block').style.display = (!isClassic && law === 1 && scene === 'A') ? '' : 'none';
document.getElementById('newton-mass1-block').style.display = (!isClassic && (law === 2 || law === 3)) ? '' : 'none';
document.getElementById('newton-mass2-block').style.display = (!isClassic && law === 3) ? '' : 'none';
document.getElementById('newton-force-block').style.display = (!isClassic && law === 2) ? '' : 'none';
if (!isClassic) {
// 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 — classic uses atwood/ramp/roll
if (isClassic) {
['atwood','ramp','roll'].forEach(s => {
const tb = document.getElementById('nscn-cl-' + s);
const pb = document.getElementById('nscn-panel-cl-' + s);
const on = s === scene;
if (tb) tb.classList.toggle('active', on);
if (pb) pb.classList.toggle('active', on);
});
// update classic sub-panel visibility
['atwood','ramp','roll'].forEach(s => {
const el = document.getElementById('cl-sub-' + s);
if (el) el.style.display = (s === scene) ? '' : 'none';
});
// sync classic sliders
_syncClassicSliders();
} else {
// sync scene highlight buttons in both topbar and panel (standard A/B/C)
['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 (only for laws 1-3)
const presetsEl = document.getElementById('newton-presets');
const presetsSection = presetsEl && presetsEl.previousElementSibling;
if (presetsEl) {
presetsEl.style.display = isClassic ? 'none' : '';
if (presetsSection && presetsSection.classList.contains('gp-section-title'))
presetsSection.style.display = isClassic ? 'none' : '';
const presets = (!isClassic && _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 standard mode
const cBtn = document.getElementById('nscn-panel-C');
const cTopBtn = document.getElementById('nscn-C');
const bBtn = document.getElementById('nscn-panel-B');
const bTopBtn = document.getElementById('nscn-B');
const aBtn = document.getElementById('nscn-panel-A');
const aTopBtn = document.getElementById('nscn-A');
// topbar scene buttons (A/B/C) hidden for classic; cl-* shown instead
if (aBtn) aBtn.style.display = isClassic ? 'none' : '';
if (aTopBtn) aTopBtn.style.display = isClassic ? 'none' : '';
if (bBtn) bBtn.style.display = isClassic ? 'none' : '';
if (bTopBtn) bTopBtn.style.display = isClassic ? 'none' : '';
const showC = !isClassic && law === 3;
if (cBtn) cBtn.style.display = showC ? '' : 'none';
if (cTopBtn) cTopBtn.style.display = showC ? '' : 'none';
// classic topbar scene buttons
['atwood','ramp','roll'].forEach(s => {
const tb = document.getElementById('nscn-cl-' + s);
if (tb) tb.style.display = isClassic ? '' : 'none';
});
}
function _syncClassicSliders() {
if (!newtonSim) return;
/* Atwood */
_setVal('sl-atw-m1', 'atw-m1-val', newtonSim.atwM1, v => v + ' кг');
_setVal('sl-atw-m2', 'atw-m2-val', newtonSim.atwM2, v => v + ' кг');
/* Ramp */
_setVal('sl-ramp-alpha','ramp-alpha-val', newtonSim.rampAlpha, v => v + '°');
_setVal('sl-ramp-mu', 'ramp-mu-val', newtonSim.rampMu, v => (+v).toFixed(2));
_setVal('sl-ramp-force','ramp-force-val',newtonSim.rampForce, v => v + ' Н');
/* Roll */
_setVal('sl-roll-alpha','roll-alpha-val', newtonSim.rollAlpha, v => v + '°');
}
function _setVal(sliderId, valId, val, fmt) {
const sl = document.getElementById(sliderId);
const vl = document.getElementById(valId);
if (sl) sl.value = val;
if (vl) vl.textContent = fmt(val);
}
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();
else if (law === 4 && scene === 'atwood') newtonSim.startAtwood();
else if (law === 4 && scene === 'ramp') newtonSim.startRamp();
else if (law === 4 && scene === 'roll') newtonSim.startRoll();
_newtonUpdateUI(newtonSim.info());
}
function _resetNewtonScene() {
if (!newtonSim) return;
const law = newtonSim.law;
const scene = newtonSim.scene;
if (law === 4) {
if (scene === 'atwood') newtonSim._resetAtwood();
else if (scene === 'ramp') newtonSim._resetRamp();
else newtonSim._resetRoll();
} else 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());
}
/* ── classic scene selector ─────────────────────────────────── */
function classicScene(s) {
if (!newtonSim) return;
newtonSim.setScene(s);
document.querySelectorAll('.cl-scene-btn').forEach(b => {
b.classList.toggle('active', b.dataset.scene === s);
});
_newtonSyncUI();
_newtonUpdateUI(newtonSim.info());
}
/* ── classic param change handlers ──────────────────────────── */
function atwM1Change() {
const v = +document.getElementById('sl-atw-m1').value;
document.getElementById('atw-m1-val').textContent = v + ' кг';
if (newtonSim) newtonSim.setAtwM1(v);
}
function atwM2Change() {
const v = +document.getElementById('sl-atw-m2').value;
document.getElementById('atw-m2-val').textContent = v + ' кг';
if (newtonSim) newtonSim.setAtwM2(v);
}
function atwMassiveToggle(massive) {
if (newtonSim) newtonSim.setAtwMassive(massive);
document.querySelectorAll('.atw-massive-btn').forEach(b =>
b.classList.toggle('active', b.dataset.val === (massive ? 'true' : 'false')));
}
function rampMassChange() {
const v = +document.getElementById('sl-ramp-mass').value;
const vl = document.getElementById('newton-m1-ramp-val');
if (vl) vl.textContent = v + ' кг';
if (newtonSim) { newtonSim.mass1 = v; newtonSim._resetRamp(); }
}
function rampAlphaChange() {
const v = +document.getElementById('sl-ramp-alpha').value;
document.getElementById('ramp-alpha-val').textContent = v + '°';
if (newtonSim) newtonSim.setRampAlpha(v);
}
function rampMuChange() {
const v = +document.getElementById('sl-ramp-mu').value;
document.getElementById('ramp-mu-val').textContent = (+v).toFixed(2);
if (newtonSim) newtonSim.setRampMu(v);
}
function rampForceChange() {
const v = +document.getElementById('sl-ramp-force').value;
document.getElementById('ramp-force-val').textContent = v + ' Н';
if (newtonSim) newtonSim.setRampForce(v);
}
function rollAlphaChange() {
const v = +document.getElementById('sl-roll-alpha').value;
document.getElementById('roll-alpha-val').textContent = v + '°';
if (newtonSim) newtonSim.setRollAlpha(v);
}
function rollFrictionToggle(val) {
if (newtonSim) newtonSim.setRollFriction(val);
document.querySelectorAll('.roll-friction-btn').forEach(b =>
b.classList.toggle('active', b.dataset.val === (val ? 'true' : 'false')));
}
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 === 4 && scene === 'atwood') {
document.getElementById('dbar-l1').textContent = 'Машина Атвуда';
document.getElementById('dbar-v1').textContent = info.eq ? 'Равновесие' : 'Движение';
document.getElementById('dbar-l2').textContent = 'Ускорение a';
document.getElementById('dbar-v2').textContent = info.a + ' м/с²';
document.getElementById('dbar-l3').textContent = 'Натяжение T';
document.getElementById('dbar-v3').textContent = info.T + ' Н';
document.getElementById('dbar-l4').textContent = 'Скорость v';
document.getElementById('dbar-v4').textContent = info.v + ' м/с';
document.getElementById('dbar-l5').textContent = 'm₁ / m₂';
document.getElementById('dbar-v5').textContent = info.m1 + ' / ' + info.m2 + ' кг';
} else if (law === 4 && scene === 'ramp') {
document.getElementById('dbar-l1').textContent = 'Наклонная';
document.getElementById('dbar-v1').textContent = info.sliding ? 'Скользит' : 'Покой';
document.getElementById('dbar-l2').textContent = 'α';
document.getElementById('dbar-v2').textContent = info.alpha + '°';
document.getElementById('dbar-l3').textContent = 'Ускорение';
document.getElementById('dbar-v3').textContent = info.accel + ' м/с²';
document.getElementById('dbar-l4').textContent = 'mg·sinα';
document.getElementById('dbar-v4').textContent = info.Fdrive + ' Н';
document.getElementById('dbar-l5').textContent = 'μmg·cosα';
document.getElementById('dbar-v5').textContent = info.Ffrmax + ' Н';
} else if (law === 4 && scene === 'roll') {
document.getElementById('dbar-l1').textContent = 'Скатывание';
document.getElementById('dbar-v1').textContent = info.winner ? 'Финиш: ' + info.winner : 't=' + info.t + ' с';
document.getElementById('dbar-l2').textContent = 'a(шар)';
document.getElementById('dbar-v2').textContent = info.aBall + ' м/с²';
document.getElementById('dbar-l3').textContent = 'a(цилиндр)';
document.getElementById('dbar-v3').textContent = info.aCyl + ' м/с²';
document.getElementById('dbar-l4').textContent = 'a(обруч)';
document.getElementById('dbar-v4').textContent = info.aHoop + ' м/с²';
document.getElementById('dbar-l5').textContent = 'α';
document.getElementById('dbar-v5').textContent = (newtonSim ? newtonSim.rollAlpha : '—') + '°';
} else 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 + ' с';
}
/* ── Energy toggle: newton ── */
function dynToggleEnergy() {
if (nSim) {
nSim._energyOn = !nSim._energyOn;
const on = nSim._energyOn;
const btn = document.getElementById('nwt-energy-btn');
if (btn) {
btn.classList.toggle('active', on);
btn.querySelector('span').textContent = on ? 'Энергия: Вкл' : 'Энергия: Выкл';
}
if (!on) { nSim._frictionWork = 0; nSim._energyScale = 0; }
nSim.draw();
}
}
/* ── chem sandbox ── */