首先我推荐阅读Greensock’s React documentation。
Next.JS 中的动画介绍
对于介绍动画,如果您将useLayoutEffect 与 SSR 一起使用,您的控制台将充满警告。为避免这种情况,请改用useIsomorphicLayoutEffect。 Go to useIsomorphicLayoutEffect.
为了防止使用 SSR 的无样式内容 (FOUC) 闪烁,您需要设置组件的初始样式状态。例如,如果我们正在淡入,该组件的初始样式应该是不透明度为零。
Next.JS 中的 Outro 动画
对于outro动画,截取页面过渡,做退出动画,然后onComplete路由到下一页。
为了实现这一点,我们可以使用 TransitionLayout 高阶组件作为包装器来延迟路由更改,直到任何动画完成之后,以及一个 TransitionProvider 组件将利用 React 的 useContext 挂钩来共享跨多个组件的结尾时间线,无论它们嵌套在哪里。
过渡上下文
为了制作页面过渡效果,我们需要在outro动画完成之前阻止渲染新页面。
我们的页面中可能嵌套了许多具有不同动画效果的组件。为了跟踪所有不同的结尾转换,我们将结合使用 React 的 Context API 和顶级 GSAP 时间线。
在TransitionContext 中,我们将创建我们的TransitionProvider,这将使我们的GSAP 时间线为outro 动画提供给任何想要在页面更改期间过渡的组件。
import React, { useState, createContext, useCallback } from "react"
import gsap from "gsap"
const TransitionContext = createContext({})
const TransitionProvider = ({ children }) => {
const [timeline, setTimeline] = useState(() =>
gsap.timeline({ paused: true })
)
return (
<TransitionContext.Provider
value={{
timeline,
setTimeline,
}}
>
{children}
</TransitionContext.Provider>
)
}
export { TransitionContext, TransitionProvider }
接下来,我们有 TransitionLayout,它将是我们的控制器,它将启动 outro 动画并在它们全部完成后更新页面。
import { gsap } from "gsap"
import { TransitionContext } from "../context/TransitionContext"
import { useState, useContext, useRef } from "react"
import useIsomorphicLayoutEffect from "../animation/useIsomorphicLayoutEffect"
export default function TransitionLayout({ children }) {
const [displayChildren, setDisplayChildren] = useState(children)
const { timeline, background } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
if (children !== displayChildren) {
if (timeline.duration() === 0) {
// there are no outro animations, so immediately transition
setDisplayChildren(children)
} else {
timeline.play().then(() => {
// outro complete so reset to an empty paused timeline
timeline.seek(0).pause().clear()
setDisplayChildren(children)
})
}
}
}, [children])
return <div ref={el}>{displayChildren}</div>
}
在自定义 App 组件中,我们可以让 TransitionProvider 和 TransitionLayout 包装其他元素,以便它们可以访问 TransitionContext 属性。 Header 和 Footer 存在于 Component 之外,因此它们在初始页面加载后将是静态的。
import { TransitionProvider } from "../src/context/TransitionContext"
import TransitionLayout from "../src/animation/TransitionLayout"
import { Box } from "theme-ui"
import Header from "../src/ui/Header"
import Footer from "../src/ui/Footer"
export default function MyApp({ Component, pageProps }) {
return (
<TransitionProvider>
<TransitionLayout>
<Box
sx={{
display: "flex",
minHeight: "100vh",
flexDirection: "column",
}}
>
<Header />
<Component {...pageProps} />
<Footer />
</Box>
</TransitionLayout>
</TransitionProvider>
)
}
组件级动画
这是我们可以在组件级别执行的基本动画示例。我们可以将任意数量的这些添加到页面中,它们都会做同样的事情,将所有子元素包裹在一个透明的 div 中,并在页面加载时淡入,然后在导航到不同页面时淡出。
import { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const FadeInOut = ({ children }) => (
const { timeline } = useContext(TransitionContext)
const el = useRef()
// useIsomorphicLayoutEffect to avoid console warnings
useIsomorphicLayoutEffect(() => {
// intro animation will play immediately
gsap.to(el.current, {
opacity: 1,
duration: 1,
})
// add outro animation to top-level outro animation timeline
timeline.add(
gsap.to(el.current, {
opacity: 1,
duration: .5,
}),
0
)
}, [])
// set initial opacity to 0 to avoid FOUC for SSR
<Box ref={el} sx={{opacity: 0}}>
{children}
</Box>
)
export default FadeInOut
我们可以采用这种模式并将其提取到可扩展的 AnimateInOut 帮助器组件中,以便在我们的应用程序中实现可重用的介绍/结尾动画模式。
import React, { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const AnimateInOut = ({
children,
as,
from,
to,
durationIn,
durationOut,
delay,
delayOut,
set,
skipOutro,
}) => {
const { timeline } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
// intro animation
if (set) {
gsap.set(el.current, { ...set })
}
gsap.to(el.current, {
...to,
delay: delay || 0,
duration: durationIn,
})
// outro animation
if (!skipOutro) {
timeline.add(
gsap.to(el.current, {
...from,
delay: delayOut || 0,
duration: durationOut,
}),
0
)
}
}, [])
return (
<Box as={as} sx={from} ref={el}>
{children}
</Box>
)
}
export default AnimateInOut
AnimateInOut 组件具有针对不同场景的内置灵活性:
- 为片头和片尾设置不同的动画、持续时间和延迟
- 跳过结尾
- 为包装器设置元素标签,例如使用
<span> 而不是 <div>
- 使用 GSAP 的
set 选项为 intro 定义初始值
使用它我们可以创建各种可重复使用的开场/开场动画,例如<FlyInOut>、<ScaleInOut>、<RotateInOut3D> 等等。
我有一个演示项目,您可以在其中看到上述内容:TweenPages