Files
Learn_System/frontend/js/phys9_ch3_widgets.js
T
Maxim Dolgolyov 29ae219025 feat(phys9 ch3): добавлены 6 виджетов Wave C — Глава 3 «Статика»
Новый модуль frontend/js/phys9_ch3_widgets.js — экспортирует
window.PHYS9_CH3_WIDGETS = { p25..p30: fn }.

Виджеты:
- §25 CALC+VIS: равновесие рычага — 4 slider'а (m₁, l₁, m₂, l₂),
  балка наклоняется при дисбалансе, статус (равновесие/перевешивание)
- §26 DnD: 8 механизмов → 3 категории (выигрыш в силе / расстоянии /
  без выигрыша)
- §27 CALC: КПД наклонной плоскости — m, h, угол α, μ → A_пол, A_зат, η
- §28 DnD: 8 ситуаций → виды равновесия (устойчивое/неустойчивое/безразл.)
- §29 CALC: F_A=ρgV для разных жидкостей (вода/керосин/ртуть/спирт),
  сравнение с весом, статус (плавает/тонет/висит в толще)
- §30 DnD: 5 жидкостей → группы плотности

Подключено в physics_9_ch3.html через тот же hook ensureBuilt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:49:05 +03:00

257 lines
16 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.
// phys9_ch3_widgets.js — виджеты для Физики 9, Глава 3 (§25-§30): статика и гидростатика.
(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='&#10003; Идеально!'; }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+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;
}
/* ====== §25 — Равновесие рычага F₁ l₁ = F₂ l₂ ====== */
function add_p25(){
const body = '<div class="sliders">'
+'<label>$m_1$, кг: <b id="p25w-m1">2</b><input type="range" id="p25w-m1-r" min="0.5" max="20" step="0.5" value="2"></label>'
+'<label>$l_1$, м: <b id="p25w-l1">1.5</b><input type="range" id="p25w-l1-r" min="0.1" max="3" step="0.1" value="1.5"></label>'
+'<label>$m_2$, кг: <b id="p25w-m2">3</b><input type="range" id="p25w-m2-r" min="0.5" max="20" step="0.5" value="3"></label>'
+'<label>$l_2$, м: <b id="p25w-l2">1.0</b><input type="range" id="p25w-l2-r" min="0.1" max="3" step="0.1" value="1.0"></label>'
+'</div>'
+'<svg id="p25w-svg" viewBox="0 0 460 200" style="width:100%;height:auto;background:var(--bg-subtle,#f8fafc);border:1px solid var(--border);border-radius:9px"></svg>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$M_1 = m_1 g l_1$ = <b id="p25w-M1">29.4</b> Н·м</span>'
+'<span>$M_2 = m_2 g l_2$ = <b id="p25w-M2">29.4</b> Н·м</span>'
+'<span><b id="p25w-eq" style="color:var(--ok)">РАВНОВЕСИЕ ✓</b></span>'
+'</div>';
if(appendTo('p25', wgWrapper('p25-extra', 'CALC+VIS', 'Равновесие рычага', '$F_1 l_1 = F_2 l_2$ — моменты сил равны.', body))){
const upd = ()=>{
const m1 = +document.getElementById('p25w-m1-r').value;
const l1 = +document.getElementById('p25w-l1-r').value;
const m2 = +document.getElementById('p25w-m2-r').value;
const l2 = +document.getElementById('p25w-l2-r').value;
document.getElementById('p25w-m1').textContent = m1;
document.getElementById('p25w-l1').textContent = l1.toFixed(1);
document.getElementById('p25w-m2').textContent = m2;
document.getElementById('p25w-l2').textContent = l2.toFixed(1);
const M1 = m1*9.8*l1, M2 = m2*9.8*l2;
document.getElementById('p25w-M1').textContent = M1.toFixed(1);
document.getElementById('p25w-M2').textContent = M2.toFixed(1);
const eq = document.getElementById('p25w-eq');
if(Math.abs(M1-M2) < 0.5){ eq.innerHTML = 'РАВНОВЕСИЕ ✓'; eq.style.color = 'var(--ok,#10b981)'; }
else if(M1 > M2){ eq.innerHTML = 'ЛЕВАЯ ПЕРЕВЕШИВАЕТ ⤵'; eq.style.color = 'var(--fail,#dc2626)'; }
else { eq.innerHTML = 'ПРАВАЯ ПЕРЕВЕШИВАЕТ ⤵'; eq.style.color = 'var(--fail,#dc2626)'; }
/* SVG */
const col = C();
const tilt = Math.max(-15, Math.min(15, (M2-M1)*0.3));
const cx = 230, cy = 100;
const len = 180;
const rad = tilt*PI/180;
const lx = cx - len*Math.cos(rad), ly = cy + len*Math.sin(rad);
const rx = cx + len*Math.cos(rad), ry = cy - len*Math.sin(rad);
let s = '';
/* опора */
s += '<polygon points="'+cx+',100 '+(cx-20)+',180 '+(cx+20)+',180" fill="'+(col.body||'#475569')+'" stroke="'+(col.axis||'#1e293b')+'" stroke-width="1.5"/>';
/* балка */
s += '<line x1="'+lx.toFixed(1)+'" y1="'+ly.toFixed(1)+'" x2="'+rx.toFixed(1)+'" y2="'+ry.toFixed(1)+'" stroke="'+(col.bodyAccent||'#1e293b')+'" stroke-width="6" stroke-linecap="round"/>';
/* грузы */
const r1 = Math.min(20, 5 + m1*2);
const r2 = Math.min(20, 5 + m2*2);
s += '<circle cx="'+lx.toFixed(1)+'" cy="'+(ly+r1+3).toFixed(1)+'" r="'+r1+'" fill="'+(col.forceGravity||'#2563eb')+'" stroke="'+(col.axis||'#1e293b')+'" stroke-width="1.4"/>';
s += '<circle cx="'+rx.toFixed(1)+'" cy="'+(ry+r2+3).toFixed(1)+'" r="'+r2+'" fill="'+(col.forceGravity||'#2563eb')+'" stroke="'+(col.axis||'#1e293b')+'" stroke-width="1.4"/>';
/* подписи */
s += '<text x="'+lx.toFixed(1)+'" y="'+(ly-r1-6).toFixed(1)+'" text-anchor="middle" font-size="12" font-weight="700" fill="'+(col.text||'#0f172a')+'">'+m1+' кг</text>';
s += '<text x="'+rx.toFixed(1)+'" y="'+(ry-r2-6).toFixed(1)+'" text-anchor="middle" font-size="12" font-weight="700" fill="'+(col.text||'#0f172a')+'">'+m2+' кг</text>';
document.getElementById('p25w-svg').innerHTML = s;
};
['p25w-m1-r','p25w-l1-r','p25w-m2-r','p25w-l2-r'].forEach(id=>document.getElementById(id).addEventListener('input', upd));
upd();
}
}
/* ====== §26 — Простые механизмы ====== */
function add_p26(){
const items = [
{id:'i1', cat:'force', html:'рычаг с длинным плечом'},
{id:'i2', cat:'force', html:'наклонная плоскость (длинная)'},
{id:'i3', cat:'force', html:'неподвижный блок + полиспаст'},
{id:'i4', cat:'force', html:'клин'},
{id:'i5', cat:'dist', html:'рычаг с коротким плечом (метла)'},
{id:'i6', cat:'dist', html:'педаль велосипеда'},
{id:'i7', cat:'none', html:'неподвижный блок (один)'},
{id:'i8', cat:'none', html:'жёсткий стержень'}
];
const body = dndPool('p26ex', items, [
{cat:'force', label:'Выигрыш в силе'},
{cat:'dist', label:'Выигрыш в скор./пути'},
{cat:'none', label:'Без выигрыша'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div><div class="feedback dnd-fb"></div>';
if(appendTo('p26', wgWrapper('p26-extra', 'DnD', 'Что даёт выигрыш?', 'Золотое правило: выигрываем в силе — проигрываем в расстоянии. И наоборот.', body))){
wireDnd('p26-extra', items);
}
}
/* ====== §27 — КПД наклонной плоскости ====== */
function add_p27(){
const body = '<div class="sliders">'
+'<label>$m$ груза, кг: <b id="p27w-m">10</b><input type="range" id="p27w-m-r" min="1" max="50" step="1" value="10"></label>'
+'<label>$h$ высота, м: <b id="p27w-h">1</b><input type="range" id="p27w-h-r" min="0.2" max="3" step="0.1" value="1"></label>'
+'<label>$\\alpha$ угол, &#176;: <b id="p27w-an">30</b><input type="range" id="p27w-an-r" min="10" max="60" step="1" value="30"></label>'
+'<label>$\\mu$ трение: <b id="p27w-mu">0.2</b><input type="range" id="p27w-mu-r" min="0" max="0.6" step="0.05" value="0.2"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$A_{пол} = m g h$ = <b id="p27w-Apol">98</b> Дж</span>'
+'<span>$A_{зат} = F \\cdot l$ = <b id="p27w-Azat">131</b> Дж</span>'
+'<span>$\\eta$ = <b id="p27w-eta">75</b>%</span>'
+'</div>';
if(appendTo('p27', wgWrapper('p27-extra', 'CALC', 'КПД наклонной плоскости', 'Полезная работа = $mgh$. Затраченная = $F \\cdot l$, где $F$ учитывает трение.', body))){
const upd = ()=>{
const m = +document.getElementById('p27w-m-r').value;
const h = +document.getElementById('p27w-h-r').value;
const a = +document.getElementById('p27w-an-r').value;
const mu = +document.getElementById('p27w-mu-r').value;
document.getElementById('p27w-m').textContent = m;
document.getElementById('p27w-h').textContent = h.toFixed(1);
document.getElementById('p27w-an').textContent = a;
document.getElementById('p27w-mu').textContent = mu.toFixed(2);
const g = 9.8;
const l = h / Math.sin(a*PI/180);
const F = m*g*(Math.sin(a*PI/180) + mu*Math.cos(a*PI/180));
const Apol = m*g*h;
const Azat = F*l;
const eta = (Apol/Azat)*100;
document.getElementById('p27w-Apol').textContent = Apol.toFixed(0);
document.getElementById('p27w-Azat').textContent = Azat.toFixed(0);
document.getElementById('p27w-eta').textContent = eta.toFixed(0);
};
['p27w-m-r','p27w-h-r','p27w-an-r','p27w-mu-r'].forEach(id=>document.getElementById(id).addEventListener('input', upd));
upd();
}
}
/* ====== §28 — Виды равновесия ====== */
function add_p28(){
const items = [
{id:'i1', cat:'st', html:'шар в углублении'},
{id:'i2', cat:'st', html:'маятник в нижней точке'},
{id:'i3', cat:'st', html:'столб с широким основанием'},
{id:'i4', cat:'un', html:'шар на вершине горы'},
{id:'i5', cat:'un', html:'карандаш на остром конце'},
{id:'i6', cat:'un', html:'пирамида на вершине'},
{id:'i7', cat:'in', html:'шар на горизонтальном столе'},
{id:'i8', cat:'in', html:'цилиндр на ровной поверхности'}
];
const body = dndPool('p28ex', items, [
{cat:'st', label:'Устойчивое'},
{cat:'un', label:'Неустойчивое'},
{cat:'in', label:'Безразличное'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div><div class="feedback dnd-fb"></div>';
if(appendTo('p28', wgWrapper('p28-extra', 'DnD', 'Вид равновесия', 'Устойчивое — возврат, неустойчивое — уход, безразличное — без изменений.', body))){
wireDnd('p28-extra', items);
}
}
/* ====== §29 — Закон Архимеда ====== */
function add_p29(){
const body = '<div class="sliders">'
+'<label>$V$ тела, см³: <b id="p29w-V">100</b><input type="range" id="p29w-V-r" min="10" max="1000" step="10" value="100"></label>'
+'<label>Жидкость: <select id="p29w-liq" class="tinp" style="width:auto;padding:6px 10px"><option value="1000">вода (1000)</option><option value="800">керосин (800)</option><option value="13600">ртуть (13600)</option><option value="789">спирт (789)</option></select></label>'
+'<label>$\\rho_{тела}$, кг/м³: <b id="p29w-rt">500</b><input type="range" id="p29w-rt-r" min="100" max="15000" step="100" value="500"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$F_A = \\rho g V$ = <b id="p29w-Fa">0.98</b> Н</span>'
+'<span>Вес тела $P$ = <b id="p29w-P">0.49</b> Н</span>'
+'<span><b id="p29w-result" style="color:var(--ok)">ПЛАВАЕТ ↑</b></span>'
+'</div>';
if(appendTo('p29', wgWrapper('p29-extra', 'CALC', 'Закон Архимеда', '$F_A = \\rho g V$. Сравни с весом тела.', body))){
const upd = ()=>{
const V_cm3 = +document.getElementById('p29w-V-r').value;
const V = V_cm3 * 1e-6;
const rho_l = +document.getElementById('p29w-liq').value;
const rho_t = +document.getElementById('p29w-rt-r').value;
document.getElementById('p29w-V').textContent = V_cm3;
document.getElementById('p29w-rt').textContent = rho_t;
const g = 9.8;
const Fa = rho_l*g*V;
const P = rho_t*g*V;
document.getElementById('p29w-Fa').textContent = Fa.toFixed(2);
document.getElementById('p29w-P').textContent = P.toFixed(2);
const r = document.getElementById('p29w-result');
if(Math.abs(Fa-P) < 0.01){ r.innerHTML = 'ВИСИТ В ТОЛЩЕ →'; r.style.color = 'var(--muted)'; }
else if(Fa > P){ r.innerHTML = 'ПЛАВАЕТ ↑'; r.style.color = 'var(--ok,#10b981)'; }
else { r.innerHTML = 'ТОНЕТ ↓'; r.style.color = 'var(--fail,#dc2626)'; }
};
['p29w-V-r','p29w-liq','p29w-rt-r'].forEach(id=>document.getElementById(id).addEventListener('input', upd));
document.getElementById('p29w-liq').addEventListener('change', upd);
upd();
}
}
/* ====== §30 — Плотности жидкостей по возрастанию ====== */
function add_p30(){
const items = [
{id:'i1', cat:'r1', html:'спирт ($789$)'},
{id:'i2', cat:'r2', html:'керосин ($800$)'},
{id:'i3', cat:'r3', html:'вода ($1000$)'},
{id:'i4', cat:'r4', html:'морская вода ($1030$)'},
{id:'i5', cat:'r5', html:'ртуть ($13600$)'}
];
const body = dndPool('p30ex', items, [
{cat:'r1', label:'$<800$'},
{cat:'r2', label:'$800999$'},
{cat:'r3', label:'$10001029$'},
{cat:'r4', label:'$10301500$'},
{cat:'r5', label:'$>10000$'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div><div class="feedback dnd-fb"></div>';
if(appendTo('p30', wgWrapper('p30-extra', 'DnD', 'Плотности жидкостей (кг/м³)', 'Расставь жидкости по группам плотности.', body))){
wireDnd('p30-extra', items);
}
}
window.PHYS9_CH3_WIDGETS = { p25:add_p25, p26:add_p26, p27:add_p27, p28:add_p28, p29:add_p29, p30:add_p30 };
})();