每次看到苹果官网的动画效果都令人震惊,总想去理解他并复刻下来,那我们今天就来实现苹果官网中AirPods的滚动变化光影的效果。
下面先看最终的效果:
airpods滚动效果
可以看出,我们向下滚动的时候,图片元素并没有跟随滚动,光影效果一直在改变。如果我们向上滚动,则会让动画效果反方向运行。这就是我们想要的结果。
根据滚动,使用Canvas的drawImage不停的画不同的图片,从而实现不同的展示效果
苹果有一组图片:
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,这个用习惯了还是很好用的。
<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>
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);
};
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();
// 要使用插件,必须先注册
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-1
const 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
在scroller-start
之上,且end
在scroller-end
之下的,那么动画就会触发。
这两部分的值可以有很多种形式,比如单词类:top
、bottom
、center
,百分比10%
、100%
, 像素 100px
等等,他们之间可以使用相对位置,比如top+=10%
,bottom-=100px
等等。
所以这个部分不同的组合就能创建出不同的触发动画的时机,只有慢慢去尝试,才能理解得非常清晰。
就拿我们的代码来说:
start: "top 10%",
end: "bottom+=300% 10%",
end 不见了是因为它在视口以外了。
因为我们需要滚动时拉长总的滚动距离(分母),而鼠标滚动一次的距离(分子)几乎是固定的,这样就让我们的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-1
const 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>
拿去就能跑,你可以试试~
如果你觉得有用,欢迎评论、点赞、转发~
当然也希望你能关注我的公众号:前端大乱炖