4bcc47e5be
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>
149 lines
4.7 KiB
JavaScript
149 lines
4.7 KiB
JavaScript
// 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);
|
||
};
|
||
}
|
||
|
||
})();
|