feat(phys9 flagships): F9 мост + F11 бильярд + F19 ракета (Wave C+D+финал)

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>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 10:19:55 +03:00
parent da6dd96aac
commit 4d53919e9a
5 changed files with 657 additions and 2 deletions
@@ -0,0 +1,217 @@
// F11. Бильярдная физика (§32 в 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 style="margin-bottom:8px;font-size:.85rem;color:var(--muted)">Тяни мышью от белого шара — увидь прицельный вектор. Отпусти — удар. Длина перетягивания = сила удара.</div>'
+ '<canvas id="F11-cv" class="flag-canvas" width="700" height="380" style="height:380px;cursor:grab"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F11-reset">Расставить шары</button>'
+ '<button class="flag-btn" id="F11-clear">Очистить трассы</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Σ импульса (до)</span><span class="val" id="F11-p0">0</span></div>'
+ '<div class="flag-stat"><span class="lbl">Σ импульса (сейчас)</span><span class="val" id="F11-pn">0</span></div>'
+ '<div class="flag-stat"><span class="lbl">Σ кин. энергии</span><span class="val" id="F11-E">0 Дж</span></div>'
+ '<div class="flag-stat"><span class="lbl">Кол-во шаров</span><span class="val" id="F11-n">4</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F11-fb"></div>';
const card = B().makeCard(secId,
'F11. Бильярдная физика',
'Упругий удар сохраняет и импульс, и кинетическую энергию. Σ p = const. Σ Ek = const (без трения).',
body);
if (!card) return false;
const cv = document.getElementById('F11-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const R = 16;
let balls = [];
let aiming = false;
let aimPos = { x: 0, y: 0 };
let p0 = 0; /* нач. импульс */
let trails = [];
function reset(){
balls = [
{ x: W * 0.25, y: H * 0.5, vx: 0, vy: 0, color: '#fff', m: 1, label: 'биток' },
{ x: W * 0.65, y: H * 0.5, vx: 0, vy: 0, color: '#fbbf24', m: 1, label: '1' },
{ x: W * 0.72, y: H * 0.5 - 20, vx: 0, vy: 0, color: '#dc2626', m: 1, label: '2' },
{ x: W * 0.72, y: H * 0.5 + 20, vx: 0, vy: 0, color: '#16a34a', m: 1, label: '3' }
];
trails = [];
p0 = 0;
update();
draw();
}
function update(){
let px = 0, py = 0, E = 0;
balls.forEach(b => {
px += b.m * b.vx;
py += b.m * b.vy;
const v2 = b.vx*b.vx + b.vy*b.vy;
E += b.m * v2 / 2;
});
document.getElementById('F11-p0').textContent = p0.toFixed(1) + ' (кг·px/с)';
document.getElementById('F11-pn').textContent = Math.hypot(px, py).toFixed(1);
document.getElementById('F11-E').textContent = (E*0.001).toFixed(2);
document.getElementById('F11-n').textContent = balls.length;
}
function getPos(e){
const rect = cv.getBoundingClientRect();
const sx = cv.width / rect.width, sy = cv.height / rect.height;
const x = ((e.touches ? e.touches[0].clientX : e.clientX) - rect.left) * sx;
const y = ((e.touches ? e.touches[0].clientY : e.clientY) - rect.top) * sy;
return { x, y };
}
function tick(dt){
/* Шаги по dt — много мелких чтобы стабильно */
const N = 4;
const ddt = dt / N;
for (let s = 0; s < N; s++){
/* движение + трение */
balls.forEach(b => {
b.x += b.vx * ddt;
b.y += b.vy * ddt;
b.vx *= (1 - 0.05*ddt);
b.vy *= (1 - 0.05*ddt);
/* стенки */
if (b.x < R){ b.x = R; b.vx = -b.vx*0.9; }
if (b.x > W-R){ b.x = W-R; b.vx = -b.vx*0.9; }
if (b.y < R){ b.y = R; b.vy = -b.vy*0.9; }
if (b.y > H-R){ b.y = H-R; b.vy = -b.vy*0.9; }
if (Math.hypot(b.vx, b.vy) < 1) { b.vx = b.vy = 0; }
});
/* столкновения */
for (let i = 0; i < balls.length; i++){
for (let j = i+1; j < balls.length; j++){
const a = balls[i], b = balls[j];
const dx = b.x - a.x, dy = b.y - a.y;
const d2 = dx*dx + dy*dy;
const minD = R*2;
if (d2 < minD*minD && d2 > 1){
const d = Math.sqrt(d2);
const nx = dx/d, ny = dy/d;
/* раздвинуть */
const overlap = minD - d;
a.x -= nx*overlap/2; a.y -= ny*overlap/2;
b.x += nx*overlap/2; b.y += ny*overlap/2;
/* упругий удар одинаковых масс */
const va = a.vx*nx + a.vy*ny;
const vb = b.vx*nx + b.vy*ny;
if (vb - va > 0) continue;
/* обмен компонентами вдоль нормали */
a.vx += (vb - va)*nx; a.vy += (vb - va)*ny;
b.vx += (va - vb)*nx; b.vy += (va - vb)*ny;
}
}
}
}
/* trails */
balls.forEach(b => {
if (Math.hypot(b.vx, b.vy) > 5){
trails.push({ x: b.x, y: b.y, c: b.color });
if (trails.length > 800) trails.shift();
}
});
update();
draw();
}
function draw(){
const col = C();
/* зелёное поле */
ctx.fillStyle = '#16a34a';
ctx.fillRect(0, 0, W, H);
/* ободок */
ctx.strokeStyle = '#78350f';
ctx.lineWidth = 8;
ctx.strokeRect(4, 4, W-8, H-8);
/* lunki по углам */
ctx.fillStyle = '#0f172a';
for (let cx of [12, W-12]) for (let cy of [12, H-12]) {
ctx.beginPath(); ctx.arc(cx, cy, 10, 0, Math.PI*2); ctx.fill();
}
/* trails */
trails.forEach(t => {
ctx.fillStyle = t.c + '40';
ctx.fillRect(t.x - 1, t.y - 1, 2, 2);
});
/* шары */
balls.forEach(b => {
ctx.fillStyle = b.color;
ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 1.5;
ctx.stroke();
if (b.label){
ctx.fillStyle = b.color === '#fff' ? '#0f172a' : '#fff';
ctx.font = 'bold 10px Inter,sans-serif';
ctx.textAlign = 'center';
ctx.fillText(b.label, b.x, b.y + 3);
ctx.textAlign = 'left';
}
});
/* aim */
if (aiming){
const biток = balls[0];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.setLineDash([8, 4]);
ctx.beginPath();
ctx.moveTo(biток.x, biток.y);
ctx.lineTo(biток.x*2 - aimPos.x, biток.y*2 - aimPos.y);
ctx.stroke();
ctx.setLineDash([]);
const force = Math.hypot(biток.x - aimPos.x, biток.y - aimPos.y);
ctx.fillStyle = '#fff';
ctx.font = 'bold 13px Inter,sans-serif';
ctx.fillText('сила: '+force.toFixed(0), 12, 22);
}
}
cv.addEventListener('mousedown', e => {
const p = getPos(e);
const bit = balls[0];
if (Math.hypot(p.x - bit.x, p.y - bit.y) < 40 && Math.hypot(bit.vx, bit.vy) < 1){
aiming = true;
aimPos = p;
}
});
cv.addEventListener('mousemove', e => {
if (aiming) { aimPos = getPos(e); draw(); }
});
cv.addEventListener('mouseup', e => {
if (!aiming) return;
aiming = false;
const p = getPos(e);
const bit = balls[0];
bit.vx = (bit.x - p.x) * 2.5;
bit.vy = (bit.y - p.y) * 2.5;
p0 = Math.hypot(bit.m * bit.vx, bit.m * bit.vy);
update();
});
document.getElementById('F11-reset').addEventListener('click', reset);
document.getElementById('F11-clear').addEventListener('click', () => { trails = []; draw(); });
reset();
B().startLoop('F11', cv, tick);
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F11', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F11', { init: init, cleanup: function(){} });
});
})();
@@ -0,0 +1,227 @@
// 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 = '&#10007; КРАШ. Ракета упала на Землю — не хватило топлива/тяги.';
} else if (st.h >= 400000 && Math.abs(st.v) < 100 && st.ended){
fb.className = 'flag-feedback ok show';
fb.innerHTML = '&#10003; УСПЕХ! Ракета на орбите МКС (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(){} });
});
})();
@@ -0,0 +1,211 @@
// F9. Конструктор моста (§28 в ch3) — drag грузов, расчёт реакций опор.
(function(){
'use strict';
const B = () => window.PHYS9_FLAG_BASE;
const C = () => window.PHYS9_COLORS || {};
function init(secId){
if (!B()) return false;
const body = ''
+ '<div style="margin-bottom:8px;font-size:.85rem;color:var(--muted)">Перетащи грузы с верхней палитры на балку. Реакции опор $N_1$, $N_2$ рассчитываются автоматически.</div>'
+ '<div class="flag-controls" id="F9-palette">'
+ '<div class="flag-btn" data-mass="50" style="cursor:grab">50 кг</div>'
+ '<div class="flag-btn" data-mass="100" style="cursor:grab">100 кг</div>'
+ '<div class="flag-btn" data-mass="200" style="cursor:grab">200 кг</div>'
+ '<div class="flag-btn" data-mass="500" style="cursor:grab">500 кг</div>'
+ '<button class="flag-btn danger" id="F9-clear">Убрать все</button>'
+ '<button class="flag-btn" id="F9-test">Тест: автомобиль (1000 кг)</button>'
+ '</div>'
+ '<canvas id="F9-cv" class="flag-canvas" width="700" height="380" style="height:380px"></canvas>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Грузов на балке</span><span class="val" id="F9-n">0</span></div>'
+ '<div class="flag-stat"><span class="lbl">Σ масс</span><span class="val" id="F9-m">0 кг</span></div>'
+ '<div class="flag-stat"><span class="lbl">$N_1$ (левая опора)</span><span class="val" id="F9-n1">0 Н</span></div>'
+ '<div class="flag-stat"><span class="lbl">$N_2$ (правая опора)</span><span class="val" id="F9-n2">0 Н</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F9-fb"></div>';
const card = B().makeCard(secId,
'F9. Конструктор моста',
'Через систему: $\\Sigma F = 0$, $\\Sigma M = 0$. Реакции опор $N_1, N_2$ балансируют все грузы. Лимит балки — 8000 Н на каждую опору.',
body);
if (!card) return false;
const cv = document.getElementById('F9-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const beamY = 180, beamH = 16;
const supL = 70, supR = W - 70;
const beamLen = supR - supL;
let loads = []; /* {x, mass} */
let broken = false;
let dragging = null; /* {mass, x, y} */
function compute(){
if (loads.length === 0) return { N1: 0, N2: 0, sum: 0, brokenSide: null };
const g = 9.8;
const L = supR - supL;
/* Σ M вокруг левой опоры (supL): N2 * L - Σ(m*g*(x - supL)) = 0 */
let sumMass = 0, sumMoment = 0;
loads.forEach(l => {
sumMass += l.mass;
sumMoment += l.mass * g * (l.x - supL);
});
const N2 = sumMoment / L;
const N1 = sumMass * g - N2;
return { N1, N2, sum: sumMass*g, brokenSide: (N1 > 8000) ? 'left' : (N2 > 8000) ? 'right' : null };
}
function update(){
const r = compute();
document.getElementById('F9-n').textContent = loads.length;
document.getElementById('F9-m').textContent = loads.reduce((s,l)=>s+l.mass,0) + ' кг';
document.getElementById('F9-n1').textContent = r.N1.toFixed(0) + ' Н';
document.getElementById('F9-n2').textContent = r.N2.toFixed(0) + ' Н';
broken = !!r.brokenSide;
const fb = document.getElementById('F9-fb');
if (broken){
fb.className = 'flag-feedback fail show';
fb.innerHTML = '&#10007; МОСТ СЛОМАН! ' + (r.brokenSide === 'left' ? 'Левая' : 'Правая') + ' опора перегружена ('+(r.brokenSide==='left'?r.N1:r.N2).toFixed(0)+' Н > 8000 Н).';
} else if (loads.length > 0){
fb.className = 'flag-feedback ok show';
fb.innerHTML = '&#10003; Балка выдерживает. Σ реакций = '+(r.N1+r.N2).toFixed(0)+' Н = $\\Sigma m g$ = '+r.sum.toFixed(0)+' Н.';
try { if(window.renderMathInElement) window.renderMathInElement(fb, { delimiters:[{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){}
} else {
fb.className = 'flag-feedback';
}
draw();
}
function getPos(e){
const rect = cv.getBoundingClientRect();
const sx = cv.width / rect.width, sy = cv.height / rect.height;
const x = ((e.touches ? e.touches[0].clientX : e.clientX) - rect.left) * sx;
const y = ((e.touches ? e.touches[0].clientY : e.clientY) - rect.top) * sy;
return { x, y };
}
function draw(){
const col = C();
ctx.fillStyle = col.bg || '#fafafa';
ctx.fillRect(0, 0, W, H);
/* земля */
ctx.fillStyle = col.surface || '#a16207';
ctx.fillRect(0, H - 30, W, 30);
/* опоры */
ctx.fillStyle = col.body || '#475569';
ctx.beginPath();
ctx.moveTo(supL, beamY + beamH);
ctx.lineTo(supL - 22, H - 30);
ctx.lineTo(supL + 22, H - 30);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(supR, beamY + beamH);
ctx.lineTo(supR - 22, H - 30);
ctx.lineTo(supR + 22, H - 30);
ctx.closePath(); ctx.fill();
/* балка */
const tilt = broken ? 0 : 0;
ctx.fillStyle = broken ? col.fail : col.bodyAccent || '#1e293b';
if (broken){
const r = compute();
const breakX = r.brokenSide === 'left' ? supL + beamLen*0.3 : supL + beamLen*0.7;
ctx.fillStyle = col.bodyAccent || '#1e293b';
ctx.fillRect(supL - 30, beamY, breakX - supL + 30, beamH);
ctx.save();
ctx.translate(breakX + 30, beamY + beamH);
ctx.rotate(0.3);
ctx.fillRect(0, -beamH, supR - breakX + 30, beamH);
ctx.restore();
} else {
ctx.fillRect(supL - 30, beamY, beamLen + 60, beamH);
}
/* подписи опор */
ctx.fillStyle = col.text || '#0f172a';
ctx.font = 'bold 13px Inter,sans-serif';
const r = compute();
ctx.fillText('N₁ = '+r.N1.toFixed(0)+' Н', supL - 50, H - 8);
ctx.fillText('N₂ = '+r.N2.toFixed(0)+' Н', supR - 50, H - 8);
/* стрелки реакций */
if (!broken && loads.length > 0){
B().arrow(ctx, supL, H - 35, supL, beamY + beamH + 2, col.force || '#10b981', 2.5);
B().arrow(ctx, supR, H - 35, supR, beamY + beamH + 2, col.force || '#10b981', 2.5);
}
/* грузы */
loads.forEach(l => {
const size = Math.min(36, 14 + Math.sqrt(l.mass) * 0.8);
ctx.fillStyle = col.forceGravity || '#2563eb';
ctx.fillRect(l.x - size/2, beamY - size, size, size);
ctx.strokeStyle = col.bodyAccent || '#1e293b';
ctx.lineWidth = 1.5;
ctx.strokeRect(l.x - size/2, beamY - size, size, size);
ctx.fillStyle = '#fff';
ctx.font = 'bold 11px Inter,sans-serif';
ctx.textAlign = 'center';
ctx.fillText(l.mass+' кг', l.x, beamY - size/2 + 4);
ctx.textAlign = 'left';
});
/* dragging shadow */
if (dragging){
const size = Math.min(36, 14 + Math.sqrt(dragging.mass) * 0.8);
ctx.globalAlpha = 0.6;
ctx.fillStyle = col.forceGravity || '#2563eb';
ctx.fillRect(dragging.x - size/2, dragging.y - size/2, size, size);
ctx.globalAlpha = 1;
}
}
/* DRAG from palette */
card.querySelectorAll('[data-mass]').forEach(btn => {
btn.addEventListener('mousedown', e => {
const mass = +btn.dataset.mass;
const onMove = ev => {
const p = getPos(ev);
dragging = { mass, x: p.x, y: p.y };
draw();
};
const onUp = ev => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
const p = getPos(ev);
if (p.y > beamY - 50 && p.y < beamY + 50 && p.x > supL - 20 && p.x < supR + 20){
loads.push({ x: p.x, mass });
}
dragging = null;
update();
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
/* Click on canvas to remove load */
cv.addEventListener('click', e => {
const p = getPos(e);
for (let i = loads.length - 1; i >= 0; i--){
if (Math.abs(loads[i].x - p.x) < 20 && p.y > beamY - 40 && p.y < beamY + 5){
loads.splice(i, 1);
update();
return;
}
}
});
document.getElementById('F9-clear').addEventListener('click', () => { loads = []; update(); });
document.getElementById('F9-test').addEventListener('click', () => {
loads.push({ x: supL + beamLen/2, mass: 1000 });
update();
});
update();
draw();
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F9', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F9', { init: init, cleanup: function(){} });
});
})();
+1 -1
View File
@@ -774,7 +774,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH3_WIDGETS && window.PHYS9_CH3_WIDGETS[id]) window.PHYS9_CH3_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p29') window.PHYS9_FLAG_BASE.mount('F10','p29'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH3_WIDGETS && window.PHYS9_CH3_WIDGETS[id]) window.PHYS9_CH3_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p29') window.PHYS9_FLAG_BASE.mount('F10','p29'); else if(id==='p28') window.PHYS9_FLAG_BASE.mount('F9','p28'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
+1 -1
View File
@@ -774,7 +774,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH4_WIDGETS && window.PHYS9_CH4_WIDGETS[id]) window.PHYS9_CH4_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p35') window.PHYS9_FLAG_BASE.mount('F12','p35'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH4_WIDGETS && window.PHYS9_CH4_WIDGETS[id]) window.PHYS9_CH4_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p35') window.PHYS9_FLAG_BASE.mount('F12','p35'); else if(id==='p32') window.PHYS9_FLAG_BASE.mount('F11','p32'); else if(id==='final4') window.PHYS9_FLAG_BASE.mount('F19','final4'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };