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); };