// 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); }; } })();