Appearance
创建项目
先创建React项目,Vite或者CRA都可以,下面安装 gsap
:
bash
pnpm i gsap
使用:
jsx
import React from 'react'
import { gsap } from 'gsap'
export default function App() {
return (
<div className="app">
<div className="box">Hello</div>
</div>
)
}
在useEffect()中创建动画
大多数时候,你应该将动画放在React的 useEffect()
中,因为它在DOM渲染之后运行。没有目标DOM,我们元素还不存在。下面是通用结构:
jsx
const comp = useRef()
useEffect(() => {
// 这里写动画代码
return () => {
// 清理工作(可选)
}
}, []) // 添加空依赖,避免每次渲染都执行
目标元素... Refs?
为了动画,我们需要告诉GSAP目标元素是哪一个。React不推荐使用选择器的方式,而是使用 Refs 的方式。
jsx
const boxRef = useRef()
return (
<div className="app">
<div className="box" ref={boxRef}>Hello</div>
</div>
)
React的一个核心概念就是模块化代码(自包容)。Refs能对组件中指定的元素DOM进行引用,确保目标元素的存在。 但动画通常会涉及多个DOM元素。假设你stagger10个不同元素,使用选择器我们可以轻松的锁定相同的类,比如 .elements
。而使用Refs,我们必须为每个DOM节点都创建一个Ref,这会使得我们代码变得很混乱和重复😅。 那么我们该如何在得到Ref的安全性的同时利用选择器的灵活呢?我们可以使用 React.context()
⭐ gsap.context() 是你最好的朋友!
gsap.context()
给React提供了2个超级有用的功能:
- 你可以传递一个
element|Ref
,这样所有在里面的选择器(比如.my-class
)都会包含在其作用域中,这也意味着,它只会选择该element|Ref
的后代
,再也不用给每个元素都创建单独的Ref了🚀。 - 它会收集所有的GSAP动画和ScrollTriggers,因此你可以很轻松的一次性
revert()
所有这些实例。合适的动画清理对React18double-useEffect()-call
行为很重要,并且这种模式遵循了 React最佳实践📚
代码结构:
jsx
const comp = useRef()
useEffect(() => {
// 创建我们的上下文
// 这个函数会立即调用,所有GSAP动画和创建的ScrollTriggers函数执行期间都会被记录下来
// 因此我们之后可以调用 revert() 对其进行清理
let ctx = gsap.context(() => {
// 所有动画都可以使用选择器,比如 '.box'
// 并且归属于我们组件作用域
gsap.to('.box', {...})
ScrollTrigger.create({ trigger: '#my-id', ... })
}, comp) // 🚀 重要,用于限定选择器作用域
return () => ctx.revert() // 清理
}, [])
WARNING
gsap.context() 和 React Context 是不同的 React18渲染2次对GSAP的影响 - gsap forum
第一个动画
React先渲染box元素,GSAP然后将box旋转360度
jsx
import { useLayoutEffect, useRef } from 'react'
function App() {
const app = useRef()
useLayoutEffect(() => {
let ctx = gsap.context(() => {
gsap.to('.box', { rotation: '+=360' })
}, app)
return () => ctx.revert()
})
return (
<div ref={app} className="app">
<div className="box">Hello</div>
</div>
)
}
⭐ Forwarding refs(转发Refs)
在一个基于组件的系统中,你可能需要对你的目标元素进行更精细的控制。你可以使用 refs转发
的方式获取特定嵌套元素🎉。
jsx
import { useLayoutEffect, useRef } from 'react'
const Box = ({ children, className }) => (
<div className={"box " + className}>{children}</div>
)
const Container = ({ children }) => (
<div><Box>Don't Animate Me</Box></div>
)
function App() {
const app = useRef()
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// 针对className 为 `animate` 的元素
gsap.to('.animate', {
x: 100,
repeat: -1,
repeatDelay: 1,
yoyo: true
})
}, app)
return () => ctx.revert()
}, [])
return (
<div className="app" ref={app}>
<Box className="animate">Box1</Box>
<Container />
<Box className="animate">Box2</Box>
</div>
)
}
⭐ 创建和控制timelines
目前为止,我们只用refs存储对DOM元素的引用,但是它们不仅仅可用于元素。 📚 Refs存在于渲染流程之外 - 因此它们可用于存储组件生命周期内持久化的任何值
。
为了避免每次渲染都创建一个新的timeline,在effect中创建timeline,并将其存储在 ref
中是很重要的😎。
jsx
function Circle({ children }) {
return <div className="circle">{children}</div>;
}
function App() {
const [reversed, setReversed] = useState(false);
const app = useRef();
// store the timeline in a ref.
const tl = useRef();
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// add a box and circle animation to our timeline and play on first render
console.log("creating timeline")
tl.current && tl.current.progress(0).kill();
tl.current = gsap.timeline()
.to(".box", { rotation: 360 })
.to(".circle", { x: 100 })
}, app)
return () => ctx.revert();
}, [])
useEffect(() => {
// toggle the direction of our timeline
console.log("toggling reverse to", reversed)
tl.current.reversed(reversed)
}, [reversed])
return (
<div className="app" ref={app}>
<div>
<button onClick={() => setReversed(!reversed)}>Toggle</button>
</div>
<Box>box</Box>
<Circle>circle</Circle>
</div>
);
}
这允许我们访问在不同的useEffect中访问同一个时间轴,并改变timeline方向
React useEffect触发时机
如果不给 useEffect
传入依赖,它每次渲染时都会执行,这一般不是我们想要的。我们可以传入一个空数组 []
,使其只在第一次时运行。
jsx
// 1️⃣ 只在第一次渲染后运行
useEffect(() => {
const ctx = gsap.context(() => {
gsap.to('.box-1', { rotation: '+=360' })
}, el)
}, [])
// 2️⃣ 在第一次渲染后运行,以及每次 `someProp` 发生改变时运行
useEffect(() => {
const ctx = gsap.context(() => {
gsap.to('.box-2', { rotation: '+=360' })
}, el)
}, [someProp])
// 3️⃣ 每次渲染都运行
useEffect(() => {
const ctx = gsap.context(() => {
gsap.to('.box-3', { rotation: '+=360' })
}, el)
})
⭐ 子组件动画
jsx
function Box({ children, endX }) {
const boxRef = useRef()
const ctx = useRef()
useEffect(() => {
// 🚀 nothing initially (we'll add() to the context when endX changes)
ctx.current = gsap.context(() => {})
return () => ctx.current.revert()
}, [])
// run when `endX` changes
useEffect(() => {
// 添加
ctx.current.add(() => {
gsap.to(boxRef.current, {
x: endX
})
})
}, [endX])
return <div className="box" ref={boxRef}>{children}</div>;
}
function App() {
const [endX, setEndX] = useState(0)
return (
<div className="app">
<button onClick={() => setEndX(randomX())}>Pass in a randomized value</button>
<Box endX={endX}>{endX}</Box>
</div>
)
}
动画交互
使用回调的方式进行交互
jsx
const onEnter = ({ currentTarget }) => {
gsap.to(currentTarget, { backgroundColor: '#e77614' })
}
const onLeave = ({ currentTarget }) => {
gsap.to(currentTarget, { backgroundColor: '#28a92b' })
}
return (
<div className="box" onMouseEnter={onEnter} onMouseLeave={onLeave}>
Hover me
</div>
)
⭐ 避免没有样式的内容闪烁(FOUC)
useEffect
在DOM绘制后触发,当对元素进行Fading效果时,可以注意到没有样式的内容的闪烁问题。(原文这里有一个gif示意图)
为了避免闪烁问题,可以使用 useLayoutEffect
替代 useEffect
,它在DOM绘制前执行:
useLayoutEffect
对于你需要进行DOM测量时很有用,因此对使用GSAP ScrollTrigger 和 Flip 插件时,推荐使用此hook。
关于useEffect & useLayoutEffect:
清理
在effects中返回一个清理函数,用于杀掉任何正在运行的动画和一些可能导致内存泄漏的东西,比如事件监听。
如果一个动画运行很长时间,使用ScrollTriggers或改变组件状态,清理函数就很重要。
这种情形,使用 gsap.context()
就很方便,因为它允许我们收集所有动画,并使用 revert()
方法杀掉它收集的所有动画。
jsx
useEffect(() => {
const ctx = gsap.context(() => {
const animation1 = gsap.to('.box1', { rotation: '+=360' })
const animation2 = gsap.to('.box2', {
scrollTrigger: {
// ...
}
})
}, el) // scopes all selector text inside the context to this ref(optional, default is document)
const onMove = () => {}
window.addEventListener('pointermove', onMove)
// 清理函数在组件卸载时调用
return () => {
ctx.revert() // 清理动画
window.removeEventListener('pointermove', onMove)
}
}, [])
原文链接:
2022年10月21日14:19:52