[3D-portfolio] 3D画布组件 | <Canvas> | Framer Motion | 预定义动画序列
无论是3D效果,还是动画效果,都是调用第三方库来实现的。当做软件需求当中想要实现某一个效果,就可以开始查询需要用到的库,然后根据库的文档来进行学习,就可以尝试调用使用了。
对于库的调用和安排就需要涉及到一些基本的设计思想,像下面的动画渲染实现自动更新的功能,就是用到了分为两部分的解耦设计,动画定义(motion.js
)与应用(variants
属性)分离
第三章:3D画布组件
欢迎回来!
在上一章《React组件》中,我们学习了标准React组件
如何构建网站用户界面的2D部分
——包括文本、按钮、导航栏和列表等元素。
但那些令人惊叹的3D元素呢?
悬浮的计算机、旋转的地球仪、科技小球?仅靠标准React组件无法创建这些交互式三维视觉效果。
为此我们需要一种特殊组件:3D画布组件。
3D画布组件解决了什么问题?
想象构建传统网站就像布置静态展览——你在墙上排列平面图片和文字。
当要在展览中添加动态三维雕塑时,仅仅有墙面是不够的
;你需要专用空间,可能需要搭建舞台或平台来展示、打光雕塑,甚至允许互动
。
在网页上创建3D图形也是类似的。标准HTML和React组件专为在平面页面上排列2D元素设计。
要渲染复杂3D模型和场景,我们需要网页中的专用"舞台"或"画布"。
这就是3D画布组件的用途。它们是通过强大库来创建和管理这些3D"舞台"的专用组件,为传统网页元素增添动态视觉层级
。
核心工具:React Three Fiber
和Drei
在React应用中处理3D图形需要以下两个主要工具:
- React Three Fiber (
@react-three/fiber
或r3f
):这是用React构建3D场景的关键桥梁。我们可以使用熟悉的React组件语法(<mesh>
,<light>
,<camera>
等)描述3D世界,无需直接编写复杂3D代码。就像用React语言编写3D舞台的脚本。- 例如后面我们用到的
<Canvas>
:来自@react-three/fiber
,创建HTML<canvas>
并在其中初始化Three.js
场景,作为3D对象的"舞台"
- 例如后面我们用到的
- Drei (
@react-three/drei
):这个辅助工具集构建在React Three Fiber之上,提供3D场景常用组件如相机控制(<OrbitControls>
)、模型加载器(useGLTF
)、实用形状等,通过预制工具节省开发时间。
3D画布组件工作原理
站点的每个3D视觉元素(计算机、地球仪、科技小球、星空)都由专用3D画布
组件渲染。
这些组件通常位于src/components/canvas
目录。
以ComputersCanvas.jsx
为例(简化版)解析核心结构:
// src/components/canvas/Computers.jsx (简化版)
import React, { Suspense, useEffect, useState } from "react";
import { Canvas } from "@react-three/fiber"; // 导入Canvas组件!
import { OrbitControls, Preload, useGLTF } from "@react-three/drei";
import CanvasLoader from "../Loader"; // 加载指示器// 实际3D模型和场景设置
const Computers = ({ isMobile }) => {const computer = useGLTF("./desktop_pc/scene.gltf"); // 加载模型文件return (<mesh> {/* 3D对象容器 */}<hemisphereLight intensity={0.15} groundColor="black" /> {/* 环境光 */}<pointLight intensity={1} /> {/* 点光源 */}<primitive // 加载的3D模型object={computer.scene}scale={isMobile ? 0.65 : 0.75}position={isMobile ? [0, -3, -2.2] : [0, -3.25, -1.5]}rotation={[-0.01, -0.2, -0.1]}/></mesh>);
};// 3D舞台设置组件
const ComputersCanvas = () => {const [isMobile, setIsMobile] = useState(false);useEffect(() => { /* 设备检测逻辑 */ }, []);return (<Canvas // React Three Fiber提供的3D舞台frameloop="demand" // 按需渲染shadows // 启用阴影camera={{ position: [20, 3, 5], fov: 25 }} // 相机参数gl={{ preserveDrawingBuffer: true }} // WebGL设置><Suspense fallback={<CanvasLoader />}> {/* 加载过渡 */}<OrbitControls // 轨道控制器enableZoom={false}maxPolarAngle={Math.PI / 2}minPolarAngle={Math.PI / 2}/><Computers isMobile={isMobile} /> {/* 3D内容 */}</Suspense><Preload all /> {/* 资源预加载 */}</Canvas>);
};export default ComputersCanvas;
关键组件解析:
<Canvas>
:来自@react-three/fiber
,创建HTML<canvas>
并在其中初始化Three.js
场景,作为3D对象的"舞台"Suspense
与CanvasLoader
:处理3D模型加载时的过渡状态OrbitControls
:来自Drei
的相机轨道控制器Computers
组件:定义场景内容,使用useGLTF
加载模型文件
3D画布组件应用实例
案例1:主视觉计算机
src/components/Hero.jsx
中简单引入:
import { ComputersCanvas } from "./canvas";
//...
<section className="relative w-full h-screen mx-auto"><ComputersCanvas /> {/* 3D组件插入 */}
</section>
案例2:联系页地球仪
src/components/Contact.jsx
布局:
import { EarthCanvas } from "./canvas";
//...
<motion.div className="xl:flex-1 xl:h-auto md:h-[550px] h-[350px]"><EarthCanvas /> {/* 地球组件 */}
</motion.div>
案例3:技术栈小球
src/components/Tech.jsx
动态生成:
import { BallCanvas } from "./canvas";
//...
{technologies.map((tech) => (<div key={tech.name}><BallCanvas icon={tech.icon} /> {/* 带图标的小球 */}</div>
))}
技术实现流程
常见的第三方库转化逻辑:初始化 定义控制器 转化 渲染
核心3D组件清单
组件 | 文件路径 | 渲染内容 | 使用位置 | 特性 |
---|---|---|---|---|
ComputersCanvas | canvas /Computers.jsx | 悬浮计算机 | Hero.jsx | 响应式布局,轨道控制 |
EarthCanvas | canvas/Earth.jsx | 旋转地球仪 | Contact.jsx | 自动旋转,轨道控制 |
BallCanvas | canvas /Ball.jsx | 带图标小球 | Tech.jsx | 使用Drei贴图 技术 |
StarsCanvas | canvas/Stars.jsx | 星空背景 | App.jsx | 基础几何体与粒子系统 |
所有组件通过src/components/canvas/index.js
统一导出。
结语
3D-Portfolio
项目中3D视觉效果的实现奥秘。
3D画布组件通过**React Three Fiber
和Drei
**创建专用3D舞台,封装了相机、光源、模型与控制器的复杂配置
让我们能用React组件化思维整合动态3D图形。
下一章我们将探索如何通过动画工具(Framer Motion)为2D元素添加流畅动效。
第四章:动画工具(Framer Motion)
在上一章《3D画布组件
》中,我们深入探讨了项目如何通过React Three Fiber
等专用组件和库创建酷炫的3D视觉效果。
现在让我们将注意力转回网站的传统2D部分——滚动时看到的文本、图像和版块。
为了让作品集呈现更生动的交互体验,这些元素不会突然出现,而是通过平滑的入场动画展现。
这正是**动画工具(Framer Motion)**的用武之地。
动画工具解决的问题
想象观看一场没有过渡效果的幻灯片演示,突兀的切换会削弱观众体验。
同理,当网页元素随着滚动突然出现时,也会显得呆板生硬。
动画通过添加平滑过渡
和视觉修饰
能:
- 引导用户关注重点内容
- 增强网站的现代感和动态感
- 通过视觉反馈提升用户体验
本项目通过**预定义动画序列**替代重复编写复杂动画代码,这些序列如同为网页元素编排好的"
舞蹈动作
"。
动画编排指南:src/utils/motion.js
与Framer Motion
本项目的"动画编排师"是Framer Motion——一个流行的React动画库
而"编舞指南"则是位于src/utils/motion.js
的文件。
该文件包含通过Framer Motion概念定义常见动画模式的JavaScript
函数,这些模式称为 “变体(variants)”。
核心概念:motion
组件与variants
Framer Motion通过为HTML元素添加motion.
前缀(如<motion.div>
、<motion.p>
、<motion.img>
)将其转换为动态组件。
这些motion
组件具备动画能力,通过variants
属性控制动画效果:
motion
组件:标准元素的动画版本,如同舞台上的"舞者"variants
:定义动画"状态"的JavaScript对象,通常包含hidden
(动画前状态)和show
(动画后状态)。- (Everything is state machine)
src/utils/motion.js
中的工具函数帮助我们轻松创建这些对象
动画工具使用示例
以src/components/About.jsx
的"About"版块为例:
首先导入所需模块:
// src/components/About.jsx(代码片段)
import { motion } from "framer-motion"; // 导入Framer Motion
import { fadeIn, textVariant } from "../utils/motion"; // 导入动画工具
// ... 其他导入
为需要动画的元素包裹motion.
组件并添加variants
属性:
// src/components/About.jsx(代码片段 - About组件内部)
return (<>{/* 标题/副标题容器动画 */}<motion.div variants={textVariant()}> {/* 使用motion.div */}<p className={styles.sectionSubText}>简介</p><h2 className={styles.sectionHeadText}>概述.</h2></motion.div>{/* "关于我"段落动画 */}<motion.pvariants={fadeIn("", "", 0.1, 1)} // 应用fadeIn变体className="mt-4 text-secondary text-[17px] max-w-3xl leading-[30px]">{personalInfo.about} {/* 来自constants的内容(第一章) */}</motion.p>{/* ... ServiceCard元素(如下所示)... */}</>
);
在此示例中:
- 包含"简介"和"概述"的
div
改用<motion.div>
- 通过
variants
属性应用从src/utils/motion.js
导入的textVariant()
工具函数 - "关于我"段落改用
<motion.p>
并应用fadeIn
变体
这些
variants
对象定义动画状态,而动画触发时机通常由initial
/animate
或whileInView
/viewport
等属性控制。
在本项目中,动画常在元素滚动至视口时触发,这由SectionWrapper
(下章详述)自动处理,原理是在主容器设置initial="hidden"
和whileInView="show"
,Framer Motion会将"show"状态传播给子motion
组件。
观察About.jsx
中的ServiceCard
组件:
// src/components/About.jsx(代码片段 - ServiceCard组件)
const ServiceCard = ({ index, title, icon }) => {return (<Tilt /* ... Tilt参数 */>{/* 卡片容器动画 */}<motion.divvariants={fadeIn("right", "spring", index * 0.5, 0.75)} // 应用渐变变体className="w-full green-pink-gradient p-[1px] rounded-[20px] shadow-card">{/* ... 卡片内容(图标、标题) */}</motion.div></Tilt>);
};
此处每个ServiceCard
的主div
都是motion.div
variants
应用带index * 0.5
延迟的fadeIn
变体,创建交错动画效果,使卡片依次渐入呈现波浪效果。
其他组件的应用模式类似:
-
Contact.jsx
:通过motion.div
配合slideIn("left")
和slideIn("right")
实现表单与地球模型的左右滑动入场// src/components/Contact.jsx(代码片段) import { motion } from "framer-motion"; import { slideIn } from "../utils/motion"; // ...<motion.divvariants={slideIn("left", "tween", 0.2, 1)} // 左侧滑入className="relative flex-[0.75] bg-black-100 p-8 rounded-2xl" >{/* ... 表单内容 ... */} </motion.div><motion.divvariants={slideIn("right", "tween", 0.2, 1)} // 右侧滑入className="xl:flex-1 xl:h-auto md:h-[550px] h-[350px]" ><EarthCanvas /> {/* 3D地球组件 */} </motion.div>
-
Works.jsx
:ProjectCard
组件使用带index
延时的fadeIn("up")
实现项目卡片弹性上浮// src/components/Works.jsx(代码片段 - ProjectCard组件) import { motion } from "framer-motion"; import { fadeIn } from "../utils/motion"; // ...const ProjectCard = ({ index, /* ... 属性 */ }) => {return (<motion.div variants={fadeIn("up", "spring", index * 0.5, 0.75)}> {/* 弹性上浮 */}<Tilt /* ... Tilt参数 */>{/* ... 项目卡片内容 ... */}</Tilt></motion.div>); };
底层原理:src/utils/motion.js
解析
src/utils/motion.js
包含返回Framer Motion可识别variants
对象的JavaScript函数。
查看简化版的fadeIn
函数:
// src/utils/motion.js(简化代码片段)export const fadeIn = (direction, type, delay, duration) => {return {hidden: { // 初始状态(动画前)x: direction === "left" ? 100 : direction === "right" ? -100 : 0, // X轴偏移y: direction === "up" ? 100 : direction === "down" ? -100 : 0, // Y轴偏移opacity: 0, // 完全透明},show: { // 结束状态(动画后)x: 0, // 回归原始水平位置y: 0, // 回归原始垂直位置opacity: 1, // 完全可见transition: { // 定义动画过渡type: type, // 动画类型(如"spring"弹性、"tween"补间)delay: delay, // 延迟时间duration: duration, // 持续时间ease: "easeOut", // 缓动函数},},};
};// ... 其他变体函数(textVariant、slideIn、zoomIn、staggerContainer)
fadeIn
函数接收参数并返回包含hidden
和show
属性的对象:
hidden
:定义初始状态(如透明度0,位置偏移)show
:定义结束状态(如完全可见,位置归零)transition
:配置动画类型、延迟、持续时间和缓动函数
当<motion.div>
应用variants={fadeIn(...)}
时,Framer Motion读取该对象。
若父组件或视口触发"show"状态,库会自动将CSS属性从hidden
值渐变至show
值。
其他函数如textVariant
、slideIn
等以类似方式运作,实现不同动画效果。
整体运作流程
- 组件渲染:React组件(如
About.jsx
)渲染包含motion
组件的JSX - 变体定义:
motion
组件通过variants
属性调用工具函数获取动画定义 - 库接收配置:Framer Motion接收变体对象及
initial
/animate
等属性 - 初始状态应用:将
hidden
状态样式应用到DOM元素 - 动画触发:根据触发条件(如进入视口)启动过渡
- 样式渐变:库计算中间状态并应用渐变
- 浏览器更新:动态更新显示效果
此架构将动画定义(motion.js
)与应用(variants
属性)分离,提升复用性与可维护性。
总结
本章探讨了**动画工具(Framer Motion)**的应用。
我们了解到src/utils/motion.js
通过Framer Motion库提供预设动画模式,这些"变体"通过motion
组件的variants
属性应用,为作品集的2D元素添加渐入、滑动、弹性等动态效果。
工具函数返回的hidden
/show
状态与transition
配置,使Framer Motion能自动完成元素状态的平滑过渡。
接下来我们将探讨如何通过版块包装高阶组件(Section Wrapper HOC)实现样式统一与动画自动触发。
版块包装高阶组件
如果想要实现一个实时的效果,就可以借助这个架构:
动画定义(motion.js
)与应用(variants
属性)分离,提升复用性与可维护性。