'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 = ' Следы'; 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: ' Отрубить нить' }, C: { desc: 'Инерция в космосе: тело движется равномерно, нет сил — нет ускорения.', action: null }, }, 2: { A: { desc: 'Второй закон: F = ma. Прикладывай силу и следи за ускорением и скоростью.', action: ' Запустить' }, B: { desc: 'Два тела, разные массы — одинаковая сила. Сравни ускорения!', action: ' Запустить' }, C: { desc: 'Второй закон: изменяй силу и массу ползунками, наблюдай в реальном времени.', action: ' Запустить' }, }, 3: { A: { desc: 'Третий закон: пушка выстрелила — отдача. Импульс сохраняется!', action: 'Выстрел' }, B: { desc: 'Третий закон: два шара сталкиваются — силы равны и противоположны.', action: ' Столкнуть' }, C: { desc: 'Реактивное движение: ракета выбрасывает газ — летит в обратную сторону.', action: 'Двигатель' }, }, 4: { atwood: { desc: 'Машина Атвуда: два груза на нити через блок. a = (m₂-m₁)g/(m₁+m₂).', action: ' Старт' }, ramp: { desc: 'Наклонная плоскость: тело скользит если mg·sinα > μ·mg·cosα.', action: ' Старт' }, roll: { desc: 'Скатывание тел: шар, цилиндр, обруч. a = g·sinα/(1+k). Кто быстрее?', action: ' Гонка' }, }, }; const _NEWTON_PRESETS = { 1: [ { label: 'Космос', fn: 'space' }, { label: 'Лёд', fn: 'ice' }, { label: 'Асфальт', fn: 'asphalt' }, { label: 'Резина', fn: 'rubber' }, ], 2: [ { label: 'Лёгкий', fn: 'light' }, { label: 'Тяжёлый', fn: 'heavy' }, { label: 'Сравнить', fn: 'compare' }, ], 3: [ { label: 'Большая пушка', fn: 'big_cannon' }, { label: 'Маленькая', fn: 'small_cannon' }, { label: 'Равные шары', fn: 'equal_balls' }, ], }; // _openNewton is now handled by _openDynamics + dynMode // newtonLaw is now handled by dynMode('law1'/'law2'/'law3') function newtonScene(s, topBtn, panelBtn) { if (!newtonSim) return; newtonSim.setScene(s); document.querySelectorAll('.nscene-btn').forEach(b => { b.classList.toggle('active', b.id === 'nscn-' + s || b.id === 'nscn-panel-' + s); }); _newtonSyncUI(); _newtonUpdateUI(newtonSim.info()); } function _newtonSyncUI() { if (!newtonSim) return; const law = newtonSim.law; const scene = newtonSim.scene; const sceneData = (_NEWTON_SCENES[law] || {})[scene] || {}; // description const desc = document.getElementById('newton-scene-desc'); if (desc) desc.textContent = sceneData.desc || ''; // action button label const lbl = sceneData.action || (law === 1 ? ' Нить' : ' Действие'); 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 => `` ).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 ── */