[源碼閱讀]解析Anime(JS動畫庫)核心(1)

本次解析將分爲2篇文章,當前是第一篇,第二篇在這裏git

另外,爲了能更好的理解這個庫,我的寫了一個此庫的壓縮版,實現了核心的功能(主要也是爲了更好理解核心功能),內容更少方便閱讀,
地址在這裏github


介紹

anime一個JS輕量動畫庫,摒棄了常規的left,top屬性,全面採用requestAnimateFrame+CSS3屬性能充分調用設備進行GPU渲染。算法

它的亮點有如下(直接引用官網):segmentfault

  • Keyframes(幀動畫): Chain multiple animation properties.
  • Timeline(同步動畫): Synchronize multiple instances together.
  • Playback controls(暫停回放功能): Play, pause, restart, seek animations or timelines.
  • CSS transforms(CSS動畫): Animate CSS transforms individually.
  • Function based values(函數定義配置(注入了內部屬性)): Multiple animated targets can have individual value.
  • SVG Animations(SVG動畫): Motion path, line drawing and morphing animations.
  • Easing functions(自定義貝塞爾函數): Use the built in functions or create your own Cubic Bézier curve easing.

這麼多亮點,其實關鍵函數就3~4個。promise

由於這裏都是使用緩動函數算法,也就是經過 初始位置, 結束位置, 持續時間,已消耗的時間 計算出當前所在位置。函數

初始位置結束位置持續時間是做爲參數傳入配置的,所以計算已消耗時間就是完成動畫的核心。 動畫

下面就深刻了解下它的核心。ui

深刻理解

先了解幾個時間的變量,動畫都是算法+時間=位置這麼算出來的:rest

// 記錄當前位置所對應的時間,根據lastTime計算
instance.cuurentTime
// 記錄當前位置所消耗的時間
engineTime
// 記錄上一次計算完畢賦值後的位置對應時間
lastTime
// 上一次調用raf的時間
startTime
// 當前位置所消耗時間(能匹配反轉狀態),根據engineTime計算
insTime
// 動畫持續時間
insDuration
// 延遲時間
delay
// 從什麼時間點開始動畫
insOffset

接着看幾個關鍵函數,這裏先不放具體代碼,只是先知道是作什麼的(按一個正常動畫順序排放):code

// anime的核心機制, 遞歸調用raf執行(關鍵)
const engine = (() => {
  // ...requestAnimateFrame
})();

// anime主體
function anime(params){
  
  // 定義instance 也是最終返回值
  let instance = createNewInstance(params);
  
  // 外部API 從當前位置開始執行動畫
  instance.play = function() {}
  
  // 配置 startTime 和 engineTime(關鍵)
   instance.tick = function(t) {}
   
  // 對當前engineTime進行判斷,肯定動畫方案(關鍵)
  function setInstanceProgress(engineTime) {}
  
  // 計算動畫當前位置 而且賦值(關鍵)
  function setAnimationsProgress(insTime){}

  // 直接跳到參數time的時間所在的位置
  instance.seek = function(time) {}
  // 外部API 暫停
  instance.pause = function() {}
  // 外部API 反轉
  instance.reverse = function() {}
  // 外部API reset
  instance.reset = function() {}
  // 外部API 從新開始
  instance.restart = function() {}
  /*...*/
  return instance
}

關鍵函數就4個,其餘都是一些對關鍵函數的具體使用

接着一個個解析:

  • createNewInstance

其實就是對屬性和方法合併成一個總體對象,這個對象是貫穿全局的,所以裏面什麼都有...

function createNewInstance(params) {
  
    /* 對params進行處理 */
    const instanceSettings = replaceObjectProps(defaultInstanceSettings, params);
    const tweenSettings = replaceObjectProps(defaultTweenSettings, params);
    const animatables = getAnimatables(params.targets);
    const properties = getProperties(instanceSettings, tweenSettings, params);
    const animations = getAnimations(animatables, properties);
        
    // mergeObjects(o1,o2)至關於 Object.assing({},o2,o1)
    return mergeObjects(instanceSettings, {
      children: [],
      animatables: animatables,
      animations: animations,
      duration: getInstanceTimings('duration', animations, instanceSettings, tweenSettings),
      delay: getInstanceTimings('delay', animations, instanceSettings, tweenSettings)
    });
  }
  • instance.play

此處先作了防禦,只有paused狀態下才會執行,lastTime這裏是調取當前動畫的位置對應的時間,所以才能夠實現從任意位置開始動畫。

// 外部API 從當前位置開始執行動畫
instance.play = function() {
  if (!instance.paused) return;
  instance.paused = false;
  // 從0 開始
  startTime = 0;
  // 調取當前動畫當前位置所對應的時間
  lastTime = adjustTime(instance.currentTime);
  // 給 activeInstances 添加當前實例,說明這是一個正在運行的動畫
  activeInstances.push(instance);
  // raf未啓動,調用engine
  if (!raf) engine();
}
  • engine

anime的核心機制,經過遞歸調用requestAnimateFrame,當檢測到須要執行動畫的集合activeInstances有值,調用instance.tick。

// IIFE 以後調用engine至關於執行內部的play
  const engine = (() => {
    // step收到一個參數,
    function play() { raf = requestAnimationFrame(step); };
    // 這裏的參數t是 raf的參數中能夠接受的一個時間戳,表示觸發調用的時間
    function step(t) {
      // activeInstances指正在被執行的動畫集合
      const activeLength = activeInstances.length;
      // 存在正在運行的動畫
      if (activeLength) {
        let i = 0;
        while (i < activeLength) {
          // 調用tick執行
          if (activeInstances[i]) activeInstances[i].tick(t);
          i++;
        }
        play();
      } else {
        // 不存在正在運行的動畫 cancel
        cancelAnimationFrame(raf);
        raf = 0;
      }
    }
    return play;
  })();
  • instance.tick

tick的做用經過參數traf的一個時間戳概念,計算出距離上一次調用實際消耗的時間engineTime

例如:上一次調用時間戳是1000,也就是1秒,中途忽然執行一個巨大的任務,等任務結束,時間戳是20000
那麼此次的engineTime就是lastTime+20000-1000,也就是計算此次動畫從上次位置再加上19秒的位置...
那麼anime對於這種狀況是怎麼處理呢?繼續看下一個setInstanceProgress

// 配置 startTime 和 engineTime
instance.tick = function(t) {
  now = t;
  // startTime 若是首次執行 就是now,不然就是上一次tick的時間
  if (!startTime) startTime = now;
  // lastTime 是上一次執行結束後動畫對應位置的時間戳
  // engineTime 是到動畫目前爲止消耗的總時間,通常理論上講是lastTime+16.6667
  const engineTime = (lastTime + now - startTime) * anime.speed;
  setInstanceProgress(engineTime);
}
  • setInstanceProgress

這個函數接受一個消耗的時間值,在內部對其進行適配和定義了各類狀況的動畫起始點,傳遞給setAnimationsProgress

例如,上面那個例子,若是消耗了19秒,就如進入這個判斷:從結束點開始動畫(考慮reverse的狀況)。

// 消耗的時間超出了持續時間 而且當前位置不在終點  或者 未設定持續時間
if ((insTime >= insDuration && insCurrentTime !== insDuration) || !insDuration){
  if ((insTime >= insDuration && insCurrentTime !== insDuration) || !insDuration) {
    // 從結束點開始
    setAnimationsProgress(insDuration);
    if (!insReversed) countIteration();
  }
}

setInstanceProgress(省略了一些配置的定義)

// 對當前engineTime進行判斷,肯定動畫方案
function setInstanceProgress(engineTime) {
  // 動畫持續時間
  const insDuration = instance.duration;
  // 從什麼時間點開始動畫
  const insOffset = instance.offset;
  // 加上延遲後的開始時間
  const insStart = insOffset + instance.delay;
  // 記錄當前位置所對應的時間
  const insCurrentTime = instance.currentTime;
  // 是不是反轉狀態
  const insReversed = instance.reversed;
  // 當前位置所消耗時間(能匹配反轉狀態)
  // 這裏adjustTime就是若是是反轉狀態,則返回 insDuration-engineTime
  const insTime = adjustTime(engineTime);
  /* ... */
  // 消耗的時間大於應該開始的時間 而且 消耗的時間在持續時間範圍內
  if (insTime > insOffset && insTime < insDuration) {
    setAnimationsProgress(insTime);
  } else {
    // 消耗的時間小於應該開始的時間 而且 當前位置不在起點
    if (insTime <= insOffset && insCurrentTime !== 0) {
      // 從頭開始
      setAnimationsProgress(0);
      if (insReversed) countIteration();
    }
    // 消耗的時間超出了持續時間 而且當前位置不在終點  或者 未設定持續時間
    if ((insTime >= insDuration && insCurrentTime !== insDuration) || !insDuration) {
      // 從結束點開始
      setAnimationsProgress(insDuration);
      if (!insReversed) countIteration();
    }
  }
  setCallback('update');
  // 消耗時間大於持續時間 而且在終點(不在終點的上面已經判斷了)
  if (engineTime >= insDuration) {
    if (instance.remaining) {
      startTime = now;
      if (instance.direction === 'alternate') toggleInstanceDirection();
      // remaining爲false,remaining>0說明還須要繼續動畫
    } else {
      // 完成動畫的執行
      instance.pause();
      if (!instance.completed) {
        instance.completed = true;
        setCallback('complete');
        if ('Promise' in window) {
          resolve();
          promise = makePromise();
        }
      }
    }
    lastTime = 0;
  }
}

關鍵函數setAnimationsProgress和後續的操做函數都放在下一篇繼續解析。

相關文章
相關標籤/搜索