diff --git a/frontend/src/components/SpinWheel.tsx b/frontend/src/components/SpinWheel.tsx index 4c2ffde..1b463d1 100644 --- a/frontend/src/components/SpinWheel.tsx +++ b/frontend/src/components/SpinWheel.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo } from 'react' +import { useState, useCallback, useMemo, useEffect } from 'react' import type { Game } from '@/types' import { Gamepad2, Loader2 } from 'lucide-react' @@ -9,27 +9,43 @@ interface SpinWheelProps { disabled?: boolean } -const SPIN_DURATION = 5000 // ms -const EXTRA_ROTATIONS = 5 +const SPIN_DURATION = 6000 // ms - увеличено для более плавного замедления +const EXTRA_ROTATIONS = 7 // больше оборотов для эффекта инерции -// Цветовая палитра секторов +// Пороги для адаптивного отображения +const TEXT_THRESHOLD = 16 // До 16 игр - показываем текст +const LINES_THRESHOLD = 40 // До 40 игр - показываем разделители + +// Цветовая палитра секторов (расширенная для большего количества) const SECTOR_COLORS = [ { bg: '#0d9488', border: '#14b8a6' }, // teal { bg: '#7c3aed', border: '#8b5cf6' }, // violet { bg: '#0891b2', border: '#06b6d4' }, // cyan { bg: '#c026d3', border: '#d946ef' }, // fuchsia { bg: '#059669', border: '#10b981' }, // emerald - { bg: '#7c2d12', border: '#ea580c' }, // orange + { bg: '#ea580c', border: '#f97316' }, // orange { bg: '#1d4ed8', border: '#3b82f6' }, // blue { bg: '#be123c', border: '#e11d48' }, // rose + { bg: '#4f46e5', border: '#6366f1' }, // indigo + { bg: '#0284c7', border: '#0ea5e9' }, // sky + { bg: '#9333ea', border: '#a855f7' }, // purple + { bg: '#16a34a', border: '#22c55e' }, // green ] export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheelProps) { const [isSpinning, setIsSpinning] = useState(false) const [rotation, setRotation] = useState(0) + const [displayedGame, setDisplayedGame] = useState(null) + const [spinStartTime, setSpinStartTime] = useState(null) + const [startRotation, setStartRotation] = useState(0) + const [targetRotation, setTargetRotation] = useState(0) - // Размеры колеса - const wheelSize = 400 + // Определяем режим отображения + const showText = games.length <= TEXT_THRESHOLD + const showLines = games.length <= LINES_THRESHOLD + + // Размеры колеса - увеличиваем для большого количества игр + const wheelSize = games.length > 50 ? 450 : games.length > 30 ? 420 : 400 const centerX = wheelSize / 2 const centerY = wheelSize / 2 const radius = wheelSize / 2 - 10 @@ -102,11 +118,16 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel const fullRotations = EXTRA_ROTATIONS * 360 const finalAngle = fullRotations + (360 - targetSectorMidAngle) + baseRotation - setRotation(rotation + finalAngle) + const newRotation = rotation + finalAngle + setStartRotation(rotation) + setTargetRotation(newRotation) + setSpinStartTime(Date.now()) + setRotation(newRotation) // Ждём окончания анимации setTimeout(() => { setIsSpinning(false) + setSpinStartTime(null) onSpinComplete(resultGame) }, SPIN_DURATION) }, [isSpinning, disabled, games, rotation, sectorAngle, onSpin, onSpinComplete]) @@ -117,13 +138,67 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel return text.slice(0, maxLength - 2) + '...' } + // Функция для вычисления игры под указателем по углу + const getGameAtAngle = useCallback((currentRotation: number) => { + if (games.length === 0) return null + const normalizedRotation = ((currentRotation % 360) + 360) % 360 + const angleUnderPointer = (360 - normalizedRotation + 360) % 360 + const sectorIndex = Math.floor(angleUnderPointer / sectorAngle) % games.length + return games[sectorIndex] || null + }, [games, sectorAngle]) + + // Вычисляем игру под указателем (статическое состояние) + const currentGameUnderPointer = useMemo(() => { + return getGameAtAngle(rotation) + }, [rotation, getGameAtAngle]) + + // Easing функция для имитации инерции - быстрый старт, долгое замедление + // Аппроксимирует CSS cubic-bezier(0.12, 0.9, 0.15, 1) + const easeOutExpo = useCallback((t: number): number => { + // Экспоненциальное замедление - очень быстро в начале, очень медленно в конце + return t === 1 ? 1 : 1 - Math.pow(2, -12 * t) + }, []) + + // Отслеживаем позицию во время вращения + useEffect(() => { + if (!isSpinning || spinStartTime === null) { + // Когда не крутится - показываем текущую игру под указателем + if (currentGameUnderPointer) { + setDisplayedGame(currentGameUnderPointer) + } + return + } + + const totalDelta = targetRotation - startRotation + + const updateDisplayedGame = () => { + const elapsed = Date.now() - spinStartTime + const progress = Math.min(elapsed / SPIN_DURATION, 1) + const easedProgress = easeOutExpo(progress) + + // Вычисляем текущий угол на основе прогресса анимации + const currentAngle = startRotation + (totalDelta * easedProgress) + const game = getGameAtAngle(currentAngle) + + if (game) { + setDisplayedGame(game) + } + } + + // Обновляем каждые 30мс для плавности + const interval = setInterval(updateDisplayedGame, 30) + updateDisplayedGame() // Сразу обновляем + + return () => clearInterval(interval) + }, [isSpinning, spinStartTime, startRotation, targetRotation, getGameAtAngle, currentGameUnderPointer, easeOutExpo]) + // Мемоизируем секторы для производительности const sectors = useMemo(() => { return games.map((game, index) => { const color = SECTOR_COLORS[index % SECTOR_COLORS.length] const path = createSectorPath(index, games.length) const textPos = getTextPosition(index, games.length) - const maxTextLength = games.length > 8 ? 10 : games.length > 5 ? 14 : 18 + const maxTextLength = games.length > 12 ? 8 : games.length > 8 ? 10 : games.length > 5 ? 14 : 18 return { game, color, path, textPos, maxTextLength } }) @@ -213,7 +288,8 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel transform: `rotate(${rotation}deg)`, transitionProperty: isSpinning ? 'transform' : 'none', transitionDuration: isSpinning ? `${SPIN_DURATION}ms` : '0ms', - transitionTimingFunction: 'cubic-bezier(0.17, 0.67, 0.12, 0.99)', + // Инерционное вращение: быстрый старт, долгое плавное замедление + transitionTimingFunction: 'cubic-bezier(0.12, 0.9, 0.15, 1)', }} > @@ -230,38 +306,42 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel - {/* Текст названия игры */} - 10 ? "10" : games.length > 6 ? "11" : "13"} - fontWeight="bold" - style={{ - textShadow: '0 1px 3px rgba(0,0,0,0.8)', - pointerEvents: 'none', - }} - > - {truncateText(game.title, maxTextLength)} - + {/* Текст названия игры - только для небольшого количества */} + {showText && ( + 12 ? "9" : games.length > 8 ? "10" : games.length > 6 ? "11" : "13"} + fontWeight="bold" + style={{ + textShadow: '0 1px 3px rgba(0,0,0,0.8)', + pointerEvents: 'none', + }} + > + {truncateText(game.title, maxTextLength)} + + )} - {/* Разделительная линия */} - + {/* Разделительная линия - только для среднего количества */} + {showLines && ( + + )} ))} @@ -322,6 +402,21 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel )} + {/* Название текущей игры (для большого количества) */} + {!showText && ( +
+

+ {games.length} игр в колесе +

+

+ {displayedGame?.title || 'Крутите колесо!'} +

+
+ )} + {/* Подсказка */}