苹果官网动画解析之airpods滚动光影效果

每次看到苹果官网的动画效果都令人震惊,总想去理解他并复刻下来,那我们今天就来实现苹果官网中AirPods的滚动变化光影的效果。

下面先看最终的效果:

airpods滚动效果

可以看出,我们向下滚动的时候,图片元素并没有跟随滚动,光影效果一直在改变。如果我们向上滚动,则会让动画效果反方向运行。这就是我们想要的结果。

核心原理

根据滚动,使用CanvasdrawImage不停的画不同的图片,从而实现不同的展示效果

准备工作

1. 图片哪里来?

苹果有一组图片:

https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/xxx.jpg

xxx: 从0001到 0147

滚动怎么监听?原生写?

当然不是,这里我们使用gsap这个库。

简单来说,这就是一个使用js创建动画的库,好用之处在于它支持很多插件,并且可以很方便的使用属性来控制css样式。缺点就是有些插件是要收费的,但是我们今天用到的 ScrollTrigger是免费的。

这里就不介绍具体使用方法,因为太多,可以自己去官方网站看看。

正式开始

样式使用的是Tailwind CSS,这个用习惯了还是很好用的。

1. 创建一个空的canvas并引入gsap

<section class="airpods bg-black py-14"><canvas id="airpods"></canvas>
</section><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.4/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.4/ScrollTrigger.min.js"></script>

2. 使用canvas画第一张图

    const canvas = document.getElementById('airpods');const context = canvas.getContext("2d");// 这里根据图片的大小设置宽高canvas.width = 1158;canvas.height = 770;const img = new Image();img.src = 'https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/0001.jpg';img.onload = () => {context.drawImage(img, 0, 0, canvas.width, canvas.height);};

在这里插入图片描述

3. 预加载所有图片,以便动画能够流畅

    const frameCount = 147;function preloadImages() {  for (let i = 1; i < frameCount; i++) {const img = new Image();img.src = currentFrame(i);}}// 处理图片地址function currentFrame(index) {  return  `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, "0")}.jpg`;}preloadImages();

4. 使用gsap创建动画

    // 要使用插件,必须先注册gsap.registerPlugin(ScrollTrigger);/** gsap语法:* gsap.to(target, {property: value, property: value, ...});** scrollTrigger语法:* scrollTrigger: {*   trigger: target, // 滚动谁触发动画*   scrub: true | false, // 动画是否重复执行,也就是再次进入后触发与否*   pin: true | false, // 是否固定在触发位置*   start: "top 10%", // 触发开始位置  后面详细介绍*   end: "bottom+=300% 10%", // 触发结束位置*   markers: true | false, // 是否显示辅助线,主要用于开发阶段*   ...* }* **/gsap.to(canvas, {scrollTrigger: {trigger: canvas,scrub: true,start: "top 10%",end: "bottom+=300% 10%",pin: true,markers: true,onUpdate: (self) => {// self.progress 表示滚动的百分比 0-1const frameIndex = Math.min(frameCount,Math.ceil(self.progress * frameCount + 1));requestAnimationFrame(() => updateImage(frameIndex));},}});function updateImage(index) {img.src = currentFrame(index);context.drawImage(img, 0, 0);}

完成上面几步就能够实现我们最终的效果了。

start、end详细介绍

我认为其他参数都还是比较好理解,唯独这两个有一些绕,所以这里单独介绍下。

首先看默认值

在这里插入图片描述

首先要清楚这两个值由两部分组成,分别控制元素视口

只要startscroller-start之上,且endscroller-end之下的,那么动画就会触发。

这两部分的值可以有很多种形式,比如单词类:topbottomcenter,百分比10%100%, 像素 100px等等,他们之间可以使用相对位置,比如top+=10%bottom-=100px等等。
所以这个部分不同的组合就能创建出不同的触发动画的时机,只有慢慢去尝试,才能理解得非常清晰。

就拿我们的代码来说:

    start: "top 10%",end: "bottom+=300% 10%",

在这里插入图片描述

end 不见了是因为它在视口以外了。

这个end为什么是300%?

因为我们需要滚动时拉长总的滚动距离(分母),而鼠标滚动一次的距离(分子)几乎是固定的,这样就让我们的progress变得更加小,从而使动画更加平滑。

progress = 拉长总的滚动距离 / 鼠标滚动一次的距离

完整代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><title>AirPods</title>
</head>
<body><main class="bg-slate-300 animate-demo "><section class="airpods  bg-black py-14"><canvas id="airpods" class=""></canvas></section></main><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.4/gsap.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.4/ScrollTrigger.min.js"></script><script>function airpods() {const frameCount = 147;const canvas = document.getElementById('airpods');const context = canvas.getContext("2d");canvas.width = 1158;canvas.height = 770;const img = new Image();img.src = currentFrame(1);img.onload = () => {context.drawImage(img, 0, 0, canvas.width, canvas.height);};/** gsap语法:* gsap.to(target, {property: value, property: value, ...});** scrollTrigger语法:* scrollTrigger: {*   trigger: target, // 滚动谁触发动画*   scrub: true | false, // 动画是否重复执行,也就是再次进入后触发与否*   pin: true | false, // 是否固定在触发位置*   start: "top 10%", // 触发开始位置  后面详细介绍*   end: "bottom+=300% 10%", // 触发结束位置*   markers: true | false, // 是否显示辅助线,主要用于开发阶段*   ...* }* **/gsap.to(canvas, {scrollTrigger: {trigger: canvas,scrub: true,start: "top 10%",end: "bottom+=300% 10%",pin: true,markers: true,onUpdate: (self) => {// self.progress 表示滚动的百分比 0-1const frameIndex = Math.min(frameCount,Math.ceil(self.progress * frameCount + 1));requestAnimationFrame(() => updateImage(frameIndex));},}});function updateImage(index) {img.src = currentFrame(index);context.drawImage(img, 0, 0);}function preloadImages() {for (let i = 1; i < frameCount; i++) {const img = new Image();img.src = currentFrame(i);}}function currentFrame(index) {return  `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, "0")}.jpg`;}preloadImages();}airpods();</script>
</body>
</html>

拿去就能跑,你可以试试~

如果你觉得有用,欢迎评论、点赞、转发~
当然也希望你能关注我的公众号:前端大乱炖
在这里插入图片描述