Files
Learn_System/frontend/js/flagships/phys9_flag_base.js
Maxim Dolgolyov 4bcc47e5be 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>
2026-05-30 10:06:37 +03:00

149 lines
4.7 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.
// phys9_flag_base.js — общая инфраструктура для всех флагман-интерактивов Физики 9.
// Экспорт: window.PHYS9_FLAG_BASE = { register, unmount, ... }.
(function(){
'use strict';
const C = () => window.PHYS9_COLORS || {};
const _flags = {}; /* id → { init, cleanup, _raf, _mounted, _io } */
/* === Регистрация флагмана === */
function register(id, def){
_flags[id] = Object.assign({ _raf: 0, _mounted: false, _io: null }, def);
}
/* === Размонтировать (вызывается при goTo другого §) === */
function unmount(id){
const f = _flags[id]; if (!f) return;
if (f._raf) { cancelAnimationFrame(f._raf); f._raf = 0; }
if (f._io) { try { f._io.disconnect(); } catch(e){} f._io = null; }
if (f.cleanup) try { f.cleanup(); } catch(e){}
f._mounted = false;
}
/* === Размонтировать все === */
function unmountAll(){
for (const id in _flags) unmount(id);
}
/* === Загрузка флагмана для секции pN === */
function mount(id, secId){
const f = _flags[id]; if (!f) return false;
if (f._mounted) return true;
const ok = f.init(secId);
if (ok !== false) f._mounted = true;
return ok !== false;
}
/* === Обёртка SVG/canvas вставки в pN-body === */
function makeCard(secId, title, desc, body){
const flagBox = document.createElement('div');
flagBox.className = 'flag-card phys9-flag-' + secId;
flagBox.innerHTML =
'<div class="flag-title">' + title + '</div>'
+ '<div class="flag-desc">' + desc + '</div>'
+ body;
const host = document.getElementById(secId + '-body');
if (!host) return null;
if (host.querySelector('.phys9-flag-' + secId)) return null;
host.appendChild(flagBox);
try { if(window.renderMathInElement) window.renderMathInElement(flagBox, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){}
return flagBox;
}
/* === Анимационный цикл с IntersectionObserver для авто-паузы === */
function startLoop(id, canvas, tick){
const f = _flags[id]; if (!f) return;
let visible = true;
/* IntersectionObserver — если canvas вне экрана, паузим */
try {
f._io = new IntersectionObserver(entries => {
visible = entries[0].isIntersecting;
}, { threshold: 0.05 });
f._io.observe(canvas);
} catch(e){}
let lastT = performance.now();
function loop(now){
const dt = Math.min(50, now - lastT) / 1000;
lastT = now;
if (visible) {
try { tick(dt); } catch(e){ console.warn('phys9 flag tick:', e.message); }
}
f._raf = requestAnimationFrame(loop);
}
f._raf = requestAnimationFrame(loop);
}
/* === Высокий-DPI canvas init === */
function initCanvas(id){
const cv = document.getElementById(id);
if (!cv) return null;
const dpr = window.devicePixelRatio || 1;
const W = cv.offsetWidth || 600;
const H = cv.offsetHeight || 400;
cv.width = Math.round(W * dpr);
cv.height = Math.round(H * dpr);
const ctx = cv.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return { cv, ctx, W, H };
}
/* === Стрелка на canvas === */
function arrow(ctx, x1, y1, x2, y2, color, width){
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = width || 2.5;
ctx.lineCap = 'round';
const dx = x2 - x1, dy = y2 - y1, len = Math.hypot(dx, dy);
if (len < 1e-3) return;
const ux = dx/len, uy = dy/len, h = 10, hw = 6;
const bx = x2 - ux*h, by = y2 - uy*h;
ctx.beginPath();
ctx.moveTo(x1, y1); ctx.lineTo(bx, by);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(bx - uy*hw, by + ux*hw);
ctx.lineTo(bx + uy*hw, by - ux*hw);
ctx.closePath();
ctx.fill();
}
/* === Сохранение рекорда === */
function saveRecord(key, value){
try {
const cur = +(localStorage.getItem('phys9_record_' + key) || -Infinity);
if (value > cur) localStorage.setItem('phys9_record_' + key, String(value));
return Math.max(cur, value);
} catch(e){ return value; }
}
function getRecord(key, def){
try { return +(localStorage.getItem('phys9_record_' + key) || (def || 0)); }
catch(e){ return def || 0; }
}
window.PHYS9_FLAG_BASE = {
register: register,
mount: mount,
unmount: unmount,
unmountAll: unmountAll,
makeCard: makeCard,
initCanvas: initCanvas,
startLoop: startLoop,
arrow: arrow,
saveRecord: saveRecord,
getRecord: getRecord,
C: C
};
/* === Хук на goTo: отменять анимации при переключении секций === */
const _origGoTo = window.goTo;
if (typeof _origGoTo === 'function') {
window.goTo = function(id){
unmountAll();
return _origGoTo.apply(this, arguments);
};
}
})();