import { useEffect, useRef, useState, memo } from 'react'; import type { WheelPrize } from '../../api/wheel'; interface FortuneWheelProps { prizes: WheelPrize[]; isSpinning: boolean; targetRotation: number | null; onSpinComplete: () => void; } // Pre-generate sparkle positions (shown only while spinning). The old formula // `20 + (i*10)%60 / 15 + (i*13)%70` left the first ~6 sparkles colinear (the // modulo never wrapped for small i), so they lined up as a diagonal streak // through the centre. Scatter them on a phyllotaxis (golden-angle) spiral // inside the wheel instead — well-distributed, never colinear. const SPARKLE_POSITIONS = Array.from({ length: 8 }, (_, i) => { const angle = i * 2.39996; // golden angle (radians) const radius = 16 + (i % 4) * 8; // 16–40% from centre, kept within the wheel return { top: `${(50 + radius * Math.sin(angle)).toFixed(1)}%`, left: `${(50 + radius * Math.cos(angle)).toFixed(1)}%`, delay: `${i * 0.15}s`, }; }); const FortuneWheel = memo(function FortuneWheel({ prizes, isSpinning, targetRotation, onSpinComplete, }: FortuneWheelProps) { const wheelRef = useRef(null); const accumulatedRotation = useRef(0); const [displayRotation, setDisplayRotation] = useState(0); useEffect(() => { if (isSpinning && targetRotation !== null && wheelRef.current) { const currentPos = accumulatedRotation.current % 360; let delta = targetRotation - currentPos; // Normalize delta to positive while (delta < 0) delta += 360; const newRotation = accumulatedRotation.current + 1800 + delta; accumulatedRotation.current = newRotation; setDisplayRotation(newRotation); const timeout = setTimeout(() => { onSpinComplete(); }, 5000); return () => clearTimeout(timeout); } }, [isSpinning, targetRotation, onSpinComplete]); if (prizes.length === 0) { return (

No prizes configured

); } const size = 400; const center = size / 2; const outerRadius = size / 2 - 20; const innerRadius = outerRadius - 15; const prizeRadius = innerRadius - 5; const sectorAngle = 360 / prizes.length; const hubRadius = 45; const createSectorPath = (index: number) => { const startAngle = (index * sectorAngle - 90) * (Math.PI / 180); const endAngle = ((index + 1) * sectorAngle - 90) * (Math.PI / 180); const x1 = center + prizeRadius * Math.cos(startAngle); const y1 = center + prizeRadius * Math.sin(startAngle); const x2 = center + prizeRadius * Math.cos(endAngle); const y2 = center + prizeRadius * Math.sin(endAngle); const x1Inner = center + hubRadius * Math.cos(startAngle); const y1Inner = center + hubRadius * Math.sin(startAngle); const x2Inner = center + hubRadius * Math.cos(endAngle); const y2Inner = center + hubRadius * Math.sin(endAngle); const largeArc = sectorAngle > 180 ? 1 : 0; return `M ${x1Inner} ${y1Inner} L ${x1} ${y1} A ${prizeRadius} ${prizeRadius} 0 ${largeArc} 1 ${x2} ${y2} L ${x2Inner} ${y2Inner} A ${hubRadius} ${hubRadius} 0 ${largeArc} 0 ${x1Inner} ${y1Inner} Z`; }; // Position for emoji - closer to outer edge const getEmojiPosition = (index: number) => { const angle = (index * sectorAngle + sectorAngle / 2 - 90) * (Math.PI / 180); const emojiRadius = prizeRadius * 0.75; return { x: center + emojiRadius * Math.cos(angle), y: center + emojiRadius * Math.sin(angle), rotation: index * sectorAngle + sectorAngle / 2, }; }; // Alternate colors for sectors const getSectorColors = (index: number, baseColor?: string) => { if (baseColor) return baseColor; const colors = [ '#8B5CF6', '#EC4899', '#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#6366F1', '#14B8A6', ]; return colors[index % colors.length]; }; return (
{/* Outer glow effect */}
{/* Pointer */}
{/* Main Wheel */}
{/* Sector gradients */} {prizes.map((prize, index) => { const color = getSectorColors(index, prize.color); return ( ); })} {/* Outer ring gradient */} {/* Hub gradient */} {/* Text shadow filter */} {/* Background shadow */} {/* Outer decorative ring */} {/* Inner ring border */} {/* LED chase animation — pure CSS, no React re-renders */} {Array.from({ length: 20 }).map((_, i) => { const angle = (i * 18 - 90) * (Math.PI / 180); const ledRadius = outerRadius + 3; const dotX = center + ledRadius * Math.cos(angle); const dotY = center + ledRadius * Math.sin(angle); // Delay as fraction of full cycle — CSS handles speed via animation-duration const delay = `${(i / 20) * 6}s`; return ( ); })} {/* Rotating wheel group */} {/* Sectors */} {prizes.map((prize, index) => ( ))} {/* Sector dividers */} {prizes.map((_, index) => { const angle = (index * sectorAngle - 90) * (Math.PI / 180); const x1 = center + hubRadius * Math.cos(angle); const y1 = center + hubRadius * Math.sin(angle); const x2 = center + prizeRadius * Math.cos(angle); const y2 = center + prizeRadius * Math.sin(angle); return ( ); })} {/* Prize content - Emoji */} {prizes.map((prize, index) => { const pos = getEmojiPosition(index); return ( {prize.emoji} ); })} {/* Prize content - Text removed, only emoji visible on wheel */} {/* Center hub */} {/* Hub inner decoration */} {/* Hub shine */} {/* Center button */} {/* Center text */} {isSpinning ? '...' : 'SPIN'} {/* Spinning overlay glow */} {isSpinning && (
)}
{/* Sparkle effects when spinning - optimized with pre-calculated positions */} {isSpinning && (
{SPARKLE_POSITIONS.map((pos, i) => (
))}
)}
); }); export default FortuneWheel;