背景介绍
大家好,我是梅利奥猪猪,你们的反内卷盟主,一个喜欢撸码和打游戏的咸鱼!这次又来更新我的掘金了!(不知道隔了多久)先来一发对自己的灵魂三问!
- 问:为什么隔了这么久才写文章?
- 答:因为我作为反内卷盟主,厌倦加班就转行了,并且刚入职了新工作(肯定不会 996 or 007)!当中也经历了很多事吧,工作的准备,外加空下来的时间玩游戏也比较多,所以就没写(罪恶中...)
- 问:那转行了为什么还要写技术文章呢
- 答:因为还喜欢撸码!并且新工作其实还是会接触代码的!
- 问:那你还是个喜欢打游戏的咸鱼吗?
- 答:那必须的,我的确在休息的时间躺平打游戏,但偶尔学习下还是要记录下写写博客的,所以这篇文章它来叻
不皮了不皮了,其实写文章的真正原因,当然是有大帅大佬和群里(猿创营)的小伙伴在卷我(滑稽脸),也真的是因为这次的这个案例效果的确不错!熟悉我的朋友,知道我是一名canvas
菜鸡玩家,但却深深的被canvas
吸引,所以入行前端也是因为当初喜欢写 canvas
各种效果啦,小游戏啦!具体有兴趣的也可以看下我的codepen,里面还是有些canvas
特效和小游戏的例子的!接下去开始我们的正题!手摸手教写刹车动效!Let's go!
效果展示
先来看下我们要做的东西是什么(要让大家知道,我们在哪,我们要做什么),打开案例网址,鼠标滚轮滑到刹车这边,然后点击按钮就可以看到了(什么,链接加载不出来,那来看下我为大家准备的 gif 效果图 吧)
什么这么酷炫?我们真的能做成这个样子吗!在没了解过PIXI和gsap的时候,感觉好像有点难啊!但接触后,在跟着大帅大佬一起学习,这效果 so easy!(下图是我们可以做出的效果)
相关文档
前面提到了 2 个技术栈,也是我们标题上提到的PIXI 和 gsap,虽然我是一名 canvas
玩家,但 PIXI 和 gsap 可是第一次接触,如何学习,当然先从文档看起!XDM,官方文档链接拿去,之后我们所有的手摸手教学,都会结合文档,带着大家一起做
- PIXI:pixijs.com/
- GSAP:greensock.com/docs/v3/GSA…
开发步骤(手摸手教)
fork 仓库(准备工作-准备架子)
首先当然是准备工作啦,把大帅大佬给我们准备好的架子拿过来!拿来吧你,这里是架子的仓库链接,XDM 把仓库 fork 下来,然后 clone 到自己本地,之后再用 vscode 打开,映入眼帘的就是
那对整个项目的架子大家应该清楚了,我们可以切个分支出来,开始我们的PIXI和gsap之旅
创建 PIXI 应用
先来看文档
XDM,接下去我们就要创建 PIXI 应用了。有朋友可能就会觉得,什么跨度太大了吧,我 PIXI 是什么都不清楚,所以我们可以打开我们的文档链接,找到 API DOCS,如下图所示
然后我们就进入了文档的首页,不得不说,pixi 的文档说明还是可以的,我们往下滑,就能看到基础的用法,如下图所示
直接复制上述代码(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 来看下效果,效果如下
好开心啊,复制复制代码就出了效果,但我们还是要熟悉下 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,并且是黑色的一小块
那我们做个全屏的特效总不能用默认的宽高(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 的大小变化而变化,如下图
那这个怎么办呢,别急,我们在文档中看到了这个resizeTo
所以我们加上这个参数在来试下,最终代码如下
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 的大小了
添加图片素材
熟悉 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 添加图片素材的玩耍到此也告一段落了
搭容器架子
分析思路 - 一共 3 个容器?
现在我们图片素材也会添加了,然后就可以快活猛写代码了吧!别急 XDM,我们在分析下后面要写些什么
上图所示,已经明确了,之后我们要处理三个容器,分别是
- 按钮容器 - 渲染按钮,处理按下抬起的事件!
- 车容器 - 渲染车车,还有刹车可以按下松开哦!
- 粒子容器 - 渲染粒子,华丽闪瞎产品的眼睛!
简单写些代码,记录我们的思路
以下代码根据我们上述分析,又再次搭个架子(全新的代码),宏观的思路就是这些!
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,
};
}
复制代码
然后效果立竿见影,但有些问题
修复细节问题,支点设置为中心
设置支点为中心,我看了文档应该有 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,居中了没有!
好耶我们又完成了按钮的居中,总觉得按钮就快要完成了
按钮 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 });
复制代码
知道大概的用法,我们在来看下精灵对象有哪些属性
那知道这些属性,那我们就可以写着玩一下了
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,
};
}
复制代码
效果如下
添加点击事件前置条件
pixi 添加点击事情并不是像我们以前document.addEventListener
这么容易,它有些前置条件,比如我们看下文档中的 buttonMode,示例代码如下
const sprite = new PIXI.Sprite(texture);
sprite.interactive = true;
sprite.buttonMode = true;
复制代码
buttonMode 是个布尔值一看就知道,设置成按钮的模式,这样我们的鼠标移动到我们的精灵上,就会显示出手指(cursor: pointer)的效果,那interactive
又是干嘛用的,我们继续查阅文档
好家伙,怪不得示例代码里有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);
}
复制代码
让我们测试下点击事件好不好使
非常完美,那到现在为止,我们的整个按钮的处理就先到这里,其实还有很多细节可以处理,比如我们的动画可以在微调,比如我们还可以在渲染个 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,
};
}
复制代码
车容器 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,
};
}
复制代码
写完以后,理论上肯定是右下角了,但还是要来看看效果的
完美搞定!
处理细节问题并完善点击事件
我们右下角的车容器 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 一起演示)
点击事件处理刹车
这个就好玩了,我们其实控制的是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);
}
复制代码
额此时就比较尴尬了,旋转是旋转了,但明显旋转中心不对,机智的 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,
};
}
复制代码
效果果然符合我们预期
然后就是移动到我们对应的位置就可以了,这里小伙伴们可以各种微调,我可以大概给你们个数字,然后调整完之后发现层级也有些问题,所以我们还要把 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,
};
}
复制代码
此时我们的效果就非常棒了
用 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 感觉怎么样,反正我先感动哭了(滑稽脸)
写粒子咯!
XDM 写粒子这一块不瞒你们说,对于我 canvas 菜鸡选手来说,非常容易上手,而且玩的贼 6!但我们这次要基于 PIXI 写,所以我们还是要多看文档慢慢练习的
我们写粒子的主角-Graphics
文档参上,看着文档 Graphics 的 Methods 板块,依次找到了beginFill
,drawCircle
,endFill
这不就是版本答案吗!(菜鸡狂喜,pixi 文档真好找)
先写死创建些粒子出来吧
有了文档辅助,我们直接就能写一堆粒子渲染到我们 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,
};
}
复制代码
然后具体的效果如下
当然我们这个时候其实可以把我们的背景颜色改成白色了,在我们初始化应用的时候就可以传入参数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);
}
复制代码
写完这个代码,包括处理了点击事件start
和pause
,我们就可以玩下 demo 看下效果
果然很成功,当我们啥都不动,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 开始
此时的效果是
怎么让粒子斜着落下
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;
...
}
复制代码
来看下粒子们有没有听话的斜着往下掉!
细节动画处理
虽然我们的粒子效果已经出来了,但整个线条还是没有 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>
复制代码
然后来看下酷炫的红球是怎么动的吧,是不是更佳的可爱活泼
所以通过这个例子,我们就可以对代码进行这样的处理
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,
};
}
}
复制代码
总结结尾
总的来说,这次体验不错,解锁了 PIXI 和 gsap 的学习姿势,并完成了一个酷炫的效果!不知道 XDM 和我有没有一起学会,掌握了这项基础本领!主要还是有大帅还有群里的小伙伴带的好,卷的漂亮!但我作为盟主,选择不卷,就不做过多的扩展,和不卷的朋友一起学习基础知识!这里是真心想教会大家的反内卷盟主,学会了记得点赞哦!
另外说下,想要和大佬组队,让大佬带你的!记得在公众号里搜大帅老猿
,在他这里可以学到很多东西