前端程序员干私活搞副业必备:Canvas 动效开发实战,PixiJS+GSAP 实现刹车动效 | 猿创营

538 阅读6分钟

前言

这是 @大帅老猿 老师上个月接的一个私活,这个私活一共要做 6 个动效,工期 15 天(实际用时 7 天),报酬 2w。而今天我们要实现的这个效果就是其中一个稍有难度的。

最终效果

是不是很酷哇?

最终效果.gif

废话不多说,开始搬砖

html 中引入 PixiJS 和 GSAP

<html>
  <head>
    <title>PixiJS+GSAP 实现刹车动效</title>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width,minimum-scale=1.0,user-scalable=no"
    />
    <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>
      window.onload = () => {
          // 这里开始写核心代码
          // 后面的代码都写在这里面
      }
    </script>
  </head>

  <body>
  </body>
</html>

PixiJS 是什么?

  • 一个 html5 的一个 2D WebGL 渲染引擎,简单地说就是一个图形渲染库
  • 支持交互,可以处理点击和触摸事件
  • 是 Flash 的替代品
  • 它非常适合网络游戏、教育内容、互动广告、数据可视化......

GSAP 是什么?

  • 从 Flash 时代一直发展到今天的老牌的前端专业动画库。

创建一个新的 PIXI.Application 实例

    const app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      backgroundColor: 0xffffff,
      resizeTo: window,
    });

将画布添加到视图

上一步创建的 app 实例上有一个属性 view,这个 view 实际上就是一个 Canvas 元素。

接着,我们将这个元素添加到页面的 Dom 上。

document.body.appendChild(app.view);

加载备用的图片资源

使用资源加载器加载所要需要用到的资源文件

    // 创建资源加载器
    const loader = new PIXI.Loader();

    // 向资源加载器添加资源
    loader.add("bg.jpeg", "images/bg.jpeg"); //背景图片
    loader.add("btn.png", "images/btn.png"); // 按钮
    loader.add("btn_circle.png", "images/btn_circle.png"); // 按钮的外圈(美化用)
    loader.add("brake_bike.png", "images/brake_bike.png"); // 车架
    loader.add("brake_handlerbar.png", "images/brake_handlerbar.png"); // 车龙头
    loader.add("brake_lever.png", "images/brake_lever.png"); // 刹车把
    loader.load();
    // 加载完成后的回调
    loader.onComplete.add(() => {
        // 后面的代码都写在这里面
    });

绘制背景图片

在 PixiJS 中,绘制图片的方式有很多种,其中最简单的就是使用 Sprite

  // 绘制背景图片
  const bgImage = new PIXI.Sprite(loader.resources["bg.jpeg"].texture);
  // 调整背景图片缩放比例,使其完整清晰(按需调节,图片刚好合适则无需调节)
  bgImage.scale.x = 30; // 横轴方式放大 30 倍
  bgImage.scale.y = 3; // 横轴方式放大 3 倍
  // 将图片放入到 stage (实际也是一个容器`Container`,可以理解为应用的根容器)
  app.stage.addChild(bgImage);

绘制控制按钮

每个独立的组件,比如这里的控制按钮就是一个整体,建议通过new PIXI.Container()新建一个容器来存放,而不是直接存入到 stage (根容器),这样做可以方便做整体的调整。

  const btnImage = new PIXI.Sprite(loader.resources["btn.png"].texture);
  // 新建一个容器来存放这个按钮
  const actionButton = new PIXI.Container();
  // 这个容器里面可以通过`addChild`来放入子组件
  actionButton.addChild(btnImage);
  // 调整按钮的坐标位置
  actionButton.x = actionButton.y = 300;
  // 再将整个按钮容器放入到 stage (根容器)
  app.stage.addChild(actionButton);

效果图

image.png

绘制自行车

自行车包括三部分:车架、车龙头、刹车手柄。

新建一个容器,把它们放在同一个容器里,方便整体缩放。

  // 在场景中创建一个整体容器来存放自行车,方便整体的调整。而不是直接在 stage (根容器)上画
  const bikeContainer = new PIXI.Container();
  app.stage.addChild(bikeContainer);
  // 因自行车图片较大,这里整体缩小下
  bikeContainer.scale.x = bikeContainer.scale.y = 0.3;

  // 车架
  const bikeImage = new PIXI.Sprite(
    loader.resources["brake_bike.png"].texture
  );
  bikeContainer.addChild(bikeImage);

  // 刹车手柄
  const bikeLeverImage = new PIXI.Sprite(
    loader.resources["brake_lever.png"].texture
  );
  bikeContainer.addChild(bikeLeverImage);

  // 车龙头
  const bikeHandlerBarImage = new PIXI.Sprite(
    loader.resources["brake_handlerbar.png"].texture
  );
  bikeContainer.addChild(bikeHandlerBarImage);

  // 调整刹车手柄到合适的位置
  // 中心点的位置
  bikeLeverImage.pivot.x = 455;
  bikeLeverImage.pivot.y = 455;
  // 起点的位置
  bikeLeverImage.x = 722;
  bikeLeverImage.y = 900;
  // 逆时针旋转 12 度
  bikeLeverImage.rotation = (Math.PI / 180) * -12;

调整自行车到右下角

因为车体图片不完整,为了美观,使它保持在画面右下角

  const resize = () => {
    bikeContainer.x = window.innerWidth - bikeContainer.width;
    bikeContainer.y = window.innerHeight - bikeContainer.height;
  };
  window.addEventListener("resize", resize);
  resize();

效果图:

image.png

实现按钮的交互效果

  // `interactive` 属性需设为 `true` ,按钮才可交互
  actionButton.interactive = true;
  // 让鼠标放上时鼠标呈手指状
  actionButton.buttonMode = true;

  // 鼠标按下时事件
  actionButton.on("mousedown", () => {
    gsap.to(bikeLeverImage, {
      duration: 0.6,
      rotation: (Math.PI / 180) * -30,
    });
  });

  // 鼠标抬起时事件
  actionButton.on("mouseup", () => {
    gsap.to(bikeLeverImage, {
      duration: 0.6,
      rotation: (Math.PI / 180) * -12,
    });
  });

效果图:

未命名.gif

实现粒子效果

绘制出不同颜色的多个点,然后让这些点动起来

绘制粒子

  // 将这些点存放在对象数组中
  const particles = [];
  const dots = [
    { x: 10, y: 10, radius: 6, color: 0x000000 },
    { x: 10, y: 200, radius: 6, color: 0x818181 },
    { x: 10, y: 400, radius: 6, color: 0xf1cf54 },
    { x: 210, y: 400, radius: 6, color: 0xb5cea8 },
    { x: 210, y: 10, radius: 6, color: 0xf1cf54 },
    { x: 210, y: 200, radius: 6, color: 0xff0000 },
    { x: 410, y: 400, radius: 6, color: 0xff0000 },
    { x: 410, y: 10, radius: 6, color: 0xff0000 },
    { x: 410, y: 200, radius: 6, color: 0xff0000 },
    { x: 610, y: 400, radius: 6, color: 0xff0000 },
    { x: 610, y: 10, radius: 6, color: 0xff0000 },
    { x: 610, y: 200, radius: 6, color: 0xff0000 },
    { x: 810, y: 400, radius: 6, color: 0xff0000 },
    { x: 810, y: 10, radius: 6, color: 0xff0000 },
    { x: 810, y: 200, radius: 6, color: 0xff0000 },
    { x: 100, y: 100, radius: 6, color: 0xffff00 },
    { x: 100, y: 300, radius: 6, color: 0xffff00 },
    { x: 300, y: 100, radius: 6, color: 0xffff00 },
    { x: 300, y: 300, radius: 6, color: 0xffff00 },
    { x: 500, y: 100, radius: 6, color: 0xffff00 },
    { x: 500, y: 300, radius: 6, color: 0xffff00 },
    { x: 700, y: 100, radius: 6, color: 0xffff00 },
    { x: 700, y: 300, radius: 6, color: 0xffff00 },
  ];
  for (let i = 0; i < dots.length; i++) {
    const gr = new PIXI.Graphics();
    const item = dots[i];
    gr.beginFill(item.color);
    gr.drawCircle(0, 0, item.radius);
    gr.endFill();
    gr.x = item.x;
    gr.y = item.y;
    particleContainer.addChild(gr);
    particles.push({ gr: gr, ...item });
  }

效果图:

image.png

让粒子在竖直方向上动起来,并且是越来越快,直至达到最大速度

通过gsap.ticker.add();添加一个屏幕渲染时机的钩子,实际上,就是requestAnimationFrame事件

  // 初始速度为 0
  let speed = 0;
  start();
  function start() {
    // 动起来
    speed = 0;
    gsap.ticker.add(loop);
  }

  function loop() {
    // 越来越快
    speed += 0.5;
    // 限制最大速度
    speed = Math.min(speed, 20);
    for (let i = 0; i < particles.length; i++) {
      const item = particles[i];
      const gr = item.gr;
      // 在y轴方向上动起来
      gr.y += speed;

      // 超出底部边界后回到顶部继续移动
      if (gr.y > window.innerHeight) {
        gr.y = 0;
      }
    }
  }

效果图:

屏幕录制2022-07-30-上午1.31.48.gif

让粒子朝一个斜角方向移动

简单!只需要旋转容器到一定角度即可

  // 改变容器的中心点为页面的中心
  particleContainer.x = window.innerWidth / 2;
  particleContainer.y = window.innerHeight / 2;
  particleContainer.pivot.x = particleContainer.x;
  particleContainer.pivot.y = particleContainer.y;
  // 顺时针转 35 度
  particleContainer.rotation = (35 * Math.PI) / 180;

让粒子有速度线的视觉效果

其实就是让粒子拉长、变细。

也就是让粒子在 x 方向上缩小,在 y 方向上放大。

修改 loop 函数:

  function loop() {
    // 越来越快
    speed += 0.5;
    // 限制最大速度
    speed = Math.min(speed, 20);
    for (let i = 0; i < particles.length; i++) {
      const item = particles[i];
      const gr = item.gr;
      // 在y轴方向上动起来
      gr.y += speed;
      // 速度达到某个值时,制造出速度线效果
      if (speed > 19) {
        gr.scale.x = 0.05;
        gr.scale.y = 40;
      }
      // 超出底部边界后回到顶部继续移动
      if (gr.y > window.innerHeight) {
        gr.y = 0;
      }
    }
  }

控制粒子的运动

按钮按下时停止运动,按钮松开时重新开始

  // 鼠标按下时事件
  actionButton.on("mousedown", () => {
    gsap.to(bikeLeverImage, {
      duration: 0.6,
      rotation: (Math.PI / 180) * -30,
    });

    pause();
  });

  // 鼠标抬起时事件
  actionButton.on("mouseup", () => {
    gsap.to(bikeLeverImage, {
      duration: 0.6,
      rotation: (Math.PI / 180) * -12,
    });
    start();
  });
  
  
  function pause() {
    // 先移除掉 requestAnimationFrame 的侦听
    gsap.ticker.remove(loop);
    for (let i = 0; i < particles.length; i++) {
      const item = particles[i];
      const gr = item.gr;

      // 恢复小圆点的拉伸比例
      gr.scale.x = 1;
      gr.scale.y = 1;

      // 让所有的小圆点使用弹性补间动画回到初始坐标
      gsap.to(gr, {
        duration: 0.6,
        x: item.x,
        y: item.y,
        ease: "elastic.out",
      });
    }
  }

效果图:

屏幕录制2022-07-30-上午1.54.44.gif

美化按钮

最后,我们来美化下按钮,给按钮加上一个外圈,并且加上波纹动效。

  • 绘制两个圆圈
  • 让其中一个圆圈从小到大无限循环
  beautifyButton();

  function beautifyButton() {
    // 绘制两个圆圈
    const btnCircle = new PIXI.Sprite(
      loader.resources["btn_circle.png"].texture
    );
    const btnCircle2 = new PIXI.Sprite(
      loader.resources["btn_circle.png"].texture
    );

    actionButton.addChild(btnCircle);
    actionButton.addChild(btnCircle2);

    // 改变中心点
    btnImage.pivot.x = btnImage.width / 2;
    btnImage.pivot.y = btnImage.height / 2;
    btnCircle.pivot.x = btnCircle.width / 2;
    btnCircle.pivot.y = btnCircle.height / 2;
    btnCircle2.pivot.x = btnCircle2.width / 2;
    btnCircle2.pivot.y = btnCircle2.height / 2;

    // 让其中一个圆圈从小到大无限循环
    btnCircle.scale.x = btnCircle.scale.y = 0.8;
    gsap.to(btnCircle.scale, {
      duration: 1,
      x: 1.2,
      y: 1.2,
      repeat: -1,
    });
    gsap.to(btnCircle, { duration: 1, alpha: 0, repeat: -1 });
  }

大功告成~

小结

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