// F3. Тахометр + спидометр + одометр (§11-13) — игра в автогонщика. (function(){ 'use strict'; const B = () => window.PHYS9_FLAG_BASE; const C = () => window.PHYS9_COLORS || {}; function init(secId){ if (!B()) return false; const body = '' + '
' + '' + '' + '' + '
' + '' + '
' + '' + '' + '' + '' + '
' + '
' + '
Время0 с
' + '
$v$ (спидометр)0 м/с
' + '
$a$ (тахометр)0 м/с²
' + '
$\\Delta x$ (одометр)0 м
' + '
Рекорд скорости0 м/с
' + '
' + '
'; const card = B().makeCard(secId, 'F3. Тахометр + спидометр + одометр', 'Управляй автомобилем кнопками. Тахометр (a), спидометр (v) и одометр (Δx) — связаны. Разгонись до цели и затормози.', body); if (!card) return false; const cv = document.getElementById('F3-cv'); const ctx = cv.getContext('2d'); const W = cv.width, H = cv.height; let st = { t: 0, v: 0, x: 0, a: 0, mode: 'coast', history: [], targetHit: false }; function readSliders(){ document.getElementById('F3-gv').textContent = (+document.getElementById('F3-g').value).toFixed(1); document.getElementById('F3-bv').textContent = (+document.getElementById('F3-b').value).toFixed(1); document.getElementById('F3-tv').textContent = (+document.getElementById('F3-t').value).toFixed(1); } function reset(){ st = { t: 0, v: 0, x: 0, a: 0, mode: 'coast', history: [], targetHit: false }; document.getElementById('F3-fb').className = 'flag-feedback'; } function tick(dt){ const gas = +document.getElementById('F3-g').value; const brake = +document.getElementById('F3-b').value; const target = +document.getElementById('F3-t').value; if (st.mode === 'gas') st.a = gas; else if (st.mode === 'brake') st.a = st.v > 0.1 ? -brake : 0; else /* coast */ st.a = st.v > 0.05 ? -0.5 : 0; /* лёгкое торможение от трения */ st.v = Math.max(0, st.v + st.a * dt); st.x += st.v * dt; st.t += dt; st.history.push({ t: st.t, v: st.v, a: st.a }); if (st.history.length > 600) st.history.shift(); /* Цель достигнута? */ if (!st.targetHit && Math.abs(st.v - target) < 0.5){ st.targetHit = true; const fb = document.getElementById('F3-fb'); fb.className = 'flag-feedback ok show'; fb.innerHTML = '✓ Цель достигнута! Скорость '+st.v.toFixed(1)+' м/с (~'+(st.v*3.6).toFixed(0)+' км/ч). Теперь затормози.'; } /* Рекорд */ const rec = B().saveRecord('F3_max_v', st.v); document.getElementById('F3-rec').textContent = rec.toFixed(1) + ' м/с'; /* UI */ document.getElementById('F3-t-st').textContent = st.t.toFixed(1) + ' с'; document.getElementById('F3-v').textContent = st.v.toFixed(1) + ' м/с'; document.getElementById('F3-a').textContent = st.a.toFixed(1) + ' м/с²'; document.getElementById('F3-x').textContent = st.x.toFixed(1) + ' м'; draw(); } function drawGauge(cx, cy, r, val, min, max, label, unit, color){ const col = C(); /* фон */ ctx.fillStyle = col.bgSubtle || '#f8fafc'; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = col.axis || '#1e293b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.stroke(); /* шкала */ const a0 = Math.PI*0.75, a1 = Math.PI*2.25; ctx.strokeStyle = col.text || '#0f172a'; ctx.lineWidth = 2; for (let i = 0; i <= 10; i++){ const a = a0 + (a1 - a0) * (i/10); const x1 = cx + (r-10) * Math.cos(a), y1 = cy + (r-10) * Math.sin(a); const x2 = cx + r * Math.cos(a), y2 = cy + r * Math.sin(a); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } /* стрелка */ const norm = Math.max(0, Math.min(1, (val - min)/(max - min))); const a = a0 + (a1 - a0) * norm; const tipX = cx + (r-15) * Math.cos(a), tipY = cy + (r-15) * Math.sin(a); ctx.strokeStyle = color; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(tipX, tipY); ctx.stroke(); ctx.fillStyle = color; ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI*2); ctx.fill(); /* подписи */ ctx.fillStyle = col.text || '#0f172a'; ctx.font = 'bold 11px Inter,sans-serif'; ctx.textAlign = 'center'; ctx.fillText(label, cx, cy - r - 6); ctx.font = 'bold 18px Inter,sans-serif'; ctx.fillText(val.toFixed(1), cx, cy + r/2); ctx.font = '11px Inter,sans-serif'; ctx.fillText(unit, cx, cy + r/2 + 14); ctx.textAlign = 'left'; } function draw(){ const col = C(); ctx.fillStyle = col.bg || '#fafafa'; ctx.fillRect(0, 0, W, H); /* 3 прибора в верху */ drawGauge(110, 80, 55, st.a, -10, 8, 'ТАХОМЕТР (a)', 'м/с²', col.acceleration || '#ea580c'); drawGauge(320, 80, 55, st.v, 0, 35, 'СПИДОМЕТР (v)', 'м/с', col.velocity || '#0891b2'); drawGauge(530, 80, 55, st.x % 1000, 0, 1000, 'ОДОМЕТР (Δx)', 'м', col.displacement || '#2563eb'); /* график v(t) и a(t) внизу */ const gx = 20, gy = 180, gw = W - 40, gh = 180; 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, gy + gh/2); ctx.lineTo(gx + gw, gy + gh/2); /* ось t для a */ ctx.moveTo(gx, gy + gh); ctx.lineTo(gx + gw, gy + gh); /* ось t для v */ ctx.stroke(); /* сетка */ ctx.strokeStyle = col.grid || '#e5e7eb'; ctx.lineWidth = 1; const tMax = Math.max(20, st.t); for (let i = 0; i <= 10; i++){ const px = gx + (i/10)*gw; ctx.beginPath(); ctx.moveTo(px, gy); ctx.lineTo(px, gy+gh); ctx.stroke(); } /* подписи */ ctx.fillStyle = col.textMuted || '#64748b'; ctx.font = '10px Inter,sans-serif'; ctx.fillText('v(t)', gx + 4, gy + gh - 4); ctx.fillText('a(t)', gx + 4, gy + gh/2 - 4); ctx.fillText('t, с', gx + gw - 24, gy + gh + 12); /* линии */ 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 = gx + (p.t / tMax) * gw; const py = gy + gh - (p.v / 35) * (gh/2); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); ctx.strokeStyle = col.acceleration || '#ea580c'; ctx.beginPath(); for (let i = 0; i < st.history.length; i++){ const p = st.history[i]; const px = gx + (p.t / tMax) * gw; const py = gy + gh/2 - (p.a / 12) * (gh/4); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); } /* цель скорости — горизонталь */ const target = +document.getElementById('F3-t').value; ctx.strokeStyle = '#fbbf24'; ctx.setLineDash([6, 4]); ctx.lineWidth = 2; const targetY = gy + gh - (target / 35) * (gh/2); ctx.beginPath(); ctx.moveTo(gx, targetY); ctx.lineTo(gx + gw, targetY); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = '#92400e'; ctx.font = 'bold 11px Inter,sans-serif'; ctx.fillText('★ цель '+target.toFixed(1)+' м/с', gx + gw - 110, targetY - 4); } document.getElementById('F3-gas').addEventListener('mousedown', ()=>{ st.mode = 'gas'; }); document.getElementById('F3-gas').addEventListener('mouseup', ()=>{ st.mode = 'coast'; }); document.getElementById('F3-brake').addEventListener('mousedown', ()=>{ st.mode = 'brake'; }); document.getElementById('F3-brake').addEventListener('mouseup', ()=>{ st.mode = 'coast'; }); document.getElementById('F3-coast').addEventListener('click', ()=>{ st.mode = 'coast'; }); document.getElementById('F3-reset').addEventListener('click', reset); ['F3-g','F3-b','F3-t'].forEach(id => document.getElementById(id).addEventListener('input', readSliders)); readSliders(); draw(); B().startLoop('F3', cv, tick); return true; } if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F3', { init: init, cleanup: function(){} }); else document.addEventListener('DOMContentLoaded', ()=>{ if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F3', { init: init, cleanup: function(){} }); }); })();