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