手摸手,带你用 PIXI+GSAP 一步步实现刹车动效 | 猿创营

1,843 阅读6分钟

前言

相信大家工作中用的最多的就是 React 或者 Vue 去搬砖,今天带大家玩一下,了解下前端可视化,看前端是怎么实现炫酷动画效果,创意来源于vanmoof电商页面的刹车动画。通过 PIXI+GSAP 一步步刨析实现过程,让我们一起开启可视化大门!

image.png

技术栈

在前端开发中,如果需要图形的绘制,或者创建图片特效和动画,使用 Canvas 是非常合适的。那么这次为什么没有直接用 Canvas?就像上古时代,前端开发用 Jquery,没有直接用 Js 是一样的道理,选用 PIXI 本质上就是 Canvas 的库(渲染引擎,既支持 WebGL 也支持 Canvas),可以便捷的操作图像绘制图像,然后在搭配动画库 GSAP,负责想要的动画效果,极大的提升我们开发效率。以上就是技术选型的依据。

  • PIXI:pixijs.com/

    PixiJS 是一个 2D 渲染引擎,可让你创建丰富的交互式图形、跨平台应用程序和游戏,而无需深入研究 WebGL API 以及处理浏览器和设备的兼容性问题。

    特点:2D 渲染方面 PixiJS 是佼佼者。跨平台应用,不用考虑移动端兼容问题。

  • GSAP:greensock.com/docs/v3/GSA…

    GSAP 是一个 JS 动画框架,可以对 JavaScript 可以操作的所有内容进行动画处理(CSS 属性,SVG,React,画布,通用对象等),同时解决了不同浏览器上存在的兼容性问题。

    特点: 老牌动画库,Flash 时代就有大量用户,不用考虑移动端兼容问题。


需求分析

开干前一起梳理下我们大概要实现那些需求, 划分为模块,这样我们就像搭积木一样,不慌不忙,一点点实现。

  • 自行车
    • 车架
    • 车轮
    • 刹车
  • 按钮
    • 按钮位置
    • 按钮点击事件
    • 按下 刹车
    • 松开 恢复行驶
  • 粒子背景
    • 随机粒子位置,大小,颜色
    • 行驶粒子倾斜 30° 向下缓动
    • 按下粒子静止
    • 松开粒子回弹

环境搭建

这次主要实现 demo, 暂不考虑使用第三方框架。直接就创建index.html 引入是需要的库 PIXIGSAP, 然后创建 brakeBanner.js后,加载实例化的brakeBanner类,之后在此文件专注写动效的逻辑。

  <!-- index.html -->
  <script src="https://pixijs.download/release/pixi.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js"></script>
  <script src="./js/brakebanner.js"></script>
  <script>
      window.onload = () => {
        new BrakeBanner("#brakebanner");
      };
  </script>


创建容器

首先创建createApp方法来管理画布容器,通过 PIXI.Application,创建一个的 PIXI 的实例,这个类会自动创建渲染器,接下来的图像操作,统统由这个类接管。然后我们设置容器的宽高,背景颜色,将它添加到 Dom 中,第一步就完成了,是不是很简单!


 	createApp (selector){
            this.app = new PIXI.Application({
                width: window.innerWidth,
                height: window.innerHeight,
                backgroundColor: 'black'
            })
            document.querySelector(selector).appendChild(this.app.view)
	}

引入图片资源

loadImg方法中引入图片资源,会涉及到 PIXI.Loader,可以加载的图片资源,因为需要引入多张图片,使用 onComplete在图片加载完成时候在调用。


 loadImg (){
    const that = this
    const loader = new PIXI.Loader();
    this.loader = loader
    const loaderMap = {
        bike: 'images/brake_bike.png',
        handlerbar: 'images/brake_handlerbar.png',
        lever: 'images/brake_lever.png',
        btn: 'images/btn.png',
        btn_circle: 'images/btn_circle.png',
    }
    for(const key in loaderMap) {
      loader.add(key, loaderMap[key])
    }
    loader.load();
    loader.onComplete.add(() => {
        that.createBike()
        that.createButton()
        that.creatParticle()
    })
}

经过以上的需求分析, 我们了解到主要内容分为,自行车,按钮,和粒子背景。那么当图片加载完成,接下来的思路就是把这部分内容加载到页面中。


添加自行车

Container 代表了一个容器,就类似与 div 可以装载多个显示对象,Sprite 是精灵对象,可以理解为容器的子元素, 而loader 加载的图片一般会创建为精灵对象,添加在容器在显示。那么先创建自行车容器,将车轮,车架,刹车的图片转为精灵对象,再添加到Container中就完成自行车的展示了。

    createBike(){
        const {app, loader} = this
        const bikeContainer = new PIXI.Container()
        // 车轮
        let bike = new PIXI.Sprite(loader.resources['bike'].texture);
        // 车架
        let handler = new PIXI.Sprite(loader.resources['handlerbar'].texture);
        // 刹车
        let lever = new PIXI.Sprite(loader.resources['lever'].texture);
        // 设置刹车位置
        lever.x = 750
        lever.y = 950
        lever.pivot.x = 483
        lever.pivot.y = 500
        lever.rotation = Math.PI  * -15 / 180

        bikeContainer.addChild(bike);
        bikeContainer.addChild(lever);
        bikeContainer.addChild(handler);
        // 设置自行车容器位置
        bikeContainer.scale.x = .25
        bikeContainer.scale.y = .25
        bikeContainer.x = window.innerWidth - bikeContainer.width;
        bikeContainer.y = window.innerHeight - bikeContainer.height;
        app.stage.addChild(bikeContainer)
        this.lever = lever
        this.bikeContainer = bikeContainer
      }

添加按钮

按照之前的套路, 创建按钮容器,引入精灵元素,添加到Container中。然后设置容器的位置,圆心位置,按钮圆心位置。 这里btn_circle2 多添加个边框,为了展示按钮波纹动画。不要着急,按钮加上动画可以效果。

  createButton() {
    const {lever, bikeContainer, loader} = this
    const btnContainer = new PIXI.Container()

    this.app.stage.addChild(btnContainer)
    let btn = new PIXI.Sprite(loader.resources['btn'].texture);
    let btn_circle = new PIXI.Sprite(loader.resources['btn_circle'].texture);
    let btn_circle2 = new PIXI.Sprite(loader.resources['btn_circle'].texture);
    // 设置按钮圆心位置
    btn.pivot.x = btn.width / 2
    btn.pivot.y = btn.height / 2
    btn_circle.pivot.x = btn_circle.width / 2
    btn_circle.pivot.y = btn_circle.height / 2

    btn_circle2.pivot.x = btn_circle.width / 2
    btn_circle2.pivot.y = btn_circle.height / 2

    btn_circle.scale.x = btn_circle.scale.y = .8
    // 缩小一半
    btnContainer.scale.x = .5
    btnContainer.scale.y = .5
    // 位置偏移
    btnContainer.x = window.innerWidth - 300;
    btnContainer.y = window.innerHeight - 350;

    btnContainer.addChild(btn);
    btnContainer.addChild(btn_circle);
    btnContainer.addChild(btn_circle2);
    this.btnContainer = btnContainer
    this.btn_circle = btn_circle
  }

按钮动画

先分析下按钮的动画,有个边框会由小到大扩散的扩散,当试下扩散到最最大会消失。另一个边框固定展示,那么对用的是 btn_circle,负责循环动画扩散 btn_circle2 是固定的边框,分析完毕,代码很简单 2 行搞定。gsap就可以登场了,to接收 2 个参数,控制动画的对象, 设置动画的参数。

  createButton() {
    ...
    // 边框由小到大扩散
    gsap.to(btn_circle.scale, {duration:1, x:1.3, y:1.3, repeat: -1})
    // 扩散最大后,隐藏
    gsap.to(btn_circle, {duration:1, alpha:0, repeat: -1})

  }

按钮事件

设置容器interactive,buttonMode的值为true,设置按钮交互属性。添加mousedown/mousedown 监听函数,梳理按下和抬起要做那些动画。代码就很好实现了。

  • 按下
    • 移除按钮扩散效果,移除btn_circle对象
    • 刹车动画,旋转 15°
    • 自行车下移,向下 y 轴平移 20
  • 抬起
    • 移除按钮扩散效果,添加btn_circle对象
    • 刹车恢复,恢复到原来旋转
    • 自行车位置恢复,恢复到原来位置
  createButton() {
    ...
    // 按钮事件
    btnContainer.interactive = true
    btnContainer.buttonMode = true
    btnContainer.on('mousedown',()=>{
      // 按钮扩散边框移除
      btnContainer.removeChild(btn_circle)
      //刹车动画
      gsap.to(lever, {duration: .6, rotation: Math.PI / 180 * -30})
      //自行车下移
      gsap.to(bikeContainer,{
        duration:.4, 
        x: window.innerWidth - bikeContainer.width,
        y: window.innerHeight - bikeContainer.height +20
      });
      btnContainer.on('mouseup',()=>{
        //刹车恢复
        gsap.to(lever, {duration: .6, rotation:  Math.PI  * -15 / 180})
        gsap.to(bikeContainer, {
            duration:.4,
            x: window.innerWidth - bikeContainer.width, 
            y: window.innerHeight - bikeContainer.height
        })
        btnContainer.addChild(btn_circle);
    })
 })


粒子容器

粒子效果是本案例的难点,因为涉及到动画,运动方向,随机颜色位置等,多种展示情况。接下来,我们慢慢分析下粒子的整体流程,大致分为一下几步:

  • 创建粒子
  • 随机颜色
  • 运动方向
  • 循环运行持续移动
  • 按下按钮停止回弹
  • 抬起粒子回弹效果

创建粒子

使用 Container 创建粒子容器,使用 Graphics 画笔工具创建粒子也就是小圆点, 随机设置圆点位置后添加到容器中。

creatParticle() {
    const particleContaner = new PIXI.Container()
    // 设置容器圆心是屏幕中心
    particleContaner.pivot.x = window.innerWidth/2;
    particleContaner.pivot.y = window.innerHeight/2;
    // 容器位置屏幕中心
    particleContaner.x = window.innerWidth/2;
    particleContaner.y = window.innerHeight/2;
    for( let i=0; i<10; i++) {
        const particle = new PIXI.Graphics();
        particle.beginFill("#666")
        // 随机位置画圆
        const x = Math.random() * window.innerWidth;
        const y = Math.random() * window.innerHeight;
        particle.drawCircle(0, 0, 6);
        particle.endFill();
        let pItem = {
            sx: x,
            sy: y,
            gr: particle
        }
        particle.x = x;
        particle.y = y;
        particleContaner.addChild(particle)
    }
}

随机颜色

首先定义个数组colors设置几个要展示的颜色,然后for循环中,使用Math.random随机获取数组下标,通过particle.beginFill然后给粒子设置颜色。

// creatParticle.js
const particleContaner = new PIXI.Container();
// 粒子颜色
const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x000000];
for (let i = 0; i < 10; i++) {
  const particle = new PIXI.Graphics();
  const color = colors[Math.floor(Math.random() * colors.length)];
  particle.beginFill(color);
}

运动方向

案例中看到, 粒子的运行方向是倾斜运行, 指定一个粒子按照倾斜角度的方向运行,那么先算一个倾斜的角度,按照这个角度向量持续移动的去移动。这样实现是不是很复杂,我们可以换个角度思考,每个粒子都是在容器中,如果容器按照倾斜角度旋转,粒子按照 Y 轴向下持续运行,这样就简单多了。

// creatParticle.js
const particleContaner = new PIXI.Container();
// 设置圆心
particleContaner.pivot.x = window.innerWidth / 2;
particleContaner.pivot.y = window.innerHeight / 2;
// 设置屏幕中心
particleContaner.x = window.innerWidth / 2;
particleContaner.y = window.innerHeight / 2;
// 容器倾斜35°
particleContaner.rotation = (35 * Math.PI) / 180;

粒子持续运行

让粒子在 Y 轴持续运行,定义个 move 函数,初始化速度为 0, Y 轴根据时间间隔累加速度,限定最大速度为 20,而且当粒子移动超出范围时回到顶部,那么 Y 轴大于页面宽就赋值 0

// creatParticle.
let speed = 0;
const move = () => {
  speed += 0.5;
  speed = Math.min(speed, 20);
  particles.forEach((item, i) => {
    let pItem = particles[i];
    pItem.gr.y += speed;
    // 当粒子移动超出范围时回到顶部
    if (pItem.gr.y > innerWidth) pItem.gr.y = 0;
  });
};
gsap.ticker.add(move);

我们继续优化下动画效果,粒子快速运行,小球变细会符合人类视觉习惯,这个看着动画会更加舒服。

// creatParticle.
if (speed >= 20) {
  pItem.gr.scale.x = 0.03;
  pItem.gr.scale.y = 40;
}

粒子停止回弹

移除粒子运动,然后通过 elastic.out 恢复到初始的粒子位置,关联到按钮事件。大功告成!

  pause(){
    const {move} = this
    // 移除粒子运动
    gsap.ticker.remove(move)
    // 粒子回弹动画
    for(let i = 0;i<particles.length;i++){
        let pItem = particles[i];
        pItem.gr.scale.y = 1;
        pItem.gr.scale.x = 1;
        gsap.to(pItem.gr,{duration:.6,x:pItem.sx,y:pItem.sy,ease:'elastic.out'});
    }
  }
  start() {
    this.speed = 0;
		gsap.ticker.add(this.move)
  }
  createButton() {
    ...
    btnContainer.on('mousedown',()=>{
        this.pause()
        btnContainer.on('mouseup',()=>{
            gsap.to(lever, {duration: .6, rotation:  Math.PI  * -15 / 180})
            gsap.to(bikeContainer,{
                duration:.4, 
                x: window.innerWidth - bikeContainer.width,
                y: window.innerHeight - bikeContainer.height
            })
            // btnContainer.addChild(btn_circle);
            this.start()
        })
    })
  }

最后

感谢大帅提供机会参加猿创营活动,本案例非常有创意,也了解 PIXI+GSAP 的基本使用。需要多看文档,多学习。也感谢大家看完本篇文章,希望你也能跟我一起动手感受下动画的魅力。大家多多支持点赞 👍

源码地址:github.com/wfyweb/YCY-…

公众号里搜 大帅老猿,在他这里可以学到很多东西。