5b075cde86
Новый модуль frontend/js/phys9_finals.js:
1. РАСШИРЯЕТ window.checkNum чтобы поддерживать сигнатуру
(id, answer, unit, tol) — раньше legacy checkNum принимал только
sec для POOLS, из-за чего кнопки «Проверить» в финалах не работали.
2. ПРОГРЕСС-БАР под заголовком каждого finalN:
- Подсчитывает количество <input id="fin1-q1"...> в финале
- При правильном ответе обновляет % решённых
- +8 XP за каждую решённую задачу
3. АЧИВКИ:
- При 100% решённых задач финала — +50 XP + бэйдж
«★ МАСТЕР ГЛАВЫ» (физика9_chN_master)
- При всех 5 финалах — +150 XP + ачивка «МАГИСТР ФИЗИКИ 9»
(Wave G — финал курса)
Подключение во все 5 ch + хук на ensureBuilt вызывает
PHYS9_FINALS_INIT(id) для id вида final1..final5.
(linter добавил { delimiters, throwOnError:false } в renderMathInElement
вызовы во всех 5 widget-модулях — сохранено).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
257 lines
16 KiB
JavaScript
257 lines
16 KiB
JavaScript
// 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='✓ Идеально!'; }
|
||
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, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } 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$ угол, °: <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:'$800–999$'},
|
||
{cat:'r3', label:'$1000–1029$'},
|
||
{cat:'r4', label:'$1030–1500$'},
|
||
{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 };
|
||
|
||
})();
|