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