canvas核心技術-如何實現複雜的動畫

這篇是學習和回顧canvas系列筆記的第五篇,完整筆記詳見:canvas核心技術javascript

在上一篇canvas核心技術-如何實現簡單的動畫筆記中,咱們詳細學習瞭如何進行canvas座標系的平移,縮放,旋轉等操做來實現一些比較簡單和單一的動畫。可是在實際動畫中,影響一個動畫的因素是不少的,好比一個小球自由落體運動,咱們不只要考慮小球的初始速度和初始方向,還要考慮重力加速度,空氣阻力等外界因素。這一篇筆記,咱們會詳細學習複雜動畫的相關知識。css

核心邏輯

咱們理解的動畫,應該是在一段時間內,物體的某些屬性,好比顏色,大小,位置,透明度等,發生改變。判斷動畫流程度的單位是動畫刷新的速率,在瀏覽器中通常是瀏覽器的幀速率。幀速率越大,動畫就越流暢。在現代瀏覽器中,咱們通常是使用requestAnimationFrame來執行動畫。java

let raf = null;
let lastFrame = 0;

//動畫
function animate(frame) {
  // todo:這裏能夠執行一些動畫更新
  console.log(frame)
  raf = requestAnimationFrame(animate);
  lastFrame = frame;
}

function start() {
  // 一些初始化的操做
  init();

  // 執行動畫
  animate(performance.now());
}

function stop() {
  cancelAnimationFrame(raf);
}
複製代碼

通常大體的結構就是這樣的,經過requestAnimationFrame不斷地在瀏覽器下一幀中執行animate,且animate函數會接受一個當前幀開始執行的時間戳的參數。若是想中斷當前進行的動畫,只須要調用cancelAnimationFrame,那麼在下一幀中就不會執行animate函數了。上一幀執行的時間,能夠用frame - lastFrame,而後再根據這個差值就能夠計算出當前動畫的幀速率了,以下,css3

let fps = 0;
let lastCalculateFpsTime = 0;
function calculateFps(frame) {
  if (lastFrame && (fps === 0 || frame - lastCalculateFpsTime > 1000)) {
    fps = 1000 / (frame - lastFrame);
    lastCalculateFpsTime = frame;
  }
}
//動畫
function animate(frame) {
  // todo:這裏能夠執行一些動畫更新
  calculateFps(frame);
  raf = requestAnimationFrame(animate);
  lastFrame = frame;
}
複製代碼

在計算fps時,咱們是用1s除以上一幀執行的時間,因爲frame的單位是毫秒,因此是用1000除的。上面咱們還作了一個優化,就是每1s纔會去計算一次fps,而不是每幀都去計算,由於每幀都去計算,意義不大,且增長了額外的計算。git

時間因素

在繪製動畫時,咱們必須按照基於時間的方式來設計,而不是當前瀏覽器的幀速率。由於,不一樣瀏覽器會有不一樣的幀速率,同一瀏覽器在不一樣的GPU負載下幀速率也可能會不一樣,因此咱們的動畫必須是基於時間的,這樣才能保證一樣的速度在相同的時間內,動畫變化的是一致的。好比,咱們在考慮小球垂直下落時,必須設置小球的下落的速度v,而後再根據公式s = v * t,得出小球在當前時間段內的移動距離,計算出當前幀內的座標。github

/* 初始化 */
  private init() {
    this.fps = 0; 
    this.lastFrameTime = 0;
    this.speed = 5; // 設置小球初速速度爲:5m/s
    this.distance = 50; //設置小球距離地面高度爲:50m
    let pixel = this.height - this.padding * 2;
    if (this.distance <= 0) {
      this.pixelPerMiter = 0;
    } else {
      this.pixelPerMiter = (this.height - this.padding * 2) / this.distance;
    }
  }
複製代碼

在上面代碼中,咱們在初始化的時候,設定了小球的初始速度爲5m/s,小球距離地面的高度爲50m,以及計算出了物理高度與像素高度的比值pixelPerMiter,這個值在後面計算小球的座標時是有用到的。canvas

/* 更新 */
  private update() {
    if (this.fps) {
      this.ball.update(1000 / this.fps); //更新小球
    }
  }
複製代碼

而後,在每幀更新小球位置時,咱們將上一幀進過的時間值傳遞給ball.update瀏覽器

/* 移動 */
  static move(ball: Ball, elapsed: number) {
    //小球是靜止狀態,不更新
    if (ball.isStill) {
      return;
    }
    let { currentSpeed } = ball;
    let t = elapsed / 1000; //elapsed是毫秒, 而速度單位是m/s,因此要除1000
    let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      ////若是小球是否已經超過實際高度,則落到地面了
       ball.isStill = true;
       ball.currentSpeed = 0;
       ball.offset = ball.verticalHeight;
    } else {
      ball.offset += distance;
    }
  }
複製代碼

再根據公式計算出上一幀時間裏,小球下落的距離distance,累加每一幀下落的距離,則能夠獲得當前小球下落的總距離,若是小球下落的總距離大於了小球距離地面的實際高度,則表示小球落到地面了,就中止小球下落。函數

/* 繪製小球 */
  public render(ctx: CanvasRenderingContext2D) {
    let { x, y, radius, offset, pixelPerMiter } = this;
    ctx.save();
    ctx.translate(x, y + offset * pixelPerMiter); //offset * pixelPerMiter獲得下落的像素
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
    ctx.fill();
    ctx.stroke();
    ctx.restore();
  }
複製代碼

最後在繪製小球時,先要根據下落的實際高度offset和前面計算獲得的實際高度與像素高度的比值,來獲得小球在屏幕上下落的像素值(offset * pixelPerMiter)。post

上面就是咱們在寫小球自由落體時的大體思路,重點是設置小球的初始下落速度,以及在每一幀裏計算出小球下落的距離,最後根據實際高度與像素高度比,計算出小球在屏幕上下落的像素高度。這個過程當中,咱們尚未考慮重力加速度和空氣阻力等物理因素,下面,咱們就來考慮物理因素對動畫的影響。

物理因素

爲了使動畫或者遊戲表現的更加真實,一般須要考慮真實世界中物理因素的影響,好比咱們繼續考慮小球自由落體運動,真實世界中,小球自由落體運動會收到重力加速度,空氣阻力,空氣流向,反彈等的影響,從而改變小球下落的速度。

/* 建立小球 */
  private createBall() {
    let { width, height, padding, speed, radius, pixelPerMiter, distance } = this;
    this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true });
    this.ball.setSpeed(speed);
    this.ball.addBehavior(Ball.move);
  }
複製代碼

在建立小球時,咱們給了一個參數userGravity來表示是否使用重力加速度,這裏咱們設置爲true,同時咱們也傳遞了小球的初始座標和半徑,以及初始速度等。

const GRAVITY = 9.8; //重力加速度9.8m/s 
  /* 移動 */
  static move(ball: Ball, elapsed: number) {
    // ...
    //若是應用了重力加速度,則更新速度
    if (ball.useGravity) {
      ball.currentSpeed += GRAVITY * t;
    }
   // ...
  }
複製代碼

而後在更新小球時,咱們增長了對小球當前速度的計算,根據公式v = g * t 計算出上一幀的速度,這樣,隨着時間,小球的速度其實是不斷增長的,小球下落的會愈來愈快。

前面咱們在處理小球落到地面時,只是單純的讓小球停在地面上。可是在實際生活中,咱們下落的小球,碰到地面後,都會反彈必定高度,而後又下落,直到小球靜止在地面上。爲了更加真實模擬小球下落,咱們來考慮反彈物理因素。

//建立小球
this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true, useRebound: true });
複製代碼

在建立小球時,咱們傳遞了useRebound:true,表示當前小球應用了反彈效果,在更新小球時,須要判斷小球在落地時,將當前速度方向反向,且大小減爲原來的0.6倍,這個0.6係數只是一個經驗值,在具體遊戲中,能夠調整,達到想要的效果。係數越大,反彈越高。

/* 移動 */
  static move(ball: Ball, elapsed: number) {
    //小球是靜止狀態,不更新
    if (ball.isStill) {
      return;
    }
    let { currentSpeed } = ball;
    let t = elapsed / 1000; //elapsed是毫秒, 而速度單位是m/s,因此要除1000
    //更新速度
    if (ball.useGravity) {
      ball.currentSpeed += GRAVITY * t;
    }
    let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      //落到地面了
      //使用反彈效果
      if (ball.useRebound) {
        ball.offset = ball.verticalHeight;
        ball.currentSpeed = -ball.currentSpeed * 0.6; //速度方向取反,大小乘0.6
        if ((distance * ball.pixelPerMiter) / t < 1) {
          //當前移動距離小於1px,應該靜止了,
          ball.isStill = true;
          ball.currentSpeed = 0;
        }
      } else {
        ball.isStill = true;
        ball.currentSpeed = 0;
        ball.offset = ball.verticalHeight;
      }
    } else {
      ball.offset += distance;
    }
  }
}
複製代碼

在應用反彈效果時,咱們判斷當前速度在1s內在下落位移小於1px時,就將小球中止,這樣,防止小球在反彈距離很小很小時,進行沒必要要的計算。

至於其餘物理因素,好比風向,阻力等,咱們就不具體討論了,具體思路跟上面同樣,先進行物理建模,而後在更新過程當中根據物理公式計算受影響的屬性,最後再根據屬性值來繪製。

這裏是個人小球自由落體完整在線示例

時間軸扭曲

動畫是持續一段時間的,咱們能夠事先給定具體的持續時間值,讓動畫在這段時間內持續執行,就像css3中animation-duration,而後經過扭曲時間軸,可讓動畫執行非線形運動,好比咱們常見緩入效果緩出效果緩入緩出效果等。

時間軸扭曲,是經過一系列對應的緩動函數,根據當前的時間完成比率compeletePercent,計算獲得一個扭曲後的值effectPercent,最後根據這2個值獲得扭曲後的時間值elapsed

eplased = actualElapsed * effectPercent/compeletePercent

線性函數,

static linear() {
    return function(percent: number) {
      return percent;
    };
  }
複製代碼

緩入函數,

static easeIn(strength: number = 1) {
    return function(percent: number) {
      return Math.pow(percent, strength * 2);
    };
  }
複製代碼

緩出函數,

static easeOut(strength: number = 1) {
    return function(percent: number) {
      return 1 - Math.pow(1 - percent, strength * 2);
    };
  }
複製代碼

緩入緩出函數,

static easeInOut() {
    return function(percent: number) {
      return percent - Math.sin(percent * Math.PI * 2) / (2 * Math.PI);
    };
  }
複製代碼

這裏是個人時間軸扭曲完整在線示例

更復雜的緩動函數還有彈簧效果,貝塞爾曲線等,詳細能夠參見EasingFunctions

小結

本篇筆記主要討論了在canvas中如何實現複雜的動畫效果,從一個小球的自由落地運動爲示例,咱們在計算小球的下落距離時,是以時間維度來計算,而不是當前瀏覽器的幀速率,由於幀速率不是一個恆定可靠的值,它會使小球的運動變得不明確。當咱們以時間爲計算值時,小球在一樣時間內下落的距離值,咱們是能夠計算出來的,是一個準確不受幀速率影響的值。爲了使小球下落的更加真實,咱們又考慮了影響小球下落的物理因素,好比重力加速度,反彈效果等。在製做其餘一些非線性運動的動畫時,咱們可使用常見的緩動函數,好比,緩入,緩出等,它們的本質都是經過扭曲時間軸,使得當前的運動受時間因素影響。

在製做canvas遊戲時,基本都會運用到動畫,有物體運動,那麼必定會發生碰撞,好比上面咱們的小球下落,就會發生小球與地面的碰撞,咱們進行了簡單的碰撞檢測。下一篇筆記,咱們詳細討論,如何在canvas中進行碰撞檢測。

相關文章
相關標籤/搜索