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:
@@ -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 = '✗ КРАШ. Ракета упала на Землю — не хватило топлива/тяги.';
|
||||
} else if (st.h >= 400000 && Math.abs(st.v) < 100 && st.ended){
|
||||
fb.className = 'flag-feedback ok show';
|
||||
fb.innerHTML = '✓ УСПЕХ! Ракета на орбите МКС (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 = '✗ МОСТ СЛОМАН! ' + (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 = '✓ Балка выдерживает. Σ реакций = '+(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(){} });
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -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); };
|
||||
|
||||
@@ -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); };
|
||||
|
||||
Reference in New Issue
Block a user