e316d39264
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>
168 lines
6.5 KiB
JavaScript
168 lines
6.5 KiB
JavaScript
// 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(){} });
|
||
});
|
||
|
||
})();
|