Files
Learn_System/frontend/js/flagships/phys9_flag_F3_dashboard.js
Maxim Dolgolyov bc64828b22 feat(phys9 flagships): F3 тахометр+спидометр + F4 орбита (Wave A продолжение)
F3. Тахометр + спидометр + одометр (§11 в ch1):
- Canvas 640×400 с 3 аналоговыми приборами вверху
  (тахометр a, спидометр v, одометр Δx mod 1000)
- Графики v(t) и a(t) внизу с горизонтальной цель-линией
- Кнопки «Газ»/«Тормоз» удержанием, «Отпустить» — coast-режим
  (лёгкое торможение от трения)
- Slider'ы: a_газа, |a_тормоз|, цель скорости (по умолчанию 16.7 м/с)
- Рекорд скорости в localStorage
- Feedback при достижении цели

F4. Орбитальный конструктор (§17 в ch2):
- Canvas 640×480 (космос со звёздами)
- Планета (Земля) в центре, спутник запускается с r=200
- Slider'ы: M, v₀, угол α
- Кнопки: Запустить/Сброс/«Круговая орбита» (вычисляет v=√(M/r))
- Физика: F=GM/r² (G=1), Эйлер 8 шагов/кадр
- Trail орбиты до 1500 точек
- Классификация: падение/круговая/эллипс/убегание
- Период T через переход через ось x
- Feedback при крайних случаях

Подключение:
- ch1: phys9_flag_F3_dashboard.js + хук на p11
- ch2: phys9-flagships.css + base + F4 + хук на p17

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:10:33 +03:00

211 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = ''
+ '<div class="flag-sliders">'
+ '<label>Газ (a₊, м/с²): <b id="F3-gv">2</b><input type="range" id="F3-g" min="0" max="8" step="0.5" value="2"></label>'
+ '<label>Тормоз (|a₋|, м/с²): <b id="F3-bv">4</b><input type="range" id="F3-b" min="0" max="10" step="0.5" value="4"></label>'
+ '<label>Цель скорости, м/с: <b id="F3-tv">16.7</b><input type="range" id="F3-t" min="5" max="30" step="0.5" value="16.7"></label>'
+ '</div>'
+ '<canvas id="F3-cv" class="flag-canvas" width="640" height="400" style="height:400px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F3-gas">⬆ Газ</button>'
+ '<button class="flag-btn danger" id="F3-brake">⬇ Тормоз</button>'
+ '<button class="flag-btn" id="F3-coast">Отпустить</button>'
+ '<button class="flag-btn" id="F3-reset">Сброс</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Время</span><span class="val" id="F3-t-st">0 с</span></div>'
+ '<div class="flag-stat"><span class="lbl">$v$ (спидометр)</span><span class="val" id="F3-v">0 м/с</span></div>'
+ '<div class="flag-stat"><span class="lbl">$a$ (тахометр)</span><span class="val" id="F3-a">0 м/с²</span></div>'
+ '<div class="flag-stat"><span class="lbl">$\\Delta x$ (одометр)</span><span class="val" id="F3-x">0 м</span></div>'
+ '<div class="flag-stat"><span class="lbl">Рекорд скорости</span><span class="val" id="F3-rec">0 м/с</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F3-fb"></div>';
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 = '&#10003; Цель достигнута! Скорость '+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(){} });
});
})();