/** * GameLoadingScreen — красивый экран загрузки игры в плеере (задача 05). * * Показывается пока грузится игра (после клика «Играть» на странице игры → * открытие плеера). Композиция как в Roblox: * - размытый фон-обложка игры с медленным Ken Burns (pan + zoom); * - карточка-витрина по центру (обложка игры); * - крупное название места; * - автор + verified-галочка; * - прогресс-бар + спиннер «ЗАГРУЗКА». * * Данные берёт из меты игры (title/thumbnail/автор) и, если автор настроил в * студии вкладку «Стартовый экран» — из project_data.scene.loadingScreen * (placeName / studioName / style / verified / background / cover). */ import React, { useEffect, useRef, useState } from 'react'; // Один раз вставляем CSS-keyframes (нельзя инлайнить в style). let _cssInjected = false; function injectCss() { if (_cssInjected || typeof document === 'undefined') return; _cssInjected = true; const s = document.createElement('style'); s.id = 'kbn-game-loading-css'; s.textContent = '@keyframes kbnGlsKen{0%{transform:scale(1.05) translate3d(0,0,0)}50%{transform:scale(1.15) translate3d(-3%,-2%,0)}100%{transform:scale(1.05) translate3d(-6%,0,0)}}' + '.kbnGlsKen{animation:kbnGlsKen 22s ease-in-out infinite}' + '@keyframes kbnGlsSpin{to{transform:rotate(360deg)}}' + '.kbnGlsSpin{animation:kbnGlsSpin 0.85s linear infinite}' + '@keyframes kbnGlsRise{0%{transform:translateY(0) scale(1);opacity:0}12%{opacity:.9}88%{opacity:.6}100%{transform:translateY(-110vh) scale(1.4);opacity:0}}' + '.kbnGlsP{animation:kbnGlsRise linear infinite}' + '@keyframes kbnGlsGlow{0%,100%{box-shadow:0 18px 60px rgba(0,0,0,.6),0 0 0 rgba(120,160,255,0)}50%{box-shadow:0 18px 60px rgba(0,0,0,.6),0 0 44px rgba(120,160,255,.4)}}' + '.kbnGlsCard{animation:kbnGlsGlow 4s ease-in-out infinite}' + '@keyframes kbnGlsBar{0%{transform:translateX(-100%)}100%{transform:translateX(250%)}}' + '.kbnGlsBarRun{animation:kbnGlsBar 1.2s ease-in-out infinite}' + '@media (prefers-reduced-motion:reduce){.kbnGlsKen,.kbnGlsP,.kbnGlsCard,.kbnGlsBarRun{animation:none}}'; document.head.appendChild(s); } function VerifiedBadge() { return ( ); } /** * props: * meta — ответ getProjectForPlay (title, thumbnail, author_username/username, ...) * loadingScreen — project_data.scene.loadingScreen (опц., настройки автора) * progress — 0..1 (если null — «бегущая» полоса без процента) */ export default function GameLoadingScreen({ meta, loadingScreen, progress }) { injectCss(); const ls = loadingScreen || {}; const [fade, setFade] = useState(0); const rootRef = useRef(null); useEffect(() => { const t = setTimeout(() => setFade(1), 20); return () => clearTimeout(t); }, []); // Источники данных: настройки автора → мета игры → дефолт. const bg = ls.background || meta?.thumbnail || null; const cover = ls.cover || meta?.thumbnail || null; const placeName = ls.placeName || meta?.title || 'Загрузка игры'; const studioName = ls.studioName || meta?.author_username || meta?.username || meta?.author || ''; const verified = ls.verified != null ? !!ls.verified : !!(meta?.author_verified || meta?.is_verified); const style = ls.style || 'ken-burns'; const accent = ls.accentColor || '#5fd0ff'; const hasProgress = typeof progress === 'number' && progress >= 0; const pct = hasProgress ? Math.round(Math.max(0, Math.min(1, progress)) * 100) : null; // parallax по мыши const bgRef = useRef(null); useEffect(() => { if (style !== 'parallax' || !bgRef.current) return; const h = (e) => { const cx = (e.clientX / window.innerWidth - 0.5) * 26; const cy = (e.clientY / window.innerHeight - 0.5) * 16; if (bgRef.current) bgRef.current.style.transform = `translate3d(${-cx}px,${-cy}px,0) scale(1.1)`; }; window.addEventListener('mousemove', h); return () => window.removeEventListener('mousemove', h); }, [style]); const particles = style === 'particles' ? Array.from({ length: 24 }, (_, i) => { const size = 2 + (i % 4); const dur = 7 + (i % 7); return ( ); }) : null; return (
{/* Фоновый слой (Ken Burns / parallax / static) */} {bg && (
)} {/* particles */} {particles &&
{particles}
} {/* Контент */}
{/* Карточка-витрина */}
{!cover && ( РУБЛОКС • 3D )}
{/* Название места */}
{placeName}
{/* Автор + verified */} {studioName && (
{studioName} {verified && }
)} {/* Прогресс-бар */}
{hasProgress ? (
) : (
)}
{/* Спиннер + статус */}
{pct != null ? `${pct}%` : 'ЗАГРУЗКА'}
); }