diff --git a/frontend/js/flagships/phys9_flag_F11_billiard.js b/frontend/js/flagships/phys9_flag_F11_billiard.js new file mode 100644 index 0000000..5fafbd7 --- /dev/null +++ b/frontend/js/flagships/phys9_flag_F11_billiard.js @@ -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 = '' + + '
Тяни мышью от белого шара — увидь прицельный вектор. Отпусти — удар. Длина перетягивания = сила удара.
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
Σ импульса (до)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(){} }); +}); + +})(); diff --git a/frontend/js/flagships/phys9_flag_F19_rocket.js b/frontend/js/flagships/phys9_flag_F19_rocket.js new file mode 100644 index 0000000..16c4dc0 --- /dev/null +++ b/frontend/js/flagships/phys9_flag_F19_rocket.js @@ -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 = '' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
Время0 с
' + + '
Высота0 м
' + + '
Скорость0 м/с
' + + '
Топливо100%
' + + '
Перегрузка0 g
' + + '
' + + '
'; + + const card = B().makeCard(secId, + 'F19. Полёт ракеты МАГИСТР', + 'Запусти ракету с Земли на орбиту 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(){} }); +}); + +})(); diff --git a/frontend/js/flagships/phys9_flag_F9_bridge.js b/frontend/js/flagships/phys9_flag_F9_bridge.js new file mode 100644 index 0000000..acab2ab --- /dev/null +++ b/frontend/js/flagships/phys9_flag_F9_bridge.js @@ -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 = '' + + '
Перетащи грузы с верхней палитры на балку. Реакции опор $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(){} }); +}); + +})(); diff --git a/frontend/textbooks/physics_9_ch3.html b/frontend/textbooks/physics_9_ch3.html index 02453cb..cc680c3 100644 --- a/frontend/textbooks/physics_9_ch3.html +++ b/frontend/textbooks/physics_9_ch3.html @@ -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); }; diff --git a/frontend/textbooks/physics_9_ch4.html b/frontend/textbooks/physics_9_ch4.html index 252ef16..14e12a7 100644 --- a/frontend/textbooks/physics_9_ch4.html +++ b/frontend/textbooks/physics_9_ch4.html @@ -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); };