上万元的兼职项目,PIXI动画初体验,酷炫刹车动效的实现|猿创营

2,233 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

1.gif

你能想象,上图中这个非常酷炫的刹车动效,据说是一个高达2W的特效中的一部分。

听到这个数字,你一定和我一样,觉得这个特效十分高级,肯定不是你我常人所能企及的。

但是看完这篇文章,我相信你便会相信,你自己也可以做的到。

面对一个需求的时候,我们需要知道我们要做什么和怎么做。

拆分需求

我们需要做的有:

  • 一个控制启停的按钮

    • 按钮有呼吸效果
    • 可以控制车身运动
    • 可以控制粒子运动
  • 一个自行车

    • 车身随着运动增删蒙层
    • 刹车随着按钮运动
  • 线性运动的粒子

    • 数个线性规则运动的粒子
    • 粒子运动时会被拉长,停止时恢复
    • 启动停止瞬间有回弹效果

渲染引擎选择

这次我们选择的是PIXI配合GSAP来实现需求。

Pixi是一个非常快的2D sprite渲染引擎。它可以帮助你显示、动画和管理交互式图形,这样你就可以轻松地使用JavaScript和其他HTML5技术制作游戏和应用程序。

GSAP:健全的web动画库之一,拥有速度快,基础动画多,兼容性好,轻量化,零依赖等特点。

具体实现

组件库的引入

工欲善其事必先利其器,首先我们需要将我们所需要的组件库引入到我们的项目中。

<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>

代码初始化

其中brakebanner.js是我们主要代码的文件。我们需要在index.html初始化时启动我们的代码。

<script>
    window.onload = init;

    function init() {
      let banner = new BrakeBanner("#brakebanner");
    }
  </script>
</head>

<body>
  <div id="brakebanner"></div>
</body>

主函数实现

下面我们就要实现主函数

class BrakeBanner {
  constructor(selector) {
    this.app = new PIXI.Application({
      width: window.innerWidth,
      height: window.innerHeight,
      backgroundColor: 0xffffff
    })

    document.querySelector(selector).appendChild(this.app.view)

  }
}

首先你可以先将backgroundColor设置为其他颜色,如0xffff00你就可以看到整个画布就呈现出来了。

图片.png 由于我们提前准备好了所需要的图片,现在我们就需要通过PIXI提过的加载器Loader将我们的图片加载到画布上。

  • 可链接的add使资源进入队列
  • ``load 方法加载资源队列,并在所有资源加载完毕后调用传入的回调。
  • loader.onComplete.add(() => {}); // 排队的资源全部加载时调用一次。
this.loader = new PIXI.Loader()
	
this.loader.add("btn", "images/btn.png")
this.loader.add("btn_circle", "images/btn_circle.png")
this.loader.add("brake_handlerbar", "images/brake_handlerbar.png")
this.loader.add("brake_bike", "images/brake_bike.png")
this.loader.add("brake_lever", "images/brake_lever.png")

this.loader.load()

this.loader.onComplete.add(() => {
	this.show()
})

通过show函数,将添加到资源队列的图片全部加载完成时,实现我们的需求。

画布内容填充

按钮的实现

我们需要实现一个有呼吸动效的按钮。

我们通过let container = new PIXI.Container();创建一个对象集合。用stage方法添加显示对象。

并且将图片对象加载到画布上。

show () {
	// 按钮
	let actionButton = new PIXI.Container()
	this.app.stage.addChild(actionButton)
	actionButton.interactive = true // 启动鼠标交互
	actionButton.buttonMode = true // 鼠标放置到图片上显示手指
	let btnImage = new PIXI.Sprite(this.loader.resources['btn'].texture)
	let btnCircle = new PIXI.Sprite(this.loader.resources['btn_circle'].texture)
	actionButton.addChild(btnImage)
	actionButton.addChild(btnCircle)
}

此时按钮便出现在画布上了。

图片.png

添加呼吸效果,移动图片位置

但是很明显我们没有呼吸效果,并且按钮的位置也不对,下面我们就要添加呼吸效果,移动图片位置。

添加呼吸效果我们需要再添加一个圆环,让后添加的圆环一直从缩小到方法循环。

实现动画效果就要用到了gsap库了。

 // 添加新环
const btnCircle2 = new PIXI.Sprite(this.loader.resources['btn_circle'].texture)
actionButton.addChild(btnCircle2)

// 将三张图中心点从左上角移动到圆心
btnImage.pivot.x = btnImage.pivot.y = btnImage.width / 2
btnCircle.pivot.x = btnCircle.pivot.y = btnCircle.width / 2
btnCircle2.pivot.x = btnCircle2.pivot.y = btnCircle2.width / 2

// 将按钮画布中心点移动
actionButton.x = actionButton.y = 600
// 将实现呼吸效果的环初始大小缩小
btnCircle.scale.x = btnCircle.scale.y = 0.8
// 呼吸环在一秒的时间内,缩放到横纵都变为1.3倍,repeat:-1代表无限重复
gsap.to(btnCircle.scale, { duration: 1, x: 1.3, y: 1.3, repeat: -1 })
// 呼吸环,一秒内变为透明,无限重复
gsap.to(btnCircle, { duration: 1, alpha: 0, repeat: -1 })

2.gif

车身与车把的实现

有了上面按钮的实现,车身与车把的实现就简单多了,同样是将其加载到画布上即可。

const bikeContainer = new PIXI.Container()
this.app.stage.addChild(bikeContainer)

// 由于车身图片过大,只好将其缩小
bikeContainer.scale.x = bikeContainer.scale.y = 0.3

// 加载图片
const bikeImage = new PIXI.Sprite(this.loader.resources['brake_bike'].texture)
const bikeHandlerbar = new PIXI.Sprite(this.loader.resources['brake_handlerbar'].texture)
const bikeLever = new PIXI.Sprite(this.loader.resources['brake_lever'].texture)
// 添加至画布
bikeContainer.addChild(bikeImage)
bikeContainer.addChild(bikeLever)
bikeContainer.addChild(bikeHandlerbar)


// 重置刹车中心
bikeLever.pivot.x = 455
bikeLever.pivot.y = 455

// 将刹车移动到车把相应位置
bikeLever.x = 722
bikeLever.y = 900

// 实现车身始终在画图右下角
const resize = () => {
	bikeContainer.x = window.innerWidth - bikeContainer.width
	bikeContainer.y = window.innerHeight - bikeContainer.height
}

window.addEventListener('resize', resize)
resize()

图片.png

可以看出车身已经出现在了画图上,但是由于加载顺序的原因,按钮在车身底层,我们需要将按钮置于车身上层。

this.app.stage.addChild(bikeContainer)

this.app.stage.addChild(actionButton)

图片.png

由于按钮将刹车挡住了,并且按钮有点过大,我们优化一下视图。

actionButton.x = actionButton.y = 400

actionButton.scale.x = actionButton.scale.y = .6

这样有助于一会儿我们实现,按住刹车,松开前行的动画。

按住刹车,松开前行

这时我们需要对按钮和车身进行关联。实现按下按钮刹车,松开按钮运动的效果。

对按钮添加按下,松开事件。

// 初始值为运动态,车轮虚化
bikeImage.alpha = .5
// 监听鼠标按下
actionButton.on("mousedown", () => {
	// 刹车旋转
	gsap.to(bikeLever, { duration: .3, rotation: Math.PI / 180 * -30 })
  // 车身后移
	gsap.to(bikeContainer, { duration: .3, y: bikeContainer.y + 30 })
	// 车轮实化
	gsap.to(bikeImage, { duration: .3, alpha: 1 })
})
// 监听鼠标抬起
actionButton.on("mouseup", () => {
	// 刹车、车身归位、车轮虚化
	gsap.to(bikeLever, { duration: .3, rotation: 0 })
	gsap.to(bikeContainer, { duration: .3, y: bikeContainer.y - 30 })
	gsap.to(bikeImage, { duration: .3, alpha: .5 })
})

3.gif

粒子实现

图片.png

我们可以看出,图片上有多个粒子分布在画布不同位置,当车身为移动态时,粒子高速向后运动,使车身实现高速运动效果。那么我们就先将粒子分布到画布上。

  • 创建多个不规则位置的粒子

    • Graphics类包含一些方法,这些方法可将原始形状(例如线条,圆形和矩形)绘制到显示器上,并为它们上色和填充。
    // 创建粒子图集
    const particleContainer = new PIXI.Container()
    const particles = []
    this.app.stage.addChild(particleContainer)
    // 创建十个粒子
    for (let i = 0; i < 10; i++) {
    	// 实现粒子
    	const gr = new PIXI.Graphics()
    	// 粒子颜色
    	gr.beginFill(0x000000)
    	// 粒子大小
    	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
    	// 像图集中添加粒子
    	particleContainer.addChild(gr)
    	// 将每一个点都保存起来
    	particles.push(pItem)
    }
    

图片.png

  • 粒子颜色

    预设颜色集合const particleColors = [0xf1cf54, 0xb5cea8, 0x000000, 0x818181]添加到粒子上gr.beginFill(particleColors[Math.floor(Math.random() * particleColors.length)])

  • 每个粒子以同一角度,高速移动,实现粒子拖影

    • 实现一个loop函数,使粒子位置循环移动
    • 当移出画面时,回到顶部
    • 这部分最难理解的是拖影问题,当粒子高速运动时,视觉上会被拉长,同时粒子会变窄
    let speed = 0 // 初始速度
    function loop() {
    	// 每一次循环,速度加0.5
    	speed += .5
    	// 设置速度上限
    	speed = Math.min(speed, 20)
    
    	// 为每一个点进行移动设置
    	for (let i = 0; i < particles.length; i++) {
    		const pItem = particles[i];
    
    		// 由于画图进行过旋转,所以直接加y轴,便是倾斜移动
    		pItem.gr.y += speed
    		// 现在加速过程中,粒子会产生拖影,将粒子压扁拉长来实现拖影效果
    		// 设置粒子拖影上限,否则将会无限拉长
    		if (speed < 20) {
    			pItem.gr.scale.y += 10
    			pItem.gr.scale.x /= 2
    		} else {
    			pItem.gr.scale.y = 40
    			pItem.gr.scale.x = 0.03
    		}
    
    		// 超出边界,回到顶部
    		if (pItem.gr.y >= window.innerHeight) pItem.gr.y = 0
    	}
    }
    // gsap.ticker就像 GSAP 引擎的心跳,用于循环,非常适合游戏开发
    gsap.ticker.add(loop)
    
  • 粒子初始速度慢,逐渐加速,按住逐渐停止。启停拥有回弹效果

    • 绑定按钮启停

4.gif

-   启动停止拥有回弹效果,使用gasp.toease,设置动画效果,有[更多的效果请查看](https://greensock.com/docs/v3/Eases)

// 启动函数
function start() {
	// 设置初始速度
	speed = 0
	// 初始化每一个粒子
	for (let i = 0; i < particles.length; i++) {
		const pItem = particles[i];
		pItem.gr.scale.y = 1
		pItem.gr.scale.x = 1
		// 启动时的回弹效果,当加速启动时,会有一个向后蓄力的效果
		gsap.to(pItem.gr, {
			duration: .2,
			x: pItem.sx,
			y: pItem.sy - 40,
			ease: "back.out"
		})
	}
	gsap.ticker.add(loop)
}
// 设置暂停
function pause() {
	// 清除心跳
	gsap.ticker.remove(loop)
	// 将每一个粒子归位
	for (let i = 0; i < particles.length; i++) {
		const pItem = particles[i];
		pItem.gr.scale.y = 1
		pItem.gr.scale.x = 1
		// 停止时的回弹效果
		gsap.to(pItem.gr, {
			duration: .4,
			x: pItem.sx,
			y: pItem.sy,
			ease: "elastic.out"
		})
	}
}	

// 初始化时自动加速
start()

最终实现

5.gif

最终效果图如上,实际项目中,还需要进行更多的修改,比如说每一个粒子都应该有固定的位置而不是每次随机生成,回弹时有更加美化的轨迹而不是线性的回弹。以及将车把至于最上层,让车把覆盖运动的粒子,使其更加有立体感,按钮应该在车把的位置,以至于有更明显的提示,这些都是优化页面的过程,而代码组件化,重构,提取则是代码层面的优化,今天所学习的只是一部分核心代码。更多的优化过程还需要小伙伴们发挥自己的想象去完成。

如果看完的小伙伴们觉得没有那么无聊的话,希望点赞支持一下,(^▽^)

公众号里搜 大帅老猿,找他做技术外包很靠谱 】