lukcy-draw/src/components/SlotMachine.jsx

181 lines
6.6 KiB
React
Raw Normal View History

2025-12-12 02:15:19 +00:00
import { useEffect, useRef, useState, useMemo } from 'react';
import { motion, useAnimation } from 'framer-motion';
const NUM_DUPLICATES = 5; // How many loops for the spin effect
// Fisher-Yates shuffle algorithm
const shuffleArray = (array) => {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
};
// Responsive slot heights based on screen size
const getSlotHeight = () => {
if (typeof window === 'undefined') return 320;
const width = window.innerWidth;
if (width < 640) return 96; // mobile (h-24)
if (width < 768) return 128; // sm (h-32)
if (width < 1024) return 176; // md (h-44)
if (width < 1280) return 224; // lg (h-56)
if (width < 1536) return 288; // xl (h-72)
return 320; // 2xl (h-80)
};
export function SlotMachine({ winner, isSpinning, onAnimationComplete, onDraw, poolLength }) {
const controls1 = useAnimation();
const controls2 = useAnimation();
const controls3 = useAnimation();
const [slotHeight, setSlotHeight] = useState(getSlotHeight());
// Generate randomized sequences for each reel (memoized so they stay consistent)
// Always start with 0 at position 0, then shuffle the rest
const randomSequences = useMemo(() => {
const createSequence = () => {
const remaining = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const shuffled = shuffleArray(remaining);
return [0, ...shuffled]; // Always put 0 first
};
return [
createSequence(), // Reel 1
createSequence(), // Reel 2
createSequence(), // Reel 3
];
}, []);
useEffect(() => {
const handleResize = () => setSlotHeight(getSlotHeight());
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
if (isSpinning && winner !== null) {
const digits = winner.toString().padStart(3, '0').split('').map(Number);
animateReels(digits);
}
}, [isSpinning, winner]);
const animateReels = async (digits) => {
// Reset positions instantly
await Promise.all([
controls1.set({ y: 0 }),
controls2.set({ y: 0 }),
controls3.set({ y: 0 })
]);
// Animate each reel with stagger
// For each digit, find its position in the randomized sequence
const spin = (control, digit, reelIndex, delay) => {
// Find where this digit appears in the last cycle of the randomized sequence
const lastCycleStart = NUM_DUPLICATES * 10;
const digitIndexInSequence = randomSequences[reelIndex].indexOf(digit);
const targetIndex = lastCycleStart + digitIndexInSequence;
const targetY = -(targetIndex * slotHeight);
return control.start({
y: targetY,
transition: {
duration: 20 + delay,
ease: [0.1, 0.9, 0.2, 1], // "Luxury" ease-out
delay: 0 // We can rely on duration difference for staggering or explicit delay
}
});
};
// Staggered finish times: 20s, 20.5s, 21s
await Promise.all([
spin(controls1, digits[0], 0, 0),
spin(controls2, digits[1], 1, 0.5),
spin(controls3, digits[2], 2, 1.0)
]);
if (onAnimationComplete) {
onAnimationComplete();
}
};
const Strip = ({ controls, reelIndex }) => {
// Generate the number strip with randomized sequence
const numbers = [];
const sequence = randomSequences[reelIndex];
// Repeat the randomized sequence NUM_DUPLICATES times, then one final cycle
for (let i = 0; i < NUM_DUPLICATES + 1; i++) {
sequence.forEach(num => numbers.push(num));
}
return (
<motion.div
className="flex flex-col items-center w-full"
animate={controls}
>
{numbers.map((num, i) => (
<div
key={i}
className="flex items-center justify-center font-black relative"
style={{
height: slotHeight,
width: '100%',
fontSize: `${slotHeight * 0.78}px`,
fontWeight: 'bold',
fontFamily: 'system-ui, -apple-system, sans-serif',
background: 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
filter: 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))',
lineHeight: '1',
}}
>
{num}
</div>
))}
</motion.div>
);
};
const handleClick = () => {
if (!isSpinning && poolLength > 0 && onDraw) {
onDraw();
}
};
return (
<div
className={`relative flex items-center justify-center z-20 w-full ${
!isSpinning && poolLength > 0 ? 'cursor-pointer' : 'cursor-not-allowed'
}`}
onClick={handleClick}
>
<div className={`flex gap-2 sm:gap-3 md:gap-4 lg:gap-5 xl:gap-6 justify-center transition-transform duration-200 ${
!isSpinning && poolLength > 0 ? 'hover:scale-105 active:scale-95' : ''
}`}>
<SlotBox><Strip controls={controls1} reelIndex={0} /></SlotBox>
<SlotBox><Strip controls={controls2} reelIndex={1} /></SlotBox>
<SlotBox><Strip controls={controls3} reelIndex={2} /></SlotBox>
</div>
</div>
);
}
const SlotBox = ({ children }) => (
<div
className="relative w-32 h-40 sm:w-40 sm:h-48 md:w-48 md:h-60 lg:w-60 lg:h-[18rem] xl:w-72 xl:h-[22rem] 2xl:w-80 2xl:h-96 flex items-start justify-center overflow-hidden"
style={{
borderRadius: '1.5rem',
background: '#FFFFFF',
border: '3px solid #D4AF37',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), 0 0 0 2px #D4AF37'
}}
>
<div className="relative w-full h-full">
{children}
</div>
</div>
);