老规矩,先放码上掘金源码展示:
觉得看着不习惯的,也可以直接在Github仓库查看,欢迎吐槽~
前言
在大帅老师的直播讲解中,第一次了解了PixiJS的实操。跟写完刹车动效,微微膨胀地写起来了变速器动效,然后就卡壳了。。
还是回过头来先趁着残留的一丝记忆,整理下刹车动效的入门笔记吧。
实现
创建应用和舞台
定义一个BrakeBanner类,在构造函数中创建一个适应屏幕宽高的PIXI应用。为了看起来方便,我们先给一个显眼的背景色。
class BrakeBanner {
constructor(selector){
this.app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
backgroundColor: 0x1d9c9c,
antialias: true,
resolution: 1,
resizeTo: window
})
document.querySelector(selector).appendChild(this.app.view)
this.stage = this.app.stage
}
}
在html
文件中定义一个承接应用的容器brakebanner
<div id="brakebanner"></div>
在html
文件中引入定义类的js
文件,并且在页面加载时,new
一下类,挂载应用实例并执行。
window.onload = init;
function init() {
let banner = new BrakeBanner("#brakebanner");
}
创建一个辅助文本函数
接下来,我们来创建一个辅助文本函数,并把它添加到舞台上。没什么鸟用,纯粹是因为光秃秃的背景看起来太单调啦。
class BrakeBanner {
constructor(selector) {
// ...
this.showText = this.createText('PixiJS and GSAP 入门笔记')
this.stage.addChild(this.showText)
}
createText(text='你好~') {
const textContainer = new PIXI.Container()
const showText = new PIXI.Text(text, {
width: 80,
fontSize: 48,
fontFamily: 'serif',
fill: 0xffffff
})
showText.x = 100
showText.y = 200
textContainer.addChild(showText)
return textContainer
}
}
Loader加载需要使用的静态资源
我们在构造函数中加载需要使用的静态资源,并且在加载完成后的onComplete
函数中执行对应的处理事件。
class BrakeBanner {
constructor(selector) {
// ...
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')
this.loader.load()
this.loader.onComplete.add(() => {
this.show()
})
}
// ...
// 资源加载完成后执行的处理函数
show() {
}
}
改造辅助文本函数
为了文本展示方便,就把文本函数createText
改造一下,可以支持传入文本样式和坐标位置等基本参数。
createText(text='你好~', textStyle = {}, props = {}) {
const textContainer = new PIXI.Container()
const finalStyle = {
width: 80,
fontSize: 48,
fontFamily: 'serif',
fill: 0xffffff,
x: 200,
...textStyle
}
const finalProps = {
x: 100,
y: 200,
...props
}
const showText = new PIXI.Text(text, finalStyle)
Object.keys(finalProps).map(key => {
showText[key] = finalProps[key]
})
textContainer.addChild(showText)
return textContainer
}
show() {
const demoText = this.createText('loader加载完成', {fontSize: 36}, {y: 300})
this.stage.addChild(demoText)
}
辅助函数:创建精灵
从上面通过loader
加载的图片可以看出来,我们需要重复加载图片、创建精灵的过程,那就简单封装一下。
resource (name) {
return new PIXI.Sprite(this.loader.resources[name].texture)
}
添加精灵到舞台
把舞台背景色恢复成白色,开始添加精灵元素。
创建刹车容器,把容器添加到舞台上。然后把车架、车闸、车把依次添加到容器中,并调整车闸的坐标点使它能够和车把贴合在一起,调整车闸的原点以便于模拟捏住/松开车闸的动效。
缩放刹车容器,使之能完整展示在舞台上。
show() {
const brakeContainer = new PIXI.Container()
this.stage.addChild(brakeContainer)
const brakeBike = this.resource('brake_bike.png')
brakeContainer.addChild(brakeBike)
const brakeLever = this.resource('brake_lever.png')
brakeLever.pivot.x = brakeLever.width
brakeLever.pivot.y = brakeLever.height
brakeLever.x = 780
brakeLever.y = 950
brakeContainer.addChild(brakeLever)
const brakeHandlerbar = this.resource('brake_handlerbar.png')
brakeContainer.addChild(brakeHandlerbar)
brakeContainer.scale.x = brakeContainer.scale.y = 0.3
}
调整容器位置,适配屏幕
我们通过resize
事件来监听屏幕宽高,调整刹车容器的位置一直处于屏幕的右下角。
show() {
//...
function resize () {
brakeContainer.x = window.innerWidth - brakeContainer.width
brakeContainer.y = window.innerHeight - brakeContainer.height
}
window.addEventListener('resize', resize);
resize()
}
创建并添加按钮
在类中添加一个创建按钮元素的createButton
函数,调整按钮的原点,并且通过GSAP
来创建按钮的不透明度、缩放过渡动画。
class BrakeBanner {
//...
createButton () {
const actionButton = new PIXI.Container();
const btnImage = this.resource('btn.png')
const btnCircleImage = this.resource('btn_circle.png')
const btnCircleImage2 = this.resource('btn_circle.png')
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.scale.x = btnCircleImage.scale.y = 0.8
actionButton.addChild(btnImage)
actionButton.addChild(btnCircleImage)
actionButton.addChild(btnCircleImage2)
gsap.to(btnCircleImage.scale, { duration: 1, x: 1.2, y: 1.2, repeat: -1 })
gsap.to(btnCircleImage, { duration: 1, alpha: 0, repeat: -1 })
actionButton.scale.x = actionButton.scale.y = 0.6
this.stage.addChild(actionButton)
return actionButton
}
//...
}
在show
方法中调用生成一个按钮元素,并且在resize
函数中,通过刹车容器的坐标位置来定位按钮元素的位置。
show() {
//...
const actionButton = this.createButton()
function resize () {
//...
actionButton.x = window.innerWidth - brakeContainer.width + 142
actionButton.y = window.innerHeight - brakeContainer.height + 220
}
//...
}
按钮的鼠标监听事件
对按钮元素的鼠标按下和松开事件进行监听,进行对应的函数处理。
当监听到按下事件时,使用GSAP
过渡动画让车闸角度旋转,达到捏紧车闸的效果。
当监听到松开事件时,使车闸旋转角度归零,达到松开车闸的效果。
show() {
//...
actionButton.interactive = true
actionButton.buttonMode = true
actionButton.on('mousedown', function () {
gsap.to(brakeLever, { duration: .6, rotation: -35 * Math.PI / 180 })
})
actionButton.on('mouseup', function () {
gsap.to(brakeLever, { duration: .6, rotation: 0 })
})
}
鼠标行为控制按钮状态
当鼠标按下/松开时,监听一个对应的状态暂停/开始事件,来控制按钮等元素的展示状态,顺便把车闸动画控制也放进去。
show() {
//...
function start () {
gsap.to(brakeLever, { duration: .6, rotation: 0 })
gsap.to(actionButton, {duration: 0.6, alpha: 1})
gsap.to(actionButton.scale, {duration: 0.6, x: 0.6, y: 0.6})
}
function pause () {
gsap.to(brakeLever, { duration: .6, rotation: -35 * Math.PI / 180 })
gsap.to(actionButton, {duration: 0.6, alpha: 0})
gsap.to(actionButton.scale, {duration: 0.6, x: 0.2, y: 0.2})
}
//...
}
创建随机粒子
创建一个粒子容器,并把容器添加到舞台上。
定义一组色值,通过Graphics
创建圆形形状并通过随机选取色值,循环创建10个粒子,并插入到粒子容器。
为了保证所有粒子是倾斜运动的,我们把粒子容器直接旋转一定的角度。
show() {
//...
const particleContainer = new PIXI.Container()
const particles = []
this.stage.addChild(particleContainer)
const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x000000]
for (let i = 0; i < 10; i++) {
const gr = new PIXI.Graphics()
gr.beginFill(colors[Math.floor(Math.random() * colors.length)])
gr.drawCircle(0, 0, 6)
gr.endFill()
const pItem = {
sx: Math.random() * window.innerWidth,
sy: Math.random() * window.innerHeight,
gr
}
gr.x = pItem.sx
gr.y = pItem.sy
particles.push(pItem)
particleContainer.addChild(gr)
particleContainer.rotation = Math.PI / 180 * 35
}
//...
}
让粒子动起来
粒子怎么动?我们要反着想一下,粒子运动时发生位移,坐标发生变化,也就是说我们直接改变粒子的坐标,在视觉上就是粒子动起来了。
而且为了让粒子有一个加速的过程,我们设置一个不断自增的speed来改变粒子的y坐标。当速度达到一定程度时改变粒子x轴和y轴的缩放比,使粒子呈现出速度线的效果,当然为了科学,我们要让speed的增大有一个上限。
let speed = 2
function loop () {
for (let i = 0; i < particles.length; i++) {
const partItem = particles[i]
let gr = partItem.gr
speed++
speed = Math.min(speed, 20)
gr.y += speed
if (gr.y >= window.innerHeight) {
gr.y = 0
}
if (speed === 20) {
gr.scale.x = 0.03
gr.scale.y = 40
}
}
}
但是,我们直接调用loop函数是肯定看不到想要的效果的。需要在鼠标按下时,让GSAP
动画添加loop函数的侦听器;在鼠标松开时,同步移除loop函数的侦听。
show() {
const conY = brakeContainer.y
function start () {
//...
speed = 0
gsap.ticker.add(loop)
}
function pause () {
//...
gsap.ticker.remove(loop)
for (let i = 0; i < particles.length; i++) {
const partItem = particles[i]
let gr = partItem.gr
gsap.to(gr, { duration: 0.3, x: partItem.sx, y: partItem.sy, ease: 'elastic.out' })
gsap.to(gr.scale, { duration: 0.3, x: 1, y: 1, ease: 'elastic.out' })
}
}
start()
}
这时候,我们可以看到效果已经基本完善了。
细节处理
对比vanmoof的动效,我们还能看到在运动时,(因为速度)车架是半透明状态,在刹车时,(突然减速)车架变得清晰,自行车容器略微抬起。
我们定义一个变量来记录自行车容器的初始Y轴坐标位置,便于运行时复位和刹车时抬起的校准。
show() {
//...
const conY = brakeContainer.y
function start() {
//...
gsap.to(brakeBike, { duration: 0.3, alpha: 0.6 })
gsap.to(brakeContainer, { duration: 0.3, y: conY })
}
function pause() {
//...
gsap.to(brakeBike, { duration: 0.3, alpha: 1 })
gsap.to(brakeContainer, { duration: 0.3, y: conY + 100 })
}
//...
}
写在最后
这个时候来看,是不是稍微好一点点啦?我们参照动效进行仿写,写完记个笔记就更便于我们入门PixiJS和GSAP。当然,其实还有一些细节需要去完善,比如按钮通过形状Graphics
和文本Text
来实现。
吃水不忘挖井人,感谢大帅老师的直播讲解。在公众号里搜 大帅老猿
,在他这里可以学到很多东西。