前言
事情是这样的,上个月接了一个活儿。客户要我们模仿一个国外的网站 https://www.vanmoof.com/en-NL... (需要科学上网)
为他们做几个展示动效。
确实很酷!这个项目一共6个动效,客户给的工期是15天,我们实际花在真正写代码上的工期大概一个礼拜左右。报酬2个W
。
可能有同学觉得赚得也不多...的确不算多,但接活儿赚钱只是一方面,更重要的是在这个项目里我带着很多同学们一起实战获得了宝贵的项目实战经验。
并且这种动效开发,很多只会用vue
,react
写页面的前端同学很少能接触到。
想一起接外包私活儿的朋友加v吧:dashuailaoyuan
最终效果
先看最终效果
还原得还是很接近的吧,下面我就来一步步教大家如何写
准备好基础环境
https://github.com/ezshine/YC...
我也给大家准备好了一个空白项目,大家可以clone
这个仓库一步步来实现。
空白项目很简单,引入了两个库,一个叫PIXI
,是性能非常优秀的webGL2d
渲染引擎。另一个GSAP
,是非常优秀的前端动画引擎,这个库非常老牌,从我还在做FLASH
的时代就一直用它。
然后大家安装一下 VSCode
里的 live preview
插件,一定要注意是微软官方出品的,不要装了山寨版。
本教程视频版
https://www.bilibili.com/vide...
创建PIXI应用
class BrakeBanner{
constructor(selector){
//初始化PIXI应用,将舞台设置为1920x1080
this.app = new PIXI.Application({
width:window.innerWidth,
height:window.innerHeight,
backgroundColor: 0xffffff,
resizeTo:window
});
document.querySelector(selector).appendChild(this.app.view);
this.stage = new PIXI.Container();
this.app.stage.addChild(this.stage);
}
}
加载图片资源
//创建资源加载器
this.loader = new PIXI.Loader();
//向资源加载器添加资源 key,path
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();
});
this.loader.resources[key];
通过图片创建精灵元素
const btnImage = new PIXI.Sprite(this.loader.resources['btn.png'].texture);
btnContainer.addChild(btnImage);
const btnCircleImage = new PIXI.Sprite(this.loader.resources['btn_circle.png'].texture);
btnContainer.addChild(btnCircleImage);
转换中心点设置
btnImage.pivot.x = btnImage.pivot.y = btnImage.width/2;
btnCircleImage.pivot.x = btnCircleImage.pivot.y = btnCircleImage.width/2;
btnContainer.x = 200;
btnContainer.y = 200;
初尝动效GSAP
btnCircleImage.scale.x = btnCircleImage.scale.y = .8;
gsap.to(btnCircleImage,{duration:1,alpha:0,repeat:-1})
gsap.to(btnCircleImage.scale,{duration:1,x:1.1,y:1.1,repeat:-1})
把按钮相关的代码整合进 createActionButton 函数中
车架和刹车把手
const bikeContainer = new PIXI.Container();
this.stage.addChild(bikeContainer);
//整体进行一个缩放
bikeContainer.scale.x = bikeContainer.scale.y = .3;
const bikeImage = new PIXI.Sprite(this.loader.resources['brake_bike.png'].texture);
bikeContainer.addChild(bikeImage);
const bikeHandlerImage = new PIXI.Sprite(this.loader.resources['brake_handlerbar.png'].texture);
bikeContainer.addChild(bikeHandlerImage);
车把手
const bikeLeverImage = new PIXI.Sprite(this.loader.resources['brake_lever.png'].texture);
bikeContainer.addChild(bikeLeverImage);
bikeLeverImage.x = 255+454;
bikeLeverImage.y = 450+462;
bikeLeverImage.pivot.x = 454;
bikeLeverImage.pivot.y = 462;
bikeLeverImage.rotation = Math.PI/180*-35;
现在我们这个刹车的旋转角度已经OK了。
添加按钮交互
actionBtn.on("mousedown",()=>{
bikeLeverImage.rotation = Math.PI/180*-35;
})
actionBtn.on("mouseup",()=>{
bikeLeverImage.rotation = 0;
})
加入GSAP
actionBtn.on("mousedown",()=>{
gsap.to(bikeLeverImage,{duration:.6,rotation : Math.PI/180*-35});
})
actionBtn.on("mouseup",()=>{
gsap.to(bikeLeverImage,{duration:.6,rotation : 0});
})
是不是更有感觉了?OK,到这里大家对PIXI和GSAP已经算是入了门了,接下来我们实现这个速度线粒子效果。
将车体永远置于画布右下角
let resize = () => {
bikeContainer.x = window.innerWidth-bikeContainer.width;
bikeContainer.y = window.innerHeight-bikeContainer.height;
}
window.addEventListener('resize', resize);
resize();
速度粒子效果
怎么做?我们再仔细观察一下参考案例
- 首先得要有粒子,也就是小圆点
- 小圆点的颜色有好几种
- 向某一个角度一直移动
- 超出底部边界后回到顶部继续移动
- 按住鼠标停止
- 停止的时候还有一点回弹的效果
- 松开鼠标继续
OK,一条一条来实现
创建粒子容器和对象数组
let particleZoneSize = window.innerWidth;
let particlesContainer = new PIXI.Container();
this.stage.addChild(particlesContainer);
let particles = [];
for(let i=0;i<10;i++){
let gr = new PIXI.Graphics();
gr.beginFill(0xff0000);
gr.drawCircle(0,0,6);
gr.endFill();
let parItem = {
sx:Math.random()*particleZoneSize,
sy:Math.random()*particleZoneSize,
}
gr.x = parItem.sx;
gr.y = parItem.sy;
particlesContainer.addChild(gr);
}
粒子随机颜色
const colors = [0xf1cf54, 0xb5cea8, 0xf1cf54, 0x818181, 0x000000];
for(let i=0;i<10;i++){
//...
gr.beginFill(colors[Math.floor(Math.random() * colors.length)]);
//...
粒子的持续移动
向一个角度一直移动大家知道算法吗?
gr.x -= cos(angle) * 1;
gr.y -= sin(angle) * 1;
上述方法不过多的说明,我教大家一个更简单的方法。旋转容器,其实我们只要控制粒子向下移动即可,然后将这个容器旋转一个角度,那么所有粒子的移动方向也就都改变了。
particlesContainer.pivot.set(particleZoneSize / 2, particleZoneSize / 2);
particlesContainer.rotation = (35 * Math.PI) / 180;
particlesContainer.x = particleZoneSize/2;
particlesContainer.y = particleZoneSize/2;
旋转的时候,我们仍然需要注意一下将旋转的圆心设置到粒子容器的中心点上。
OK,现在让我们让粒子移动起来
通过gsap.ticker.add();
添加一个屏幕渲染时机的钩子,就是requestAnimationFrame
事件。
function loop(){
for (let i = 0; i < particles.length; i++) {
//拿到粒子item和小圆点实例
let pItem = particles[i];
let gr = pItem.gr;
//让粒子走起来
gr.y += 1;
//当粒子移动超出范围时回到顶部
if(gr.y>innerWidth)gr.y=0;
}
}
gsap.ticker.add(loop);
这个速度显然不够快,我们可以多调整一下每次移动的距离看看效果。略微有点感觉了,对吧
然后我们还希望速度是由慢到快的,所以我们定义一个speed的变量,让它从0 开始不断变大,直到一个固定值。
let speed = 0;
function loop(){
speed+=.5;
speed = Math.min(speed,20);
for (let i = 0; i < particles.length; i++) {
//拿到粒子item和小圆点实例
let pItem = particles[i];
let gr = pItem.gr;
gr.y += speed;
if(gr.y>innerWidth)gr.y=0;
}
}
感觉更加舒服了一些,但还是不到位,现在我来教大家一些魔法
人的眼睛观察一个球体直线运动时,球体会发生什么变化?
它会慢慢被拉长,变细对不对?
所以我们用代码来模拟这个拉长变细的效果。
if(speed>=20){
gr.scale.y = 40;
gr.scale.x = 0.03
}
OK ,接下来我们写停止,停止的时候,有一个回弹的效果。
有停止就有启动,把之前的代码定义成一个启动函数,loop函数不变
function start(){
speed = 0;
gsap.ticker.add(loop);
}
停止粒子运动
function pause(){
//先移除掉requestAnimationFrame的侦听
gsap.ticker.remove(loop);
for (let i = 0; i < particles.length; i++) {
let pItem = particles[i];
let gr = pItem.gr;
//恢复小圆点的拉伸比例
gr.scale.x = gr.scale.y = 1;
//恢复小圆点透明度
gr.alpha = 1;
//让所有的小圆点使用弹性补间动画回到初始坐标
gsap.to(gr, {
duration: 0.6,
x: pItem.sx,
y: pItem.sy,
ease: "elastic.out",
});
}
}
OK,把这个两个函数和咱们按钮的按下和松开事件关联上。这个动效就做好了。
最终效果
咱们还原得还是相当可以的吧,动效基本都是这样的原理,希望大家都可以自己去练习一下。
在公众号里搜 大帅老猿
,在他这里可以学到很多东西