// F2. Гонка двух тел (§9) — симуляция + real-time графики x₁(t), x₂(t). (function(){ 'use strict'; const B = () => window.PHYS9_FLAG_BASE; const C = () => window.PHYS9_COLORS || {}; function init(secId){ if (!B()) return false; const body = '' + '
' + '' + '' + '' + '' + '' + '
' + '' + '
' + '' + '' + '' + '
' + '
' + '
Время0 с
' + '
$x_1$ ($v_1$)0 м (10 м/с)
' + '
$x_2$ ($v_2$)80 м (−3 м/с)
' + '
Встреча
' + '
' + '
'; const card = B().makeCard(secId, 'F2. Гонка двух тел', 'Меняй параметры до старта. Слева — реальная гонка, справа — графики $x(t)$. Пересечение графиков = встреча. Кнопка «Случайный сценарий» — для тренировки.', body); if (!card) return false; const cv = document.getElementById('F2-cv'); const ctx = cv.getContext('2d'); /* layout: левая половина — гонка, правая — графики */ const W = cv.width, H = cv.height; const RACE_W = Math.floor(W * 0.5); const GRAPH_X = RACE_W + 10; const GRAPH_W = W - GRAPH_X - 10; /* state */ let st = { x1: 0, x2: 80, v1: 10, v2: -3, a1: 0, a2: 0, t: 0, running: false, met: false, tMet: -1, xMet: -1, history: [] }; function readSliders(){ st.v1 = +document.getElementById('F2-v1').value; st.v2 = +document.getElementById('F2-v2').value; st.a1 = +document.getElementById('F2-a1').value; st.a2 = +document.getElementById('F2-a2').value; st.x2 = +document.getElementById('F2-x2').value; document.getElementById('F2-v1v').textContent = st.v1; document.getElementById('F2-a1v').textContent = st.a1; document.getElementById('F2-x2v').textContent = st.x2; document.getElementById('F2-v2v').textContent = st.v2; document.getElementById('F2-a2v').textContent = st.a2; /* теоретическая точка встречи (при текущих параметрах) */ /* x1(t) = v1*t + 0.5*a1*t² , x2(t) = x2_0 + v2*t + 0.5*a2*t² */ /* Δ = 0.5*(a1-a2)*t² + (v1-v2)*t - x2_0 = 0 */ const A = 0.5*(st.a1 - st.a2); const Bcoeff = st.v1 - st.v2; const Ccoeff = -st.x2; let tMeet = -1; if (Math.abs(A) < 1e-6){ if (Math.abs(Bcoeff) > 1e-6) tMeet = -Ccoeff / Bcoeff; } else { const D = Bcoeff*Bcoeff - 4*A*Ccoeff; if (D >= 0){ const t1 = (-Bcoeff + Math.sqrt(D)) / (2*A); const t2 = (-Bcoeff - Math.sqrt(D)) / (2*A); const tCand = [t1, t2].filter(x => x > 0.01).sort((a,b)=>a-b); if (tCand.length) tMeet = tCand[0]; } } document.getElementById('F2-meet').textContent = (tMeet > 0 && tMeet < 60) ? tMeet.toFixed(2)+' с' : '—'; } function reset(){ st.t = 0; st.x1 = 0; st.met = false; st.tMet = -1; st.xMet = -1; st.history = []; readSliders(); st.running = false; document.getElementById('F2-go').textContent = 'Старт'; document.getElementById('F2-fb').className = 'flag-feedback'; } function tick(dt){ if (!st.running) { draw(); return; } /* Эйлер по dt */ const N = 4; const ddt = dt/N; for (let i = 0; i < N; i++){ st.v1 += st.a1 * ddt; st.v2 += st.a2 * ddt; st.x1 += st.v1 * ddt; st.x2 += st.v2 * ddt; st.t += ddt; if (!st.met && Math.abs(st.x1 - st.x2) < 0.6){ st.met = true; st.tMet = st.t; st.xMet = st.x1; st.running = false; document.getElementById('F2-go').textContent = 'Старт'; const fb = document.getElementById('F2-fb'); fb.className = 'flag-feedback ok show'; fb.innerHTML = '✓ Встреча! $t = $ '+st.tMet.toFixed(2)+' с, $x = $ '+st.xMet.toFixed(1)+' м.'; break; } st.history.push({ t: st.t, x1: st.x1, x2: st.x2 }); if (st.history.length > 600) st.history.shift(); } if (st.t > 60) { st.running = false; document.getElementById('F2-go').textContent='Старт'; } draw(); } function draw(){ const col = C(); ctx.fillStyle = col.bg || '#fafafa'; ctx.fillRect(0, 0, W, H); /* === ЛЕВАЯ ПОЛОВИНА: гонка === */ /* трасса */ const trackY = 80; const trackH = 100; ctx.fillStyle = col.surface || '#a16207'; ctx.fillRect(0, trackY, RACE_W, trackH); /* разметка */ ctx.strokeStyle = '#fff'; ctx.setLineDash([12, 8]); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, trackY + trackH/2); ctx.lineTo(RACE_W, trackY + trackH/2); ctx.stroke(); ctx.setLineDash([]); /* шкала на трассе: 0..200 м → 0..RACE_W */ const x2px = x => (x / 200) * RACE_W; /* отметки 0, 50, 100, 150, 200 */ ctx.fillStyle = col.text || '#0f172a'; ctx.font = '11px Inter,sans-serif'; for (let k = 0; k <= 200; k += 50){ const px = x2px(k); ctx.fillRect(px-1, trackY + trackH, 2, 8); ctx.fillText(k+' м', px - 12, trackY + trackH + 22); } /* машины */ const car1x = Math.max(8, Math.min(RACE_W - 32, x2px(st.x1))); const car2x = Math.max(8, Math.min(RACE_W - 32, x2px(st.x2))); /* car 1 — голубая */ ctx.fillStyle = col.velocity || '#0891b2'; ctx.fillRect(car1x - 12, trackY + 18, 24, 14); ctx.fillRect(car1x - 8, trackY + 13, 16, 7); ctx.fillStyle = '#fff'; ctx.font = 'bold 11px Inter,sans-serif'; ctx.fillText('1', car1x - 3, trackY + 28); /* car 2 — красная */ ctx.fillStyle = col.fail || '#dc2626'; ctx.fillRect(car2x - 12, trackY + 68, 24, 14); ctx.fillRect(car2x - 8, trackY + 63, 16, 7); ctx.fillStyle = '#fff'; ctx.fillText('2', car2x - 3, trackY + 78); /* подпись времени */ ctx.fillStyle = col.text || '#0f172a'; ctx.font = 'bold 14px Inter,sans-serif'; ctx.fillText('t = ' + st.t.toFixed(1) + ' с', 12, 30); ctx.font = '12px Inter,sans-serif'; /* === ПРАВАЯ ПОЛОВИНА: графики x(t) === */ const gx = GRAPH_X, gy = 30, gw = GRAPH_W, gh = H - 70; /* фон */ ctx.fillStyle = col.bgSubtle || '#f8fafc'; ctx.fillRect(gx, gy, gw, gh); /* оси */ ctx.strokeStyle = col.axis || '#1e293b'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(gx + 30, gy); ctx.lineTo(gx + 30, gy + gh); ctx.lineTo(gx + gw - 5, gy + gh); ctx.stroke(); /* шкала: x от 0..210 м, t от 0..max(20, текущ.) */ const tMax = Math.max(20, st.t + 2); const xMax = 220; const toGx = t => gx + 30 + (t / tMax) * (gw - 40); const toGy = xx => gy + gh - (xx / xMax) * (gh - 10); /* сетка */ ctx.strokeStyle = col.grid || '#e5e7eb'; ctx.lineWidth = 1; for (let k = 0; k <= tMax; k += Math.max(2, Math.floor(tMax/8))){ const px = toGx(k); ctx.beginPath(); ctx.moveTo(px, gy); ctx.lineTo(px, gy+gh); ctx.stroke(); } for (let k = 0; k <= xMax; k += 50){ const py = toGy(k); ctx.beginPath(); ctx.moveTo(gx+30, py); ctx.lineTo(gx+gw-5, py); ctx.stroke(); } /* подписи осей */ ctx.fillStyle = col.textMuted || '#64748b'; ctx.font = '10px Inter,sans-serif'; ctx.fillText('t, с', gx + gw - 22, gy + gh - 4); ctx.fillText('x, м', gx + 4, gy + 10); for (let k = 0; k <= tMax; k += Math.max(5, Math.floor(tMax/5))){ const px = toGx(k); ctx.fillText(k.toFixed(0), px - 6, gy + gh + 14); } for (let k = 0; k <= xMax; k += 50){ const py = toGy(k); ctx.fillText(k, gx + 6, py + 3); } /* линия x1(t) — голубая */ if (st.history.length > 1){ ctx.strokeStyle = col.velocity || '#0891b2'; ctx.lineWidth = 2.5; ctx.beginPath(); for (let i = 0; i < st.history.length; i++){ const p = st.history[i]; const px = toGx(p.t), py = toGy(Math.max(0, Math.min(xMax, p.x1))); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); /* линия x2(t) — красная */ ctx.strokeStyle = col.fail || '#dc2626'; ctx.beginPath(); for (let i = 0; i < st.history.length; i++){ const p = st.history[i]; const px = toGx(p.t), py = toGy(Math.max(0, Math.min(xMax, p.x2))); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); } /* момент встречи */ if (st.tMet > 0){ const px = toGx(st.tMet), py = toGy(st.xMet); ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(px, py, 7, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#0f172a'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.fillStyle = col.text || '#0f172a'; ctx.font = 'bold 11px Inter,sans-serif'; ctx.fillText('★ встреча', px + 10, py - 6); } /* легенда */ ctx.font = '11px Inter,sans-serif'; ctx.fillStyle = col.velocity || '#0891b2'; ctx.fillRect(gx + 36, gy + 4, 12, 4); ctx.fillStyle = col.text || '#0f172a'; ctx.fillText('тело 1', gx + 52, gy + 10); ctx.fillStyle = col.fail || '#dc2626'; ctx.fillRect(gx + 90, gy + 4, 12, 4); ctx.fillStyle = col.text || '#0f172a'; ctx.fillText('тело 2', gx + 106, gy + 10); /* обновить статы текстом */ document.getElementById('F2-t').textContent = st.t.toFixed(1) + ' с'; document.getElementById('F2-x1').textContent = st.x1.toFixed(1) + ' м ('+st.v1.toFixed(1)+' м/с)'; document.getElementById('F2-x2disp').textContent = st.x2.toFixed(1) + ' м ('+st.v2.toFixed(1)+' м/с)'; } document.getElementById('F2-go').addEventListener('click', ()=>{ if (st.met) reset(); if (st.t === 0) readSliders(); st.running = !st.running; document.getElementById('F2-go').textContent = st.running ? 'Пауза' : 'Старт'; }); document.getElementById('F2-reset').addEventListener('click', reset); document.getElementById('F2-random').addEventListener('click', ()=>{ document.getElementById('F2-v1').value = Math.round(5 + Math.random()*15); document.getElementById('F2-a1').value = (Math.round((Math.random()-0.3)*8))/2; document.getElementById('F2-x2').value = 60 + Math.round(Math.random()*120); document.getElementById('F2-v2').value = Math.round(-8 + Math.random()*10); document.getElementById('F2-a2').value = (Math.round((Math.random()-0.5)*6))/2; reset(); }); ['F2-v1','F2-a1','F2-x2','F2-v2','F2-a2'].forEach(id => document.getElementById(id).addEventListener('input', ()=>{ if (!st.running) reset(); else readSliders(); }) ); readSliders(); draw(); B().startLoop('F2', cv, tick); return true; } if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F2', { init: init, cleanup: function(){} }); else document.addEventListener('DOMContentLoaded', ()=>{ if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F2', { init: init, cleanup: function(){} }); }); })();