背景
这件事要从大促说起….每年411都是豌豆公主的专属樱花节。既然是樱花节,那么很多活动和元素都是围绕着樱花展来的。就比如这次要分享的内容,需要做一个樱花飞舞的画面,当然由于时间问题,可能还不是那么炫酷,这里主要是和大家分享一些思路,具体的效果,大家可以自己发挥想象不是。
具体需求
这里咱们暂且只说一部分需求的描述。整个活动就是一个抽奖活动,点击抽奖按钮之后,需要有一个樱花飞舞的转场动画。动画要求:每个花瓣都能从大到小的的变化,并且能够做出层次感的透视效果,当全部樱花都缩小完成,开始逐渐飞出屏幕。
实现思路
动画的动作主要分几个步奏:
- 花瓣要能从大到小的变换,我们可以通过scale来处理。
- 花瓣从大到小的变化需要有层次感,也就是说每个花瓣都需要有延迟绘制的能力,也就是需要delay方法
- 花瓣依此飞出屏幕。这个的话其实可以沿用上边delay的逻辑,只要一片花瓣延迟了缩小动作,那么理论上之后的动画都会慢半拍。
实现
每个花瓣都应该拥有自己的属性
每个花瓣都应该拥有自己的属性
这句话应该怎么去理解?
大家不妨这么想,我们的这一组樱花飞舞的动画其实不是一个花瓣就能完成的,但是如果针对全部花瓣一个个的处理就太麻烦了,所以我们就需要一个类将花瓣进行抽象,那这个类需要那些东西呢?如下:
- 花瓣的图片信息:imgResource
- 花瓣的坐标:x、y
- 花瓣的大小:width、height
- 花瓣的缩放程度:scale
- 最终缩放的倍数:finalSizeScale
- 花瓣的旋转度数:rotate
- 花瓣的透明度:opacity
- 花瓣的变化的系数(包括透明度,大小,移动速度):speed
- 改变花瓣大小与透明度之前的等待时间:delay
- 花瓣的移动前的等待时间:shrinkDelay
花瓣需要随机摆放
代码如下
// 生成樱花;
for(let i = 0; i < 20; i++) {
sakuraList[i] = [];
for (let j = 0; j < i; j++) {
sakuraList[i].push(new Sakura(
ctx,
imgInfo,
Math.random() * canvasCenterX * 4 - canvasCenterX * 2, //x
Math.random() * canvasCenterY * 4 - canvasCenterY * 2, //y
1.8, // scale
(Math.random() * 180) * Math.PI/180, // rotate
// 速度这里i越小越快
Math.floor(Math.random() * 3 + 3) / 100, // speed
0, //opacity
canvas, //canvas
Math.floor(Math.random() * 7 + 20) / 100, // final
i * 2, //delay
));
}
}
复制代码
通过上边的代码,我们不难看出这是个初始化樱花花瓣的过程,其中我们应该关注的是随机的位置的计算。
这里边有一个比较关键的就是,如何可以相对随机均匀的分布花瓣,也许很多同学第一反应就是用random
,然后通过画布的width、height
来进行随机分布。
这个方法乍一看,感觉一切正常,那我们也用代码实现一下,看看分布效果。
Math.random() * canvas.width, //x
Math.random() * canvas.height, //y
复制代码
如图所示,其实这个分布效果并不会很理想。解决方案也很简单就是我们可以通过画布的中心点进行计算,用代码实现一下~
Math.random() * canvasCenterX * 4 - canvasCenterX * 2, //x
Math.random() * canvasCenterY * 4 - canvasCenterY * 2, //y
复制代码
这么一看,是不是均匀不少了。
花瓣的出现需要从大到小变化
需求上说,咱们花瓣的出现需要由大到小的一个变化过程,那么逻辑也就比较简单了,在不考虑其他情况下,我们只需要定义两个值:
- 花瓣在初始化的时候的大小
- 花瓣花瓣最终静止之后的大小
这两个值我们在初始化的时候就已经定义了,代码片段就是:
class Sakura {
/**
* imgResource 图片地址
* x
* y
* scale 图片缩放
* rotate 旋转
* speed 速度
* opacity 透明度
* finalSizeScale
* delay 延迟
*/
constructor(
imgResource,
x = 0,
y = 0,
scale = 4,
rotate = 0,
speed = 1,
opacity = 0,
finalSizeScale = 0.2,
delay
) {
this.imgResource = imgResource;
this.width = imgResource.width;
this.height = imgResource.height;
this.x = x;
this.y = y;
this.scale = scale; // 初始化的大小
this.rotate = rotate;
this.speed = speed;
this.opacity = opacity;
this.finalSizeScale = finalSizeScale; // 最终要缩放到的大小
this.delay = delay;
this.shrinkDelay = 7;
}
}
复制代码
那有了这两个值,我们就可以通过刷新机制进行计算,那先看看效果。
/**
* 缩放方法
* sakura就是我们之前初始化的花瓣对象
*/
changeScale(sakura) {
if(sakura.scale > sakura.finalSizeScale){
sakura.scale -= sakura.speed;
} else {
sakura.scale = sakura.finalSizeScale;
}
},
复制代码
那么动画效果是实现了,但是总觉的是有些生硬,那我们可以加入一些缓动的算子进入,进行一些优化。
/**
* 缩放方法
* sakura就是我们之前初始化的花瓣对象
*/
changeScale(sakura) {
if(sakura.scale > sakura.finalSizeScale){
sakura.scale -= ((sakura.scale - sakura.finalSizeScale) * sakura.speed);
} else {
sakura.scale = sakura.finalSizeScale;
}
},
复制代码
如上代码,我们将缩小的方式进行了一些优化,替换上了一个缓动公式
当前值 += (目标值-当前值)*系数
当然,这个系数大家可以慢慢的去调自己想要的感觉,我这里就是个小丑。
花瓣的出现需要有透明度变化
透明度的变化其实与缩放是相同的思路,所以函数也是可以直接使用
/**
* 透明度变换的方法
* sakura就是我们之前初始化的花瓣对象
*/
changeOpacity(sakura) {
if(sakura.opacity < 1) {
// 当前值 += (目标值-当前值)*系数
sakura.opacity += ((1 - sakura.opacity) * sakura.speed);
} else {
sakura.opacity = 1;
}
},
复制代码
这边透明度的变换就直接使用之前的缓动公式来实现,让我们看看效果
看样子,貌似没有问题,只是我们花瓣太多了,显得不那么明显,但是从和上边的动图进行比较,其实已经发生了一些视觉上的变化
花瓣的出现需要有层次感觉
需要一个层次感,说白了,就是需要有一个对每一个花瓣进行一个延迟设置,以此来实现一个视觉差。那其实我们可以通过设定一个delay
属性,通过减法操作来实现一个delayAnimation
的方法。
/**
* 延迟方法
* sakura就是我们之前初始化的花瓣对象
*/
delayAnimation(sakura) {
sakura.delay > 0 && sakura.delay --;
},
复制代码
delay的方法设计好之后,我们还需要考虑一点就是对透明度和缩放做一个拦截,要不然delay就没有任何意义了。
drawSakuraAnimation() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let item of sakuraList) {
for(let item2 of item) {
this.ctx.globalAlpha = item2.opacity;
item2.delay <= 0 && this.changeScale(item2);
item2.delay <= 0 && this.changeOpacity(item2);
if(item2.opacity > 0) {
this.ctx.save();
this.ctx.rotate(item2.rotate);
item2.ctx.drawImage(item2.imgResource, item2.x, item2.y, item2.width*item2.scale, item2.height*item2.scale);
this.ctx.restore();
}
this.delayAnimation(item2);
}
}
window.requestAnimationFrame(this.drawSakuraAnimation);
},
复制代码
那如上,可以看到只有在delay为0之后才会开始执行绘制,那我们来看看效果。
那可以看到,和之前的动画比起来,确实多了一个渐入的效果,当然时间可以由大家进行优化。
花瓣需要依次飞出屏幕
仔细分析一下,其实这是两个动作:
- 依此
- 飞出屏幕
依次
目前来看依此,其实我们是已经实现了,我们每一个花瓣都执行的是同一套动作流程,但是因为之前的delayAnimation
方法,就会导致每个花瓣的状态是有差异的,但是总体的运动曲线不变。说白了就是,delay导致有些花瓣执行方法慢半拍。
飞出屏幕
飞出屏幕的话,我们又可以用到缓动方法啦~ 但是这里要注意的是,因为我们不可能一开始就会飞,是需要等到一定的值之后,花瓣才会飞舞。因此需要一个值进行一个判断,这里我用的是透明度
drawSakuraAnimation() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let item of sakuraList) {
for(let item2 of item) {
this.ctx.globalAlpha = item2.opacity;
item2.delay <= 0 && this.changeScale(item2);
item2.delay <= 0 && this.changeOpacity(item2);
if(item2.opacity > 0) {
this.ctx.save();
this.ctx.rotate(item2.rotate);
item2.ctx.drawImage(item2.imgResource, item2.x, item2.y, item2.width*item2.scale, item2.height*item2.scale);
this.ctx.restore();
}
// 当透明度到达一个值之后会进行一些位移操作
// 当前值 += (目标值-当前值)*系数
if (item2.opacity >= 0.9) {
item2.x += (-600-item2.x)*item2.speed;
item2.y += (-600-item2.y)*item2.speed;
item2.ctx.drawImage(item2.imgResource, item2.x, item2.y, item2.width*item2.scale, item2.height*item2.scale);
}
this.delayAnimation(item2);
}
}
window.requestAnimationFrame(this.drawSakuraAnimation);
},
复制代码
大家不难看出,我这边是在当透明度大于0.9的时候,花瓣就会往一个方向进行移动。接下来我们看看效果。
因为gif把动画的一些关键帧给省略了,所以略显尴尬,但是请相信我,其实还可以…但是其实我觉得可能飞出屏幕太快了,我想等花瓣落地差不多了再飞舞,那容我偷个懒吧,我用一个简单的减法操作一下。
稍微优化一下“依次”
细心的同学可能之前有看到,我声明的属性里边有一个shrinkDelay
还没有用到,那这个其实就是用来作“依次”的延迟的。来来来~show me the code!
drawSakuraAnimation() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let item of sakuraList) {
for(let item2 of item) {
this.ctx.globalAlpha = item2.opacity;
item2.delay <= 0 && this.changeScale(item2);
item2.delay <= 0 && this.changeOpacity(item2);
if(item2.opacity > 0) {
this.ctx.save();
this.ctx.rotate(item2.rotate);
item2.ctx.drawImage(item2.imgResource, item2.x, item2.y, item2.width*item2.scale, item2.height*item2.scale);
this.ctx.restore();
}
if (item2.opacity >= 0.9) {
item2.shrinkDelay -= 0.1;
if (item2.shrinkDelay <= 0) {
item2.x += (-600-item2.x)*item2.speed;
item2.y += (-600-item2.y)*item2.speed;
}
item2.ctx.drawImage(item2.imgResource, item2.x, item2.y, item2.width*item2.scale, item2.height*item2.scale);
}
this.delayAnimation(item2);
}
}
window.requestAnimationFrame(this.drawSakuraAnimation);
},
复制代码
可以看到,我就是简单的用shrinkDelay
的自减,来进行一个延迟,不要和我一样懒…..那看一下效果
怎么样?就目前来看,其实我们的需求基本上是完成了,当然大家可以机进行一些优化~码起来!
总结
通关了这个动画,其实大家会发现,当遇到一个稍微复杂一些动画,我们就可以进行一个动作拆解,当然不包括那些脑中自带显卡的大神们,23333。
我总结了一下我的拆解:
- 对花瓣进行属性的抽象,就是找共同点
- 随机分布樱花以及优化
- 花瓣入场的缩放过程
- 花瓣入场的透明度变换过程
- 花瓣入场的渐序透视
- 花瓣落地之后飞出屏幕,以及优化
已上,就是我本次的分享,不知道有没有收获呢?要有收获的话,不妨点个赞吧,嘿嘿嘿?。同时也欢迎大家提出各种意见与思路。