// 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 = ''
+ '
Перетащи грузы с верхней палитры на балку. Реакции опор $N_1$, $N_2$ рассчитываются автоматически.
'
+ ''
+ '
50 кг
'
+ '
100 кг
'
+ '
200 кг
'
+ '
500 кг
'
+ '
'
+ '
'
+ '
'
+ ''
+ ''
+ '
Грузов на балке0
'
+ '
Σ масс0 кг
'
+ '
$N_1$ (левая опора)0 Н
'
+ '
$N_2$ (правая опора)0 Н
'
+ '
'
+ '';
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(){} });
});
})();