LoginSignup
2
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-29

はじめに

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

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

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

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

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];
      }
    });
  });
}

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

使用例
/* .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で動的に指定できたらいいですよね。

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;
    });
  };
})();

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

使用例
/* 要素をいろいろ動かしてみる */
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 を利用する方法はちょっとめんどくさいですが。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4