JavaScript - 基於CSS3動畫的實現


在痛苦的IE8時代,所有的動畫都只能基於自己計算相關動畫屬性,開定時器setTimeout/setInterval輪詢動畫任務。

而肩負重任的HTML5,早已注意到了日益增強的動畫,隨着HTML5的降臨,帶來了強勁的CSS3動畫,本文主要探討:乘着CSS3的風,實現JS動畫——探索現代畫風的js動畫。

> 本文內容如下: > - CSS3動畫 > - 基於CSS3的動畫本質 > - 封裝基於CSS3的動畫API > - 事件處理 > - 結語 > - 參考和引用

JavaScript - 前端開發交流群:377786580

CSS3動畫

CSS3的動畫各種文章漫天飛已經講爛了,CSS3到目前為止總共新增了兩個動畫屬性:transitionanimation。這里只關注我們目前要用的的部分:transition。至於animation的部分請參考《MDN - 使用CSS動畫》。

CSS3讓動畫前所未有的簡單,下面的例子演示了transition,當點擊div.demo的時候,div.demo向右偏移200px:

        <style>
            .demo { background-color: #0094ff; width: 100px; height: 100px; position: absolute;left: 0; 
                    transition: left /*要執行動畫的屬性*/
                                linear /*動畫曲線*/
                                1s; /*動畫執行時間*/ }
        </style>
        <div class="demo" onclick="javascript: this.style.left = '200px';"></div>

上面一段transition的意思就是:

  • 指定動畫的屬性為left,對left所有的操作都會觸發動畫
  • 指定動畫的曲線(貝塞爾函數)
  • 指定動畫的執行時間

仔細看看代碼也就能發現,其實transitionbackground一樣也是簡寫屬性,是這幾個CSS3新增屬性的簡寫:

  • transition-property - 指定動畫的屬性,可以指定多個,用,號分隔
  • transition-timing-function - 指定動畫的曲線(貝塞爾曲線)
  • transition-duration - 指定動畫的執行時間
  • transition-delay - 指定動畫延遲時間

這兩段代碼意義相同:

            transition: left            /*要執行動畫的屬性*/
                        linear: ;       /*動畫曲線*/
                        1s;             /*動畫執行時間*/



            transition-property:left;           /*動畫屬性*/
            transition-timing-function:linear;  /*動畫曲線*/
            transition-duration:1s;             /*執行時間*/
            transition-delay:0;                 /*動畫延遲時間*/

效果如圖:
css-animation-demo

早期實現動畫比較麻煩,需要使用類似下面JS的原理來實現:

    <div class="demo" id="demo" style="left:0"></div>
    <script>
        var elem = document.getElementById('demo'),//獲取元素
            elemStyleSheet = elem.style,//元素的內聯樣式對象
            left = parseInt(elemStyleSheet.left),//獲取當前left
            targetLeft = 200,//目標left
            time = 13,//動畫每幀間隔
            offsetValue = targetLeft / parseInt(1000 / 13),//每幀偏移量
            intervalId,
            temp;//臨時變量
        
        elem.onclick = function () {
            intervalId = setInterval(function () {
                //追加偏移量
                temp = parseInt(elemStyleSheet.left) + offsetValue;
                elemStyleSheet.left = temp + 'px';

                if (temp >= targetLeft)//完成動畫
                    clearInterval(intervalId);
            }, time);
        };
    </script>

效果和上面的css3實現的一樣。大體意思就是計算出動畫的幀數、每幀間隔、每幀動畫的偏移量,然后開個定時器一直重復執行,直到動畫完成。具體高能版實現可以參閱jQuery.animate

這里需要注意:早期的動畫都是基於定時器setTimeout/setInterval來輪詢動畫任務,它們本身的模型就不是為了動畫而打造的,實現動畫的性能上實在堪憂,所以現代瀏覽器都部署了新的API requestAnimationFrame來彌補setTimeout/setInterval在動畫方面天生的表現力不足。

近期jQuery發布了jQuery3.0 預覽版,就使用了requestAnimationFrame來完成動畫。

 

基於CSS3的動畫本質

transition可以驅動(作為動畫)的屬性太多太多,例如:

  • 位置:left、top、right、bottom
  • CSS3的transform變形和z軸偏移,參閱CSS3 Transform
  • 透明度:opacity
  • 寬高:height/width
  • 顏色(color)
  • 邊框(border)
  • 邊距(margin/padding)
  • 等等等等

在支持CSS3的情況下,如果我們想執行動畫,本質上都可以使用transition來驅動。因為transition已經封裝好了動畫的行為,我們只需要指定transition需要的一些關鍵屬性值即可。
所以這個動畫的實現,本質上就是一個給DOM賦上CSS3的屬性transition,然而我們早已就看透了一切。

 

封裝基於CSS3的動畫API

既然CSS3自身就已經實現了相關動畫屬性,那么封裝API這種事情就變得十分簡單了,拿我們最初的例子來說,可以使用如下js代碼:

        <style>
            .demo { background-color: #0094ff; width: 100px; height: 100px; position: absolute; }
        </style>
        <div class="demo" id="demo"></div>
        <script>
            var elem = document.getElementById('demo'),
                elemStyleSheet = elem.style;//元素的內聯樣式對象
            //賦上transition
            elemStyleSheet.cssText = 'left:0; transition: left linear 1s;';
            elem.onclick = function () {
                elemStyleSheet.left = '200px';
            }
        </script>

核心代碼其實就2行,附上transition和關鍵屬性即可。是不是突然覺得動畫真是so easy~~~

程序是要有健壯性的,既然我們發現了這么個"天大的秘密",是不是封裝成API以后給自己、給基友使用更好呢?看起來就覺得很高大上一樣,那我們來封裝下API吧。

最簡短的封裝就是把transition的四個屬性封裝傳遞進來就可以了:

    var animate = function (elem, propertys, ease, duration, delay) {
        var cssText = [],
            props = [];
        for (var name in propertys) {
            props.push(name);//提取要執行動畫的屬性

            //提取動畫目標樣式
            cssText.push(name + ':' +
                    //如果是number,則追加px單位
                    (typeof propertys[name] === 'number' ? propertys[name] + 'px' : propertys[name]));
        }

        //添加transition樣式
        cssText.push('transition-property:' + props.join(''));
        cssText.push('transition-timing-function:' + (ease || 'linear'));
        cssText.push('transition-duration:' + (duration || 300) + 's')
        cssText.push('transition-delay:' + (delay || 0));

        //添加元素樣式
        elem.style.cssText += ';' + cssText.join(';');
    };

大體意思就是把transition-*的屬性通過外面的參數傳遞進來,然后我們做下拼接的處理,然后給元素新增上樣式屬性就可以了。
代碼到了這里,我們來看看成果,畢竟我們僅使用了12行代碼就完成了JS的動畫。戳這里查看運行demo

 

事件處理

到了這里,我們會發現其實API和jQuery.animate很像很像,喲吼,看起來很不錯的樣子,等等,好像還缺了點什么。
仔細想想,我們還缺少一個重要的東西:動畫結束事件
我們往往有很多的需要的任務都是在動畫結束事件里完成的,但是我們怎么能沒有動畫結束這么重要的事件呢,別着急,國外那群搞瀏覽器的,也已經為大家討論出了結果(當然也還有其他動畫事件):
使用transition的動畫,提供一個動畫結束的事件:onTransitionEnd

我們再在剛才的demo下追加一行添加onTransitionEnd事件的代碼:

    //動畫結束事件
    elem.addEventListener('transitionend', function () {
        alert('動畫結束了!!!');
    });

戳這里查看運行demo

我們再來看看onTransitionEnd事件的兼容性:
onTransitionEnd

嗯,其實有些瀏覽器很早以前的實現都是私有實現,這里為了防止出現意外,還是嗅探一下瀏覽器吧,針對瀏覽器的私有實現,我們綁定私有事件。
當然我們應該考慮的更周全一點,既然都已經嗅探了私有事件,我們一同嗅探出私有屬性吧,防止有些瀏覽器不支持標准的CSS但是私有實現了transition

    var testElem = document.createElement('div'),
        //各大瀏覽器私有屬性:transitionProperty、webkitTransitionProperty、transitionProperty、oTransitionProperty、msTransitionProperty
        vendors = { '': '', 'Webkit': 'webkit', 'Moz': '', 'O': 'o', 'ms': 'ms' },
         /*
             https://github.com/madrobby/zepto/pull/742
             firefox從未支持過mozTransitionEnd或MozTransitionEnd,firefox一直支持標准的事件transitionend
         */
        normalizeEvent = function (name) {
            return eventPrefix ? eventPrefix + name : name.toLowerCase();
        },
        //私有css前綴
        cssPrefix = null,
        //私有事件前綴
        eventPrefix = null,
        //私有事件
        onTransitionEnd = null;

    for (var name in vendors) {
        //嗅探特性
        if (testElem.style[(name ? name + 'T' : 't') + 'ransitionProperty'] !== undefined) {
            cssPrefix = name ? '-' + name.toLowerCase() + '-' : name;
            eventPrefix = vendors[name];
            onTransitionEnd = normalizeEvent('TransitionEnd');
            break;
        }
    }

 

最后,我們整理一下代碼,把這些私有屬性的嗅探和之前的代碼進行融合,同時做一些優雅降級的處理。一個輕量級的,基於CSS3的JS動畫就這么實現了。

 

(function (window) {
    var Support = {
        cssPrefix: null,
        eventPrefix: null,
        onTransitionEnd: null
    },
    testElem = document.createElement('div'),
    //transitionProperty、webkitTransitionProperty、transitionDuration、oTransitionDuration、msTransitionProperty
    vendors = { '': '', 'Webkit': 'webkit', 'Moz': '', 'O': 'o', 'ms': 'ms' },
    /*
        https://github.com/madrobby/zepto/pull/742
        firefox從未支持過mozTransitionEnd或MozTransitionEnd,firefox一直支持標准的事件transitionend
    */
    normalizeEvent = function (name) {
        return Support.eventPrefix ? Support.eventPrefix + name : name.toLowerCase();
    };
    Object.keys(vendors).some(function (name) {
        var eventPrefix = vendors[name];
        //嗅探特性
        if (testElem.style[(name ? name + 'T' : 't') + 'ransitionProperty'] !== undefined) {
            Support.cssPrefix = name ? '-' + name.toLowerCase() + '-' : name;
            Support.eventPrefix = vendors[eventPrefix];
            Support.onTransitionEnd = normalizeEvent('TransitionEnd');
            return true;
        }
    })
    //動畫結束事件
    var onTransitionEnd = Support.onTransitionEnd !== null ?
        //animationEnd從android 4.1支持
         function (el, callback, time) {
             //支持transition
             var onEndCallbackFn = function (e) {
                 if (typeof e !== 'undefined') {
                     if (e.target !== e.currentTarget) return;//防止冒泡
                 }
                 this.removeEventListener(Support.onTransitionEnd, onEndCallbackFn);
                 callback.call(el);
             };
             el.addEventListener(Support.onTransitionEnd, onEndCallbackFn);
         } : function (el, callback, time) {
             //不支持就使用setTimeout
             setTimeout(function () {
                 callback.call(el);
             }, time);
         };
    if (Support.cssPrefix == null) {
        Support.cssPrefix = '';
        Support.eventPrefix = '';
    }

    //動畫
    var animatePrototypes = {
        transitionProperty: Support.cssPrefix + 'transition-property',
        transitionDuration: Support.cssPrefix + 'transition-duration',
        transitionDelay: Support.cssPrefix + 'transition-delay',
        transitionTiming: Support.cssPrefix + 'transition-timing-function'
    };

    /**
    * 動畫
    * animate(elem, properties, duration)
    * animate(elem, properties, duration, delay)
    * animate(elem, properties, duration, ease)
    * animate(elem, properties, duration, callback, delay)
    * animate(elem, properties, duration, ease, callback, delay)
    * @param {int} elem - 要執行動畫的元素
    * @param {function|string} properties - 動畫執行的目標屬性,為String則表示是animation-name,為object則是transition-property
    * @param {int} duration - 動畫執行時間(ms)
    * @param {string} [ease = linear] - 動畫線性
    * @param {function} [callback = null] - 動畫執行完成的回調函數
    * @param {int} [delay = 0] - 動畫延遲(s)
    * @returns {null}
    */
    var animate = function (elem, properties, duration, ease, callback, delay) {
        //修正參數支持重載
        if (typeof ease === 'function') {
            //重載
            delay = callback;
            callback = ease;
            ease = null;
        }
        if (ease > 0) {
            delay = ease;
            ease = null;
        };
        var cssProperties = [],
            cssValues = {},
            transformValues = '',
            eventCallback,
            cssStr = [],
            value;
        Object.keys(properties).forEach(function (name) {
            value = properties[name];
            cssValues[name] = typeof value === 'number' ? value + 'px' : value;
            cssProperties.push(name);
        });
        //填補transition樣式
        cssValues[animatePrototypes.transitionProperty] = cssProperties.join(', ');
        cssValues[animatePrototypes.transitionDuration] = duration + 's';
        cssValues[animatePrototypes.transitionDelay] = (delay || 0) + 's';
        cssValues[animatePrototypes.transitionTiming] = (ease || 'linear');

        if (callback) {
            onTransitionEnd(elem, callback, duration);
        }
        //設置樣式
        for (var key in cssValues) {
            cssStr.push(key + ':' + cssValues[key]);
        }

        //聰明的同學,想想為什么?
        setTimeout(function () {
            elem.style.cssText += ';' + cssStr.join(';');
        }, 0);
    };

    window.animate = animate;
})(window);

運行效果如圖:
animate-demo

戳這里查看完整demo

結語

HTML5和CSS3為前端注入了巨大的力量,CSS3強大的動畫現在各大網站隨處可見,而現在web前端的技術更新又異常的快,前端的技術也層出不窮,各種新的卓越的概念各種涌現。
但是我們在這一片繁華的背后,也應該深刻的思考技術的根基。在眼花繚亂的技術背后,看破本質,務必時刻掌控住你的代碼和思想。
這篇文章主要分析了transition,其實這里還可以兼容CSS另外一個動畫:animation,聰明的童鞋,思考思考如何去做?
再談點自己:最近很忙,離職了又入職。重構組里的前端開發,各種評估框架,完善基礎類庫,組里准備給前端中間駕一層Node,自己又在開發自己的個人博客網站,忙的各種沒時間更新。
另外,招人,要求只有一點:對代碼有追求。

JavaScript - 前端開發交流群:377786580

參考和引用

zepto.js - fx模塊源碼
Modernizr.js源碼
swipe.js源碼

作者:linkFly
聲明:嘿!你都拷走上面那么一大段了,我覺得你應該也不介意順便拷走這一小段,希望你能夠在每一次的引用中都保留這一段聲明,尊重作者的辛勤勞動成果,本文與博客園共享。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM