Files
Learn_System/frontend/js/flagships/phys9_flag_F12_coaster.js
T
Maxim Dolgolyov 1f82a980de feat(phys9 flagships): F10 аквариум + F12 горки (Wave C+D пилоты)
F10. Виртуальный аквариум (§29 в ch3):
- Canvas 640×380 с переключателем жидкости (вода/масло/ртуть)
- Палитра 7 материалов: дерево, пенопласт, пластик, лёд, алюминий,
  железо, золото (с указанием ρ)
- Клик по материалу → бросает кубик в аквариум
- Реальная физика плавания: F_Архимеда vs F_тяжести
- Тела плавают/тонут/висят согласно ρ_тела vs ρ_жидкости
- При смене жидкости тела перераспределяются
- Феномен: «золото плавает в ртути!»
- Контекстный feedback по последнему уложенному телу

F12. Американские горки (§35 в ch4):
- Canvas 700×360 — рисуй мышкой профиль горки слева направо
- Шаблоны: «горка» (V-образная), «петля» (волнистая)
- Slider'ы: μ трения (0..0.5), масса шарика (0.1..5 кг)
- Кнопки: Старт/Сброс/Очистить
- Реальная физика по сегментам:
  a = g·sinα - μg·cosα·sign(v)
- Real-time stats: Ep, Ek, E_total, v
- Зелёная пунктир E₀ на canvas — начальная энергия
- Красная пунктир — диссипация при трении (растёт со временем)
- ЗСМЭ виден визуально: без трения линии совпадают

Подключение:
- ch3: phys9-flagships.css + base + F10 + хук на p29
- ch4: phys9-flagships.css + base + F12 + хук на p35

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:15:41 +03:00

255 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = ''
+ '<div style="margin-bottom:8px;font-size:.85rem;color:var(--muted)">Нажми и проведи мышкой по канвасу — нарисуй профиль горки (слева направо).</div>'
+ '<canvas id="F12-cv" class="flag-canvas" width="700" height="360" style="height:360px;cursor:crosshair"></canvas>'
+ '<div class="flag-sliders">'
+ '<label>$\\mu$ трение: <b id="F12-muv">0</b><input type="range" id="F12-mu" min="0" max="0.5" step="0.02" value="0"></label>'
+ '<label>Масса шарика, кг: <b id="F12-mv">1</b><input type="range" id="F12-m" min="0.1" max="5" step="0.1" value="1"></label>'
+ '</div>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F12-go">Старт</button>'
+ '<button class="flag-btn" id="F12-reset">Сброс</button>'
+ '<button class="flag-btn" id="F12-preset1">Шаблон: горка</button>'
+ '<button class="flag-btn" id="F12-preset2">Шаблон: петля</button>'
+ '<button class="flag-btn danger" id="F12-clear">Очистить</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">$E_p$ (mgh)</span><span class="val" id="F12-Ep">0 Дж</span></div>'
+ '<div class="flag-stat"><span class="lbl">$E_k$ (mv²/2)</span><span class="val" id="F12-Ek">0 Дж</span></div>'
+ '<div class="flag-stat"><span class="lbl">$E$ полная</span><span class="val" id="F12-Et">0 Дж</span></div>'
+ '<div class="flag-stat"><span class="lbl">$v$</span><span class="val" id="F12-v">0 м/с</span></div>'
+ '</div>';
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(){} });
});
})();