Fix wheel

This commit is contained in:
2025-12-21 00:15:21 +07:00
parent 95e2a77335
commit 9d2dba87b8

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useMemo } from 'react' import { useState, useCallback, useMemo, useEffect } from 'react'
import type { Game } from '@/types' import type { Game } from '@/types'
import { Gamepad2, Loader2 } from 'lucide-react' import { Gamepad2, Loader2 } from 'lucide-react'
@@ -9,27 +9,43 @@ interface SpinWheelProps {
disabled?: boolean disabled?: boolean
} }
const SPIN_DURATION = 5000 // ms const SPIN_DURATION = 6000 // ms - увеличено для более плавного замедления
const EXTRA_ROTATIONS = 5 const EXTRA_ROTATIONS = 7 // больше оборотов для эффекта инерции
// Цветовая палитра секторов // Пороги для адаптивного отображения
const TEXT_THRESHOLD = 16 // До 16 игр - показываем текст
const LINES_THRESHOLD = 40 // До 40 игр - показываем разделители
// Цветовая палитра секторов (расширенная для большего количества)
const SECTOR_COLORS = [ const SECTOR_COLORS = [
{ bg: '#0d9488', border: '#14b8a6' }, // teal { bg: '#0d9488', border: '#14b8a6' }, // teal
{ bg: '#7c3aed', border: '#8b5cf6' }, // violet { bg: '#7c3aed', border: '#8b5cf6' }, // violet
{ bg: '#0891b2', border: '#06b6d4' }, // cyan { bg: '#0891b2', border: '#06b6d4' }, // cyan
{ bg: '#c026d3', border: '#d946ef' }, // fuchsia { bg: '#c026d3', border: '#d946ef' }, // fuchsia
{ bg: '#059669', border: '#10b981' }, // emerald { bg: '#059669', border: '#10b981' }, // emerald
{ bg: '#7c2d12', border: '#ea580c' }, // orange { bg: '#ea580c', border: '#f97316' }, // orange
{ bg: '#1d4ed8', border: '#3b82f6' }, // blue { bg: '#1d4ed8', border: '#3b82f6' }, // blue
{ bg: '#be123c', border: '#e11d48' }, // rose { 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) { export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheelProps) {
const [isSpinning, setIsSpinning] = useState(false) const [isSpinning, setIsSpinning] = useState(false)
const [rotation, setRotation] = useState(0) const [rotation, setRotation] = useState(0)
const [displayedGame, setDisplayedGame] = useState<Game | null>(null)
const [spinStartTime, setSpinStartTime] = useState<number | null>(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 centerX = wheelSize / 2
const centerY = wheelSize / 2 const centerY = wheelSize / 2
const radius = wheelSize / 2 - 10 const radius = wheelSize / 2 - 10
@@ -102,11 +118,16 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel
const fullRotations = EXTRA_ROTATIONS * 360 const fullRotations = EXTRA_ROTATIONS * 360
const finalAngle = fullRotations + (360 - targetSectorMidAngle) + baseRotation const finalAngle = fullRotations + (360 - targetSectorMidAngle) + baseRotation
setRotation(rotation + finalAngle) const newRotation = rotation + finalAngle
setStartRotation(rotation)
setTargetRotation(newRotation)
setSpinStartTime(Date.now())
setRotation(newRotation)
// Ждём окончания анимации // Ждём окончания анимации
setTimeout(() => { setTimeout(() => {
setIsSpinning(false) setIsSpinning(false)
setSpinStartTime(null)
onSpinComplete(resultGame) onSpinComplete(resultGame)
}, SPIN_DURATION) }, SPIN_DURATION)
}, [isSpinning, disabled, games, rotation, sectorAngle, onSpin, onSpinComplete]) }, [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) + '...' 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(() => { const sectors = useMemo(() => {
return games.map((game, index) => { return games.map((game, index) => {
const color = SECTOR_COLORS[index % SECTOR_COLORS.length] const color = SECTOR_COLORS[index % SECTOR_COLORS.length]
const path = createSectorPath(index, games.length) const path = createSectorPath(index, games.length)
const textPos = getTextPosition(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 } return { game, color, path, textPos, maxTextLength }
}) })
@@ -213,7 +288,8 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel
transform: `rotate(${rotation}deg)`, transform: `rotate(${rotation}deg)`,
transitionProperty: isSpinning ? 'transform' : 'none', transitionProperty: isSpinning ? 'transform' : 'none',
transitionDuration: isSpinning ? `${SPIN_DURATION}ms` : '0ms', 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)',
}} }}
> >
<defs> <defs>
@@ -230,12 +306,13 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel
<path <path
d={path} d={path}
fill={color.bg} fill={color.bg}
stroke={color.border} stroke={showLines ? color.border : 'transparent'}
strokeWidth="2" strokeWidth={showLines ? "1" : "0"}
filter="url(#sectorShadow)" filter="url(#sectorShadow)"
/> />
{/* Текст названия игры */} {/* Текст названия игры - только для небольшого количества */}
{showText && (
<text <text
x={textPos.x} x={textPos.x}
y={textPos.y} y={textPos.y}
@@ -243,7 +320,7 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel
textAnchor="middle" textAnchor="middle"
dominantBaseline="middle" dominantBaseline="middle"
fill="white" fill="white"
fontSize={games.length > 10 ? "10" : games.length > 6 ? "11" : "13"} fontSize={games.length > 12 ? "9" : games.length > 8 ? "10" : games.length > 6 ? "11" : "13"}
fontWeight="bold" fontWeight="bold"
style={{ style={{
textShadow: '0 1px 3px rgba(0,0,0,0.8)', textShadow: '0 1px 3px rgba(0,0,0,0.8)',
@@ -252,16 +329,19 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel
> >
{truncateText(game.title, maxTextLength)} {truncateText(game.title, maxTextLength)}
</text> </text>
)}
{/* Разделительная линия */} {/* Разделительная линия - только для среднего количества */}
{showLines && (
<line <line
x1={centerX} x1={centerX}
y1={centerY} y1={centerY}
x2={centerX + radius * Math.cos((index * sectorAngle - 90) * Math.PI / 180)} x2={centerX + radius * Math.cos((index * sectorAngle - 90) * Math.PI / 180)}
y2={centerY + radius * Math.sin((index * sectorAngle - 90) * Math.PI / 180)} y2={centerY + radius * Math.sin((index * sectorAngle - 90) * Math.PI / 180)}
stroke="rgba(255,255,255,0.3)" stroke="rgba(255,255,255,0.2)"
strokeWidth="1" strokeWidth="1"
/> />
)}
</g> </g>
))} ))}
@@ -322,6 +402,21 @@ export function SpinWheel({ games, onSpin, onSpinComplete, disabled }: SpinWheel
)} )}
</div> </div>
{/* Название текущей игры (для большого количества) */}
{!showText && (
<div className="glass rounded-xl px-6 py-3 min-w-[280px] text-center">
<p className="text-xs text-gray-500 mb-1">
{games.length} игр в колесе
</p>
<p className={`
font-semibold transition-all duration-100 truncate max-w-[280px]
${isSpinning ? 'text-neon-400 animate-pulse' : 'text-white'}
`}>
{displayedGame?.title || 'Крутите колесо!'}
</p>
</div>
)}
{/* Подсказка */} {/* Подсказка */}
<p className={` <p className={`
text-sm transition-all duration-300 text-sm transition-all duration-300