Files
Learn_System/frontend/js/phys9_ch4_widgets.js
Maxim Dolgolyov 5b075cde86 feat(phys9 finals): прогресс-бары и ачивки финалов Wave F + G
Новый модуль 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>
2026-05-30 09:55:44 +03:00

259 lines
16 KiB
JavaScript
Raw Permalink 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_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='&#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, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } 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 };
})();