Files
Learn_System/frontend/js/flagships/phys9_flag_F11_billiard.js
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

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