diff --git a/src/KubikonPlayer/GameLoadingScreen.jsx b/src/KubikonPlayer/GameLoadingScreen.jsx new file mode 100644 index 0000000..63ade54 --- /dev/null +++ b/src/KubikonPlayer/GameLoadingScreen.jsx @@ -0,0 +1,198 @@ +/** + * 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 ( +