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>
504 lines
30 KiB
JavaScript
504 lines
30 KiB
JavaScript
// 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='✓ Идеально! Правильное распределение.'; if(onCheck) onCheck(true); }
|
||
else { fb.className='feedback fail'; fb.innerHTML='✗ Ошибок: '+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$, °: <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
|
||
};
|
||
|
||
})();
|