该项目由React + vite 搭建 基于GSAP和ScrollTrigger实现炫酷3D动画网页
本期接着 上篇 继续实现炫酷3D动画网页
这篇文章主要讲解以下部分
- 安装 介绍 使用依赖库
GSAP
ScrollTrigger
lenis
- code可复用hooks
initSmoothScrolling.js
useImagePreloader.js
- 响应式设计
chooseAnimation
安装依赖
GSAP + ScrollTrigger
-
GSAP(GreenSock Animation Platform)是Web动画开发的工具库,可使CSS 属性、SVG、React、画布、通用对象等动画化,并解决不同浏览器上存在的兼容问题
-
ScrollTrigger 是 GSAP 的插件,ScrollTrigger控制滚动动画,而真正处理动画是GSAP,二者组合使用才能实现丝滑滚动动画
-
安装 使用
zsh
npm i -D gsap
jsx
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
lenis库介绍
- lenis也是动画库 可以实现惯性滚动 给鼠标滚轮增加阻尼感 纵享丝滑
- 查看 lenis官方效果
当我们在移动终端上滑动页面,手指离开屏幕后,页面的滚动并不会马上停止,而是在一段时间内继续保持惯性滚动,并且滑动阻尼感和持续时间与滑动手势的幅度成正比
zsh
npm i @studio-freight/lenis
简单实例
jsx
import Lenis from '@studio-freight/lenis'
const lenis = new Lenis()
lenis.on('scroll', (e) => {
console.log(e)
})
function raf(time) {
lenis.raf(time)
requestAnimationFrame(raf)
}
requestAnimationFrame(raf)
封装hooks !!
lenis + ScrollTrigger 初始化丝滑滚动效果
- 创建 Lenis实例
- 配置平滑滚动的速度 值越小平滑效果越明显
lerp: 0.1
- 启用了鼠标滚轮事件的平滑滚动
smoothWheel: true
- 配置平滑滚动的速度 值越小平滑效果越明显
- 用户滚动时更新 ScrollTrigger
- lenis.on 监听 'scroll' 事件
- 用户滚动页面时更新 ScrollTrigger
- 定义动画帧函数
- crollFn在每一帧动画执行时被调用
- 通过调用 Lenis 实例的 raf 方法来实现平滑滚动
- requestAnimationFrame 来实现递归调用
- 启用动画帧更新
- 调用requestAnimationFrame(scrollFn) 监听和执行 scrollFn 函数,以维护平滑滚动效果
js
// utils/initSmoothScrolling.js
import Lenis from '@studio-freight/lenis'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
export const initSmoothScrolling = () => {
const lenis = new Lenis({
lerp: 0.1,
smoothWheel: true
})
lenis.on('scroll', () => ScrollTrigger.update())
const scrollFn = time => {
lenis.raf(time)
requestAnimationFrame(scrollFn)
}
requestAnimationFrame(scrollFn)
}
预加载图片 useImagePreloader
- 导入图片: 图片都在src/img目录之下 由于vite不支持require 我们可以通过import.meta.globEager方法获取所有以'.jpg'结尾的图片模块
- 创建imagePromises[] 容纳导出image promise对象,每个image创建了一个 Image 对象,设src属性为module.default
- 使用 Promise.all 等待所有图片加载完成 然后储存在images中 return出去
jsx
// utils/useImagePreloader.js
import { useEffect, useState } from 'react'
export const useImagePreloader = () => {
const [images, setImages] = useState([])
useEffect(() => {
async function preloadImages() {
const imageModules = import.meta.globEager('../img/*.jpg')
const imagePromises = Object.values(imageModules).map(module => {
const image = new Image()
image.src = module.default
return image
})
const loadedImages = await Promise.all(imagePromises)
setImages(loadedImages)
}
preloadImages()
}, [])
return images
}
结合上期文章说的检测浏览器是否支持css 函数supportsCssVars
可以将代码组织如下
useLayoutEffect
在浏览器绘制 渲染真正dom之前初始化initSmoothScrolling- 之后还会将选择动画
chooseAnimation
在其中组织
jsx
// src/GridAnimation.jsx
import { useLayoutEffect, useState } from 'react'
import { initSmoothScrolling, useImagePreloader, supportsCssVars } from './utils'
function GridAnimation() {
const [loading, setLoading] = useState(true)
const images = useImagePreloader()
useLayoutEffect(() => {
supportsCssVars() || alert('请在支持CSS变量的现代浏览器中查看此演示')
initSmoothScrolling()
// chooseAnimation(gridType)
setLoading(false)
}, [images])
if (loading || !images.length) return <div className="loading"></div>
return (
<div>
<main>
<div className="intro">
<h1 className="intro__title">
<span className="intro__title-pre">On-Scroll</span>
<span className="intro__title-sub">Perspective Grid Animations</span>
</h1>
<span className="intro__info">Scroll moderately to fully experience the animations</span>
</div>
</main>
</div>
)
}
export default GridAnimation
可以看到图片也已经加载好了 差不多 ! 前期准备工作基本完成 进入正题!
进入正题 ! Animation!
整体布局优化
我们整体的结构应该是这样的
- 先关注其中一个section结构 其他的循环出来就好了
- 其中要注意grid-wrap之下里面套了一层循环渲染image 因为很大所有没有展开 具体可以看下面的结构
html
<div>
<main>
<div className="intro">...</div>
<section key={index} className='content'>
<div className={'grid'}>
<div className="grid-wrap">
{images.map((item, index) => (
<div className="grid__item" key={index}>
<img className="grid__item-inner" src={item.src} />
</div>
))}
</div>
</div>
<h3 className='content__title'>{children}</h3>
</section>
<section>...<section/>
</main>
</div>
美化样式
- 采用BEM命名规范
- 中划线(-) :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。
- 双下划线(__):双下划线用来连接块和块的子元素
- 单下划线(_):单下划线用来描述一个块或者块的子元素的一种状态
- 采用var自定义属性增强复用性 动态可扩展
- vh(视窗高度单位) rem(根元素字体大小单位)clamp(min, preferred, max)实现响应式设计
css
.content {
position: relative;
margin-bottom: 20vh;
}
.grid {
display: grid;
place-items: center;
padding: 2rem;
width: 100%;
perspective: var(--perspective);
}
.grid-wrap {
height: var(--grid-height);
width: var(--grid-width);
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--grid-gap);
transform-style: preserve-3d;
}
.grid__item {
aspect-ratio: var(--grid-item-ratio);
width: 100%;
height: auto;
overflow: hidden;
position: relative;
border-radius: 8px;
display: grid;
place-items: center;
}
.grid__item-inner {
position: relative;
width: calc(1 / var(--grid-inner-scale) * 100%);
height: calc(1 / var(--grid-inner-scale) * 100%);
background-size: cover;
background-position: 50% 50%;
}
.content__title {
position: absolute;
height: 100vh;
width: 100vw;
top: 50%;
left: 50%;
margin: -50vh 0 0 -50vw;
padding: 0 10vw;
display: grid;
place-items: center;
text-align: center;
font-weight: 300;
font-size: clamp(1.5rem, 15vw, 6.5rem);
}
效果
现在差不多能够看了 但效果只能说四平八稳 没什么特点 上Animation!
chooseAnimation 响应式设计
getGrid函数 -- 操作奇偶数行grid
是由GreenSock官方 提供的可以动态地计算网格的行/列 操作奇数或偶数行设置动画 当库函数直接拿过来用就行
js
const getGrid = selector => {
let elements = gsap.utils.toArray(selector),
bounds,
getSubset = (axis, dimension, alternating, merge) => {
let a = [],
subsets = {},
onlyEven = alternating === 'even',
p
bounds.forEach((b, i) => {
let position = Math.round(b[axis] + b[dimension] / 2),
subset = subsets[position]
subset || (subsets[position] = subset = [])
subset.push(elements[i])
})
for (p in subsets) {
a.push(subsets[p])
}
if (onlyEven || alternating === 'odd') {
a = a.filter((el, i) => !(i % 2) === onlyEven)
}
if (merge) {
let a2 = []
a.forEach(subset => a2.push(...subset))
return a2
}
return a
}
elements.refresh = () => (bounds = elements.map(el => el.getBoundingClientRect()))
elements.columns = (alternating, merge) => getSubset('left', 'width', alternating, merge)
elements.rows = (alternating, merge) => getSubset('top', 'height', alternating, merge)
elements.refresh()
return elements
}
chooseAnimation
列举其中一个AnimationType grid--1
来举例子 其他的type可以看源码
js
export const chooseAnimation = (animationType, grid) => {
const gridWrap = grid.querySelector('.grid-wrap')
const gridItems = grid.querySelectorAll('.grid__item')
const gridItemsInner = [...gridItems].map(item => item.querySelector('.grid__item-inner'))
const timeline = gsap.timeline({
defaults: { ease: 'none' },
scrollTrigger: {
trigger: gridWrap,
start: 'top bottom+=5%',
end: 'bottom top-=5%',
scrub: true,
},
})
switch (animationType) {
case 'grid--1':
grid.style.setProperty('--perspective', '1000px')
grid.style.setProperty('--grid-inner-scale', '0.5')
timeline
.set(gridWrap, { rotationY: 25 })
.set(gridItems, { z: () => gsap.utils.random(-1600, 200) })
.fromTo(gridItems, { xPercent: () => gsap.utils.random(-1000, -500) }, { xPercent: () => gsap.utils.random(500, 1000) }, 0)
.fromTo(gridItemsInner, { scale: 2 }, { scale: 0.5 }, 0)
break
case 'grid--2':
...
break
...
default:
console.error('未知animation type')
break
}
}
函数详细注释
-
chooseAnimation接收俩个参数 animationType(动画类型的名称) 和 grid(包含网格元素的DOM元素)
-
获取 操作dom元素
- 在grid元素之下查找grid-wrap和gridItem
- 将gridItems 转换为数组,遍历查找每个子元素中具有类名'grid__item-inner' 的元素储存在gridItemsInner中
-
创建gsap(GreenSock Animation Platform)的时间线(timeline)
- 在时间线配置对象中,设置了默认的缓动函数为 'none'
- 配置scrollTrigger 指定gridWrap 元素作为滚动触发器的触发器
start: 'top bottom+=5%'
触发器的开始位置被设置在 gridWrap 的顶部,然后再下移5%的距离触发动画end: 'bottom top-=5%'
触发器的结束位置被设置在 gridWrap 的底部,然后再上移5%的距离触发动画。scrub: true
动画会根据滚动位置进行缓慢播放
-
根据传入的 animationType选择不同的动画类型
- 对每个grid重载不同css自定义属性
--perspective
,--grid-inner-scale
等等
- 对每个grid重载不同css自定义属性
-
具体化时间线设置
- timeline.set方法将 gridWrap 元素的 rotationY 属性设置为 25
timeline.set(gridWrap, { rotationY: 25 })
- 使用随机函数设置 gridItems 元素的 z 属性,范围在 -1600 到 200 之间
timeline.set(gridItems, { z: () => gsap.utils.random(-1600, 200) })
- 使用
fromTo
方法来设置gridItems
元素的 xPercent 属性,从一个随机值过渡到另一个随机值。 0 表示从时间线的开始位置执行这个动画 - 设置gridItemsInner 元素的 scale 属性,从 2倍缩小到 0.5倍
- timeline.set方法将 gridWrap 元素的 rotationY 属性设置为 25
chooseAnimation使用
jsx
useLayoutEffect(() => {
const grids = document.querySelectorAll('.grid')
const promises = Array.from(grids).map((grid, i) => chooseAnimation(`grid--${i + 1}`, grid))
await Promise.all(promises)
}
那么现在就可以重新组织一下代码啦
jsx
import { useLayoutEffect, useState } from 'react'
import { initSmoothScrolling, useImagePreloader, supportsCssVars, chooseAnimation } from './utils'
import './style/index.less'
function GridAnimation() {
const [loading, setLoading] = useState(true)
const images = useImagePreloader()
useLayoutEffect(() => {
const animateGrids = async () => {
supportsCssVars() || alert('请在支持CSS变量的现代浏览器中查看此演示')
initSmoothScrolling()
const grids = document.querySelectorAll('.grid')
const promises = Array.from(grids).map((grid, i) => chooseAnimation(`grid--${i + 1}`, grid))
await Promise.all(promises)
setLoading(false)
}
animateGrids()
}, [images])
if (loading || !images.length) return <div className="loading"></div>
return (
<div>
<main>
<div className="intro">
<h1 className="intro__title">
<span className="intro__title-pre">On-Scroll</span>
<span className="intro__title-sub">Perspective Grid Animations</span>
</h1>
<span className="intro__info">Scroll moderately to fully experience the animations</span>
</div>
<section className={'content'}>
<div className={'grid'}>
<div className="grid-wrap">
{images.map((item, index) => (
<div className="grid__item" key={index}>
<img className="grid__item-inner" src={item.src} />
</div>
))}
</div>
</div>
<h3 className={'content__title'}>
{'Fleeting moments,'} <br />
{`existence's dance.`}
</h3>
</section>
<section>...</section>
<section>...</section>
<section>...</section>
</main>
</div>
)
}
export default GridAnimation
如何优化代码结构 ?
假设我们有很多个AnimationType 而每个section结构都高度耦合 同时还会设置不同的css属性 不可能直接就粘贴复制改改className刷刷刷完成任务吧。。虽然这个小项目没啥人看 但也还是坚守职业修为!! 所以!!必须做一些抽取封装!那么下一期就是重新组织代码结构 + 部署上线咯
今天的任务就圆满结束啦 有收获可以点赞收藏哦~
推荐文章
- React + GSAP + ScrollTrigger + Vite ==> 炫酷3D动画网页 -- 上篇
- React + GSAP + ScrollTrigger + Vite ==> 炫酷3D动画网页 -- 下篇