【学习记录】我用 PIXI 和 GSAP 仿写 vanmoof 刹车动效 | 猿创营

64 阅读3分钟

chrome-capture-2023-0-5.gif

最近加入了大帅老师带领的猿创营学习,在看完大帅在哔哩哔哩的直播后,自己也动手实现了该动效。尝试写一下软文记录自己的收获与成长(第一次在掘金(网上)发表文章,其实心里慌得一批)。

准备工作

  1. 大帅老师的github仓库把工程克隆到本地。
  2. 修改 src/index.html 下的 pixi.js 引用路径。(因为 pixi 更新了方法有所改变,换回旧版就可以了)
  3. 在 vscode 安装微软的 Live Preview 插件,该插件可以在编辑器里面实时的预览代码的执行结果。

初始化页面

// index.html
<html>
<head>
    <title>猿创营</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,minimum-scale=1.0,user-scalable=no">
    <style type="text/css">
        html,
        body {
            margin: 0;
            padding: 0;
        }
        div {
            width: 100%;
        }
    </style>
    <script src="https://pixijs.download/v6.5.8/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 = init;
        function init() {
            let banner = new BrakeBanner("#brakebanner");
        }
    </script>
</head>
<body>
    <div id="brakebanner"></div>
</body>
</html>

创建PIXI应用

// brakebanner.js
class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      backgroundColor: 0xff0000,
      resizeTo: window,
    });

    document.querySelector(selector).appendChild(this.app.view);
  }
}

效果如下

chrome-capture-2023-0-5.png

引入素材

// brakebanner.js
class BrakeBanner {
  constructor(selector) {
      ...省略未改动代码
      backgroundColor: 0xffffff, // 背景改为白色
    });
      ...省略未改动代码      
    this.stage = this.app.stage;
    this.loader = new PIXI.Loader();
    // 添加素材
    this.loader.add("btn.png", "images/btn.png");
    this.loader.add("btn_circle.png", "images/btn_circle.png");
    this.loader.add("brake_bike.png", "images/brake_bike.png");
    this.loader.add("brake_handlerbar.png", "images/brake_handlerbar.png");
    this.loader.add("brake_lever.png", "images/brake_lever.png");
    // 执行 load 方法加载素材
    this.loader.load();
    // 添加回调函数,待加载完素材后显示素材
    this.loader.onComplete.add(() => {
      this.show(); // 执行 show 方法显示素材
    });
  }
}

添加 show 方法

// brakebanner.js
class BrakeBanner {
  ...省略未改动代码
  show(){
      // 创建 bike
      let bikeContainer = this.createBikeContainer();
      // 调整 bike 大小
      bikeContainer.scale.x = bikeContainer.scale.y = 0.3;
      // 把 bike 添加到场景
      this.stage.addChild(bikeContainer);
      // 创建按钮
      let actionButton = this.createActionButton();
      // 调整按钮位置
      actionButton.x = actionButton.y = 300;
      // 把按钮添加到场景
      this.stage.addChild(actionButton);
      // 添加按钮互动效果
      actionButton.interactive = true;
      actionButton.buttonMode = true;
      // 调整 bike 位置
      // 添加 resize 方法
      let resize = () => {
          bikeContainer.x = window.innerWidth - bikeContainer.width;
          bikeContainer.y = window.innerHeight - bikeContainer.height;
      };
      // 添加 resize 监听事件
      window.addEventListener("resize", resize);
      // 执行 resize 方法
      resize();
  }
}

添加 createBikeContainer 方法

// brakebanner.js
class BrakeBanner {
  ...省略未改动代码
  createBikeContainer() {
    // 创建容器
    let bikeContainer = new PIXI.Container();
    // 创建精灵
    let bikeImage = new PIXI.Sprite(
      this.loader.resources["brake_bike.png"].texture
    );
    let bikeHandlerbarImage = new PIXI.Sprite(
      this.loader.resources["brake_handlerbar.png"].texture
    );
    let bikeLeverImage = new PIXI.Sprite(
      this.loader.resources["brake_lever.png"].texture
    );
    // 把精灵添加到容器
    bikeContainer.addChild(bikeImage);
    bikeContainer.addChild(bikeLeverImage);
    bikeContainer.addChild(bikeHandlerbarImage);
    // 调整把手轴心
    bikeLeverImage.pivot.x = bikeLeverImage.pivot.y = 455;
    // 调整把手位置
    bikeLeverImage.x = 722;
    bikeLeverImage.y = 900;
    return bikeContainer;
  }
}

添加 createActionButton 方法

// brakebanner.js
class BrakeBanner {
  ...省略未改动代码
  createActionButton() {
    // 创建容器
    let actionButton = new PIXI.Container();
    // 创建精灵
    let btnImage = new PIXI.Sprite(this.loader.resources["btn.png"].texture);
    let btnCircleImage = new PIXI.Sprite(
      this.loader.resources["btn_circle.png"].texture
    );
    let btnCircleImage2 = new PIXI.Sprite(
      this.loader.resources["btn_circle.png"].texture
    );
    // 把精灵添加到容器
    actionButton.addChild(btnImage);
    actionButton.addChild(btnCircleImage);
    actionButton.addChild(btnCircleImage2);
    // 调整精灵轴心
    btnImage.pivot.x = btnImage.pivot.y = btnImage.width / 2;
    btnCircleImage.pivot.x = btnCircleImage.pivot.y = btnCircleImage.width / 2;
    btnCircleImage2.pivot.x = btnCircleImage2.pivot.y = btnCircleImage2.width / 2;
    // 缩小 btnCircleImage
    btnCircleImage.scale.x = btnCircleImage.scale.y = 0.8;
    return actionButton;
  }
}

效果如下

chrome-capture-2023-0-5.png

添加按钮动效

修改 createActionButton 方法

  createActionButton() {
    ...省略未改动代码
    // 添加 gsap 动效
    gsap.to(btnCircleImage.scale, {
      duration: 1, // 动效时长
      x: 1.3, // x 缩放比例
      y: 1.3, // y 缩放比例
      repeat: -1, // 无限循环
    });
    gsap.to(btnCircleImage, {
      duration: 1, // 动效时长
      alpha: 0, // 透明度
      repeat: -1, // 无限循环
    });
    ...省略未改动代码
  }

效果如下

chrome-capture-2023-0-5.gif

添加把手互动

修改 show 方法

  show() {
    ...省略未改动代码
    // 添加鼠标按下事件
    actionButton.on("mousedown", () => {
      // 添加动效---把手逆时针旋转30°
      gsap.to(bikeContainer.children[1], {
        duration: 0.6,
        rotation: (Math.PI / 180) * -30,
      });
    });
    // 添加鼠标抬起事件
    actionButton.on("mouseup", () => {
      // 添加动效---把手复原
      gsap.to(bikeContainer.children[1], {
        duration: 0.4,
        rotation: 0,
      });
    });
    ...省略未改动代码
  }

效果如下

chrome-capture-2023-0-5.gif

添加速度线动效

创建粒子

  show() {
    ...省略未改动代码
    // 创建粒子容器
    let particleContainer = new PIXI.Container();
    // 把粒子容器添加到场景
    this.stage.addChild(particleContainer);
    // 调整容器轴心
    particleContainer.pivot.x = window.innerWidth / 2;
    particleContainer.pivot.y = window.innerHeight / 2;
    // 调整容器位置
    particleContainer.x = window.innerWidth / 2;
    particleContainer.y = window.innerHeight / 2;
    // 旋转容器
    particleContainer.rotation = (Math.PI / 180) * 35;
    // 粒子数组
    let particles = [];
    // 粒子颜色
    let colors = [0xf1cf54, 0xb5cea8, 0x818181, 0x000000];
    // 创建粒子
    for (let i = 0; i < 10; i++) {
      let gr = new PIXI.Graphics();
      // 填充粒子颜色
      gr.beginFill(colors[Math.floor(Math.random() * colors.length)]);
      // 绘制圆形粒子
      gr.drawCircle(0, 0, 4);
      gr.endFill();
      // 粒子初始信息
      let pItem = {
        sx: Math.random() * window.innerWidth,
        sy: Math.random() * window.innerHeight,
        gr: gr,
      };
      // 调整粒子位置
      gr.x = pItem.sx;
      gr.y = pItem.sy;
      // 添加到容器
      particleContainer.addChild(gr);
      // 添加到数组
      particles.push(pItem);
    }
  }

添加粒子动效

  show() {
    ...省略未改动代码
    // 设置初始速度
    let speed = 0;
    // 添加动效方法
    function loop() {
      speed += 0.5;
      // 设置速度最大值
      speed = Math.min(speed, 20);
      for (let i = 0; i < particles.length; i++) {
        let pItem = particles[i];
        // 让粒子动起来
        pItem.gr.y += speed;
        // 粒子移动速度大于等于20时形状变化
        if (speed >= 20) {
          pItem.gr.scale.y = 40; // y 拉长至 40
          pItem.gr.scale.x = 0.06; // x 压缩至 0.06
        }
        // 粒子超出屏幕高度返回最高点
        if (pItem.gr.y > window.innerHeight) pItem.gr.y = 0;
      }
    }
    // 添加 start 方法
    function start() {
      // 初始化速度
      speed = 0;
      // 添加 gsap 动效
      gsap.ticker.add(loop);
    }
    // 执行 start 方法
    start();
  }

添加按钮互动

  show() {
    ...省略未改动代码
    // 修改鼠标按下事件
    actionButton.on("mousedown", () => {
      ...省略未改动代码
      pause();
    });
    // 修改鼠标抬起事件
    actionButton.on("mouseup", () => {
      ...省略未改动代码
      start();
    });
    // 添加 pause 方法
    function pause() {
      // 移除粒子动效
      gsap.ticker.remove(loop);
      for (let i = 0; i < particles.length; i++) {
        let pItem = particles[i];
        // 复原粒子
        pItem.gr.scale.x = 1;
        pItem.gr.scale.y = 1;
        // 添加粒子停止回弹动效
        gsap.to(pItem.gr, {
          duration: 0.6,
          x: pItem.sx,
          y: pItem.sy,
          ease: "elastic.out",
        });
      }
    }
    ...省略未改动代码
  }

最终效果

chrome-capture-2023-0-5.gif

总结

  • 通过学习大帅老师的刹车动效项目后,我对pixi.js、gsap.js有了初步的认识。
  • 填补了我对 canvas 或 css 动画这类技术的不足。
  • 要多了解一些国外比较优秀的技术。
  • 在公众号里搜 大帅老猿,在他这里可以学到很多东西!