bc64828b22
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>
231 lines
9.7 KiB
JavaScript
231 lines
9.7 KiB
JavaScript
// 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>Угол запуска α, °: <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 = '✗ Спутник упал на планету! Нужна бо́льшая начальная скорость.';
|
||
} 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(){} });
|
||
});
|
||
|
||
})();
|