所有分类
  • 所有分类
  • Html5资源
  • React资源
  • Vue资源
  • Php资源
  • ‌小程序资源
  • Python资源

motion —— 优雅的页面过渡效果效果

优雅的页面过渡效果组件

基于 React 和 Framer Motion 打造的现代化页面过渡效果组件,提供丰富的动画效果,让您的页面切换更加流畅自然。

源码

PageScroll.jsx

import { useRef, useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { transitions } from "./transitions";

// 过渡效果选择器组件
function TransitionSelector({ currentTransition, onTransitionChange }) {
  return (
    <div className="fixed left-8 top-8 z-50">
      <select
        value={currentTransition}
        onChange={(e) => onTransitionChange(e.target.value)}
        className="bg-white/10 text-white border border-white/20 rounded-lg px-4 py-2 backdrop-blur-sm"
      >
        <option value="fade">渐隐渐现</option>
        <option value="radial">径向展开</option>
        <option value="fold">折叠效果</option>
        <option value="spiral">螺旋展开</option>
        <option value="chess">棋盘格</option>
        <option value="blindsH">水平百叶窗</option>
        <option value="windmill">风车旋转</option>
        <option value="split">双向展开</option>
        <option value="push">推入效果</option>
        <option value="reveal">揭开效果</option>
        <option value="cylinder">圆筒翻转</option>
        <option value="rubikHorizontal">水平魔方</option>
      </select>
    </div>
  );
}

// Pagination 组件
function Pagination({ total, current, onPageChange }) {
  return (
    <div className="fixed right-8 top-1/2 transform -translate-y-1/2 z-50">
      <div className="flex flex-col gap-3">
        {Array.from({ length: total }, (_, i) => (
          <motion.button
            key={i}
            onClick={() => onPageChange(i)}
            className={`w-3 h-3 rounded-full transition-all duration-300 ${
              current === i
                ? "bg-white scale-125"
                : "bg-white/50 hover:bg-white/80"
            }`}
            whileHover={{ scale: 1.2 }}
            whileTap={{ scale: 0.9 }}
            initial={{ opacity: 0, x: 20 }}
            animate={{ opacity: 1, x: 0 }}
            transition={{ delay: i * 0.1 }}
          />
        ))}
      </div>
    </div>
  );
}

export default function PageScroll() {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [currentTransition, setCurrentTransition] = useState("fade");
  const containerRef = useRef(null);

  const sections = [
    {
      color: "bg-red-500",
      title: "Red Section",
      desc: "A vibrant red section with dynamic animations",
    },
    {
      color: "bg-orange-500",
      title: "Orange Section",
      desc: "Warm orange tones with smooth transitions",
    },
    {
      color: "bg-yellow-500",
      title: "Yellow Section",
      desc: "Bright yellow energy with playful movements",
    },
    {
      color: "bg-green-500",
      title: "Green Section",
      desc: "Fresh green space with natural flow",
    },
    {
      color: "bg-blue-500",
      title: "Blue Section",
      desc: "Calm blue atmosphere with gentle animations",
    },
    {
      color: "bg-indigo-500",
      title: "Indigo Section",
      desc: "Deep indigo depth with smooth transitions",
    },
    {
      color: "bg-purple-500",
      title: "Purple Section",
      desc: "Rich purple elegance with dynamic effects",
    },
  ];

  useEffect(() => {
    let isAnimating = false;
    let scrollTimeout;
    const animationDuration = 800; // 动画时间
    const scrollThreshold = 50; // 滚动阈值
    let accumulatedDelta = 0; // 累积的滚动值

    const handleWheel = (e) => {
      e.preventDefault();

      if (isAnimating) return; // 如果正在动画中,忽略滚动事件

      // 累积滚动值
      accumulatedDelta += e.deltaY;

      // 清除之前的定时器
      clearTimeout(scrollTimeout);

      // 设置新的定时器,在短暂延迟后处理滚动
      scrollTimeout = setTimeout(() => {
        if (Math.abs(accumulatedDelta) >= scrollThreshold) {
          isAnimating = true;

          // 根据累积的滚动值决定方向
          if (accumulatedDelta > 0 && currentIndex < sections.length - 1) {
            setCurrentIndex((prev) => prev + 1);
          } else if (accumulatedDelta < 0 && currentIndex > 0) {
            setCurrentIndex((prev) => prev - 1);
          }

          // 重置累积值
          accumulatedDelta = 0;

          // 动画结束后解除锁定
          setTimeout(() => {
            isAnimating = false;
          }, animationDuration);
        }
      }, 50); // 50ms的防抖延迟
    };

    const container = containerRef.current;
    if (container) {
      container.addEventListener("wheel", handleWheel, { passive: false });
      return () => {
        container.removeEventListener("wheel", handleWheel);
        clearTimeout(scrollTimeout);
      };
    }
  }, [currentIndex, sections.length]);

  return (
    <div ref={containerRef} className="h-screen overflow-hidden">
      <TransitionSelector
        currentTransition={currentTransition}
        onTransitionChange={setCurrentTransition}
      />
      <Pagination
        total={sections.length}
        current={currentIndex}
        onPageChange={setCurrentIndex}
      />
      <AnimatePresence mode="popLayout">
        <Section
          key={currentIndex}
          section={sections[currentIndex]}
          transition={transitions[currentTransition]}
        />
      </AnimatePresence>
    </div>
  );
}

function Section({
  section = {
    color: "bg-gray-900",
    title: "",
    desc: "",
  },
  transition,
}) {
  // 打字机效果的变体
  const titleVariants = {
    initial: { opacity: 0 },
    animate: {
      opacity: 1,
      transition: {
        staggerChildren: 0.05,
      },
    },
    exit: {
      opacity: 0,
      transition: {
        duration: 0.5,
      },
    },
  };

  const letterVariants = {
    initial: { opacity: 0, y: 20 },
    animate: {
      opacity: 1,
      y: 0,
    },
  };

  // 描述文本的变体
  const descVariants = {
    initial: { opacity: 0, y: 50 },
    animate: {
      opacity: 1,
      y: 0,
      transition: {
        duration: 0.8,
        ease: "easeOut",
        delay: 0.5,
      },
    },
    exit: {
      opacity: 0,
      y: -50,
      transition: {
        duration: 0.5,
      },
    },
  };

  return (
    <motion.div
      className={`${section.color} h-screen w-full flex items-center justify-center absolute inset-0`}
      {...transition}
    >
      <div className="text-center px-4">
        <motion.div
          variants={titleVariants}
          initial="initial"
          animate="animate"
          exit="exit"
        >
          <motion.h2 className="text-6xl font-bold text-white mb-6 overflow-hidden">
            {section.title.split("").map((char, index) => (
              <motion.span
                key={index}
                variants={letterVariants}
                style={{ display: "inline-block" }}
              >
                {char}
              </motion.span>
            ))}
          </motion.h2>
        </motion.div>
        <motion.p
          className="text-xl text-white/80 max-w-2xl mx-auto"
          variants={descVariants}
          initial="initial"
          animate="animate"
          exit="exit"
        >
          {section.desc}
        </motion.p>
      </div>
    </motion.div>
  );
}

transitions.js 特效配置

export const transitions = {
  // 渐隐渐现效果
  fade: {
    initial: { opacity: 0 },
    animate: { opacity: 1 },
    exit: { opacity: 0, position: "absolute" },
    transition: { duration: 0.3, ease: "easeInOut" },
  },
  // 径向展开效果
  radial: {
    initial: {
      opacity: 0,
      clipPath: "circle(0% at 50% 50%)",
    },
    animate: {
      opacity: 1,
      clipPath: "circle(100% at 50% 50%)",
    },
    exit: {
      opacity: 0,
      clipPath: "circle(0% at 50% 50%)",
    },
    transition: { duration: 0.8, ease: "easeInOut" },
  },

  // 推入效果
  push: {
    initial: { x: "100%", position: "absolute" },
    animate: { x: 0, position: "absolute" },
    exit: { x: "-100%", position: "absolute" },
    transition: {
      duration: 0.4,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 揭开效果
  reveal: {
    initial: {
      opacity: 0,
      clipPath: "inset(0 50% 0 50%)",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      clipPath: "inset(0 0% 0 0%)",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      clipPath: "inset(0 0% 0 100%)",
      position: "absolute",
    },
    transition: {
      duration: 0.45,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 折叠效果
  fold: {
    initial: {
      opacity: 0,
      clipPath: "polygon(50% 0%, 50% 0%, 50% 100%, 50% 100%)",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      clipPath: "polygon(50% 0%, 50% 0%, 50% 100%, 50% 100%)",
      position: "absolute",
    },
    transition: {
      duration: 0.4,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 螺旋展开
  spiral: {
    initial: {
      opacity: 0,
      scale: 0,
      rotate: -180,
      position: "absolute",
    },
    animate: {
      opacity: 1,
      scale: 1,
      rotate: 0,
      position: "absolute",
    },
    exit: {
      opacity: 0,
      scale: 0,
      rotate: 180,
      position: "absolute",
    },
    transition: {
      duration: 0.5,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 棋盘格
  chess: {
    initial: {
      opacity: 0,
      clipPath: "inset(0 0 100% 100%)",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      clipPath: "inset(0 0 0 0)",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      clipPath: "inset(100% 100% 0 0)",
      position: "absolute",
    },
    transition: {
      duration: 0.4,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 百叶窗水平
  blindsH: {
    initial: {
      opacity: 0,
      clipPath: "inset(0 100% 0 0)",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      clipPath: "inset(0 0 0 0)",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      clipPath: "inset(0 0 0 100%)",
      position: "absolute",
    },
    transition: {
      duration: 0.4,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 风车旋转
  windmill: {
    initial: {
      opacity: 0,
      clipPath: "polygon(50% 50%, 50% 0, 50% 0, 50% 50%)",
      rotate: -180,
      position: "absolute",
    },
    animate: {
      opacity: 1,
      clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)",
      rotate: 0,
      position: "absolute",
    },
    exit: {
      opacity: 0,
      clipPath: "polygon(50% 50%, 100% 50%, 100% 50%, 50% 50%)",
      rotate: 180,
      position: "absolute",
    },
    transition: {
      duration: 0.6,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 双向展开
  split: {
    initial: {
      opacity: 0,
      clipPath: "inset(50% 50% 50% 50%)",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      clipPath: "inset(0 0 0 0)",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      clipPath: "inset(0 50% 0 50%)",
      position: "absolute",
    },
    transition: {
      duration: 0.45,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 圆筒翻转效果
  cylinder: {
    initial: {
      opacity: 0,
      transform: [
        "perspective(2000px)",
        "rotateX(-90deg)",
        "translateY(100%)",
      ].join(" "),
      transformStyle: "preserve-3d",
      transformOrigin: "bottom center",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      transform: ["perspective(2000px)", "rotateX(0deg)", "translateY(0)"].join(
        " "
      ),
      transformStyle: "preserve-3d",
      transformOrigin: "center",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      transform: [
        "perspective(2000px)",
        "rotateX(90deg)",
        "translateY(-100%)",
      ].join(" "),
      transformStyle: "preserve-3d",
      transformOrigin: "top center",
      position: "absolute",
    },
    transition: {
      duration: 0.8,
      ease: [0.4, 0, 0.2, 1],
    },
  },

  // 水平魔方翻转
  rubikHorizontal: {
    initial: {
      opacity: 0,
      transform: [
        "perspective(2000px)",
        "rotateY(-90deg)",
        "translateX(-100%)",
        "translateZ(-200px)",
      ].join(" "),
      transformStyle: "preserve-3d",
      transformOrigin: "left center",
      position: "absolute",
    },
    animate: {
      opacity: 1,
      transform: [
        "perspective(2000px)",
        "rotateY(0deg)",
        "translateX(0)",
        "translateZ(0)",
      ].join(" "),
      transformStyle: "preserve-3d",
      transformOrigin: "center",
      position: "absolute",
    },
    exit: {
      opacity: 0,
      transform: [
        "perspective(2000px)",
        "rotateY(90deg)",
        "translateX(100%)",
        "translateZ(-200px)",
      ].join(" "),
      transformStyle: "preserve-3d",
      transformOrigin: "right center",
      position: "absolute",
    },
    transition: {
      duration: 1,
      ease: [0.4, 0, 0.2, 1],
    },
  },
};

✨ 特性

  • 🎨 丰富的过渡效果
  • 🚀 流畅的动画表现
  • 📱 响应式设计
  • 🛠️ 易于集成
  • ⚙️ 高度可定制
  • 🎯 直观的 API

🎬 内置过渡效果

基础效果

  • 渐隐渐现:简约而优雅的透明度过渡
  • 径向展开:从中心点扩散的圆形展开效果
  • 折叠效果:类似折纸的立体折叠动画
  • 螺旋展开:旋转缩放的螺旋动画
  • 棋盘格:方块矩阵的错落过渡
  • 水平百叶窗:横向百叶窗切换效果
  • 风车旋转:中心旋转的风车动画
  • 双向展开:水平或垂直方向的展开效果

3D 效果

  • 推入效果:3D 空间的推入动画
  • 揭开效果:立体的揭开翻转效果
  • 圆筒翻转:圆柱形的立体翻转
  • 水平魔方:经典的魔方旋转效果

🎨 自定义过渡效果

每个过渡效果都可以通过以下属性进行自定义:

{
  initial: {}, // 初始状态
  animate: {}, // 动画状态
  exit: {},    // 退出状态
  transition: {
    duration: 0.8,
    ease: [0.4, 0, 0.2, 1]
  }
}

🛠️ 技术栈

  • React
  • Framer Motion
  • Tailwind CSS
  • TypeScript
原文链接:https://code.ifrontend.net/archives/415,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?