Files
Learn_System/frontend/js/phys9_ch1_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

504 lines
30 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_ch1_widgets.js — дополнительные виджеты для Физики 9, Глава 1 (§1-§14).
// Калькуляторы, DnD-сортировки, SVG-визуализации, дополняющие legacy-контент.
// Экспорт: window.PHYS9_CH1_WIDGETS = { p1: fn, p2: fn, ... }.
(function(){
'use strict';
const C = () => window.PHYS9_COLORS || {};
const PI = Math.PI;
/* === Общие хелперы === */
function svg(content, vbW, vbH){
return '<svg viewBox="0 0 '+vbW+' '+vbH+'" style="width:100%;height:auto;max-width:560px;background:var(--bg-subtle,#f8fafc);border:1px solid var(--border,#e2e8f0);border-radius:9px">'+content+'</svg>';
}
function arrow(x1, y1, x2, y2, color, w){
const dx = x2-x1, dy = y2-y1, len = Math.hypot(dx,dy);
if(len < 1e-6) return '';
const ux = dx/len, uy = dy/len, h = 10, hw = 6;
const bx = x2 - ux*h, by = y2 - uy*h;
const lx = bx - uy*hw, ly = by + ux*hw;
const rx = bx + uy*hw, ry = by - ux*hw;
return '<line x1="'+x1.toFixed(1)+'" y1="'+y1.toFixed(1)+'" x2="'+bx.toFixed(1)+'" y2="'+by.toFixed(1)+'" stroke="'+color+'" stroke-width="'+(w||2.5)+'" stroke-linecap="round"/>'
+ '<polygon points="'+x2.toFixed(1)+','+y2.toFixed(1)+' '+lx.toFixed(1)+','+ly.toFixed(1)+' '+rx.toFixed(1)+','+ry.toFixed(1)+'" fill="'+color+'"/>';
}
function dndPool(secId, items, cats){
let pool = '<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=>{
pool += '<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>';
});
pool += '</div>';
let boxes = '<div style="display:grid;grid-template-columns:repeat('+cats.length+',1fr);gap:10px">';
cats.forEach(c=>{
boxes += '<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>';
});
boxes += '</div>';
return pool + boxes;
}
function wireDnd(scopeId, items, onCheck){
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);
});
});
const checkBtn = scope.querySelector('.dnd-check');
if(checkBtn) checkBtn.addEventListener('click', ()=>{
let wrong = 0, 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++;
});
});
/* посчитать placed */
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; Идеально! Правильное распределение.'; if(onCheck) onCheck(true); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wrong+'. Перетащи неверные чипы в другие зоны.'; if(onCheck) onCheck(false); }
});
}
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; /* idempotent */
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;
}
/* ====== §1 — Точка / не точка ====== */
function add_p1(){
const items = [
{id:'i1', cat:'pt', html:'Самолёт в перелёте Москва-Сочи'},
{id:'i2', cat:'pt', html:'Земля вокруг Солнца'},
{id:'i3', cat:'pt', html:'Молекула газа'},
{id:'i4', cat:'pt', html:'Корабль в океане'},
{id:'i5', cat:'no', html:'Самолёт на посадке'},
{id:'i6', cat:'no', html:'Земля при вращении вокруг оси'},
{id:'i7', cat:'no', html:'Колесо поезда'},
{id:'i8', cat:'no', html:'Человек при беге'}
];
const body = dndPool('p1ex', items, [
{cat:'pt', label:'Можно как точку'},
{cat:'no', label:'Нельзя как точку'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div>'
+ '<div class="feedback dnd-fb"></div>';
if(appendTo('p1', wgWrapper('p1-extra', 'DnD', 'Точка или нет?', 'Подсказка: тело можно считать точкой, если его размеры много меньше, чем изучаемое расстояние.', body))){
wireDnd('p1-extra', items);
}
}
/* ====== §2 — Относительность скорости ====== */
function add_p2(){
/* Калькулятор скорости относительно берега */
const body = '<div class="sliders">'
+'<label>$v_к$ катер отн. воды, м/с: <b id="p2w-vk">5</b><input type="range" id="p2w-vk-r" min="1" max="15" step="0.5" value="5"></label>'
+'<label>$v_р$ течение, м/с: <b id="p2w-vr">2</b><input type="range" id="p2w-vr-r" min="0" max="8" step="0.5" value="2"></label>'
+'<label>Направление: <select id="p2w-dir" class="tinp" style="width:auto;padding:6px 10px"><option value="1">по течению</option><option value="-1">против течения</option></select></label>'
+'</div>'
+'<div class="score-display"><span>Скорость отн. берега: <b id="p2w-res">7</b> м/с</span></div>';
if(appendTo('p2', wgWrapper('p2-extra', 'CALC', 'Относительная скорость', 'По течению скорости складываются, против — вычитаются.', body))){
const upd = ()=>{
const vk = +document.getElementById('p2w-vk-r').value;
const vr = +document.getElementById('p2w-vr-r').value;
const dir = +document.getElementById('p2w-dir').value;
document.getElementById('p2w-vk').textContent = vk;
document.getElementById('p2w-vr').textContent = vr;
document.getElementById('p2w-res').textContent = (vk + dir*vr).toFixed(1);
};
document.getElementById('p2w-vk-r').addEventListener('input', upd);
document.getElementById('p2w-vr-r').addEventListener('input', upd);
document.getElementById('p2w-dir').addEventListener('change', upd);
upd();
}
}
/* ====== §3 — Вектор / скаляр ====== */
function add_p3(){
const items = [
{id:'i1', cat:'v', html:'скорость $\\vec v$'},
{id:'i2', cat:'v', html:'сила $\\vec F$'},
{id:'i3', cat:'v', html:'перемещение $\\Delta\\vec r$'},
{id:'i4', cat:'v', html:'ускорение $\\vec a$'},
{id:'i5', cat:'s', html:'масса $m$'},
{id:'i6', cat:'s', html:'время $t$'},
{id:'i7', cat:'s', html:'длина $L$'},
{id:'i8', cat:'s', html:'температура $T$'}
];
const body = dndPool('p3ex', items, [
{cat:'v', label:'Вектор'},
{cat:'s', label:'Скаляр'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div>'
+ '<div class="feedback dnd-fb"></div>';
if(appendTo('p3', wgWrapper('p3-extra', 'DnD', 'Вектор или скаляр?', 'Векторная величина имеет направление, скалярная — только число.', body))){
wireDnd('p3-extra', items);
}
}
/* ====== §4 — Знак проекции вектора ====== */
function add_p4(){
/* Калькулятор: дан a и угол → проекции */
const body = '<div class="sliders">'
+'<label>$|a|$: <b id="p4w-av">10</b><input type="range" id="p4w-a-r" min="1" max="20" step="1" value="10"></label>'
+'<label>$\\alpha$, &#176;: <b id="p4w-an">37</b><input type="range" id="p4w-an-r" min="0" max="360" step="5" value="37"></label>'
+'</div>'
+'<svg id="p4w-svg" viewBox="0 0 360 240" style="width:100%;height:auto;background:var(--bg-subtle,#f8fafc);border:1px solid var(--border);border-radius:9px"></svg>'
+'<div class="score-display"><span>$a_x$ = <b id="p4w-ax">8.0</b></span><span>$a_y$ = <b id="p4w-ay">6.0</b></span><span>$|a| = \\sqrt{a_x^2+a_y^2}$ = <b id="p4w-mod">10</b></span></div>';
if(appendTo('p4', wgWrapper('p4-extra', 'CALC', 'Проекции вектора', 'Меняй модуль и угол — проекции рассчитываются по $a_x = a\\cos\\alpha$, $a_y = a\\sin\\alpha$.', body))){
const cx = 180, cy = 120, R = 60;
const upd = ()=>{
const a = +document.getElementById('p4w-a-r').value;
const an = +document.getElementById('p4w-an-r').value;
document.getElementById('p4w-av').textContent = a;
document.getElementById('p4w-an').textContent = an;
const ax = a * Math.cos(an*PI/180);
const ay = -a * Math.sin(an*PI/180); /* SVG y вниз */
document.getElementById('p4w-ax').textContent = ax.toFixed(1);
document.getElementById('p4w-ay').textContent = (-ay).toFixed(1);
document.getElementById('p4w-mod').textContent = a;
let s = '';
/* оси */
const col = C();
s += '<line x1="30" y1="'+cy+'" x2="330" y2="'+cy+'" stroke="'+(col.axis||'#1e293b')+'" stroke-width="1.5"/>';
s += '<line x1="'+cx+'" y1="20" x2="'+cx+'" y2="220" stroke="'+(col.axis||'#1e293b')+'" stroke-width="1.5"/>';
s += '<text x="332" y="'+(cy+4)+'" font-size="13" font-weight="700" fill="'+(col.text||'#0f172a')+'">x</text>';
s += '<text x="'+(cx+4)+'" y="18" font-size="13" font-weight="700" fill="'+(col.text||'#0f172a')+'">y</text>';
/* окружность r=60 */
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+R+'" fill="none" stroke="'+(col.grid||'#e2e8f0')+'" stroke-dasharray="3 3"/>';
/* вектор */
const tipX = cx + R * (ax/a);
const tipY = cy + R * (ay/a);
s += arrow(cx, cy, tipX, tipY, col.acceleration||'#ea580c', 3);
/* проекции */
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+tipX+'" y2="'+cy+'" stroke="'+(col.displacement||'#2563eb')+'" stroke-width="2"/>';
s += '<line x1="'+tipX+'" y1="'+cy+'" x2="'+tipX+'" y2="'+tipY+'" stroke="'+(col.force||'#10b981')+'" stroke-width="2" stroke-dasharray="4 3"/>';
s += '<text x="'+(cx+tipX)/2+'" y="'+(cy+18)+'" text-anchor="middle" font-size="12" font-weight="700" fill="'+(col.displacement||'#2563eb')+'">$a_x$='+ax.toFixed(1)+'</text>';
s += '<text x="'+(tipX+14)+'" y="'+(cy+tipY)/2+'" font-size="12" font-weight="700" fill="'+(col.force||'#10b981')+'">$a_y$='+(-ay).toFixed(1)+'</text>';
document.getElementById('p4w-svg').innerHTML = s;
try { if(window.renderMathInElement) window.renderMathInElement(document.getElementById('p4-extra').parentNode, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){}
};
document.getElementById('p4w-a-r').addEventListener('input', upd);
document.getElementById('p4w-an-r').addEventListener('input', upd);
upd();
}
}
/* ====== §5 — Путь vs перемещение ====== */
function add_p5(){
/* Сравнение для типичных траекторий */
const items = [
{id:'i1', cat:'eq', html:'Поезд 100 км по прямой'},
{id:'i2', cat:'eq', html:'Луч света 1 м'},
{id:'i3', cat:'gt', html:'Бегун: круг по стадиону'},
{id:'i4', cat:'gt', html:'Авто туда и обратно (50 км в каждую сторону)'},
{id:'i5', cat:'gt', html:'Шарик по дуге радиуса 5 м'},
{id:'i6', cat:'gt', html:'Спутник 1 оборот по орбите'}
];
const body = dndPool('p5ex', items, [
{cat:'eq', label:'$s = |\\Delta\\vec r|$'},
{cat:'gt', label:'$s > |\\Delta\\vec r|$'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div>'
+ '<div class="feedback dnd-fb"></div>';
if(appendTo('p5', wgWrapper('p5-extra', 'DnD', 'Когда $s = |\\Delta\\vec r|$?', 'Только при прямолинейном движении в одном направлении.', body))){
wireDnd('p5-extra', items);
}
}
/* ====== §6 — Калькулятор v = s/t + перевод ед. ====== */
function add_p6(){
const body = '<div class="sliders">'
+'<label>$s$, м: <b id="p6w-s">1000</b><input type="range" id="p6w-s-r" min="10" max="10000" step="10" value="1000"></label>'
+'<label>$t$, с: <b id="p6w-t">100</b><input type="range" id="p6w-t-r" min="1" max="1000" step="1" value="100"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$v$ = $s/t$ = <b id="p6w-v">10.0</b> м/с</span>'
+'<span>= <b id="p6w-kmh">36.0</b> км/ч</span>'
+'<span style="font-size:.86rem;color:var(--muted)">Аналог: <b id="p6w-cmp">скорость автомобиля в городе</b></span>'
+'</div>';
if(appendTo('p6', wgWrapper('p6-extra', 'CALC', 'Калькулятор $v = s/t$', 'Меняй путь и время — получи скорость и сравни с привычными объектами.', body))){
const upd = ()=>{
const s = +document.getElementById('p6w-s-r').value;
const t = +document.getElementById('p6w-t-r').value;
document.getElementById('p6w-s').textContent = s;
document.getElementById('p6w-t').textContent = t;
const v = s/t;
document.getElementById('p6w-v').textContent = v.toFixed(2);
document.getElementById('p6w-kmh').textContent = (v*3.6).toFixed(1);
let cmp;
if(v < 2) cmp = 'медленный пешеход';
else if(v < 5) cmp = 'быстрый шаг';
else if(v < 15) cmp = 'велосипедист';
else if(v < 30) cmp = 'городской автомобиль';
else if(v < 100) cmp = 'автомобиль на трассе';
else if(v < 350) cmp = 'самолёт';
else cmp = 'сверхзвук';
document.getElementById('p6w-cmp').textContent = cmp;
};
document.getElementById('p6w-s-r').addEventListener('input', upd);
document.getElementById('p6w-t-r').addEventListener('input', upd);
upd();
}
}
/* ====== §7 — Средняя скорость, ловушки ====== */
function add_p7(){
/* Калькулятор: 2 участка с разной v и t. */
const body = '<div class="sliders">'
+'<label>$v_1$, м/с: <b id="p7w-v1">20</b><input type="range" id="p7w-v1-r" min="1" max="50" step="1" value="20"></label>'
+'<label>$t_1$, с: <b id="p7w-t1">60</b><input type="range" id="p7w-t1-r" min="5" max="300" step="5" value="60"></label>'
+'<label>$v_2$, м/с: <b id="p7w-v2">10</b><input type="range" id="p7w-v2-r" min="1" max="50" step="1" value="10"></label>'
+'<label>$t_2$, с: <b id="p7w-t2">120</b><input type="range" id="p7w-t2-r" min="5" max="300" step="5" value="120"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$\\langle v\\rangle = (v_1 t_1 + v_2 t_2)/(t_1+t_2)$ = <b id="p7w-vavg">13.3</b> м/с</span>'
+'<span style="font-size:.86rem;color:var(--muted)">Ловушка: $(v_1+v_2)/2$ = <b id="p7w-trap">15.0</b> м/с — <span id="p7w-trap-lbl" style="font-weight:700;color:var(--fail)">НЕВЕРНО</span></span>'
+'</div>';
if(appendTo('p7', wgWrapper('p7-extra', 'CALC', 'Средняя скорость', 'Меняй $v$ и $t$ на двух участках. Сравни средневзвешенное и арифметическое.', body))){
const upd = ()=>{
const v1 = +document.getElementById('p7w-v1-r').value;
const t1 = +document.getElementById('p7w-t1-r').value;
const v2 = +document.getElementById('p7w-v2-r').value;
const t2 = +document.getElementById('p7w-t2-r').value;
document.getElementById('p7w-v1').textContent = v1;
document.getElementById('p7w-t1').textContent = t1;
document.getElementById('p7w-v2').textContent = v2;
document.getElementById('p7w-t2').textContent = t2;
const vavg = (v1*t1 + v2*t2)/(t1+t2);
const arith = (v1+v2)/2;
document.getElementById('p7w-vavg').textContent = vavg.toFixed(2);
document.getElementById('p7w-trap').textContent = arith.toFixed(2);
document.getElementById('p7w-trap-lbl').textContent = Math.abs(vavg-arith) < 0.01 ? 'СОВПАЛО (t₁=t₂)' : 'НЕВЕРНО';
document.getElementById('p7w-trap-lbl').style.color = Math.abs(vavg-arith) < 0.01 ? 'var(--ok,#10b981)' : 'var(--fail,#dc2626)';
};
['p7w-v1-r','p7w-t1-r','p7w-v2-r','p7w-t2-r'].forEach(id => document.getElementById(id).addEventListener('input', upd));
upd();
}
}
/* ====== §8 — Графики при равном. движении (4 ситуации) ====== */
function add_p8(){
const items = [
{id:'i1', cat:'stay', html:'$x = 5$ м (горизонталь)'},
{id:'i2', cat:'go+', html:'$x = 2 + 3t$'},
{id:'i3', cat:'go-', html:'$x = 20 - 5t$'},
{id:'i4', cat:'go+', html:'$x = 4t$ (стартовала из 0)'},
{id:'i5', cat:'stay', html:'$x = -3$ (стоит слева)'},
{id:'i6', cat:'go-', html:'$x = -2t + 10$'}
];
const body = dndPool('p8ex', items, [
{cat:'stay', label:'Стоит на месте'},
{cat:'go+', label:'Движется в + направл.'},
{cat:'go-', label:'Движется в − направл.'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div>'
+ '<div class="feedback dnd-fb"></div>';
if(appendTo('p8', wgWrapper('p8-extra', 'DnD', 'Что делает тело?', 'По уравнению $x(t) = x_0 + v_x t$ определи характер движения.', body))){
wireDnd('p8-extra', items);
}
}
/* ====== §9 — Встреча двух тел ====== */
function add_p9(){
const body = '<div class="sliders">'
+'<label>$x_{01}$, м: <b id="p9w-x1">0</b><input type="range" id="p9w-x1-r" min="-50" max="50" step="5" value="0"></label>'
+'<label>$v_1$, м/с: <b id="p9w-v1">10</b><input type="range" id="p9w-v1-r" min="-15" max="15" step="1" value="10"></label>'
+'<label>$x_{02}$, м: <b id="p9w-x2">100</b><input type="range" id="p9w-x2-r" min="-50" max="200" step="5" value="100"></label>'
+'<label>$v_2$, м/с: <b id="p9w-v2">-5</b><input type="range" id="p9w-v2-r" min="-15" max="15" step="1" value="-5"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>Время встречи: $t_{в} = (x_{02}-x_{01})/(v_1-v_2)$ = <b id="p9w-t">6.7</b> с</span>'
+'<span>Точка встречи: $x_в = x_{01} + v_1 t_в$ = <b id="p9w-x">66.7</b> м</span>'
+'</div>';
if(appendTo('p9', wgWrapper('p9-extra', 'CALC', 'Встреча двух тел', 'Меняй параметры — рассчитай время и место встречи.', body))){
const upd = ()=>{
const x1 = +document.getElementById('p9w-x1-r').value;
const v1 = +document.getElementById('p9w-v1-r').value;
const x2 = +document.getElementById('p9w-x2-r').value;
const v2 = +document.getElementById('p9w-v2-r').value;
document.getElementById('p9w-x1').textContent = x1;
document.getElementById('p9w-v1').textContent = v1;
document.getElementById('p9w-x2').textContent = x2;
document.getElementById('p9w-v2').textContent = v2;
const dv = v1 - v2;
if(Math.abs(dv) < 1e-6){
document.getElementById('p9w-t').textContent = 'нет встречи';
document.getElementById('p9w-x').textContent = '—';
} else {
const t = (x2 - x1)/dv;
const x = x1 + v1 * t;
document.getElementById('p9w-t').textContent = t.toFixed(2);
document.getElementById('p9w-x').textContent = x.toFixed(2);
}
};
['p9w-x1-r','p9w-v1-r','p9w-x2-r','p9w-v2-r'].forEach(id => document.getElementById(id).addEventListener('input', upd));
upd();
}
}
/* ====== §10 — Мгновенная скорость, направление ====== */
function add_p10(){
const items = [
{id:'i1', cat:'fwd', html:'$x$ растёт со временем'},
{id:'i2', cat:'fwd', html:'график идёт круто вверх'},
{id:'i3', cat:'back',html:'$x$ убывает'},
{id:'i4', cat:'back',html:'график идёт вниз'},
{id:'i5', cat:'zero',html:'горизонтальная касательная'},
{id:'i6', cat:'zero',html:'максимум или минимум на графике $x(t)$'}
];
const body = dndPool('p10ex', items, [
{cat:'fwd', label:'$v > 0$'},
{cat:'zero',label:'$v = 0$'},
{cat:'back',label:'$v < 0$'}
]) + '<div class="actions"><button class="btn primary dnd-check">Проверить</button></div>'
+ '<div class="feedback dnd-fb"></div>';
if(appendTo('p10', wgWrapper('p10-extra', 'DnD', 'Направление $v$', 'Мгновенная скорость = тангенс угла наклона касательной к $x(t)$.', body))){
wireDnd('p10-extra', items);
}
}
/* ====== §11 — Ускорение или торможение ====== */
function add_p11(){
const body = '<div class="sliders">'
+'<label>$v_0$, м/с: <b id="p11w-v0">5</b><input type="range" id="p11w-v0-r" min="-20" max="20" step="1" value="5"></label>'
+'<label>$a$, м/с²: <b id="p11w-a">2</b><input type="range" id="p11w-a-r" min="-10" max="10" step="0.5" value="2"></label>'
+'<label>$t$, с: <b id="p11w-t">3</b><input type="range" id="p11w-t-r" min="0" max="20" step="0.5" value="3"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$v = v_0 + at$ = <b id="p11w-v">11</b> м/с</span>'
+'<span>Режим: <b id="p11w-mode" style="color:var(--ok)">УСКОРЕНИЕ</b></span>'
+'</div>';
if(appendTo('p11', wgWrapper('p11-extra', 'CALC', 'Ускорение или торможение?', 'Если $\\vec a$ и $\\vec v$ сонаправлены — ускорение. Если противоположны — торможение.', body))){
const upd = ()=>{
const v0 = +document.getElementById('p11w-v0-r').value;
const a = +document.getElementById('p11w-a-r').value;
const t = +document.getElementById('p11w-t-r').value;
document.getElementById('p11w-v0').textContent = v0;
document.getElementById('p11w-a').textContent = a;
document.getElementById('p11w-t').textContent = t;
const v = v0 + a*t;
document.getElementById('p11w-v').textContent = v.toFixed(2);
const mode = document.getElementById('p11w-mode');
if(Math.abs(a) < 0.05){ mode.textContent = 'РАВНОМЕРНОЕ'; mode.style.color = 'var(--muted)'; }
else if(v0*a > 0 || (v0 === 0 && Math.abs(a) > 0.05)){ mode.textContent = 'УСКОРЕНИЕ'; mode.style.color = 'var(--ok,#10b981)'; }
else if(Math.sign(v) !== Math.sign(v0) && v0 !== 0){ mode.textContent = 'ТОРМОЖЕНИЕ → РАЗГОН В ОБРАТНУЮ'; mode.style.color = 'var(--warn,#f59e0b)'; }
else { mode.textContent = 'ТОРМОЖЕНИЕ'; mode.style.color = 'var(--fail,#dc2626)'; }
};
['p11w-v0-r','p11w-a-r','p11w-t-r'].forEach(id => document.getElementById(id).addEventListener('input', upd));
upd();
}
}
/* ====== §12 — Момент остановки при торможении ====== */
function add_p12(){
const body = '<div class="sliders">'
+'<label>$v_0$, м/с: <b id="p12w-v0">20</b><input type="range" id="p12w-v0-r" min="5" max="40" step="1" value="20"></label>'
+'<label>$a$, м/с² (тормоз): <b id="p12w-a">-5</b><input type="range" id="p12w-a-r" min="-10" max="-0.5" step="0.5" value="-5"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>Время до остановки $t_{ост} = -v_0/a$ = <b id="p12w-t">4.0</b> с</span>'
+'<span>Тормозной путь $s_{ост} = v_0^2 / (2|a|)$ = <b id="p12w-s">40</b> м</span>'
+'</div>';
if(appendTo('p12', wgWrapper('p12-extra', 'CALC', 'Тормозной путь автомобиля', 'Через какое время остановится и какой пройдёт путь?', body))){
const upd = ()=>{
const v0 = +document.getElementById('p12w-v0-r').value;
const a = +document.getElementById('p12w-a-r').value;
document.getElementById('p12w-v0').textContent = v0;
document.getElementById('p12w-a').textContent = a;
const t = -v0/a;
const s = v0*v0/(2*Math.abs(a));
document.getElementById('p12w-t').textContent = t.toFixed(2);
document.getElementById('p12w-s').textContent = s.toFixed(2);
};
document.getElementById('p12w-v0-r').addEventListener('input', upd);
document.getElementById('p12w-a-r').addEventListener('input', upd);
upd();
}
}
/* ====== §13 — Перемещение при равноуск. движении ====== */
function add_p13(){
const body = '<div class="sliders">'
+'<label>$v_0$, м/с: <b id="p13w-v0">0</b><input type="range" id="p13w-v0-r" min="0" max="30" step="1" value="0"></label>'
+'<label>$a$, м/с²: <b id="p13w-a">2</b><input type="range" id="p13w-a-r" min="0.5" max="10" step="0.5" value="2"></label>'
+'<label>$t$, с: <b id="p13w-t">5</b><input type="range" id="p13w-t-r" min="1" max="20" step="0.5" value="5"></label>'
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$\\Delta x = v_0 t + \\frac{at^2}{2}$ = <b id="p13w-dx">25</b> м</span>'
+'<span>$v = v_0 + at$ = <b id="p13w-v">10</b> м/с</span>'
+'<span>Проверка: $v^2 - v_0^2 = 2a\\Delta x$ → <b id="p13w-chk">100 = 100</b> ✓</span>'
+'</div>';
if(appendTo('p13', wgWrapper('p13-extra', 'CALC', 'Калькулятор $\\Delta x$ при $a = $ const', 'Меняй параметры. Обе формулы сверены автоматически.', body))){
const upd = ()=>{
const v0 = +document.getElementById('p13w-v0-r').value;
const a = +document.getElementById('p13w-a-r').value;
const t = +document.getElementById('p13w-t-r').value;
document.getElementById('p13w-v0').textContent = v0;
document.getElementById('p13w-a').textContent = a;
document.getElementById('p13w-t').textContent = t;
const dx = v0*t + a*t*t/2;
const v = v0 + a*t;
const lhs = v*v - v0*v0;
const rhs = 2*a*dx;
document.getElementById('p13w-dx').textContent = dx.toFixed(2);
document.getElementById('p13w-v').textContent = v.toFixed(2);
document.getElementById('p13w-chk').textContent = lhs.toFixed(1)+' = '+rhs.toFixed(1);
};
['p13w-v0-r','p13w-a-r','p13w-t-r'].forEach(id => document.getElementById(id).addEventListener('input', upd));
upd();
}
}
/* ====== §14 — Знак ускорения по графику $v(t)$ ====== */
function add_p14(){
const items = [
{id:'i1', cat:'pos', html:'$v(t)$ растёт линейно из $v_0 > 0$'},
{id:'i2', cat:'pos', html:'$v(t)$ растёт из 0 (старт)'},
{id:'i3', cat:'neg', html:'$v(t)$ убывает (тормозим)'},
{id:'i4', cat:'neg', html:'$v(t)$ становится отрицательной'},
{id:'i5', cat:'zero',html:'$v(t)$ — горизонтальная линия'},
{id:'i6', cat:'zero',html:'постоянная скорость 30 м/с'}
];
const body = dndPool('p14ex', items, [
{cat:'pos', label:'$a > 0$'},
{cat:'zero',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('p14', wgWrapper('p14-extra', 'DnD', 'Знак ускорения по $v(t)$', 'Угол наклона графика $v(t)$ = ускорение. Растёт — $a > 0$, убывает — $a < 0$, горизонталь — $a = 0$.', body))){
wireDnd('p14-extra', items);
}
}
window.PHYS9_CH1_WIDGETS = {
p1: add_p1, p2: add_p2, p3: add_p3, p4: add_p4, p5: add_p5, p6: add_p6, p7: add_p7,
p8: add_p8, p9: add_p9, p10: add_p10, p11: add_p11, p12: add_p12, p13: add_p13, p14: add_p14
};
})();