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>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// F13. Маятник Фуко (§36 в ch4) — вращение Земли и розетка.
|
||||
(function(){
|
||||
'use strict';
|
||||
const B = () => window.PHYS9_FLAG_BASE;
|
||||
const C = () => window.PHYS9_COLORS || {};
|
||||
|
||||
function init(secId){
|
||||
if (!B()) return false;
|
||||
const body = ''
|
||||
+ '<div class="flag-sliders">'
|
||||
+ '<label>Широта φ, °: <b id="F13-lv">52</b><input type="range" id="F13-l" min="0" max="90" step="1" value="52"></label>'
|
||||
+ '<label>Ускорение времени, ×: <b id="F13-sv">3600</b><input type="range" id="F13-s" min="100" max="20000" step="100" value="3600"></label>'
|
||||
+ '</div>'
|
||||
+ '<canvas id="F13-cv" class="flag-canvas" width="640" height="380" style="height:380px"></canvas>'
|
||||
+ '<div class="flag-controls">'
|
||||
+ '<button class="flag-btn primary" id="F13-go">Запустить</button>'
|
||||
+ '<button class="flag-btn" id="F13-reset">Сброс</button>'
|
||||
+ '<button class="flag-btn" id="F13-clear">Очистить следы</button>'
|
||||
+ '</div>'
|
||||
+ '<div class="flag-stats">'
|
||||
+ '<div class="flag-stat"><span class="lbl">Текущая широта</span><span class="val" id="F13-loc">Минск (52°)</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">Период оборота плоскости</span><span class="val" id="F13-T">30.4 ч</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">Реальное время</span><span class="val" id="F13-t">0 ч</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">Поворот плоскости</span><span class="val" id="F13-rot">0°</span></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="flag-feedback" id="F13-fb"></div>';
|
||||
|
||||
const card = B().makeCard(secId,
|
||||
'F13. Маятник Фуко',
|
||||
'Маятник в Париже (1851) доказал вращение Земли. Период оборота плоскости: $T = 24/\\sin\\varphi$ часов. На полюсе — 24 ч, на экваторе — никогда.',
|
||||
body);
|
||||
if (!card) return false;
|
||||
|
||||
const cv = document.getElementById('F13-cv');
|
||||
const ctx = cv.getContext('2d');
|
||||
const W = cv.width, H = cv.height;
|
||||
const cx = W/2, cy = H/2;
|
||||
const R = 130;
|
||||
|
||||
let st = { phase: 0, planeAngle: 0, t: 0, running: false, trail: [] };
|
||||
|
||||
function update(){
|
||||
const lat = +document.getElementById('F13-l').value;
|
||||
const speed = +document.getElementById('F13-s').value;
|
||||
document.getElementById('F13-lv').textContent = lat;
|
||||
document.getElementById('F13-sv').textContent = speed;
|
||||
let loc;
|
||||
if (lat >= 89) loc = 'Северный полюс (90°)';
|
||||
else if (lat >= 70) loc = 'Заполярье (70-89°)';
|
||||
else if (lat >= 55) loc = 'Москва (55°)';
|
||||
else if (lat >= 50) loc = 'Минск (52°)';
|
||||
else if (lat >= 40) loc = 'Рим (42°)';
|
||||
else if (lat >= 20) loc = 'Каир (30°)';
|
||||
else if (lat >= 5) loc = 'тропики';
|
||||
else loc = 'экватор (0°)';
|
||||
document.getElementById('F13-loc').textContent = loc;
|
||||
const sinL = Math.sin(lat * Math.PI/180);
|
||||
const T = sinL > 0.001 ? 24 / sinL : Infinity;
|
||||
document.getElementById('F13-T').textContent = isFinite(T) ? T.toFixed(1) + ' ч' : '∞ (нет поворота)';
|
||||
}
|
||||
|
||||
function reset(){
|
||||
st = { phase: 0, planeAngle: 0, t: 0, running: false, trail: [] };
|
||||
document.getElementById('F13-go').textContent = 'Запустить';
|
||||
draw();
|
||||
}
|
||||
|
||||
function tick(dt){
|
||||
if (!st.running) { draw(); return; }
|
||||
const lat = +document.getElementById('F13-l').value;
|
||||
const speed = +document.getElementById('F13-s').value;
|
||||
const sinL = Math.sin(lat * Math.PI/180);
|
||||
const realDt = dt * speed; /* условное реальное время в секундах */
|
||||
st.t += realDt;
|
||||
/* колебание маятника: период ~6 с реального времени */
|
||||
st.phase += dt * 2 * Math.PI * 0.5; /* псевдо-период для красоты */
|
||||
/* поворот плоскости: ω = 2π/(24·3600/sinL) рад/с */
|
||||
const omega = sinL * 2 * Math.PI / (24*3600);
|
||||
st.planeAngle += omega * realDt;
|
||||
/* trail — текущее положение маятника */
|
||||
const r = R * Math.cos(st.phase);
|
||||
const px = cx + r * Math.cos(st.planeAngle);
|
||||
const py = cy + r * Math.sin(st.planeAngle);
|
||||
st.trail.push({ x: px, y: py });
|
||||
if (st.trail.length > 5000) st.trail.shift();
|
||||
document.getElementById('F13-t').textContent = (st.t/3600).toFixed(2) + ' ч';
|
||||
document.getElementById('F13-rot').textContent = ((st.planeAngle * 180/Math.PI) % 360).toFixed(1) + '°';
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw(){
|
||||
const col = C();
|
||||
ctx.fillStyle = col.bg || '#fafafa';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
/* пол */
|
||||
ctx.fillStyle = col.bgSubtle || '#f8fafc';
|
||||
ctx.beginPath(); ctx.arc(cx, cy, R + 30, 0, Math.PI*2); ctx.fill();
|
||||
ctx.strokeStyle = col.axis || '#1e293b';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
/* радиальные направления (страны света) */
|
||||
ctx.font = '12px Inter,sans-serif';
|
||||
ctx.fillStyle = col.textMuted || '#64748b';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('С', cx, cy - R - 15);
|
||||
ctx.fillText('Ю', cx, cy + R + 25);
|
||||
ctx.fillText('З', cx - R - 18, cy + 4);
|
||||
ctx.fillText('В', cx + R + 18, cy + 4);
|
||||
/* следы маятника */
|
||||
if (st.trail.length > 1){
|
||||
ctx.strokeStyle = col.velocity || '#0891b2';
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(st.trail[0].x, st.trail[0].y);
|
||||
for (let i = 1; i < st.trail.length; i++) ctx.lineTo(st.trail[i].x, st.trail[i].y);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
/* центральная точка крепления */
|
||||
ctx.fillStyle = col.axis || '#1e293b';
|
||||
ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI*2); ctx.fill();
|
||||
/* плоскость колебаний — линия */
|
||||
ctx.strokeStyle = col.acceleration || '#ea580c';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([6, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - R * Math.cos(st.planeAngle), cy - R * Math.sin(st.planeAngle));
|
||||
ctx.lineTo(cx + R * Math.cos(st.planeAngle), cy + R * Math.sin(st.planeAngle));
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
/* маятник — груз */
|
||||
const r = R * Math.cos(st.phase);
|
||||
const px = cx + r * Math.cos(st.planeAngle);
|
||||
const py = cy + r * Math.sin(st.planeAngle);
|
||||
ctx.fillStyle = col.forceGravity || '#2563eb';
|
||||
ctx.beginPath(); ctx.arc(px, py, 10, 0, Math.PI*2); ctx.fill();
|
||||
ctx.strokeStyle = col.bodyAccent || '#1e293b';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
|
||||
document.getElementById('F13-go').addEventListener('click', () => {
|
||||
st.running = !st.running;
|
||||
document.getElementById('F13-go').textContent = st.running ? 'Пауза' : 'Запустить';
|
||||
});
|
||||
document.getElementById('F13-reset').addEventListener('click', reset);
|
||||
document.getElementById('F13-clear').addEventListener('click', () => { st.trail = []; draw(); });
|
||||
['F13-l','F13-s'].forEach(id => document.getElementById(id).addEventListener('input', update));
|
||||
|
||||
update();
|
||||
draw();
|
||||
B().startLoop('F13', cv, tick);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F13', { init: init, cleanup: function(){} });
|
||||
else document.addEventListener('DOMContentLoaded', ()=>{
|
||||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F13', { init: init, cleanup: function(){} });
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,209 @@
|
||||
// F14. Резонанс (§36 в ch4) — пружина + внешняя сила.
|
||||
(function(){
|
||||
'use strict';
|
||||
const B = () => window.PHYS9_FLAG_BASE;
|
||||
const C = () => window.PHYS9_COLORS || {};
|
||||
|
||||
function init(secId){
|
||||
if (!B()) return false;
|
||||
const body = ''
|
||||
+ '<div class="flag-sliders">'
|
||||
+ '<label>$m$, кг: <b id="F14-mv">1</b><input type="range" id="F14-m" min="0.1" max="5" step="0.1" value="1"></label>'
|
||||
+ '<label>$k$, Н/м: <b id="F14-kv">40</b><input type="range" id="F14-k" min="5" max="200" step="5" value="40"></label>'
|
||||
+ '<label>Внешняя частота $\\nu_{вн}$, Гц: <b id="F14-fv">1.00</b><input type="range" id="F14-f" min="0.05" max="3" step="0.05" value="1"></label>'
|
||||
+ '<label>Затухание $\\gamma$: <b id="F14-gv">0.1</b><input type="range" id="F14-g" min="0.02" max="1" step="0.02" value="0.1"></label>'
|
||||
+ '</div>'
|
||||
+ '<canvas id="F14-cv" class="flag-canvas" width="700" height="380" style="height:380px"></canvas>'
|
||||
+ '<div class="flag-controls">'
|
||||
+ '<button class="flag-btn primary" id="F14-go">Запустить</button>'
|
||||
+ '<button class="flag-btn" id="F14-reset">Сброс</button>'
|
||||
+ '<button class="flag-btn" id="F14-tune">Настроить на резонанс</button>'
|
||||
+ '</div>'
|
||||
+ '<div class="flag-stats">'
|
||||
+ '<div class="flag-stat"><span class="lbl">$\\nu_{собств}$</span><span class="val" id="F14-nu0">1.01 Гц</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">$\\nu_{внешн}$</span><span class="val" id="F14-nuext">1.00 Гц</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">Амплитуда</span><span class="val" id="F14-A">0 м</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">Состояние</span><span class="val" id="F14-mode">—</span></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="flag-feedback" id="F14-fb"></div>';
|
||||
|
||||
const card = B().makeCard(secId,
|
||||
'F14. Резонанс пружинного маятника',
|
||||
'Слева: пружина с грузом + внешняя сила. Справа: график амплитуды от частоты — резонансный пик при $\\nu_{вн} = \\nu_{собств}$.',
|
||||
body);
|
||||
if (!card) return false;
|
||||
|
||||
const cv = document.getElementById('F14-cv');
|
||||
const ctx = cv.getContext('2d');
|
||||
const W = cv.width, H = cv.height;
|
||||
const cx = W * 0.25;
|
||||
|
||||
let st = { x: 0, v: 0, t: 0, running: false, ampHistory: [] };
|
||||
|
||||
function nuOwn(){
|
||||
const m = +document.getElementById('F14-m').value;
|
||||
const k = +document.getElementById('F14-k').value;
|
||||
return Math.sqrt(k/m) / (2*Math.PI);
|
||||
}
|
||||
|
||||
function update(){
|
||||
document.getElementById('F14-mv').textContent = (+document.getElementById('F14-m').value).toFixed(1);
|
||||
document.getElementById('F14-kv').textContent = +document.getElementById('F14-k').value;
|
||||
document.getElementById('F14-fv').textContent = (+document.getElementById('F14-f').value).toFixed(2);
|
||||
document.getElementById('F14-gv').textContent = (+document.getElementById('F14-g').value).toFixed(2);
|
||||
document.getElementById('F14-nu0').textContent = nuOwn().toFixed(2) + ' Гц';
|
||||
document.getElementById('F14-nuext').textContent = (+document.getElementById('F14-f').value).toFixed(2) + ' Гц';
|
||||
}
|
||||
|
||||
function reset(){
|
||||
st = { x: 0, v: 0, t: 0, running: false, ampHistory: [] };
|
||||
document.getElementById('F14-go').textContent = 'Запустить';
|
||||
draw();
|
||||
}
|
||||
|
||||
function tick(dt){
|
||||
if (!st.running) { draw(); return; }
|
||||
const m = +document.getElementById('F14-m').value;
|
||||
const k = +document.getElementById('F14-k').value;
|
||||
const omega_ext = 2*Math.PI*(+document.getElementById('F14-f').value);
|
||||
const gamma = +document.getElementById('F14-g').value;
|
||||
const F0 = 5;
|
||||
/* m·x'' = -kx - γ·v + F0·cos(ωt) */
|
||||
const N = 8;
|
||||
const ddt = dt / N;
|
||||
for (let i = 0; i < N; i++){
|
||||
const F = F0 * Math.cos(omega_ext * st.t);
|
||||
const a = (-k*st.x - gamma*st.v + F) / m;
|
||||
st.v += a * ddt;
|
||||
st.x += st.v * ddt;
|
||||
st.t += ddt;
|
||||
}
|
||||
/* записываем амплитуду */
|
||||
st.ampHistory.push({ t: st.t, x: st.x });
|
||||
if (st.ampHistory.length > 800) st.ampHistory.shift();
|
||||
/* текущая амплитуда — максимум за последние 2 с */
|
||||
let amp = 0;
|
||||
for (const h of st.ampHistory){
|
||||
if (st.t - h.t < 5) amp = Math.max(amp, Math.abs(h.x));
|
||||
}
|
||||
document.getElementById('F14-A').textContent = amp.toFixed(3) + ' м';
|
||||
/* классификация */
|
||||
const nu0 = nuOwn();
|
||||
const nuExt = +document.getElementById('F14-f').value;
|
||||
const mode = document.getElementById('F14-mode');
|
||||
const fb = document.getElementById('F14-fb');
|
||||
if (Math.abs(nu0 - nuExt) / nu0 < 0.08){
|
||||
mode.textContent = 'РЕЗОНАНС!';
|
||||
mode.style.color = 'var(--fail,#dc2626)';
|
||||
fb.className = 'flag-feedback fail show';
|
||||
fb.innerHTML = '⚠ РЕЗОНАНС! Амплитуда нарастает. Так разрушаются мосты — рота не должна маршировать через них в ногу.';
|
||||
} else if (Math.abs(nu0 - nuExt) / nu0 < 0.25){
|
||||
mode.textContent = 'близко к резонансу';
|
||||
mode.style.color = 'var(--warn,#f59e0b)';
|
||||
fb.className = 'flag-feedback warn show';
|
||||
fb.innerHTML = 'Близко к резонансу — амплитуда заметно больше внешней силы.';
|
||||
} else {
|
||||
mode.textContent = 'вынужденные колебания';
|
||||
mode.style.color = 'var(--ok,#10b981)';
|
||||
fb.className = 'flag-feedback';
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw(){
|
||||
const col = C();
|
||||
ctx.fillStyle = col.bg || '#fafafa';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
/* левая половина — пружина с грузом */
|
||||
/* потолок */
|
||||
ctx.fillStyle = col.surface || '#a16207';
|
||||
ctx.fillRect(cx - 60, 30, 120, 14);
|
||||
/* пружина */
|
||||
const massY = 160 + st.x * 400;
|
||||
ctx.strokeStyle = col.forceSpring || '#d97706';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx, 44);
|
||||
for (let i = 0; i < 14; i++){
|
||||
const t = i / 14;
|
||||
const y = 44 + t * (massY - 44);
|
||||
const x = cx + Math.sin(t * 14 * Math.PI) * 12;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo(cx, massY);
|
||||
ctx.stroke();
|
||||
/* груз */
|
||||
ctx.fillStyle = col.forceGravity || '#2563eb';
|
||||
ctx.fillRect(cx - 25, massY, 50, 30);
|
||||
ctx.strokeStyle = col.bodyAccent || '#1e293b';
|
||||
ctx.strokeRect(cx - 25, massY, 50, 30);
|
||||
/* нулевая линия */
|
||||
ctx.strokeStyle = col.grid || '#e5e7eb';
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - 60, 160); ctx.lineTo(cx + 60, 160);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
/* стрелка внешней силы */
|
||||
const F_now = 5 * Math.cos(2*Math.PI*(+document.getElementById('F14-f').value) * st.t);
|
||||
if (st.running){
|
||||
B().arrow(ctx, cx + 35, massY + 15, cx + 35 + F_now*8, massY + 15, col.acceleration || '#ea580c', 2);
|
||||
}
|
||||
/* правая половина — график x(t) */
|
||||
const gx = W/2 + 20, gy = 40, gw = W/2 - 40, gh = H - 90;
|
||||
ctx.fillStyle = col.bgSubtle || '#f8fafc';
|
||||
ctx.fillRect(gx, gy, gw, gh);
|
||||
ctx.strokeStyle = col.axis || '#1e293b';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(gx, gy, gw, gh);
|
||||
/* центральная ось */
|
||||
ctx.strokeStyle = col.grid || '#e5e7eb';
|
||||
ctx.beginPath(); ctx.moveTo(gx, gy + gh/2); ctx.lineTo(gx + gw, gy + gh/2); ctx.stroke();
|
||||
/* линия x(t) */
|
||||
if (st.ampHistory.length > 1){
|
||||
const tMin = Math.max(0, st.t - 20);
|
||||
const tMax = st.t;
|
||||
let maxAbsX = 0.1;
|
||||
for (const h of st.ampHistory) if (h.t >= tMin) maxAbsX = Math.max(maxAbsX, Math.abs(h.x));
|
||||
ctx.strokeStyle = col.velocity || '#0891b2';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
let first = true;
|
||||
for (const h of st.ampHistory){
|
||||
if (h.t < tMin) continue;
|
||||
const px = gx + ((h.t - tMin)/(tMax - tMin)) * gw;
|
||||
const py = gy + gh/2 - (h.x / maxAbsX) * (gh/2 - 5);
|
||||
if (first){ ctx.moveTo(px, py); first = false; } else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = col.textMuted || '#64748b';
|
||||
ctx.font = '11px Inter,sans-serif';
|
||||
ctx.fillText('x(t) — последние 20 с', gx + 6, gy + 14);
|
||||
ctx.fillText('t →', gx + gw - 24, gy + gh - 4);
|
||||
}
|
||||
|
||||
document.getElementById('F14-go').addEventListener('click', () => {
|
||||
st.running = !st.running;
|
||||
document.getElementById('F14-go').textContent = st.running ? 'Пауза' : 'Запустить';
|
||||
});
|
||||
document.getElementById('F14-reset').addEventListener('click', reset);
|
||||
document.getElementById('F14-tune').addEventListener('click', () => {
|
||||
document.getElementById('F14-f').value = nuOwn().toFixed(2);
|
||||
update();
|
||||
reset();
|
||||
});
|
||||
['F14-m','F14-k','F14-f','F14-g'].forEach(id => document.getElementById(id).addEventListener('input', update));
|
||||
|
||||
update();
|
||||
reset();
|
||||
B().startLoop('F14', cv, tick);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F14', { init: init, cleanup: function(){} });
|
||||
else document.addEventListener('DOMContentLoaded', ()=>{
|
||||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F14', { init: init, cleanup: function(){} });
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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(){} });
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -790,7 +790,7 @@ function _injectTasks(id){
|
||||
var body = document.getElementById(id + '-body');
|
||||
if(!body || body.querySelector('.legacy-tasks')) return;
|
||||
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
|
||||
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH2_WIDGETS && window.PHYS9_CH2_WIDGETS[id]) window.PHYS9_CH2_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p17') window.PHYS9_FLAG_BASE.mount('F4','p17'); else if(id==='p22') window.PHYS9_FLAG_BASE.mount('F5','p22'); else if(id==='p24') window.PHYS9_FLAG_BASE.mount('F7','p24'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
|
||||
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH2_WIDGETS && window.PHYS9_CH2_WIDGETS[id]) window.PHYS9_CH2_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p17') window.PHYS9_FLAG_BASE.mount('F4','p17'); else if(id==='p22') window.PHYS9_FLAG_BASE.mount('F5','p22'); else if(id==='p24') window.PHYS9_FLAG_BASE.mount('F7','p24'); else if(id==='p20') window.PHYS9_FLAG_BASE.mount('F6','p20'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
|
||||
}
|
||||
var _origEnsureBuilt = ensureBuilt;
|
||||
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
|
||||
|
||||
@@ -774,7 +774,7 @@ function _injectTasks(id){
|
||||
var body = document.getElementById(id + '-body');
|
||||
if(!body || body.querySelector('.legacy-tasks')) return;
|
||||
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
|
||||
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH4_WIDGETS && window.PHYS9_CH4_WIDGETS[id]) window.PHYS9_CH4_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p35') window.PHYS9_FLAG_BASE.mount('F12','p35'); else if(id==='p32') window.PHYS9_FLAG_BASE.mount('F11','p32'); else if(id==='final4') window.PHYS9_FLAG_BASE.mount('F19','final4'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
|
||||
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH4_WIDGETS && window.PHYS9_CH4_WIDGETS[id]) window.PHYS9_CH4_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='p35') window.PHYS9_FLAG_BASE.mount('F12','p35'); else if(id==='p32') window.PHYS9_FLAG_BASE.mount('F11','p32'); else if(id==='final4') window.PHYS9_FLAG_BASE.mount('F19','final4'); else if(id==='p36'){ window.PHYS9_FLAG_BASE.mount('F13','p36'); window.PHYS9_FLAG_BASE.mount('F14','p36'); } } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
|
||||
}
|
||||
var _origEnsureBuilt = ensureBuilt;
|
||||
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
|
||||
|
||||
Reference in New Issue
Block a user