feat(phys9 flagships): инфраструктура + F1 траектория + F2 гонка (Wave A pilot)
Phase 1 + Wave A пилоты — большие интерактивы Физики 9.
frontend/css/phys9-flagships.css — стили карточек флагманов
(.flag-card с бейджем «★ ФЛАГМАН», .flag-canvas, .flag-controls,
.flag-stats, .flag-sliders, .flag-feedback). Тёмная тема поддержана.
frontend/js/flagships/phys9_flag_base.js — общая инфраструктура:
- register(id, def) — регистрация флагмана
- mount/unmount/unmountAll — управление жизненным циклом
- makeCard(secId, title, desc, body) — создание карточки
- initCanvas(id) — высокий-DPI canvas
- startLoop(id, canvas, tick) — RAF с IntersectionObserver
(авто-пауза если canvas за экраном)
- arrow(ctx, ...) — стрелка на canvas
- saveRecord/getRecord — сохранение в localStorage
- хук на goTo: unmountAll при смене параграфа
Флагман F1. Конструктор траектории (§5):
- Canvas 600×320, рисуется мышкой/пальцем (touch support)
- Real-time расчёт пути s и перемещения |Δr|
- Шаблоны: прямая / полуокружность / замкнутая окружность
- Feedback: «прямая → s=|Δr|», «замкнутая → |Δr|→0», «кривая → s>|Δr|»
- Кнопка «Замкнуть петлю» соединяет начало и конец
Флагман F2. Гонка двух тел (§9):
- Двухпанельный canvas 640×360 (трасса слева, графики справа)
- 5 slider'ов: v₀₁, a₁, x₀₂, v₀₂, a₂
- Запуск/Пауза/Сброс/Случайный сценарий
- Реальная физика равноуск. движения, симуляция Эйлером (4 шага/кадр)
- Real-time графики x₁(t) и x₂(t), пересечение = встреча
- Автоматическое определение момента встречи (квадратное уравнение)
- При встрече — звёздочка на пересечении графиков + feedback с t и x
В physics_9_ch1.html:
- Подключены CSS + 3 JS
- Расширен хук ensureBuilt: на p5 → mount('F1','p5'), на p9 → mount('F2','p9')
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
// F1. Конструктор траектории (§5) — рисуй мышкой, видишь s vs |Δr|.
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
const B = () => window.PHYS9_FLAG_BASE;
|
||||
const C = () => window.PHYS9_COLORS || {};
|
||||
|
||||
function init(secId){
|
||||
if (!B()) return false;
|
||||
const body = ''
|
||||
+ '<canvas id="F1-cv" class="flag-canvas" width="600" height="320" style="height:320px;cursor:crosshair"></canvas>'
|
||||
+ '<div class="flag-controls">'
|
||||
+ '<button class="flag-btn primary" id="F1-clear">Очистить</button>'
|
||||
+ '<button class="flag-btn" id="F1-close">Замкнуть петлю</button>'
|
||||
+ '<button class="flag-btn" id="F1-line">Прямая</button>'
|
||||
+ '<button class="flag-btn" id="F1-arc">Полуокружность</button>'
|
||||
+ '<button class="flag-btn" id="F1-circle">Замкнутая окружность</button>'
|
||||
+ '</div>'
|
||||
+ '<div class="flag-stats">'
|
||||
+ '<div class="flag-stat"><span class="lbl">Путь $s$</span><span class="val" id="F1-s">0</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">|Δ<i>r</i>| перемещение</span><span class="val" id="F1-dr">0</span></div>'
|
||||
+ '<div class="flag-stat"><span class="lbl">Отношение $s/|Δ r|$</span><span class="val" id="F1-ratio">—</span></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="flag-feedback" id="F1-fb"></div>';
|
||||
|
||||
const card = B().makeCard(secId,
|
||||
'F1. Конструктор траектории',
|
||||
'Нарисуй мышкой/пальцем кривую — посмотри, как соотносятся путь $s$ и перемещение $|Δ\\vec r|$. Они равны только для прямой.',
|
||||
body);
|
||||
if (!card) return false;
|
||||
|
||||
const cv = document.getElementById('F1-cv');
|
||||
const ctx = cv.getContext('2d');
|
||||
let points = []; /* [{x, y}] */
|
||||
let drawing = false;
|
||||
|
||||
function getPos(e){
|
||||
const rect = cv.getBoundingClientRect();
|
||||
const sx = cv.width / rect.width;
|
||||
const sy = cv.height / rect.height;
|
||||
const tx = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
|
||||
const ty = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
|
||||
return { x: tx * sx, y: ty * sy };
|
||||
}
|
||||
|
||||
function calc(){
|
||||
let s = 0;
|
||||
for (let i = 1; i < points.length; i++){
|
||||
s += Math.hypot(points[i].x - points[i-1].x, points[i].y - points[i-1].y);
|
||||
}
|
||||
let dr = 0;
|
||||
if (points.length >= 2) {
|
||||
const a = points[0], b = points[points.length-1];
|
||||
dr = Math.hypot(b.x - a.x, b.y - a.y);
|
||||
}
|
||||
/* px → м: считаем что canvas 600px = 6 м */
|
||||
const m_per_px = 6 / 600;
|
||||
return { s: s * m_per_px, dr: dr * m_per_px };
|
||||
}
|
||||
|
||||
function draw(){
|
||||
ctx.clearRect(0, 0, cv.width, cv.height);
|
||||
/* сетка */
|
||||
const col = C();
|
||||
ctx.strokeStyle = col.grid || '#e5e7eb';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < cv.width; x += 50){ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, cv.height); ctx.stroke(); }
|
||||
for (let y = 0; y < cv.height; y += 50){ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cv.width, y); ctx.stroke(); }
|
||||
if (points.length === 0){
|
||||
ctx.fillStyle = col.textMuted || '#64748b';
|
||||
ctx.font = '15px Inter,sans-serif';
|
||||
ctx.fillText('Нажми и проведи курсором/пальцем — нарисуй траекторию', 60, cv.height/2);
|
||||
return;
|
||||
}
|
||||
/* траектория */
|
||||
ctx.strokeStyle = col.velocity || '#0891b2';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
||||
ctx.stroke();
|
||||
/* перемещение */
|
||||
if (points.length >= 2){
|
||||
const a = points[0], b = points[points.length-1];
|
||||
ctx.strokeStyle = col.displacement || '#2563eb';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.setLineDash([8, 5]);
|
||||
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
/* стрелка перемещения */
|
||||
B().arrow(ctx, a.x, a.y, b.x, b.y, col.displacement || '#2563eb', 2.5);
|
||||
/* точки start/end */
|
||||
ctx.fillStyle = '#10b981';
|
||||
ctx.beginPath(); ctx.arc(a.x, a.y, 7, 0, Math.PI*2); ctx.fill();
|
||||
ctx.fillStyle = '#dc2626';
|
||||
ctx.beginPath(); ctx.arc(b.x, b.y, 7, 0, Math.PI*2); ctx.fill();
|
||||
ctx.fillStyle = col.text || '#0f172a';
|
||||
ctx.font = 'bold 13px Inter,sans-serif';
|
||||
ctx.fillText('старт', a.x + 10, a.y - 8);
|
||||
ctx.fillText('конец', b.x + 10, b.y - 8);
|
||||
}
|
||||
/* обновить статы */
|
||||
const r = calc();
|
||||
document.getElementById('F1-s').textContent = r.s.toFixed(2) + ' м';
|
||||
document.getElementById('F1-dr').textContent = r.dr.toFixed(2) + ' м';
|
||||
const ratio = r.dr > 0.01 ? (r.s / r.dr).toFixed(2) : '∞';
|
||||
document.getElementById('F1-ratio').textContent = ratio;
|
||||
/* feedback */
|
||||
const fb = document.getElementById('F1-fb');
|
||||
if (r.dr > 0.01 && Math.abs(r.s/r.dr - 1) < 0.05){
|
||||
fb.className = 'flag-feedback ok show';
|
||||
fb.innerHTML = '✓ Прямая траектория: $s = |Δ\\vec r|$ — это единственный случай равенства!';
|
||||
} else if (r.dr < 0.05 && r.s > 0.1){
|
||||
fb.className = 'flag-feedback warn show';
|
||||
fb.innerHTML = 'Замкнутая или почти замкнутая кривая: $|Δ\\vec r| \\to 0$, но путь $s$ остался.';
|
||||
} else if (r.s > 0.1) {
|
||||
fb.className = 'flag-feedback ok show';
|
||||
fb.innerHTML = 'Криволинейное движение: $s > |Δ\\vec r|$ всегда (в '+ratio+' раз больше).';
|
||||
} else {
|
||||
fb.className = 'flag-feedback';
|
||||
}
|
||||
try { if(window.renderMathInElement) window.renderMathInElement(card, { delimiters:[{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){}
|
||||
}
|
||||
|
||||
function start(e){
|
||||
drawing = true;
|
||||
points = [getPos(e)];
|
||||
draw();
|
||||
e.preventDefault();
|
||||
}
|
||||
function move(e){
|
||||
if (!drawing) return;
|
||||
const p = getPos(e);
|
||||
const last = points[points.length-1];
|
||||
if (Math.hypot(p.x - last.x, p.y - last.y) > 3) {
|
||||
points.push(p);
|
||||
draw();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
function end(){ drawing = false; }
|
||||
|
||||
cv.addEventListener('mousedown', start);
|
||||
cv.addEventListener('mousemove', move);
|
||||
cv.addEventListener('mouseup', end);
|
||||
cv.addEventListener('mouseleave', end);
|
||||
cv.addEventListener('touchstart', start, {passive:false});
|
||||
cv.addEventListener('touchmove', move, {passive:false});
|
||||
cv.addEventListener('touchend', end);
|
||||
|
||||
document.getElementById('F1-clear').addEventListener('click', ()=>{ points = []; draw(); });
|
||||
document.getElementById('F1-close').addEventListener('click', ()=>{
|
||||
if (points.length < 2) return;
|
||||
points.push(Object.assign({}, points[0])); draw();
|
||||
});
|
||||
document.getElementById('F1-line').addEventListener('click', ()=>{
|
||||
points = [{x:80, y:160}, {x:520, y:160}]; draw();
|
||||
});
|
||||
document.getElementById('F1-arc').addEventListener('click', ()=>{
|
||||
points = [];
|
||||
const cx = 300, cy = 250, r = 180;
|
||||
for (let a = Math.PI; a >= 0; a -= 0.05) points.push({ x: cx + r*Math.cos(a), y: cy - r*Math.sin(a)*0.6 });
|
||||
draw();
|
||||
});
|
||||
document.getElementById('F1-circle').addEventListener('click', ()=>{
|
||||
points = [];
|
||||
const cx = 300, cy = 160, r = 110;
|
||||
for (let a = 0; a <= 2*Math.PI + 0.05; a += 0.05) points.push({ x: cx + r*Math.cos(a), y: cy + r*Math.sin(a)*0.7 });
|
||||
draw();
|
||||
});
|
||||
|
||||
draw();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F1', { init: init, cleanup: function(){} });
|
||||
else {
|
||||
/* base ещё не загружен — отложить регистрацию */
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F1', { init: init, cleanup: function(){} });
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user