import { useEffect, useRef, useState } from 'react'; import './FlipDigit.css'; interface FlipDigitProps { value: number; size: number; } export function FlipDigit({ value, size }: FlipDigitProps) { // Background top value (updates immediately at animation start) const [bgTopValue, setBgTopValue] = useState(value); // Background bottom value (updates after bottom flip completes) const [bgBottomValue, setBgBottomValue] = useState(value); // Flip top value (holds old value at animation start) const [flipTopValue, setFlipTopValue] = useState(value); // Flip bottom value (holds new value) const [flipBottomValue, setFlipBottomValue] = useState(null); const animating = useRef(false); const halfHeight = size * 0.5; const cardWidth = size * 0.75; const fontSize = size * 0.75; // Text offset for vertical alignment const topTextOffset = halfHeight * 0.75; const bottomTextOffset = -halfHeight * 0.75; // Animation state const [topRotation, setTopRotation] = useState(0); const [bottomRotation, setBottomRotation] = useState(-90); const [isAnimating, setIsAnimating] = useState(false); // Finish animation const finishAnimation = () => { setIsAnimating(false); requestAnimationFrame(() => { setTopRotation(0); setBottomRotation(-90); setFlipBottomValue(null); animating.current = false; }); }; // Start bottom animation after top completes const startBottomAnimation = (targetValue: number) => { // 上半片翻转完成后,更新上半翻转片的值为新值 // 这样当 finishAnimation 重置 rotation 时,上半翻转片显示正确的值 setFlipTopValue(targetValue); // 第二阶段:下半片从-90°向下翻转90°到0° requestAnimationFrame(() => { setBottomRotation(0); }); // 下半片翻转完成后,背景下半更新为新值 setTimeout(() => { setBgBottomValue(targetValue); setTimeout(() => { finishAnimation(); }, 50); }, 300); }; // When value changes, trigger animation useEffect(() => { if (value !== bgTopValue && !animating.current) { animating.current = true; // Save current value as flip top value setFlipTopValue(bgTopValue); // Set flip bottom value to new value setFlipBottomValue(value); // Update bgTop immediately (revealed when top flips away) setBgTopValue(value); // First, ensure transition is disabled before resetting rotation setIsAnimating(false); // Reset rotation states instantly (without transition) setTopRotation(0); setBottomRotation(-90); // Wait for browser to apply the reset (without transition) requestAnimationFrame(() => { // Now enable transition for the actual animation setIsAnimating(true); // Wait one more frame to ensure transition is enabled requestAnimationFrame(() => { // Start top animation: flip from 0° to 90° setTopRotation(90); }); }); // After top animation completes, start bottom animation setTimeout(() => { startBottomAnimation(value); }, 300); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, bgTopValue]); const containerStyle = { width: `${cardWidth}px`, height: `${size}px`, }; const halfHeightStyle = { height: `${halfHeight}px`, }; const topTextStyle = { fontSize: `${fontSize}px`, transform: `translateY(${topTextOffset}px)`, }; const bottomTextStyle = { fontSize: `${fontSize}px`, transform: `translateY(${bottomTextOffset}px)`, }; const topFlipStyle = { height: `${halfHeight}px`, transform: `rotateX(${topRotation}deg)`, }; const bottomFlipStyle = { height: `${halfHeight}px`, transform: `rotateX(${bottomRotation}deg)`, }; const centerLineStyle = { top: `${halfHeight - 0.5}px`, }; return (
{/* Background top - shows new value's top half */}
{bgTopValue}
{/* Background bottom - shows old value's bottom half */}
{bgBottomValue}
{/* Top flip flap - shows old value's top half, flips down */}
{flipTopValue}
{/* Bottom flip flap - shows new value's bottom half, flips down */} {flipBottomValue !== null && (
{flipBottomValue}
)} {/* Center divider line */}
); }