diff --git a/frontend/js/phys9_ch1_widgets.js b/frontend/js/phys9_ch1_widgets.js new file mode 100644 index 0000000..3c2dfa2 --- /dev/null +++ b/frontend/js/phys9_ch1_widgets.js @@ -0,0 +1,503 @@ +// 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 ''+content+''; +} + +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 '' + + ''; +} + +function dndPool(secId, items, cats){ + let pool = '
'; + items.forEach(it=>{ + pool += '
'+it.html+'
'; + }); + pool += '
'; + let boxes = '
'; + cats.forEach(c=>{ + boxes += '
'+c.label+'
'; + }); + boxes += '
'; + 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 '
' + +'
'+badge+'
'+title+'
' + +'
'+help+'
' + + body + +'
'; +} + +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); } 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:'Нельзя как точку'} + ]) + '
' + + ''; + if(appendTo('p1', wgWrapper('p1-extra', 'DnD', 'Точка или нет?', 'Подсказка: тело можно считать точкой, если его размеры много меньше, чем изучаемое расстояние.', body))){ + wireDnd('p1-extra', items); + } +} + +/* ====== §2 — Относительность скорости ====== */ +function add_p2(){ + /* Калькулятор скорости относительно берега */ + const body = '
' + +'' + +'' + +'' + +'
' + +'
Скорость отн. берега: 7 м/с
'; + 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:'Скаляр'} + ]) + '
' + + ''; + if(appendTo('p3', wgWrapper('p3-extra', 'DnD', 'Вектор или скаляр?', 'Векторная величина имеет направление, скалярная — только число.', body))){ + wireDnd('p3-extra', items); + } +} + +/* ====== §4 — Знак проекции вектора ====== */ +function add_p4(){ + /* Калькулятор: дан a и угол → проекции */ + const body = '
' + +'' + +'' + +'
' + +'' + +'
$a_x$ = 8.0$a_y$ = 6.0$|a| = \\sqrt{a_x^2+a_y^2}$ = 10
'; + 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 += ''; + s += ''; + s += 'x'; + s += 'y'; + /* окружность r=60 */ + s += ''; + /* вектор */ + const tipX = cx + R * (ax/a); + const tipY = cy + R * (ay/a); + s += arrow(cx, cy, tipX, tipY, col.acceleration||'#ea580c', 3); + /* проекции */ + s += ''; + s += ''; + s += '$a_x$='+ax.toFixed(1)+''; + s += '$a_y$='+(-ay).toFixed(1)+''; + document.getElementById('p4w-svg').innerHTML = s; + try { if(window.renderMathInElement) window.renderMathInElement(document.getElementById('p4-extra').parentNode); } 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|$'} + ]) + '
' + + ''; + 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 = '
' + +'' + +'' + +'
' + +'
' + +'$v$ = $s/t$ = 10.0 м/с' + +'= 36.0 км/ч' + +'Аналог: скорость автомобиля в городе' + +'
'; + 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 = '
' + +'' + +'' + +'' + +'' + +'
' + +'
' + +'$\\langle v\\rangle = (v_1 t_1 + v_2 t_2)/(t_1+t_2)$ = 13.3 м/с' + +'Ловушка: ($v_1+v_2)/2$ = 15.0 м/с — НЕВЕРНО' + +'
'; + 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:'Движется в − направл.'} + ]) + '
' + + ''; + 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 = '
' + +'' + +'' + +'' + +'' + +'
' + +'
' + +'Время встречи: $t_{в} = (x_{02}-x_{01})/(v_1-v_2)$ = 6.7 с' + +'Точка встречи: $x_в = x_{01} + v_1 t_в$ = 66.7 м' + +'
'; + 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$'} + ]) + '
' + + ''; + if(appendTo('p10', wgWrapper('p10-extra', 'DnD', 'Направление $v$', 'Мгновенная скорость = тангенс угла наклона касательной к $x(t)$.', body))){ + wireDnd('p10-extra', items); + } +} + +/* ====== §11 — Ускорение или торможение ====== */ +function add_p11(){ + const body = '
' + +'' + +'' + +'' + +'
' + +'
' + +'$v = v_0 + at$ = 11 м/с' + +'Режим: УСКОРЕНИЕ' + +'
'; + 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 = '
' + +'' + +'' + +'
' + +'
' + +'Время до остановки $t_{ост} = -v_0/a$ = 4.0 с' + +'Тормозной путь $s_{ост} = v_0^2 / (2|a|)$ = 40 м' + +'
'; + 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 = '
' + +'' + +'' + +'' + +'
' + +'
' + +'$\\Delta x = v_0 t + \\frac{at^2}{2}$ = 25 м' + +'$v = v_0 + at$ = 10 м/с' + +'Проверка: $v^2 - v_0^2 = 2a\\Delta x$ → 100 = 100' + +'
'; + 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$'} + ]) + '
' + + ''; + 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 +}; + +})(); diff --git a/frontend/textbooks/physics_9_ch1.html b/frontend/textbooks/physics_9_ch1.html index dfb591b..aa7c960 100644 --- a/frontend/textbooks/physics_9_ch1.html +++ b/frontend/textbooks/physics_9_ch1.html @@ -17,6 +17,7 @@ +