diff --git a/frontend/js/flagships/phys9_flag_F18_master.js b/frontend/js/flagships/phys9_flag_F18_master.js new file mode 100644 index 0000000..2204573 --- /dev/null +++ b/frontend/js/flagships/phys9_flag_F18_master.js @@ -0,0 +1,271 @@ +// F18. Магистр-симулятор (final5 в ch5) — конструктор сценария движения тела. +(function(){ +'use strict'; +const B = () => window.PHYS9_FLAG_BASE; +const C = () => window.PHYS9_COLORS || {}; + +const STAGE_TYPES = { + uniform: { name: 'Равномерное', color: '#0891b2' }, + accel: { name: 'Равноуск.', color: '#ea580c' }, + fall: { name: 'Своб. падение', color: '#2563eb' }, + stop: { name: 'Стоп / покой', color: '#475569' } +}; + +function init(secId){ + if (!B()) return false; + let typeBtns = ''; + for (const t in STAGE_TYPES){ + const s = STAGE_TYPES[t]; + typeBtns += ''; + } + const body = '' + + '
Собери цепочку этапов движения как LEGO. Программа рассчитает $x(t)$, $v(t)$, $a(t)$.
' + + '
' + + typeBtns + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '
Этапов0
' + + '
Общая длительность0 с
' + + '
Итог $x$0 м
' + + '
Итог $v$0 м/с
' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + + const card = B().makeCard(secId, + 'F18. Магистр-симулятор КОНСТРУКТОР', + 'Финальный конструктор сценариев. Каждый этап — кусок движения. Программа считает $x(t)$, $v(t)$, $a(t)$ и проверяет согласованность.', + body); + if (!card) return false; + + const cv = document.getElementById('F18-cv'); + const ctx = cv.getContext('2d'); + const W = cv.width, H = cv.height; + + let stages = []; /* {type, duration, param} */ + + function makeStageCard(idx){ + const s = stages[idx]; + const type = STAGE_TYPES[s.type]; + const paramLabel = { + uniform: 'v, м/с', + accel: 'a, м/с²', + fall: '— (g)', + stop: '—' + }[s.type]; + const div = document.createElement('div'); + div.style.cssText = 'background:var(--card);border:2px solid '+type.color+';border-radius:9px;padding:8px 10px;display:flex;gap:8px;align-items:center;font-size:.85rem'; + div.innerHTML = '' + + '' + (idx+1) + '. ' + type.name + '' + + '' + + (paramLabel === '—' || paramLabel === '— (g)' ? ''+paramLabel+'' : + '') + + ''; + return div; + } + + function refreshStages(){ + const host = document.getElementById('F18-stages'); + host.innerHTML = ''; + if (stages.length === 0){ + host.innerHTML = 'Добавь этапы кнопками сверху…'; + } else { + stages.forEach((s, i) => host.appendChild(makeStageCard(i))); + } + host.querySelectorAll('input').forEach(inp => { + inp.addEventListener('input', e => { + const i = +e.target.dataset.i, f = e.target.dataset.f; + const val = +e.target.value; + if (!isNaN(val)) { stages[i][f] = val; recalculate(); } + }); + }); + host.querySelectorAll('[data-rm]').forEach(btn => { + btn.addEventListener('click', e => { + const i = +e.target.dataset.rm; + stages.splice(i, 1); + refreshStages(); + recalculate(); + }); + }); + try { if(window.renderMathInElement) window.renderMathInElement(host, { delimiters:[{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} + recalculate(); + } + + function simulate(){ + /* Возвращает массив точек {t, x, v, a} */ + const pts = []; + let t = 0, x = 0, v = 0, a = 0; + pts.push({ t, x, v, a: 0 }); + for (const s of stages){ + const dt_step = 0.05; + const end = t + s.duration; + while (t < end - 1e-6){ + const step = Math.min(dt_step, end - t); + if (s.type === 'uniform') { a = 0; v = s.param; } + else if (s.type === 'accel') { a = s.param; } + else if (s.type === 'fall') { a = -9.8; } + else if (s.type === 'stop') { a = 0; v = 0; } + v += a * step; + x += v * step; + t += step; + pts.push({ t, x, v, a }); + } + } + return pts; + } + + function recalculate(){ + const pts = simulate(); + const last = pts[pts.length-1]; + document.getElementById('F18-n').textContent = stages.length; + document.getElementById('F18-tt').textContent = last.t.toFixed(1) + ' с'; + document.getElementById('F18-xt').textContent = last.x.toFixed(2) + ' м'; + document.getElementById('F18-vt').textContent = last.v.toFixed(2) + ' м/с'; + /* проверки */ + const fb = document.getElementById('F18-fb'); + if (stages.length === 0){ + fb.className = 'flag-feedback'; + } else { + /* для разрывов: проверяем согласованность v между этапами */ + let warning = ''; + for (let i = 0; i < stages.length - 1; i++){ + const a = stages[i], b = stages[i+1]; + /* в реальности v сохраняется при переходе. Если b.type=uniform с фикс. v, может быть разрыв */ + if (b.type === 'uniform'){ + /* считаем v на конце a */ + let v_end = 0; + if (a.type === 'uniform') v_end = a.param; + else if (a.type === 'stop') v_end = 0; + /* для accel и fall нужно знать v на начало a — пропускаем для упрощения */ + if (Math.abs(v_end - b.param) > 0.1){ + warning = '⚠ После этапа '+(i+1)+' скорость '+v_end.toFixed(1)+' м/с, но '+(i+2)+'-й этап задаёт '+b.param.toFixed(1)+' м/с — это рывок.'; + break; + } + } + } + if (warning){ + fb.className = 'flag-feedback warn show'; + fb.innerHTML = warning; + } else { + fb.className = 'flag-feedback ok show'; + fb.innerHTML = '✓ Сценарий согласован. Этапов: '+stages.length+', итог: x = '+last.x.toFixed(1)+' м, v = '+last.v.toFixed(1)+' м/с.'; + } + } + draw(pts); + } + + function draw(pts){ + const col = C(); + ctx.fillStyle = col.bg || '#fafafa'; + ctx.fillRect(0, 0, W, H); + if (pts.length < 2){ + ctx.fillStyle = col.textMuted || '#64748b'; + ctx.font = '15px Inter,sans-serif'; + ctx.fillText('Добавь этапы движения — увидь графики x(t), v(t), a(t)', 90, H/2); + return; + } + /* 3 графика горизонтально */ + const tMax = pts[pts.length-1].t || 1; + let xMin = 0, xMax = 0, vMin = 0, vMax = 0, aMin = 0, aMax = 0; + for (const p of pts){ + xMin = Math.min(xMin, p.x); xMax = Math.max(xMax, p.x); + vMin = Math.min(vMin, p.v); vMax = Math.max(vMax, p.v); + aMin = Math.min(aMin, p.a); aMax = Math.max(aMax, p.a); + } + if (xMax - xMin < 0.5) xMax = xMin + 1; + if (vMax - vMin < 0.5) vMax = vMin + 1; + if (aMax - aMin < 0.5) aMax = aMin + 1; + function plot(yOff, hOff, vals, vMin0, vMax0, color, label){ + ctx.fillStyle = col.bgSubtle || '#f8fafc'; + ctx.fillRect(40, yOff, W - 60, hOff); + ctx.strokeStyle = col.axis || '#1e293b'; + ctx.lineWidth = 1.5; + ctx.strokeRect(40, yOff, W - 60, hOff); + /* zero-line */ + const zeroY = yOff + hOff - (0 - vMin0)/(vMax0 - vMin0) * hOff; + ctx.strokeStyle = col.grid || '#e5e7eb'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(40, zeroY); ctx.lineTo(W - 20, zeroY); + ctx.stroke(); + /* линия */ + ctx.strokeStyle = color; + ctx.lineWidth = 2.5; + ctx.beginPath(); + for (let i = 0; i < pts.length; i++){ + const t = pts[i].t; + const v = vals[i]; + const px = 40 + (t / tMax) * (W - 60); + const py = yOff + hOff - (v - vMin0)/(vMax0 - vMin0) * hOff; + if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); + } + ctx.stroke(); + /* label */ + ctx.fillStyle = color; + ctx.font = 'bold 13px Inter,sans-serif'; + ctx.fillText(label, 8, yOff + 16); + ctx.fillStyle = col.textMuted || '#64748b'; + ctx.font = '10px Inter,sans-serif'; + ctx.fillText(vMax0.toFixed(1), 8, yOff + 12); + ctx.fillText(vMin0.toFixed(1), 8, yOff + hOff - 2); + } + const blockH = 130, gap = 10; + plot(10, blockH, pts.map(p => p.x), xMin, xMax, col.displacement || '#2563eb', 'x(t), м'); + plot(10 + blockH + gap, blockH, pts.map(p => p.v), vMin, vMax, col.velocity || '#0891b2', 'v(t), м/с'); + plot(10 + 2*(blockH + gap), blockH, pts.map(p => p.a), aMin, aMax, col.acceleration || '#ea580c', 'a(t), м/с²'); + /* x-axis label */ + ctx.fillStyle = col.text || '#0f172a'; + ctx.font = '11px Inter,sans-serif'; + ctx.fillText('t = '+tMax.toFixed(1)+' с', W - 70, H - 4); + } + + card.querySelectorAll('[data-add]').forEach(btn => { + btn.addEventListener('click', () => { + const type = btn.dataset.add; + const def = { uniform: 5, accel: 1, fall: 0, stop: 0 }[type]; + stages.push({ type, duration: 3, param: def }); + refreshStages(); + }); + }); + document.getElementById('F18-clear').addEventListener('click', () => { stages = []; refreshStages(); }); + document.getElementById('F18-preset').addEventListener('click', () => { + stages = [ + { type:'accel', duration:5, param: 2 }, + { type:'uniform', duration:8, param:10 }, + { type:'accel', duration:4, param:-2.5 } + ]; + refreshStages(); + }); + document.getElementById('F18-save').addEventListener('click', () => { + try { + localStorage.setItem('phys9_F18_scenario', JSON.stringify(stages)); + const fb = document.getElementById('F18-fb'); + fb.className = 'flag-feedback ok show'; + fb.innerHTML = '✓ Сценарий сохранён в браузере.'; + } catch(e){} + }); + document.getElementById('F18-load').addEventListener('click', () => { + try { + const s = JSON.parse(localStorage.getItem('phys9_F18_scenario') || 'null'); + if (Array.isArray(s)) { stages = s; refreshStages(); } + } catch(e){} + }); + + refreshStages(); + draw([{t:0,x:0,v:0,a:0}]); + return true; +} + +if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F18', { init: init, cleanup: function(){} }); +else document.addEventListener('DOMContentLoaded', ()=>{ + if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F18', { init: init, cleanup: function(){} }); +}); + +})(); diff --git a/frontend/textbooks/physics_9_ch5.html b/frontend/textbooks/physics_9_ch5.html index 5d1bea5..15dfc8f 100644 --- a/frontend/textbooks/physics_9_ch5.html +++ b/frontend/textbooks/physics_9_ch5.html @@ -9,6 +9,7 @@ + @@ -797,7 +798,7 @@ function _injectTasks(id){ var body = document.getElementById(id + '-body'); if(!body || body.querySelector('.legacy-tasks')) return; body.insertAdjacentHTML('beforeend', _makeTaskBlock(id)); - setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH5_WIDGETS && window.PHYS9_CH5_WIDGETS[id]) window.PHYS9_CH5_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60); + setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH5_WIDGETS && window.PHYS9_CH5_WIDGETS[id]) window.PHYS9_CH5_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='final5') window.PHYS9_FLAG_BASE.mount('F18','final5'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60); } var _origEnsureBuilt = ensureBuilt; ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };