4d53919e9a
F9. Конструктор моста (§28 в ch3): - Canvas 700×380: балка на 2 опорах - Палитра грузов 50/100/200/500 кг (drag&drop) + тест 1000 кг - Расчёт реакций опор через ΣF=0 + ΣM=0 - При перегрузке (>8000 Н) балка ломается визуально F11. Бильярдная физика (§32 в ch4): - Canvas 700×380, зелёный стол, 4 шара - Тяни мышью от битка → прицельный вектор, отпусти → удар - Реальные упругие столкновения по нормали - Трение поля, отскоки от бортов, trails - Stats: Σ p, Σ Ek F19. Полёт ракеты (final4, финальный босс): - Canvas 700×420 — космос со звёздами, Земля внизу - 4 slider'а: m₀, m_f, v_газов, расход q - Реальная физика: тяга F = q·u, g(h), сопротивление ρ(h)e^(-h/8000) - Анимация ракеты с пламенем + перемещение по высоте - Цель: 400 км (МКС) - При успехе: +150 XP, localStorage 'phys9_F19_success' Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
228 lines
10 KiB
JavaScript
228 lines
10 KiB
JavaScript
// F19. Полёт ракеты (финал курса в ch4) — Циолковский + гравитация + сопротивление.
|
||
(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_0$, т: <b id="F19-m0v">5</b><input type="range" id="F19-m0" min="1" max="20" step="0.5" value="5"></label>'
|
||
+ '<label>Топливо $m_f$, т: <b id="F19-mfv">95</b><input type="range" id="F19-mf" min="10" max="500" step="5" value="95"></label>'
|
||
+ '<label>$v_{газов}$, м/с: <b id="F19-uv">3000</b><input type="range" id="F19-u" min="1000" max="5000" step="100" value="3000"></label>'
|
||
+ '<label>Расход топлива, т/с: <b id="F19-qv">2</b><input type="range" id="F19-q" min="0.2" max="10" step="0.1" value="2"></label>'
|
||
+ '</div>'
|
||
+ '<canvas id="F19-cv" class="flag-canvas" width="700" height="420" style="height:420px"></canvas>'
|
||
+ '<div class="flag-controls">'
|
||
+ '<button class="flag-btn primary" id="F19-go">ЗАПУСК!</button>'
|
||
+ '<button class="flag-btn" id="F19-reset">Сброс</button>'
|
||
+ '</div>'
|
||
+ '<div class="flag-stats">'
|
||
+ '<div class="flag-stat"><span class="lbl">Время</span><span class="val" id="F19-t">0 с</span></div>'
|
||
+ '<div class="flag-stat"><span class="lbl">Высота</span><span class="val" id="F19-h">0 м</span></div>'
|
||
+ '<div class="flag-stat"><span class="lbl">Скорость</span><span class="val" id="F19-v">0 м/с</span></div>'
|
||
+ '<div class="flag-stat"><span class="lbl">Топливо</span><span class="val" id="F19-fuel">100%</span></div>'
|
||
+ '<div class="flag-stat"><span class="lbl">Перегрузка</span><span class="val" id="F19-g">0 g</span></div>'
|
||
+ '</div>'
|
||
+ '<div class="flag-feedback" id="F19-fb"></div>';
|
||
|
||
const card = B().makeCard(secId,
|
||
'F19. Полёт ракеты <span class="flag-medal">МАГИСТР</span>',
|
||
'Запусти ракету с Земли на орбиту 400 км (МКС). Физика: тяга по Циолковскому, сила тяжести g(h), сопротивление атмосферы. Финальный босс курса!',
|
||
body);
|
||
if (!card) return false;
|
||
|
||
const cv = document.getElementById('F19-cv');
|
||
const ctx = cv.getContext('2d');
|
||
const W = cv.width, H = cv.height;
|
||
|
||
let st = { t: 0, h: 0, v: 0, m: 0, mf: 0, running: false, ended: false, history: [] };
|
||
|
||
function readSliders(){
|
||
document.getElementById('F19-m0v').textContent = (+document.getElementById('F19-m0').value).toFixed(1);
|
||
document.getElementById('F19-mfv').textContent = (+document.getElementById('F19-mf').value).toFixed(0);
|
||
document.getElementById('F19-uv').textContent = (+document.getElementById('F19-u').value).toFixed(0);
|
||
document.getElementById('F19-qv').textContent = (+document.getElementById('F19-q').value).toFixed(1);
|
||
}
|
||
|
||
function reset(){
|
||
const m0 = +document.getElementById('F19-m0').value * 1000; /* кг */
|
||
const mf = +document.getElementById('F19-mf').value * 1000;
|
||
st = { t: 0, h: 0, v: 0, m: m0 + mf, mf: mf, m0: m0, mf0: mf, running: false, ended: false, history: [] };
|
||
document.getElementById('F19-go').textContent = 'ЗАПУСК!';
|
||
document.getElementById('F19-fb').className = 'flag-feedback';
|
||
draw();
|
||
}
|
||
|
||
function tick(dt){
|
||
if (!st.running || st.ended) { draw(); return; }
|
||
const u = +document.getElementById('F19-u').value;
|
||
const q = +document.getElementById('F19-q').value * 1000; /* кг/с */
|
||
const G = 6.674e-11, M = 5.972e24, R = 6.371e6;
|
||
const N = 4;
|
||
const ddt = dt / N;
|
||
for (let i = 0; i < N; i++){
|
||
const dm = Math.min(st.mf, q * ddt);
|
||
const thrust = (dm > 1e-3) ? q * u : 0;
|
||
const g = G * M / Math.pow(R + st.h, 2);
|
||
/* атм сопротивление: плотность падает с высотой, упрощённо */
|
||
const rho = st.h < 80000 ? 1.225 * Math.exp(-st.h/8000) : 0;
|
||
const drag = 0.3 * rho * st.v * Math.abs(st.v) * 5; /* k*ρ*v²*A */
|
||
const F_net = thrust - st.m * g - Math.sign(st.v) * drag;
|
||
const a = F_net / st.m;
|
||
st.v += a * ddt;
|
||
st.h += st.v * ddt;
|
||
st.mf = Math.max(0, st.mf - dm);
|
||
st.m = st.m0 + st.mf;
|
||
st.t += ddt;
|
||
st.history.push({ t: st.t, h: st.h, v: st.v });
|
||
if (st.history.length > 1000) st.history.shift();
|
||
/* окончание */
|
||
if (st.h < 0){ st.ended = true; st.running = false; document.getElementById('F19-go').textContent='ЗАПУСК!'; break; }
|
||
if (st.t > 600){ st.ended = true; st.running = false; document.getElementById('F19-go').textContent='ЗАПУСК!'; break; }
|
||
}
|
||
/* stats */
|
||
document.getElementById('F19-t').textContent = st.t.toFixed(1) + ' с';
|
||
document.getElementById('F19-h').textContent = (st.h/1000).toFixed(1) + ' км';
|
||
document.getElementById('F19-v').textContent = st.v.toFixed(0) + ' м/с';
|
||
document.getElementById('F19-fuel').textContent = (st.mf/st.mf0*100).toFixed(0) + '%';
|
||
/* перегрузка во время тяги */
|
||
const u2 = +document.getElementById('F19-u').value;
|
||
const q2 = +document.getElementById('F19-q').value * 1000;
|
||
const thrust2 = (st.mf > 1e-3) ? q2 * u2 : 0;
|
||
const G2 = 6.674e-11, M2 = 5.972e24, R2 = 6.371e6;
|
||
const g2 = G2 * M2 / Math.pow(R2 + st.h, 2);
|
||
const a2 = (thrust2 - st.m * g2) / st.m;
|
||
document.getElementById('F19-g').textContent = ((a2/9.8) + 1).toFixed(2) + ' g';
|
||
/* feedback */
|
||
const fb = document.getElementById('F19-fb');
|
||
if (st.ended && st.h < 1){
|
||
fb.className = 'flag-feedback fail show';
|
||
fb.innerHTML = '✗ КРАШ. Ракета упала на Землю — не хватило топлива/тяги.';
|
||
} else if (st.h >= 400000 && Math.abs(st.v) < 100 && st.ended){
|
||
fb.className = 'flag-feedback ok show';
|
||
fb.innerHTML = '✓ УСПЕХ! Ракета на орбите МКС (400 км). Магистр Физики 9 — Вы!';
|
||
try { localStorage.setItem('phys9_F19_success', '1'); if(window.addXp) window.addXp(150, 'F19-magistr'); } catch(e){}
|
||
} else if (st.h > 400000){
|
||
fb.className = 'flag-feedback warn show';
|
||
fb.innerHTML = 'Высоко! Но нужна ещё горизонтальная скорость для орбиты ($\\sim 7.7$ км/с). Это упрощённая 1D модель.';
|
||
}
|
||
draw();
|
||
}
|
||
|
||
function draw(){
|
||
const col = C();
|
||
ctx.fillStyle = '#0f172a'; /* космос */
|
||
ctx.fillRect(0, 0, W, H);
|
||
/* звёзды */
|
||
ctx.fillStyle = '#fff';
|
||
for (let i = 0; i < 80; i++) ctx.fillRect((i*37+11)%W, (i*73+19)%H, 1, 1);
|
||
/* Земля внизу */
|
||
const earthY = H - 30;
|
||
ctx.fillStyle = '#16a34a';
|
||
ctx.fillRect(0, earthY, W, 30);
|
||
ctx.fillStyle = col.forceGravity || '#2563eb';
|
||
ctx.beginPath(); ctx.arc(W/2, earthY + 100, 80, 0, Math.PI*2); ctx.fill();
|
||
/* шкала высоты по правому краю */
|
||
ctx.fillStyle = '#fff';
|
||
ctx.font = '10px Inter,sans-serif';
|
||
const maxH = Math.max(450000, st.h * 1.1);
|
||
for (let k = 0; k <= 4; k++){
|
||
const h = maxH * k/4;
|
||
const py = earthY - (h/maxH) * (earthY - 40);
|
||
ctx.fillText((h/1000).toFixed(0) + ' км', W - 60, py + 3);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
|
||
ctx.beginPath(); ctx.moveTo(40, py); ctx.lineTo(W - 65, py); ctx.stroke();
|
||
}
|
||
/* 400 км — целевая орбита */
|
||
const targetY = earthY - (400000/maxH) * (earthY - 40);
|
||
ctx.strokeStyle = '#fbbf24';
|
||
ctx.setLineDash([8, 5]);
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath(); ctx.moveTo(0, targetY); ctx.lineTo(W, targetY); ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
ctx.fillStyle = '#fbbf24';
|
||
ctx.font = 'bold 11px Inter,sans-serif';
|
||
ctx.fillText('★ цель: 400 км (МКС)', 10, targetY - 4);
|
||
/* Ракета */
|
||
const rocketX = 100;
|
||
const rocketY = earthY - (st.h/maxH) * (earthY - 40);
|
||
ctx.save();
|
||
ctx.translate(rocketX, rocketY);
|
||
/* корпус */
|
||
ctx.fillStyle = '#e5e7eb';
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -22);
|
||
ctx.lineTo(7, -16);
|
||
ctx.lineTo(7, 14);
|
||
ctx.lineTo(-7, 14);
|
||
ctx.lineTo(-7, -16);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.strokeStyle = '#0f172a';
|
||
ctx.stroke();
|
||
/* окошко */
|
||
ctx.fillStyle = '#0891b2';
|
||
ctx.beginPath(); ctx.arc(0, -8, 3, 0, Math.PI*2); ctx.fill();
|
||
/* плавники */
|
||
ctx.fillStyle = '#dc2626';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-7, 14); ctx.lineTo(-12, 22); ctx.lineTo(-7, 18); ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.moveTo(7, 14); ctx.lineTo(12, 22); ctx.lineTo(7, 18); ctx.fill();
|
||
/* выхлоп */
|
||
if (st.running && st.mf > 0){
|
||
ctx.fillStyle = '#fbbf24';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-5, 18);
|
||
ctx.lineTo(0, 18 + 10 + Math.random()*8);
|
||
ctx.lineTo(5, 18);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#dc2626';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-3, 18);
|
||
ctx.lineTo(0, 18 + 6 + Math.random()*4);
|
||
ctx.lineTo(3, 18);
|
||
ctx.fill();
|
||
}
|
||
ctx.restore();
|
||
/* график высота(t) справа */
|
||
if (st.history.length > 1){
|
||
ctx.strokeStyle = '#fbbf24';
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath();
|
||
for (let i = 0; i < st.history.length; i++){
|
||
const p = st.history[i];
|
||
const py = earthY - (p.h/maxH) * (earthY - 40);
|
||
const px = 200 + (p.t/Math.max(60, st.t)) * (W - 290);
|
||
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
document.getElementById('F19-go').addEventListener('click', ()=>{
|
||
if (st.ended) reset();
|
||
st.running = !st.running;
|
||
document.getElementById('F19-go').textContent = st.running ? 'ПАУЗА' : 'ЗАПУСК!';
|
||
});
|
||
document.getElementById('F19-reset').addEventListener('click', reset);
|
||
['F19-m0','F19-mf','F19-u','F19-q'].forEach(id => document.getElementById(id).addEventListener('input', ()=>{
|
||
readSliders();
|
||
if (!st.running) reset();
|
||
}));
|
||
|
||
readSliders();
|
||
reset();
|
||
B().startLoop('F19', cv, tick);
|
||
return true;
|
||
}
|
||
|
||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F19', { init: init, cleanup: function(){} });
|
||
else document.addEventListener('DOMContentLoaded', ()=>{
|
||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F19', { init: init, cleanup: function(){} });
|
||
});
|
||
|
||
})();
|