// 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 = '' + '
Тяни мышью от белого шара — увидь прицельный вектор. Отпусти — удар. Длина перетягивания = сила удара.
' + '' + '
' + '' + '' + '
' + '
' + '
Σ импульса (до)0
' + '
Σ импульса (сейчас)0
' + '
Σ кин. энергии0 Дж
' + '
Кол-во шаров4
' + '
' + '
'; 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(){} }); }); })();