Files
Learn_System/frontend/js/labs/newton.js
T
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

1205 lines
45 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)
/* Состояние сцен */
this._1A = {};
this._1B = {};
this._2 = {};
this._3A = {};
this._3B = {};
this._3C = {};
/* Петля */
this._raf = null;
this._last = 0;
this._paused = false;
/* Геометрия */
this.W = 0; this.H = 0;
this._g = {};
this.onUpdate = null;
this.onModeChange = null;
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();
}
/* ── Сброс каждой сцены ──────────────────────────────────── */
_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 = 'A'; this._resetAll(); if (this.onModeChange) this.onModeChange(); }
setScene(s) { this.scene = s; this._resetAll(); }
setMu(v) { this.mu = v; }
setMass1(v) { this.mass1 = v; this._reset3B(); }
setMass2(v) { this.mass2 = v; this._reset3B(); }
setForce(v) { this.force = v; }
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 dt = Math.min((now - this._last) / 1000, 0.05);
this._last = now;
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.scene === 'A') this._step3A(dt);
else if (this.scene === 'B') this._step3B(dt);
else this._step3C(dt);
}
this.draw();
if (this.onUpdate) this.onUpdate(this.info());
}
/* ── Физика 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 : орбита <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> прямолинейное движение ────────── */
_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);
}
/* ── Мышь ─────────────────────────────────────────────────── */
_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';
ctx.fillText(['', 'ЗАКОН I', 'ЗАКОН II', 'ЗАКОН III'][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.scene === 'A') this._drawL3A(ctx);
else if (this.scene === 'B') this._drawL3B(ctx);
else this._drawL3C(ctx);
}
/* ── Закон 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);
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 */
this._arrow(ctx, bx + BW / 2, by,
bx + BW / 2 + 48 + this.force * 0.9, by,
'#EF476F', `F = ${this.force} Н`, 2.5);
/* Ускорение 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';
}
}
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;
/* Сила на ядро <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> вправо */
this._arrow(ctx, s.cx + CW / 2 + 20, ny, s.cx + CW / 2 + 20 + fScale, ny, '#EF476F', 'F<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>ядро', 2.5);
/* Реакция на пушку <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> влево */
this._arrow(ctx, s.cx - CW / 2 - 20, ny, s.cx - CW / 2 - 20 - fScale, ny, '#4CC9F0', 'F<svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>пушка', 2.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';
}
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<svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>', 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, 'Газ вниз <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ракета вверх\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();
}
_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() {
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),
};
}
}
/* ── Утилиты ─────────────────────────────────────────────────── */
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)})`;
}