/** * ModalOverlay — рендерит затемнение модальной сцены. * Задача 04. Подписан на ModalManager.setOnChange — получает state. * * Архитектура: * - Слой ПОД GUI-overlay (z-index ниже GuiOverlay) но НАД Babylon-канвасом. * - Если target='screen' — слой поверх ВСЕГО (включая GUI). z-index выше. * - Spotlights через CSS mask-image: radial-gradient(...) — вырезает «дырки». * - pointer-events: auto когда модал открыт (перехватывает клики кроме GUI). */ import React, { useEffect, useState } from 'react'; export default function ModalOverlay({ scene }) { const [state, setState] = useState(null); // Поллинг — надёжнее чем setOnChange callback, который может перетереться // или не вызваться если scene изменился на следующем кадре. useEffect(() => { if (!scene?.modalManager) return; let cancelled = false; const tick = () => { if (cancelled) return; const s = scene.modalManager.getState?.(); // Снимок shallow-clone — иначе React не увидит изменение setState(s ? { id: s.id, fadePhase: s.fadePhase, currentAlpha: s.currentAlpha, opts: s.opts, spotlightScreens: s.spotlightScreens, } : null); requestAnimationFrame(tick); }; tick(); return () => { cancelled = true; }; }, [scene]); if (!state || state.fadePhase === 'closed') return null; if (state.currentAlpha <= 0.001) return null; console.log('[ModalOverlay] RENDERING alpha=', state.currentAlpha.toFixed(2), 'phase=', state.fadePhase, 'target=', state.opts?.target); const opts = state.opts; const isScreen = opts.target === 'screen'; const color = opts.darkenColor || '#000000'; const alpha = Math.max(0, Math.min(1, state.currentAlpha)); // RGBA bg const bg = _hexToRgba(color, alpha); // mask-image для spotlights (только для target='scene' — на 'screen' нет смысла) let maskStyle = {}; if (!isScreen && Array.isArray(state.spotlightScreens) && state.spotlightScreens.length) { const softEdge = opts.spotlightSoftEdge ?? 40; const gradients = state.spotlightScreens.map(s => { const inner = Math.max(0, s.r - softEdge); const outer = s.r; // mask-image: внутри круга — transparent (вырезаем), снаружи — black (показываем затемнение) return `radial-gradient(circle at ${s.x.toFixed(0)}px ${s.y.toFixed(0)}px, transparent ${inner}px, black ${outer}px)`; }); maskStyle = { WebkitMaskImage: gradients.join(', '), maskImage: gradients.join(', '), WebkitMaskComposite: 'source-in', maskComposite: 'intersect', }; } // ВАЖНО pointer-events: none — иначе overlay перехватывает клики и кнопки модала не работают. // Затемнение — это просто визуальный фильтр, blockInput реализован в PlayerController. // zIndex: // target='scene' → 24 (под GuiOverlay zIndex=25 чтобы GUI был ВИДЕН поверх затемнения) // target='screen' → 60 (поверх GUI — закрывает ВСЁ) // Для 'screen' GUI модала всё равно поверх (GuiOverlay zIndex=25, наш ScreenOverlay 60, // GUI элементы модала рендерятся в GuiOverlay — поэтому надо ставить их в отдельный // слой ВЫШЕ overlay). Простой фикс: для screen ставим overlay на 24 тоже. const zIdx = 24; return (
); } function _hexToRgba(hex, a) { if (typeof hex !== 'string' || !hex.startsWith('#')) return `rgba(0,0,0,${a})`; let h = hex.slice(1); if (h.length === 3) h = h.split('').map(c => c + c).join(''); if (h.length !== 6) return `rgba(0,0,0,${a})`; const r = parseInt(h.slice(0, 2), 16); const g = parseInt(h.slice(2, 4), 16); const b = parseInt(h.slice(4, 6), 16); return `rgba(${r},${g},${b},${a})`; }