chore: 更新构建ID和相关文件引用

This commit is contained in:
张翔
2026-02-23 10:38:29 +08:00
parent b38fd022b6
commit 25586a5a6c
152 changed files with 3230 additions and 571 deletions
+946
View File
@@ -0,0 +1,946 @@
'use client';
import { motion, useInView, useSpring, useTransform, type MotionValue, type Variants, type HTMLMotionProps } from 'framer-motion';
import { useRef, useEffect, useState, type ReactNode } from 'react';
export const inkVariants: Variants = {
hidden: {
opacity: 0,
scale: 0.8,
filter: 'blur(10px)',
},
visible: {
opacity: 1,
scale: 1,
filter: 'blur(0px)',
transition: {
duration: 0.8,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const sealStampVariants: Variants = {
hidden: {
opacity: 0,
scale: 1.5,
rotate: -15,
},
visible: {
opacity: 1,
scale: 1,
rotate: 0,
transition: {
type: 'spring',
stiffness: 300,
damping: 20,
mass: 1,
},
},
};
export const brushStrokeVariants: Variants = {
hidden: {
pathLength: 0,
opacity: 0,
},
visible: {
pathLength: 1,
opacity: 1,
transition: {
pathLength: {
duration: 1.5,
ease: 'easeInOut',
},
opacity: {
duration: 0.3,
},
},
},
};
export const fadeUpVariants: Variants = {
hidden: {
opacity: 0,
y: 30,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const staggerContainerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.1,
},
},
};
export const staggerItemVariants: Variants = {
hidden: {
opacity: 0,
y: 20,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: [0.16, 1, 0.3, 1],
},
},
};
interface InkRevealProps extends HTMLMotionProps<'div'> {
children: ReactNode;
delay?: number;
className?: string;
}
export function InkReveal({ children, delay = 0, className = '', ...props }: InkRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={inkVariants}
transition={{ delay }}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface SealStampProps extends HTMLMotionProps<'div'> {
children: ReactNode;
delay?: number;
className?: string;
}
export function SealStamp({ children, delay = 0, className = '', ...props }: SealStampProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={sealStampVariants}
transition={{ delay }}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface FadeUpProps extends HTMLMotionProps<'div'> {
children: ReactNode;
delay?: number;
duration?: number;
className?: string;
}
export function FadeUp({ children, delay = 0, duration = 0.6, className = '', ...props }: FadeUpProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={{
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration,
delay,
ease: [0.16, 1, 0.3, 1],
},
},
}}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface StaggerContainerProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
staggerDelay?: number;
}
export function StaggerContainer({ children, className = '', staggerDelay = 0.1, ...props }: StaggerContainerProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: staggerDelay,
delayChildren: 0.1,
},
},
}}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface StaggerItemProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
}
export function StaggerItem({ children, className = '', ...props }: StaggerItemProps) {
return (
<motion.div
variants={staggerItemVariants}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface RippleButtonProps {
children: ReactNode;
onClick?: () => void;
className?: string;
rippleColor?: string;
}
export function RippleButton({ children, onClick, className = '', rippleColor = 'rgba(196, 30, 58, 0.3)' }: RippleButtonProps) {
const [ripples, setRipples] = useState<Array<{ x: number; y: number; id: number }>>([]);
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const button = e.currentTarget;
const rect = button.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const id = Date.now();
setRipples((prev) => [...prev, { x, y, id }]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== id));
}, 600);
onClick?.();
};
return (
<motion.button
onClick={handleClick}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className={`relative overflow-hidden ${className}`}
>
{children}
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
className="absolute w-4 h-4 rounded-full pointer-events-none"
style={{
left: ripple.x - 8,
top: ripple.y - 8,
backgroundColor: rippleColor,
}}
/>
))}
</motion.button>
);
}
interface InkCardProps {
children: ReactNode;
className?: string;
hoverScale?: number;
hoverShadow?: string;
}
export function InkCard({ children, className = '', hoverScale = 1.02, hoverShadow = '0 20px 40px rgba(28, 28, 28, 0.1)' }: InkCardProps) {
return (
<motion.div
whileHover={{
scale: hoverScale,
boxShadow: hoverShadow,
y: -4,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 20,
}}
className={className}
>
{children}
</motion.div>
);
}
export function useParallax(value: MotionValue<number>, distance: number) {
return useTransform(value, [0, 1], [-distance, distance]);
}
export function useSmoothSpring(value: MotionValue<number>, springConfig = { stiffness: 100, damping: 30, restDelta: 0.001 }) {
return useSpring(value, springConfig);
}
interface CountUpProps {
end: number;
duration?: number;
delay?: number;
prefix?: string;
suffix?: string;
className?: string;
}
export function CountUp({ end, duration = 2000, delay = 0, prefix = '', suffix = '', className = '' }: CountUpProps) {
const [count, setCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const hasAnimated = useRef(false);
useEffect(() => {
if (!isInView || hasAnimated.current) return;
hasAnimated.current = true;
const startTime = Date.now() + delay;
const endTime = startTime + duration;
const animate = () => {
const now = Date.now();
if (now < startTime) {
requestAnimationFrame(animate);
return;
}
if (now >= endTime) {
setCount(end);
return;
}
const progress = (now - startTime) / duration;
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = Math.floor(easeOutQuart * end);
setCount(currentValue);
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [isInView, end, duration, delay]);
return (
<motion.span
ref={ref}
initial={{ opacity: 0, scale: 0.5 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.5, delay: delay / 1000 }}
className={className}
>
{prefix}
{count}
{suffix}
</motion.span>
);
}
interface TypewriterProps {
text: string;
delay?: number;
speed?: number;
className?: string;
cursorClassName?: string;
}
export function Typewriter({ text, delay = 0, speed = 50, className = '', cursorClassName = '' }: TypewriterProps) {
const [displayText, setDisplayText] = useState('');
const [isTyping, setIsTyping] = useState(false);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
useEffect(() => {
if (!isInView) return;
const startTyping = setTimeout(() => {
setIsTyping(true);
let currentIndex = 0;
const typeInterval = setInterval(() => {
if (currentIndex < text.length) {
setDisplayText(text.slice(0, currentIndex + 1));
currentIndex++;
} else {
clearInterval(typeInterval);
setIsTyping(false);
}
}, speed);
return () => clearInterval(typeInterval);
}, delay);
return () => clearTimeout(startTyping);
}, [isInView, text, delay, speed]);
return (
<span ref={ref} className={className}>
{displayText}
<motion.span
animate={{ opacity: [1, 0] }}
transition={{ duration: 0.5, repeat: Infinity, repeatType: 'reverse' }}
className={cursorClassName}
style={{ marginLeft: '2px' }}
>
|
</motion.span>
</span>
);
}
interface FloatingElementProps {
children: ReactNode;
amplitude?: number;
duration?: number;
delay?: number;
className?: string;
}
export function FloatingElement({ children, amplitude = 10, duration = 3, delay = 0, className = '' }: FloatingElementProps) {
return (
<motion.div
animate={{
y: [-amplitude, amplitude, -amplitude],
}}
transition={{
duration,
delay,
repeat: Infinity,
ease: 'easeInOut',
}}
className={className}
>
{children}
</motion.div>
);
}
interface PulseElementProps {
children: ReactNode;
scale?: number;
duration?: number;
className?: string;
}
export function PulseElement({ children, scale = 1.05, duration = 2, className = '' }: PulseElementProps) {
return (
<motion.div
animate={{
scale: [1, scale, 1],
}}
transition={{
duration,
repeat: Infinity,
ease: 'easeInOut',
}}
className={className}
>
{children}
</motion.div>
);
}
export function InkDropSVG({ className = '' }: { className?: string }) {
return (
<motion.svg
viewBox="0 0 100 100"
className={className}
initial="hidden"
animate="visible"
>
<motion.circle
cx="50"
cy="50"
r="40"
fill="currentColor"
variants={{
hidden: { scale: 0, opacity: 0 },
visible: {
scale: [0, 1.2, 1],
opacity: [0, 1, 1],
transition: {
duration: 0.8,
ease: [0.16, 1, 0.3, 1],
},
},
}}
/>
</motion.svg>
);
}
interface GradientTextProps {
children: ReactNode;
className?: string;
colors?: string[];
duration?: number;
}
export function GradientText({
children,
className = '',
colors = ['#C41E3A', '#E04A68', '#C41E3A'],
duration = 3
}: GradientTextProps) {
return (
<motion.span
className={className}
style={{
background: `linear-gradient(90deg, ${colors.join(', ')})`,
backgroundSize: '200% 100%',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}
animate={{
backgroundPosition: ['0% 50%', '100% 50%', '0% 50%'],
}}
transition={{
duration,
repeat: Infinity,
ease: 'linear',
}}
>
{children}
</motion.span>
);
}
interface SplitTextProps {
text: string;
className?: string;
delay?: number;
staggerDelay?: number;
}
export function SplitText({ text, className = '', delay = 0, staggerDelay = 0.03 }: SplitTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: staggerDelay,
delayChildren: delay,
},
},
};
const child: Variants = {
hidden: {
opacity: 0,
y: 20,
rotateX: -90,
},
visible: {
opacity: 1,
y: 0,
rotateX: 0,
transition: {
type: 'spring' as const,
stiffness: 100,
damping: 12,
},
},
};
return (
<motion.span
ref={ref}
variants={container}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
className={className}
style={{ display: 'inline-block' }}
>
{text.split('').map((char, index) => (
<motion.span
key={index}
variants={child}
style={{ display: 'inline-block' }}
>
{char === ' ' ? '\u00A0' : char}
</motion.span>
))}
</motion.span>
);
}
interface GlitchTextProps {
text: string;
className?: string;
}
export function GlitchText({ text, className = '' }: GlitchTextProps) {
return (
<span className={`relative ${className}`}>
<span className="relative z-10">{text}</span>
<motion.span
className="absolute top-0 left-0 text-[#C41E3A] z-0"
animate={{ x: [-2, 2, -2], opacity: [0.8, 0.4, 0.8] }}
transition={{ duration: 0.3, repeat: Infinity }}
>
{text}
</motion.span>
<motion.span
className="absolute top-0 left-0 text-[#1C1C1C] z-0"
animate={{ x: [2, -2, 2], opacity: [0.8, 0.4, 0.8] }}
transition={{ duration: 0.3, repeat: Infinity, delay: 0.15 }}
>
{text}
</motion.span>
</span>
);
}
interface MagneticButtonProps {
children: ReactNode;
className?: string;
strength?: number;
onClick?: () => void;
}
export function MagneticButton({ children, className = '', strength = 0.3, onClick }: MagneticButtonProps) {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const x = (e.clientX - centerX) * strength;
const y = (e.clientY - centerY) * strength;
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
return (
<motion.div
ref={ref}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
animate={{ x: position.x, y: position.y }}
transition={{ type: 'spring', stiffness: 150, damping: 15, mass: 0.1 }}
className={className}
onClick={onClick}
style={{ display: 'inline-block' }}
>
{children}
</motion.div>
);
}
interface BlurRevealProps {
children: ReactNode;
className?: string;
delay?: number;
}
export function BlurReveal({ children, className = '', delay = 0 }: BlurRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial={{ filter: 'blur(20px)', opacity: 0, scale: 1.1 }}
animate={isInView ? { filter: 'blur(0px)', opacity: 1, scale: 1 } : {}}
transition={{ duration: 1, delay, ease: [0.16, 1, 0.3, 1] }}
className={className}
>
{children}
</motion.div>
);
}
interface WaveTextProps {
text: string;
className?: string;
delay?: number;
}
export function WaveText({ text, className = '', delay = 0 }: WaveTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: delay,
},
},
};
const child: Variants = {
hidden: { y: 0 },
visible: {
y: [0, -10, 0],
transition: {
duration: 0.5,
ease: 'easeInOut' as const,
},
},
};
return (
<motion.span
ref={ref}
variants={container}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
className={className}
style={{ display: 'inline-block' }}
>
{text.split('').map((char, index) => (
<motion.span
key={index}
variants={child}
style={{ display: 'inline-block' }}
transition={{ delay: index * 0.05 }}
>
{char === ' ' ? '\u00A0' : char}
</motion.span>
))}
</motion.span>
);
}
interface RotatingBorderProps {
children: ReactNode;
className?: string;
borderWidth?: number;
duration?: number;
colors?: string[];
}
export function RotatingBorder({
children,
className = '',
borderWidth = 2,
duration = 4,
colors = ['#C41E3A', '#1C1C1C', '#C41E3A']
}: RotatingBorderProps) {
const gradient = `conic-gradient(from 0deg, ${colors.join(', ')})`;
return (
<div className={`relative p-[${borderWidth}px] rounded-lg overflow-hidden ${className}`}>
<motion.div
className="absolute inset-0"
style={{ background: gradient }}
animate={{ rotate: 360 }}
transition={{ duration, repeat: Infinity, ease: 'linear' }}
/>
<div className="relative bg-white rounded-md">
{children}
</div>
</div>
);
}
interface ShimmerButtonProps {
children: ReactNode;
className?: string;
shimmerColor?: string;
onClick?: () => void;
}
export function ShimmerButton({ children, className = '', shimmerColor = 'rgba(255,255,255,0.3)', onClick }: ShimmerButtonProps) {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={`relative overflow-hidden ${className}`}
onClick={onClick}
>
<motion.div
className="absolute inset-0"
style={{
background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
backgroundSize: '200% 100%',
}}
animate={{
backgroundPosition: ['-200% 0', '200% 0'],
}}
transition={{
duration: 1.5,
repeat: Infinity,
repeatDelay: 2,
ease: 'linear',
}}
/>
<span className="relative z-10">{children}</span>
</motion.button>
);
}
interface InkSplashProps {
className?: string;
color?: string;
size?: number;
}
export function InkSplash({ className = '', color = '#C41E3A', size = 100 }: InkSplashProps) {
const pathVariants: Variants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
pathLength: 1,
opacity: 1,
transition: { duration: 1.5, ease: [0.16, 1, 0.3, 1] as const },
},
};
return (
<motion.svg
viewBox="0 0 100 100"
className={className}
width={size}
height={size}
initial="hidden"
animate="visible"
>
<motion.path
d="M50 10 C30 20 20 40 25 60 C30 80 50 90 50 90 C50 90 70 80 75 60 C80 40 70 20 50 10"
fill={color}
variants={pathVariants}
/>
</motion.svg>
);
}
interface CounterWithEffectProps {
end: number;
duration?: number;
prefix?: string;
suffix?: string;
className?: string;
effect?: 'bounce' | 'slide' | 'flip';
}
export function CounterWithEffect({
end,
duration = 2000,
prefix = '',
suffix = '',
className = '',
effect = 'bounce'
}: CounterWithEffectProps) {
const [count, setCount] = useState(0);
const [prevCount, setPrevCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
const hasAnimated = useRef(false);
useEffect(() => {
if (!isInView || hasAnimated.current) return;
hasAnimated.current = true;
const startTime = Date.now();
const animate = () => {
const progress = Math.min((Date.now() - startTime) / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const newCount = Math.floor(easeOutQuart * end);
if (newCount !== count) {
setPrevCount(count);
setCount(newCount);
}
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}, [isInView, end, duration, count]);
const effectVariants = {
bounce: {
initial: { y: 0 },
animate: { y: [0, -10, 0] },
transition: { duration: 0.3 },
},
slide: {
initial: { y: 20, opacity: 0 },
animate: { y: 0, opacity: 1 },
transition: { duration: 0.2 },
},
flip: {
initial: { rotateX: 90 },
animate: { rotateX: 0 },
transition: { duration: 0.2 },
},
};
return (
<motion.span
ref={ref}
key={count}
initial={effectVariants[effect].initial}
animate={effectVariants[effect].animate}
transition={effectVariants[effect].transition}
className={className}
>
{prefix}{count}{suffix}
</motion.span>
);
}