Files
Maxim Dolgolyov bc64828b22 feat(phys9 flagships): F3 тахометр+спидометр + F4 орбита (Wave A продолжение)
F3. Тахометр + спидометр + одометр (§11 в ch1):
- Canvas 640×400 с 3 аналоговыми приборами вверху
  (тахометр a, спидометр v, одометр Δx mod 1000)
- Графики v(t) и a(t) внизу с горизонтальной цель-линией
- Кнопки «Газ»/«Тормоз» удержанием, «Отпустить» — coast-режим
  (лёгкое торможение от трения)
- Slider'ы: a_газа, |a_тормоз|, цель скорости (по умолчанию 16.7 м/с)
- Рекорд скорости в localStorage
- Feedback при достижении цели

F4. Орбитальный конструктор (§17 в ch2):
- Canvas 640×480 (космос со звёздами)
- Планета (Земля) в центре, спутник запускается с r=200
- Slider'ы: M, v₀, угол α
- Кнопки: Запустить/Сброс/«Круговая орбита» (вычисляет v=√(M/r))
- Физика: F=GM/r² (G=1), Эйлер 8 шагов/кадр
- Trail орбиты до 1500 точек
- Классификация: падение/круговая/эллипс/убегание
- Период T через переход через ось x
- Feedback при крайних случаях

Подключение:
- ch1: phys9_flag_F3_dashboard.js + хук на p11
- ch2: phys9-flagships.css + base + F4 + хук на p17

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:10:33 +03:00

231 lines
9.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// F4. Орбитальный конструктор (§17-18) — запусти спутник, увидь орбиту.
(function(){
'use strict';
const B = () => window.PHYS9_FLAG_BASE;
const C = () => window.PHYS9_COLORS || {};
function init(secId){
if (!B()) return false;
const body = ''
+ '<div class="flag-sliders">'
+ '<label>Масса центра (M, у.е.): <b id="F4-Mv">10000</b><input type="range" id="F4-M" min="1000" max="30000" step="500" value="10000"></label>'
+ '<label>Скорость запуска $v_0$ (px/с): <b id="F4-v0v">120</b><input type="range" id="F4-v0" min="20" max="300" step="5" value="120"></label>'
+ '<label>Угол запуска α, &#176;: <b id="F4-anv">90</b><input type="range" id="F4-an" min="0" max="360" step="5" value="90"></label>'
+ '</div>'
+ '<canvas id="F4-cv" class="flag-canvas" width="640" height="480" style="height:480px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F4-go">Запустить</button>'
+ '<button class="flag-btn" id="F4-reset">Сброс</button>'
+ '<button class="flag-btn" id="F4-circle">Круговая орбита</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Тип орбиты</span><span class="val" id="F4-type">—</span></div>'
+ '<div class="flag-stat"><span class="lbl">$T$ (период, если эллипс)</span><span class="val" id="F4-T">—</span></div>'
+ '<div class="flag-stat"><span class="lbl">$|r|$ от центра</span><span class="val" id="F4-r">200 px</span></div>'
+ '<div class="flag-stat"><span class="lbl">$|v|$</span><span class="val" id="F4-v">120 px/с</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F4-fb"></div>';
const card = B().makeCard(secId,
'F4. Орбитальный конструктор',
'Запусти спутник с разной начальной скоростью. Слишком мало — упадёт. Точно — круговая. Чуть больше — эллипс. Слишком много — улетит навсегда.',
body);
if (!card) return false;
const cv = document.getElementById('F4-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const cx = W/2, cy = H/2;
const planetR = 30;
const startR = 200; /* начальное расстояние от центра */
let st = {
x: cx + startR, y: cy,
vx: 0, vy: -120,
trail: [],
running: false,
crashed: false,
escaped: false,
/* для определения периода — фиксируем угол при старте */
lastAngle: 0,
revolutions: 0,
tStart: 0,
t: 0
};
function readSliders(){
document.getElementById('F4-Mv').textContent = (+document.getElementById('F4-M').value).toFixed(0);
document.getElementById('F4-v0v').textContent = (+document.getElementById('F4-v0').value).toFixed(0);
document.getElementById('F4-anv').textContent = (+document.getElementById('F4-an').value).toFixed(0);
}
function reset(){
const v0 = +document.getElementById('F4-v0').value;
const an = +document.getElementById('F4-an').value;
st.x = cx + startR; st.y = cy;
st.vx = v0 * Math.cos(an*Math.PI/180);
st.vy = -v0 * Math.sin(an*Math.PI/180);
st.trail = [];
st.crashed = false; st.escaped = false;
st.running = false;
st.t = 0; st.revolutions = 0; st.tStart = 0;
st.lastAngle = Math.atan2(st.y - cy, st.x - cx);
document.getElementById('F4-fb').className = 'flag-feedback';
document.getElementById('F4-go').textContent = 'Запустить';
draw();
}
function setCircular(){
/* для круговой v = sqrt(G*M/r) — у нас G=1, M из slider'a */
const M = +document.getElementById('F4-M').value;
const vCirc = Math.sqrt(M / startR);
document.getElementById('F4-v0').value = Math.round(vCirc);
document.getElementById('F4-an').value = 90;
readSliders();
reset();
}
function tick(dt){
if (!st.running || st.crashed || st.escaped) { draw(); return; }
/* Гравитация: F = G*M*m/r², a = G*M/r² к центру */
const M = +document.getElementById('F4-M').value;
/* RK4 was bы лучше но Эйлер с малым шагом норм */
const N = 8;
const ddt = dt / N;
for (let i = 0; i < N; i++){
const dx = cx - st.x, dy = cy - st.y;
const r2 = dx*dx + dy*dy;
const r = Math.sqrt(r2);
if (r < planetR){ st.crashed = true; break; }
if (r > 2000){ st.escaped = true; break; }
const aMag = M / r2;
const ax = aMag * dx / r, ay = aMag * dy / r;
st.vx += ax * ddt;
st.vy += ay * ddt;
st.x += st.vx * ddt;
st.y += st.vy * ddt;
st.t += ddt;
/* trail */
if (i === N-1) st.trail.push({ x: st.x, y: st.y });
if (st.trail.length > 1500) st.trail.shift();
/* определение периода — переход через ось x */
const ang = Math.atan2(st.y - cy, st.x - cx);
if (st.lastAngle > Math.PI*0.9 && ang < -Math.PI*0.9 || st.lastAngle < -Math.PI*0.9 && ang > Math.PI*0.9){
/* skip — wrap */
} else if (st.lastAngle < 0 && ang >= 0){
st.revolutions++;
if (st.revolutions === 1) st.tStart = st.t;
}
st.lastAngle = ang;
}
const fb = document.getElementById('F4-fb');
if (st.crashed){
st.running = false;
document.getElementById('F4-go').textContent = 'Запустить';
fb.className = 'flag-feedback fail show';
fb.innerHTML = '&#10007; Спутник упал на планету! Нужна бо́льшая начальная скорость.';
} else if (st.escaped){
st.running = false;
document.getElementById('F4-go').textContent = 'Запустить';
fb.className = 'flag-feedback warn show';
fb.innerHTML = 'Спутник улетел! Это вторая космическая (~$\\sqrt 2 v_{круг}$). Уменьши скорость.';
}
/* статистика */
const dx = st.x - cx, dy = st.y - cy;
const r = Math.sqrt(dx*dx + dy*dy);
const v = Math.sqrt(st.vx*st.vx + st.vy*st.vy);
document.getElementById('F4-r').textContent = r.toFixed(0) + ' px';
document.getElementById('F4-v').textContent = v.toFixed(0) + ' px/с';
/* классификация */
const Mclassify = +document.getElementById('F4-M').value;
const vCirc = Math.sqrt(Mclassify / startR);
const ratio = +document.getElementById('F4-v0').value / vCirc;
let type;
if (st.crashed) type = '✗ падение';
else if (st.escaped) type = '↗ убегание';
else if (Math.abs(ratio - 1) < 0.05) type = '○ круговая';
else if (ratio < 0.95) type = '⬭ эллипс (ближе к планете)';
else if (ratio < 1.41) type = '⬮ эллипс (дальше)';
else type = '↗ убегание';
document.getElementById('F4-type').textContent = type;
if (st.revolutions >= 2 && st.tStart > 0){
const T = (st.t - st.tStart) / (st.revolutions - 1);
document.getElementById('F4-T').textContent = T.toFixed(2) + ' с';
}
draw();
}
function draw(){
const col = C();
ctx.fillStyle = col.bg || '#fafafa';
ctx.fillRect(0, 0, W, H);
/* звёзды для красоты */
ctx.fillStyle = col.textMuted || '#94a3b8';
for (let i = 0; i < 60; i++){
const sx = (i * 37 + 17) % W;
const sy = (i * 61 + 23) % H;
ctx.fillRect(sx, sy, 1, 1);
}
/* планета (Земля) */
ctx.fillStyle = col.forceGravity || '#2563eb';
ctx.beginPath(); ctx.arc(cx, cy, planetR, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px Inter,sans-serif';
ctx.textAlign = 'center';
ctx.fillText('M', cx, cy + 4);
ctx.textAlign = 'left';
/* trail */
if (st.trail.length > 1){
ctx.strokeStyle = col.velocity || '#0891b2';
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.moveTo(st.trail[0].x, st.trail[0].y);
for (let i = 1; i < st.trail.length; i++) ctx.lineTo(st.trail[i].x, st.trail[i].y);
ctx.stroke();
}
/* спутник */
ctx.fillStyle = st.crashed ? col.fail : (col.acceleration || '#ea580c');
ctx.beginPath(); ctx.arc(st.x, st.y, 7, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.2; ctx.stroke();
/* вектор v */
if (st.running || st.t === 0){
const v = Math.sqrt(st.vx*st.vx + st.vy*st.vy);
const k = v > 0 ? 40 / v : 0;
B().arrow(ctx, st.x, st.y, st.x + st.vx*k, st.y + st.vy*k, col.velocity || '#0891b2', 2);
}
}
document.getElementById('F4-go').addEventListener('click', ()=>{
if (st.crashed || st.escaped) reset();
if (!st.running) {
reset();
st.running = true;
document.getElementById('F4-go').textContent = 'Пауза';
} else {
st.running = false;
document.getElementById('F4-go').textContent = 'Запустить';
}
});
document.getElementById('F4-reset').addEventListener('click', reset);
document.getElementById('F4-circle').addEventListener('click', setCircular);
['F4-M','F4-v0','F4-an'].forEach(id => document.getElementById(id).addEventListener('input', ()=>{
readSliders();
if (!st.running) reset();
}));
readSliders();
reset();
B().startLoop('F4', cv, tick);
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F4', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F4', { init: init, cleanup: function(){} });
});
})();