// 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 =
'
' + title + '
'
+ '' + desc + '
'
+ 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);
};
}
})();