從零開始學前端動畫 —— 簡單的特效登入

luffyZhou發表於2018-07-07

最近忽然對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,就不夠意思了哦,我會在心裡詛咒你的^_^

相關文章