Files
Learn_System/frontend/js/flagships/phys9_flag_F10_aquarium.js
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

185 lines
7.5 KiB
JavaScript
Raw Permalink 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.
// 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 += '<button class="flag-btn" data-mat="'+m.id+'" style="background:'+m.col+';color:'+(m.rho>5000?'#fff':'#0f172a')+';font-size:.78rem;padding:6px 10px">'+m.name+' (ρ='+m.rho+')</button>';
});
const body = ''
+ '<div style="margin-bottom:10px;font-size:.85rem;color:var(--muted)">Жидкость:</div>'
+ '<div class="flag-controls" style="margin-bottom:6px">'
+ '<button class="flag-btn primary" data-liq="water">Вода (1000)</button>'
+ '<button class="flag-btn" data-liq="oil">Масло (800)</button>'
+ '<button class="flag-btn" data-liq="mercury">Ртуть (13600)</button>'
+ '</div>'
+ '<div style="margin-bottom:10px;font-size:.85rem;color:var(--muted)">Кликни по телу, чтобы бросить его в аквариум:</div>'
+ '<div class="flag-controls">'+buttons+'</div>'
+ '<canvas id="F10-cv" class="flag-canvas" width="640" height="380" style="height:380px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn danger" id="F10-clear">Очистить</button>'
+ '</div>'
+ '<div class="flag-feedback" id="F10-fb"></div>';
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 = '&#10003; '+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(){} });
});
})();