diff --git a/frontend/js/flagships/phys9_flag_F10_aquarium.js b/frontend/js/flagships/phys9_flag_F10_aquarium.js new file mode 100644 index 0000000..ab6a809 --- /dev/null +++ b/frontend/js/flagships/phys9_flag_F10_aquarium.js @@ -0,0 +1,184 @@ +// F10. Виртуальный аквариум (§29 в ch3) — Архимед, плавание тел. +(function(){ +'use strict'; +const B = () => window.PHYS9_FLAG_BASE; +const C = () => window.PHYS9_COLORS || {}; + +const MATERIALS = [ + { id:'wood', name:'дерево', rho:600, col:'#a16207' }, + { id:'foam', name:'пенопласт', rho:50, col:'#fef3c7' }, + { id:'plastic', name:'пластик', rho:950, col:'#06b6d4' }, + { id:'ice', name:'лёд', rho:917, col:'#bfdbfe' }, + { id:'al', name:'алюминий', rho:2700, col:'#94a3b8' }, + { id:'iron', name:'железо', rho:7800, col:'#475569' }, + { id:'gold', name:'золото', rho:19300, col:'#fbbf24' } +]; + +const LIQUIDS = { + water: { rho:1000, name:'вода', col:'rgba(96,165,250,0.5)' }, + oil: { rho:800, name:'масло', col:'rgba(217,119,6,0.4)' }, + mercury: { rho:13600, name:'ртуть', col:'rgba(229,231,235,0.7)' } +}; + +function init(secId){ + if (!B()) return false; + let buttons = ''; + MATERIALS.forEach(m => { + buttons += ''; + }); + const body = '' + + '
Жидкость:
' + + '
' + + '' + + '' + + '' + + '
' + + '
Кликни по телу, чтобы бросить его в аквариум:
' + + '
'+buttons+'
' + + '' + + '
' + + '' + + '
' + + '
'; + + const card = B().makeCard(secId, + 'F10. Виртуальный аквариум', + 'Выбери жидкость и бросай тела. Тело $\\rho_T < \\rho_Ж$ — плавает. Равны — висит. Тяжелее — тонет. Попробуй золото в ртути!', + body); + if (!card) return false; + + const cv = document.getElementById('F10-cv'); + const ctx = cv.getContext('2d'); + const W = cv.width, H = cv.height; + const liqTop = 60; + + let bodies = []; /* { x, y, vy, mat, size, settled } */ + let liquid = 'water'; + + function addBody(matId){ + const mat = MATERIALS.find(m => m.id === matId); + if (!mat) return; + const size = 22 + Math.random()*10; + bodies.push({ x: 60 + Math.random()*(W - 120), y: 30, vy: 0, mat: mat, size: size, settled: false }); + } + + function clear(){ bodies = []; } + + function tick(dt){ + const liq = LIQUIDS[liquid]; + bodies.forEach(b => { + if (b.settled) return; + const g = 9.8; + const inLiquid = b.y > liqTop; + const submerged = Math.min(1, Math.max(0, (b.y - liqTop + b.size/2) / b.size)); + /* Сила тяжести вниз: m*g, m = ρ_T * V (V в условных единицах ∝ size^2) */ + const V = (b.size * b.size) / 1000; + const Fg = b.mat.rho * V * g; + const Fa = liq.rho * V * g * submerged; + const Fnet = Fg - Fa; + /* a = Fnet / m, m = ρ_T * V */ + const a = Fnet / (b.mat.rho * V); + b.vy += a * dt * 30; /* px/m scale */ + /* демпфирование в жидкости */ + if (inLiquid) b.vy *= 0.96; + b.y += b.vy * dt; + /* дно */ + if (b.y > H - b.size/2){ b.y = H - b.size/2; b.vy = 0; } + /* потолок жидкости — для плавающих */ + if (b.y < liqTop && b.vy < 0){ + b.y = liqTop; + b.vy = 0; + } + /* «осёл» — если скорость мала и в равновесии, settling */ + if (Math.abs(b.vy) < 0.05 && Math.abs(a) < 0.1){ + b.settled = true; + } + }); + draw(); + } + + function draw(){ + const col = C(); + /* небо */ + ctx.fillStyle = col.gas || '#dbeafe'; + ctx.fillRect(0, 0, W, liqTop); + /* жидкость */ + const liq = LIQUIDS[liquid]; + ctx.fillStyle = liq.col; + ctx.fillRect(0, liqTop, W, H - liqTop); + /* поверхность воды */ + ctx.strokeStyle = col.liquid || '#3b82f6'; + ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(0, liqTop); ctx.lineTo(W, liqTop); ctx.stroke(); + /* стенки аквариума */ + ctx.strokeStyle = col.axis || '#1e293b'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(0, liqTop - 5); ctx.lineTo(0, H); + ctx.moveTo(W, liqTop - 5); ctx.lineTo(W, H); + ctx.moveTo(0, H); ctx.lineTo(W, H); + ctx.stroke(); + /* подпись жидкости */ + ctx.fillStyle = col.text || '#0f172a'; + ctx.font = 'bold 13px Inter,sans-serif'; + ctx.fillText(liq.name + ' (ρ = ' + liq.rho + ' кг/м³)', 12, 24); + /* тела */ + bodies.forEach(b => { + ctx.fillStyle = b.mat.col; + ctx.fillRect(b.x - b.size/2, b.y - b.size/2, b.size, b.size); + ctx.strokeStyle = col.bodyAccent || '#1e293b'; + ctx.lineWidth = 1.5; + ctx.strokeRect(b.x - b.size/2, b.y - b.size/2, b.size, b.size); + ctx.fillStyle = b.mat.rho > 5000 ? '#fff' : '#0f172a'; + ctx.font = '10px Inter,sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(b.mat.rho, b.x, b.y + 3); + ctx.textAlign = 'left'; + }); + /* feedback */ + if (bodies.length > 0){ + const last = bodies[bodies.length-1]; + if (last.settled){ + const fb = document.getElementById('F10-fb'); + const liqRho = LIQUIDS[liquid].rho; + if (last.mat.rho < liqRho){ + fb.className = 'flag-feedback ok show'; + fb.innerHTML = '✓ '+last.mat.name+' ('+last.mat.rho+') ПЛАВАЕТ в '+liq.name+' ('+liqRho+'): $\\rho_T < \\rho_Ж$.'; + } else if (last.mat.rho > liqRho){ + fb.className = 'flag-feedback warn show'; + fb.innerHTML = last.mat.name+' ('+last.mat.rho+') ТОНЕТ в '+liq.name+' ('+liqRho+'): $\\rho_T > \\rho_Ж$.'; + } else { + fb.className = 'flag-feedback ok show'; + fb.innerHTML = last.mat.name+' ВИСИТ в толще: $\\rho_T = \\rho_Ж$.'; + } + try { if(window.renderMathInElement) window.renderMathInElement(fb, { delimiters:[{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} + } + } + } + + /* binding */ + card.querySelectorAll('[data-mat]').forEach(btn => { + btn.addEventListener('click', () => addBody(btn.dataset.mat)); + }); + card.querySelectorAll('[data-liq]').forEach(btn => { + btn.addEventListener('click', () => { + liquid = btn.dataset.liq; + card.querySelectorAll('[data-liq]').forEach(b => b.classList.remove('primary')); + btn.classList.add('primary'); + /* «разбудить» все тела */ + bodies.forEach(b => b.settled = false); + }); + }); + document.getElementById('F10-clear').addEventListener('click', clear); + + draw(); + B().startLoop('F10', cv, tick); + return true; +} + +if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F10', { init: init, cleanup: function(){} }); +else document.addEventListener('DOMContentLoaded', ()=>{ + if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F10', { init: init, cleanup: function(){} }); +}); + +})(); diff --git a/frontend/js/flagships/phys9_flag_F12_coaster.js b/frontend/js/flagships/phys9_flag_F12_coaster.js new file mode 100644 index 0000000..fc81d8e --- /dev/null +++ b/frontend/js/flagships/phys9_flag_F12_coaster.js @@ -0,0 +1,254 @@ +// F12. Американские горки (§35 в ch4) — нарисуй профиль, шарик катится, ЗСМЭ. +(function(){ +'use strict'; +const B = () => window.PHYS9_FLAG_BASE; +const C = () => window.PHYS9_COLORS || {}; + +function init(secId){ + if (!B()) return false; + const body = '' + + '
Нажми и проведи мышкой по канвасу — нарисуй профиль горки (слева направо).
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
$E_p$ (mgh)0 Дж
' + + '
$E_k$ (mv²/2)0 Дж
' + + '
$E$ полная0 Дж
' + + '
$v$0 м/с
' + + '
'; + + const card = B().makeCard(secId, + 'F12. Американские горки', + 'Нарисуй профиль горки и запусти шарик. Без трения $E_k + E_p = $ const. Со трением — энергия диссипирует.', + body); + if (!card) return false; + + const cv = document.getElementById('F12-cv'); + const ctx = cv.getContext('2d'); + const W = cv.width, H = cv.height; + const m_per_px = 0.05; /* 5 см / px */ + + let profile = []; /* отсортированный по x массив {x, y} */ + let drawing = false; + let ball = { idx: 0, fraction: 0, vAlong: 0, energy0: 0, lossE: 0, running: false }; + + function getPos(e){ + const rect = cv.getBoundingClientRect(); + const sx = cv.width / rect.width; + const 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 start(e){ + drawing = true; + profile = [getPos(e)]; + e.preventDefault(); + draw(); + } + function move(e){ + if (!drawing) return; + const p = getPos(e); + const last = profile[profile.length-1]; + if (p.x > last.x + 3) profile.push(p); + e.preventDefault(); + draw(); + } + function end(){ drawing = false; } + + function presetHill(){ + profile = []; + for (let x = 30; x <= W - 30; x += 8){ + const norm = x / (W - 60); + const y = 50 + 250 * Math.abs(norm - 0.5) * (1 - norm*0.8); + profile.push({ x, y }); + } + reset(); + } + function presetLoop(){ + profile = []; + /* три горки */ + for (let x = 30; x <= W - 30; x += 6){ + const t = (x - 30) / (W - 60); + const y = 70 + 240 * (1 - t) * 0.4 + 100 * Math.sin(t * Math.PI * 2.2); + profile.push({ x, y: Math.max(40, Math.min(H - 20, y)) }); + } + reset(); + } + function clear(){ profile = []; reset(); } + + function reset(){ + if (profile.length > 1){ + ball = { idx: 0, fraction: 0, vAlong: 0, running: false }; + const m = +document.getElementById('F12-m').value; + const h = (H - profile[0].y) * m_per_px; + ball.energy0 = m * 9.8 * h; + ball.lossE = 0; + } + document.getElementById('F12-go').textContent = 'Старт'; + draw(); + } + + function tick(dt){ + if (!ball.running || profile.length < 2) { draw(); return; } + /* Движение по профилю: используем сегменты. */ + const m = +document.getElementById('F12-m').value; + const mu = +document.getElementById('F12-mu').value; + const g = 9.8; + /* Текущая высота */ + const i = ball.idx; + if (i >= profile.length - 1){ ball.running = false; document.getElementById('F12-go').textContent='Старт'; draw(); return; } + const p1 = profile[i], p2 = profile[i+1]; + const segLen_px = Math.hypot(p2.x - p1.x, p2.y - p1.y); + const segLen = segLen_px * m_per_px; + const slope = (p2.y - p1.y) / (p2.x - p1.x); + const sinA = slope / Math.sqrt(1 + slope*slope); /* y SVG вниз — slope>0 = вниз */ + const cosA = 1 / Math.sqrt(1 + slope*slope); + /* Ускорение вдоль профиля: a = g*sinA - μ*g*cosA*sign(v) */ + let aAlong = g * sinA; + if (Math.abs(ball.vAlong) > 0.01) aAlong -= Math.sign(ball.vAlong) * mu * g * cosA; + /* шаг */ + ball.vAlong += aAlong * dt; + /* трение тратит энергию */ + if (mu > 0) ball.lossE += mu * m * g * cosA * Math.abs(ball.vAlong * dt); + const ds = ball.vAlong * dt; /* в метрах */ + ball.fraction += ds / Math.max(0.01, segLen); + /* переход к следующему сегменту */ + while (ball.fraction >= 1 && ball.idx < profile.length - 1){ + ball.fraction -= 1; + ball.idx++; + } + while (ball.fraction < 0 && ball.idx > 0){ + ball.fraction += 1; + ball.idx--; + } + if (ball.idx >= profile.length - 1 || ball.idx < 0){ + ball.running = false; + document.getElementById('F12-go').textContent='Старт'; + } + /* статистика */ + const px = p1.x + ball.fraction * (p2.x - p1.x); + const py = p1.y + ball.fraction * (p2.y - p1.y); + const h = (H - py) * m_per_px; + const v = Math.abs(ball.vAlong); + const Ep = m * g * h; + const Ek = m * v * v / 2; + document.getElementById('F12-Ep').textContent = Ep.toFixed(1) + ' Дж'; + document.getElementById('F12-Ek').textContent = Ek.toFixed(1) + ' Дж'; + document.getElementById('F12-Et').textContent = (Ep + Ek).toFixed(1) + ' Дж'; + document.getElementById('F12-v').textContent = v.toFixed(2) + ' м/с'; + draw(); + } + + 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 - 10, W, 10); + if (profile.length < 2){ + ctx.fillStyle = col.textMuted || '#64748b'; + ctx.font = '15px Inter,sans-serif'; + ctx.fillText('Нарисуй профиль горки слева направо мышкой/пальцем', 90, H/2); + return; + } + /* профиль */ + ctx.strokeStyle = col.bodyAccent || '#1e293b'; + ctx.lineWidth = 4; + ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(profile[0].x, profile[0].y); + for (let i = 1; i < profile.length; i++) ctx.lineTo(profile[i].x, profile[i].y); + ctx.stroke(); + /* заливка под профилем */ + ctx.fillStyle = 'rgba(161,98,7,0.2)'; + ctx.beginPath(); + ctx.moveTo(profile[0].x, H); + for (let i = 0; i < profile.length; i++) ctx.lineTo(profile[i].x, profile[i].y); + ctx.lineTo(profile[profile.length-1].x, H); + ctx.closePath(); + ctx.fill(); + /* шарик */ + if (ball.idx < profile.length - 1){ + const p1 = profile[ball.idx], p2 = profile[ball.idx+1]; + const px = p1.x + ball.fraction * (p2.x - p1.x); + const py = p1.y + ball.fraction * (p2.y - p1.y) - 9; + ctx.fillStyle = col.fail || '#dc2626'; + ctx.beginPath(); ctx.arc(px, py, 9, 0, Math.PI*2); ctx.fill(); + ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); + } + /* Линия энергии — горизонтальный уровень energy0 показывает потери при трении */ + if (ball.energy0 > 0){ + const m = +document.getElementById('F12-m').value; + const g = 9.8; + const E_height = ball.energy0 / (m*g); /* высота, эквивалентная энергии */ + const E_py = H - E_height / m_per_px; + ctx.strokeStyle = '#10b981'; + ctx.setLineDash([8, 5]); + ctx.lineWidth = 1.5; + ctx.beginPath(); ctx.moveTo(0, E_py); ctx.lineTo(W, E_py); ctx.stroke(); + ctx.setLineDash([]); + ctx.fillStyle = '#10b981'; + ctx.font = 'bold 11px Inter,sans-serif'; + ctx.fillText('E₀ = ' + ball.energy0.toFixed(1) + ' Дж', 8, E_py - 4); + /* линия потерь */ + if (ball.lossE > 0){ + const lossH = ball.lossE / (m*g); + const py = E_py + lossH / m_per_px; + ctx.strokeStyle = '#dc2626'; + ctx.setLineDash([4, 3]); + ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke(); + ctx.setLineDash([]); + ctx.fillStyle = '#dc2626'; + ctx.fillText('потери: ' + ball.lossE.toFixed(1) + ' Дж', 8, py - 4); + } + } + } + + cv.addEventListener('mousedown', start); + cv.addEventListener('mousemove', move); + cv.addEventListener('mouseup', end); + cv.addEventListener('mouseleave', end); + cv.addEventListener('touchstart', start, {passive:false}); + cv.addEventListener('touchmove', move, {passive:false}); + cv.addEventListener('touchend', end); + + document.getElementById('F12-go').addEventListener('click', ()=>{ + if (profile.length < 2) return; + if (ball.idx >= profile.length - 1) reset(); + ball.running = !ball.running; + document.getElementById('F12-go').textContent = ball.running ? 'Пауза' : 'Старт'; + }); + document.getElementById('F12-reset').addEventListener('click', reset); + document.getElementById('F12-preset1').addEventListener('click', presetHill); + document.getElementById('F12-preset2').addEventListener('click', presetLoop); + document.getElementById('F12-clear').addEventListener('click', clear); + ['F12-mu','F12-m'].forEach(id => document.getElementById(id).addEventListener('input', () => { + document.getElementById('F12-muv').textContent = (+document.getElementById('F12-mu').value).toFixed(2); + document.getElementById('F12-mv').textContent = (+document.getElementById('F12-m').value).toFixed(1); + if (!ball.running) reset(); + })); + + presetHill(); + B().startLoop('F12', cv, tick); + return true; +} + +if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F12', { init: init, cleanup: function(){} }); +else document.addEventListener('DOMContentLoaded', ()=>{ + if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F12', { init: init, cleanup: function(){} }); +}); + +})(); diff --git a/frontend/textbooks/physics_9_ch3.html b/frontend/textbooks/physics_9_ch3.html index cbc4287..02453cb 100644 --- a/frontend/textbooks/physics_9_ch3.html +++ b/frontend/textbooks/physics_9_ch3.html @@ -9,6 +9,7 @@ + @@ -773,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); } }, 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'); } } 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 d601207..252ef16 100644 --- a/frontend/textbooks/physics_9_ch4.html +++ b/frontend/textbooks/physics_9_ch4.html @@ -9,6 +9,7 @@ + @@ -773,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); } }, 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'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60); } var _origEnsureBuilt = ensureBuilt; ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };