Files
Learn_System/frontend/js/flagships/phys9_flag_F7_elevator.js
T
Maxim Dolgolyov d190fd2de9 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>
2026-05-30 10:13:08 +03:00

237 lines
11 KiB
JavaScript
Raw 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.
// F7. Лифт с динамометром (§24 в 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$, кг: <b id="F7-mv">1.0</b><input type="range" id="F7-m" min="0.1" max="10" step="0.1" value="1"></label>'
+ '<label>$a$ разгона, м/с²: <b id="F7-av">3</b><input type="range" id="F7-a" min="0.5" max="9.8" step="0.5" value="3"></label>'
+ '</div>'
+ '<canvas id="F7-cv" class="flag-canvas" width="640" height="460" style="height:460px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F7-up">Разгон ⬆</button>'
+ '<button class="flag-btn primary" id="F7-dn">Разгон ⬇</button>'
+ '<button class="flag-btn" id="F7-stop">Стоп (a=0)</button>'
+ '<button class="flag-btn danger" id="F7-fall">Свободное падение!</button>'
+ '<button class="flag-btn" id="F7-reset">Сброс</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Вес динамометра</span><span class="val" id="F7-P">9.8 Н</span></div>'
+ '<div class="flag-stat"><span class="lbl">Отношение P/mg</span><span class="val" id="F7-pmg">1.0 g</span></div>'
+ '<div class="flag-stat"><span class="lbl">$v$ лифта</span><span class="val" id="F7-v">0 м/с</span></div>'
+ '<div class="flag-stat"><span class="lbl">$h$ высота</span><span class="val" id="F7-h">0 м</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F7-fb"></div>';
const card = B().makeCard(secId,
'F7. Лифт с динамометром',
'Управляй лифтом — наблюдай, как меняется показание динамометра. $P = m(g + a)$. При $a = -g$ — невесомость.',
body);
if (!card) return false;
const cv = document.getElementById('F7-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
let st = { y: 0, v: 0, a: 0, t: 0, mode: 'idle' };
function readSliders(){
document.getElementById('F7-mv').textContent = (+document.getElementById('F7-m').value).toFixed(2);
document.getElementById('F7-av').textContent = (+document.getElementById('F7-a').value).toFixed(1);
}
function reset(){
st = { y: 0, v: 0, a: 0, t: 0, mode: 'idle' };
document.getElementById('F7-fb').className = 'flag-feedback';
draw();
}
function tick(dt){
const m = +document.getElementById('F7-m').value;
const a_in = +document.getElementById('F7-a').value;
const g = 9.8;
/* Управление режимом */
if (st.mode === 'up') st.a = a_in;
else if (st.mode === 'dn') st.a = -a_in;
else if (st.mode === 'stop') st.a = 0;
else if (st.mode === 'fall') st.a = -g;
else st.a = 0;
st.v += st.a * dt;
st.y += st.v * dt;
/* ограничения */
if (st.y < 0){ st.y = 0; if (st.v < 0) st.v = 0; }
if (st.y > 50){ st.y = 50; if (st.v > 0) st.v = 0; }
st.t += dt;
/* Вес */
const P = m * (g + st.a);
const ratio = P/(m*g);
document.getElementById('F7-P').textContent = P.toFixed(2) + ' Н';
document.getElementById('F7-pmg').textContent = ratio.toFixed(2) + ' g';
document.getElementById('F7-v').textContent = st.v.toFixed(2) + ' м/с';
document.getElementById('F7-h').textContent = st.y.toFixed(1) + ' м';
const fb = document.getElementById('F7-fb');
if (Math.abs(P) < m*g*0.05){
fb.className = 'flag-feedback warn show';
fb.innerHTML = '&#9888; НЕВЕСОМОСТЬ! Динамометр показывает почти 0.';
} else if (P < 0){
fb.className = 'flag-feedback fail show';
fb.innerHTML = 'P < 0: предмет «отрывается» от пола (или сильно прижимается к потолку).';
} else if (ratio > 1.5){
fb.className = 'flag-feedback warn show';
fb.innerHTML = 'ПЕРЕГРУЗКА '+ratio.toFixed(1)+'g — космонавты тренируются на таких.';
} else if (Math.abs(ratio - 1) < 0.05){
fb.className = 'flag-feedback ok show';
fb.innerHTML = 'Нормальный вес — $a = 0$ или равномерное движение.';
} else {
fb.className = 'flag-feedback';
}
draw();
}
function draw(){
const col = C();
ctx.fillStyle = col.bg || '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Шахта */
const shaftX = 80, shaftY = 30, shaftW = 280, shaftH = H - 60;
ctx.fillStyle = col.bgSubtle || '#f8fafc';
ctx.fillRect(shaftX, shaftY, shaftW, shaftH);
ctx.strokeStyle = col.axis || '#1e293b';
ctx.lineWidth = 2;
ctx.strokeRect(shaftX, shaftY, shaftW, shaftH);
/* Этажи */
ctx.strokeStyle = col.grid || '#e5e7eb';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++){
const py = shaftY + (i/5) * shaftH;
ctx.beginPath(); ctx.moveTo(shaftX, py); ctx.lineTo(shaftX + shaftW, py); ctx.stroke();
ctx.fillStyle = col.textMuted || '#64748b';
ctx.font = '11px Inter,sans-serif';
ctx.fillText((5-i)+' эт', shaftX + 6, py + 12);
}
/* Лифт (кабина) */
const yNorm = st.y / 50; /* 0..1 */
const liftY = shaftY + shaftH - 100 - yNorm * (shaftH - 120);
ctx.fillStyle = col.bodyLight || '#cbd5e1';
ctx.fillRect(shaftX + 50, liftY, shaftW - 100, 90);
ctx.strokeStyle = col.bodyAccent || '#1e293b';
ctx.lineWidth = 2;
ctx.strokeRect(shaftX + 50, liftY, shaftW - 100, 90);
/* Трос */
ctx.strokeStyle = st.mode === 'fall' ? col.fail : col.axis || '#1e293b';
ctx.lineWidth = st.mode === 'fall' ? 1 : 2.5;
if (st.mode === 'fall'){
ctx.setLineDash([4, 4]);
}
ctx.beginPath();
ctx.moveTo(shaftX + shaftW/2, shaftY);
ctx.lineTo(shaftX + shaftW/2, liftY);
ctx.stroke();
ctx.setLineDash([]);
/* Динамометр внутри лифта */
const dynaX = shaftX + shaftW/2, dynaY = liftY + 20;
ctx.strokeStyle = col.axis || '#1e293b';
ctx.lineWidth = 1.5;
ctx.fillStyle = col.bg || '#fafafa';
ctx.fillRect(dynaX - 12, dynaY, 24, 22);
ctx.strokeRect(dynaX - 12, dynaY, 24, 22);
/* стрелка */
const m = +document.getElementById('F7-m').value;
const g = 9.8;
const P = m*(g + st.a);
const Pmax = m*g*2.5;
const norm = Math.max(0, Math.min(1, P/Pmax));
const angle = -Math.PI*0.75 + norm * Math.PI*1.5;
ctx.strokeStyle = col.fail || '#dc2626';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(dynaX, dynaY + 11);
ctx.lineTo(dynaX + 9*Math.cos(angle), dynaY + 11 + 9*Math.sin(angle));
ctx.stroke();
/* Груз ниже динамометра */
ctx.fillStyle = col.forceGravity || '#2563eb';
const gSize = Math.min(30, 10 + m*3);
ctx.fillRect(dynaX - gSize/2, dynaY + 30, gSize, gSize);
ctx.strokeStyle = col.bodyAccent || '#1e293b';
ctx.strokeRect(dynaX - gSize/2, dynaY + 30, gSize, gSize);
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px Inter,sans-serif';
ctx.textAlign = 'center';
ctx.fillText(m.toFixed(1)+' кг', dynaX, dynaY + 30 + gSize/2 + 3);
ctx.textAlign = 'left';
/* подпись режима */
ctx.fillStyle = col.text || '#0f172a';
ctx.font = 'bold 13px Inter,sans-serif';
const modeText = { idle: '— пауза —', up: '↑ РАЗГОН ВВЕРХ', dn: '↓ РАЗГОН ВНИЗ', stop: '— равномерно/стоит', fall: 'СВОБОДНОЕ ПАДЕНИЕ' }[st.mode];
ctx.fillText(modeText, shaftX + shaftW + 20, 60);
/* большой циферблат справа */
const dialX = shaftX + shaftW + 130, dialY = 200, dialR = 70;
ctx.fillStyle = col.bg || '#fafafa';
ctx.beginPath(); ctx.arc(dialX, dialY, dialR, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = col.axis || '#1e293b';
ctx.lineWidth = 3;
ctx.stroke();
/* шкала 0..2.5 g */
ctx.lineWidth = 2;
for (let i = 0; i <= 10; i++){
const a = Math.PI*0.75 + (i/10) * Math.PI*1.5;
const r1 = dialR - 12, r2 = dialR;
ctx.strokeStyle = col.axis || '#1e293b';
ctx.beginPath();
ctx.moveTo(dialX + r1*Math.cos(a), dialY + r1*Math.sin(a));
ctx.lineTo(dialX + r2*Math.cos(a), dialY + r2*Math.sin(a));
ctx.stroke();
ctx.fillStyle = col.textMuted || '#64748b';
ctx.font = '10px Inter,sans-serif';
const lbl = (i*0.25).toFixed(1);
const lblR = dialR - 25;
ctx.fillText(lbl, dialX + lblR*Math.cos(a) - 8, dialY + lblR*Math.sin(a) + 4);
}
/* стрелка на большом */
const normBig = Math.max(0, Math.min(1, P/(m*g*2.5)));
const aBig = Math.PI*0.75 + normBig * Math.PI*1.5;
ctx.strokeStyle = col.fail || '#dc2626';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(dialX, dialY);
ctx.lineTo(dialX + (dialR-15)*Math.cos(aBig), dialY + (dialR-15)*Math.sin(aBig));
ctx.stroke();
ctx.fillStyle = col.fail || '#dc2626';
ctx.beginPath(); ctx.arc(dialX, dialY, 5, 0, Math.PI*2); ctx.fill();
/* подпись */
ctx.fillStyle = col.text || '#0f172a';
ctx.textAlign = 'center';
ctx.font = 'bold 14px Inter,sans-serif';
ctx.fillText(P.toFixed(1) + ' Н', dialX, dialY + dialR + 22);
ctx.font = '11px Inter,sans-serif';
ctx.fillText('(' + (P/(m*g)).toFixed(2) + ' g)', dialX, dialY + dialR + 38);
ctx.textAlign = 'left';
}
document.getElementById('F7-up').addEventListener('mousedown', ()=>{ st.mode = 'up'; });
document.getElementById('F7-up').addEventListener('mouseup', ()=>{ st.mode = 'stop'; });
document.getElementById('F7-dn').addEventListener('mousedown', ()=>{ st.mode = 'dn'; });
document.getElementById('F7-dn').addEventListener('mouseup', ()=>{ st.mode = 'stop'; });
document.getElementById('F7-stop').addEventListener('click', ()=>{ st.mode = 'stop'; });
document.getElementById('F7-fall').addEventListener('click', ()=>{ st.mode = 'fall'; });
document.getElementById('F7-reset').addEventListener('click', reset);
['F7-m','F7-a'].forEach(id => document.getElementById(id).addEventListener('input', readSliders));
readSliders();
draw();
B().startLoop('F7', cv, tick);
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F7', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F7', { init: init, cleanup: function(){} });
});
})();