Files
novalon-website/src/components/ui/scroll-animations.tsx
T
张翔 1f591fe2b4 feat: 完成网站功能开发与优化
- 完善产品页面布局与交互
- 优化服务详情页用户体验
- 增强新闻模块内容展示
- 改进团队页面设计
- 优化全局样式和响应式布局
- 添加分页组件支持
- 提升性能与SEO优化
- 修复已知问题与改进代码质量
2026-04-27 20:53:39 +08:00

444 lines
9.9 KiB
TypeScript

'use client';
import { motion, useScroll, useTransform, useSpring, type Variants, type HTMLMotionProps } from 'framer-motion';
import { useRef, type ReactNode } from 'react';
export const scrollRevealVariants: Variants = {
hidden: {
opacity: 0,
y: 50,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.8,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const inkRevealVariants: Variants = {
hidden: {
opacity: 0,
scale: 0.8,
filter: 'blur(10px)',
},
visible: {
opacity: 1,
scale: 1,
filter: 'blur(0px)',
transition: {
duration: 1,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const slideInLeftVariants: Variants = {
hidden: {
opacity: 0,
x: -50,
},
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
},
},
};
export const slideInRightVariants: Variants = {
hidden: {
opacity: 0,
x: 50,
},
visible: {
opacity: 1,
x: 0,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
},
},
};
interface ScrollRevealProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
delay?: number;
variants?: Variants;
once?: boolean;
threshold?: number;
}
export function ScrollReveal({
children,
className = '',
delay = 0,
variants: _variants = scrollRevealVariants,
once = true,
threshold = 0.1,
...props
}: ScrollRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'start center'],
});
const opacity = useTransform(scrollYProgress, [0, threshold], [0, 1]);
const y = useTransform(scrollYProgress, [0, threshold], [50, 0]);
const scale = useTransform(scrollYProgress, [0, threshold], [0.95, 1]);
const smoothOpacity = useSpring(opacity, { stiffness: 100, damping: 30 });
const smoothY = useSpring(y, { stiffness: 100, damping: 30 });
const smoothScale = useSpring(scale, { stiffness: 100, damping: 30 });
return (
<motion.div
ref={ref}
style={{
opacity: smoothOpacity,
y: smoothY,
scale: smoothScale,
}}
initial={{ opacity: 0, y: 50, scale: 0.95 }}
whileInView={{ opacity: 1, y: 0, scale: 1 }}
viewport={{ once, amount: threshold }}
transition={{
duration: 0.8,
delay,
ease: [0.16, 1, 0.3, 1],
}}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface ParallaxSectionProps extends HTMLMotionProps<'section'> {
children: ReactNode;
className?: string;
speed?: number;
}
export function ParallaxSection({
children,
className = '',
speed = 0.5,
...props
}: ParallaxSectionProps) {
const ref = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const y = useTransform(scrollYProgress, [0, 1], ['0%', `${speed * 100}%`]);
const smoothY = useSpring(y, { stiffness: 100, damping: 30 });
return (
<motion.section ref={ref} style={{ y: smoothY }} className={className} {...props}>
{children}
</motion.section>
);
}
interface ParallaxImageProps {
src: string;
alt: string;
className?: string;
speed?: number;
}
export function ParallaxImage({ src, alt, className = '', speed = 0.3 }: ParallaxImageProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const y = useTransform(scrollYProgress, [0, 1], [`${-speed * 50}%`, `${speed * 50}%`]);
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1.1, 1, 1.1]);
return (
<div ref={ref} className={`overflow-hidden ${className}`}>
<motion.img
src={src}
alt={alt}
style={{ y, scale }}
className="w-full h-full object-cover"
/>
</div>
);
}
interface ScaleOnScrollProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
minScale?: number;
maxScale?: number;
}
export function ScaleOnScroll({
children,
className = '',
minScale = 0.8,
maxScale = 1,
...props
}: ScaleOnScrollProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'center center'],
});
const scale = useTransform(scrollYProgress, [0, 1], [minScale, maxScale]);
const smoothScale = useSpring(scale, { stiffness: 100, damping: 30 });
return (
<motion.div ref={ref} style={{ scale: smoothScale }} className={className} {...props}>
{children}
</motion.div>
);
}
interface FadeOnScrollProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
direction?: 'up' | 'down' | 'left' | 'right';
distance?: number;
}
export function FadeOnScroll({
children,
className = '',
direction = 'up',
distance = 50,
...props
}: FadeOnScrollProps) {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'center center'],
});
const transformUp = useTransform(scrollYProgress, [0, 1], [distance, 0]);
const transformDown = useTransform(scrollYProgress, [0, 1], [-distance, 0]);
const transformLeft = useTransform(scrollYProgress, [0, 1], [distance, 0]);
const transformRight = useTransform(scrollYProgress, [0, 1], [-distance, 0]);
const getTransform = () => {
switch (direction) {
case 'up':
return transformUp;
case 'down':
return transformDown;
case 'left':
return transformLeft;
case 'right':
return transformRight;
default:
return transformUp;
}
};
const transform = getTransform();
const opacity = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
const smoothTransform = useSpring(transform, { stiffness: 100, damping: 30 });
const smoothOpacity = useSpring(opacity, { stiffness: 100, damping: 30 });
const style =
direction === 'left' || direction === 'right' ? { x: smoothTransform, opacity: smoothOpacity } : { y: smoothTransform, opacity: smoothOpacity };
return (
<motion.div ref={ref} style={style} className={className} {...props}>
{children}
</motion.div>
);
}
interface StaggerRevealProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
staggerDelay?: number;
containerDelay?: number;
}
export function StaggerReveal({
children,
className = '',
staggerDelay = 0.1,
containerDelay = 0,
...props
}: StaggerRevealProps) {
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: staggerDelay,
delayChildren: containerDelay,
},
},
};
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={containerVariants}
className={className}
{...props}
>
{children}
</motion.div>
);
}
interface StaggerItemProps extends HTMLMotionProps<'div'> {
children: ReactNode;
className?: string;
}
export function StaggerItem({ children, className = '', ...props }: StaggerItemProps) {
const itemVariants: Variants = {
hidden: {
opacity: 0,
y: 30,
scale: 0.95,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
},
},
};
return (
<motion.div variants={itemVariants} className={className} {...props}>
{children}
</motion.div>
);
}
interface TextRevealProps extends HTMLMotionProps<'div'> {
text: string;
className?: string;
delay?: number;
}
export function TextReveal({ text, className = '', delay = 0, ...props }: TextRevealProps) {
const words = text.split(' ');
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: delay,
},
},
};
const wordVariants: Variants = {
hidden: {
opacity: 0,
y: 20,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.16, 1, 0.3, 1],
},
},
};
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={containerVariants}
className={className}
{...props}
>
{words.map((word, index) => (
<motion.span
key={index}
variants={wordVariants}
className="inline-block mr-2"
>
{word}
</motion.span>
))}
</motion.div>
);
}
interface ProgressIndicatorProps {
className?: string;
color?: string;
}
export function ProgressIndicator({ className = '', color = 'var(--color-brand-primary)' }: ProgressIndicatorProps) {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30 });
return (
<motion.div
className={`fixed top-0 left-0 right-0 h-1 origin-left z-50 ${className}`}
style={{ scaleX, backgroundColor: color }}
/>
);
}
interface ScrollTriggeredCounterProps {
end: number;
duration?: number;
prefix?: string;
suffix?: string;
className?: string;
}
export function ScrollTriggeredCounter({
end,
duration: _duration = 2,
prefix = '',
suffix = '',
className = '',
}: ScrollTriggeredCounterProps) {
const ref = useRef<HTMLSpanElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'center center'],
});
const displayNumber = useTransform(scrollYProgress, [0, 1], [0, end]);
const rounded = useTransform(displayNumber, (latest) => Math.round(latest));
return (
<motion.span ref={ref} className={className}>
{prefix}
<motion.span>{rounded}</motion.span>
{suffix}
</motion.span>
);
}