从零开始学前端动画 —— 简单的特效登录

9,724 阅读12分钟

最近忽然对canvas动画感兴趣,然后就心血来潮的看了一些文章,事先声明,部分原创,我只是代码的搬运工。我先上一下截图,然后再说下我的想法。

【我的想法】:其实我想做网上特别多的那种,当输入密码的时候用手捂住眼睛那种,但是原谅我戏精本色,我想做成那种当输入密码的时候,用木槌敲击小黄人或者闪电劈晕小黄人的那种特效,最好是配上音效。。。但是实现效果都不太好,马马虎虎能用不过不完美就对了,由于达到了学习目的以及时间原因,就不继续往下做了,以后有时间可能会做,或者各位CSS大牛帮我完成,完成优秀的特效后记得把链接甩我一脸~万分感谢。代码在下方---

雪花飘落场景

还是那句话,我只是代码的搬运工,首先是雪花飘落的场景。参考文章点击这里 —— canvas实现雪花飘落,文章上来就贴了代码,直接就能用,但是没有任何注释,既然是初探canvas和动画,讲道理一点注释都没有,真心看不太懂,所以我在这里把代码来解析一下。效果如下:

重点一 一张合适的背景图

不吹不黑,个人觉得我的审美还是挺不错的,这种下雪的特效需要配一个高对比度的背景图,否则你背景色就是浅色冬天,然后飘白色雪花,其实感觉看不太清楚,所以找了很久,发现这张动漫圣诞背景图,时间点在晚上,配合灯光飘着雪花,还是很清晰的~

重点二 canvas绘制雪花

  • 定义一些场景参数 首先,需要定义一些场景相关的参数,比如,雪花飘落的速度,位置和方向等。代码如下:
// 存储所有的雪花
const snows = [];
// 下落的加速度
const G = 0.015;
// 60是人眼所能见到流畅动画的最小阈值
const fps = 60; 
// 速度上限,避免速度过快
const SPEED_LIMIT_X = 1;
const SPEED_LIMIT_Y = 1;
  • 定义雪花构造函数
 /**
   * 雪花对象
   * @param {x} x 
   * @param {y} y 
   * @param {半径或长宽} radius 
   */
  function Snow(x, y, radius) {
    this.x = x;
    this.y = y;
    // 如果是圆形就是半径,否则就是长宽相同的正方形
    this.radius = radius;
    // x方向的移动速度,可以向左也可以向右,范围在[-1, 1]
    this.speed_x = 0;
    // y方向的移动速度,只能向下,最快为1
    this.speed_y = 0;
    // 雪花自身旋转的角度
    this.deg = 0;
    // x方向,下落速度参数,飘落效果 > 0向左飘; < 0 向右飘 
    this.ax = Math.random() < 0.5 ? 0.005 : -0.005;
  }
  • 生成雪花
// 绘制新雪花,x位置为随机数,y为顶部0,半径为随机数随机生成大小不一的雪花
new Snow(Math.random() * W, 0, Math.random() * 15 + 5);
  • 为雪花添加绘制以及更新位置的方法
// 绘制雪花
Snow.prototype.draw = function () {
// 获取半径宽高
const radius = this.radius;
// 保存画布的当前状态,因为下面用到了变换坐标和旋转画布
ctx.save();
/**
 * 下面这两句变化也挺重要的,因为旋转是按照画布原点进行的
 * 因此,如果想让雪花旋转明显,就需要将画布坐标移动到雪花的坐标点
 * 如果不加上坐标转换,那么所有雪花都在左上角也就是坐标原点旋转,x, y也不会变,没有飘落效果
 */
// 将画布的坐标原点移动到(x, y)的位置,canvas默认是(0, 0)
ctx.translate(this.x, this.y);
// 将画布顺时针旋转的角度
ctx.rotate(this.deg * Math.PI / 180);
// 绘制雪花图像,因为画布坐标移动到了(x, y),所以从0,0开始就是(-radius, radius)
ctx.drawImage(snowImage, -radius, -radius, radius * 2 , radius * 2);
// 恢复canvas旋转、translate等操作的状态,一般与save配合使用就是恢复到上一个save的状态
// 如果不恢复上一个状态的话,话不旋转角度坐标都没变化,也就不会出现动画效果,必须恢复
ctx.restore();
}
// 更新雪花位置
Snow.prototype.update = function () {
// 雪花自身旋转的角度增值
const deltaDeg = Math.random() * 0.6 + 0.2;
// 不断变化x方向的移动速度
this.speed_x += this.ax;
// x向左或者向右速度过大的时候改变方向
if (this.speed_x >= SPEED_LIMIT_X || this.speed_x <= -SPEED_LIMIT_X) {
  this.ax *= -1;
}
// 雪花下落速度,最高是1
if (this.speed_y < SPEED_LIMIT_Y) {
  // 雪花下落速度不断增加
  this.speed_y += G;
}
// 角度不断变化
this.deg += deltaDeg;
// x坐标不断变化
this.x += this.speed_x;
// y坐标不断变化
this.y += this.speed_y;
}
  • 实现飘落下降的动画效果 实现不断飘落的动画效果,这里运用了浏览器专门为动画提供的API requestAnimationFrame,通过将循环函数作为参数传入,该API会不断进行重绘形成动画效果。
/**
* 主循环函数, 生成雪花以及绘制更新雪花位置
*/
function loop() {
// 擦除当前画布内容,否则原有的雪花不会消失,新绘制的雪花不断覆盖看起来会像一条雪花白色实线在下降
ctx.clearRect(0, 0, W, H);
// 两个雪花之间的时间差,不能生成的太快,要不然就成了鹅毛大雪了^_^
const now = Date.now();
// 距离上一次绘制的时间差
deltaTime = now - lastTime;
// 重置结束时间
lastTime = now;
// 时间控制器,当timer > snowLevelTime的时候,才增加雪花,否则不增加雪花
timer += deltaTime;
/**
 * 不加控制的话雪花会特别多, 150~300之间都合适
 */
if (timer > snowLevelTime) {
  snows.push(
    // 绘制新雪花,x位置为随机数,y为顶部0,半径为随机数随机生成大小不一的雪花
    new Snow(Math.random() * W, 0, Math.random() * 15 + 5)
  );
  timer %= snowLevelTime;
}
const length = snows.length;
snows.map(function (s, i) {
  s.update();
  s.draw();
  if (s.y >= H) {
    snows.splice(i, 1);
  }
});
// 避免失真,浏览器页面每次重绘之前调用
requestAnimationFrame(loop);
}

小黄人效果

我只是代码的搬运工,这里有另一个大神写了一篇非常详细的文章,CSS3手绘小黄人,我只是将代码进行了适配的扩展,让代码在大部分屏幕分辨率下都可以使用。这里就不写过程了,文章里写的真的挺清楚的~

眩晕效果

眩晕效果其实我也想找现成的代码,我喜欢站在巨人的肩膀上,但是奈何找不到,连图片都找不到,最后没办法了,自己写吧,就简单的实现了下眩晕效果,至于眩晕的旋转动画,下面会简单介绍:

  /* CSS */
 .circle-container {
      position: relative;
      width: 45px;
      height: 45px;
      transform: rotate(180deg);
    }
    .one-circle {
      position: absolute;
      width: 45px;
      height: 45px;
      background-color: #fff;
      border-left: 3px solid #333;
      border-top: 3px solid #333; 
      border-radius: 50%; 
    }
    .two-circle {
      position: absolute;
      top: 9px;
      width: 34px;
      height: 34px;
      background-color: #fff;
      border-right: 3px solid #333;
      border-bottom: 3px solid #333; 
      border-radius: 50%; 
    }
    .three-circle {
      position: absolute;
      bottom: 10px;
      left: 8px;
      width: 23px;
      height: 23px;
      background-color: #fff;
      border-top: 3px solid #333;
      border-left: 3px solid #333; 
      border-radius: 50%; 
    }
    .four-circle {
      position: absolute;
      bottom: 0px;
      right: 6px;
      width: 13px;
      height: 13px;
      background-color: #fff;
      border-bottom: 3px solid #333;
      border-right: 3px solid #333; 
      border-radius: 50%; 
    }
  /* Html */
  <div class='circle-container'>
    <div class="one-circle">
      <div class="two-circle">
        <div class="three-circle">
          <div class="four-circle"></div>
        </div>
      </div>
    </div>
  </div>

无论是代码还是实现都挺简单的,但是奈何是原创,还是写出来吧,O(∩_∩)O哈哈~

隔行如隔山啊,才发现即使是前端,也有如此多的分支,确实像张鑫旭老师自我介绍那样,前段偏前工程师,我倒是觉得偏前偏后偏业务每个方向都值得深入研究啊,不过我这个阶段还是大杂烩一下吧。既然是对动画感兴趣,用完canvas了,也就想了解了解其他几种实现方法了,顺便简单学习一下,下面是学体会。

聊聊几种浏览器动画实现方法:

setTimeout和setInterval

最原始的方法就是使用window.setTimout()或者window.setInterval()通过不断更新元素的状态位置等来实现动画,前提是画面的更新频率要达到每秒60次才能让肉眼看到流畅的动画效果。

这个就不贴代码了,说实话,应该没有人在用这种方式实现动画了,如果有,为你的网页性能所担忧~

CSS3的transition和transform属性

transtion可以理解为过渡,两点之间,连续性的变化。transform理解为变换,平移、旋转等是常用的,transform + transition结合起来,也就是 过渡 + 变换就会形成简单的动画。 transtion实现动画的规则是需要定义对应的属性,比如我想让宽度发生变化,那么就定义属性为width,变化持续时间,也就是多久完成变换,最后在指定事件中改变定义的属性值即可。

// 实现鼠标放到篮球上篮球放大
.ball {
width: 128px;
height: 128px;
transition-property: width,height;
transition-duration: 2s;
-moz-transition-property: width,height; /* Firefox 4 */
-moz-transition-duration: 2s; /* Firefox 4 */
-webkit-transition-property: width,height; /* Safari and Chrome */
-webkit-transition-duration: 2s; /* Safari and Chrome */
-o-transition-property: width,height; /* Opera */
-o-transition-duration: 2s; /* Opera */
}
.ball:hover {
width: 256px;
height: 256px;
}

CSS3的animation和keyframes

与transition和transform不一样,animation就可以直接理解为动画,个人觉得功能也要更强大一些。 如上面说的那样,animation就是CSS为动画而出的,基本可以完成大部分简易的动画效果,他需要定义一个动画特效名,然后通过keyframe定义特效效果。下面我简单实现了一个篮球自由落体的效果,很粗糙,只是一个简单的demo:

.ball-animation {
  position: absolute;
  width: 128px;
  height: 128px;
  transform: rotate(0);
  animation: ball-drop 6s linear;
  animation-fill-mode: forwards;
  -webkit-animation: ball-drop 6s linear;
  -webkit-transform: rotate(0);
  -webkit-animation-fill-mode: forwards;
}
@keyframes ball-drop {
  0% {
    transform: rotate(0);
    top: 0
  }
  10% {
    transform: rotate(60);
    top: calc(var(--height) * 1px );
  }
  20% {
    transform: rotate(120);
    top: 40px;
  }
  30% {
    transform: rotate(240);
    top: calc(var(--height) * 1px );
  }
  40% {
    transform: rotate(300);
    top: 80px;
  }
  50% {
    transform: rotate(360);
    top: calc(var(--height) * 1px );
  }
  60% {
    transform: rotate(60);
    top: 160px;
  }
  70% {
    transform: rotate(120);
    top: calc(var(--height) * 1px );
  }
  80% {
    transform: rotate(180);
    top: 380px;
  }
  90% {
    transform: rotate(240);
    top: calc((var(--height) - 20) * 1px );
  }
  100% {
    transform: rotate(260);
    top: calc(var(--height) * 1px );
  }
}

虽然实现的很粗糙,但是我还是写了影子之类的东西,所以也算是走心,哈哈。详细的介绍我就不多说了,我觉得去看张鑫旭老师的文章就可以了,浅显易懂,传送门

requestAnimationFrame

因为使用canvas实现动画最重要的就是这个API了,所以放到最后来说。MDN里关于它有这么个高亮注意:若您想要在下次重绘时产生另一个动画画面,您的回调例程必须调用 requestAnimationFrame()。所以也就是说,如果想要不断地重绘产生动画效果必须调用这个API。

window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。 一般来说,人眼如果想看到流畅的动画,至少每秒更新60次,当然你也可以控制想要刷新的次数,同时,requestAnimationFrame()会返回一个整形ID,通过这个ID你可以调用cancleAnimationFrame(ID)来停止动画通过下面的代码:

let timeoutId;
let animateId;
const fps = 20; // 比如我每秒就想刷新20次
window.requestAnimationFrame = (function () {
    return window.requestAnimationFrame ||
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame ||
      window.oRequestAnimationFrame ||
      window.msRequestAnimationFrame ||
      function (callback) {
        timeoutId = setTimeout(callback, 1000 / fps);
      }
  })();
}
function loop() {
  // 执行动画代码
  animateId = requestAnimaitonFrame(loop);
}

// 停止动画,先停止timeout再停止requestAnimationFrame
clearTimeout(timeoutId);
cancleAnimationFrame(animateId);

同样是js实现动画效果,由于这个API是专门用来在浏览器实现动画的,所以浏览器对其也进行了优化,相比setTimeout和setInterval的形式来说,requestAnimationFrame()性能更好,在大多数浏览器里,当运行在后台标签页或者隐藏的 里时,requestAnimationFrame() 会暂停调用以提升性能和电池寿命。

animation和requestAnimationFrame的对比

其实不用比也知道,同一个动画效果,用纯CSS3实现和用requestAnimationFrame这种js实现手段,肯定CSS3的性能要高一些的。下面是我做的同一个动效,二者渲染时间对比: animation:

requestAnimationFrame

好吧,我其实是个假测试,我觉得首先我没有控制速度一致性,其次,我在annimation实现的时候使用了calc函数计算位置,可能对性能也会有影响,不过在重绘时间上,animation的方法还是要优秀一点的。

强烈声明:我只是为了结果而作比较,没有谁高谁低的意思,我觉得根据场景来使用吧~

可以改进的地方

最后实现的代码有很多可以改进的地方,在这里记录下来,万一哪位大神心情好给我改了咋办,O(∩_∩)O哈哈~。

  • 木槌可以使用CSS3绘制,然后敲击小黄人脑袋出现撞击声和眩晕声
  • 雪花飘落过程遇到遮挡物,小黄人和输入框的时候,应该停留下来,这样更真实
  • 雪花落到地面应该形成积雪,但是积雪太多影响性能就没加~加也很容易,判断雪花位置指定雪花不重绘就可以了。
  • 原谅我没有做移动端适配,下面的demo尽量别用手机来打开了。。。pad可能还可以,O(∩_∩)O哈哈~

总结

这两天简单看了看动画,感觉一入前端深似海啊,目前计划还是所有感兴趣的都涉猎一下,然后慢慢选择一个方向吧~代码地址:luffyZhou的动画Demo欢迎大家多提意见,多给STAR

只clone不star,就不够意思了哦,我会在心里诅咒你的^_^