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

发布时间:2024年01月16日

每次看到苹果官网的动画效果都令人震惊,总想去理解他并复刻下来,那我们今天就来实现苹果官网中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-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、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-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>

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

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

文章来源:https://blog.csdn.net/yanyi24/article/details/135623759
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。