唯美的樱花飞舞动画

我心飞翔 分类:javascript
唯美的樱花飞舞动画
Ctrl+C、V工程师 @ 豌豆公主

背景

这件事要从大促说起....每年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之后才会开始执行绘制,那我们来看看效果。

唯美的樱花飞舞动画

那可以看到,和之前的动画比起来,确实多了一个渐入的效果,当然时间可以由大家进行优化。

花瓣需要依次飞出屏幕

仔细分析一下,其实这是两个动作:

  1. 依此
  2. 飞出屏幕

依次

目前来看依此,其实我们是已经实现了,我们每一个花瓣都执行的是同一套动作流程,但是因为之前的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。

我总结了一下我的拆解:

  • 对花瓣进行属性的抽象,就是找共同点
  • 随机分布樱花以及优化
  • 花瓣入场的缩放过程
  • 花瓣入场的透明度变换过程
  • 花瓣入场的渐序透视
  • 花瓣落地之后飞出屏幕,以及优化

已上,就是我本次的分享,不知道有没有收获呢?要有收获的话,不妨点个赞吧,嘿嘿嘿🌹。同时也欢迎大家提出各种意见与思路。

回复

我来回复
  • 暂无回复内容