1f82a980de
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>
185 lines
7.5 KiB
JavaScript
185 lines
7.5 KiB
JavaScript
// 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 = '✓ '+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(){} });
|
||
});
|
||
|
||
})();
|