fix(phys7): главный визуал курса работает + §22, §24 интерактивы улучшены

1. БАГ В HillSlideSim (phys.js):
   - При reset() начальное состояние x=0, h=hStart, v=0.
   - Первый step(): dropped=0 → v=0 → x не растёт → h не падает → тележка
     навсегда стоит на вершине (бесконечный нуль). Анимация ничего не показывала.
   - Фикс: reset() даёт начальный толчок (x = L*0.01) и v по энергии для
     этой малой высоты падения. step() теперь корректно ускоряет тележку.
   - Тест node: за 2.05 с тележка проходит 11.7 м, h падает с 4.9 м до 0.86 м,
     v растёт с 1.4 до 9.0 м/с. Е_полн ≈ const.

2. §22 «Сила тяжести» — новый IV-2 «Падение на 4 планетах»:
   - SVG 4-колоночная сцена, 4 шарика стартуют с одной высоты.
   - Slider высоты 2..20 м, кнопки «Уронить» / «Сброс».
   - Свободное падение по h(t) = h₀ − gt²/2 для каждой планеты (Земля 9.8,
     Луна 1.6, Марс 3.7, Юпитер 24.8).
   - Видно: Юпитер падает первым, Луна последней; для каждого сохраняется
     время падения √(2h/g) и итоговая v = g·t.
   - Live info: текущее t, статус каждого шарика (падает / упал за X с,
     v = Y м/с).

3. §24 «Вес тела» — переработан IV-1 «Лифт с динамометром»:
   - Было: 4 статичных схемы покой/падение/верх/вниз.
   - Стало: динамический симулятор. Кабина лифта со стрелкой ускорения
     снаружи, внутри — груз на пружинном динамометре с шкалой.
   - 2 slider'а: масса 0.5..10 кг, ускорение −10..+10 м/с².
   - 4 кнопки-пресета: Покой / Едет вверх / Едет вниз / Свободное падение.
   - Формула P = m(g + a) считается в реальном времени.
   - 4 режима с автоопределением: ПОКОЙ / НЕВЕСОМОСТЬ / ПЕРЕГРУЗКА /
     ПОНИЖЕННЫЙ ВЕС с разной цветовой индикацией.
   - Пружина динамометра реально растягивается/сжимается в зависимости
     от P; указатель и шкала тоже.

Parse OK, smoke (15 экспортов CH3) OK.
This commit is contained in:
Maxim Dolgolyov
2026-05-30 12:14:48 +03:00
parent a60349d339
commit 98f955a85e
2 changed files with 194 additions and 74 deletions
+23 -5
View File
@@ -672,15 +672,33 @@ class HillSlideSim {
this.scale = opts.scale || 30;
this.reset();
}
reset() { this.t = 0; this.x = 0; this.v = 0; this.h = this.hStart; }
reset() {
// Начальный импульс: тележка стартует с лёгким толчком (1% от L) и небольшой скоростью,
// иначе при h=hStart, v=0 — она навсегда останется на вершине (бесконечный нуль).
const L = this.hStart * 4;
this.t = 0;
this.x = L * 0.01;
const xRel = this.x / L;
this.h = this.hStart * Math.pow(1 - xRel, 2);
this.v = Math.sqrt(2 * this.g * (this.hStart - this.h));
}
step(dt) {
const gEff = this.g * (1 - this.friction);
this.t += dt;
const dropped = this.hStart - this.h;
if (this.h <= 0) { this.h = 0; this.v = Math.sqrt(2 * gEff * this.hStart); return; }
this.v = Math.sqrt(2 * gEff * Math.max(0, dropped));
this.x += this.v * dt;
const L = this.hStart * 4;
if (this.h <= 0.001 || this.x >= L) {
this.h = 0;
// Финальная скорость с учётом потерь на трение.
this.v = Math.sqrt(2 * gEff * this.hStart);
this.x = L;
return;
}
// Скорость по закону сохранения энергии (с учётом трения).
const dropped = Math.max(0.0001, this.hStart - this.h);
this.v = Math.sqrt(2 * gEff * dropped);
// Скорость по x — это горизонтальная компонента; для наглядного моделирования
// используем её напрямую как темп роста x.
this.x += this.v * dt;
const xRel = Math.min(this.x, L) / L;
this.h = this.hStart * Math.pow(1 - xRel, 2);
}
+171 -69
View File
@@ -1223,8 +1223,18 @@ function add_p22(){
+ '$F_т = m g = $ <b id="p22-Ft" style="color:' + ACCENT_D + ';font-family:JetBrains Mono,monospace">0.98</b> Н (на <span id="p22-pl-nm" style="font-weight:700">Земле</span>, $g = $ <span id="p22-g-val" style="font-family:JetBrains Mono,monospace;font-weight:700">9.8</span> Н/кг)'
+ '</div>');
/* IV-2 КВИЗ */
h += wgWrap('p22-iv2', 'КВИЗ', 'Сила тяжести', '',
/* IV-2 СИМ — Падение тела на 4 планетах */
h += wgWrap('p22-iv2', 'СИМ', 'Падение с одной высоты на 4 планетах', 'Нажми «Уронить» — увидь, кто упадёт быстрее. Время и скорость в реальном времени.',
'<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">'
+ '<button id="p22-drop" type="button" style="background:#dc2626;color:#fff;border:none;padding:9px 18px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.9rem">Уронить</button>'
+ '<button id="p22-drop-reset" type="button" style="background:#fff;color:#dc2626;border:1.5px solid #dc2626;padding:9px 14px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.88rem">Сброс</button>'
+ '<label style="display:inline-flex;align-items:center;gap:6px;background:#fff;border:1px solid ' + ACCENT_SOFT + ';padding:6px 12px;border-radius:8px;font-size:.86rem;color:#475569">Высота $h$, м: <b id="p22-drop-h" style="color:#dc2626;font-family:JetBrains Mono,monospace">10</b><input type="range" id="p22-drop-h-r" min="2" max="20" step="1" value="10" style="width:120px;accent-color:#dc2626"></label>'
+ '</div>'
+ '<svg id="p22-drop-svg" viewBox="0 0 380 200" width="100%" style="max-width:600px;display:block;margin:0 auto;background:#fef2f2;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="p22-drop-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.92rem;line-height:1.55"></div>');
/* IV-3 КВИЗ (был IV-2) */
h += wgWrap('p22-iv3', 'КВИЗ', 'Сила тяжести', '',
'<div id="p22-q-host">'
+ quizQuestion('p22-q', 0, 'Куда направлена сила тяжести на поверхности Земли?', ['Вверх','Вертикально вниз (к центру Земли)','В сторону Солнца','В произвольном направлении'], 1)
+ quizQuestion('p22-q', 1, '$g$ на Земле примерно равно…', ['1 Н/кг','9,8 Н/кг','100 Н/кг','1000 Н/кг'], 1)
@@ -1280,6 +1290,80 @@ function add_p22(){
document.getElementById('p22-m-r').addEventListener('input', upd22);
upd22();
// §22 IV-2 — Падение тел на 4 планетах
const planets22 = [
{ nm:'Земля', g:9.8, col:'#0284c7' },
{ nm:'Луна', g:1.6, col:'#94a3b8' },
{ nm:'Марс', g:3.7, col:'#dc2626' },
{ nm:'Юпитер', g:24.8, col:'#d97706' }
];
let drop22 = { t:0, raf:0, running:false, h0:10 };
function drawDrop22(){
const svg = document.getElementById('p22-drop-svg');
if(!svg) return;
const W = 380, H = 200, baseY = 175;
const pxPerM = (baseY - 25) / drop22.h0;
const colW = W / planets22.length;
let s = '';
s += '<line x1="0" y1="' + baseY + '" x2="' + W + '" y2="' + baseY + '" stroke="#0f172a" stroke-width="1.5"/>';
planets22.forEach((p, i) => {
const cx = colW * i + colW/2;
const startY = baseY - drop22.h0 * pxPerM;
// Падение: h(t) = h0 - g*t²/2 (свободное падение)
const fall = 0.5 * p.g * drop22.t * drop22.t;
const hNow = Math.max(0, drop22.h0 - fall);
const cy = baseY - hNow * pxPerM - 6;
const v = p.g * drop22.t;
const fellTime = Math.sqrt(2 * drop22.h0 / p.g);
const onGround = drop22.t >= fellTime;
// Опорная линия высоты
s += '<line x1="' + (cx - colW/2 + 10) + '" y1="' + startY + '" x2="' + (cx + colW/2 - 10) + '" y2="' + startY + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3 2"/>';
// Шарик
s += '<circle cx="' + cx.toFixed(1) + '" cy="' + (onGround ? baseY - 6 : cy).toFixed(1) + '" r="9" fill="' + p.col + '" stroke="#0f172a" stroke-width="1.5"/>';
// Подпись планеты
s += '<text x="' + cx + '" y="' + (baseY + 16) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="' + p.col + '">' + p.nm + '</text>';
s += '<text x="' + cx + '" y="' + 18 + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" fill="#475569">g=' + p.g + '</text>';
if(onGround) s += '<text x="' + cx + '" y="' + (baseY - 14) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" font-weight="700" fill="#10b981">' + fellTime.toFixed(2) + ' с</text>';
});
svg.innerHTML = s;
// Info
const status = planets22.map(p => {
const fellTime = Math.sqrt(2 * drop22.h0 / p.g);
const onGround = drop22.t >= fellTime;
const vNow = onGround ? p.g * fellTime : p.g * drop22.t;
return '<b style="color:' + p.col + '">' + p.nm + '</b>: ' + (onGround ? 'упал за ' + fellTime.toFixed(2) + ' с, $v = ' + vNow.toFixed(1) + '$ м/с' : 'падает, $v = ' + vNow.toFixed(1) + '$ м/с');
}).join(' &middot; ');
document.getElementById('p22-drop-info').innerHTML = '$t = ' + drop22.t.toFixed(2) + '$ с &middot; ' + status;
renderMath(document.getElementById('p22-drop-info'));
}
function loop22(){
if(!drop22.running) return;
drop22.t += 0.04;
drawDrop22();
// Останавливаем когда даже самая медленная (Луна) упала
const maxT = Math.sqrt(2 * drop22.h0 / 1.6);
if(drop22.t >= maxT + 0.5){ drop22.running = false; return; }
drop22.raf = requestAnimationFrame(loop22);
}
document.getElementById('p22-drop-h-r').addEventListener('input', () => {
drop22.h0 = +document.getElementById('p22-drop-h-r').value;
document.getElementById('p22-drop-h').textContent = drop22.h0;
drop22.t = 0; drop22.running = false;
if(drop22.raf) cancelAnimationFrame(drop22.raf);
drawDrop22();
});
document.getElementById('p22-drop').addEventListener('click', () => {
drop22.t = 0; drop22.running = true;
if(drop22.raf) cancelAnimationFrame(drop22.raf);
loop22();
});
document.getElementById('p22-drop-reset').addEventListener('click', () => {
drop22.t = 0; drop22.running = false;
if(drop22.raf) cancelAnimationFrame(drop22.raf);
drawDrop22();
});
drawDrop22();
wireDnd('p22-dnd', [
{ id:'a1', cat:'1n' },{ id:'a2', cat:'10n' },{ id:'a3', cat:'100n' },
{ id:'a4', cat:'1n' },{ id:'a5', cat:'10n' },{ id:'a6', cat:'100n' }
@@ -1444,14 +1528,18 @@ function add_p24(){
+ 'На <b>пружинных весах (динамометре)</b> измеряют силу, с которой тело растягивает пружину. '
+ 'Это и есть вес тела в Ньютонах.');
/* IV-1 СИМ: тело в 3 ситуациях */
h += wgWrap('p24-iv1', 'СИМ', 'Три ситуации: покой, падение, ускорение', 'Выбери ситуацию — увидь стрелки сил.',
'<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">'
+ [['rest','На столе (покой)'],['fall','Свободное падение'],['up','Ускоряется вверх'],['down','Ускоряется вниз']].map((s, i) =>
'<button class="p24-sit" data-s="' + s[0] + '" type="button" style="background:' + (i===0 ? '#dc2626' : '#fff') + ';color:' + (i===0 ? '#fff' : '#dc2626') + ';border:2px solid #dc2626;padding:7px 12px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.84rem">' + s[1] + '</button>').join('')
/* IV-1 СИМ: динамический лифт с динамометром */
h += wgWrap('p24-iv1', 'СИМ', 'Лифт с динамометром: вес меняется при ускорении', 'Подвинь slider ускорения — увидь, как меняется показание динамометра (вес).',
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-bottom:10px">'
+ '<label style="display:block;font-size:.86rem;color:#475569;background:#fff;padding:7px 11px;border-radius:8px;border:1px solid ' + ACCENT_SOFT + '">Масса груза $m$, кг: <b id="p24-m" style="color:#dc2626;font-family:JetBrains Mono,monospace">2</b><input type="range" id="p24-m-r" min="0.5" max="10" step="0.5" value="2" style="display:block;width:100%;margin-top:5px;accent-color:#dc2626"></label>'
+ '<label style="display:block;font-size:.86rem;color:#475569;background:#fff;padding:7px 11px;border-radius:8px;border:1px solid ' + ACCENT_SOFT + '">Ускорение лифта $a$, м/с²: <b id="p24-a" style="color:#dc2626;font-family:JetBrains Mono,monospace">0</b><input type="range" id="p24-a-r" min="-10" max="10" step="0.5" value="0" style="display:block;width:100%;margin-top:5px;accent-color:#dc2626"></label>'
+ '</div>'
+ '<svg id="p24-svg" viewBox="0 0 360 200" width="100%" style="max-width:500px;display:block;margin:0 auto;background:#fef2f2;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="p24-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.92rem;line-height:1.6"></div>');
+ '<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">'
+ [['0','Покой'],['2','Едет вверх с ускорением'],['-2','Едет вниз с ускорением'],['-10','Свободное падение']].map(p =>
'<button class="p24-preset" data-a="' + p[0] + '" type="button" style="background:#fff;color:#dc2626;border:1.5px solid #dc2626;padding:6px 12px;border-radius:8px;cursor:pointer;font-weight:600;font-family:inherit;font-size:.82rem">' + p[1] + '</button>').join('')
+ '</div>'
+ '<svg id="p24-svg" viewBox="0 0 380 260" width="100%" style="max-width:560px;display:block;margin:0 auto;background:#fef2f2;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="p24-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.94rem;line-height:1.65"></div>');
/* IV-2 КВИЗ */
h += wgWrap('p24-iv2', 'КВИЗ', 'Вес vs сила тяжести', '',
@@ -1489,72 +1577,86 @@ function add_p24(){
h += readButton('p24');
body.innerHTML = h;
function draw24(s){
function draw24(){
const svg = document.getElementById('p24-svg');
if(!svg) return;
const cx = 180, cy = 100;
let html = '';
let info = '';
if(s === 'rest'){
// Тело на столе
html += '<rect x="20" y="160" width="320" height="10" fill="#475569"/>';
for(let i = 0; i < 14; i++) html += '<line x1="' + (30 + i*22) + '" y1="170" x2="' + (24 + i*22) + '" y2="180" stroke="#0f172a" stroke-width="0.8"/>';
html += '<rect x="' + (cx-25) + '" y="' + (cy+30) + '" width="50" height="30" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5" rx="3"/>';
// F_т вниз (фиолетовая)
html += '<line x1="' + (cx-30) + '" y1="' + (cy+45) + '" x2="' + (cx-30) + '" y2="' + (cy+85) + '" stroke="#7c3aed" stroke-width="3"/>';
html += '<polygon points="' + (cx-35) + ',' + (cy+78) + ' ' + (cx-25) + ',' + (cy+78) + ' ' + (cx-30) + ',' + (cy+88) + '" fill="#7c3aed"/>';
html += '<text x="' + (cx-70) + '" y="' + (cy+85) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#7c3aed">F_т (на тело)</text>';
// P вниз от низа тела (индиго)
html += '<line x1="' + (cx+30) + '" y1="' + (cy+60) + '" x2="' + (cx+30) + '" y2="' + (cy+100) + '" stroke="#4338ca" stroke-width="3"/>';
html += '<polygon points="' + (cx+25) + ',' + (cy+93) + ' ' + (cx+35) + ',' + (cy+93) + ' ' + (cx+30) + ',' + (cy+103) + '" fill="#4338ca"/>';
html += '<text x="' + (cx+40) + '" y="' + (cy+100) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#4338ca">P (на стол)</text>';
info = '<b>Покой на горизонтальной опоре:</b> $P = F_т = mg$, но приложены к разным телам.';
} else if(s === 'fall'){
html += '<rect x="' + (cx-25) + '" y="' + (cy-15) + '" width="50" height="30" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5" rx="3"/>';
// F_т вниз
html += '<line x1="' + cx + '" y1="' + (cy+20) + '" x2="' + cx + '" y2="' + (cy+70) + '" stroke="#7c3aed" stroke-width="3"/>';
html += '<polygon points="' + (cx-5) + ',' + (cy+63) + ' ' + (cx+5) + ',' + (cy+63) + ' ' + cx + ',' + (cy+73) + '" fill="#7c3aed"/>';
html += '<text x="' + (cx+10) + '" y="' + (cy+55) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#7c3aed">F_т ↓</text>';
// P = 0 — пиктограмма
html += '<text x="' + (cx-100) + '" y="' + (cy-15) + '" font-family="Inter,sans-serif" font-size="14" font-weight="700" fill="#10b981">P = 0</text>';
html += '<text x="' + (cx-100) + '" y="' + (cy+5) + '" font-family="Inter,sans-serif" font-size="10" fill="#475569">невесомость</text>';
info = '<b>Свободное падение / орбита:</b> сила тяжести есть ($F_т = mg \\ne 0$), но вес $P = 0$ — тело ни на что не давит.';
} else if(s === 'up'){
html += '<rect x="20" y="160" width="320" height="10" fill="#475569"/>';
html += '<line x1="' + cx + '" y1="160" x2="' + cx + '" y2="' + (cy+30) + '" stroke="#92400e" stroke-width="2"/>'; // тяга вверх
html += '<rect x="' + (cx-25) + '" y="' + (cy+30) + '" width="50" height="30" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5" rx="3"/>';
// F_т
html += '<line x1="' + (cx-30) + '" y1="' + (cy+45) + '" x2="' + (cx-30) + '" y2="' + (cy+85) + '" stroke="#7c3aed" stroke-width="3"/>';
html += '<polygon points="' + (cx-35) + ',' + (cy+78) + ' ' + (cx-25) + ',' + (cy+78) + ' ' + (cx-30) + ',' + (cy+88) + '" fill="#7c3aed"/>';
html += '<text x="' + (cx-90) + '" y="' + (cy+88) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#7c3aed">F_т = mg</text>';
// P больше — длинная стрелка
html += '<line x1="' + (cx+30) + '" y1="' + (cy+60) + '" x2="' + (cx+30) + '" y2="' + (cy+115) + '" stroke="#dc2626" stroke-width="3.5"/>';
html += '<polygon points="' + (cx+25) + ',' + (cy+108) + ' ' + (cx+35) + ',' + (cy+108) + ' ' + (cx+30) + ',' + (cy+118) + '" fill="#dc2626"/>';
html += '<text x="' + (cx+40) + '" y="' + (cy+118) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">P > mg (перегрузка)</text>';
info = '<b>Лифт ускоряется вверх:</b> вес <b>больше</b> $mg$ — это перегрузка. Космонавты на старте испытывают $P \\approx 3 mg$.';
} else {
// down
html += '<rect x="20" y="160" width="320" height="10" fill="#475569"/>';
html += '<rect x="' + (cx-25) + '" y="' + (cy+30) + '" width="50" height="30" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5" rx="3"/>';
html += '<line x1="' + (cx-30) + '" y1="' + (cy+45) + '" x2="' + (cx-30) + '" y2="' + (cy+85) + '" stroke="#7c3aed" stroke-width="3"/>';
html += '<polygon points="' + (cx-35) + ',' + (cy+78) + ' ' + (cx-25) + ',' + (cy+78) + ' ' + (cx-30) + ',' + (cy+88) + '" fill="#7c3aed"/>';
html += '<text x="' + (cx-90) + '" y="' + (cy+88) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#7c3aed">F_т = mg</text>';
// P меньше — короткая стрелка
html += '<line x1="' + (cx+30) + '" y1="' + (cy+60) + '" x2="' + (cx+30) + '" y2="' + (cy+80) + '" stroke="#10b981" stroke-width="2.5"/>';
html += '<polygon points="' + (cx+25) + ',' + (cy+73) + ' ' + (cx+35) + ',' + (cy+73) + ' ' + (cx+30) + ',' + (cy+83) + '" fill="#10b981"/>';
html += '<text x="' + (cx+40) + '" y="' + (cy+78) + '" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#10b981">P < mg</text>';
info = '<b>Лифт ускоряется вниз:</b> вес <b>меньше</b> $mg$. Если ускорение $= g$, то $P = 0$ — невесомость.';
const m = +document.getElementById('p24-m-r').value;
const a = +document.getElementById('p24-a-r').value; // положительное = вверх
const g = 10;
// Вес на пружине: P = m(g + a). a > 0 (лифт ускоряется вверх) → P > mg.
// Если лифт в свободном падении (a = -g), P = 0.
const P = Math.max(0, m * (g + a));
const W = 380, H = 260;
// Кабина лифта 160×220, центрирована
const cabX = 110, cabY = 20, cabW = 160, cabH = 220;
let s = '';
// Шахта (стенки)
s += '<line x1="' + (cabX - 10) + '" y1="0" x2="' + (cabX - 10) + '" y2="' + H + '" stroke="#94a3b8" stroke-width="2" stroke-dasharray="6 4"/>';
s += '<line x1="' + (cabX + cabW + 10) + '" y1="0" x2="' + (cabX + cabW + 10) + '" y2="' + H + '" stroke="#94a3b8" stroke-width="2" stroke-dasharray="6 4"/>';
// Кабина
s += '<rect x="' + cabX + '" y="' + cabY + '" width="' + cabW + '" height="' + cabH + '" fill="#fff" stroke="#0f172a" stroke-width="2" rx="3"/>';
// Стрелка ускорения слева снаружи
if(Math.abs(a) > 0.3){
const aY = cabY + cabH/2;
const arrLen = Math.min(50, Math.abs(a) * 4);
const dir = a > 0 ? -1 : 1; // вверх = -y, вниз = +y
s += '<line x1="60" y1="' + aY + '" x2="60" y2="' + (aY + arrLen * dir) + '" stroke="#10b981" stroke-width="3"/>';
s += '<polygon points="' + 55 + ',' + (aY + arrLen * dir - 8 * (-dir)) + ' ' + 65 + ',' + (aY + arrLen * dir - 8 * (-dir)) + ' ' + 60 + ',' + (aY + arrLen * dir) + '" fill="#10b981"/>';
s += '<text x="60" y="' + (a > 0 ? aY - arrLen - 8 : aY + arrLen + 18) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#10b981">a = ' + (a > 0 ? '+' : '') + a + '</text>';
}
svg.innerHTML = html;
document.getElementById('p24-info').innerHTML = info;
// Динамометр (вертикальный): корпус, пружина, груз
const dynX = cabX + cabW/2, dynTop = cabY + 30;
s += '<line x1="' + dynX + '" y1="' + cabY + '" x2="' + dynX + '" y2="' + dynTop + '" stroke="#475569" stroke-width="2"/>';
// Корпус
s += '<rect x="' + (dynX - 15) + '" y="' + dynTop + '" width="30" height="120" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="3"/>';
// Пружина — длина пропорц. P
const maxStretch = 80;
const Pmax = m * (g + 10); // P при a = +10
const stretch = Math.min(maxStretch, (P / Pmax) * maxStretch);
const sprBot = dynTop + 5 + stretch;
const coils = 6;
let path = 'M ' + dynX + ' ' + (dynTop + 5);
for(let i = 0; i < coils; i++){
path += ' L ' + (dynX + (i%2 ? 9 : -9)) + ' ' + (dynTop + 5 + (i + 0.5) * stretch/coils);
}
path += ' L ' + dynX + ' ' + sprBot;
s += '<path d="' + path + '" fill="none" stroke="#92400e" stroke-width="1.5"/>';
// Шкала справа
for(let i = 0; i <= 10; i++){
const ty = dynTop + 5 + (i / 10) * maxStretch;
s += '<line x1="' + (dynX + 15) + '" y1="' + ty + '" x2="' + (dynX + 22) + '" y2="' + ty + '" stroke="#92400e" stroke-width="1"/>';
if(i % 2 === 0) s += '<text x="' + (dynX + 26) + '" y="' + (ty + 3) + '" font-family="Inter,sans-serif" font-size="8" fill="#92400e">' + ((Pmax * i / 10).toFixed(0)) + '</text>';
}
// Указатель
s += '<line x1="' + (dynX - 10) + '" y1="' + sprBot + '" x2="' + (dynX + 12) + '" y2="' + sprBot + '" stroke="#dc2626" stroke-width="2.5"/>';
// Груз — кружок снизу пружины
s += '<circle cx="' + dynX + '" cy="' + (sprBot + 18) + '" r="14" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5"/>';
s += '<text x="' + dynX + '" y="' + (sprBot + 22) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" font-weight="700" fill="#fff">' + m + ' кг</text>';
// Показание динамометра в нижней части кабины
s += '<text x="' + dynX + '" y="' + (cabY + cabH - 12) + '" text-anchor="middle" font-family="Unbounded,sans-serif" font-size="13" font-weight="800" fill="' + (P < 0.1 ? '#10b981' : (P > m*g ? '#dc2626' : (P < m*g ? '#0284c7' : '#475569'))) + '">P = ' + P.toFixed(1) + ' Н</text>';
// Подпись режима
let mode, modeCol;
if(Math.abs(P - m*g) < 0.01){ mode = 'ПОКОЙ или равномерно: P = mg'; modeCol = '#475569'; }
else if(P < 0.1){ mode = 'НЕВЕСОМОСТЬ: P = 0'; modeCol = '#10b981'; }
else if(P > m*g){ mode = 'ПЕРЕГРУЗКА: P > mg'; modeCol = '#dc2626'; }
else { mode = 'ПОНИЖЕННЫЙ ВЕС: P < mg'; modeCol = '#0284c7'; }
s += '<text x="' + (cabX + cabW/2) + '" y="14" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="800" fill="' + modeCol + '">' + mode + '</text>';
svg.innerHTML = s;
document.getElementById('p24-m').textContent = m;
document.getElementById('p24-a').textContent = a > 0 ? '+' + a : a;
const Ft = m * g;
document.getElementById('p24-info').innerHTML =
'<b>$F_т = mg = ' + Ft.toFixed(1) + '$ Н</b> — сила тяжести на тело (не меняется при ускорении).<br>'
+ '<b>$P = m(g + a) = ' + m + ' \\cdot (' + g + (a >= 0 ? ' + ' : ' ') + Math.abs(a) + ') = ' + P.toFixed(1) + '$ Н</b> — показание динамометра (вес).<br>'
+ '<span style="color:' + modeCol + ';font-weight:700">' + mode + '</span>';
renderMath(document.getElementById('p24-info'));
}
body.querySelectorAll('.p24-sit').forEach(btn => btn.addEventListener('click', () => {
body.querySelectorAll('.p24-sit').forEach(b => { b.style.background = '#fff'; b.style.color = '#dc2626'; });
btn.style.background = '#dc2626'; btn.style.color = '#fff';
draw24(btn.dataset.s);
body.querySelectorAll('.p24-preset').forEach(btn => btn.addEventListener('click', () => {
document.getElementById('p24-a-r').value = btn.dataset.a;
draw24();
}));
draw24('rest');
['p24-m-r','p24-a-r'].forEach(id => document.getElementById(id).addEventListener('input', draw24));
draw24();
wireDnd('p24-dnd', [
{ id:'a1', cat:'ft' },{ id:'a2', cat:'ft' },{ id:'a3', cat:'p' },