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