在写这篇文章之前,先感谢大帅老师@大帅老猿,是他让我重新拿起了笔在掘金上输出文章。本人17年就注册了掘金账号,潜水到21年初,才在掘金写了第一篇文章,之后又陆续写了3篇,都没有啥点赞和阅读量,兴致就低了,息笔一年。直到今年4月份,在大帅老师的带领下,参加了游戏创意大赛,并输出了自己的文章,虽然在创意大赛上没有得奖,只拿了一个阳光普照,但是在码上掘金活动中,运气好获得了14名的成绩,拿到了我在掘金社区的第一个奖:小爱音箱。原来我并不是不能写,我只是缺少机会,我只是缺少锻炼!于是在之后的掘金活动中,我几乎都参与了,大奖不容易拿,拿拿小奖,撸撸羊毛还是很开心的(6月更文挑战不小心写成了满勤,羊毛大大滴多)~
这次大帅带着猿创营的小伙伴搞了个全员实训 - 用PIXI + GSAP实现刹车动效的 掘金更文活动,输出自己在看完大帅在哔哩哔哩的直播后自己的动效实现的文章。个人认为是很有意义的,所以还没在掘金写过文章的同学,不要怂,上来就直接干,相信大家也能收获自己的成长,爱上写作。逼逼太多了,赶紧上点最近很多人都上过的干货:
工程准备
fork 大帅老师的github仓库到自己账号,并克隆到本地,工程目录如下,极其简单啊,就5张资源图片,一个js文件一个html文件,咱不整花里胡哨的,直接html+js开干:
入口页
<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/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 = init;
function init() {
let banner = new BrakeBanner("#brakebanner");
}
</script>
</head>
<body>
<div id="brakebanner"></div>
</body>
</html>
在我们的入口页引入了两个js:pixi.min.js和gsap.min.js。PIXI和GSAP正是打造咱们今天的主角的称手工具。先附上两个官网链接供大家直达,证明他俩不是并夕夕买的,是从海外淘来的呢:
PixiJS官网 github star: 36.8k
Gsap官网 github star: 14.4k
另外我们还引用了一个brakebanner.js。在后面调用了一个init函数,用以实例化一个brakeBanner类,这个才是我们今天的主角。
brakeBanner类实现
镜头转向我们的主角:
是的,这是一个小白,需要大家慢慢调教~ 小白说,脸都不要了吗?行,给他来个门面:
创建PIXI应用
class BrakeBanner {
constructor(selector) {
this.app = new PIXI.Application({
width: 100,
height: 100,
backgroundColor: 'black'
});
document.querySelector(selector).appendChild(this.app.view);
}
}
效果如下:
小白说: PIXI.Application是个啥呀? this.app.view又是个啥呀?为啥我那么黑呀?给我洗白呀!
导演: 知道那么多干嘛?会演戏就行,只会背台词没演技是走不远的,演就完事了。
小白:行吧,那我要一黑到底!
导演:你特么演我?
《刹车》第一幕:彻底黑化的小白
小白延伸了他的边界至窗口大小,并将自己抹黑:
this.app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 'black'
});
这下好了,小白彻底黑化了!
导入资源
《刹车》第二幕:小白的好帮手PIXI
在一个风和日丽的早晨,小白骑上他心爱的小摩托自行车(贫穷买不起摩托)去郊游。出门前,小白打算掏出了他刚收到的淘宝拼装自行车,于是喊来了PIXI,PIXI在拼装图片方面很有优势:
this.loader = new PIXI.Loader();
this.loader.add('bike', 'images/brake_bike.png');
this.loader.add('handlerbar', 'images/brake_handlerbar.png');
this.loader.add('lever', 'images/brake_lever.png');
this.loader.load();
this.loader.onComplete.add(() => {
let bike = new PIXI.Sprite(this.loader.resources['bike'].texture);
let handler = new PIXI.Sprite(this.loader.resources['handlerbar'].texture);
let lever = new PIXI.Sprite(this.loader.resources['lever'].texture);
this.app.stage.addChild(bike);
this.app.stage.addChild(handler);
this.app.stage.addChild(lever);
})
不一会儿,PIXI就把车把,车架,和车把手搬到了观众面前:
吃瓜群众:这个是自行车?
什么是容器
小白:别急,车子太大了,我让PIXI调整下,PIXI在吗?帮我把车子车架和车把手给我缩小到原来的1/4。
PIXI:好的,巴啦啦能量,scale变身:
this.loader.onComplete.add(() => {
+ const bikeContainer = new PIXI.Container();
let bike = new PIXI.Sprite(this.loader.resources['bike'].texture);
let handler = new PIXI.Sprite(this.loader.resources['handlerbar'].texture);
let lever = new PIXI.Sprite(this.loader.resources['lever'].texture);
+ bikeContainer.addChild(bike);
+ bikeContainer.addChild(handler);
+ bikeContainer.addChild(lever);
+ bikeContainer.scale.x = bikeContainer.scale.y = .25;
this.app.stage.addChild(bikeContainer);
})
着色器的使用
PIXI很老道啊,这边车架,车把,车把手始终是一起的,他把他们都装在一个容器Container里,干完这些后,PIXI以为小白会夸他两句,没想到小白眉头微皱道:“这个颜色骑出去,满大街都是,能不能换个颜色呢?可以稍微夸张点也是OK的”。PIXI摸了摸自己的下巴,眼珠子转了转:“有了”,然后从口袋里掏出了一个工具:无敌调色板,还有一个好听的英文名字tint:
this.loader.onComplete.add(() => {
const bikeContainer = new PIXI.Container();
let bike = new PIXI.Sprite(this.loader.resources['bike'].texture);
let handler = new PIXI.Sprite(this.loader.resources['handlerbar'].texture);
let lever = new PIXI.Sprite(this.loader.resources['lever'].texture);
+ bike.tint = 0xff0000; // 给自行车着色
+ handler.tint = 0xff00ff;
+ lever.tint = 0x00ffff;
bikeContainer.addChild(bike);
bikeContainer.addChild(handler);
bikeContainer.addChild(lever);
bikeContainer.scale.x = bikeContainer.scale.y = .25;
this.app.stage.addChild(bikeContainer);
})
小白: 够骚气!奖励你一个棒棒糖🍭。
PIXI实例的位置调整
PIXI接过棒棒糖,继续手头上的工作,很明显,他在调车把手,这不是他在行的,不过做事得做全套嘛!心疼PIXI两秒,一顿操作之后:
this.loader.onComplete.add(() => {
const bikeContainer = new PIXI.Container();
let bike = new PIXI.Sprite(this.loader.resources['bike'].texture);
let handler = new PIXI.Sprite(this.loader.resources['handlerbar'].texture);
let lever = new PIXI.Sprite(this.loader.resources['lever'].texture);
bike.tint = 0xff0000;
handler.tint = 0xff00ff;
lever.tint = 0x00ffff;
+ lever.x = 365; // 调整车把手的x坐标
+ lever.y = 420; // 调整车把手的y坐标
bikeContainer.addChild(bike);
bikeContainer.addChild(handler);
bikeContainer.addChild(lever);
bikeContainer.scale.x = bikeContainer.scale.y = .25;
this.app.stage.addChild(bikeContainer);
})
小白眉头正准备皱的时候,PIXI迅速调整了一下顺序:
this.loader.onComplete.add(() => {
const bikeContainer = new PIXI.Container();
let bike = new PIXI.Sprite(this.loader.resources['bike'].texture);
let handler = new PIXI.Sprite(this.loader.resources['handlerbar'].texture);
let lever = new PIXI.Sprite(this.loader.resources['lever'].texture);
bike.tint = 0xff0000;
handler.tint = 0xff00ff;
lever.tint = 0x00ffff;
lever.x = 365;
lever.y = 420;
bikeContainer.addChild(bike);
↑ bikeContainer.addChild(lever); // 先加到容器里的会被放在最底下
↓ bikeContainer.addChild(handler);
bikeContainer.scale.x = bikeContainer.scale.y = .25;
this.app.stage.addChild(bikeContainer);
})
嗯,车子装好了,要出发咯!
PIXI: 等等!这车子没有刹车,我再给它装个刹车按钮。
this.loader.onComplete.add(() => {
// 自行车容器
// ...
// 刹车按钮容器
const btnContainer = new PIXI.Container();
let btn = new PIXI.Sprite(this.loader.resources['btn'].texture);
let btn_circle = new PIXI.Sprite(this.loader.resources['btn_circle'].texture);
btnContainer.addChild(btn);
btnContainer.addChild(btn_circle);
btnContainer.scale.x = btnContainer.scale.y = .5;
this.app.stage.addChild(btnContainer);
})
PIXI: 别急,调整下位置:
this.loader.onComplete.add(() => {
// 自行车容器
// ...
// 刹车按钮容器
const btnContainer = new PIXI.Container();
let btn = new PIXI.Sprite(this.loader.resources['btn'].texture);
let btn_circle = new PIXI.Sprite(this.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;
btnContainer.addChild(btn);
btnContainer.addChild(btn_circle);
btnContainer.scale.x = btnContainer.scale.y = .5;
+ btnContainer.x = 350;
+ btnContainer.y = 350;
this.app.stage.addChild(btnContainer);
})
《刹车》第三幕:PIXI的好基友GSAP
GSAP:简化你的动效逻辑
PIXI:再赠送你一个按钮特效吧,这样好在别人面前装逼,不过这个还是我的好兄弟GSAP比较在行。
GSAP: 这回 终于轮到我了吧,弟弟们,看好了:
this.loader.onComplete.add(() => {
// 自行车容器
// ...
// 刹车按钮容器
const btnContainer = new PIXI.Container();
let btn = new PIXI.Sprite(this.loader.resources['btn'].texture);
let btn_circle = new PIXI.Sprite(this.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;
+ gsap.to(btn_circle.scale, { duration: 1, x: 1.3,y:1.3, repeat: -1 }); // 通过gsap控制圆圈的缩放。
btnContainer.addChild(btn);
btnContainer.addChild(btn_circle);
btnContainer.scale.x = btnContainer.scale.y = .5;
btnContainer.x = 350;
btnContainer.y = 350;
this.app.stage.addChild(btnContainer);
})
是的,你没看错,就是一行代码!
不过还是差点意思,作为动效界的扛把子,怎么允许不完美出现,再酷炫点:
this.loader.onComplete.add(() => {
// 自行车容器
// ...
// 刹车按钮容器
const btnContainer = new PIXI.Container();
let btn = new PIXI.Sprite(this.loader.resources['btn'].texture);
let btn_circle = new PIXI.Sprite(this.loader.resources['btn_circle'].texture);
+ let btn_circle2 = new PIXI.Sprite(this.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_circle2.width / 2;
+ btn_circle2.pivot.y = btn_circle2.height / 2;
+ btn_circle.scale.x = btn_circle.scale.y = .8; // 起始从0.8倍开始,更具动感
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 }); // 让圆圈变到透明
btnContainer.addChild(btn);
btnContainer.addChild(btn_circle);
+ btnContainer.addChild(btn_circle2);
btnContainer.scale.x = btnContainer.scale.y = .5;
btnContainer.x = 350;
btnContainer.y = 350;
this.app.stage.addChild(btnContainer);
})
吃瓜群众:卧槽!牛逼。
GSAP: 洒洒水啦~ 加上按钮事件,控制你的刹车把手:
this.loader.onComplete.add(() => {
// 自行车容器
// ...
// 刹车按钮容器
const btnContainer = new PIXI.Container();
let btn = new PIXI.Sprite(this.loader.resources['btn'].texture);
let btn_circle = new PIXI.Sprite(this.loader.resources['btn_circle'].texture);
let btn_circle2 = new PIXI.Sprite(this.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_circle2.width / 2;
btn_circle2.pivot.y = btn_circle2.height / 2;
btn_circle.scale.x = btn_circle.scale.y = .8; // 起始从0.8倍开始,更具动感
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 }); // 让圆圈变到透明
btnContainer.addChild(btn);
btnContainer.addChild(btn_circle);
btnContainer.addChild(btn_circle2);
btnContainer.scale.x = btnContainer.scale.y = .5;
btnContainer.x = 350;
btnContainer.y = 350;
+ btnContainer.interactive = true;
+ btnContainer.buttonMode = true;
+ btnContainer.on('mousedown', () => {
+ gsap.to(lever, { duration: .6, rotation: Math.PI / 180 * -30 });
+ })
+ btnContainer.on('mouseup', () => {
+ gsap.to(lever, { duration: .6, rotation: 0 });
+ })
this.app.stage.addChild(btnContainer);
})
实际上这边我省略了一步,就是需要设置车把手的旋转圆心为右下角:
this.loader.onComplete.add(() => {
// ...
+ lever.x = 750;
+ lever.y = 930;
+ lever.pivot.x = 483;
+ lever.pivot.y = 500;
// ...
})
ok!效果不错!可以出发!
PIXI: 给你整点东西,让你看着像在前进,毕竟咱们是在拍戏,不能真的上路。来,给你整点参照物,参照物在动,假装是你在动~
PIXI绘制图形
在画面中随机添加粒子:
this.loader.onComplete.add(() => {
// ...
// 粒子容器
const particleContaner = new PIXI.Container();
this.app.stage.addChild(particleContaner);
// 预设一些颜色
const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x000000];
for (let i = 0; i < 10; i++) {
let particle = new PIXI.Graphics();
// 随机取一种颜色
let color = colors[Math.floor(Math.random() * colors.length)];
particle.beginFill(color);
// 在窗口随机位置画半径为3-6的圆
let x = Math.random() * window.innerWidth;
let y = Math.random() * window.innerHeight;
let r = 3 + Math.ceil(Math.random() * 3)
particle.drawCircle(x, y, r);
particle.endFill();
particleContaner.addChild(particle);
}
})
导演:PIXI,麻烦把车子挪到角落去!
PIXI:好的,导演
this.loader.onComplete.add(() => {
// 自行车容器
const bikeContainer = new PIXI.Container();
let bike = new PIXI.Sprite(this.loader.resources['bike'].texture);
let lever = new PIXI.Sprite(this.loader.resources['lever'].texture);
let handler = new PIXI.Sprite(this.loader.resources['handlerbar'].texture);
bike.tint = 0xff0000;
handler.tint = 0xff00ff;
lever.tint = 0x00ffff;
lever.x = 750;
lever.y = 930;
lever.pivot.x = 483;
lever.pivot.y = 500;
bikeContainer.addChild(bike);
bikeContainer.addChild(lever);
bikeContainer.addChild(handler);
bikeContainer.scale.x = bikeContainer.scale.y = .25;
+ bikeContainer.x = window.innerWidth - bikeContainer.width;
+ bikeContainer.y = window.innerHeight - bikeContainer.height;
this.app.stage.addChild(bikeContainer);
})
按钮也一起调整下吧,让它和车子始终保持位置不变:
// 刹车按钮容器
// ...
btnContainer.x = window.innerWidth - 350;
btnContainer.y = window.innerHeight - 350;
// ...
一切准备就绪,现在要让我们的粒子动起来:
this.loader.onComplete.add(() => {
// ...
// 粒子容器
const particleContaner = new PIXI.Container();
this.app.stage.addChild(particleContaner);
// 预设一些颜色
const colors = [0xf1cf54, 0xb5cea8, 0xcf09d3, 0xf8b62a, 0x3b8f09];
+ const particles = []; // 保存粒子
for (let i = 0; i < 10; i++) {
let particle = new PIXI.Graphics();
// 随机取一种颜色
let color = colors[Math.floor(Math.random() * colors.length)];
particle.beginFill(color);
// 在窗口随机位置画半径为3-6的圆
let x = Math.random() * window.innerWidth;
let y = Math.random() * window.innerHeight;
let r = 3 + Math.ceil(Math.random() * 3)
particle.drawCircle(x, y, r);
particle.endFill();
particleContaner.addChild(particle);
+ particles.push(particle);
}
+ const move = () => {
+ particles.forEach(item => {
+ item.y += 1;
+ })
+ }
+ gsap.ticker.add(move)
})
是的,他神奇地动起来了!
吃瓜群众:牛逼
但是我们的车子和粒子的运动方向不一致,怎么办呢?将每个粒子都旋转某个角度?no!no!no!,只需旋转粒子容器即可:
// 粒子容器
const particleContaner = new PIXI.Container();
+ particleContaner.rotation = Math.PI / 180 * 30;
this.app.stage.addChild(particleContaner);
导演:让粒子铺满整个画面,并且无限循环 GSAP:这个嘛。简单
复习三角函数
啊!初中数学三角函数不记得了...
先求个斜边吧:
// 斜边
let hyp = Math.floor(Math.pow((Math.pow(window.innerHeight, 2) + Math.pow(window.innerWidth, 2)), 0.5));
再计算下屏幕的tan角:
let angle = 180 / (Math.PI / Math.atan(window.innerHeight / window.innerWidth));
最终计算出z的高度:
let z = Math.abs(Math.cos(90 - 30 - angle) * hyp);
// 粒子容器
const particleContaner = new PIXI.Container();
+ particleContaner.x = window.innerWidth / 2;
+ particleContaner.y = window.innerHeight / 2;
+ particleContaner.pivot.x = window.innerWidth / 2;
+ particleContaner.pivot.y = window.innerHeight / 2;
particleContaner.rotation = Math.PI / 180 * 30;
this.app.stage.addChild(particleContaner);
// 预设一些颜色
const colors = [0xf1cf54, 0xb5cea8, 0xcf09d3, 0xf8b62a, 0x3b8f09];
const particles = [];
for (let i = 0; i < 100; i++) {
let particle = new PIXI.Graphics();
// 随机取一种颜色
let color = colors[Math.floor(Math.random() * colors.length)];
particle.beginFill(color);
// 在窗口随机位置画半径为3-6的圆
let x = Math.random() * window.innerWidth;
let y = Math.random() * window.innerHeight;
let r = 3 + Math.ceil(Math.random() * 3)
particle.drawCircle(x, y, r);
particle.endFill();
particleContaner.addChild(particle);
// 将粒子信息保存
particles.push(particle);
}
+ let hyp = Math.floor(Math.pow((Math.pow(window.innerHeight, 2) + Math.pow(window.innerWidth, 2)), 0.5));
+ let angle = 180 / (Math.PI / Math.atan(window.innerHeight / window.innerWidth));
+ let z = Math.abs(Math.cos(90 - 30 - angle) * hyp);
+ let ext = (z - window.innerHeight) / 2; // 向下和向上扩展的长度
const move = () => {
particles.forEach(item => {
+ item.y += 10;
+ if(item.y > z - ext) {
+ item.y = -ext;
+ }
})
}
gsap.ticker.add(move)
导演:可以的,GSAP,给你加个鸡腿!咱们给这个自行车搞个百公里加速的效果?
GSAP:交给我吧,为了看着舒服,我把粒子减少到20个。
+ let speed = 0;
const move = () => {
+ speed += .1;
+ speed = Math.min(20, speed);
let ext = (z - window.innerHeight) / 2;
particles.forEach(item => {
+ item.y += speed;
if(item.y > z - ext) {
item.y = -ext;
}
})
}
粒子现在越来越快了。但是还是差点意思,你们有没有发现一个现象,就是当速度快到一定程度的时候,只能看到残影。那么如何做到有残影的效果呢?GSAP带你实现:
let speed = 0;
const move = () => {
speed += .1;
speed = Math.min(20, speed);
let ext = (z - window.innerHeight) / 2;
particles.forEach(item => {
item.y += speed;
+ item.scale.x -= .0003 * speed;
+ item.scale.y += .003 * speed;
+ item.scale.x = Math.max(.01 , item.scale.x);
+ item.scale.y = Math.min(40, item.scale.y);
if(item.y > z - ext) {
item.y = -ext;
}
})
}
只要把粒子的宽度随着速度的增加逐渐减小,高度随着速度的增加逐渐增大,就有那种快到变形的感觉了
最后加上按钮控制,不能刹车的话,这就不算什么好戏了。
定义开始和暂停事件,并在按钮事件中调用:
const start = () => {
speed = 0;
gsap.ticker.add(move)
}
const pause = () => {
gsap.ticker.remove(move)
particles.forEach(x => {
x.scale.x = x.scale.y = 1;
gsap.to(x, { duration: .6, x: x.sx, y: x.sy, ease: 'elastic.out' });
})
}
start();
btnContainer.on('mousedown', () => {
gsap.to(lever, { duration: .6, rotation: Math.PI / 180 * -30 });
+ pause();
})
btnContainer.on('mouseup', () => {
gsap.to(lever, { duration: .6, rotation: 0 });
+ start();
})
ok,这回小白可以骑上这个自行车出门了!
杀青
咔!收摊,领盒饭!
在公众号里搜 大帅老猿,在他这里可以学到很多东西。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。