d2ce0d70b2
Новый модуль frontend/js/phys9_ch4_widgets.js — экспортирует
window.PHYS9_CH4_WIDGETS = { p31..p36: fn }.
Виджеты:
- §31 CALC: импульс p=mv с slider'ами m, v и бытовой аналогией
(футбольный мяч / автомобиль / грузовик и т.д.)
- §32 CALC: ЗСИ — упругий и неупругий удар двух тел. m₁, v₁, m₂, v₂ →
v₁', v₂', проверка сохранения импульса
- §33 DnD: 9 ситуаций → знак работы (A>0/A<0/A=0)
- §34 CALC: Ek+Ep с slider'ами m, v, h
- §35 CALC: ЗСМЭ — найти v в любой точке горки по высоте старта
(h_старт=5, h_тек=0 → v=9.9 м/с)
- §36 CALC: период маятника (математ./пружинный) — переключатель,
формула обновляется автоматически
Подключено в physics_9_ch4.html через тот же hook ensureBuilt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
259 lines
16 KiB
JavaScript
259 lines
16 KiB
JavaScript
// phys9_ch4_widgets.js — виджеты для Физики 9, Глава 4 (§31-§36): импульс, энергия, колебания.
|
||
(function(){
|
||
'use strict';
|
||
const C = () => window.PHYS9_COLORS || {};
|
||
const PI = Math.PI;
|
||
|
||
function dndPool(secId, items, cats){
|
||
let p='<div class="dnd-pool" id="'+secId+'-pool" style="display:flex;flex-wrap:wrap;gap:6px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;margin-bottom:10px">';
|
||
items.forEach(it=>{ p += '<div class="dnd-chip" draggable="true" data-id="'+it.id+'" data-cat="'+it.cat+'" style="padding:6px 11px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);cursor:grab;font-size:.92rem">'+it.html+'</div>'; });
|
||
p += '</div><div style="display:grid;grid-template-columns:repeat('+cats.length+',1fr);gap:10px">';
|
||
cats.forEach(c=>{ p += '<div class="drop-box" data-cat="'+c.cat+'" style="border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:80px"><h5 style="font-size:.78rem;font-weight:800;margin-bottom:6px;color:var(--sec-acc-d,var(--pri-d))">'+c.label+'</h5><div class="drop-items"></div></div>'; });
|
||
return p + '</div>';
|
||
}
|
||
function wireDnd(scopeId, items){
|
||
const scope = document.querySelector('#'+scopeId); if(!scope) return;
|
||
scope.querySelectorAll('.dnd-chip').forEach(chip=>{
|
||
chip.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', chip.dataset.id); chip.style.opacity='0.5'; });
|
||
chip.addEventListener('dragend', e=>{ chip.style.opacity='1'; });
|
||
});
|
||
scope.querySelectorAll('.drop-box').forEach(box=>{
|
||
box.addEventListener('dragover', e=>{ e.preventDefault(); box.style.borderColor=C().force||'#10b981'; });
|
||
box.addEventListener('dragleave', e=>{ box.style.borderColor=''; });
|
||
box.addEventListener('drop', e=>{
|
||
e.preventDefault(); box.style.borderColor='';
|
||
const id = e.dataTransfer.getData('text/plain');
|
||
const chip = scope.querySelector('.dnd-chip[data-id="'+id+'"]');
|
||
if(chip) box.querySelector('.drop-items').appendChild(chip);
|
||
});
|
||
});
|
||
scope.querySelector('.dnd-check').addEventListener('click', ()=>{
|
||
let wrong=0; const total = items.length;
|
||
scope.querySelectorAll('.drop-box').forEach(box=>{
|
||
const cat = box.dataset.cat;
|
||
box.querySelectorAll('.dnd-chip').forEach(chip=>{ if(chip.dataset.cat !== cat) wrong++; });
|
||
});
|
||
let placed=0; scope.querySelectorAll('.drop-box .dnd-chip').forEach(()=>placed++);
|
||
const fb = scope.querySelector('.dnd-fb');
|
||
if(placed < total){ fb.className='feedback fail'; fb.innerHTML='Распредели все — осталось '+(total-placed)+'.'; return; }
|
||
if(wrong===0){ fb.className='feedback ok'; fb.innerHTML='✓ Идеально!'; }
|
||
else { fb.className='feedback fail'; fb.innerHTML='✗ Ошибок: '+wrong+'.'; }
|
||
});
|
||
}
|
||
function wgWrapper(secId, badge, title, help, body){
|
||
return '<div class="wg" id="'+secId+'"><div class="wg-header"><span class="wg-badge">'+badge+'</span><div class="wg-title">'+title+'</div></div><div class="wg-help">'+help+'</div>'+body+'</div>';
|
||
}
|
||
function appendTo(secId, html){
|
||
const box = document.getElementById(secId+'-body');
|
||
if(!box) return false;
|
||
if(box.querySelector('.wg-phys9-extra-'+secId)) return false;
|
||
const div = document.createElement('div'); div.className = 'wg-phys9-extra-'+secId; div.innerHTML = html;
|
||
box.appendChild(div);
|
||
try { if(window.renderMathInElement) window.renderMathInElement(box); } catch(e){}
|
||
return true;
|
||
}
|
||
|
||
/* ====== §31 — Импульс p = mv ====== */
|
||
function add_p31(){
|
||
const body = '<div class="sliders">'
|
||
+'<label>$m$, кг: <b id="p31w-m">2</b><input type="range" id="p31w-m-r" min="0.5" max="100" step="0.5" value="2"></label>'
|
||
+'<label>$v$, м/с: <b id="p31w-v">10</b><input type="range" id="p31w-v-r" min="-30" max="30" step="0.5" value="10"></label>'
|
||
+'</div>'
|
||
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
|
||
+'<span>$p = mv$ = <b id="p31w-p">20</b> кг·м/с</span>'
|
||
+'<span style="font-size:.85rem;color:var(--muted)">Аналог: <b id="p31w-cmp">футбольный мяч после удара</b></span>'
|
||
+'</div>';
|
||
if(appendTo('p31', wgWrapper('p31-extra', 'CALC', 'Импульс тела', '$\\vec p = m \\vec v$ — векторная величина. Направление = направление скорости.', body))){
|
||
const upd = ()=>{
|
||
const m = +document.getElementById('p31w-m-r').value;
|
||
const v = +document.getElementById('p31w-v-r').value;
|
||
document.getElementById('p31w-m').textContent = m;
|
||
document.getElementById('p31w-v').textContent = v;
|
||
const p = m*v;
|
||
document.getElementById('p31w-p').textContent = p.toFixed(1);
|
||
let cmp = '';
|
||
const ap = Math.abs(p);
|
||
if(ap < 0.5) cmp = 'медленный пешеход с ребёнком';
|
||
else if(ap < 5) cmp = 'идущий человек';
|
||
else if(ap < 30) cmp = 'футбольный мяч';
|
||
else if(ap < 300) cmp = 'велосипедист';
|
||
else if(ap < 5000) cmp = 'автомобиль';
|
||
else cmp = 'грузовик / поезд';
|
||
document.getElementById('p31w-cmp').textContent = cmp;
|
||
};
|
||
document.getElementById('p31w-m-r').addEventListener('input', upd);
|
||
document.getElementById('p31w-v-r').addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
}
|
||
|
||
/* ====== §32 — ЗСИ: упругий и неупругий удар ====== */
|
||
function add_p32(){
|
||
const body = '<div class="sliders">'
|
||
+'<label>$m_1$, кг: <b id="p32w-m1">2</b><input type="range" id="p32w-m1-r" min="0.5" max="10" step="0.5" value="2"></label>'
|
||
+'<label>$v_1$, м/с: <b id="p32w-v1">5</b><input type="range" id="p32w-v1-r" min="-10" max="10" step="0.5" value="5"></label>'
|
||
+'<label>$m_2$, кг: <b id="p32w-m2">3</b><input type="range" id="p32w-m2-r" min="0.5" max="10" step="0.5" value="3"></label>'
|
||
+'<label>$v_2$, м/с: <b id="p32w-v2">-2</b><input type="range" id="p32w-v2-r" min="-10" max="10" step="0.5" value="-2"></label>'
|
||
+'<label>Тип: <select id="p32w-type" class="tinp" style="width:auto;padding:6px 10px"><option value="abs">неупругий (слиплись)</option><option value="elast">упругий</option></select></label>'
|
||
+'</div>'
|
||
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
|
||
+'<span>$p_{до} = m_1 v_1 + m_2 v_2$ = <b id="p32w-pdo">4</b> кг·м/с</span>'
|
||
+'<span>$v_1\'$ = <b id="p32w-v1p">0.8</b> м/с, $v_2\'$ = <b id="p32w-v2p">0.8</b> м/с</span>'
|
||
+'<span>$p_{после}$ = <b id="p32w-ppos">4</b> кг·м/с (сохраняется ✓)</span>'
|
||
+'</div>';
|
||
if(appendTo('p32', wgWrapper('p32-extra', 'CALC', 'Закон сохранения импульса', 'В замкнутой системе $\\Sigma p =$ const. При неупругом ударе тела движутся одной скоростью.', body))){
|
||
const upd = ()=>{
|
||
const m1 = +document.getElementById('p32w-m1-r').value;
|
||
const v1 = +document.getElementById('p32w-v1-r').value;
|
||
const m2 = +document.getElementById('p32w-m2-r').value;
|
||
const v2 = +document.getElementById('p32w-v2-r').value;
|
||
const tp = document.getElementById('p32w-type').value;
|
||
document.getElementById('p32w-m1').textContent = m1;
|
||
document.getElementById('p32w-v1').textContent = v1;
|
||
document.getElementById('p32w-m2').textContent = m2;
|
||
document.getElementById('p32w-v2').textContent = v2;
|
||
const pdo = m1*v1 + m2*v2;
|
||
let v1p, v2p;
|
||
if(tp === 'abs'){ v1p = pdo/(m1+m2); v2p = v1p; }
|
||
else { v1p = ((m1-m2)*v1 + 2*m2*v2)/(m1+m2); v2p = ((m2-m1)*v2 + 2*m1*v1)/(m1+m2); }
|
||
document.getElementById('p32w-pdo').textContent = pdo.toFixed(2);
|
||
document.getElementById('p32w-v1p').textContent = v1p.toFixed(2);
|
||
document.getElementById('p32w-v2p').textContent = v2p.toFixed(2);
|
||
document.getElementById('p32w-ppos').textContent = (m1*v1p + m2*v2p).toFixed(2);
|
||
};
|
||
['p32w-m1-r','p32w-v1-r','p32w-m2-r','p32w-v2-r','p32w-type'].forEach(id=>document.getElementById(id).addEventListener('input', upd));
|
||
document.getElementById('p32w-type').addEventListener('change', upd);
|
||
upd();
|
||
}
|
||
}
|
||
|
||
/* ====== §33 — Работа: знак ====== */
|
||
function add_p33(){
|
||
const items = [
|
||
{id:'i1', cat:'pos', html:'тянем сани горизонтально по снегу'},
|
||
{id:'i2', cat:'pos', html:'поднимаем груз вверх (рукой)'},
|
||
{id:'i3', cat:'pos', html:'двигатель толкает машину'},
|
||
{id:'i4', cat:'neg', html:'сила трения тормозит брусок'},
|
||
{id:'i5', cat:'neg', html:'сила тяжести при подъёме вверх'},
|
||
{id:'i6', cat:'neg', html:'тормоза автомобиля'},
|
||
{id:'i7', cat:'zer', html:'несём чемодан по горизонтали (вес ⊥ путь)'},
|
||
{id:'i8', cat:'zer', html:'тело неподвижно (s=0)'},
|
||
{id:'i9', cat:'zer', html:'сила перпендикулярна скорости (по окружности)'}
|
||
];
|
||
const body = dndPool('p33ex', items, [
|
||
{cat:'pos', label:'$A > 0$'},
|
||
{cat:'zer', label:'$A = 0$'},
|
||
{cat:'neg', label:'$A < 0$'}
|
||
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div><div class="feedback dnd-fb"></div>';
|
||
if(appendTo('p33', wgWrapper('p33-extra', 'DnD', 'Знак работы $A = Fs\\cos\\alpha$', 'Сила вдоль движения → $A > 0$; против → $A < 0$; перпендикулярно → $A = 0$.', body))){
|
||
wireDnd('p33-extra', items);
|
||
}
|
||
}
|
||
|
||
/* ====== §34 — Энергия Ek, Ep ====== */
|
||
function add_p34(){
|
||
const body = '<div class="sliders">'
|
||
+'<label>$m$, кг: <b id="p34w-m">2</b><input type="range" id="p34w-m-r" min="0.5" max="20" step="0.5" value="2"></label>'
|
||
+'<label>$v$, м/с: <b id="p34w-v">10</b><input type="range" id="p34w-v-r" min="0" max="30" step="0.5" value="10"></label>'
|
||
+'<label>$h$, м: <b id="p34w-h">5</b><input type="range" id="p34w-h-r" min="0" max="50" step="0.5" value="5"></label>'
|
||
+'</div>'
|
||
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
|
||
+'<span>$E_k = mv^2/2$ = <b id="p34w-Ek">100</b> Дж</span>'
|
||
+'<span>$E_p = mgh$ = <b id="p34w-Ep">98</b> Дж</span>'
|
||
+'<span>$E = E_k + E_p$ = <b id="p34w-E">198</b> Дж</span>'
|
||
+'</div>';
|
||
if(appendTo('p34', wgWrapper('p34-extra', 'CALC', 'Кинетическая и потенциальная энергия', 'Ek зависит от $v$, Ep — от $h$. Их сумма — полная механическая энергия.', body))){
|
||
const upd = ()=>{
|
||
const m = +document.getElementById('p34w-m-r').value;
|
||
const v = +document.getElementById('p34w-v-r').value;
|
||
const h = +document.getElementById('p34w-h-r').value;
|
||
document.getElementById('p34w-m').textContent = m;
|
||
document.getElementById('p34w-v').textContent = v;
|
||
document.getElementById('p34w-h').textContent = h.toFixed(1);
|
||
const Ek = m*v*v/2;
|
||
const Ep = m*9.8*h;
|
||
document.getElementById('p34w-Ek').textContent = Ek.toFixed(0);
|
||
document.getElementById('p34w-Ep').textContent = Ep.toFixed(0);
|
||
document.getElementById('p34w-E').textContent = (Ek+Ep).toFixed(0);
|
||
};
|
||
['p34w-m-r','p34w-v-r','p34w-h-r'].forEach(id=>document.getElementById(id).addEventListener('input', upd));
|
||
upd();
|
||
}
|
||
}
|
||
|
||
/* ====== §35 — ЗСМЭ: v внизу горки ====== */
|
||
function add_p35(){
|
||
const body = '<div class="sliders">'
|
||
+'<label>$m$, кг: <b id="p35w-m">1</b><input type="range" id="p35w-m-r" min="0.5" max="10" step="0.5" value="1"></label>'
|
||
+'<label>$h_{старт}$, м: <b id="p35w-h0">5</b><input type="range" id="p35w-h0-r" min="0.5" max="50" step="0.5" value="5"></label>'
|
||
+'<label>$h_{в точке}$, м: <b id="p35w-h">0</b><input type="range" id="p35w-h-r" min="0" max="50" step="0.5" value="0"></label>'
|
||
+'</div>'
|
||
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
|
||
+'<span>$E_0 = mgh_0$ = <b id="p35w-E0">49</b> Дж</span>'
|
||
+'<span>$E_{p}^{тек} = mgh$ = <b id="p35w-Ep">0</b> Дж</span>'
|
||
+'<span>$E_k = E_0 - E_p$ = <b id="p35w-Ek">49</b> Дж</span>'
|
||
+'<span>$v = \\sqrt{2E_k/m}$ = <b id="p35w-v">9.9</b> м/с</span>'
|
||
+'</div>';
|
||
if(appendTo('p35', wgWrapper('p35-extra', 'CALC', 'Закон сохранения энергии', 'В отсутствие трения: $E_p^{старт} = E_p + E_k$. Найди $v$ в любой точке.', body))){
|
||
const upd = ()=>{
|
||
const m = +document.getElementById('p35w-m-r').value;
|
||
const h0 = +document.getElementById('p35w-h0-r').value;
|
||
const h = +document.getElementById('p35w-h-r').value;
|
||
document.getElementById('p35w-m').textContent = m;
|
||
document.getElementById('p35w-h0').textContent = h0.toFixed(1);
|
||
document.getElementById('p35w-h').textContent = h.toFixed(1);
|
||
const E0 = m*9.8*h0;
|
||
const Ep = m*9.8*Math.min(h, h0);
|
||
const Ek = Math.max(0, E0 - Ep);
|
||
const v = Math.sqrt(2*Ek/m);
|
||
document.getElementById('p35w-E0').textContent = E0.toFixed(1);
|
||
document.getElementById('p35w-Ep').textContent = Ep.toFixed(1);
|
||
document.getElementById('p35w-Ek').textContent = Ek.toFixed(1);
|
||
document.getElementById('p35w-v').textContent = v.toFixed(2);
|
||
};
|
||
['p35w-m-r','p35w-h0-r','p35w-h-r'].forEach(id=>document.getElementById(id).addEventListener('input', upd));
|
||
upd();
|
||
}
|
||
}
|
||
|
||
/* ====== §36 — Период маятника ====== */
|
||
function add_p36(){
|
||
const body = '<div class="sliders">'
|
||
+'<label>Тип: <select id="p36w-t" class="tinp" style="width:auto;padding:6px 10px"><option value="math">математический</option><option value="spr">пружинный</option></select></label>'
|
||
+'<label>Длина $l$ (мат.) / $m$ (пруж.): <b id="p36w-x">1</b><input type="range" id="p36w-x-r" min="0.1" max="5" step="0.1" value="1"></label>'
|
||
+'<label>$g$ (мат.) / $k$ (пруж.): <b id="p36w-y">9.8</b><input type="range" id="p36w-y-r" min="1" max="100" step="1" value="10"></label>'
|
||
+'</div>'
|
||
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
|
||
+'<span>Формула: <b id="p36w-formula">$T = 2\\pi\\sqrt{l/g}$</b></span>'
|
||
+'<span>$T$ = <b id="p36w-T">2.01</b> с</span>'
|
||
+'<span>$\\nu = 1/T$ = <b id="p36w-nu">0.50</b> Гц</span>'
|
||
+'</div>';
|
||
if(appendTo('p36', wgWrapper('p36-extra', 'CALC', 'Период колебаний', 'Математический: $T = 2\\pi\\sqrt{l/g}$. Пружинный: $T = 2\\pi\\sqrt{m/k}$.', body))){
|
||
const upd = ()=>{
|
||
const tp = document.getElementById('p36w-t').value;
|
||
const x = +document.getElementById('p36w-x-r').value;
|
||
const y = +document.getElementById('p36w-y-r').value;
|
||
document.getElementById('p36w-x').textContent = x.toFixed(2);
|
||
document.getElementById('p36w-y').textContent = y.toFixed(1);
|
||
let T;
|
||
if(tp === 'math'){
|
||
T = 2*PI*Math.sqrt(x/y);
|
||
document.getElementById('p36w-formula').textContent = 'T = 2π√(l/g)';
|
||
} else {
|
||
T = 2*PI*Math.sqrt(x/y);
|
||
document.getElementById('p36w-formula').textContent = 'T = 2π√(m/k)';
|
||
}
|
||
document.getElementById('p36w-T').textContent = T.toFixed(2);
|
||
document.getElementById('p36w-nu').textContent = (1/T).toFixed(2);
|
||
};
|
||
document.getElementById('p36w-t').addEventListener('change', upd);
|
||
document.getElementById('p36w-x-r').addEventListener('input', upd);
|
||
document.getElementById('p36w-y-r').addEventListener('input', upd);
|
||
upd();
|
||
}
|
||
}
|
||
|
||
window.PHYS9_CH4_WIDGETS = { p31:add_p31, p32:add_p32, p33:add_p33, p34:add_p34, p35:add_p35, p36:add_p36 };
|
||
|
||
})();
|