// 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(){} }); }); })();