Files
Maxim Dolgolyov e316d39264 feat(phys9 flagships): F6 дорога + F13 Фуко + F14 резонанс
F6. Симулятор скоростной дороги (§20 в ch2):
- 5 покрытий: сухой/мокрый асфальт, гравий, снег, лёд (μ=0.7..0.08)
- Slider скорости 20..180 км/ч
- Автомобиль едет по дороге, кнопка ТОРМОЗ → тормозит до 0
- Расчёт: s = v²/(2μg), t = v/(μg)
- На льду тормозной путь в ~8 раз длиннее асфальта

F13. Маятник Фуко (§36 в ch4):
- Маятник в виде розетки, плоскость вращается со скоростью
  ω = sin(φ) · 2π / 24h
- Slider широты 0..90° (от экватора до полюса)
- На полюсе — 24ч полного оборота, на экваторе — никогда
- Slider «ускорение времени» × (100..20000) — чтобы увидеть розетку
- Места: экватор/Каир/Рим/Минск/Москва/Заполярье/полюс

F14. Резонанс пружинного маятника (§36 в ch4):
- Слева: пружина с грузом + внешняя гармоническая сила
- Slider'ы: m, k, ν_внешн, γ затухание
- Кнопка «Настроить на резонанс» (ν_внешн = ν_собств)
- Реальная физика затухания: m·x'' = -kx - γv + F0·cos(ωt)
- Справа: график x(t) за последние 20 с
- Классификация: вынужденные / близко к резонансу / РЕЗОНАНС!

Подключение:
- ch2: F6 на p20
- ch4: F13 + F14 оба на p36 (маятники)

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

168 lines
6.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.
// F6. Симулятор скоростной дороги (§20 в ch2) — μ и тормозной путь.
(function(){
'use strict';
const B = () => window.PHYS9_FLAG_BASE;
const C = () => window.PHYS9_COLORS || {};
const SURFACES = {
asphalt_dry: { mu: 0.7, name: 'асфальт сухой', col: '#1e293b' },
asphalt_wet: { mu: 0.4, name: 'асфальт мокрый', col: '#475569' },
gravel: { mu: 0.55, name: 'гравий', col: '#92400e' },
snow: { mu: 0.2, name: 'снег', col: '#cbd5e1' },
ice: { mu: 0.08, name: 'лёд', col: '#bfdbfe' }
};
function init(secId){
if (!B()) return false;
let surfBtns = '';
for (const id in SURFACES){
const s = SURFACES[id];
surfBtns += '<button class="flag-btn" data-surf="'+id+'" style="background:'+s.col+';color:'+(s.col.includes('cbd5')||s.col.includes('bfd')?'#0f172a':'#fff')+';font-size:.78rem;padding:6px 10px">'+s.name+' (μ='+s.mu+')</button>';
}
const body = ''
+ '<div class="flag-controls">'+surfBtns+'</div>'
+ '<div class="flag-sliders">'
+ '<label>Скорость, км/ч: <b id="F6-vv">60</b><input type="range" id="F6-v" min="20" max="180" step="5" value="60"></label>'
+ '</div>'
+ '<canvas id="F6-cv" class="flag-canvas" width="700" height="280" style="height:280px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn danger primary" id="F6-brake">ТОРМОЗ!</button>'
+ '<button class="flag-btn" id="F6-reset">Сброс</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Покрытие</span><span class="val" id="F6-surf">сухой асфальт</span></div>'
+ '<div class="flag-stat"><span class="lbl">μ</span><span class="val" id="F6-mu">0.7</span></div>'
+ '<div class="flag-stat"><span class="lbl">Тормозной путь</span><span class="val" id="F6-stop">20.4 м</span></div>'
+ '<div class="flag-stat"><span class="lbl">Время торможения</span><span class="val" id="F6-tstop">2.43 с</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F6-fb"></div>';
const card = B().makeCard(secId,
'F6. Симулятор скоростной дороги',
'Выбери покрытие и скорость. При торможении: $s = v^2/(2\\mu g)$. На льду путь в 8 раз длиннее!',
body);
if (!card) return false;
const cv = document.getElementById('F6-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
let surf = 'asphalt_dry';
let st = { x: 50, v: 0, vTarget: 60/3.6, mode: 'cruise' };
function update(){
const v_kmh = +document.getElementById('F6-v').value;
document.getElementById('F6-vv').textContent = v_kmh;
st.vTarget = v_kmh / 3.6;
const mu = SURFACES[surf].mu;
const v = st.vTarget;
const sStop = v*v / (2 * mu * 9.8);
const tStop = v / (mu * 9.8);
document.getElementById('F6-surf').textContent = SURFACES[surf].name;
document.getElementById('F6-mu').textContent = mu;
document.getElementById('F6-stop').textContent = sStop.toFixed(1) + ' м';
document.getElementById('F6-tstop').textContent = tStop.toFixed(2) + ' с';
}
function reset(){
st = { x: 50, v: st.vTarget, mode: 'cruise' };
document.getElementById('F6-fb').className = 'flag-feedback';
draw();
}
function tick(dt){
const mu = SURFACES[surf].mu;
if (st.mode === 'cruise'){
st.v = st.vTarget;
st.x += st.v * dt * 5; /* 5 px = 1 м */
if (st.x > W + 50) st.x = -20;
} else if (st.mode === 'brake'){
st.v -= mu * 9.8 * dt;
if (st.v < 0){ st.v = 0; st.mode = 'stopped'; }
st.x += st.v * dt * 5;
}
draw();
}
function draw(){
const col = C();
ctx.fillStyle = col.bg || '#fafafa';
ctx.fillRect(0, 0, W, H);
/* небо */
ctx.fillStyle = col.gas || '#dbeafe';
ctx.fillRect(0, 0, W, 80);
/* дорога */
const s = SURFACES[surf];
ctx.fillStyle = s.col;
ctx.fillRect(0, 80, W, 80);
/* разметка */
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.setLineDash([24, 16]);
ctx.beginPath();
ctx.moveTo(0, 120); ctx.lineTo(W, 120);
ctx.stroke();
ctx.setLineDash([]);
/* шкала метров */
ctx.fillStyle = col.text || '#0f172a';
ctx.font = '11px Inter,sans-serif';
for (let m = 0; m <= 140; m += 10){
const px = 50 + m*5;
if (px > W) break;
ctx.fillRect(px-1, 160, 2, 6);
if (m % 20 === 0) ctx.fillText(m + ' м', px - 8, 178);
}
/* трава */
ctx.fillStyle = '#16a34a';
ctx.fillRect(0, 180, W, H - 180);
/* машина */
ctx.fillStyle = col.fail || '#dc2626';
ctx.fillRect(st.x - 25, 95, 50, 22);
ctx.fillRect(st.x - 18, 88, 36, 9);
ctx.fillStyle = '#0f172a';
ctx.fillRect(st.x - 16, 117, 8, 8);
ctx.fillRect(st.x + 8, 117, 8, 8);
/* подпись */
ctx.fillStyle = col.text || '#0f172a';
ctx.font = 'bold 13px Inter,sans-serif';
ctx.fillText((st.v*3.6).toFixed(0) + ' км/ч', st.x - 22, 78);
/* shadow */
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(st.x - 25, 130, 50, 4);
}
card.querySelectorAll('[data-surf]').forEach(btn => {
btn.addEventListener('click', () => {
surf = btn.dataset.surf;
card.querySelectorAll('[data-surf]').forEach(b => b.classList.remove('primary'));
btn.classList.add('primary');
update();
reset();
});
});
document.getElementById('F6-v').addEventListener('input', () => { update(); if(st.mode==='cruise') st.v = st.vTarget; });
document.getElementById('F6-brake').addEventListener('click', () => {
if (st.mode === 'cruise'){
st.mode = 'brake';
const fb = document.getElementById('F6-fb');
const mu = SURFACES[surf].mu;
const v = st.v;
const sStop = v*v / (2 * mu * 9.8);
fb.className = 'flag-feedback warn show';
fb.innerHTML = 'Торможение на ' + SURFACES[surf].name + ' с ' + (v*3.6).toFixed(0) + ' км/ч — потребуется ' + sStop.toFixed(1) + ' м.';
}
});
document.getElementById('F6-reset').addEventListener('click', reset);
update();
reset();
B().startLoop('F6', cv, tick);
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F6', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F6', { init: init, cleanup: function(){} });
});
})();