feat(labs): механика V2 — 4 ключевые симы школьной физики расширены
pendulum V2 (472 → 1651 строк): - Математический (default, сохранён) - Двойной маятник (Lagrangian RK4, ghost-копия для демо хаоса) - Связанные маятники (биения, чарт θ₁/θ₂) - Пружинный (вертикальный/горизонтальный, T=2π√(m/k)) - Физический (4 формы: стержень/обруч/диск/прямоугольник, с моментом инерции) - Маятник Фуко (Кориолис, slider широты, период прецессии) - Резонанс (внешняя F₀·cos(ω_d·t), резонансная кривая A(ω)) - Фазовый портрет (универсальный toggle для всех режимов) collision V2 (~1000 → 2416 строк): - 1D (default, сохранён) - 2D под углом (импульс по осям, slider e, до/после стат) - Multi-ball (N=2-10, стены с отскоками, перемешать) - Бильярдный стол (6 луз, кий с прицелом, треугольник шаров, реалистичное трение) - Реф.фрейм ЦМ (universal toggle) newton V2 (1693 → 2585 строк): - 4-й закон-таб «Классические задачи» - Машина Атвуда (a=(m₂-m₁)g/(m₁+m₂), идеальный/массивный блок) - Тело на наклонной плоскости (FBD, статика/скольжение, slider α/μ/F_app) - Скатывание шара/цилиндра/обруча (момент инерции, гонка, наглядно почему обруч медленнее) projectile V2 (1900 → 2400 строк): - Парашют: F_d = ½C_d·ρ·A·v² с терминальной скоростью v_t = √(2mg/(C_d·ρ·A)) - C_d selector: парашют/куб/сфера/полусфера/диск; раскрытие парашюта на заданной высоте - Горка-катапульта: v_0 = √(2gL(sinα-μcosα)) автомат - 10 планет: Земля/Луна/Марс/Юпитер/Меркурий/Венера/Сатурн/Уран/Нептун/Плутон с реальными g + плотностью атмосферы (для drag) - Сравнительный режим: 3 планеты одновременно с разными цветами Все 4 симы — additive, существующая функциональность сохранена. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1296
-27
File diff suppressed because it is too large
Load Diff
+933
-43
File diff suppressed because it is too large
Load Diff
+1374
-195
File diff suppressed because it is too large
Load Diff
+529
-54
@@ -1,11 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
ProjectileSim v3 — physics simulation
|
||||
ProjectileSim v4 — physics simulation
|
||||
Features: air drag (RK4) · wind · bounce · speed multiplier
|
||||
ghost trail comparison · velocity vector labels
|
||||
range arrow · landing angle · canvas click play/pause
|
||||
target challenge mode · x/y/vx/vy graphs · dual throw
|
||||
parachute physics · ramp launch · multi-planet gravity
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
class ProjectileSim {
|
||||
@@ -86,6 +87,43 @@ class ProjectileSim {
|
||||
t: 0, trail: [],
|
||||
};
|
||||
|
||||
/* ── Feature 4: parachute ── */
|
||||
this.parachute = false; // parachute mode on/off
|
||||
this.chuteArea = 1.0; // A m² cross-section
|
||||
this.chuteCd = 1.5; // drag coefficient (preset: parachute)
|
||||
this.chuteOpenHeight = -1; // -1 = immediate; >=0 = open at this altitude
|
||||
this._chuteOpen = false; // runtime: is chute deployed?
|
||||
this._chuteOpenedTs = -999; // perf.now when deployed
|
||||
this._chimeEmitted = false; // v_t chime fired once per run
|
||||
|
||||
/* ── Feature 5: ramp launch ── */
|
||||
this.ramp = false; // ramp/slope mode on/off
|
||||
this.rampAngle = 30; // degrees
|
||||
this.rampLength = 10; // m
|
||||
this.rampMu = 0.1; // friction coefficient
|
||||
this._rampV0 = 0; // computed launch speed from ramp
|
||||
|
||||
/* ── Feature 6: planet gravity ── */
|
||||
// planets table: { name, g, rho } (rho = atmospheric density kg/m³)
|
||||
this.planets = [
|
||||
{ id: 'earth', name: 'Земля', g: 9.81, rho: 1.225 },
|
||||
{ id: 'moon', name: 'Луна', g: 1.62, rho: 0 },
|
||||
{ id: 'mars', name: 'Марс', g: 3.71, rho: 0.020 },
|
||||
{ id: 'venus', name: 'Венера', g: 8.87, rho: 65 },
|
||||
{ id: 'jupiter', name: 'Юпитер', g: 24.79, rho: 1.3 },
|
||||
{ id: 'mercury', name: 'Меркурий', g: 3.7, rho: 0 },
|
||||
{ id: 'saturn', name: 'Сатурн', g: 10.44, rho: 0.19 },
|
||||
{ id: 'uranus', name: 'Уран', g: 8.69, rho: 0.42 },
|
||||
{ id: 'neptune', name: 'Нептун', g: 11.15, rho: 0.45 },
|
||||
{ id: 'pluto', name: 'Плутон', g: 0.62, rho: 0.0001 },
|
||||
];
|
||||
this.planetId = 'earth'; // active planet
|
||||
this.rho = 1.225; // air density (set by planet or override)
|
||||
|
||||
/* ── Feature 6b: multi-planet compare ── */
|
||||
this.planetCompare = false; // show 3 planet trajectories simultaneously
|
||||
this.comparePlanets = ['earth', 'moon', 'mars']; // which 3
|
||||
|
||||
canvas.addEventListener('click', () => {
|
||||
if (this.onPlayPause) this.onPlayPause();
|
||||
});
|
||||
@@ -110,19 +148,42 @@ class ProjectileSim {
|
||||
getParams() {
|
||||
return { v0: this.v0, angle: this.angle, h0: this.h0, g: this.g,
|
||||
drag: this.drag, Cd: this.Cd, mass: this.mass, wind: this.wind,
|
||||
bounce: this.bounce, restitution: this.restitution };
|
||||
bounce: this.bounce, restitution: this.restitution,
|
||||
parachute: this.parachute, chuteArea: this.chuteArea, chuteCd: this.chuteCd,
|
||||
chuteOpenHeight: this.chuteOpenHeight,
|
||||
ramp: this.ramp, rampAngle: this.rampAngle, rampLength: this.rampLength, rampMu: this.rampMu,
|
||||
planetId: this.planetId };
|
||||
}
|
||||
setParams({ v0, angle, h0, g, drag, Cd, mass, wind, bounce, restitution } = {}) {
|
||||
if (v0 !== undefined) this.v0 = +v0;
|
||||
if (angle !== undefined) this.angle = +angle;
|
||||
if (h0 !== undefined) this.h0 = +h0;
|
||||
if (g !== undefined) this.g = +g;
|
||||
if (drag !== undefined) this.drag = !!drag;
|
||||
if (Cd !== undefined) this.Cd = +Cd;
|
||||
if (mass !== undefined) this.mass = Math.max(0.1, +mass);
|
||||
if (wind !== undefined) this.wind = +wind;
|
||||
if (bounce !== undefined) this.bounce = !!bounce;
|
||||
if (restitution !== undefined) this.restitution = Math.max(0, Math.min(1, +restitution));
|
||||
setParams({ v0, angle, h0, g, drag, Cd, mass, wind, bounce, restitution,
|
||||
parachute, chuteArea, chuteCd, chuteOpenHeight,
|
||||
ramp, rampAngle, rampLength, rampMu,
|
||||
planetId } = {}) {
|
||||
if (v0 !== undefined) this.v0 = +v0;
|
||||
if (angle !== undefined) this.angle = +angle;
|
||||
if (h0 !== undefined) this.h0 = +h0;
|
||||
if (g !== undefined) this.g = +g;
|
||||
if (drag !== undefined) this.drag = !!drag;
|
||||
if (Cd !== undefined) this.Cd = +Cd;
|
||||
if (mass !== undefined) this.mass = Math.max(0.1, +mass);
|
||||
if (wind !== undefined) this.wind = +wind;
|
||||
if (bounce !== undefined) this.bounce = !!bounce;
|
||||
if (restitution !== undefined) this.restitution = Math.max(0, Math.min(1, +restitution));
|
||||
if (parachute !== undefined) this.parachute = !!parachute;
|
||||
if (chuteArea !== undefined) this.chuteArea = Math.max(0.1, +chuteArea);
|
||||
if (chuteCd !== undefined) this.chuteCd = +chuteCd;
|
||||
if (chuteOpenHeight !== undefined) this.chuteOpenHeight = +chuteOpenHeight;
|
||||
if (ramp !== undefined) this.ramp = !!ramp;
|
||||
if (rampAngle !== undefined) this.rampAngle = Math.max(1, Math.min(89, +rampAngle));
|
||||
if (rampLength !== undefined) this.rampLength = Math.max(1, +rampLength);
|
||||
if (rampMu !== undefined) this.rampMu = Math.max(0, Math.min(1, +rampMu));
|
||||
if (planetId !== undefined) {
|
||||
this.planetId = planetId;
|
||||
const pl = this.planets.find(p => p.id === planetId);
|
||||
if (pl) {
|
||||
this.g = pl.g;
|
||||
this.rho = pl.rho;
|
||||
}
|
||||
}
|
||||
this._computePath();
|
||||
if (this.dualMode) this._computeP2Path();
|
||||
this._resetFX();
|
||||
@@ -135,7 +196,10 @@ class ProjectileSim {
|
||||
play() {
|
||||
if (this.playing) return;
|
||||
if (this._pathTf > 0 && this.t >= this._pathTf) this._resetFX();
|
||||
this._launchFlash = 1;
|
||||
this._launchFlash = 1;
|
||||
this._chuteOpen = this.parachute && this.chuteOpenHeight < 0;
|
||||
this._chuteOpenedTs = this._chuteOpen ? performance.now() : -999;
|
||||
this._chimeEmitted = false;
|
||||
this.playing = true;
|
||||
this._lastTs = null;
|
||||
/* reset p2 at launch so both start simultaneously */
|
||||
@@ -517,7 +581,7 @@ class ProjectileSim {
|
||||
return;
|
||||
}
|
||||
|
||||
const rho = 1.225, A = 0.00785;
|
||||
const rho = this.rho, A = 0.00785;
|
||||
const k = this.drag ? 0.5 * this.Cd * rho * A / Math.max(0.1, this.mass) : 0;
|
||||
const g = this.g, W = this.wind, e = this.restitution;
|
||||
const maxBounces = this.bounce ? 7 : 0;
|
||||
@@ -573,12 +637,13 @@ class ProjectileSim {
|
||||
|
||||
/* pure analytical solution (no drag/wind/bounce) */
|
||||
_stateAnalytical(t) {
|
||||
const rad = this.angle * Math.PI / 180;
|
||||
const vx = this.v0 * Math.cos(rad);
|
||||
const vy0 = this.v0 * Math.sin(rad);
|
||||
const launch = this._effectiveLaunch();
|
||||
const rad = launch.angle * Math.PI / 180;
|
||||
const vx = launch.v0 * Math.cos(rad);
|
||||
const vy0 = launch.v0 * Math.sin(rad);
|
||||
return {
|
||||
x: vx * t,
|
||||
y: this.h0 + vy0 * t - 0.5 * this.g * t * t,
|
||||
y: launch.h0 + vy0 * t - 0.5 * this.g * t * t,
|
||||
vx,
|
||||
vy: vy0 - this.g * t,
|
||||
};
|
||||
@@ -586,18 +651,43 @@ class ProjectileSim {
|
||||
|
||||
/* analytical flight time (for reference / no-effect comparison) */
|
||||
_tFlightAnalytical() {
|
||||
const rad = this.angle * Math.PI / 180;
|
||||
const vy0 = this.v0 * Math.sin(rad);
|
||||
const disc = vy0 * vy0 + 2 * this.g * this.h0;
|
||||
const launch = this._effectiveLaunch();
|
||||
const rad = launch.angle * Math.PI / 180;
|
||||
const vy0 = launch.v0 * Math.sin(rad);
|
||||
const disc = vy0 * vy0 + 2 * this.g * launch.h0;
|
||||
if (disc < 0) return 0;
|
||||
return Math.max(0, (vy0 + Math.sqrt(disc)) / this.g);
|
||||
}
|
||||
|
||||
_needsNumerical() {
|
||||
return this.drag || this.wind !== 0 || this.bounce;
|
||||
return this.drag || this.parachute || this.wind !== 0 || this.bounce || this.ramp;
|
||||
}
|
||||
|
||||
/* RK4 integration — handles drag, wind, bounce */
|
||||
/* compute launch speed from ramp: v = sqrt(2·g·L·sinα·(1-μ·cosα/sinα))
|
||||
v = sqrt(2·g·L·(sinα - μ·cosα)) assuming μ < tanα else no motion */
|
||||
_rampComputeV0() {
|
||||
const a = this.rampAngle * Math.PI / 180;
|
||||
const sin = Math.sin(a), cos = Math.cos(a);
|
||||
const net = sin - this.rampMu * cos;
|
||||
if (net <= 0) return 0;
|
||||
return Math.sqrt(2 * this.g * this.rampLength * net);
|
||||
}
|
||||
|
||||
/* effective launch angle = ramp angle when ramp is active */
|
||||
_effectiveLaunch() {
|
||||
if (this.ramp) {
|
||||
const v = this._rampComputeV0();
|
||||
return { v0: v, angle: this.rampAngle, h0: this.h0 };
|
||||
}
|
||||
return { v0: this.v0, angle: this.angle, h0: this.h0 };
|
||||
}
|
||||
|
||||
/* terminal velocity for current parachute config */
|
||||
_terminalVelocity() {
|
||||
return Math.sqrt(2 * this.mass * this.g / (this.chuteCd * this.rho * this.chuteArea));
|
||||
}
|
||||
|
||||
/* RK4 integration — handles drag, parachute, wind, bounce, ramp */
|
||||
_computePath() {
|
||||
if (!this._needsNumerical()) {
|
||||
this._path = null;
|
||||
@@ -605,28 +695,47 @@ class ProjectileSim {
|
||||
return;
|
||||
}
|
||||
|
||||
const rho = 1.225, A = 0.00785; // air density, ball cross-section
|
||||
const k = this.drag ? 0.5 * this.Cd * rho * A / Math.max(0.1, this.mass) : 0;
|
||||
const g = this.g;
|
||||
const W = this.wind;
|
||||
const e = this.restitution;
|
||||
const rho = this.rho; // air density (planet-aware)
|
||||
const A_ball = 0.00785; // small ball cross-section m²
|
||||
const g = this.g;
|
||||
const W = this.wind;
|
||||
const e = this.restitution;
|
||||
const maxBounces = this.bounce ? 7 : 0;
|
||||
const mass = Math.max(0.1, this.mass);
|
||||
|
||||
const rad = this.angle * Math.PI / 180;
|
||||
let x = 0, y = this.h0;
|
||||
let vx = this.v0 * Math.cos(rad);
|
||||
let vy = this.v0 * Math.sin(rad);
|
||||
/* simple-drag k factor (ball drag, legacy mode) */
|
||||
const kBall = this.drag && !this.parachute
|
||||
? 0.5 * this.Cd * rho * A_ball / mass
|
||||
: 0;
|
||||
|
||||
/* parachute: open immediately if chuteOpenHeight < 0, else on altitude trigger */
|
||||
const chuteAutoOpen = this.parachute && this.chuteOpenHeight < 0;
|
||||
const chuteThreshold = this.parachute ? Math.max(0, this.chuteOpenHeight) : Infinity;
|
||||
|
||||
const launch = this._effectiveLaunch();
|
||||
const rad = launch.angle * Math.PI / 180;
|
||||
let x = 0, y = launch.h0;
|
||||
let vx = launch.v0 * Math.cos(rad);
|
||||
let vy = launch.v0 * Math.sin(rad);
|
||||
let chuteOpen = chuteAutoOpen;
|
||||
const dt = 0.005;
|
||||
const path = [{ x, y, vx, vy, t: 0 }];
|
||||
const path = [{ x, y, vx, vy, t: 0, chuteOpen }];
|
||||
let bounceCount = 0;
|
||||
|
||||
const deriv = (sx, sy, svx, svy) => {
|
||||
const rvx = svx - W; // velocity relative to wind
|
||||
const rvy = svy;
|
||||
const deriv = (sx, sy, svx, svy, chute) => {
|
||||
const rvx = svx - W;
|
||||
const rvy = svy;
|
||||
const speed = Math.sqrt(rvx * rvx + rvy * rvy);
|
||||
const dragF = speed > 0 ? k * speed : 0;
|
||||
// wind-only pseudo-force when drag is off (simplified model)
|
||||
const windAcc = (!this.drag && W !== 0) ? W * 0.05 : 0;
|
||||
let dragF = 0;
|
||||
if (chute) {
|
||||
/* parachute: F_d = 0.5 * Cd * rho * A * v² / m → acceleration */
|
||||
dragF = speed > 0
|
||||
? 0.5 * this.chuteCd * rho * this.chuteArea * speed / mass
|
||||
: 0;
|
||||
} else if (kBall > 0) {
|
||||
dragF = speed > 0 ? kBall * speed : 0;
|
||||
}
|
||||
const windAcc = (!this.drag && !chute && W !== 0) ? W * 0.05 : 0;
|
||||
return {
|
||||
dx: svx,
|
||||
dy: svy,
|
||||
@@ -636,10 +745,15 @@ class ProjectileSim {
|
||||
};
|
||||
|
||||
for (let step = 0; step < 200000; step++) {
|
||||
const k1 = deriv(x, y, vx, vy);
|
||||
const k2 = deriv(x + k1.dx * dt / 2, y + k1.dy * dt / 2, vx + k1.dvx * dt / 2, vy + k1.dvy * dt / 2);
|
||||
const k3 = deriv(x + k2.dx * dt / 2, y + k2.dy * dt / 2, vx + k2.dvx * dt / 2, vy + k2.dvy * dt / 2);
|
||||
const k4 = deriv(x + k3.dx * dt, y + k3.dy * dt, vx + k3.dvx * dt, vy + k3.dvy * dt);
|
||||
/* check if chute should open by altitude trigger */
|
||||
if (this.parachute && !chuteOpen && y <= chuteThreshold && y > 0) {
|
||||
chuteOpen = true;
|
||||
}
|
||||
|
||||
const k1 = deriv(x, y, vx, vy, chuteOpen);
|
||||
const k2 = deriv(x + k1.dx * dt / 2, y + k1.dy * dt / 2, vx + k1.dvx * dt / 2, vy + k1.dvy * dt / 2, chuteOpen);
|
||||
const k3 = deriv(x + k2.dx * dt / 2, y + k2.dy * dt / 2, vx + k2.dvx * dt / 2, vy + k2.dvy * dt / 2, chuteOpen);
|
||||
const k4 = deriv(x + k3.dx * dt, y + k3.dy * dt, vx + k3.dvx * dt, vy + k3.dvy * dt, chuteOpen);
|
||||
|
||||
x += (k1.dx + 2 * k2.dx + 2 * k3.dx + k4.dx) * dt / 6;
|
||||
y += (k1.dy + 2 * k2.dy + 2 * k3.dy + k4.dy) * dt / 6;
|
||||
@@ -655,11 +769,11 @@ class ProjectileSim {
|
||||
const lvx = prev.vx + (vx - prev.vx) * frac;
|
||||
const lvy = prev.vy + (vy - prev.vy) * frac;
|
||||
const lt = prev.t + dt * frac;
|
||||
path.push({ x: lx, y: 0, vx: lvx, vy: lvy, t: lt });
|
||||
path.push({ x: lx, y: 0, vx: lvx, vy: lvy, t: lt, chuteOpen });
|
||||
|
||||
if (this.bounce && bounceCount < maxBounces && Math.abs(lvy) > 0.4) {
|
||||
vy = -e * lvy;
|
||||
vx = lvx * (1 - 0.04); // small horizontal friction
|
||||
vx = lvx * (1 - 0.04);
|
||||
y = 0.001;
|
||||
x = lx;
|
||||
bounceCount++;
|
||||
@@ -669,13 +783,72 @@ class ProjectileSim {
|
||||
break;
|
||||
}
|
||||
|
||||
path.push({ x, y, vx, vy, t });
|
||||
path.push({ x, y, vx, vy, t, chuteOpen });
|
||||
}
|
||||
|
||||
this._path = path;
|
||||
this._pathTf = path[path.length - 1].t;
|
||||
}
|
||||
|
||||
/* compute a trajectory for a given planet (for compare mode) */
|
||||
_computePlanetPath(planetId) {
|
||||
const pl = this.planets.find(p => p.id === planetId) || this.planets[0];
|
||||
const rho = pl.rho;
|
||||
const g = pl.g;
|
||||
const W = this.wind;
|
||||
const mass = Math.max(0.1, this.mass);
|
||||
const A_ball = 0.00785;
|
||||
const kBall = this.drag ? 0.5 * this.Cd * rho * A_ball / mass : 0;
|
||||
|
||||
const rad = this.angle * Math.PI / 180;
|
||||
let x = 0, y = this.h0;
|
||||
let vx = this.v0 * Math.cos(rad);
|
||||
let vy = this.v0 * Math.sin(rad);
|
||||
const dt = 0.005;
|
||||
const path = [{ x, y, vx, vy, t: 0 }];
|
||||
|
||||
const deriv2 = (sx, sy, svx, svy) => {
|
||||
const rvx = svx - W;
|
||||
const rvy = svy;
|
||||
const speed = Math.sqrt(rvx * rvx + rvy * rvy);
|
||||
const dragF = speed > 0 ? kBall * speed : 0;
|
||||
const windAcc = (!this.drag && W !== 0) ? W * 0.05 : 0;
|
||||
return {
|
||||
dx: svx, dy: svy,
|
||||
dvx: -dragF * rvx + windAcc,
|
||||
dvy: -g - dragF * rvy,
|
||||
};
|
||||
};
|
||||
|
||||
for (let step = 0; step < 100000; step++) {
|
||||
const k1 = deriv2(x, y, vx, vy);
|
||||
const k2 = deriv2(x + k1.dx * dt / 2, y + k1.dy * dt / 2, vx + k1.dvx * dt / 2, vy + k1.dvy * dt / 2);
|
||||
const k3 = deriv2(x + k2.dx * dt / 2, y + k2.dy * dt / 2, vx + k2.dvx * dt / 2, vy + k2.dvy * dt / 2);
|
||||
const k4 = deriv2(x + k3.dx * dt, y + k3.dy * dt, vx + k3.dvx * dt, vy + k3.dvy * dt);
|
||||
x += (k1.dx + 2 * k2.dx + 2 * k3.dx + k4.dx) * dt / 6;
|
||||
y += (k1.dy + 2 * k2.dy + 2 * k3.dy + k4.dy) * dt / 6;
|
||||
vx += (k1.dvx + 2 * k2.dvx + 2 * k3.dvx + k4.dvx) * dt / 6;
|
||||
vy += (k1.dvy + 2 * k2.dvy + 2 * k3.dvy + k4.dvy) * dt / 6;
|
||||
const t = (step + 1) * dt;
|
||||
if (y <= 0) {
|
||||
const prev = path[path.length - 1];
|
||||
if (prev && prev.y > 0) {
|
||||
const frac = prev.y / (prev.y - y);
|
||||
path.push({
|
||||
x: prev.x + (x - prev.x) * frac,
|
||||
y: 0,
|
||||
vx: prev.vx + (vx - prev.vx) * frac,
|
||||
vy: prev.vy + (vy - prev.vy) * frac,
|
||||
t: prev.t + dt * frac,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
path.push({ x, y, vx, vy, t });
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
_pathStateAt(t) {
|
||||
const path = this._path;
|
||||
if (!path || path.length < 2) return { x: 0, y: this.h0, vx: 0, vy: 0 };
|
||||
@@ -768,6 +941,39 @@ class ProjectileSim {
|
||||
if (this.targetMode) this._targetAttempts++;
|
||||
}
|
||||
|
||||
/* parachute: check altitude-triggered deployment */
|
||||
if (this.parachute && !this._chuteOpen && this.chuteOpenHeight >= 0) {
|
||||
const cs = this._curState(this.t);
|
||||
if (cs.y <= this.chuteOpenHeight && cs.y > 0) {
|
||||
this._chuteOpen = true;
|
||||
this._chuteOpenedTs = performance.now();
|
||||
if (window.LabFX) {
|
||||
LabFX.sound.play('whoosh');
|
||||
const _vp = this._viewParams;
|
||||
if (_vp) {
|
||||
const scX = (_vp.W - _vp.PL - _vp.PR) / _vp.xMax;
|
||||
const scY = (_vp.H - _vp.PB - _vp.PT) / _vp.yMax;
|
||||
LabFX.particles.emit({
|
||||
ctx: this.ctx,
|
||||
x: _vp.PL + cs.x * scX, y: _vp.H - _vp.PB - cs.y * scY,
|
||||
count: 30, color: ['#06D6E0', '#FFD166'], speed: 90,
|
||||
spread: Math.PI, angle: -Math.PI / 2, life: 800, glow: true, shape: 'spark',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* parachute: chime when ~90% terminal velocity reached */
|
||||
if (this.parachute && this._chuteOpen && !this._chimeEmitted) {
|
||||
const vt = this._terminalVelocity();
|
||||
const spd = Math.sqrt(cur.vx ** 2 + cur.vy ** 2);
|
||||
if (spd <= vt * 1.1) {
|
||||
this._chimeEmitted = true;
|
||||
if (window.LabFX) LabFX.sound.play('chime');
|
||||
}
|
||||
}
|
||||
|
||||
/* target hit detection on this step interval */
|
||||
this._checkTargetHits(prevT, Math.min(this.t, tf));
|
||||
|
||||
@@ -830,11 +1036,14 @@ class ProjectileSim {
|
||||
}
|
||||
|
||||
_resetFX() {
|
||||
this.t = 0;
|
||||
this._trail = [];
|
||||
this._sparks = [];
|
||||
this._impactTs = -999;
|
||||
this._launchFlash = 0;
|
||||
this.t = 0;
|
||||
this._trail = [];
|
||||
this._sparks = [];
|
||||
this._impactTs = -999;
|
||||
this._launchFlash = 0;
|
||||
this._chuteOpen = this.parachute && this.chuteOpenHeight < 0;
|
||||
this._chuteOpenedTs = -999;
|
||||
this._chimeEmitted = false;
|
||||
this._computePath();
|
||||
if (this.dualMode) {
|
||||
this._p2.t = 0;
|
||||
@@ -966,6 +1175,33 @@ class ProjectileSim {
|
||||
for (let y = stY; y < yMax * 0.97; y += stY)
|
||||
ctx.fillText(_projFmt(y) + ' м', PL - 6, tpy(y));
|
||||
|
||||
/* ── 6.4. Planet compare trajectories ── */
|
||||
if (this.planetCompare) {
|
||||
const PCOLORS = ['#06D6E0', '#7BF5A4', '#F15BB5'];
|
||||
for (let ci = 0; ci < this.comparePlanets.length; ci++) {
|
||||
const pid = this.comparePlanets[ci];
|
||||
const pl = this.planets.find(p => p.id === pid);
|
||||
if (!pl) continue;
|
||||
const ppath = this._computePlanetPath(pid);
|
||||
const col = PCOLORS[ci % PCOLORS.length];
|
||||
ctx.strokeStyle = col; ctx.lineWidth = 1.8; ctx.setLineDash([5, 3]);
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < ppath.length; i++) {
|
||||
const pp = ppath[i];
|
||||
i === 0 ? ctx.moveTo(tpx(pp.x), tpy(Math.max(0, pp.y)))
|
||||
: ctx.lineTo(tpx(pp.x), tpy(Math.max(0, pp.y)));
|
||||
}
|
||||
ctx.stroke(); ctx.setLineDash([]);
|
||||
/* label at landing */
|
||||
const plast = ppath[ppath.length - 1];
|
||||
const plx = tpx(plast.x), ply = tpy(0);
|
||||
ctx.fillStyle = col;
|
||||
ctx.font = 'bold 9px Manrope'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
ctx.fillText(pl.name, plx, ply + 8);
|
||||
ctx.fillText(_projFmt(plast.x) + ' м', plx, ply + 20);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 6.5. Ghost trails ── */
|
||||
for (const gh of this._ghosts) {
|
||||
ctx.strokeStyle = gh.color; ctx.lineWidth = 2;
|
||||
@@ -1009,7 +1245,7 @@ class ProjectileSim {
|
||||
}
|
||||
|
||||
/* ── 7. Launch platform ── */
|
||||
if (this.h0 > 0.2) {
|
||||
if (this.h0 > 0.2 && !this.ramp) {
|
||||
const px0 = tpx(0), py0 = tpy(0), pyH = tpy(this.h0);
|
||||
ctx.strokeStyle = 'rgba(255,200,60,.35)'; ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 4]);
|
||||
@@ -1022,6 +1258,40 @@ class ProjectileSim {
|
||||
ctx.fillText(_projFmt(this.h0) + ' м', px0 - 14, pyH);
|
||||
}
|
||||
|
||||
/* ── 7.5. Ramp visualization ── */
|
||||
if (this.ramp) {
|
||||
const rA = this.rampAngle * Math.PI / 180;
|
||||
const rL = this.rampLength;
|
||||
/* ramp starts at (0, h0) going left-down at angle rA */
|
||||
const rxStart = -rL * Math.cos(rA);
|
||||
const ryStart = this.h0;
|
||||
const rxEnd = 0;
|
||||
const ryEnd = this.h0 + rL * Math.sin(rA); /* ramp bottom */
|
||||
|
||||
/* clamp start x to left edge */
|
||||
const sx = Math.max(PL, tpx(rxStart));
|
||||
const sy = tpy(ryStart);
|
||||
const ex = tpx(rxEnd);
|
||||
const ey = tpy(ryEnd);
|
||||
|
||||
/* ramp surface */
|
||||
ctx.strokeStyle = 'rgba(255,180,50,.7)'; ctx.lineWidth = 3;
|
||||
ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(ex, ey); ctx.stroke();
|
||||
|
||||
/* angle arc */
|
||||
ctx.strokeStyle = 'rgba(255,200,60,.5)'; ctx.lineWidth = 1.2;
|
||||
ctx.beginPath(); ctx.arc(ex, ey, 22, -Math.PI / 2, -rA - Math.PI / 2, true); ctx.stroke();
|
||||
ctx.fillStyle = 'rgba(255,200,60,.8)';
|
||||
ctx.font = '9px Manrope'; ctx.textAlign = 'left'; ctx.textBaseline = 'bottom';
|
||||
ctx.fillText(this.rampAngle + '°', ex + 25, ey - 2);
|
||||
|
||||
/* ramp speed label */
|
||||
const rv = this._rampComputeV0();
|
||||
ctx.fillStyle = 'rgba(255,214,102,.7)';
|
||||
ctx.font = 'bold 9px Manrope'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||
ctx.fillText('v = ' + rv.toFixed(1) + ' м/с', (sx + ex) / 2, (sy + ey) / 2 - 4);
|
||||
}
|
||||
|
||||
/* ── 8. Reference / full trajectories ── */
|
||||
if (tf > 0) {
|
||||
// analytical reference (always shown as faint dashed)
|
||||
@@ -1311,6 +1581,36 @@ class ProjectileSim {
|
||||
ctx.beginPath(); ctx.arc(bx, by, 10, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.6)'; ctx.lineWidth = 1.5; ctx.stroke();
|
||||
|
||||
/* ── 14.5. Parachute ── */
|
||||
if (this.parachute && this._chuteOpen && this.t < tf && cur.y > 0) {
|
||||
this._drawParachute(ctx, bx, by);
|
||||
}
|
||||
|
||||
/* ── 14.6. Parachute HUD ── */
|
||||
if (this.parachute) {
|
||||
const vt = this._terminalVelocity();
|
||||
const pct = Math.min(100, Math.round((1 - (speed - vt) / Math.max(vt, 0.01)) * 100));
|
||||
const pctC = Math.min(100, Math.round(speed / (vt * 2 + 0.01) * 100));
|
||||
const hudRows = [
|
||||
'v = ' + speed.toFixed(1) + ' м/с',
|
||||
'v_t = ' + vt.toFixed(1) + ' м/с',
|
||||
(this._chuteOpen ? 'Откр' : 'Закр') + ' ' + Math.max(0, Math.min(100, Math.round(vt / Math.max(speed, 0.01) * 100))) + '%',
|
||||
];
|
||||
const hudX = W - PR - 8;
|
||||
const hudY = PT + 34;
|
||||
ctx.font = '9px Manrope, sans-serif';
|
||||
const maxW = Math.max(...hudRows.map(r => ctx.measureText(r).width));
|
||||
ctx.fillStyle = 'rgba(5,5,20,.8)';
|
||||
ctx.beginPath(); ctx.roundRect(hudX - maxW - 20, hudY - 4, maxW + 24, hudRows.length * 16 + 8, 7); ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(6,214,224,.4)'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.roundRect(hudX - maxW - 20, hudY - 4, maxW + 24, hudRows.length * 16 + 8, 7); ctx.stroke();
|
||||
for (let ri = 0; ri < hudRows.length; ri++) {
|
||||
ctx.fillStyle = ri === 2 ? (this._chuteOpen ? '#7BF5A4' : '#FFD166') : '#06D6E0';
|
||||
ctx.textAlign = 'right'; ctx.textBaseline = 'top';
|
||||
ctx.fillText(hudRows[ri], hudX - 6, hudY + ri * 16);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 15. Velocity arrows + labels ── */
|
||||
if (speed > 0.3 && this.t < tf) {
|
||||
const VX_LEN = Math.min(55, 50 * Math.abs(cur.vx) / Math.max(1, this.v0));
|
||||
@@ -1380,10 +1680,14 @@ class ProjectileSim {
|
||||
|
||||
/* ── 18. Info badges (top-right) ── */
|
||||
let bRight = W - PR - 8;
|
||||
if (this.drag) {
|
||||
if (this.drag && !this.parachute) {
|
||||
this._drawBadge(ctx, bRight, PT + 6, 'Cd=' + this.Cd.toFixed(2) + ' m=' + this.mass + 'кг', 'rgba(239,71,111,.15)', 'rgba(239,71,111,.75)');
|
||||
bRight -= 130;
|
||||
}
|
||||
if (this.parachute) {
|
||||
this._drawBadge(ctx, bRight, PT + 6, 'A=' + this.chuteArea.toFixed(1) + 'м² Cd=' + this.chuteCd, 'rgba(6,214,224,.12)', 'rgba(6,214,224,.8)');
|
||||
bRight -= 150;
|
||||
}
|
||||
if (this.wind !== 0) {
|
||||
const dir = this.wind > 0 ? '→' : '←';
|
||||
this._drawBadge(ctx, bRight, PT + 6, dir + ' ветер ' + Math.abs(this.wind) + 'м/с', 'rgba(6,214,224,.12)', 'rgba(6,214,224,.8)');
|
||||
@@ -1391,6 +1695,17 @@ class ProjectileSim {
|
||||
}
|
||||
if (this.bounce) {
|
||||
this._drawBadge(ctx, bRight, PT + 6, '↩ e=' + this.restitution.toFixed(2), 'rgba(123,245,164,.1)', 'rgba(123,245,164,.75)');
|
||||
bRight -= 100;
|
||||
}
|
||||
if (this.ramp) {
|
||||
this._drawBadge(ctx, bRight, PT + 6, 'Горка ' + this.rampAngle + '° L=' + this.rampLength + 'м', 'rgba(255,180,50,.12)', 'rgba(255,180,50,.85)');
|
||||
bRight -= 140;
|
||||
}
|
||||
if (this.planetId !== 'earth') {
|
||||
const pl = this.planets.find(p => p.id === this.planetId);
|
||||
if (pl) {
|
||||
this._drawBadge(ctx, bRight, PT + 6, pl.name + ' g=' + pl.g, 'rgba(123,245,164,.1)', 'rgba(123,245,164,.8)');
|
||||
}
|
||||
}
|
||||
|
||||
/* speed badge bottom-right */
|
||||
@@ -1607,6 +1922,47 @@ class ProjectileSim {
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/* Draw parachute dome above the ball */
|
||||
_drawParachute(ctx, bx, by) {
|
||||
const now = performance.now();
|
||||
const age = (now - this._chuteOpenedTs) / 1000;
|
||||
/* deploy animation: scale from 0 to 1 over 0.3 s */
|
||||
const scale = Math.min(1, age / 0.3);
|
||||
const R = 26 * scale; /* dome radius */
|
||||
const cy = by - R - 12; /* centre of dome */
|
||||
|
||||
ctx.save();
|
||||
/* dome fill */
|
||||
const fill = ctx.createRadialGradient(bx, cy, 0, bx, cy, R);
|
||||
fill.addColorStop(0, 'rgba(6,214,224,0.55)');
|
||||
fill.addColorStop(0.7, 'rgba(6,214,224,0.25)');
|
||||
fill.addColorStop(1, 'rgba(6,214,224,0.05)');
|
||||
ctx.fillStyle = fill;
|
||||
ctx.beginPath();
|
||||
ctx.arc(bx, cy, R, Math.PI, 0);
|
||||
ctx.lineTo(bx + R, cy);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
/* dome border */
|
||||
ctx.strokeStyle = 'rgba(6,214,224,0.75)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.arc(bx, cy, R, Math.PI, 0);
|
||||
ctx.stroke();
|
||||
|
||||
/* suspension lines (4) */
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.45)'; ctx.lineWidth = 0.8;
|
||||
for (let li = 0; li < 4; li++) {
|
||||
const a = Math.PI + (li + 0.5) / 4 * Math.PI;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx + Math.cos(a) * R, cy + Math.sin(a) * R);
|
||||
ctx.lineTo(bx, by - 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── module helpers ── */
|
||||
@@ -1921,5 +2277,124 @@ function _projArrow(ctx, x1, y1, x2, y2, color, lw) {
|
||||
if (pSim && pSim._graphsVisible && !pSim.playing) pSim.drawGraphs();
|
||||
}
|
||||
|
||||
/* ── Feature 4: Parachute UI ── */
|
||||
|
||||
function projToggleParachute(rowEl) {
|
||||
if (!pSim) return;
|
||||
pSim.parachute = !pSim.parachute;
|
||||
const on = pSim.parachute;
|
||||
if (rowEl) rowEl.classList.toggle('active', on);
|
||||
const tog = document.getElementById('chute-toggle');
|
||||
if (tog) {
|
||||
tog.style.background = on ? 'var(--cyan,#06D6E0)' : 'rgba(255,255,255,0.12)';
|
||||
tog.querySelector('span').style.marginLeft = on ? '14px' : '2px';
|
||||
}
|
||||
document.getElementById('chute-params').style.display = on ? '' : 'none';
|
||||
/* parachute and simple drag are mutually exclusive */
|
||||
if (on) pSim.setParams({ parachute: true, drag: false });
|
||||
else pSim.setParams({ parachute: false });
|
||||
/* also reflect drag row */
|
||||
const dragRow = document.getElementById('drag-row');
|
||||
if (dragRow) dragRow.classList.toggle('active', false);
|
||||
const dragTog = document.getElementById('drag-toggle');
|
||||
if (dragTog) {
|
||||
dragTog.style.background = 'rgba(255,255,255,0.12)';
|
||||
dragTog.querySelector('span').style.marginLeft = '2px';
|
||||
}
|
||||
document.getElementById('drag-params').style.display = 'none';
|
||||
}
|
||||
|
||||
function projChuteAreaChange() {
|
||||
const A = +document.getElementById('sl-chute-area').value / 10;
|
||||
document.getElementById('p-chute-area').textContent = A.toFixed(1) + ' м²';
|
||||
if (pSim) pSim.setParams({ chuteArea: A });
|
||||
}
|
||||
|
||||
function projChuteCdChange() {
|
||||
const sel = document.getElementById('sel-chute-cd');
|
||||
if (!sel || !pSim) return;
|
||||
const cd = +sel.value;
|
||||
pSim.setParams({ chuteCd: cd });
|
||||
}
|
||||
|
||||
function projChuteHeightChange() {
|
||||
const val = +document.getElementById('sl-chute-height').value;
|
||||
const h = val <= 0 ? -1 : val;
|
||||
const lbl = document.getElementById('p-chute-height');
|
||||
if (lbl) lbl.textContent = h < 0 ? 'Сразу' : h.toFixed(0) + ' м';
|
||||
if (pSim) pSim.setParams({ chuteOpenHeight: h });
|
||||
}
|
||||
|
||||
/* ── Feature 5: Ramp UI ── */
|
||||
|
||||
function projToggleRamp(rowEl) {
|
||||
if (!pSim) return;
|
||||
pSim.ramp = !pSim.ramp;
|
||||
const on = pSim.ramp;
|
||||
if (rowEl) rowEl.classList.toggle('active', on);
|
||||
const tog = document.getElementById('ramp-toggle');
|
||||
if (tog) {
|
||||
tog.style.background = on ? 'rgba(255,180,50,.9)' : 'rgba(255,255,255,0.12)';
|
||||
tog.querySelector('span').style.marginLeft = on ? '14px' : '2px';
|
||||
}
|
||||
document.getElementById('ramp-params').style.display = on ? '' : 'none';
|
||||
pSim.setParams({ ramp: on });
|
||||
}
|
||||
|
||||
function projRampChange() {
|
||||
const angle = +document.getElementById('sl-ramp-angle').value;
|
||||
const length = +document.getElementById('sl-ramp-length').value;
|
||||
const mu = +document.getElementById('sl-ramp-mu').value / 100;
|
||||
document.getElementById('p-ramp-angle').textContent = angle + '°';
|
||||
document.getElementById('p-ramp-length').textContent = length + ' м';
|
||||
document.getElementById('p-ramp-mu').textContent = mu.toFixed(2);
|
||||
if (pSim) pSim.setParams({ rampAngle: angle, rampLength: length, rampMu: mu });
|
||||
}
|
||||
|
||||
/* ── Feature 6: Planet UI ── */
|
||||
|
||||
function projPlanetChange() {
|
||||
const sel = document.getElementById('sel-planet');
|
||||
if (!sel || !pSim) return;
|
||||
const planetId = sel.value;
|
||||
pSim.planetId = planetId;
|
||||
const pl = pSim.planets.find(p => p.id === planetId);
|
||||
if (pl) {
|
||||
pSim.g = pl.g;
|
||||
pSim.rho = pl.rho;
|
||||
/* sync g slider */
|
||||
const gSl = document.getElementById('sl-g');
|
||||
if (gSl) {
|
||||
gSl.value = Math.min(+gSl.max, pl.g);
|
||||
document.getElementById('p-g').textContent = pl.g.toFixed(2) + ' м/с²';
|
||||
}
|
||||
}
|
||||
pSim._computePath();
|
||||
if (pSim.dualMode) pSim._computeP2Path();
|
||||
pSim._resetFX();
|
||||
pSim.draw();
|
||||
pSim._emit();
|
||||
}
|
||||
|
||||
function projTogglePlanetCompare() {
|
||||
if (!pSim) return;
|
||||
pSim.planetCompare = !pSim.planetCompare;
|
||||
const on = pSim.planetCompare;
|
||||
const btn = document.getElementById('proj-planet-compare-btn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', on);
|
||||
btn.querySelector('span').textContent = on ? 'Сравн.планет: Вкл' : 'Сравн.планет: Выкл';
|
||||
}
|
||||
const panel = document.getElementById('proj-planet-compare-panel');
|
||||
if (panel) panel.style.display = on ? '' : 'none';
|
||||
pSim.draw();
|
||||
}
|
||||
|
||||
function projPlanetCompareChange(idx, val) {
|
||||
if (!pSim) return;
|
||||
pSim.comparePlanets[idx] = val;
|
||||
pSim.draw();
|
||||
}
|
||||
|
||||
/* ── collision ── */
|
||||
|
||||
|
||||
+353
-30
@@ -141,7 +141,7 @@
|
||||
<button class="zoom-btn" id="coll-play-btn" onclick="collPlayPause()" title="Запустить">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
</button>
|
||||
<button class="zoom-btn" onclick="cSim && cSim.reset()" title="Сброс">
|
||||
<button class="zoom-btn" onclick="var _as=_activeSim&&_activeSim();if(_as)_as.reset();_collSyncBtn();" title="Сброс">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -237,6 +237,10 @@
|
||||
<button class="zoom-btn nscene-btn active" id="nscn-A" onclick="newtonScene('A',this)" style="font-size:.65rem;font-weight:800">A</button>
|
||||
<button class="zoom-btn nscene-btn" id="nscn-B" onclick="newtonScene('B',this)" style="font-size:.65rem;font-weight:800">B</button>
|
||||
<button class="zoom-btn nscene-btn" id="nscn-C" onclick="newtonScene('C',this)" style="font-size:.65rem;font-weight:800">C</button>
|
||||
<!-- classic scenes (law 4, hidden by default) -->
|
||||
<button class="zoom-btn nscene-btn cl-scene-btn" id="nscn-cl-atwood" data-scene="atwood" onclick="classicScene('atwood')" style="display:none;font-size:.6rem;font-weight:800">Атвуд</button>
|
||||
<button class="zoom-btn nscene-btn cl-scene-btn" id="nscn-cl-ramp" data-scene="ramp" onclick="classicScene('ramp')" style="display:none;font-size:.6rem;font-weight:800">Наклон</button>
|
||||
<button class="zoom-btn nscene-btn cl-scene-btn" id="nscn-cl-roll" data-scene="roll" onclick="classicScene('roll')" style="display:none;font-size:.6rem;font-weight:800">Качение</button>
|
||||
<div style="width:1px;height:20px;background:rgba(255,255,255,0.15);margin:0 3px"></div>
|
||||
<button class="zoom-btn" id="newton-action-top" onclick="newtonAction()" style="font-size:.65rem;font-weight:800;font-family:Manrope,sans-serif"><svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Действие</button>
|
||||
</span>
|
||||
@@ -1499,19 +1503,84 @@
|
||||
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law1" onclick="dynMode('law1',this)" style="flex:1;font-size:.68rem;padding:5px 0">I закон</button>
|
||||
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law2" onclick="dynMode('law2',this)" style="flex:1;font-size:.68rem;padding:5px 0">II закон</button>
|
||||
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law3" onclick="dynMode('law3',this)" style="flex:1;font-size:.68rem;padding:5px 0">III закон</button>
|
||||
<button class="mag-mode-btn dyn-mode" id="dyn-mode-law4" onclick="dynMode('law4',this)" style="flex:1;font-size:.68rem;padding:5px 0">Классич.</button>
|
||||
</div>
|
||||
|
||||
<!-- ══ Newton controls (shown in law modes) ══ -->
|
||||
<div id="dyn-newton-panel" style="display:none">
|
||||
|
||||
<!-- Scene selector -->
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Сцена</div>
|
||||
<!-- Scene selector (standard A/B/C — hidden for law 4) -->
|
||||
<div class="gp-section-title" style="margin-bottom:8px" id="newton-scene-title">Сцена</div>
|
||||
<div style="display:flex;gap:5px;margin-bottom:12px" id="newton-scene-row">
|
||||
<button class="mag-mode-btn nscene-btn active" id="nscn-panel-A" onclick="newtonScene('A',null,this)" style="flex:1;font-size:.72rem">A</button>
|
||||
<button class="mag-mode-btn nscene-btn" id="nscn-panel-B" onclick="newtonScene('B',null,this)" style="flex:1;font-size:.72rem">B</button>
|
||||
<button class="mag-mode-btn nscene-btn" id="nscn-panel-C" onclick="newtonScene('C',null,this)" style="flex:1;font-size:.72rem">C</button>
|
||||
</div>
|
||||
|
||||
<!-- Classic scene selector (law 4 only) -->
|
||||
<div id="newton-classic-panel" style="display:none">
|
||||
<div style="display:flex;gap:5px;margin-bottom:10px">
|
||||
<button class="mag-mode-btn cl-scene-btn active" id="nscn-panel-cl-atwood" data-scene="atwood" onclick="classicScene('atwood')" style="flex:1;font-size:.68rem">Атвуд</button>
|
||||
<button class="mag-mode-btn cl-scene-btn" id="nscn-panel-cl-ramp" data-scene="ramp" onclick="classicScene('ramp')" style="flex:1;font-size:.68rem">Наклон</button>
|
||||
<button class="mag-mode-btn cl-scene-btn" id="nscn-panel-cl-roll" data-scene="roll" onclick="classicScene('roll')" style="flex:1;font-size:.68rem">Качение</button>
|
||||
</div>
|
||||
|
||||
<!-- Atwood sub-panel -->
|
||||
<div id="cl-sub-atwood">
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">m₁ (кг)</span><span class="param-val" id="atw-m1-val">5 кг</span></div>
|
||||
<input type="range" class="param-slider" id="sl-atw-m1" min="1" max="20" value="5" oninput="atwM1Change()">
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">m₂ (кг)</span><span class="param-val" id="atw-m2-val">8 кг</span></div>
|
||||
<input type="range" class="param-slider" id="sl-atw-m2" min="1" max="20" value="8" oninput="atwM2Change()">
|
||||
</div>
|
||||
<div class="gp-section-title" style="margin:4px 0">Блок</div>
|
||||
<div style="display:flex;gap:5px;margin-bottom:8px">
|
||||
<button class="mag-mode-btn atw-massive-btn active" data-val="false" onclick="atwMassiveToggle(false)" style="flex:1;font-size:.68rem">Идеальный</button>
|
||||
<button class="mag-mode-btn atw-massive-btn" data-val="true" onclick="atwMassiveToggle(true)" style="flex:1;font-size:.68rem">Массивный</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ramp sub-panel -->
|
||||
<div id="cl-sub-ramp" style="display:none">
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">Угол α</span><span class="param-val" id="ramp-alpha-val">30°</span></div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-alpha" min="1" max="89" value="30" oninput="rampAlphaChange()">
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">Масса m (кг)</span><span class="param-val" id="newton-m1-ramp-val">5 кг</span></div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-mass" min="1" max="20" value="5" oninput="rampMassChange()">
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">Коэфф. трения μ</span><span class="param-val" id="ramp-mu-val">0.20</span></div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-mu" min="0" max="1.5" step="0.01" value="0.20" oninput="rampMuChange()">
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">Внеш. сила F (Н)</span><span class="param-val" id="ramp-force-val">0 Н</span></div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-force" min="0" max="80" value="0" oninput="rampForceChange()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roll sub-panel -->
|
||||
<div id="cl-sub-roll" style="display:none">
|
||||
<div class="param-block">
|
||||
<div class="param-header"><span class="param-name">Угол α</span><span class="param-val" id="roll-alpha-val">20°</span></div>
|
||||
<input type="range" class="param-slider" id="sl-roll-alpha" min="5" max="60" value="20" oninput="rollAlphaChange()">
|
||||
</div>
|
||||
<div class="gp-section-title" style="margin:4px 0">Режим</div>
|
||||
<div style="display:flex;gap:5px;margin-bottom:8px">
|
||||
<button class="mag-mode-btn roll-friction-btn active" data-val="false" onclick="rollFrictionToggle(false)" style="flex:1;font-size:.68rem">Качение</button>
|
||||
<button class="mag-mode-btn roll-friction-btn" data-val="true" onclick="rollFrictionToggle(true)" style="flex:1;font-size:.68rem">+Трение</button>
|
||||
</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-3);line-height:1.5;padding:4px 6px;background:rgba(255,255,255,0.03);border-radius:6px;border:1px solid var(--border);margin-bottom:6px">
|
||||
Шар: a = (5/7)g·sinα<br>
|
||||
Цилиндр: a = (2/3)g·sinα<br>
|
||||
Обруч: a = (1/2)g·sinα
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /#newton-classic-panel -->
|
||||
|
||||
<!-- Scene description -->
|
||||
<div id="newton-scene-desc" style="font-size:0.71rem;color:var(--text-3);line-height:1.6;margin-bottom:10px;padding:6px 8px;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid var(--border)">
|
||||
Закон инерции: тело движется равномерно при отсутствии сил.
|
||||
@@ -1777,6 +1846,117 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parachute -->
|
||||
<div class="gp-section-title" style="margin-top:6px">Парашют</div>
|
||||
<label class="tri-layer-row" id="chute-row" onclick="projToggleParachute(this)" style="margin-bottom:6px">
|
||||
<span class="tri-dot" style="background:rgba(6,214,224,0.5)"></span>
|
||||
<span class="tri-layer-name">Парашют</span>
|
||||
<span class="tri-toggle" id="chute-toggle" style="background:rgba(255,255,255,0.12)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px;transition:margin-left .15s"></span></span>
|
||||
</label>
|
||||
<div id="chute-params" style="display:none">
|
||||
<div class="param-block">
|
||||
<div class="param-header">
|
||||
<span class="param-name">Площадь A</span>
|
||||
<span class="param-val" id="p-chute-area">1.0 м²</span>
|
||||
</div>
|
||||
<input type="range" class="param-slider" id="sl-chute-area" min="1" max="100" value="10" oninput="projChuteAreaChange()">
|
||||
</div>
|
||||
<div class="param-block" style="margin-bottom:6px">
|
||||
<span class="param-name" style="font-size:.68rem">Форма (Cd)</span>
|
||||
<select id="sel-chute-cd" onchange="projChuteCdChange()" style="width:100%;margin-top:4px;background:#0d0d2a;color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:4px 6px;font-size:.75rem">
|
||||
<option value="1.5" selected>Парашют (1.5)</option>
|
||||
<option value="1.05">Куб (1.05)</option>
|
||||
<option value="0.47">Сфера (0.47)</option>
|
||||
<option value="0.42">Полусфера (0.42)</option>
|
||||
<option value="1.17">Плоский диск (1.17)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header">
|
||||
<span class="param-name">Раскрыть на h</span>
|
||||
<span class="param-val" id="p-chute-height">Сразу</span>
|
||||
</div>
|
||||
<input type="range" class="param-slider" id="sl-chute-height" min="0" max="100" value="0" oninput="projChuteHeightChange()">
|
||||
<div style="font-size:.6rem;color:var(--text-3);margin-top:2px">0 = немедленно, >0 = раскрыть на этой высоте</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ramp -->
|
||||
<div class="gp-section-title" style="margin-top:6px">Наклонная горка</div>
|
||||
<label class="tri-layer-row" id="ramp-row" onclick="projToggleRamp(this)" style="margin-bottom:6px">
|
||||
<span class="tri-dot" style="background:rgba(255,180,50,0.5)"></span>
|
||||
<span class="tri-layer-name">Старт с горки</span>
|
||||
<span class="tri-toggle" id="ramp-toggle" style="background:rgba(255,255,255,0.12)"><span style="display:block;width:12px;height:12px;border-radius:50%;background:#fff;margin:2px;margin-left:2px;transition:margin-left .15s"></span></span>
|
||||
</label>
|
||||
<div id="ramp-params" style="display:none">
|
||||
<div class="param-block">
|
||||
<div class="param-header">
|
||||
<span class="param-name">Угол горки α</span>
|
||||
<span class="param-val" id="p-ramp-angle">30°</span>
|
||||
</div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-angle" min="5" max="85" value="30" oninput="projRampChange()">
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header">
|
||||
<span class="param-name">Длина L</span>
|
||||
<span class="param-val" id="p-ramp-length">10 м</span>
|
||||
</div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-length" min="1" max="50" value="10" oninput="projRampChange()">
|
||||
</div>
|
||||
<div class="param-block">
|
||||
<div class="param-header">
|
||||
<span class="param-name">Трение μ</span>
|
||||
<span class="param-val" id="p-ramp-mu">0.10</span>
|
||||
</div>
|
||||
<input type="range" class="param-slider" id="sl-ramp-mu" min="0" max="80" value="10" oninput="projRampChange()">
|
||||
<div style="font-size:.6rem;color:var(--text-3);margin-top:2px">v = sqrt(2gL(sinα − μ cosα))</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Planet -->
|
||||
<div class="gp-section-title" style="margin-top:6px">Планета / гравитация</div>
|
||||
<div class="param-block" style="margin-bottom:6px">
|
||||
<select id="sel-planet" onchange="projPlanetChange()" style="width:100%;background:#0d0d2a;color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:4px 6px;font-size:.75rem">
|
||||
<option value="earth" selected>Земля — g=9.81</option>
|
||||
<option value="moon">Луна — g=1.62</option>
|
||||
<option value="mars">Марс — g=3.71</option>
|
||||
<option value="venus">Венера — g=8.87</option>
|
||||
<option value="jupiter">Юпитер — g=24.79</option>
|
||||
<option value="mercury">Меркурий — g=3.7</option>
|
||||
<option value="saturn">Сатурн — g=10.44</option>
|
||||
<option value="uranus">Уран — g=8.69</option>
|
||||
<option value="neptune">Нептун — g=11.15</option>
|
||||
<option value="pluto">Плутон — g=0.62</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="proj-planet-compare-btn" class="proj-preset-chip" onclick="projTogglePlanetCompare()" style="width:100%;text-align:left;display:flex;align-items:center;gap:6px;margin-bottom:4px">
|
||||
<svg class="ic" viewBox="0 0 24 24"><circle cx="5" cy="12" r="3"/><circle cx="12" cy="5" r="3"/><circle cx="19" cy="19" r="3"/><line x1="5" y1="12" x2="12" y2="5"/><line x1="12" y1="5" x2="19" y2="19"/></svg>
|
||||
<span>Сравн.планет: Выкл</span>
|
||||
</button>
|
||||
<div id="proj-planet-compare-panel" style="display:none;font-size:.65rem;color:rgba(255,255,255,.6);margin-bottom:4px">
|
||||
<div style="display:flex;gap:4px;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span style="color:#06D6E0;font-weight:700">1:</span>
|
||||
<select onchange="projPlanetCompareChange(0,this.value)" style="flex:1;background:#0d0d2a;color:#06D6E0;border:1px solid rgba(6,214,224,.3);border-radius:4px;padding:2px 4px;font-size:.7rem">
|
||||
<option value="earth" selected>Земля</option><option value="moon">Луна</option><option value="mars">Марс</option><option value="venus">Венера</option><option value="jupiter">Юпитер</option><option value="mercury">Меркурий</option><option value="saturn">Сатурн</option><option value="uranus">Уран</option><option value="neptune">Нептун</option><option value="pluto">Плутон</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span style="color:#7BF5A4;font-weight:700">2:</span>
|
||||
<select onchange="projPlanetCompareChange(1,this.value)" style="flex:1;background:#0d0d2a;color:#7BF5A4;border:1px solid rgba(123,245,164,.3);border-radius:4px;padding:2px 4px;font-size:.7rem">
|
||||
<option value="earth">Земля</option><option value="moon" selected>Луна</option><option value="mars">Марс</option><option value="venus">Венера</option><option value="jupiter">Юпитер</option><option value="mercury">Меркурий</option><option value="saturn">Сатурн</option><option value="uranus">Уран</option><option value="neptune">Нептун</option><option value="pluto">Плутон</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span style="color:#F15BB5;font-weight:700">3:</span>
|
||||
<select onchange="projPlanetCompareChange(2,this.value)" style="flex:1;background:#0d0d2a;color:#F15BB5;border:1px solid rgba(241,91,181,.3);border-radius:4px;padding:2px 4px;font-size:.7rem">
|
||||
<option value="earth">Земля</option><option value="moon">Луна</option><option value="mars" selected>Марс</option><option value="venus">Венера</option><option value="jupiter">Юпитер</option><option value="mercury">Меркурий</option><option value="saturn">Сатурн</option><option value="uranus">Уран</option><option value="neptune">Нептун</option><option value="pluto">Плутон</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gp-section-title" style="margin-top:6px">Пресеты</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||
<button class="proj-preset-chip" onclick="projPreset(20,45,0,9.81)">Земля 45°</button>
|
||||
@@ -2017,7 +2197,7 @@
|
||||
</svg>
|
||||
<span id="coll-launch-label">Запустить</span>
|
||||
</button>
|
||||
<button class="proj-reset-btn" onclick="cSim && cSim.reset(); _collSyncBtn()">
|
||||
<button class="proj-reset-btn" onclick="var _rs=_activeSim&&_activeSim();if(_rs)_rs.reset();_collSyncBtn()">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>
|
||||
</svg>
|
||||
@@ -2444,44 +2624,187 @@
|
||||
|
||||
<!-- ── PENDULUM sim body ── -->
|
||||
<div id="sim-pendulum" class="sim-proj-wrap" style="display:none">
|
||||
<!-- mode bar -->
|
||||
<div style="display:flex;gap:4px;padding:6px 12px;background:rgba(22,22,38,0.8);flex-wrap:wrap;border-bottom:1px solid rgba(255,255,255,0.07)">
|
||||
<button class="pend-mode-btn active" data-mode="math" onclick="pendSetMode('math')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(155,93,229,0.2);color:#ccc;cursor:pointer">Математ.</button>
|
||||
<button class="pend-mode-btn" data-mode="double" onclick="pendSetMode('double')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Двойной</button>
|
||||
<button class="pend-mode-btn" data-mode="coupled" onclick="pendSetMode('coupled')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Связанные</button>
|
||||
<button class="pend-mode-btn" data-mode="spring" onclick="pendSetMode('spring')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Пружинный</button>
|
||||
<button class="pend-mode-btn" data-mode="physical" onclick="pendSetMode('physical')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Физич.</button>
|
||||
<button class="pend-mode-btn" data-mode="foucault" onclick="pendSetMode('foucault')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Фуко</button>
|
||||
<button class="pend-mode-btn" data-mode="resonance" onclick="pendSetMode('resonance')" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Резонанс</button>
|
||||
<button id="btn-pend-phase" onclick="pendTogglePhase()" style="font-size:.72rem;padding:3px 10px;border-radius:14px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer;margin-left:auto">Фаз. портрет</button>
|
||||
</div>
|
||||
<div class="sim-body-wrap">
|
||||
<div class="proj-panel" style="width:220px;gap:0">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Параметры</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:55px">θ = <span id="pend-theta-val" style="color:var(--violet);font-weight:700">45</span>°</label>
|
||||
<input type="range" id="sl-pend-theta" min="5" max="170" step="1" value="45" oninput="pendParam('theta',this.value)" style="flex:1">
|
||||
<div class="proj-panel" style="width:230px;gap:0;overflow-y:auto;max-height:100%">
|
||||
|
||||
<!-- MATH params -->
|
||||
<div class="pend-params" data-mode="math">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Математич. маятник</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:55px">θ = <span id="pend-theta-val" style="color:var(--violet);font-weight:700">45</span>°</label>
|
||||
<input type="range" id="sl-pend-theta" min="5" max="170" step="1" value="45" oninput="pendParam('theta',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:55px">L = <span id="pend-L-val" style="color:var(--cyan);font-weight:700">200</span></label>
|
||||
<input type="range" id="sl-pend-L" min="60" max="300" step="5" value="200" oninput="pendParam('L',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:55px">g = <span id="pend-g-val" style="color:#FFD166;font-weight:700">9.81</span></label>
|
||||
<input type="range" id="sl-pend-g" min="1" max="25" step="0.1" value="9.81" oninput="pendParam('g',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:70px">Затух. <span id="pend-damp-val" style="color:#EF476F;font-weight:700">0</span></label>
|
||||
<input type="range" id="sl-pend-damp" min="0" max="2" step="0.05" value="0" oninput="pendParam('damping',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="gp-section-title" style="margin-bottom:6px;margin-top:8px">Пресеты</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
|
||||
<button class="preset-btn" onclick="pendPreset(45,200,9.81,0)">Земля</button>
|
||||
<button class="preset-btn" onclick="pendPreset(45,200,1.62,0)">Луна</button>
|
||||
<button class="preset-btn" onclick="pendPreset(170,200,9.81,0)">Большой θ</button>
|
||||
<button class="preset-btn" onclick="pendPreset(45,200,9.81,0.5)">Затухание</button>
|
||||
</div>
|
||||
<div class="pp-hint">Тащи грузик мышью для установки угла</div>
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:55px">L = <span id="pend-L-val" style="color:var(--cyan);font-weight:700">200</span></label>
|
||||
<input type="range" id="sl-pend-L" min="60" max="300" step="5" value="200" oninput="pendParam('L',this.value)" style="flex:1">
|
||||
|
||||
<!-- DOUBLE params -->
|
||||
<div class="pend-params" data-mode="double" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Двойной маятник</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">L1 = <span id="pend-d-L1-val" style="color:var(--cyan);font-weight:700">130</span></label>
|
||||
<input type="range" min="50" max="200" step="5" value="130" oninput="pendDoubleParam('L1',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">L2 = <span id="pend-d-L2-val" style="color:var(--cyan);font-weight:700">100</span></label>
|
||||
<input type="range" min="50" max="200" step="5" value="100" oninput="pendDoubleParam('L2',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">m1 = <span id="pend-d-m1-val" style="color:#FFD166;font-weight:700">1.5</span></label>
|
||||
<input type="range" min="0.5" max="4" step="0.1" value="1.5" oninput="pendDoubleParam('m1',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">m2 = <span id="pend-d-m2-val" style="color:#FFD166;font-weight:700">1.0</span></label>
|
||||
<input type="range" min="0.5" max="4" step="0.1" value="1.0" oninput="pendDoubleParam('m2',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">θ1 = <span id="pend-d-th1-val" style="color:var(--violet);font-weight:700">108</span>°</label>
|
||||
<input type="range" min="10" max="170" step="1" value="108" oninput="pendDoubleParam('th1',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">θ2 = <span id="pend-d-th2-val" style="color:var(--violet);font-weight:700">72</span>°</label>
|
||||
<input type="range" min="10" max="170" step="1" value="72" oninput="pendDoubleParam('th2',this.value)" style="flex:1">
|
||||
</div>
|
||||
<button id="btn-pend-ghost" onclick="pendToggleGhost()" style="width:100%;margin-top:6px;font-size:.75rem;padding:5px;border-radius:8px;border:1px solid rgba(239,71,111,0.4);background:rgba(239,71,111,0.1);color:#EF476F;cursor:pointer">Сравнить траектории (хаос)</button>
|
||||
<div class="pp-hint" style="margin-top:6px">Чувствительность к нач. условиям (+0.001 рад)</div>
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:55px">g = <span id="pend-g-val" style="color:#FFD166;font-weight:700">9.81</span></label>
|
||||
<input type="range" id="sl-pend-g" min="1" max="25" step="0.1" value="9.81" oninput="pendParam('g',this.value)" style="flex:1">
|
||||
|
||||
<!-- COUPLED params -->
|
||||
<div class="pend-params" data-mode="coupled" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Связанные маятники</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">L = <span id="pend-cp-L-val" style="color:var(--cyan);font-weight:700">160</span></label>
|
||||
<input type="range" min="80" max="260" step="5" value="160" oninput="pendCoupledParam('L',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">k = <span id="pend-cp-k-val" style="color:#FFD166;font-weight:700">0.30</span></label>
|
||||
<input type="range" min="0.01" max="2" step="0.01" value="0.3" oninput="pendCoupledParam('k',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">θ1 = <span id="pend-cp-th1-val" style="color:var(--violet);font-weight:700">36</span>°</label>
|
||||
<input type="range" min="5" max="90" step="1" value="36" oninput="pendCoupledParam('th1',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="pp-hint">θ2=0: энергия перекачивается от маятника 1 к маятнику 2 и обратно</div>
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:70px">Затух. <span id="pend-damp-val" style="color:#EF476F;font-weight:700">0</span></label>
|
||||
<input type="range" id="sl-pend-damp" min="0" max="2" step="0.05" value="0" oninput="pendParam('damping',this.value)" style="flex:1">
|
||||
|
||||
<!-- SPRING params -->
|
||||
<div class="pend-params" data-mode="spring" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Пружинный маятник</div>
|
||||
<div style="display:flex;gap:4px;margin-bottom:10px">
|
||||
<button class="sp-mode-btn active" data-m="vert" onclick="pendSpringMode('vert')" style="flex:1;font-size:.72rem;padding:4px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(6,214,224,0.15);color:#06D6E0;cursor:pointer">Вертик.</button>
|
||||
<button class="sp-mode-btn" data-m="horiz" onclick="pendSpringMode('horiz')" style="flex:1;font-size:.72rem;padding:4px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Горизонт.</button>
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">k = <span id="pend-sp-k-val" style="color:#FFD166;font-weight:700">20</span></label>
|
||||
<input type="range" min="1" max="100" step="1" value="20" oninput="pendSpringParam('k',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">m = <span id="pend-sp-m-val" style="color:var(--violet);font-weight:700">1</span> кг</label>
|
||||
<input type="range" min="0.1" max="5" step="0.1" value="1" oninput="pendSpringParam('m',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">x0 = <span id="pend-sp-x0-val" style="color:#EF476F;font-weight:700">8</span> см</label>
|
||||
<input type="range" min="1" max="20" step="0.5" value="8" oninput="pendSpringParam('x0',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="pp-hint">T = 2π√(m/k) не зависит от g (горизонт.)</div>
|
||||
</div>
|
||||
<div style="margin-top:8px"></div>
|
||||
<div class="gp-section-title" style="margin-bottom:6px">Пресеты</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">
|
||||
<button class="preset-btn" onclick="pendPreset(45,200,9.81,0)">Земля</button>
|
||||
<button class="preset-btn" onclick="pendPreset(45,200,1.62,0)">Луна</button>
|
||||
<button class="preset-btn" onclick="pendPreset(170,200,9.81,0)">Большой θ</button>
|
||||
<button class="preset-btn" onclick="pendPreset(45,200,9.81,0.5)">Затухание</button>
|
||||
|
||||
<!-- PHYSICAL params -->
|
||||
<div class="pend-params" data-mode="physical" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Физический маятник</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:10px">
|
||||
<button class="ph-shape-btn active" data-s="rod" onclick="pendPhysShape('rod')" style="font-size:.72rem;padding:3px 8px;border-radius:8px;border:1px solid rgba(155,93,229,0.4);background:rgba(155,93,229,0.15);color:#9B5DE5;cursor:pointer">Стержень</button>
|
||||
<button class="ph-shape-btn" data-s="hoop" onclick="pendPhysShape('hoop')" style="font-size:.72rem;padding:3px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Обруч</button>
|
||||
<button class="ph-shape-btn" data-s="disk" onclick="pendPhysShape('disk')" style="font-size:.72rem;padding:3px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Диск</button>
|
||||
<button class="ph-shape-btn" data-s="rect" onclick="pendPhysShape('rect')" style="font-size:.72rem;padding:3px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(22,22,38,0.6);color:#ccc;cursor:pointer">Прямоуг.</button>
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">L = <span id="pend-ph-L-val" style="color:var(--cyan);font-weight:700">200</span></label>
|
||||
<input type="range" min="60" max="260" step="5" value="200" oninput="pendPhysParam('L',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">θ = <span id="pend-ph-theta-val" style="color:var(--violet);font-weight:700">36</span>°</label>
|
||||
<input type="range" min="5" max="120" step="1" value="36" oninput="pendPhysParam('theta',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">Затух. <span id="pend-ph-damping-val" style="color:#EF476F;font-weight:700">0</span></label>
|
||||
<input type="range" min="0" max="1" step="0.02" value="0" oninput="pendPhysParam('damping',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="pp-hint">T зависит от момента инерции I и расст. до ц.т. d</div>
|
||||
</div>
|
||||
<div class="pp-hint">Тащи грузик мышью для установки угла</div>
|
||||
|
||||
<!-- FOUCAULT params -->
|
||||
<div class="pend-params" data-mode="foucault" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Маятник Фуко</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:65px">φ = <span id="pend-fc-phi-val" style="color:#FFD166;font-weight:700">45</span>°</label>
|
||||
<input type="range" min="1" max="90" step="1" value="45" oninput="pendFoucaultParam('phi',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="pp-hint">Вид сверху. Плоскость качаний вращаетс�; из-за вращения Земли. T = 24 ч / sin(φ)</div>
|
||||
<div class="pp-hint" style="color:#FFD166;margin-top:4px">Полюс (90°): оборот за 24 ч. Экватор: почти не вращается.</div>
|
||||
</div>
|
||||
|
||||
<!-- RESONANCE params -->
|
||||
<div class="pend-params" data-mode="resonance" style="display:none">
|
||||
<div class="gp-section-title" style="margin-bottom:8px">Резонанс</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:70px">ω = <span id="pend-rs-dOmega-val" style="color:#EF476F;font-weight:700">1.50</span></label>
|
||||
<input type="range" min="0.1" max="6" step="0.05" value="1.5" oninput="pendResonanceParam('dOmega',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:70px">F0 = <span id="pend-rs-F0-val" style="color:#FFD166;font-weight:700">0.80</span></label>
|
||||
<input type="range" min="0.1" max="3" step="0.05" value="0.8" oninput="pendResonanceParam('F0',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:70px">γ = <span id="pend-rs-gamma-val" style="color:var(--cyan);font-weight:700">0.30</span></label>
|
||||
<input type="range" min="0.01" max="2" step="0.01" value="0.3" oninput="pendResonanceParam('gamma',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="proj-slider-row" style="margin-bottom:8px">
|
||||
<label style="font-size:.78rem;color:#ccc;width:70px">L = <span id="pend-rs-L-val" style="color:var(--violet);font-weight:700">180</span></label>
|
||||
<input type="range" min="60" max="300" step="5" value="180" oninput="pendResonanceParam('L',this.value)" style="flex:1">
|
||||
</div>
|
||||
<div class="pp-hint">Красная стрелка — вынуждающая сила. Пик при ω ≈ ω₀.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="proj-canvas-outer">
|
||||
<canvas id="pendulum-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proj-stats-bar" id="pendbar">
|
||||
<div class="pstat"><div class="pstat-label">Угол</div><div class="pstat-val" id="pendbar-v1" style="color:var(--violet)">45°</div></div>
|
||||
<div class="pstat"><div class="pstat-label">ω</div><div class="pstat-val" id="pendbar-v2" style="color:var(--cyan)">0</div></div>
|
||||
<div class="pstat"><div class="pstat-label">Период T</div><div class="pstat-val" id="pendbar-v3" style="color:#FFD166">—</div></div>
|
||||
<div class="pstat"><div class="pstat-label">Энергия</div><div class="pstat-val" id="pendbar-v4" style="color:#EF476F">—</div></div>
|
||||
<div class="pstat"><div class="pstat-label">Угол / коорд.</div><div class="pstat-val" id="pendbar-v1" style="color:var(--violet)">45°</div></div>
|
||||
<div class="pstat"><div class="pstat-label">ω</div><div class="pstat-val" id="pendbar-v2" style="color:var(--cyan)">0</div></div>
|
||||
<div class="pstat"><div class="pstat-label">Период T</div><div class="pstat-val" id="pendbar-v3" style="color:#FFD166">—</div></div>
|
||||
<div class="pstat"><div class="pstat-label">Режим</div><div class="pstat-val" id="pendbar-v4" style="color:#EF476F">—</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user