Files
Maxim Dolgolyov 4d53919e9a 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>
2026-05-30 10:19:55 +03:00

212 lines
8.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.
// 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(){} });
});
})();