Framer Motion
Framer Motion
用于构建流畅、生产级 UI 动画的 React 动画库
版本:12.23.24
安装
安装依赖
1
npm install motion在代码中使用
1
import { motion } from "motion/react"测试使用
新建个项目
创建个组件
一个旋转的正方体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39import * as motion from "motion/react-client"
export default function MyHome() {
return (
<div style={container}>
<Rotate/>
</div>
)
}
function Rotate() {
return (
<motion.div
style={box}
animate={{ rotate: 360 }}
transition={{ duration: 1 }}
/>
)
}
const box = {
width: 100,
height: 100,
backgroundColor: "#ff0088",
borderRadius: 5,
}
const container = {
// 元素撑满整个宽度
width: "100vw",
// 元素撑满整个高度
height: "100vh",
// 弹性布局容器, 开启 flex 后才能使用对齐方式
display: "flex",
// 垂直方向居中
alignItems: "center",
// 水平方向居中
justifyContent: "center",
}修改路由配置
router.ts1
2
3import { type RouteConfig, index } from "@react-router/dev/routes";
export default [index("routes/my_home.tsx")] satisfies RouteConfig;
动画
通过<motion.HTML元素 />来使用一系列特殊的动画属性
常见动画属性
animate:传递给
animate的值发生变化时,元素将自动动画到该值1
<motion.div animate={{ rotate: 360 }} />initial:元素设置初始值
1
2
3
4<motion.article
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
/>originX/originY/originZ:设置做变换的原点,默认值是0.5
1
<motion.div style={{ originX: 0.5 }} />transition:定义动画的过渡效果
1
2
3
4<motion.div
animate={{ x: 100 }}
transition={{ ease: "easeOut", duration: 2 }}
/>通过
MotionConfig可以给多个子组件设置默认的过渡效果1
2
3<MotionConfig transition={{ duration: 0.3 }}>
<motion.div animate={{ opacity: 1 }} />
....ease的取值 效果 linear 匀速 easeIn 先慢后快 easeOut 先快后慢 easeInOut 慢 → 快 → 慢 backIn 往后缩一下再冲出去 backOut 冲过头一点再回来 backInOut 两边都带一点回弹 自定义贝塞尔曲线 ease: [0.42, 0, 0.58, 1] … 在线贝塞尔曲线网站:https://cubic-bezier.com/
exit:当组件被移除时的动画
1
2
3
4
5
6
7
8
9
10<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
动画关键帧
传入的值为数组时,会按顺序通过这些值进行动画
1 | |
注意:
数组中的值为null时表示使用当前值
1
2// null的时候为2
scale: [1, 2, 2, null, 1],times是一个介于 0和1之间的进度值数组,用于定义每个关键帧在动画中的位置
交互动画
常用的手势交互动画有:
whileHover:悬停时whileTap:按下时whileFocus:聚焦时whileDrag:拖拽时whileInView:滚动进入视口时
手势动画结束后会回到animate或initial的值
交互时过渡效果及事件处理
1 | |
拖拽
1 | |
注意点:
- 加上
drag后就可以拖动,可以指定能拖动的方向drag="x" whileDrag为拖动的时候加上的样式dragMomentum={false}可以去掉放开后的惯性dragTransition={{}}可以自定义惯性效果dragConstraints={}设置可拖拽的容器dragElastic={}可设置超出容器回弹的力度,值在[0, 1]之间- 常用到的相关事件有
onDragStart、onDrag、onDragEnd
✨定义变量
通过定义变量实现复用和方便管理
1 | |
使用变量还可以控制父子动画的间隔
1 | |
变量还可以定义为函数
1 | |
过渡
transition定义了在两个值之间动画时使用的动画类型
https://motion.dev/docs/react-transitions
过渡类型
1 | |
type可以取:
- tween:根据持续时间
duration和缓动ease来动 - spring:类似弹簧
- inertia:惯性
定义动画帧的位置
1 | |
布局动画
layout
加上layout后,DOM的大小、位置、布局变化时也会触发动画过渡效果
1 | |
如果要专门给layout设置过渡动画,可以这么做:
1 | |
注意:
可滚动的元素是加
layoutScroll1
2
3
4<motion.div
layoutScroll
style={{ overflow: "scroll" }} // 当里面的内容超过容器大小时,强制出现滚动条
/>固定的元素是加
layoutRoot1
2
3
4<motion.div
layoutRoot
style={{ position: "fixed" }} // 不受父元素影响,直接按照浏览器窗口定位
/>
layoutId
在两个不同组件之间实现动画过渡
1 | |
滚动动画
进入窗口时触发
whileInView:进入窗口时的动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function Test() {
return(
<div
style={{height: "10000px"}} // 让窗口长一点
>
<motion.div
style={{
background: "#c90b0b",
width: "100px",
height: "100px",
borderRadius: "0%",
}}
initial={{ opacity: 0 }}
whileInView={{ // 每次元素进入窗口时都会触发,出窗口会变回原来的值
opacity: 1,
borderRadius: "50%",
rotate: 360
}}
transition={{ duration: 0.5}}
>
</motion.div>
</div>
)
}viewport:设置为只会触发一次
1
2
3
4
5<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
/>
和滚动进度值关联
获取滚动的值
// scrollX:滚动的像素值,scrollXProgress:[0, 1]之间的进度值
// container:在哪滚动
// target:监听的元素,默认是监听窗口的滚动
// offset:定义什么时候为0,什么时候为1
// 如offset: [“start end”, “end start”]
// 表示当元素顶部碰到视口底部时为 0,当元素底部碰到视口顶部时为 1
const {scrollX, scrollY, scrollXProgress, scrollYProgress} = useScroll({container, target, offset})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import {motion, useScroll} from "motion/react";
// 范围为[0, 1],随着滚动的深度而增加
const { scrollYProgress } = useScroll()
// 随着滚动深度而增长的简易进度条
<motion.div
id="scroll-indicator"
style={{
scaleX: scrollYProgress, // 长度的缩放和滚动值绑定
position: "fixed",
top: 0,
left: 0,
right: 0,
height: 10,
originX: 0,
backgroundColor: "#ff0088",
}}
/>判断用户是往上还是往下滚动
1
2
3
4
5
6
7
8
9import {motion, useMotionValueEvent, useScroll} from "motion/react";
const { scrollY } = useScroll()
const [scrollDirection, setScrollDirection] = useState("down")
useMotionValueEvent(scrollY, "change", (current) => {
const diff = current - scrollY.getPrevious() // 现在的值和先前值的差
setScrollDirection(diff > 0 ? "down" : "up")
})让进度值的变动有过渡效果
1
2
3
4
5
6const { scrollYProgress } = useScroll()
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
})将进度值转化成其他值
1
2
3
4
5
6// 颜色变化是平滑变化的
const backgroundColor = useTransform(
scrollYProgress,
[0, 0.5, 1],
["#f00", "#0f0", "#00f"]
)
实例
控制滚动的速度和方向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77import {
motion,
useMotionValue,
useAnimationFrame,
useTransform, useScroll, useVelocity, useSpring
} from "framer-motion";
import {useRef, useState} from "react";
import {handle, c} from "~/styles/my_home";
import { wrap } from "@motionone/utils";
export default function MyHome() {
return (
<section style={{
background: "#3739fd",
}}>
<ParallaxText baseVelocity={-5}>Framer Motion</ParallaxText>
<ParallaxText baseVelocity={5}>Scroll velocity</ParallaxText>
</section>
)
}
function ParallaxText({ children, baseVelocity = 100 } : any){
// 文字x轴的位移值
const baseX = useMotionValue(0)
// 实现循环滚动
const x = useTransform(baseX, (v) => `${wrap(-5, -30, v)}%`)
// 获取滚动速度
const { scrollY } = useScroll()
const scrollVelocity = useVelocity(scrollY);
// 让滚动速度平滑
const smoothVelocity = useSpring(scrollVelocity, {
damping: 50,
stiffness: 400
});
// 把滚动速度映射为小的值
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
clamp: false,
})
// 运动方向
const directionFactor = useRef(1)
// 每一帧都会执行这个回调函数
useAnimationFrame((t, delta) => {
// delta: 两帧之间的间隔
let moveBy = directionFactor.current * baseVelocity * delta / 1000
// 根据滚动方向改变文字的运动方向
if(velocityFactor.get() < 0) {
directionFactor.current = -1
} else if(velocityFactor.get() > 0) {
directionFactor.current = 1
}
moveBy += directionFactor.current * moveBy * velocityFactor.get()
baseX.set(baseX.get() + moveBy)
})
return(
<div className="parallax">
<motion.div
className="scroller"
style={{
x: x,
color: "#ffffff"
}}
>
<span>{ children }</span>
<span>{ children }</span>
<span>{ children }</span>
<span>{ children }</span>
</motion.div>
</div>
)
}
矢量图动画
SVG,Scalable Vector Graphics,可缩放矢量图
https://motion.dev/docs/react-svg-animation
画矢量
用motion.circle、motion.line、motion.rect等来画矢量
支持属性pathLength来绘制线条
例
画一个圆圈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46<motion.svg // 就像定义了一块画布来画SVG
width="600"
height="600"
viewBox="0 0 600 600"
initial="hidden"
animate="visible"
style={image}
>
<motion.circle
className="circle-path"
cx="100"
cy="100"
r="80"
stroke="#ff0088"
variants={draw}
custom={1} // 延迟
style={shape}
/>
</motion.svg>
const draw: Variants = {
// 初始状态:完全透明,且路径长度为0(没画)
hidden: { pathLength: 0, opacity: 0 },
// 结束状态:接收一个参数 i (index)
visible: (i: number) => {
const delay = i * 0.5; // 核心:根据传入的 i 计算延迟时间
return {
pathLength: 1, // 画满
opacity: 1, // 变不透明
transition: {
// pathLength 使用弹簧动画(spring),时长1.5秒,没有回弹(bounce: 0)
pathLength: { delay, type: "spring", duration: 1.5, bounce: 0 },
opacity: { delay, duration: 0.01 },
},
}
},
}
// Styles
const shape: React.CSSProperties = {
strokeWidth: 10, // 线条宽度
strokeLinecap: "round", // 线条末端是圆头的
fill: "transparent", // 线条间填充是透明的,不写的话就是个实心圆了
}