源码 · page.tsx

// page.tsx
import BlurTextFramer from '../components/BlurText-Framer'
import BlurTextGsap from '../components/BlurText-Gsap'

const BlurTextPage = () => {
  return (
    <div className="relative flex h-full flex-col gap-4">
      {}
      

      {/* Framer Motion 卡片 */}
      <div className="relative flex flex-1 flex-col justify-center rounded border border-gray-700 bg-gray-900/50 p-6">
        <h3 className="absolute top-10 left-10 mb-2 text-sm font-medium text-gray-300">Framer Motion</h3>

        <BlurTextFramer
          text="seize the day"
          delay={150}
          animateBy="words"
          direction="top"
          // onAnimationComplete={() => {}}
          className="inline-block text-center text-4xl font-bold"
        />
      </div>

      {/* GSAP 卡片 */}
      <div className="relative flex flex-1 flex-col justify-center rounded border border-gray-700 bg-gray-900/50 p-6">
        <h3 className="absolute top-10 left-10 mb-2 text-sm font-medium text-gray-300">GSAP</h3>

        <BlurTextGsap
          text="seize the day"
          delay={50}
          animateBy="words"
          direction="top"
          ease="none"
          className="text-center text-4xl font-bold"
        />
      </div>
    </div>
  )
}

export default BlurTextPage

// BlurText-Framer.tsx
'use client'

import { motion } from 'framer-motion'
import { useRef, useMemo } from 'react'

// 定义动画状态的类型
interface AnimationState {
  filter?: string
  opacity?: number
  y?: number
  [key: string]: any
}

// 定义组件props的类型
interface BlurTextProps {
  text?: string
  delay?: number
  className?: string
  animateBy?: 'words' | 'characters'
  direction?: 'top' | 'bottom'
  threshold?: number
  rootMargin?: string
  animationFrom?: AnimationState
  animationTo?: AnimationState[]
  easing?: (t: number) => number
  onAnimationComplete?: () => void
  stepDuration?: number
}

const buildKeyframes = (from: AnimationState, steps: AnimationState[]) => {
  const keys = new Set([...Object.keys(from), ...steps.flatMap((s) => Object.keys(s))])

  const keyframes: Record<string, any[]> = {}
  keys.forEach((k) => {
    keyframes[k] = [from[k], ...steps.map((s) => s[k])]
  })
  return keyframes
}

const BlurTextFramer = ({
  text = '',
  delay = 200,
  className = '',
  animateBy = 'words',
  direction = 'top',
  threshold = 0.1,
  rootMargin = '0px',
  animationFrom,
  animationTo,
  easing = (t: number) => t,
  onAnimationComplete,
  stepDuration = 0.35
}: BlurTextProps) => {
  const elements = animateBy === 'words' ? text.split(' ') : text.split('')
  const ref = useRef<HTMLParagraphElement>(null)

  const defaultFrom = useMemo(
    () =>
      direction === 'top' ? { filter: 'blur(10px)', opacity: 0, y: -50 } : { filter: 'blur(10px)', opacity: 0, y: 50 },
    [direction]
  )

  const defaultTo = useMemo(
    () => [
      {
        filter: 'blur(5px)',
        opacity: 0.5,
        y: direction === 'top' ? 5 : -5
      },
      { filter: 'blur(0px)', opacity: 1, y: 0 }
    ],
    [direction]
  )

  const fromSnapshot = animationFrom ?? defaultFrom
  const toSnapshots = animationTo ?? defaultTo

  // 3
  const stepCount = toSnapshots.length + 1

  // 0.7
  const totalDuration = stepDuration * (stepCount - 1)

  // 均匀分布动画
  const times = Array.from({ length: stepCount }, (_, i) => (stepCount === 1 ? 0 : i / (stepCount - 1)))

  return (
    <p ref={ref} className={`blur-text ${className} flex flex-wrap`}>
      {elements.map((segment, index) => {
        const animateKeyframes = buildKeyframes(fromSnapshot, toSnapshots)

        const spanTransition = {
          duration: totalDuration,
          times,
          delay: (index * delay) / 1000,
          ease: easing
        }

        return (
          <motion.span
            className="inline-block will-change-[transform,filter,opacity]"
            key={index}
            initial={fromSnapshot}
            animate={animateKeyframes}
            transition={spanTransition}
            onAnimationComplete={index === elements.length - 1 ? onAnimationComplete : undefined}
          >
            {segment === ' ' ? '\u00A0' : segment}
            {animateBy === 'words' && index < elements.length - 1 && '\u00A0'}
          </motion.span>
        )
      })}
    </p>
  )
}

export default BlurTextFramer


// BlurText-Gsap.tsx
'use client'

import React, { useRef } from 'react'
import { gsap } from 'gsap'
import { useGSAP } from '@gsap/react'
import { SplitText } from 'gsap/SplitText'

// 注册SplitText插件
gsap.registerPlugin(SplitText)

interface BlurTextGsapProps {
  text?: string
  children?: React.ReactNode
  delay?: number
  className?: string
  animateBy?: 'words' | 'chars'
  direction?: 'top' | 'bottom'
  onAnimationComplete?: () => void
  ease?: string
}

const BlurTextGsap = ({
  text = '',
  children,
  delay = 50, // 默认更快的delay,像React Spring版本
  className = '',
  animateBy = 'words',
  direction = 'top',
  onAnimationComplete,
  ease = 'none' // 使用linear ease让动画更匀速
}: BlurTextGsapProps) => {
  const containerRef = useRef<HTMLParagraphElement>(null)
  const splitRef = useRef<SplitText | null>(null)

  useGSAP(() => {
    if (!containerRef.current) return

    // 使用SplitText拆分文字
    splitRef.current = new SplitText(containerRef.current, {
      type: animateBy
    })

    const elements = splitRef.current[animateBy] as HTMLElement[]

    // 设置初始状态 - 更接近React Spring版本
    gsap.set(elements, {
      filter: 'blur(10px)',
      opacity: 0,
      y: 0, // 移除y轴移动,更专注于模糊效果
      willChange: 'transform, filter, opacity'
    })

    // 创建时间线动画
    const tl = gsap.timeline({
      onComplete: onAnimationComplete
    })

    // 第一阶段:从完全模糊到半模糊
    tl.to(elements, {
      filter: 'blur(5px)',
      opacity: 0.5,
      y: 0,
      stagger: {
        amount: (elements.length * delay) / 1000, // 使用amount而不是each来控制总时间
        ease: 'none' // stagger也使用linear
      },
      ease: ease
    }).to(elements, {
      filter: 'blur(0px)',
      opacity: 1,
      stagger: {
        amount: (elements.length * delay) / 1000,
        ease: 'none'
      },
      ease: ease
    })

    return () => {
      // 清理SplitText实例
      splitRef.current?.revert()
    }
  }, [text, children, delay, animateBy, direction, onAnimationComplete, ease])

  return (
    <p ref={containerRef} className={`${className}`}>
      {children || text}
    </p>
  )
}

export default BlurTextGsap

Framer Motion

seize the day

GSAP

seize the day