携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
你能想象,上图中这个非常酷炫的刹车动效,据说是一个高达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你就可以看到整个画布就呈现出来了。
由于我们提前准备好了所需要的图片,现在我们就需要通过
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)
}
此时按钮便出现在画布上了。
添加呼吸效果,移动图片位置
但是很明显我们没有呼吸效果,并且按钮的位置也不对,下面我们就要添加呼吸效果,移动图片位置。
添加呼吸效果我们需要再添加一个圆环,让后添加的圆环一直从缩小到方法循环。
实现动画效果就要用到了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 })
车身与车把的实现
有了上面按钮的实现,车身与车把的实现就简单多了,同样是将其加载到画布上即可。
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()
可以看出车身已经出现在了画图上,但是由于加载顺序的原因,按钮在车身底层,我们需要将按钮置于车身上层。
this.app.stage.addChild(bikeContainer)
this.app.stage.addChild(actionButton)
由于按钮将刹车挡住了,并且按钮有点过大,我们优化一下视图。
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 })
})
粒子实现
我们可以看出,图片上有多个粒子分布在画布不同位置,当车身为移动态时,粒子高速向后运动,使车身实现高速运动效果。那么我们就先将粒子分布到画布上。
-
创建多个不规则位置的粒子
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) }
-
粒子颜色
预设颜色集合
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) -
粒子初始速度慢,逐渐加速,按住逐渐停止。启停拥有回弹效果
- 绑定按钮启停
- 启动停止拥有回弹效果,使用gasp.to的ease,设置动画效果,有[更多的效果请查看](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()
最终实现
最终效果图如上,实际项目中,还需要进行更多的修改,比如说每一个粒子都应该有固定的位置而不是每次随机生成,回弹时有更加美化的轨迹而不是线性的回弹。以及将车把至于最上层,让车把覆盖运动的粒子,使其更加有立体感,按钮应该在车把的位置,以至于有更明显的提示,这些都是优化页面的过程,而代码组件化,重构,提取则是代码层面的优化,今天所学习的只是一部分核心代码。更多的优化过程还需要小伙伴们发挥自己的想象去完成。
如果看完的小伙伴们觉得没有那么无聊的话,希望点赞支持一下,(^▽^)
在公众号里搜
大帅老猿,找他做技术外包很靠谱 】