前言
相信大家工作中用的最多的就是 React 或者 Vue 去搬砖,今天带大家玩一下,了解下前端可视化,看前端是怎么实现炫酷动画效果,创意来源于vanmoof电商页面的刹车动画。通过 PIXI+GSAP 一步步刨析实现过程,让我们一起开启可视化大门!
技术栈
在前端开发中,如果需要图形的绘制,或者创建图片特效和动画,使用 Canvas 是非常合适的。那么这次为什么没有直接用 Canvas?就像上古时代,前端开发用 Jquery,没有直接用 Js 是一样的道理,选用 PIXI 本质上就是 Canvas 的库(渲染引擎,既支持 WebGL 也支持 Canvas),可以便捷的操作图像绘制图像,然后在搭配动画库 GSAP,负责想要的动画效果,极大的提升我们开发效率。以上就是技术选型的依据。
-
PIXI:pixijs.com/
PixiJS 是一个 2D 渲染引擎,可让你创建丰富的交互式图形、跨平台应用程序和游戏,而无需深入研究 WebGL API 以及处理浏览器和设备的兼容性问题。
特点:2D 渲染方面 PixiJS 是佼佼者。跨平台应用,不用考虑移动端兼容问题。
-
GSAP:greensock.com/docs/v3/GSA…
GSAP 是一个 JS 动画框架,可以对 JavaScript 可以操作的所有内容进行动画处理(CSS 属性,SVG,React,画布,通用对象等),同时解决了不同浏览器上存在的兼容性问题。
特点: 老牌动画库,Flash 时代就有大量用户,不用考虑移动端兼容问题。
需求分析
开干前一起梳理下我们大概要实现那些需求, 划分为模块,这样我们就像搭积木一样,不慌不忙,一点点实现。
- 自行车
- 车架
- 车轮
- 刹车
- 按钮
- 按钮位置
- 按钮点击事件
- 按下 刹车
- 松开 恢复行驶
- 粒子背景
- 随机粒子位置,大小,颜色
- 行驶粒子倾斜 30° 向下缓动
- 按下粒子静止
- 松开粒子回弹
环境搭建
这次主要实现 demo, 暂不考虑使用第三方框架。直接就创建index.html 引入是需要的库 PIXI 和 GSAP, 然后创建 brakeBanner.js后,加载实例化的brakeBanner类,之后在此文件专注写动效的逻辑。
<!-- index.html -->
<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 = () => {
new BrakeBanner("#brakebanner");
};
</script>
创建容器
首先创建createApp方法来管理画布容器,通过 PIXI.Application,创建一个的 PIXI 的实例,这个类会自动创建渲染器,接下来的图像操作,统统由这个类接管。然后我们设置容器的宽高,背景颜色,将它添加到 Dom 中,第一步就完成了,是不是很简单!
createApp (selector){
this.app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 'black'
})
document.querySelector(selector).appendChild(this.app.view)
}
引入图片资源
在loadImg方法中引入图片资源,会涉及到 PIXI.Loader,可以加载的图片资源,因为需要引入多张图片,使用 onComplete在图片加载完成时候在调用。
loadImg (){
const that = this
const loader = new PIXI.Loader();
this.loader = loader
const loaderMap = {
bike: 'images/brake_bike.png',
handlerbar: 'images/brake_handlerbar.png',
lever: 'images/brake_lever.png',
btn: 'images/btn.png',
btn_circle: 'images/btn_circle.png',
}
for(const key in loaderMap) {
loader.add(key, loaderMap[key])
}
loader.load();
loader.onComplete.add(() => {
that.createBike()
that.createButton()
that.creatParticle()
})
}
经过以上的需求分析, 我们了解到主要内容分为,自行车,按钮,和粒子背景。那么当图片加载完成,接下来的思路就是把这部分内容加载到页面中。
添加自行车
Container 代表了一个容器,就类似与 div 可以装载多个显示对象,Sprite 是精灵对象,可以理解为容器的子元素, 而loader 加载的图片一般会创建为精灵对象,添加在容器在显示。那么先创建自行车容器,将车轮,车架,刹车的图片转为精灵对象,再添加到Container中就完成自行车的展示了。
createBike(){
const {app, loader} = this
const bikeContainer = new PIXI.Container()
// 车轮
let bike = new PIXI.Sprite(loader.resources['bike'].texture);
// 车架
let handler = new PIXI.Sprite(loader.resources['handlerbar'].texture);
// 刹车
let lever = new PIXI.Sprite(loader.resources['lever'].texture);
// 设置刹车位置
lever.x = 750
lever.y = 950
lever.pivot.x = 483
lever.pivot.y = 500
lever.rotation = Math.PI * -15 / 180
bikeContainer.addChild(bike);
bikeContainer.addChild(lever);
bikeContainer.addChild(handler);
// 设置自行车容器位置
bikeContainer.scale.x = .25
bikeContainer.scale.y = .25
bikeContainer.x = window.innerWidth - bikeContainer.width;
bikeContainer.y = window.innerHeight - bikeContainer.height;
app.stage.addChild(bikeContainer)
this.lever = lever
this.bikeContainer = bikeContainer
}
添加按钮
按照之前的套路, 创建按钮容器,引入精灵元素,添加到Container中。然后设置容器的位置,圆心位置,按钮圆心位置。 这里btn_circle2 多添加个边框,为了展示按钮波纹动画。不要着急,按钮加上动画可以效果。
createButton() {
const {lever, bikeContainer, loader} = this
const btnContainer = new PIXI.Container()
this.app.stage.addChild(btnContainer)
let btn = new PIXI.Sprite(loader.resources['btn'].texture);
let btn_circle = new PIXI.Sprite(loader.resources['btn_circle'].texture);
let btn_circle2 = new PIXI.Sprite(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_circle.width / 2
btn_circle2.pivot.y = btn_circle.height / 2
btn_circle.scale.x = btn_circle.scale.y = .8
// 缩小一半
btnContainer.scale.x = .5
btnContainer.scale.y = .5
// 位置偏移
btnContainer.x = window.innerWidth - 300;
btnContainer.y = window.innerHeight - 350;
btnContainer.addChild(btn);
btnContainer.addChild(btn_circle);
btnContainer.addChild(btn_circle2);
this.btnContainer = btnContainer
this.btn_circle = btn_circle
}
按钮动画
先分析下按钮的动画,有个边框会由小到大扩散的扩散,当试下扩散到最最大会消失。另一个边框固定展示,那么对用的是 btn_circle,负责循环动画扩散 btn_circle2 是固定的边框,分析完毕,代码很简单 2 行搞定。gsap就可以登场了,to接收 2 个参数,控制动画的对象, 设置动画的参数。
createButton() {
...
// 边框由小到大扩散
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})
}
按钮事件
设置容器interactive,buttonMode的值为true,设置按钮交互属性。添加mousedown/mousedown 监听函数,梳理按下和抬起要做那些动画。代码就很好实现了。
- 按下
- 移除按钮扩散效果,移除
btn_circle对象 - 刹车动画,旋转 15°
- 自行车下移,向下 y 轴平移 20
- 移除按钮扩散效果,移除
- 抬起
- 移除按钮扩散效果,添加
btn_circle对象 - 刹车恢复,恢复到原来旋转
- 自行车位置恢复,恢复到原来位置
- 移除按钮扩散效果,添加
createButton() {
...
// 按钮事件
btnContainer.interactive = true
btnContainer.buttonMode = true
btnContainer.on('mousedown',()=>{
// 按钮扩散边框移除
btnContainer.removeChild(btn_circle)
//刹车动画
gsap.to(lever, {duration: .6, rotation: Math.PI / 180 * -30})
//自行车下移
gsap.to(bikeContainer,{
duration:.4,
x: window.innerWidth - bikeContainer.width,
y: window.innerHeight - bikeContainer.height +20
});
btnContainer.on('mouseup',()=>{
//刹车恢复
gsap.to(lever, {duration: .6, rotation: Math.PI * -15 / 180})
gsap.to(bikeContainer, {
duration:.4,
x: window.innerWidth - bikeContainer.width,
y: window.innerHeight - bikeContainer.height
})
btnContainer.addChild(btn_circle);
})
})
粒子容器
粒子效果是本案例的难点,因为涉及到动画,运动方向,随机颜色位置等,多种展示情况。接下来,我们慢慢分析下粒子的整体流程,大致分为一下几步:
- 创建粒子
- 随机颜色
- 运动方向
- 循环运行持续移动
- 按下按钮停止回弹
- 抬起粒子回弹效果
创建粒子
使用 Container 创建粒子容器,使用 Graphics 画笔工具创建粒子也就是小圆点, 随机设置圆点位置后添加到容器中。
creatParticle() {
const particleContaner = new PIXI.Container()
// 设置容器圆心是屏幕中心
particleContaner.pivot.x = window.innerWidth/2;
particleContaner.pivot.y = window.innerHeight/2;
// 容器位置屏幕中心
particleContaner.x = window.innerWidth/2;
particleContaner.y = window.innerHeight/2;
for( let i=0; i<10; i++) {
const particle = new PIXI.Graphics();
particle.beginFill("#666")
// 随机位置画圆
const x = Math.random() * window.innerWidth;
const y = Math.random() * window.innerHeight;
particle.drawCircle(0, 0, 6);
particle.endFill();
let pItem = {
sx: x,
sy: y,
gr: particle
}
particle.x = x;
particle.y = y;
particleContaner.addChild(particle)
}
}
随机颜色
首先定义个数组colors设置几个要展示的颜色,然后for循环中,使用Math.random随机获取数组下标,通过particle.beginFill然后给粒子设置颜色。
// creatParticle.js
const particleContaner = new PIXI.Container();
// 粒子颜色
const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x000000];
for (let i = 0; i < 10; i++) {
const particle = new PIXI.Graphics();
const color = colors[Math.floor(Math.random() * colors.length)];
particle.beginFill(color);
}
运动方向
案例中看到, 粒子的运行方向是倾斜运行, 指定一个粒子按照倾斜角度的方向运行,那么先算一个倾斜的角度,按照这个角度向量持续移动的去移动。这样实现是不是很复杂,我们可以换个角度思考,每个粒子都是在容器中,如果容器按照倾斜角度旋转,粒子按照 Y 轴向下持续运行,这样就简单多了。
// creatParticle.js
const particleContaner = new PIXI.Container();
// 设置圆心
particleContaner.pivot.x = window.innerWidth / 2;
particleContaner.pivot.y = window.innerHeight / 2;
// 设置屏幕中心
particleContaner.x = window.innerWidth / 2;
particleContaner.y = window.innerHeight / 2;
// 容器倾斜35°
particleContaner.rotation = (35 * Math.PI) / 180;
粒子持续运行
让粒子在 Y 轴持续运行,定义个 move 函数,初始化速度为 0, Y 轴根据时间间隔累加速度,限定最大速度为 20,而且当粒子移动超出范围时回到顶部,那么 Y 轴大于页面宽就赋值 0
// creatParticle.
let speed = 0;
const move = () => {
speed += 0.5;
speed = Math.min(speed, 20);
particles.forEach((item, i) => {
let pItem = particles[i];
pItem.gr.y += speed;
// 当粒子移动超出范围时回到顶部
if (pItem.gr.y > innerWidth) pItem.gr.y = 0;
});
};
gsap.ticker.add(move);
我们继续优化下动画效果,粒子快速运行,小球变细会符合人类视觉习惯,这个看着动画会更加舒服。
// creatParticle.
if (speed >= 20) {
pItem.gr.scale.x = 0.03;
pItem.gr.scale.y = 40;
}
粒子停止回弹
移除粒子运动,然后通过 elastic.out 恢复到初始的粒子位置,关联到按钮事件。大功告成!
pause(){
const {move} = this
// 移除粒子运动
gsap.ticker.remove(move)
// 粒子回弹动画
for(let i = 0;i<particles.length;i++){
let pItem = particles[i];
pItem.gr.scale.y = 1;
pItem.gr.scale.x = 1;
gsap.to(pItem.gr,{duration:.6,x:pItem.sx,y:pItem.sy,ease:'elastic.out'});
}
}
start() {
this.speed = 0;
gsap.ticker.add(this.move)
}
createButton() {
...
btnContainer.on('mousedown',()=>{
this.pause()
btnContainer.on('mouseup',()=>{
gsap.to(lever, {duration: .6, rotation: Math.PI * -15 / 180})
gsap.to(bikeContainer,{
duration:.4,
x: window.innerWidth - bikeContainer.width,
y: window.innerHeight - bikeContainer.height
})
// btnContainer.addChild(btn_circle);
this.start()
})
})
}
最后
感谢大帅提供机会参加猿创营活动,本案例非常有创意,也了解 PIXI+GSAP 的基本使用。需要多看文档,多学习。也感谢大家看完本篇文章,希望你也能跟我一起动手感受下动画的魅力。大家多多支持点赞 👍
在公众号里搜 大帅老猿,在他这里可以学到很多东西。