跳轉到內容

Canvas 2D Web 應用程式/動畫

來自華夏公益教科書

本章介紹如何使用關鍵幀動畫與 cui2d。具體來說,它展示瞭如何根據預定義的關鍵幀陣列來更改數值變數。如果更改的變數在 process 函式中用作座標、顏色等,則生成的圖形將看起來像是動畫。當然,還有其他方法可以使用畫布元素來實現動畫,但這裡介紹的方法具有一些優點,而且非常靈活。

本章的示例(可以線上獲取 線上;也可以作為 可下載版本)擴充套件了關於 響應式按鈕 的章節的示例。因此,以下部分只討論程式碼中與動畫相關的部分;有關其他部分的討論,請參閱關於 響應式按鈕 的章節和前面的章節。

在示例中,使用三個按鈕來啟動三種不同的動畫。如果再次點選,第一個按鈕只是重新啟動動畫(即使它已經在播放)。第二個按鈕如果動畫已經在播放,就不會重新啟動動畫。最後,第三個按鈕在動畫播放時處於非活動狀態,並且這種狀態也透過半透明地繪製它來視覺地傳達,從而實現了“灰顯”的效果。

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
    <meta name="viewport"
      content="width=device-width, initial-scale=1.0, user-scalable=no">

    <script src="cui2d.js"></script>

    <script>
      function init() {
        // get images
        imageNormalButton.src = "normal.png";
        imageNormalButton.onload = cuiRepaint;
        imageFocusedButton.src = "selected.png";
        imageFocusedButton.onload = cuiRepaint;
        imagePressedButton.src = "depressed.png";
        imagePressedButton.onload = cuiRepaint;
        imageAlien.src = "alien_smiley.png";
        imageAlien.onload = cuiRepaint;

        // set defaults for all pages
        cuiBackgroundFillStyle = "#000000";
        cuiDefaultFont = "bold 20px Helvetica, sans-serif";
        cuiDefaultFillStyle = "#FFFFFF";

        // initialize cui2d and start with myPage
        cuiInit(myPage);
      }

      // create images for the buttons
      var imageNormalButton = new Image();
      var imageFocusedButton = new Image();
      var imagePressedButton = new Image();
      var imageAlien = new Image();

      // create buttons
      var button0 = new cuiButton();
      var button1 = new cuiButton();
      var button2 = new cuiButton();

      // create new arrays of keyframes (arrays of cuiKeyframe Objects)
      var widthAndHeightKeys = [ // keyframes for width and height
        {time : 0.00,          out : -1, values : [125, 158]}, // start fast
        {time : 0.25, in :  1, out :  1, values : [100, 190]}, // turn smoothly
        {time : 0.80, in :  1, out :  1, values : [150, 126]}, // turn smoothly
        {time : 1.50, in :  0,           values : [125, 158]}  // end slowly
      ];
      var yKeys = [ // keyframes for y coordinate
        {time : 0.00,          out : -3, values : [200]}, // start extra fast
        {time : 0.50, in :  1, out :  1, values : [ 50]}, // turn smoothly
        {time : 1.00, in : -3,           values : [200]}  // end extra fast
      ];
      var xAndAngleKeys = [ // keyframes for x coordinate and angle (in degrees)
        new cuiKeyframe(0.00, 0, 0, [190,   0]), // start slowly
        new cuiKeyframe(1.00, 1, 1, [90,  -20]), // turn smoothly
        new cuiKeyframe(2.00, 1, 1, [290, +20]), // turn smoothly
        new cuiKeyframe(3.00, 0, 0, [190,   0])  // end slowly
      ];

      // create new animations
      var widthAndHeightAnimation = new cuiAnimation();
      var yAnimation = new cuiAnimation();
      var xAndAngleAnimation = new cuiAnimation();

      // create a new page of size 400x300 and attach myPageProcess
      var myPage = new cuiPage(400, 300, myPageProcess);

      // a function to repaint the canvas and return false (if null == event)
      // or to process user events (if null != event) and return true
      // if the event has been processed
      function myPageProcess(event) {

        // draw animated image
        if (null == event) {
          var xAndAngle = [190, 0];
          if (xAndAngleAnimation.isPlaying()) {
            xAndAngle = xAndAngleAnimation.animateValues();
          }
          var x = xAndAngle[0];
          var angle = xAndAngle[1];

          var y = 200;
          if (yAnimation.isPlaying()) {
            y = yAnimation.animateValues()[0];
          }

          var widthAndHeight = [125, 158];
          if (widthAndHeightAnimation.isPlaying()) {
            widthAndHeight = widthAndHeightAnimation.animateValues();
          }
          var width = widthAndHeight[0];
          var height = widthAndHeight[1];

          cuiContext.save(); // save current coordinate transformation

          // read the following three lines backwards, starting with the last
          cuiContext.translate(x, y); // translate pivot point back to original position
          cuiContext.rotate(angle * Math.PI / 180.0); // rotate around pivot point
          cuiContext.translate(-x, -y); // translate pivot point to origin

          cuiContext.drawImage(imageAlien, x - width/2, y - height/2, width, height);
          cuiContext.restore(); // restore previous coordinate transformation
        }

        // draw and react to buttons

        if (button0.process(event, 40, 50, 90, 50, "wobble",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button0.isClicked()) {
            widthAndHeightAnimation.play(widthAndHeightKeys, 0.6, false);
              // restart animation even if playing
          }
          return true;
        }

        if (button1.process(event, 150, 50, 80, 50, "jump",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button1.isClicked() && !yAnimation.isPlaying()) {
            // only restart when not playing
            yAnimation.play(yKeys, 1.0, false);
          }
          return true;
        }

        if (!xAndAngleAnimation.isPlaying()) { // usual button
          if (button2.process(event, 250, 50, 80, 50, "rock",
            imageNormalButton, imageFocusedButton, imagePressedButton)) {
            if (button2.isClicked()) {
              xAndAngleAnimation.play(xAndAngleKeys, 0.5, false);
            }
            return true;
          }
        }
        else { // inactive button while animating
          if (null == event) {
            cuiContext.save(); // save current global alpha (and all context settings)
            cuiContext.globalAlpha = 0.2; // use semitransparent rendering
            button2.process(event, 250, 50, 80, 50, "rock",
              imageNormalButton, imageNormalButton, imageNormalButton);
            cuiContext.restore(); // restore previous global alpha
          }
        }

        if (null == event) {
          // draw background
          cuiContext.fillStyle = "#804000";
          cuiContext.fillRect(0, 0, this.width, this.height);
        }

        return false; // event should be further processed
      }
    </script>
  </head>

  <body bgcolor="#000000" onload="init()"
    style="-webkit-user-drag:none; -webkit-user-select:none; ">
    <span style="color:white;">A canvas element cannot be displayed.</span>
  </body>
</html>

定義動畫

[編輯 | 編輯原始碼]

示例定義了三個關鍵幀動畫來為單個影像製作動畫:widthAndHeightAnimation 動畫影像的寬度和高度以建立搖擺效果;yAnimation 用於透過更改影像的 y 座標來建立一個動畫跳躍;xAndAngleAnimation 用於更改影像的 x 座標和旋轉角度以建立一個搖擺動作。

      // create new animations
      var widthAndHeightAnimation = new cuiAnimation();
      var yAnimation = new cuiAnimation();
      var xAndAngleAnimation = new cuiAnimation();

此外,還透過以下幾行定義了三個關鍵幀陣列(每個動畫一個):

      // create new arrays of keyframes (arrays of cuiKeyframe Objects)
      var widthAndHeightKeys = [ // keyframes for width and height
        {time : 0.00,          out : -1, values : [125, 158]}, // start fast
        {time : 0.25, in :  1, out :  1, values : [100, 190]}, // turn smoothly
        {time : 0.80, in :  1, out :  1, values : [150, 126]}, // turn smoothly
        {time : 1.50, in :  0,           values : [125, 158]}  // end slowly
      ];
      var yKeys = [ // keyframes for y coordinate
        {time : 0.00,          out : -3, values : [200]}, // start extra fast
        {time : 0.50, in :  1, out :  1, values : [ 50]}, // turn smoothly
        {time : 1.00, in : -3,           values : [200]}  // end extra fast
      ];
      var xAndAngleKeys = [ // keyframes for x coordinate and angle (in degrees)
        new cuiKeyframe(0.00, 0, 0, [190,   0]), // start slowly
        new cuiKeyframe(1.00, 1, 1, [90,  -20]), // turn smoothly
        new cuiKeyframe(2.00, 1, 1, [290, +20]), // turn smoothly
        new cuiKeyframe(3.00, 0, 0, [190,   0])  // end slowly
      ];

每個陣列都使用語法 [ 第 0 個關鍵幀 , 第 1 個關鍵幀 , ... ] 定義。前兩個陣列將各個關鍵幀定義為具有 timeinoutvalues 屬性的物件。但是,第 0 個關鍵幀的 in 屬性和最後一個關鍵幀的 out 屬性不是必需的。time 是動畫開始後關鍵幀的秒數。注意,關鍵幀必須按其時間的升序排列。inout 定義了在每個關鍵幀之前和之後值變化的速度(即切線或斜率)。最重要的選擇是

  • 0 表示零速度,即緩入/緩出(也稱為 ease in/out)
  • 1 表示由平滑的 Catmull-Rom 樣條曲線定義的速度(但只有當一個關鍵幀的 inout 相同時才平滑)
  • -1 表示對相鄰關鍵幀的恆定速度(也稱為線性插值,但只有當相鄰關鍵幀的相應切線也由 -1 指定時才為線性插值)

對於更快或更慢的速度,這些數字可以進行縮放;即,0.5 的值表示 Catmull-Rom 樣條曲線使用速度的一半。-3 的值表示線性插值使用速度的三倍。在關鍵幀之間,將應用三次埃爾米特樣條曲線。(Catmull-Rom 樣條曲線和線性插值都是三次埃爾米特樣條曲線的特例。)

values 屬性是關鍵幀處實際資料值的陣列。它們的實際含義(是座標、顏色分量還是大小等)取決於後來實際使用動畫值的具體方式。在本示例中,yKeys 僅為影像的 y 座標指定關鍵幀;因此,關鍵幀的 values 屬性是隻有一個數字的陣列,例如,[200] 表示具有一個值為 200 畫素的座標的陣列。xAndAngleKeys 同時為 x 座標和旋轉角度製作動畫;因此,values 屬性是包含兩個元素的陣列。例如,[90, -20] 表示包含兩個元素的陣列,其中第一個將解釋為值為 90 畫素的 x 座標,第二個將解釋為值為 -20 度的旋轉角度。

第三個關鍵幀陣列 xAndAngleKeys 使用建構函式 cuiKeyframe(time, in, out, values) 來構造具有 timeinoutvalues 屬性的物件。您可以選擇自己喜歡的初始化關鍵幀的方式。

啟動動畫

[編輯 | 編輯原始碼]

在如上一節中所述定義動畫後,可以使用函式 play(keyframes, stretch, isLooping)(在處理事件時)播放動畫。

此函式使用指定的引數設定 keyframesstretchisLooping 屬性。在本示例中,keyframes 引數分別為 widthAndHeightKeysxAndAngleKeysyKeys,用於三個動畫。第二個引數(確定 stretch 屬性)是一個因子,用於使動畫比關鍵幀定義的動畫更長(因子大於 1)或更短(因子小於 1)。這在不更改所有關鍵幀的時間座標的情況下更改動畫的整體速度時很有用。第三個引數允許以無限迴圈的方式播放動畫。(可以透過函式 stopLooping() 停止迴圈播放)。

在本示例中,第一個按鈕每當點選按鈕時就重新啟動動畫 widthAndHeightAnimation

        if (button0.process(event, 40, 50, 90, 50, "wobble",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button0.isClicked()) {
            widthAndHeightAnimation.play(widthAndHeightKeys, 0.6, false); 
              // restart animation even if playing
          }
          return true;
        }

第二個按鈕在重新啟動動畫之前檢查動畫是否未播放(使用 isPlaying

        if (button1.process(event, 150, 50, 80, 50, "jump",
          imageNormalButton, imageFocusedButton, imagePressedButton)) {
          if (button1.isClicked() && !yAnimation.isPlaying()) { 
            // only restart when not playing 
            yAnimation.play(yKeys, 1.0, false);
          }
          return true;
        }

因此,在動畫播放時無法重新啟動動畫。從某種意義上說,這使得按鈕在動畫播放時處於非活動狀態。為了傳達非活動按鈕,許多 GUI 會“灰顯”這些按鈕。第三個按鈕透過使用半透明渲染並僅繪製圖像 imageNormalButton 來實現類似的效果。

        if (!xAndAngleAnimation.isPlaying()) { // usual button 
          if (button2.process(event, 250, 50, 80, 50, "rock",
            imageNormalButton, imageFocusedButton, imagePressedButton)) {
            if (button2.isClicked()) {
              xAndAngleAnimation.play(xAndAngleKeys, 0.5, false);
            }
            return true;
          }
        }
        else { // inactive button while animating
          if (null == event) {
            cuiContext.save(); // save current global alpha (and all context settings)
            cuiContext.globalAlpha = 0.2; // use semitransparent rendering
            button2.process(event, 250, 50, 80, 50, "rock",
              imageNormalButton, imageNormalButton, imageNormalButton);
            cuiContext.restore(); // restore previous global alpha
          }
        }

cuiContext.save() 儲存當前全域性 alpha(控制繪製命令的不透明度),cuiContext.globalAlpha = 0.2; 將全域性 alpha 設定為一個相當低的值,即不透明度大幅降低,即以下影像幾乎透明地渲染。在使用 process() 繪製按鈕後,全域性 alpha 使用 cuiContext.restore(); 恢復。注意,只有當 eventnull 時才會呼叫 process(),這僅僅意味著按鈕處於非活動狀態,並且不會對事件做出反應。

動畫值

[編輯 | 編輯原始碼]

使用函式 animateValues() 為動畫的關鍵幀值製作動畫(即為當前時間進行插值)。animateValues() 的結果是一個值陣列,就像關鍵幀陣列中指定的值一樣,但包含當前時間的插值值。在本示例中,它以這種方式使用

        // draw animated image 
        if (null == event) {
          var xAndAngle = [190, 0];
          if (xAndAngleAnimation.isPlaying()) {
            xAndAngle = xAndAngleAnimation.animateValues();
          } 
          var x = xAndAngle[0];
          var angle = xAndAngle[1];

          var y = 200;
          if (yAnimation.isPlaying()) {
            y = yAnimation.animateValues()[0];
          }

          var widthAndHeight = [125, 158];
          if (widthAndHeightAnimation.isPlaying()) {
            widthAndHeight = widthAndHeightAnimation.animateValues();
          }
          var width = widthAndHeight[0];
          var height = widthAndHeight[1];
          ...

每次呼叫 animateValues() 都返回一個包含動畫值的當前值的陣列。從這些陣列中提取各個元素並將其分配給某些變數(這裡:xangleywidthheight)。然後,這些變數用於繪製動畫影像。注意,透過簡單地在動畫的每一幀中重新繪製畫布,複雜的動畫比我們必須分別修改所有動畫物件的動畫屬性要容易得多。

在本示例中,動畫影像的實際繪製方式如下所示

          ...
          cuiContext.save(); // save current coordinate transformation

          // read the following three lines backwards, starting with the last
          cuiContext.translate(x, y); // translate pivot point back to original position
          cuiContext.rotate(angle * Math.PI / 180.0); // rotate around pivot point
          cuiContext.translate(-x, -y); // translate pivot point to origin

          cuiContext.drawImage(imageAlien, x - width/2, y - height/2, width, height);
          cuiContext.restore(); // restore previous coordinate transformation
        }

最重要的行(也是唯一真正繪製內容的行)是

          cuiContext.drawImage(imageAlien, x - width/2, y - height/2, width, height);

它以 width × height 的大小,在座標 xy 處繪製圖像 imageAlien,使其居中。(座標 x - width/2y - height/2 指定左上角。)由於 xywidthheight 的值都隨時間變化,因此這涵蓋了大部分動畫。其餘程式碼僅用於處理 angle 旋轉。

為此,我們應用行 cuiContext.rotate(angle * Math.PI / 180.0);,它告訴 cuiContext 將所有後續繪製旋轉 angle。(乘以 Math.PI / 180.0 將度數轉換為弧度。)但是,此旋轉始終圍繞座標系的原點進行。因此,我們必須平移(即移動)旋轉的樞軸點,在本示例中它位於座標 xy 處,到座標系的原點(即座標 x=0 和 y=0)處,以便圍繞它進行旋轉。這是透過行 cuiContext.translate(-x, -y); 實現的。在旋轉之後,我們必須使用 cuiContext.translate(x, y); 將樞軸點移回其原始位置。如果您檢視程式碼,您會發現實際上必須以相反的順序指定這些平移。(如果您想按指定順序讀取變換,您必須考慮如何變換座標系:cuiContext.translate(x, y); 將座標系的原點移動到我們的樞軸點,然後我們圍繞原點(在我們樞軸點的新位置)旋轉,最後我們將原點移回其原始位置。)

由於我們不希望此旋轉影響任何進一步的繪圖命令,因此必須再次恢復標準變換。 最好的方法是首先使用 `cuiContext.save();` 儲存上下文的當前設定(即狀態)(包括變換,還包括填充顏色、字型設定等),然後在完成繪製後,可以使用 `cuiContext.restore();` 恢復這些設定。

至此,關於應用程式程式設計師如何使用示例動畫系統的討論已結束。 下一節將討論如何在 cui2d.js 中實現它。

實現動畫系統

[edit | edit source]

首先,建構函式只是設定 `cuiKeyframe` 和 `cuiAnimation` 物件的屬性

/**
 * @class cuiKeyframe
 * @classdesc A keyframe defines an array of numeric values at a certain time with tangents for the interpolation 
 * right before and right after that time. Instead of using the constructor, objects can also be initialized
 * with "{time : ..., in : ..., out : ..., values : [..., ...]}"
 * (See {@link cuiAnimation}.)
 * 
 * @desc Create a new cuiKeyframe.
 * @param {number} time - The time of the keyframe (in seconds relative to the start of the animation). 
 * @param {number} inTangent - Number specifying the tangent before the keyframe; -1: linear interpolation,  
 * 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases. 
 * @param {number} outTangent - Number specifying the tangent after the keyframe; -1: linear interpolation, 
 * 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases. 
 * @param {number[]} values - An array of numbers; all keyframes of one animation should have 
 * values arrays of the same size.
 */
function cuiKeyframe(time, inTangent, outTangent, values) {
  /** 
   * The time of the keyframe (in seconds relative to the start of the animation). 
   * @member {number} cuiKeyframe.time 
   */
  this.time = time;
  /** 
   * Number specifying the tangent before the keyframe; -1: linear interpolation,  
   * 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases. 
   * @member {number} cuiKeyframe.in 
   */
  this.in = inTangent;
  /** 
   * Number specifying the tangent after the keyframe; -1: linear interpolation,  
   * 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases. 
   * @member {number} cuiKeyframe.out 
   */
  this.out = outTangent;
  /** 
   * An array of numbers; all keyframes of one animation should have 
   * values arrays of the same size.
   * @member {number[]} cuiKeyframe.values 
   */
  this.values = values;
}

/**
 * @class cuiAnimation
 * @classdesc Animations allow to animate (i.e. interpolate) numbers specified by keyframes.
 * (See {@link cuiKeyframe}.)
 *
 * @desc Create a new cuiAnimation.
 */
function cuiAnimation() {
  this.keyframes = null;
  this.stretch = 1.0;
  this.start = 0;
  this.end = 0;
  this.isLooping = false;
}

如上所述,函式 `play()` 啟動動畫,`stopLooping()` 停止迴圈

/** 
 * Play an animation. 
 * @param {cuiKeyframe[]} keyframes - An array of keyframe objects. (Object initialization with 
 * something like var keys = [{time : ..., in : ..., out : ..., values : [..., ...]}, {...}, ...];
 * is encouraged.) (See {@link cuiKeyframe}.)
 * @param {number} stretch - A scale factor for the times in the keyframes; 
 * one way of usage: start designing keyframe times with stretch = 1 and 
 * adjust the overall timing at the end by adjusting stretch;
 * another way of usage: define all times of keyframes between 0 and 1 (as in CSS transitions) 
 * and then set stretch to the length of the animation in seconds.
 * @param {boolean} isLooping - Whether to repeat the animation endlessly.
 */
cuiAnimation.prototype.play = function(keyframes, stretch, isLooping) {
  this.keyframes = keyframes;
  this.stretch = stretch;
  this.isLooping = isLooping;
  this.start = (new Date()).getTime();
  this.end = this.start +
    1000.0 * this.keyframes[this.keyframes.length - 1].time * this.stretch;
  if (this.end > cuiAnimationsEnd) { // new maximum end?
    cuiAnimationsEnd = this.end;
  }
  cuiRepaint();
}

/**
 * Stop looping the animation.
 */
cuiAnimation.prototype.stopLooping = function() {
  this.isLooping = false;
}

基本上,`play()` 只設置 `start` 和 `end` 屬性。 兩者都是自 1970 年 1 月 1 日以來的毫秒數。 這些值基於當前時間(對於 `start`)以及最後一個關鍵幀的 `time` 屬性乘以 `stretch`(並將從動畫開始後的秒數轉換為自 1970 年 1 月 1 日以來的毫秒數)。 此外,如果需要,可以設定全域性變數 `cuiAnimationsEnd`。 `cuiAnimationsEnd` 指定所有動畫結束的時間(自 1970 年 1 月 1 日以來的毫秒數)。 因此,僅噹噹前動畫應該在所有其他動畫之後停止時才需要更新它。 最後,呼叫 `cuiRepaint()` 以儘快請求重繪。

我們還定義了一個輔助函式 `isPlaying()`,它返回 `true` 或 `false` 來指定特定動畫是否正在播放(有關如何使用它的示例,請參見上面)

/** 
 * Determine whether the animation is currently playing. 
 * @returns {boolean} True if the animation is currently playing, false otherwise.
 */
cuiAnimation.prototype.isPlaying = function() {
  if (!this.isLooping) {
    return ((new Date()).getTime() < this.end);
  }
  else {
    return (this.end > 0);
  }
}

為了不斷重繪畫布,渲染迴圈會檢查是否有任何動畫正在播放

// Render loop of cui2d, which calls cuiProcess(null) if needed. 
function cuiRenderLoop() {
  var now = (new Date()).getTime();
  if (cuiAnimationsEnd < now ) { // all animations over?
    if (cuiAnimationsArePlaying) {  
      cuiRepaint();
      // repaint one more time since the rendering might differ
      // after the animations have stopped
    }
    cuiAnimationsArePlaying = false; 
  }
  else {
    cuiAnimationsArePlaying = true;
  }

  if (cuiCanvasNeedsRepaint || cuiAnimationsArePlaying) {
    cuiProcess(null);
  }
  window.setTimeout("cuiRenderLoop()", cuiAnimationStep); // call myself again
    // using setTimeout allows to easily change cuiAnimationStep dynamically
}

它透過比較 `cuiAnimationsEnd` 與當前時間來檢查是否有任何動畫正在播放,並相應地更新全域性變數 `cuiAnimationsArePlaying`。 請注意,當動畫剛剛停止時會呼叫 `cuiRepaint()`。 這是必需的,因為畫布可能會在動畫停止後發生變化。 如果 `cuiAnimationsArePlaying` 為 `true`,則透過呼叫 `cuiProcess(null)` 渲染一個新幀。

除了對動畫值的實際計算(將在下一節中討論),這完成了對動畫系統的描述。 請注意,該系統允許重新啟動正在播放的動畫,它允許任意數量的動畫同時播放,它允許使用單個關鍵幀陣列對無限數量的值進行動畫處理,並且它允許使用任意三次 Hermite 樣條曲線進行動畫處理值,同時使其易於指定緩慢的進出運動(透過將特定關鍵幀的 `in` 和 `out` 屬性設定為 0)、Catmull-Rom 樣條曲線(透過將所有 `in` 和 `out` 屬性設定為 1)和線性插值(透過將它們全部設定為 -1)。 最後一個功能是透過插值實現的,將在下一節中討論。

動畫中關鍵幀的值透過函式 `animateValues()` 進行動畫處理(即為當前時間插值)。 如上所述,`animateValues()` 的結果是一個值陣列,與關鍵幀陣列中指定的值類似,但具有當前時間的插值值。 `animateValues()` 的實現基本上評估了每個 `values` 陣列元素的 三次 Hermite 樣條曲線,其斜率來自 Catmull-Rom 樣條曲線 或線性插值(取決於 `in` 和 `out` 屬性的符號)。 不幸的是,樣條曲線的評估有些繁瑣,Catmull-Rom 樣條曲線與線性插值的混合並沒有使其變得更容易; 因此,我們不會詳細討論這段程式碼。

但是,在函式開頭有一個重要功能:它首先檢查動畫是否尚未開始或開始時間和結束時間是否沒有意義(在它們都初始化為 0 之後就是這樣)。 在這種情況下,將返回第 0 個關鍵幀的 `values` 陣列。 然後它檢查動畫是否已經結束。 在這種情況下,將返回最後一個關鍵幀的 `values` 陣列。 此功能使即使動畫未播放也可以呼叫 `animateValues()`,實際上在上面描述的三個動畫的示例中使用了它。

/** 
 * Compute an array of interpolated values based on the keyframes and the current time. 
 * Returns the values array of the 0th keyframe if the animation hasn't started yet 
 * and the values array of the last keyframe if it has finished playing. 
 * This makes it possible to use animateValues even after the animation has stopped. 
 * (See {@link cuiKeyframe}.)
 * @returns {number[]} An array of interpolated values.
 */
cuiAnimation.prototype.animateValues = function() { 
  var now = (new Date()).getTime();
  if (now < this.start || this.end <= this.start) { // animation not started?
    return this.keyframes[0].values; 
  }
  if (now > this.end) { // current loop of animation already over?
    if (!this.isLooping) {
      return this.keyframes[this.keyframes.length - 1].values; 
    }
    // restart the animation
    var length = 1000.0 * this.keyframes[this.keyframes.length - 1].time * this.stretch;
    this.start = this.start + Math.floor((now - this.start) / length) * length;
    this.end = this.start + length;
    if (this.end > cuiAnimationsEnd) { // new maximum end?
      cuiAnimationsEnd = this.end;
    }
  }

  // determine index iTo of keyframe after(!) current time t
  var iTo = 0;
  var ut = 0.001 * (now - this.start) / this.stretch;
    // unstretched time relative to animation start in seconds
  while (iTo < this.keyframes.length &&
    this.keyframes[iTo].time < ut) {
    iTo = iTo + 1;
  }
  var iFrom = iTo - 1; // index of keyframe before t
  if (iTo == 0) {
    return this.keyframes[0].values;
  }
  if (iTo >= this.keyframes.length) {
    return this.keyframes[this.keyframes.length - 1].values;
  }
  // interpolate each value
  var newValues = this.keyframes[iFrom].values.slice(0);
  var t0 = this.keyframes[iFrom].time;
  var t1 = this.keyframes[iTo].time;
  var t = (ut - t0) / (t1 - t0)
  var tt = t * t;
  var ttt = tt * t;
  for (var iValue = 0; iValue < newValues.length; iValue++) {
    // compute values for cubic Hermite spline with out/in determining
    // the velocity: out/in = -1: linear, out/in = 0: slow (i.e. 0),
    // out/in = 1: smooth (Catmull-Rom spline).
    // The magnitude of in/out changes the velocity accordingly.
    var p0, p1, m0, m1;
    p0 = this.keyframes[iFrom].values[iValue];
    p1 = this.keyframes[iTo].values[iValue];
    // compute out slope m0 at iFrom
    if (this.keyframes[iFrom].out < 0.0) { // linear
      m0 = (p1 - p0) / (t1 - t0) * (-this.keyframes[iFrom].out);
    }
    else if (iFrom > 0) { // smooth, not in first interval
      m0 = (p1 - this.keyframes[iFrom - 1].values[iValue]) /
        (t1 - this.keyframes[iFrom - 1].time) *
        this.keyframes[iFrom].out;
    }
    else { // smooth, in first interval
      m0 = (p1 - p0) / (t1 - t0) * this.keyframes[iFrom].out;
    } 
    // compute in slope m1 at iTo
    if (this.keyframes[iTo].in < 0.0) { // linear
      m1 = (p1 - p0) / (t1 - t0) * (-this.keyframes[iTo].in);
    }
    else if (iTo < this.keyframes.length - 1) { // smooth, not last interval
      m1 = (this.keyframes[iTo + 1].values[iValue] - p0) /
        (this.keyframes[iTo + 1].time - t0) *
        this.keyframes[iTo].in;
    }
    else { // smooth, in last interval
      m1 = (p1 - p0) / (t1 - t0) * this.keyframes[iTo].in;
    } 
    // cubic Hermite curve interpolation
    newValues[iValue] =  (2.0*ttt-3.0*tt+1.0) * p0 +
      (ttt-2.0*tt+t)*(t1-t0) * m0 +
      (-2.0*ttt+3.0*tt) * p1 +
      (ttt-tt) * (t1-t0) * m1;
  }
  return newValues;
}


< Canvas 2D Web Apps

除非另有說明,否則此頁面上的所有示例原始碼均授予公有領域。
華夏公益教科書