IE11でもbackground-sizeをアニメーション

CSS Animationのbackground-sizeのアニメ化をIE11に適用させるJavaScript

IE11にはいろいろと毀誉褒貶があるようだが、Web標準の実装では今や他のブラウザとほとんど遜色ないし、個人的には良くできたブラウザだと思う。 そんなIE11だが、CSSのAnimation関連で、いくつかアニメ化が実装されなかったプロパティがある。

IE11に欠けているアニメーション可能な主なプロパティ
  • background-size
  • flexbox関連のプロパティのいくつか
  • multi-column関連のプロパティのいくつか
  • font-size-adjust / font-stretch / outline-offset / text-decoration-colorなど

flexboxとかmulti-columnは最近になって勧告候補(Candidate Recommendation)になったばかりだし、ほかのプロパティもまだ草案(Working Draft)段階だったりするものもあるので、それらが実装されなかったのはまぁしょうがない、という気がする。 でも、残念なのが、background-sizeまでもが動かせないこと。

そこで、例によって、JavaScriptで代替実装させてみた。
サンプルはこちら。 使用前使用後

Win7のIE11で動作確認済み。 IE10も、たぶん大丈夫でしょう、…おそらく、…きっと、…うまくすればw。 IE10でもとりあえず動作確認できました

使用方法と制限事項

  • ソースのanimateBgSizeSettings内に、アニメーションさせる要素(ターゲット)のCSSセレクタとanimation-name(@keyframesの名前)を記述。
  • tがターゲット要素、nがアニメーション名。
    pはオプション(省略可)で、一時停止・再生させたい場合にトリガーとなるイベント名を記述。
  • 複数の設定をまとめて記述する場合には、{ }, { }とオブジェクトをカンマで繋げればOK。
    例: { t: '#animateMe', n: 'aniMania', p : 'click' }, { t: '.animateUs', n: 'aniMate' }
  • tのターゲット要素に、/^#[\-]?[a-z_][a-z0-9_\-]+$/i(「#」で始まり、以降はアルファベット(大文字小文字問わず)と数字、アンダースコア、ハイフンのみの組み合わせ、…手抜き実装だし、説明も手抜きですがw)にマッチする形でid値を指定した場合、多少早くなります(たぶん)。
    一方、複数の要素に対して同じアニメーションを適用させたい場合は、tにはカンマ区切りで複数の要素をまとめて指定できます。
    例: { t: '#animateMe, #animateYou', n: 'aniMania' }
  • 同様にpのイベントもカンマ区切りでまとめて指定できます。
  • 一方、1つのオブジェクトにつき、nに入れられる値(animation-name)は1つだけとしています。 CSS上で1つの要素に対して複数のアニメーションを適用させている場合などには、CSSのようにanimation-nameのリスト全てをnに列記するのではなく、background-sizeを動かしているanimation-nameのうちでリストの最後に最も近いものを記述してください。
    例えば、 { t: '.animateUs', n: 'aniMate, aniMania' } このような指定は無効となります。
  • @keyframesの中でbackground-sizeしか記述されていない場合、IEはアニメーションさせるプロパティがないと判断してその@keyframesを無視してしまうため、動きません(animationstartが発火しない)。 (無駄ですが)IEでアニメーション可能なプロパティを併記してください(fromtoの値を一致させておくなどしておけばOK)。 @keyframes aniMate { from { width: 100%; background-size: 0 auto; } to { width: 100%; background-size: 100% auto; } }
  • ::before / ::after擬似要素には対応していません(インラインのstyle属性では擬似要素のスタイルの設定ができないため)。
  • アニメーションの使用方法やユーザーの閲覧環境によっては、サイトをモッサリさせてしまうかもしれません。 アニメーションはそうでなくても負荷が高めで、それに加えてJavaScriptも動かしているわけなので、使い過ぎにはご注意を(※個人の体験に基づく感想ですw)。

スクリプトソース (最終更新:2016/2/5)


/**
*   animateBackgroundSize()
*   IE(10~11)にCSS Animationでのbackground-sizeのアニメ化を適用
*   Copyright (c) 2013-2016 Kazz
*   http://asamuzak.jp
*   Dual licensed under MIT or GPL
*   http://asamuzak.jp/license
*/

(function(_win, _doc) {
  "use strict";
  function animateBackgroundSize() {
    // User Settings
    // t - querySelector of the target element.
    //     ターゲットのセレクタ(querySelector)  (例) "#animateMe"
    // n - a name of @keyframes (animation-name).
    //     keyframesの名前(animation-nameの値)  (例) "aniMania"
    // p - event trigger to pause / restart animation.
    //     一時停止のトリガーevent(オプション)  (例) "click"
    // 複数指定する場合は { }, { } のようにカンマで繋ぐ
    var animateBgSizeSettings = [
      // { t: "#animateMe", n: "zoomInBg", p: "click, dblclick" },
      // { t: ".mirror_initial", n: "transformInitial" }
    ];

    var aBg = {},
        _cNum = (function(n) { return function() { return n++; };})(0);
    function getBezierSplineAtP(x1, y1, x2, y2, p) {
      var x;
      if(x1 === y1 && x2 === y2) {
        x = p;
      }
      else {
        for(var y = p, a = (1 - 3 * (x2 - x1)), b = (3 * (x2 - 2 * x1)), c = (3 * x1),
                i = 0; i < 4; i++) {
          x = 3 * a * y * y + 2 * b * y + c;
          if(x === 0) {
            break;
          }
          y -= (((a * y + b) * y + c) * y - p) / x;
        }
        x = (((1 - 3 * (y2 - y1)) * y + 3 * (y2 - 2 * y1)) * y + 3 * y1) * y;
      }
      return x.toFixed(4);
    }
    function getSteps(p, s) {
      for(var x = s[1] * 1, y = s[2], z = 1 / x, i = 0, l = x; i <= l; i++) {
        switch(y) {
          case "start":
            switch(true) {
              case p === 0:
                p = z;
                break;
              case (p === 1 || i === l):
                p = 1;
                break;
              case (i > 0 && i < l && p === z * i):
                p = z * (i + 1);
                break;
              case (i > 0 && p > z * (i - 1) && p < z * i):
                p = z * i;
                break;
            }
            break;
          case "end":
            switch(true) {
              case (p === 0 || p === 1 || p === z * i):
                break;
              case (i === l):
                p = 1;
                break;
              case (i > 0 && p > z * (i - 1) && p < z * i):
                p = z * (i - 1);
                break;
            }
            break;
        }
      }
      return p;
    }
    function setCalcVal(t, f, c) {
      var x,
          y = /^([0-9]+(?:\.[0-9]+)?)([a-z%]+)?$/,
          tv = (x = y.exec(t)) && x[1] * 1,
          tu = x[2] ? x[2] : "",
          fv = (x = y.exec(f)) && x[1] * 1,
          fu = x[2] ? x[2] : "";
      return c === 0 ? f :
             c === 1 ? t :
             tv === 0 ? fv * (1 - c) + fu :
             fv === 0 ? tv * c + tu :
             tu === fu ? ((tv - fv) * c + fv) + tu :
             "calc((" + t + "-" + f + ")*" + c + "+" + f + ")";
    }
    function convKeywordToBezier(k) {
      switch(k) {
        case "ease":
          k = "cubic-bezier(0.25,0.1,0.25,1)";
          break;
        case "linear":
          k = "cubic-bezier(0,0,1,1)";
          break;
        case "ease-in":
          k = "cubic-bezier(0.42,0,1,1)";
          break;
        case "ease-out":
          k = "cubic-bezier(0,0,0.58,1)";
          break;
        case "ease-in-out":
          k = "cubic-bezier(0.42,0,0.58,1)";
          break;
        default:
          k = /cubic\-bezier/.test(k) ? k : false;
      }
      return k;
    }
    function convSecToMiliSec(s) {
      return s && (s = /^(\-?[0-9]*(?:\.[0-9]+)?)(m?s)$/.exec(s)) ?
        /ms/.test(s[2]) ? s[1] * 1 :
        s[1] * 1000 : 0;
    }
    function convKeyToCoef(k) {
      return (k = /^([0-9]+(?:\.[0-9]+)?)%?$/.exec(k)) ? k[1] / 100 : 1;
    }
    function getKeyframes(s) {
      for(var y, z = _doc.styleSheets, i = 0, l = z.length; i < l; i++) {
        for(var j = 0, k = z[i].cssRules.length; j < k; j++) {
          var a = z[i].cssRules[j];
          if(a.type === 7 && a.name === s) {
            y = a;
            break;
          }
        }
      }
      return y;
    }
    function getBgKeyframesValue(s) {
      function getBgKeyframeRule(k) {
        for(var y = [], z, i = 0, l = k.cssRules.length; i < l; i++) {
          k.cssRules[i].type === 8 && (
            z = /background\-size\s*:\s*(.+);/.exec(k.cssRules[i].cssText)
          ) &&
            y.push({ key: k.cssRules[i].keyText, rule: z[1] });
        }
        return y;
      }
      function convRuleToArr(r) {
        var a = [];
        r = r.split(",");
        for(var i = 0, l = r.length; i < l; i++) {
          var b = r[i].replace(/^\s*/, "").replace(/\s*$/, "").split(/\s+/);
          a[i] = {
            w: b[0],
            h: b.length === 2 ? b[1] : /co(?:ntain|ver)/.test(b[0]) ? "" : "auto"
          };
        }
        return a;
      }
      function setKeyframeBgSize(a) {
        var y = [], z = 0, i, l, j, k, w, h;
        for(i = 0, l = a.length; i < l; i++) {
          y[i] = {
            coef: convKeyToCoef(a[i].key),
            val: convRuleToArr(a[i].rule)
          };
        }
        for(i = 0, l = y.length; i < l; i++) {
          z = Math.max(z, y[i].val.length);
        }
        for(i = 0, l = y.length; i < l; i++) {
          for(j = 0, k = z; j < k; j++) {
            y[i].val.length < z && (y[i].val[y[i].val.length] = y[i].val[j]);
          }
        }
        for(i = 0, l = z; i < l; i++) {
          for(w = "", h = "", j = 0, k = y.length; j < k; j++) {
            w += y[j].val[i].w;
            h += y[j].val[i].h;
          }
          if(/auto|co(?:ntain|ver)/.test(w)) {
            for(w = y[y.length - 1].val[i].w, j = 0, k = y.length; j < k; j++) {
              y[j].val[i].w = w;
            }
          }
          if(/auto|co(?:ntain|ver)/.test(h)) {
            for(h = y[y.length - 1].val[i].h, j = 0, k = y.length; j < k; j++) {
              y[j].val[i].h = h;
            }
          }
        }
        return z > 0 ? y : [];
      }
      var x = getKeyframes(s);
      x && (
        x = getBgKeyframeRule(x), x = x.length > 1 ?
        setKeyframeBgSize(x) : []
      );
      return x ? x : [];
    }
    function getAnimationOrdinal(v, s) {
      var x = null;
      if(/,/.test(v) && s) {
        v = v.replace(/\s+/g, "").split(",");
        for(var i = 0, l = v.length; i < l; i++) {
          if(v[i] === s) {
            x = i;
            break;
          }
        }
      }
      return x;
    }
    function getPropValFromComputedStyle(v, n) {
      v = v.replace(/\s+/g, "");
      return /,/.test(v) && !/^(?:cubic\-bezier\([\-\.0-9,]+\)|steps\([0-9]+,(?:start|end)\))$/.test(v) && n !== null ?
        (v = /(?:cubic\-bezier\([\-\.0-9,]+\)|steps\([0-9]+,(?:start|end)\)),/.test(v) ?
          v.split(/,(?!(?:[\-\.0-9]|(?:start|end)\)))/) :
          v.split(","), v[n * 1]) :
        v;
    }
    function runBgSizeAnimation(tObj, aObj) {
      function setBgSizeValue(c) {
        for(var x = aObj.aVal, y = "", z = /auto|co(?:ntain|ver)/,
                f, t, i = 1, l = x.length; i < l; i++) {
          if((t = x[i], f = x[i - 1]) && t.coef >= c) {
            for(var j = 0, k = t.val.length; j < k; j++) {
              y = z.test(t.val[j].w) || t.val[j].w === f.val[j].w ?
                y + t.val[j].w + " ":
                y + setCalcVal(t.val[j].w, f.val[j].w, c) + " ";
              y = z.test(t.val[j].h) || t.val[j].h === f.val[j].h ?
                y + t.val[j].h + "," :
                y + setCalcVal(t.val[j].h, f.val[j].h, c) + ",";
            }
            break;
          }
        }
        return y.replace(/,$/, "");
      }
      function animateBgSize(t) {
        var a,
            p = (a = ((t - aObj.aStart - aObj.aDly) / aObj.aDur).toFixed(4) * 1) &&
                  aObj.aCount % 2 === 1 ?
                    a : 1 - a,
            c = (a = /cubic\-bezier\(\s*([01]?(?:\.[0-9]+)?)\s*,\s*([01]?(?:\.[0-9]+)?)\s*,\s*([01]?(?:\.[0-9]+)?)\s*,\s*([01]?(?:\.[0-9]+)?)\s*\)/.exec(aObj.aFunc)) ?
                  getBezierSplineAtP(a[1] * 1, a[2] * 1, a[3] * 1, a[4] * 1, p) * 1 :
                  (a = /steps\(\s*([0-9]+)\s*,\s*(start|end)\s*\)/.exec(aObj.aFunc)) ?
                    getSteps(p, a) : aObj.aCount % 2 === 1 ?
                    1 : 0;
        tObj.style.setProperty("background-size", setBgSizeValue(c));
        (c > 0 && aObj.aCount % 2 === 0 || c < 1 && aObj.aCount % 2 === 1) && (
          aObj.doAnimate = _win.requestAnimationFrame(animateBgSize)
        );
      }
      function pauseAnimate(t) {
        var x = getPropValFromComputedStyle(
          _win.getComputedStyle(tObj).animationPlayState,
          aObj.aOrder
        );
        aObj.aStop !== null && (
          (x === "paused" && aObj.aStop === 0) ||
          (x === "running" && aObj.aStop > 0)
        ) && (
          x === "paused" ? (
            aObj.aStop = t, aObj.aDly -= (aObj.aStop - aObj.aStart),
            _win.cancelAnimationFrame(aObj.doAnimate)
          ) : x === "running" && (
            aObj.aStart = t,
            aObj.aStop = 0,
            aObj.doAnimate = _win.requestAnimationFrame(animateBgSize)
          )
        );
      }
      function cuePauseAnimate() {
        aObj.aStop !== null && _win.requestAnimationFrame(pauseAnimate);
      }
      function toggleAnimationEvent(evt) {
        var x;
        if(evt.currentTarget === tObj && evt.animationName === aObj.aName) {
          switch(evt.type) {
            case "animationstart":
              aObj.aStart = _win.performance.now();
              aObj.aStop = 0;
              aObj.doAnimate = _win.requestAnimationFrame(animateBgSize);
              break;
            case "animationiteration":
              aObj.aStart = _win.performance.now();
              aObj.aDly = 0;
              /alternate/.test(getPropValFromComputedStyle(
                _win.getComputedStyle(tObj).animationDirection, aObj.aOrder
              )) &&
                aObj.aCount++;
              aObj.doAnimate = _win.requestAnimationFrame(animateBgSize);
              break;
            case "animationend":
              _win.cancelAnimationFrame(aObj.doAnimate);
              (x = getPropValFromComputedStyle(
                _win.getComputedStyle(tObj).animationFillMode, aObj.aOrder)
              ) && /both|forwards/.test(x) ?
                tObj.style.setProperty("background-size", setBgSizeValue(1)) :
                /none|backwards/.test(x) && tObj.style.removeProperty("background-size");
              break;
            case "error":
              _win.cancelAnimationFrame(aObj.doAnimate);
              tObj.style.removeProperty("background-size");
              break;
          }
        }
      }
      function setAnimationProp() {
        var a = _win.getComputedStyle(tObj),
            x = getAnimationOrdinal(a.animationName, aObj.aName),
            y;
        aObj.aOrder = x;
        aObj.aVal = getBgKeyframesValue(aObj.aName);
        aObj.aDur = convSecToMiliSec(getPropValFromComputedStyle(a.animationDuration, x));
        aObj.aDly = (y = convSecToMiliSec(
          getPropValFromComputedStyle(a.animationDelay, x)
        )) < 0 ? y : 0;
        aObj.aFunc = convKeywordToBezier(
          getPropValFromComputedStyle(a.animationTimingFunction, x)
        );
        aObj.aCount = /reverse/.test(getPropValFromComputedStyle(a.animationDirection, x)) ?
          0 : 1;
        if(aObj.aDur > 0 && aObj.aFunc && aObj.aVal.length > 1) {
          /b(?:ackwards|oth)/.test(getPropValFromComputedStyle(a.animationFillMode, x)) &&
            tObj.style.setProperty("background-size", setBgSizeValue(0));
          tObj.addEventListener("animationstart", toggleAnimationEvent, false);
          tObj.addEventListener("animationiteration", toggleAnimationEvent, false);
          tObj.addEventListener("animationend", toggleAnimationEvent, false);
          tObj.addEventListener("error", toggleAnimationEvent, false);
          if(aObj.aPause) {
            y = aObj.aPause.split(",");
            for(var i = 0, l = y.length; i < l; i++) {
              _win.addEventListener(y[i].replace(/\s+/g, ""), cuePauseAnimate, true);
            }
          }
          aObj.aTime > 0 && (aObj.doAnimate = _win.requestAnimationFrame(animateBgSize));
        }
      }
      setAnimationProp();
    }
    (function(a) {
      function setABgObj(o, p) {
        var q = "aBgId" + _cNum();
        aBg[q] = {
          aName: p.n,
          aPause: p.p ? p.p : null,
          aStart: null,
          aStop: null,
          aOrder: null,
          aVal: null,
          aDur: 0,
          aDly: 0,
          aFunc: null,
          aCount: null,
          doAnimate: null
        };
        runBgSizeAnimation(o, aBg[q]);
      }
      for(var x, z, i = 0, l = a.length; i < l; i++) {
        var y = a[i];
        if((x = /^#([\-]?[a-z_][a-z0-9_\-]+)$/i.exec(y.t)) &&
           (z = _doc.getElementById(x[1]))) {
          setABgObj(z, y);
        }
        else {
          z = _doc.querySelectorAll(y.t);
          for(var j = 0, k = z.length; j < k; j++) {
            setABgObj(z[j], y);
          }
        }
      }
    })(animateBgSizeSettings);
  }
  _doc.documentMode && _win.requestAnimationFrame &&
    _doc.addEventListener("DOMContentLoaded", animateBackgroundSize, false);
})(window, document);

"IE11でもbackground-sizeをアニメーション"へのTwitter上でのコメントやRT

23件のツイートがあります。

ツイート 1

ツイート 2

ツイート 3

ツイート 4

ツイート 5

ツイート 6

ツイート 7

ツイート 8

ツイート 9

ツイート 10

ツイート 11

ツイート 12

ツイート 13

ツイート 14

ツイート 15

ツイート 16

ツイート 17

ツイート 18

ツイート 19

ツイート 20

ツイート 21

ツイート 22

ツイート 23