feat(phys9 flagships): F5 Атвуд + F7 Лифт (Wave B пилоты)

F5. Машина Атвуда (§22):
- Canvas 640×420: блок с двумя массами на нити
- Slider'ы: m₁, m₂, трение в блоке
- Запуск: бо́льшая масса опускается, меньшая поднимается
- Физика: a = ((m₁-m₂)g - μ)/(m₁+m₂)
- Анимация: блок вращается, грузы движутся, размер пропорц. m
- Показ векторов сил тяжести (m₁g, m₂g) и натяжений (T) в покое
- Stats: a, T, v, t

F7. Лифт с динамометром (§24):
- Canvas 640×460: шахта с 5 этажами + большой циферблат справа
- Слева кабина с динамометром и грузом m
- Slider'ы: m груза, a разгона
- 5 режимов кнопок:
  - Разгон ⬆ (hold) → a = +a_in
  - Разгон ⬇ (hold) → a = -a_in
  - Стоп → a = 0
  - Свободное падение → a = -g (трос показывается пунктиром)
  - Сброс
- 2 динамометра: мини в кабине + большой круглый (шкала 0..2.5g)
- Stats: P, P/(mg), v лифта, h высота
- Контекстный feedback: невесомость / норма / перегрузка / P<0

Подключение в ch2: F5 на p22 (закон Ньютона II), F7 на p24 (вес).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 10:13:08 +03:00
parent d701d824ba
commit d190fd2de9
3 changed files with 419 additions and 1 deletions
@@ -0,0 +1,182 @@
// F5. Машина Атвуда (§22 в ch2) — связанные массы через блок.
(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_1$ (левая), кг: <b id="F5-m1v">3</b><input type="range" id="F5-m1" min="0.1" max="10" step="0.1" value="3"></label>'
+ '<label>$m_2$ (правая), кг: <b id="F5-m2v">2</b><input type="range" id="F5-m2" min="0.1" max="10" step="0.1" value="2"></label>'
+ '<label>Трение в блоке: <b id="F5-frv">0</b><input type="range" id="F5-fr" min="0" max="3" step="0.05" value="0"></label>'
+ '</div>'
+ '<canvas id="F5-cv" class="flag-canvas" width="640" height="420" style="height:420px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F5-go">Запустить</button>'
+ '<button class="flag-btn" id="F5-reset">Сброс</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">$a = (m_1-m_2)g/(m_1+m_2)$</span><span class="val" id="F5-a">1.96 м/с²</span></div>'
+ '<div class="flag-stat"><span class="lbl">Натяжение T</span><span class="val" id="F5-T">23.5 Н</span></div>'
+ '<div class="flag-stat"><span class="lbl">$v$ текущая</span><span class="val" id="F5-v">0 м/с</span></div>'
+ '<div class="flag-stat"><span class="lbl">$t$ прошло</span><span class="val" id="F5-t">0 с</span></div>'
+ '</div>';
const card = B().makeCard(secId,
'F5. Машина Атвуда',
'Две массы через блок на нити. Бо́льшая опускается, меньшая поднимается. Физика: $a = (m_1-m_2)g/(m_1+m_2)$.',
body);
if (!card) return false;
const cv = document.getElementById('F5-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const blockX = W/2, blockY = 60, blockR = 28;
let st = { y1: 220, y2: 220, v: 0, t: 0, running: false };
/* y — расстояние от блока вниз, +v = m1 опускается */
function readSliders(){
const m1 = +document.getElementById('F5-m1').value;
const m2 = +document.getElementById('F5-m2').value;
const fr = +document.getElementById('F5-fr').value;
document.getElementById('F5-m1v').textContent = m1.toFixed(2);
document.getElementById('F5-m2v').textContent = m2.toFixed(2);
document.getElementById('F5-frv').textContent = fr.toFixed(2);
const g = 9.8;
const a = ((m1 - m2)*g - fr) / (m1 + m2);
const T = m2 * (g + a);
document.getElementById('F5-a').textContent = a.toFixed(2) + ' м/с²';
document.getElementById('F5-T').textContent = T.toFixed(1) + ' Н';
}
function reset(){
st = { y1: 220, y2: 220, v: 0, t: 0, running: false };
document.getElementById('F5-go').textContent = 'Запустить';
document.getElementById('F5-v').textContent = '0 м/с';
document.getElementById('F5-t').textContent = '0 с';
draw();
}
function tick(dt){
if (!st.running) { draw(); return; }
const m1 = +document.getElementById('F5-m1').value;
const m2 = +document.getElementById('F5-m2').value;
const fr = +document.getElementById('F5-fr').value;
const g = 9.8;
const a = ((m1 - m2)*g - fr*Math.sign(st.v || (m1-m2))) / (m1 + m2);
st.v += a * dt;
/* движение: m1 опускается со скоростью v, m2 поднимается */
const dy = st.v * dt * 30; /* масштаб 30 px/м */
st.y1 += dy;
st.y2 -= dy;
/* ограничения */
if (st.y1 > H - 80){ st.y1 = H - 80; st.v = 0; st.running = false; document.getElementById('F5-go').textContent='Запустить'; }
if (st.y2 < 100){ st.y2 = 100; st.v = 0; st.running = false; document.getElementById('F5-go').textContent='Запустить'; }
if (st.y1 < 100){ st.y1 = 100; st.v = 0; st.running = false; document.getElementById('F5-go').textContent='Запустить'; }
if (st.y2 > H - 80){ st.y2 = H - 80; st.v = 0; st.running = false; document.getElementById('F5-go').textContent='Запустить'; }
st.t += dt;
document.getElementById('F5-v').textContent = st.v.toFixed(2) + ' м/с';
document.getElementById('F5-t').textContent = st.t.toFixed(1) + ' с';
draw();
}
function draw(){
const col = C();
ctx.fillStyle = col.bg || '#fafafa';
ctx.fillRect(0, 0, W, H);
/* потолок */
ctx.fillStyle = col.surface || '#a16207';
ctx.fillRect(0, 0, W, 30);
for (let x = 0; x < W; x += 12){
ctx.strokeStyle = '#7c4a08';
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x+6, 6); ctx.stroke();
}
/* кронштейн к блоку */
ctx.strokeStyle = col.axis || '#1e293b';
ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(blockX, 30); ctx.lineTo(blockX, blockY - blockR); ctx.stroke();
/* блок */
ctx.fillStyle = col.bodyLight || '#cbd5e1';
ctx.beginPath(); ctx.arc(blockX, blockY, blockR, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = col.axis || '#1e293b';
ctx.lineWidth = 2.5;
ctx.stroke();
/* радиальные линии (вращение) */
const rot = st.v * 0.1;
for (let i = 0; i < 4; i++){
const a = rot + i*Math.PI/2;
ctx.beginPath();
ctx.moveTo(blockX + (blockR-5)*Math.cos(a), blockY + (blockR-5)*Math.sin(a));
ctx.lineTo(blockX + 5*Math.cos(a), blockY + 5*Math.sin(a));
ctx.stroke();
}
/* нити */
const cordL = blockX - blockR;
const cordR = blockX + blockR;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cordL, blockY); ctx.lineTo(cordL, st.y1);
ctx.moveTo(cordR, blockY); ctx.lineTo(cordR, st.y2);
ctx.stroke();
/* грузы */
const m1 = +document.getElementById('F5-m1').value;
const m2 = +document.getElementById('F5-m2').value;
const r1 = Math.min(40, 12 + m1*3);
const r2 = Math.min(40, 12 + m2*3);
ctx.fillStyle = col.forceGravity || '#2563eb';
ctx.fillRect(cordL - r1, st.y1, r1*2, r1);
ctx.strokeStyle = col.axis || '#1e293b';
ctx.lineWidth = 1.5;
ctx.strokeRect(cordL - r1, st.y1, r1*2, r1);
ctx.fillStyle = col.fail || '#dc2626';
ctx.fillRect(cordR - r2, st.y2, r2*2, r2);
ctx.strokeRect(cordR - r2, st.y2, r2*2, r2);
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px Inter,sans-serif';
ctx.textAlign = 'center';
ctx.fillText('m₁ = '+m1.toFixed(1)+' кг', cordL, st.y1 + r1/2 + 4);
ctx.fillText('m₂ = '+m2.toFixed(1)+' кг', cordR, st.y2 + r2/2 + 4);
ctx.textAlign = 'left';
/* векторы силы тяжести */
if (st.t < 0.5 || !st.running){
B().arrow(ctx, cordL, st.y1 + r1, cordL, st.y1 + r1 + 40, col.forceGravity || '#2563eb', 2);
ctx.fillStyle = col.forceGravity || '#2563eb';
ctx.font = 'bold 12px Inter,sans-serif';
ctx.fillText('m₁g', cordL + 8, st.y1 + r1 + 30);
B().arrow(ctx, cordR, st.y2 + r2, cordR, st.y2 + r2 + 40, col.forceGravity || '#2563eb', 2);
ctx.fillText('m₂g', cordR + 8, st.y2 + r2 + 30);
/* T снизу */
B().arrow(ctx, cordL, st.y1, cordL, st.y1 - 30, col.forceTension || '#16a34a', 2);
ctx.fillStyle = col.forceTension || '#16a34a';
ctx.fillText('T', cordL + 8, st.y1 - 20);
B().arrow(ctx, cordR, st.y2, cordR, st.y2 - 30, col.forceTension || '#16a34a', 2);
ctx.fillText('T', cordR + 8, st.y2 - 20);
}
}
document.getElementById('F5-go').addEventListener('click', ()=>{
if (st.y1 >= H - 80 || st.y2 >= H - 80 || st.y1 < 110 || st.y2 < 110) reset();
st.running = !st.running;
document.getElementById('F5-go').textContent = st.running ? 'Пауза' : 'Запустить';
});
document.getElementById('F5-reset').addEventListener('click', reset);
['F5-m1','F5-m2','F5-fr'].forEach(id => document.getElementById(id).addEventListener('input', ()=>{
readSliders();
if (!st.running) reset();
}));
readSliders();
draw();
B().startLoop('F5', cv, tick);
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F5', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F5', { init: init, cleanup: function(){} });
});
})();