首次接触 PIXI 和 gsap 能否做出刹车视觉动效?简单的很!【爆肝万字】手摸手教! | 猿创营

背景介绍

大家好,我是梅利奥猪猪,你们的反内卷盟主,一个喜欢撸码和打游戏的咸鱼!这次又来更新我的掘金了!(不知道隔了多久)先来一发对自己的灵魂三问!

  • 问:为什么隔了这么久才写文章?
  • 答:因为我作为反内卷盟主,厌倦加班就转行了,并且刚入职了新工作(肯定不会 996 or 007)!当中也经历了很多事吧,工作的准备,外加空下来的时间玩游戏也比较多,所以就没写(罪恶中...)
  • 问:那转行了为什么还要写技术文章呢
  • 答:因为还喜欢撸码!并且新工作其实还是会接触代码的!
  • 问:那你还是个喜欢打游戏的咸鱼吗?
  • 答:那必须的,我的确在休息的时间躺平打游戏,但偶尔学习下还是要记录下写写博客的,所以这篇文章它来叻

不皮了不皮了,其实写文章的真正原因,当然是有大帅大佬和群里(猿创营)的小伙伴在卷我(滑稽脸),也真的是因为这次的这个案例效果的确不错!熟悉我的朋友,知道我是一名canvas菜鸡玩家,但却深深的被canvas吸引,所以入行前端也是因为当初喜欢写 canvas 各种效果啦,小游戏啦!具体有兴趣的也可以看下我的codepen,里面还是有些canvas特效和小游戏的例子的!接下去开始我们的正题!手摸手教写刹车动效!Let's go!

效果展示

先来看下我们要做的东西是什么(要让大家知道,我们在哪,我们要做什么),打开案例网址,鼠标滚轮滑到刹车这边,然后点击按钮就可以看到了(什么,链接加载不出来,那来看下我为大家准备的 gif 效果图 吧)

vanmoof-刹车.gif

什么这么酷炫?我们真的能做成这个样子吗!在没了解过PIXIgsap的时候,感觉好像有点难啊!但接触后,在跟着大帅大佬一起学习,这效果 so easy!(下图是我们可以做出的效果)

最终开发效果展示.gif

相关文档

前面提到了 2 个技术栈,也是我们标题上提到的PIXIgsap,虽然我是一名 canvas 玩家,但 PIXIgsap 可是第一次接触,如何学习,当然先从文档看起!XDM,官方文档链接拿去,之后我们所有的手摸手教学,都会结合文档,带着大家一起做

开发步骤(手摸手教)

fork 仓库(准备工作-准备架子)

首先当然是准备工作啦,把大帅大佬给我们准备好的架子拿过来!拿来吧你,这里是架子的仓库链接,XDM 把仓库 fork 下来,然后 clone 到自己本地,之后再用 vscode 打开,映入眼帘的就是

架子.jpg

那对整个项目的架子大家应该清楚了,我们可以切个分支出来,开始我们的PIXIgsap之旅

创建 PIXI 应用

先来看文档

XDM,接下去我们就要创建 PIXI 应用了。有朋友可能就会觉得,什么跨度太大了吧,我 PIXI 是什么都不清楚,所以我们可以打开我们的文档链接,找到 API DOCS,如下图所示

文档指路人.jpg

然后我们就进入了文档的首页,不得不说,pixi 的文档说明还是可以的,我们往下滑,就能看到基础的用法,如下图所示

文档首页说明.jpg

直接复制上述代码(cv 后做少许改动)

我们直接复制文档里的代码,但要做少许的调整只要改 2 处就可以,代码如下,我会加上注释带着大家一起玩

class BrakeBanner {
  constructor(selector) {
    const app = new PIXI.Application();

    // 文档里直接在body中添加了app.view,但我们有对应的选择器,所以需要修改
    // document.body.appendChild(app.view);

    // 更改的第一处,用我们自己的选择器
    document.querySelector(selector).appendChild(app.view);

    /**
     * 我们的图片比如先加载个btn
     * 为了偷懒,我们的变量就不改了,key还是用bunny,
     * 解释 app.loader.add("bunny", "bunny.png") add函数的第一个参数就是key,我们这边不改
     * 但是,我们引入图片的路径需要更改
     * 更改的第二处:app.loader.add("bunny", "images/btn.png")
     */
    app.loader.add("bunny", "images/btn.png").load((loader, resources) => {
      const bunny = new PIXI.Sprite(resources.bunny.texture);

      bunny.x = app.renderer.width / 2;
      bunny.y = app.renderer.height / 2;

      bunny.anchor.x = 0.5;
      bunny.anchor.y = 0.5;

      app.stage.addChild(bunny);

      app.ticker.add(() => {
        bunny.rotation += 0.01;
      });
    });
  }
}
复制代码

LiveServer 打开浏览器看效果了

复制好改完代码,当然是打开我们的 index.html 来看下效果,效果如下

pixi_helloworld.gif

好开心啊,复制复制代码就出了效果,但我们还是要熟悉下 hello world 的例子的,上述代码中有些关键的类,比如Application, Sprite等,那我们先来看 Application 吧

删除我们 hello world 的代码,根据文档手写 Application

打开我们的文档搜索 Application

此时看到了一堆参数,并且又有了一个 example 供我们参考,参考代码如下

// Create the application
const app = new PIXI.Application();

// Add the view to the DOM
document.body.appendChild(app.view);

// ex, add display objects
app.stage.addChild(PIXI.Sprite.from("something.png"));
复制代码

因为考虑到我们的 app 实例和 stage 会经常用到,所以我们代码可以挂载到实例上,写成这样

class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application();
    document.querySelector(selector).appendChild(this.app.view);
    this.stage = this.app.stage;
  }
}
复制代码

此时此刻我们就发现了页面中添加了 canvas,并且是黑色的一小块

默认参数添加canvas.jpg

那我们做个全屏的特效总不能用默认的宽高(800*600)吧,所以我们传入我们的参数

class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application({
+++      width: window.innerWidth,
+++      height: window.innerHeight,
    });
    document.querySelector(selector).appendChild(this.app.view);
    this.stage = this.app.stage;
  }
}
复制代码

这个时候舒服了,我们的 canvas 撑满了我们的浏览器,但同时又有个问题,拖动控制台,发现 canvas 并没有随着我们 window 的大小变化而变化,如下图

canvas尺寸问题.gif

那这个怎么办呢,别急,我们在文档中看到了这个resizeTo

resizeTo.jpg

所以我们加上这个参数在来试下,最终代码如下

class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
+++      resizeTo: window,
    });
    document.querySelector(selector).appendChild(this.app.view);
    this.stage = this.app.stage;
  }
}

复制代码

经过调试,我们的 application 创建圆满成功,也能自动的 resize 我们 canvas 的大小了

resizeTo效果.gif

添加图片素材

熟悉 Loader

前面我们把 application 玩爽了,接下去就要开始玩耍我们的 loader 了,老套路,我们先来找文档,打开Loader 的链接

文档真好,又给了我们一个 example,代码如下

const loader = PIXI.Loader.shared; // PixiJS exposes a premade instance for you to use.
// or 注意了这里是or
const loader = new PIXI.Loader(); // You can also create your own if you want

const sprites = {};

// Chainable `add` to enqueue a resource
loader
  .add("bunny", "data/bunny.png")
  .add("spaceship", "assets/spritesheet.json");
loader.add("scoreFont", "assets/score.fnt");

// Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource.
// This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc).
loader.pre(cachingMiddleware);

// Chainable `use` to add a middleware that runs for each resource, *after* loading that resource.
// This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc).
loader.use(parsingMiddleware);

// The `load` method loads the queue of resources, and calls the passed in callback called once all
// resources have loaded.
loader.load((loader, resources) => {
  // resources is an object where the key is the name of the resource loaded and the value is the resource object.
  // They have a couple default properties:
  // - `url`: The URL that the resource was loaded from
  // - `error`: The error that happened when trying to load (if any)
  // - `data`: The raw data that was loaded
  // also may contain other properties based on the middleware that runs.
  sprites.bunny = new PIXI.TilingSprite(resources.bunny.texture);
  sprites.spaceship = new PIXI.TilingSprite(resources.spaceship.texture);
  sprites.scoreFont = new PIXI.TilingSprite(resources.scoreFont.texture);
});

// throughout the process multiple signals can be dispatched.
loader.onProgress.add(() => {}); // called once per loaded/errored file
loader.onError.add(() => {}); // called once per errored file
loader.onLoad.add(() => {}); // called once per loaded file
loader.onComplete.add(() => {}); // called once when the queued resources all load.
复制代码

注意看,比如说实例化 loader 这里,文档上有写 or,所以我们可以直接用const loader = new PIXI.Loader();,比如他还有个 sprites 的对象,我们也可以参考,把精灵都放在这个对象上等

代码的搬运工,抄写 loader

为了方便我们使用 loader,我们一边抄代码,一边把该挂载的挂载到我们实例上,并在适度的封装,代码如下

class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      resizeTo: window,
    });
    document.querySelector(selector).appendChild(this.app.view);
    this.stage = this.app.stage;

+++    this.loader = new PIXI.Loader();
+++    this.sprites = {};
+++    this.loadImages(); // 封装个方法专门加载图片
  }
  /**
   * 以下代码全是新加的,是根据官方文档的示例改写封装
   */
  loadImages() {
    const imageNameArr = [
      "brake_bike",
      "brake_handlerbar",
      "brake_lever",
      "btn_circle",
      "btn",
    ];
    imageNameArr.forEach((imageName) => {
      // 先要load我们的素材,key值就用我们的图片名
      this.loader.add(imageName, `images/${imageName}.png`);
    });
    this.loader.load((loader, resources) => {
      // 这个回调函数就是已经加载好图片了
      imageNameArr.forEach((imageName) => {
        // 这里使用Sprite精灵,并且把值赋值给对象sprites上,key也用的是图片名
        this.sprites[imageName] = new PIXI.Sprite(resources[imageName].texture);
      });
      // 然后我们可以做一下显示的逻辑
      this.show();
    });
  }

  show() {
    // 先随便添加个按钮试试
    this.stage.addChild(this.sprites.btn);
  }
}
复制代码

查看图片是否渲染 - 代码抄完当然继续看结果啦

因为我们用的是 live-server,所以可以直接看到结果

loader初次使用.jpg

那我们 loader 添加图片素材的玩耍到此也告一段落了

搭容器架子

分析思路 - 一共 3 个容器?

现在我们图片素材也会添加了,然后就可以快活猛写代码了吧!别急 XDM,我们在分析下后面要写些什么

上图所示,已经明确了,之后我们要处理三个容器,分别是

  • 按钮容器 - 渲染按钮,处理按下抬起的事件!
  • 车容器 - 渲染车车,还有刹车可以按下松开哦!
  • 粒子容器 - 渲染粒子,华丽闪瞎产品的眼睛!

分析.jpg

简单写些代码,记录我们的思路

以下代码根据我们上述分析,又再次搭个架子(全新的代码),宏观的思路就是这些!

  show() {
    const { btnContainer } = this.createBtn();
    this.stage.addChild(btnContainer);

    const { bikeContainer } = this.createBike();
    this.stage.addChild(bikeContainer);

    const { particlesContainer } = this.createParticles();
    this.stage.addChild(particlesContainer);
  }
  createBtn() {
    /**
     * 按钮容器
     * 为什么要有按钮容器呢
     * 因为我们按钮图片一共有2张,纯按钮还有按钮外的那个圈,我们把他作为一个整体
     */
    const btnContainer = new PIXI.Container();
    return {
      btnContainer,
    };
  }
  createBike() {
    /**
     * 车容器
     * 为什么有车容器
     * 因为我们车的图片一共三张,作为一个整体,我们也会对其操作
     * 还要对单独的刹车把手进行操作
     */
    const bikeContainer = new PIXI.Container();
    return {
      bikeContainer,
    };
  }
  createParticles() {
    /**
     * 粒子容器
     * 为什么有粒子容器
     * 因为我们要创建很多粒子,用个粒子容器包着
     * 后续还有个巧妙的思路,让粒子斜着往下掉,拭目以待吧!
     */
    const particlesContainer = new PIXI.Container();
    return {
      particlesContainer,
    };
  }
复制代码

写按钮咯!

渲染出按钮

XDM 还记得前面我们简单搭了个按钮容器嘛,接下来我们就来完成按钮这个功能,首先我们把前面 sprites 的 2 个按钮图片加载到我们场景去吧

  createBtn() {
    /**
     * 按钮容器
     * 为什么要有按钮容器呢
     * 因为我们按钮图片一共有2张,纯按钮还有按钮外的那个圈,我们把他作为一个整体
     */
    const btnContainer = new PIXI.Container();
+++    btnContainer.addChild(this.sprites.btn); // 添加btn图片
+++    btnContainer.addChild(this.sprites.btn_circle); // 添加btn_circle图片
+++    btnContainer.x = window.innerWidth / 2; // 把容器放到x轴中间
+++    btnContainer.y = window.innerHeight / 2; // 把容器放到y轴中间
    return {
      btnContainer,
    };
  }
复制代码

然后效果立竿见影,但有些问题

初次渲染按钮图片.jpg

修复细节问题,支点设置为中心

设置支点为中心,我看了文档应该有 2 种方式,大家可以去搜索 pivot 或者 anchor,代码如下

  createBtn() {
    /**
     * 按钮容器
     * 为什么要有按钮容器呢
     * 因为我们按钮图片一共有2张,纯按钮还有按钮外的那个圈,我们把他作为一个整体
     */
    const btnContainer = new PIXI.Container();
+++    const btn = this.sprites.btn;
+++    const btn_circle = this.sprites.btn_circle;
+++    btn.anchor.set(0.5); // 设置支点为中心
+++    btn_circle.anchor.set(0.5); // 设置支点为中心
+++    // btn.pivot.x = btn.pivot.y = btn.width / 2;
+++    // btn_circle.pivot.x = btn_circle.pivot.y = btn_circle.width / 2;
+++    btnContainer.addChild(btn); // 添加btn图片
+++    btnContainer.addChild(btn_circle); // 添加btn_circle图片
---    btnContainer.addChild(this.sprites.btn);
---    btnContainer.addChild(this.sprites.btn_circle);
    btnContainer.x = window.innerWidth / 2; // 把容器放到x轴中间
    btnContainer.y = window.innerHeight / 2; // 把容器放到y轴中间
    return {
      btnContainer,
    };
  }
复制代码

到这个时候不打开浏览器还愣着干嘛,赶紧看下有没有 work,居中了没有!

按钮居中处理.jpg

好耶我们又完成了按钮的居中,总觉得按钮就快要完成了

按钮 btn_circle 动画

讲到 btn_circle 的动画,这里就又要用到新的知识 gsap,不要慌,反内卷盟主带你看文档以及复制代码!我们主要用到的是gsap.to,所以打开我们的gsap 文档,有示例代码并且还有效果,不错不错,我们先来看下示例代码,好好研究下

/**
 * gsap.to
 * 第一个参数 可以传选择器,也可以传对象 这里的代码传的是选择器
 * 第二个参数是对象 里面的key值和你做动画息息相关,并且分为特殊字段以及对象中自身的属性
 *      - duration 代表时常,你要做的动画是要多久 (Special Properties)
 *      - rotation 代表旋转
 *      - x 代表x方向偏移
 * 示例的代码意思就是,1s中 旋转27并且往右跑100像素
 * duration是特殊字段,类似它的还有repeat,也是特殊字段,代表动画重复多少次,当设置-1的时候循环播放
 */
gsap.to(".box", { rotation: 27, x: 100, duration: 1 });
复制代码

知道大概的用法,我们在来看下精灵对象有哪些属性

查看sprite属性.jpg

那知道这些属性,那我们就可以写着玩一下了

  createBtn() {
    /**
     * 按钮容器
     * 为什么要有按钮容器呢
     * 因为我们按钮图片一共有2张,纯按钮还有按钮外的那个圈,我们把他作为一个整体
     */
    const btnContainer = new PIXI.Container();
    const btn = this.sprites.btn;
    const btn_circle = this.sprites.btn_circle;
    btn.anchor.set(0.5); // 设置支点为中心
    btn_circle.anchor.set(0.5); // 设置支点为中心
    // btn.pivot.x = btn.pivot.y = btn.width / 2;
    // btn_circle.pivot.x = btn_circle.pivot.y = btn_circle.width / 2;
    btnContainer.addChild(btn); // 添加btn图片
    btnContainer.addChild(btn_circle); // 添加btn_circle图片
    btnContainer.x = window.innerWidth / 2; // 把容器放到x轴中间
    btnContainer.y = window.innerHeight / 2; // 把容器放到y轴中间

    /**
     * 该动画处理的是缩放
     * 注意传入第一个参数是btn_circle.scale
     * 0.4s的时间,circle变大1.3倍,且无限做动画
     */
+++    gsap.to(btn_circle.scale, { duration: 0.4, x: 1.3, y: 1.3, repeat: -1 });
    /**
     * 该动画处理的是透明度
     * 注意传入的第一个参数就是btn_circle
     * 0.4s的事件,变透明度为0(即看不到),且无限做动画
     */
+++    gsap.to(btn_circle, { duration: 0.4, alpha: 0, repeat: -1 });
    return {
      btnContainer,
    };
  }
复制代码

btn_circle动画.gif 效果如下

添加点击事件前置条件

pixi 添加点击事情并不是像我们以前document.addEventListener这么容易,它有些前置条件,比如我们看下文档中的 buttonMode,示例代码如下

const sprite = new PIXI.Sprite(texture);
sprite.interactive = true;
sprite.buttonMode = true;
复制代码

buttonMode 是个布尔值一看就知道,设置成按钮的模式,这样我们的鼠标移动到我们的精灵上,就会显示出手指(cursor: pointer)的效果,那interactive又是干嘛用的,我们继续查阅文档

interactive说明.jpg

好家伙,怪不得示例代码里有interactive,那我们就可以开写

  createBtn() {
    /**
     * 按钮容器
     * 为什么要有按钮容器呢
     * 因为我们按钮图片一共有2张,纯按钮还有按钮外的那个圈,我们把他作为一个整体
     */
    const btnContainer = new PIXI.Container();
    const btn = this.sprites.btn;
    const btn_circle = this.sprites.btn_circle;
    btn.anchor.set(0.5); // 设置支点为中心
    btn_circle.anchor.set(0.5); // 设置支点为中心
    // btn.pivot.x = btn.pivot.y = btn.width / 2;
    // btn_circle.pivot.x = btn_circle.pivot.y = btn_circle.width / 2;
    btnContainer.addChild(btn); // 添加btn图片
    btnContainer.addChild(btn_circle); // 添加btn_circle图片
    btnContainer.x = window.innerWidth / 2; // 把容器放到x轴中间
    btnContainer.y = window.innerHeight / 2; // 把容器放到y轴中间

    /**
     * 该动画处理的是缩放
     * 注意传入第一个参数是btn_circle.scale
     * 0.4s的时间,circle变大1.3倍,且无限做动画
     */
    gsap.to(btn_circle.scale, { duration: 0.4, x: 1.3, y: 1.3, repeat: -1 });
    /**
     * 该动画处理的是透明度
     * 注意传入的第一个参数就是btn_circle
     * 0.4s的事件,变透明度为0(即看不到),且无限做动画
     */
    gsap.to(btn_circle, { duration: 0.4, alpha: 0, repeat: -1 });
+++    btnContainer.interactive = true; // 该属性不设置不能绑定事件
+++    btnContainer.buttonMode = true; // 设置成按钮模式,鼠标就能变成手手(cursor: pointer)
    return {
      btnContainer,
    };
  }
复制代码

添加点击事件

处理好前置条件后,当然就可以绑定事件啦,注意我们这里把show方法改名为showAndBindEvents

  // 注意这里我们改了名字
  showAndBindEvents() {
    const { btnContainer } = this.createBtn();
    /**
     * 绑定事件
     */
+++    btnContainer.on("mousedown", () => {
+++      console.log("mousedown");
+++    });
+++    btnContainer.on("mouseup", () => {
+++      console.log("mouseup");
+++    });
    this.stage.addChild(btnContainer);

    const { bikeContainer } = this.createBike();
    this.stage.addChild(bikeContainer);

    const { particlesContainer } = this.createParticles();
    this.stage.addChild(particlesContainer);
  }
复制代码

让我们测试下点击事件好不好使

测试点击事件.gif

非常完美,那到现在为止,我们的整个按钮的处理就先到这里,其实还有很多细节可以处理,比如我们的动画可以在微调,比如我们还可以在渲染个 btn_circle 等等,这边我就不演示了,XDM 自己好好玩哈

写车车咯!

车容器添加素材渲染

因为之前我们渲染了按钮,这里的车车也是一样的,迅速添加图片渲染即可,因为代码比较简单,所以这里就不做多余的注释,简单来说,就是添加素材

  createBike() {
    /**
     * 车容器
     * 为什么有车容器
     * 因为我们车的图片一共三张,作为一个整体,我们也会对其操作
     * 还要对单独的刹车把手进行操作
     */
    const bikeContainer = new PIXI.Container();
+++    const brake_bike = this.sprites.brake_bike;
+++    const brake_handlerbar = this.sprites.brake_handlerbar;
+++    const brake_lever = this.sprites.brake_lever;
+++    bikeContainer.addChild(brake_bike);
+++    bikeContainer.addChild(brake_handlerbar);
+++    bikeContainer.addChild(brake_lever);
    return {
      bikeContainer,
    };
  }
复制代码

车容器初步渲染.jpg

车容器 scale 缩小,并且固定在屏幕右下角

  createBike() {
    /**
     * 车容器
     * 为什么有车容器
     * 因为我们车的图片一共三张,作为一个整体,我们也会对其操作
     * 还要对单独的刹车把手进行操作
     */
    const bikeContainer = new PIXI.Container();
    const brake_bike = this.sprites.brake_bike;
    const brake_handlerbar = this.sprites.brake_handlerbar;
    const brake_lever = this.sprites.brake_lever;
    bikeContainer.addChild(brake_bike);
    bikeContainer.addChild(brake_handlerbar);
    bikeContainer.addChild(brake_lever);
+++    bikeContainer.scale.x = bikeContainer.scale.y = 0.3; // 图片太大我们缩小点
+++    bikeContainer.x = window.innerWidth - bikeContainer.width; // 固定在右下角处理x坐标
+++    bikeContainer.y = window.innerHeight - bikeContainer.height; // 固定在右下角处理y坐标
    return {
      bikeContainer,
    };
  }
复制代码

写完以后,理论上肯定是右下角了,但还是要来看看效果的

车容器固定在右下角.jpg

完美搞定!

处理细节问题并完善点击事件

我们右下角的车容器 resize 后应该也在右下角

前面我们处理右下角的逻辑显然是写死了车容器的坐标,然而我们如果拖动控制台改变 window 的宽高,就是有问题的,所以我们要把对应的逻辑写在 resize 里,可以写在我们的方法showAndBindEvents, 具体代码如下

const { bikeContainer } = this.createBike();
const resize = () => {
  bikeContainer.scale.x = bikeContainer.scale.y = 0.3; // 图片太大我们缩小点
  bikeContainer.x = window.innerWidth - bikeContainer.width; // 固定在右下角处理x坐标
  bikeContainer.y = window.innerHeight - bikeContainer.height; // 固定在右下角处理y坐标
};
resize();
window.addEventListener("resize", resize);
this.stage.addChild(bikeContainer);
复制代码
按钮应该在最上层

按钮在最上层的方式很简单,因为 pixi 添加元素,后添加的就在上面,所以我们按钮容器写在最后添加即可,所以就是移动下代码的位置即可,在车容器和粒子容器添加完后在添加按钮容器

效果如下(这里的效果,和上面 resize 一起演示)

resize和按钮效果.gif

点击事件处理刹车

这个就好玩了,我们其实控制的是brake_lever的旋转角度,所以我们先在 createBike 这个方法中,把 lever 给 return 出去,具体代码如下

  createBike() {
    .....
    return {
      bikeContainer,
+++      brake_lever,
    };
  }
复制代码

然后我们就能在点击事件里拿到 lever 并且调整他的角度(逆时针旋转)

  showAndBindEvents() {
    const { bikeContainer, brake_lever } = this.createBike();
    ...
    btnContainer.on("mousedown", () => {
      //   console.log("mousedown");
      brake_lever.rotation = (-30 * Math.PI) / 180; // 这里的刹车把手应该逆时针旋转
    });
    btnContainer.on("mouseup", () => {
      //   console.log("mouseup");
      brake_lever.rotation = 0;
    });
    this.stage.addChild(btnContainer);
  }
复制代码

旋转中心不对.gif

额此时就比较尴尬了,旋转是旋转了,但明显旋转中心不对,机智的 XDM 也发现了,我们的中心明显在左上角所以导致了问题

刹车旋转中心处理以及移动到对应的位置,并注意层级

那旋转中心怎么调整呢,还记得xxx.anchor.set(0.5),这个是设置中心,那么我们应该设置在右下角,但又不完全是右下角,那我们设置个 0.9 先来试试

  createBike() {
    /**
     * 车容器
     * 为什么有车容器
     * 因为我们车的图片一共三张,作为一个整体,我们也会对其操作
     * 还要对单独的刹车把手进行操作
     */
    const bikeContainer = new PIXI.Container();
    const brake_bike = this.sprites.brake_bike;
    const brake_handlerbar = this.sprites.brake_handlerbar;
    const brake_lever = this.sprites.brake_lever;
+++    brake_lever.anchor.set(0.9);
    bikeContainer.addChild(brake_bike);
    bikeContainer.addChild(brake_handlerbar);
    bikeContainer.addChild(brake_lever);
    return {
      bikeContainer,
      brake_lever,
    };
  }
复制代码

效果果然符合我们预期

设置刹车把手旋转中心.gif

然后就是移动到我们对应的位置就可以了,这里小伙伴们可以各种微调,我可以大概给你们个数字,然后调整完之后发现层级也有些问题,所以我们还要把 brake_lever 放在前面去 addChild

  createBike() {
    /**
     * 车容器
     * 为什么有车容器
     * 因为我们车的图片一共三张,作为一个整体,我们也会对其操作
     * 还要对单独的刹车把手进行操作
     */
    const bikeContainer = new PIXI.Container();
    const brake_bike = this.sprites.brake_bike;
    const brake_handlerbar = this.sprites.brake_handlerbar;
    const brake_lever = this.sprites.brake_lever;
    brake_lever.anchor.set(0.9);
+++    brake_lever.x = 720;
+++    brake_lever.y = 900;
+++    bikeContainer.addChild(brake_lever);
    bikeContainer.addChild(brake_bike);
    bikeContainer.addChild(brake_handlerbar);
---    bikeContainer.addChild(brake_lever);
    return {
      bikeContainer,
      brake_lever,
    };
  }
复制代码

此时我们的效果就非常棒了

刹车把手初步处理完成.gif

用 gsap 添加动画

为了让我们效果更佳丝滑,变态的好看,我们又可以使用 gsap 了,来来来,各位看官,搞起!代码如下

    btnContainer.on("mousedown", () => {
      //   console.log("mousedown");
      //   brake_lever.rotation = (-30 * Math.PI) / 180; // 这里的刹车把手应该逆时针旋转
+++      gsap.to(brake_lever, { duration: 0.4, rotation: (-30 * Math.PI) / 180 });
    });
    btnContainer.on("mouseup", () => {
      //   console.log("mouseup");
      //   brake_lever.rotation = 0;
+++      gsap.to(brake_lever, { duration: 0.4, rotation: 0 });
    });
复制代码

我们的处理只是加了个 gsap 的动画,效果就非常感人!不管 XDM 感觉怎么样,反正我先感动哭了(滑稽脸)

刹车把手的最终效果.gif

写粒子咯!

XDM 写粒子这一块不瞒你们说,对于我 canvas 菜鸡选手来说,非常容易上手,而且玩的贼 6!但我们这次要基于 PIXI 写,所以我们还是要多看文档慢慢练习的

我们写粒子的主角-Graphics

文档参上,看着文档 Graphics 的 Methods 板块,依次找到了beginFill,drawCircle,endFill这不就是版本答案吗!(菜鸡狂喜,pixi 文档真好找)

画粒子查文档.jpg

先写死创建些粒子出来吧

有了文档辅助,我们直接就能写一堆粒子渲染到我们 canvas 上

  createParticles() {
    /**
     * 粒子容器
     * 为什么有粒子容器
     * 因为我们要创建很多粒子,用个粒子容器包着
     * 后续还有个巧妙的思路,让粒子斜着往下掉,拭目以待吧!
     */
    const particlesContainer = new PIXI.Container();
    const count = 100; // 粒子个数
    const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x0000]; // 随机的一些颜色
    for (var i = 0; i < count; i++) {
      const gr = new PIXI.Graphics();
      gr.beginFill(colors[Math.floor(Math.random() * colors.length)]); // 填充随机颜色
      gr.drawCircle(0, 0, 6); // 画粒子
      gr.endFill(); // 画完填充结束
      const pItem = {
        gr,
        sx: Math.floor(Math.random() * window.innerWidth), // 随机的开始x坐标,sx意思是startX
        sy: Math.floor(Math.random() * window.innerHeight), // 随机的开始y坐标,sy意思是startY
      };
      gr.x = pItem.sx; // 给gr也设置下x坐标
      gr.y = pItem.sy; // 给gr也设置下y坐标
      particlesContainer.addChild(gr); // 不要忘记加到容器里
    }
    return {
      particlesContainer,
    };
  }
复制代码

然后具体的效果如下

写死一堆粒子.jpg

当然我们这个时候其实可以把我们的背景颜色改成白色了,在我们初始化应用的时候就可以传入参数backgroundColor

    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      resizeTo: window,
+++      backgroundColor: 0xffffff,
    });
复制代码

先搭 loop 的架子

离我们完工越来越近了,接下去我们就是要搭建 loop 的架子!什么意思,兄弟们你们肯定做过动画,也知道什么requestAnimationFrame简称raf!然后我们的粒子一开始是运动的(先不管动画什么样子),当按住按钮刹车的时候是不是停止动画哈(就像真的刹住车一样),当我们松开手的时候,要放飞飙车的时候粒子是不是又动起来了!所以我们现在要做的事情应该就是这样一个架子

  • 一个 loop 函数,比如只打印 loop 这个字符串,一开始它应该是一直打印的状态
  • 当我们按住按钮(刹车的时候),他应该停止打印
  • 当我们松开按钮的时候(飙车的时候),他应该继续打印 loop

知道要干嘛了我们就开撸,这个时候就要介绍gsap另外个东东,叫做ticker,那个和善的文档示例它又来了

//add listener
gsap.ticker.add(myFunction);

function myFunction() {
  //executes on every tick after the core engine updates
}

//to remove the listener later...
gsap.ticker.remove(myFunction);
复制代码

注意啦,它这里的 myFunction 是不是就是我们的 loop!所以我们可以这么写

  createParticles() {
    ...

    /**
     * 我们的loop,心跳函数
     */
    function loop() {
      console.log("loop");
    }

    /**
     * 开始咯
     */
    function start() {
      gsap.ticker.add(loop);
    }

    /**
     * 暂停咯
     */
    function pause() {
      gsap.ticker.remove(loop);
    }

    return {
      particlesContainer,
+++      start,
+++      pause,
    };
  }
复制代码
  showAndBindEvents() {
    ...
+++    const { particlesContainer, start, pause } = this.createParticles();
    this.stage.addChild(particlesContainer);
+++    start();

    const { btnContainer } = this.createBtn();
    /**
     * 绑定事件
     */
    btnContainer.on("mousedown", () => {
      //   console.log("mousedown");
      //   brake_lever.rotation = (-30 * Math.PI) / 180; // 这里的刹车把手应该逆时针旋转
      gsap.to(brake_lever, { duration: 0.4, rotation: (-30 * Math.PI) / 180 });
+++      pause();
    });
    btnContainer.on("mouseup", () => {
      //   console.log("mouseup");
      //   brake_lever.rotation = 0;
      gsap.to(brake_lever, { duration: 0.4, rotation: 0 });
+++      start();
    });
    this.stage.addChild(btnContainer);
  }
复制代码

写完这个代码,包括处理了点击事件startpause,我们就可以玩下 demo 看下效果

loop调试.gif

果然很成功,当我们啥都不动,loop 持续输出打印;当我们按刹车,loop 就停了;当我们松开手飙车,loop 又继续打印了!所以 loop 架子成功搭建

处理粒子下落!

要做粒子下落,那想必粒子是有速度的,而且根据 demo 的样子,粒子的速度是会越来越快的,但肯定又给最大值,所以我们在我们的实例上,设置速度初始值,成长值,最大值,然后记录个粒子数组没问题吧,xdm!代码如下

  constructor(selector) {
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      resizeTo: window,
      backgroundColor: 0xffffff,
    });
    document.querySelector(selector).appendChild(this.app.view);
    this.stage = this.app.stage;

    this.loader = new PIXI.Loader();
    this.sprites = {};
    this.loadImages(); // 封装个方法专门加载图片
+++    this.particles = []; // 粒子数组
+++    this.count = 100; // 粒子个数
+++    this.speed = 0; // 粒子初始速度
+++    this.speedStep = 0.3; // 粒子每次增速多少
+++    this.maxSpeed = 20; // 粒子最大速度
  }
复制代码

然后我们简单重构下我们创建粒子的代码

    // count记录在实例上,所以this.count
+++    for (var i = 0; i < this.count; i++) {
      const gr = new PIXI.Graphics();
      gr.beginFill(colors[Math.floor(Math.random() * colors.length)]); // 填充随机颜色
      gr.drawCircle(0, 0, 6); // 画粒子
      gr.endFill(); // 画完填充结束
      const pItem = {
        gr,
        sx: Math.floor(Math.random() * window.innerWidth), // 随机的开始x坐标,sx意思是startX
        sy: Math.floor(Math.random() * window.innerHeight), // 随机的开始y坐标,sy意思是startY
      };
      gr.x = pItem.sx; // 给gr也设置下x坐标
      gr.y = pItem.sy; // 给gr也设置下y坐标
      particlesContainer.addChild(gr); // 不要忘记加到容器里
+++      this.particles.push(pItem); // 保存粒子数组
    }
复制代码

紧接着我们就能开始写下落了

/**
 * 我们的loop,心跳函数
 * 因为this指向问题要用箭头函数
 */
const loop = () => {
  // console.log("loop");
  this.speed += this.speedStep; // 每次增速
  this.speed = Math.min(this.speed, this.maxSpeed); // 但增速不能超过最大速度
  this.particles.forEach((pItem) => {
    pItem.gr.y += this.speed; // 向下掉落 += 粒子速度
    if (pItem.gr.y >= window.innerHeight) {
      // 当调出屏幕外了,我们把y重新设置为0,让他重新掉落
      pItem.gr.y = 0;
    }
  });
};

/**
 * 开始咯
 * 因为this指向问题要用箭头函数
 */
const start = () => {
  // 开始的时候设置速度为0
  this.speed = 0;
  gsap.ticker.add(loop);
};

/**
 * 暂停咯
 * 因为this指向问题要用箭头函数
 */
const pause = () => {
  gsap.ticker.remove(loop);
  this.particles.forEach((pItem) => {
    // 暂停的时候回到初始位置
    gsap.to(pItem.gr, { duration: 0.4, x: pItem.sx, y: pItem.sy });
  });
};
复制代码

具体的注释已经加上,简单来说,

  • 就是控制速度让粒子往下落,超出屏幕重新在让他下落,下落速度不要超过最大速度
  • 暂停的时候还需要回到我们初始位置,
  • 继续的时候,我们速度从 0 开始

此时的效果是

粒子下落效果.gif

怎么让粒子斜着落下

XDM 还记得我们的粒子放在一个容器吗!如果容器旋转了会怎么样!(继续滑稽脸)说干就干

  createParticles() {
    /**
     * 粒子容器
     * 为什么有粒子容器
     * 因为我们要创建很多粒子,用个粒子容器包着
     * 后续还有个巧妙的思路,让粒子斜着往下掉,拭目以待吧!
     */
    const particlesContainer = new PIXI.Container();
    particlesContainer.rotation = (30 * Math.PI) / 180; // 倾斜容器
    // 放在window的中间
    particlesContainer.x = window.innerWidth / 2;
    particlesContainer.y = window.innerHeight / 2;
    // 设置中心
    particlesContainer.pivot.x = window.innerWidth / 2;
    particlesContainer.pivot.y = window.innerHeight / 2;
    ...
  }
复制代码

来看下粒子们有没有听话的斜着往下掉!

粒子斜着下.gif

细节动画处理

虽然我们的粒子效果已经出来了,但整个线条还是没有 demo 中这么酷炫,并且我们的暂停回弹效果也没有出来,这个该怎么处理呢(由于 gif 图效果不佳,所以最终的效果会在完结撒花里提供链接让大家体验,并把完整代码贴出)

动效的原理

如果一个 div,做成圆形,动画从左移动到右,它是没有灵魂的,看不出任何动效

但如果它在运动过程中,有了几次缩放,那感觉就是不一样了

先来看下这个案例(可以另写一个 demo.html)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>scale的特效</title>
    <style>
      .box {
        position: relative;
        width: 50px;
        height: 50px;
        border-radius: 50%;
        margin-bottom: 20px;
      }

      .green-box {
        background: green;
        animation: move 2s infinite alternate;
      }

      .red-box {
        background: red;
        animation: scaleMove 2s infinite alternate;
      }

      @keyframes move {
        0% {
          left: 0;
        }

        100% {
          left: 500px;
        }
      }

      @keyframes scaleMove {
        0% {
          left: 0;
        }

        25% {
          transform: scale(1.1, 0.8);
        }

        50% {
          transform: scale(1, 1);
        }

        75% {
          transform: scale(0.9, 1.2);
        }

        100% {
          left: 500px;
        }
      }
    </style>
  </head>

  <body>
    <div class="box green-box"></div>
    <div class="box red-box"></div>
    <script></script>
  </body>
</html>
复制代码

然后来看下酷炫的红球是怎么动的吧,是不是更佳的可爱活泼

红球移动.gif

所以通过这个例子,我们就可以对代码进行这样的处理

const loop = () => {
  // console.log("loop");
  this.speed += this.speedStep; // 每次增速
  this.speed = Math.min(this.speed, this.maxSpeed); // 但增速不能超过最大速度
  this.particles.forEach((pItem) => {
    pItem.gr.y += this.speed; // 向下掉落 += 粒子速度
+++    if (this.speed >= this.maxSpeed) {
          // 当超过最大速度的时候,处理缩放
+++      pItem.gr.scale.x = 0.03;
+++      pItem.gr.scale.y = 40;
    }
    if (pItem.gr.y >= window.innerHeight) {
      // 当调出屏幕外了,我们把y重新设置为0,让他重新掉落
      pItem.gr.y = 0;
    }
  });
};
复制代码
const pause = () => {
  gsap.ticker.remove(loop);
  this.particles.forEach((pItem) => {
+++    pItem.gr.scale.x = 1; // 恢复缩放
+++    pItem.gr.scale.y = 1; // 恢复缩放
    // 暂停的时候回到初始位置
    gsap.to(pItem.gr, {
      duration: 0.4,
      x: pItem.sx,
      y: pItem.sy,
    });
  });
};
复制代码
回弹效果

gsap 加回弹效果,即加字段ease: "elastic.out",,最终 pause 的代码如下

/**
 * 暂停咯
 * 因为this指向问题要用箭头函数
 */
const pause = () => {
  gsap.ticker.remove(loop);
  this.particles.forEach((pItem) => {
    pItem.gr.scale.x = 1; // 恢复缩放
    pItem.gr.scale.y = 1; // 恢复缩放
    // 暂停的时候回到初始位置
    gsap.to(pItem.gr, {
      duration: 0.4,
      x: pItem.sx,
      y: pItem.sy,
      ease: "elastic.out",
    });
  });
};
复制代码

完结撒花

这个案例,基本是完成了,但还是有很多细节 XDM 可以好好调整的,比如我们粒子运动前随机的坐标,如果设置几个特殊的位置会怎么样!我们动画的各个字段参数微调,调到自己满意的会不会更爽!总之,对于自己已经入门了个基础的 PIXI 和 gsap 还是很开心的,和 XDM 共勉,一起进步哈!

// brakebanner.js 完整代码
class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      resizeTo: window,
      backgroundColor: 0xffffff,
    });
    document.querySelector(selector).appendChild(this.app.view);
    this.stage = this.app.stage;

    this.loader = new PIXI.Loader();
    this.sprites = {};
    this.loadImages(); // 封装个方法专门加载图片
    this.particles = []; // 粒子数组
    this.count = 100; // 粒子个数
    this.speed = 0; // 粒子初始速度
    this.speedStep = 0.3; // 粒子每次增速多少
    this.maxSpeed = 20; // 粒子最大速度
  }
  loadImages() {
    const imageNameArr = [
      "brake_bike",
      "brake_handlerbar",
      "brake_lever",
      "btn_circle",
      "btn",
    ];
    imageNameArr.forEach((imageName) => {
      // 先要load我们的素材,key值就用我们的图片名
      this.loader.add(imageName, `images/${imageName}.png`);
    });
    this.loader.load((loader, resources) => {
      // 这个回调函数就是已经加载好图片了
      imageNameArr.forEach((imageName) => {
        // 这里使用Sprite精灵,并且把值赋值给对象sprites上,key也用的是图片名
        this.sprites[imageName] = new PIXI.Sprite(resources[imageName].texture);
      });
      // 然后我们可以做一下显示的逻辑,并且该方法还能绑定事件
      this.showAndBindEvents();
    });
  }

  showAndBindEvents() {
    const { bikeContainer, brake_lever } = this.createBike();
    const resize = () => {
      bikeContainer.scale.x = bikeContainer.scale.y = 0.3; // 图片太大我们缩小点
      bikeContainer.x = window.innerWidth - bikeContainer.width; // 固定在右下角处理x坐标
      bikeContainer.y = window.innerHeight - bikeContainer.height; // 固定在右下角处理y坐标
    };
    resize();
    window.addEventListener("resize", resize);
    this.stage.addChild(bikeContainer);

    const { particlesContainer, start, pause } = this.createParticles();
    this.stage.addChild(particlesContainer);
    start();

    const { btnContainer } = this.createBtn();
    /**
     * 绑定事件
     */
    btnContainer.on("mousedown", () => {
      //   console.log("mousedown");
      //   brake_lever.rotation = (-30 * Math.PI) / 180; // 这里的刹车把手应该逆时针旋转
      gsap.to(brake_lever, { duration: 0.4, rotation: (-30 * Math.PI) / 180 });
      pause();
    });
    btnContainer.on("mouseup", () => {
      //   console.log("mouseup");
      //   brake_lever.rotation = 0;
      gsap.to(brake_lever, { duration: 0.4, rotation: 0 });
      start();
    });
    this.stage.addChild(btnContainer);
  }
  createBtn() {
    /**
     * 按钮容器
     * 为什么要有按钮容器呢
     * 因为我们按钮图片一共有2张,纯按钮还有按钮外的那个圈,我们把他作为一个整体
     */
    const btnContainer = new PIXI.Container();
    const btn = this.sprites.btn;
    const btn_circle = this.sprites.btn_circle;
    btn.anchor.set(0.5); // 设置支点为中心
    btn_circle.anchor.set(0.5); // 设置支点为中心
    // btn.pivot.x = btn.pivot.y = btn.width / 2;
    // btn_circle.pivot.x = btn_circle.pivot.y = btn_circle.width / 2;
    btnContainer.addChild(btn); // 添加btn图片
    btnContainer.addChild(btn_circle); // 添加btn_circle图片
    btnContainer.x = window.innerWidth / 2; // 把容器放到x轴中间
    btnContainer.y = window.innerHeight / 2; // 把容器放到y轴中间

    /**
     * 该动画处理的是缩放
     * 注意传入第一个参数是btn_circle.scale
     * 0.4s的时间,circle变大1.3倍,且无限做动画
     */
    gsap.to(btn_circle.scale, { duration: 0.4, x: 1.3, y: 1.3, repeat: -1 });
    /**
     * 该动画处理的是透明度
     * 注意传入的第一个参数就是btn_circle
     * 0.4s的事件,变透明度为0(即看不到),且无限做动画
     */
    gsap.to(btn_circle, { duration: 0.4, alpha: 0, repeat: -1 });
    btnContainer.interactive = true; // 该属性不设置不能绑定事件
    btnContainer.buttonMode = true; // 设置成按钮模式,鼠标就能变成手手(cursor: pointer)
    return {
      btnContainer,
    };
  }
  createBike() {
    /**
     * 车容器
     * 为什么有车容器
     * 因为我们车的图片一共三张,作为一个整体,我们也会对其操作
     * 还要对单独的刹车把手进行操作
     */
    const bikeContainer = new PIXI.Container();
    const brake_bike = this.sprites.brake_bike;
    const brake_handlerbar = this.sprites.brake_handlerbar;
    const brake_lever = this.sprites.brake_lever;
    brake_lever.anchor.set(0.9);
    brake_lever.x = 720;
    brake_lever.y = 900;
    bikeContainer.addChild(brake_lever);
    bikeContainer.addChild(brake_bike);
    bikeContainer.addChild(brake_handlerbar);
    return {
      bikeContainer,
      brake_lever,
    };
  }
  createParticles() {
    /**
     * 粒子容器
     * 为什么有粒子容器
     * 因为我们要创建很多粒子,用个粒子容器包着
     * 后续还有个巧妙的思路,让粒子斜着往下掉,拭目以待吧!
     */
    const particlesContainer = new PIXI.Container();
    particlesContainer.rotation = (30 * Math.PI) / 180; // 倾斜容器
    // 放在window的中间
    particlesContainer.x = window.innerWidth / 2;
    particlesContainer.y = window.innerHeight / 2;
    // 设置中心
    particlesContainer.pivot.x = window.innerWidth / 2;
    particlesContainer.pivot.y = window.innerHeight / 2;
    const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x0000]; // 随机的一些颜色
    // count记录在实例上,所以this.count
    for (var i = 0; i < this.count; i++) {
      const gr = new PIXI.Graphics();
      gr.beginFill(colors[Math.floor(Math.random() * colors.length)]); // 填充随机颜色
      gr.drawCircle(0, 0, 6); // 画粒子
      gr.endFill(); // 画完填充结束
      const pItem = {
        gr,
        sx: Math.floor(Math.random() * window.innerWidth), // 随机的开始x坐标,sx意思是startX
        sy: Math.floor(Math.random() * window.innerHeight), // 随机的开始y坐标,sy意思是startY
      };
      gr.x = pItem.sx; // 给gr也设置下x坐标
      gr.y = pItem.sy; // 给gr也设置下y坐标
      particlesContainer.addChild(gr); // 不要忘记加到容器里
      this.particles.push(pItem); // 保存粒子数组
    }

    /**
     * 我们的loop,心跳函数
     * 因为this指向问题要用箭头函数
     */
    const loop = () => {
      // console.log("loop");
      this.speed += this.speedStep; // 每次增速
      this.speed = Math.min(this.speed, this.maxSpeed); // 但增速不能超过最大速度
      this.particles.forEach((pItem) => {
        pItem.gr.y += this.speed; // 向下掉落 += 粒子速度
        if (this.speed >= this.maxSpeed) {
          pItem.gr.scale.x = 0.03;
          pItem.gr.scale.y = 40;
        }
        if (pItem.gr.y >= window.innerHeight) {
          // 当调出屏幕外了,我们把y重新设置为0,让他重新掉落
          pItem.gr.y = 0;
        }
      });
    };

    /**
     * 开始咯
     * 因为this指向问题要用箭头函数
     */
    const start = () => {
      // 开始的时候设置速度为0
      this.speed = 0;
      gsap.ticker.add(loop);
    };

    /**
     * 暂停咯
     * 因为this指向问题要用箭头函数
     */
    const pause = () => {
      gsap.ticker.remove(loop);
      this.particles.forEach((pItem) => {
        pItem.gr.scale.x = 1; // 恢复缩放
        pItem.gr.scale.y = 1; // 恢复缩放
        // 暂停的时候回到初始位置
        gsap.to(pItem.gr, {
          duration: 0.4,
          x: pItem.sx,
          y: pItem.sy,
          ease: "elastic.out",
        });
      });
    };

    return {
      particlesContainer,
      start,
      pause,
    };
  }
}
复制代码

最终开发效果展示.gif

总结结尾

总的来说,这次体验不错,解锁了 PIXIgsap 的学习姿势,并完成了一个酷炫的效果!不知道 XDM 和我有没有一起学会,掌握了这项基础本领!主要还是有大帅还有群里的小伙伴带的好,卷的漂亮!但我作为盟主,选择不卷,就不做过多的扩展,和不卷的朋友一起学习基础知识!这里是真心想教会大家的反内卷盟主,学会了记得点赞哦!

另外说下,想要和大佬组队,让大佬带你的!记得在公众号里搜大帅老猿,在他这里可以学到很多东西

猜你喜欢

转载自juejin.im/post/7120171879599439909