jQueryを使わずに要素のアニメーションをしてみる

#はじめに

jQueryのアニメーション `.animate()` はとても便利ですが、プラグインなしでは色が変わるアニメーションを作れなかったり `transform` 関係のアニメーションが若干面倒です。
そして、CSS3からはJavaScriptなしでもアニメーションができるようになりましたが、CSSを別に書かなければならないのでJavaScriptと連携させる時に面倒だったりするのも事実。
それならJavaScriptで動的にCSSアニメーションをしてみようという感じです。

※あくまでも参考用のコードなので、何か予期せぬ動作を起こしたりするかもしれません。

#`transition` を利用したJavaScriptのアニメーション

単純なものならこんな感じでしょうか。
jQueryの `.animate()` に似た表記で書けるように。

```js:transitionアニメーション
// アニメーション用関数
function transition(selector, style, option, callback) {
  var
    // 要素の取得
    elements = document.querySelectorAll(selector),

    // アニメーションさせるプロパティの配列
    properties = Object.keys(style),

    // element.style.transition 用の変数
    transitionValue;

  // element.style.transition 用の値に整える
  if (!option) {
    // 何も指定されてない場合
    transitionValue = 'all 400ms ease';
  } else if (typeof option == 'string') {
    // transition の値そのままの場合
    transitionValue = option;
  } else if (typeof option == 'number') {
    // 数値で duration が指定された場合
    transitionValue = 'all ' + option + 'ms ease';
  } else {
    // オブジェクトで渡されたとき
    transitionValue =
      (option.property || 'all') + ' ' +
      (option.duration || 400) + 'ms ' +
      (option.easing || 'ease') + ' ' +
      (option.delay || 0) + 'ms';
  }

  // 各要素に適用させる
  Array.prototype.forEach.call(elements, function (element) {
    var
      // 要素のスタイル
      elemStyle = window.getComputedStyle(element),

      // タイマー
      timer = null,
      delay,

      // アニメーション終了後の処理
      transitionend = function () {
        clearTimeout(timer);

        element.removeEventListener('transitionend', transitionend);
        element.style.transition = '';

        // コールバック関数がある場合は実行
        if (typeof callback == 'function') {
          callback.call(element);
        }
      };

    // 要素に transition を適用する
    element.style.transition = transitionValue;

    // transitionend でアニメーションの終わりに実行させる
    element.addEventListener('transitionend', transitionend);

    // transitionend が発火しなかった時の保険
    delay = (parseFloat(elemStyle.transitionDuration) + parseFloat(elemStyle.transitionDelay)) * 1000 + 50;
    timer = setTimeout(transitionend, delay);

    // 要素にスタイルを適用（チェインケースかキャメルケースかで分ける）
    properties.forEach(function (property) {
      if (property.indexOf('-') > -1) {
        element.style.setProperty(property, style[property]);
      } else {
        element.style[property] = style[property];
      }
    });
  });
}
```

使い方としてはこんな感じです。

```js:使用例
/* .moveBox が1秒かけて右下に 200px, 200px 動くアニメーション */
// パターン1
transition('.moveBox', {
  transform: 'translate(200px, 200px)',
}, 'all 1s ease');

// パターン2
transition('.moveBox', {
  transform: 'translate(200px, 200px)',
}, 1000);

// パターン3
transition('.moveBox', {
  transform: 'translate(200px, 200px)',
}, {
  duration: 1000,
});

/* .fadeBox が1秒後に600ミリ秒かけて180°回転しながらフェードアウトした後にコンソールに出力 */
// パターン1
transition('.fadeBox', {
  opacity: '0',
  transform: 'rotate(180deg)',
}, 'all 600ms ease-in-out 1s', function () {
  console.log('終わったよ');
});

// パターン2
transition('.fadeBox', {
  opacity: '0',
  transform: 'rotate(180deg)',
}, {
  duration: 600,
  easing: 'ease-in-out',
  delay: 1000,
}, function () {
  console.log('終わったよ');
});

/* jQueryの標準ではできない色の変化アニメーション */
// パターン1
transition('.colorBox', {
  backgroundColor: '#f00',
});

// パターン2
transition('.colorBox', {
  'background-color': '#f00',
});
```

コールバックは、取得した要素の数だけ実行されます。
またコールバック関数の `this` にはそれぞれの要素が入っています。jQueryと似たような感じです。

#`animation` と `@keyframes` を利用したJavaScriptのアニメーション

`transition` では直線的なアニメーションしかできませんが、`animation` と `@keyframes` で複雑なアニメーションを作ることができます。
これをJavaScriptで動的に指定できたらいいですよね。

```js:keyframesアニメーション
(function () {
  // @keyframes用のstyle要素の生成
  var styleSheet = document.head.appendChild(document.createElement('style')).sheet;

  // keyframes の名前用ランダムキー
  function randomKey() {
    return 'keyframes' + Math.random().toString().replace('.', '');
  }

  // CSSの @keyframes ルールの削除
  function resetAnimation(key) {
    var
      rules = styleSheet.cssRules,
      length = rules.length,
      i = 0;

    for (; i < length; i++) {
      if (rules[i].name === key) {
        styleSheet.deleteRule(i);
        break;
      }
    }
  }

  // 本体
  window.keyframes = function (selector, keys, option, callback) {
    var
      // 要素の取得
      elements = document.querySelectorAll(selector),

      // @keyframes 用の変数
      keyframesRulue = '',

      // アニメーションに使用するプロパティの配列
      fillStyle = [],

      // element.style.animation 用の変数
      animationValue;

    // @keyframes 用に整える
    Object.keys(keys).forEach(function (per) {
      var
        style = keys[per],
        styleArray = Object.keys(style),
        cssText = '';

      keyframesRulue += per + '{';

      styleArray.forEach(function (property) {
        cssText += property + ':' + style[property] + ';';
      });

      keyframesRulue += cssText + '}';

      fillStyle.push.apply(fillStyle, styleArray);
    });

    // 重複しているプロパティを削除
    fillStyle = fillStyle.filter(function (value, i) {
      return fillStyle.indexOf(value) === i;
    });

    // element.style.animation 用に整える
    if (!option) {
      // 何も指定されてない場合
      animationValue = '400ms ease 0s 1 normal none';
    } else if (typeof option == 'string') {
      // テキストの場合
      animationValue = option;
    } else if (typeof option == 'number') {
      // 数値で duration が指定された場合
      animationValue = option + 'ms ease 0s 1 normal none';
    } else {
      // オブジェクトで渡されたとき
      animationValue =
        (option.duration || 400) + 'ms ' +
        (option.easing || 'ease') + ' ' +
        (option.delay || 0) + 'ms ' +
        (option.count || 1) + ' ' +
        (option.direction || 'normal') + ' ' +
        (option.fill || 'none');
    }

    // 各要素に適用させる
    fillStyle.forEach.call(elements, function (element) {
      var
        // @keyframes 用の名前の生成
        key = randomKey(),

        // 要素のスタイルオブジェクトの取得
        elemStyle = window.getComputedStyle(element);

      // style要素に @keyframes ルールの追加
      styleSheet.insertRule('@keyframes ' + key + '{' + keyframesRulue + '}', styleSheet.cssRules.length);

      // animationend でアニメーションの終わりに実行させる
      element.addEventListener('animationend', function animationend() {
        element.removeEventListener('animationend', animationend);

        // animation-fill-mode: none; 以外の場合の処理
        if (elemStyle.animationFillMode !== 'none') {
          fillStyle.forEach(function (property) {
            element.style.setProperty(property, elemStyle.getPropertyValue(property));
          });
        }

        element.style.animation = '';
        resetAnimation(key);

        // コールバック関数がある場合は実行
        if (typeof callback == 'function') {
          callback.call(element);
        }
      });

      // 要素に animation を適用する
      element.style.animation = key + ' ' + animationValue;
    });
  };
})();
```

使い方としてはこんな感じです。

```js:使用例
/* 要素をいろいろ動かしてみる */
keyframes('#moveBox01', {
  // @keyframes に似た構造のオブジェクト
  '0%': {
    'background-color': '#f00',
    'transform': 'translate(0, 0) rotate(0deg)',
  },
  '50%': {
    'background-color': '#0f0',
    'transform': 'translate(250px, 0) rotate(0deg)',
  },
  '100%': {
    'background-color': '#00f',
    'transform': 'translate(500px, 0) rotate(360deg)',
  },
}, {
  duration: 2000,          // animation-duration と同じ
  easing: 'ease',          // animation-timing-function と同じ
  count: 3,                // animation-iteration-count と同じ
  direction: 'alternate',  // animation-direction と同じ
  fill: 'forwards',        // animation-fill-mode と同じ
}, function () {
  console.log('終わったよ');
});
```

動的に `@keyframes` によるアニメーションが実現できました。
（サンプルを置けばわかりやすいのですが）

#おわりに

jQueryなしでも結構実現できるものですね。
`@keyframes` を利用する方法はちょっとめんどくさいですが。