// 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(){} }); }); })();