現現在,許多頁面上均有一些動畫效果。適當的動畫效果能夠在必定程度上提升頁面的美觀度,具備提示效果的動畫能夠加強頁面的易用性。javascript
實現頁面動畫的途徑通常有兩種。css
咱們今天只講第一種實現方式。html
所謂的動畫,就是經過一些列的運動造成的動的畫面。在網頁中,咱們能夠經過不斷的改變元素的css值,來達到動的效果。 java
JavaScript的動畫用的最多的3個api就是setInterval()、setTimeout()和requestAnimationFrame()算法
聽說,普通人眼能看到1/24秒,就是說1秒至少24幀,每次移位間隔須要小於1000/24=41.7毫秒,也就說setInterval要每隔至少40毫秒執行一次,通常地,咱們採用10毫秒,固然間隔時間越短,客戶端執行計算次數就越多,若是你code計算量大則能夠適當調長些。api
像setTimeout、setInterval同樣,requestAnimationFrame是一個全局函數。調用requestAnimationFrame後,它會要求瀏覽器根據本身的頻率進行一次重繪,它接收一個回調函數做爲參數,在即將開始的瀏覽器重繪時,會調用這個函數,並會給這個函數傳入調用回調函數時的時間做爲參數。因爲requestAnimationFrame的功效只是一次性的,因此若想達到動畫效果,則必須接二連三的調用requestAnimationFrame,就像咱們使用setTimeout來實現動畫所作的那樣。requestAnimationFrame函數會返回一個資源標識符,能夠把它做爲參數傳入cancelAnimationFrame函數來取消requestAnimationFrame的回調。跟setTimeout的clearTimeout很類似啊。瀏覽器
能夠這麼說,requestAnimationFrame是setTimeout的性能加強版。異步
有一點須要注意的是,requestAnimationFrame不能自行指定函數運行頻率,而是有瀏覽器決定刷新頻率。因此這個更能達到瀏覽器所能達到的最佳動畫效果了。函數
這個方法不是全部的瀏覽器都兼容。oop
div{ width: 100px; height: 100px; background-color: #f00; position: absolute; }
<div id="div"></div>
<script type="text/javascript"> var id; function step() { var temp = div.offsetLeft + 2; div.style.left = temp + "px"; //和setTimeout同樣,要手動調用才能實現連續動畫。 id = window.requestAnimationFrame(step); //返回值是一個id,能夠經過這個id來取消 } id = window.requestAnimationFrame(step); //取消回調函數 window.cancelAnimationFrame(step); </script>
咱們知道JavaScript試單線程的產物,兩個函數就是利用了插入代碼的方式實現了僞異步,和AJAX的原理其實是同樣的。
console.log("1"); setTimeout(function(){ console.log("3") },0); console.log("2"); //輸出結果是什麼? //1 2 3
function fn() { setTimeout(function(){ console.log('can you see me?'); },1000); while(true) {} } //輸出結果是什麼?
function step() { var temp = div.offsetLeft + 2; div.style.left = temp + "px"; window.requestAnimationFrame(step); for (var i = 0; i < 50000; i++) { console.log("再牛逼的定時器也得等到我執行完才能執行") } } window.requestAnimationFrame(step);
動畫實際上是 「位移」關於「時間」的函數:s=f(t)
因此不應採用增量的方式來執行動畫,爲了更精確的控制動畫,更合適的方法是將 動畫與時間關聯起來
function startAnimation() { var startTime = Date.now(); requestAnimationFrame(function change() { var current = Date.now() - startTime; console.log("動畫已執行時間" + current); requestAnimationFrame(change); }); } startAnimation();
動畫一般狀況下有終止時間,若是是循環動畫,咱們也能夠看作特殊的——當動畫達到終止時間以後,從新開始動畫。所以,咱們能夠將動畫時間歸一(Normalize)表示:
//duration 是動畫執行時間 isLoop是否爲循環執行。 function startAnimation(duration, isLoop){ var startTime = Date.now(); requestAnimationFrame(function change(){ // 動畫已經用去的時間佔總時間的比值 var p = (Date.now() - startTime) / duration; if(p >= 1.0){ if(isLoop){ // 若是是循環執行,則開啓下一個循環週期。而且把開始時間改爲上個週期的結束時間 startTime += duration; p -= 1.0; //動畫進度初始化 }else{ p = 1.0; //若是不是循環,則把時間進度至爲 1.0 表示動畫執行結束 } } console.log("動畫已執行進度", p); if(p < 1.0){ //若是小於1.0表示動畫還誒有值完畢,繼續執行動畫。 requestAnimationFrame(change); } }); }
示例1:用時間控制動畫週期精確到2s中
block.addEventListener("click", function() { var self = this, startTime = Date.now(), duration = 2000; setInterval(function() { var p = (Date.now() - startTime) / duration; // 時間已經完成了2000的比例,則360度也是進行了這麼個比例。 self.style.transform = "rotate(" + (360 * p) + "deg)"; }, 100); });
示例2:讓滑塊在2秒內向右勻速移動600px
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 600, duration = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); self.style.transform = "translateX(" + (distance * p) +"px)"; if(p < 1.0) { requestAnimationFrame(step); } }); });
用時間來控制進度
$$
s = S*p
$$
加速度恆定,速度從0開始隨時間增長而均勻增長。
勻加速公式:大寫S:要移動的總距離 p:歸一化的時間進度
$$
s = S*p^2
$$
// 2s中內勻加速運動2000px block.addEventListener("click", function() { var self = this, startTime = Date.now(), distance = 1000, duration = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / duration); self.style.transform = "translateX(" + (distance * p * p) + "px)"; if(p < 1.0) requestAnimationFrame(step); }); });
勻減速運動公式:
$$
s=Sp(2-p)
$$
//2s中使用速度從最大勻減速到0運動1000px block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 1000, duration = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); self.style.transform = "translateX(" + (distance * p * (2-p)) +"px)"; if(p < 1.0) requestAnimationFrame(step); }); });
課堂練習:小球的自由落體運動
勻速水平運動和自由落體運動的組合。
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), disX = 1000, disY = 1000, duration = Math.sqrt(2 * disY / 10 / 9.8) * 1000; // 落到地面須要的時間 單位ms //假設10px是1米,disY = 100米 requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); var tx = disX * p; //水平方向是勻速運動 var ty = disY * p * p; //垂直方向是勻加速運動 self.style.transform = "translate(" + tx + "px" + "," + ty +"px)"; if(p < 1.0) requestAnimationFrame(step); }); });
正弦運動:x方向勻速,垂直方向是時間t的正弦函數
block.addEventListener("click", function(){ var self = this, startTime = Date.now(), distance = 800, duration = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / duration); var ty = distance * Math.sin(2 * Math.PI * p); var tx = 2 * distance * p; self.style.transform = "translate(" + tx + "px," + ty + "px)"; if(p < 1.0) requestAnimationFrame(step); }); });
圓周運動公式:
$$
x = R.sin(2πp) , y = R.cos(2πp)
$$
block.addEventListener("click", function() { var self = this, startTime = Date.now(), r = 100, duration = 2000; requestAnimationFrame(function step() { var p = Math.min(1.0, (Date.now() - startTime) / duration); var tx = r * Math.sin(2 * Math.PI * p), ty = -r * Math.cos(2 * Math.PI * p); self.style.transform = "translate(" + tx + "px," + ty + "px)"; requestAnimationFrame(step); }); });
對於一些比較複雜的變化,算法也比較複雜,就要用到動畫算子。動畫算子 是一個函數,能夠把進度轉化成另一個值。其實也就是一種算法。
咱們總結一下上面的各種動畫,發現它們是很是類似的,勻速運動、勻加速運動、勻減速運動、圓周運動惟一的區別僅僅在於位移方程:
勻速運動:
$$
s = S *p
$$
勻加速運動:
$$
s = S *p^2
$$
$$
s = Sp(2-p)
$$
$$
x = Rsin(2PI*p)
$$
$$
y = Rcos(2PI*p)
$$
咱們把共同的部分 S 或R 去掉,獲得一個關於 p 的方程 ,這個方程咱們稱爲動畫的算子(easing),它決定了動畫的性質。
$$
e = p
$$
$$
e = p^2
$$
$$
e = p*(2 - p)
$$
$$
e = sin(2PIp)
$$
$$
e = cos(2 * PI * p)
$$
一些經常使用的動畫算子
var pow = Math.pow, BACK_CONST = 1.70158; // t指的的是動畫進度 前面的p Easing = { // 勻速運動 linear: function (t) { return t; }, // 加速運動 easeIn: function (t) { return t * t; }, // 減速運動 easeOut: function (t) { return (2 - t) * t; }, //先加速後減速 easeBoth: function (t) { return (t *= 2) < 1 ? .5 * t * t : .5 * (1 - (--t) * (t - 2)); }, // 4次方加速 easeInStrong: function (t) { return t * t * t * t; }, // 4次方法的減速 easeOutStrong: function (t) { return 1 - (--t) * t * t * t; }, // 先加速後減速,加速和減速的都比較劇烈 easeBothStrong: function (t) { return (t *= 2) < 1 ? .5 * t * t * t * t : .5 * (2 - (t -= 2) * t * t * t); }, // easeOutQuart: function (t) { return -(Math.pow((t - 1), 4) - 1) }, // 指數變化 加減速 easeInOutExpo: function (t) { if (t === 0) return 0; if (t === 1) return 1; if ((t /= 0.5) < 1) return 0.5 *Math.pow(2, 10 * (t - 1)); return 0.5 * (-Math.pow(2, - 10 * --t) + 2); }, //指數式減速 easeOutExpo: function (t) { return (t === 1) ? 1 : -Math.pow(2, - 10 * t) + 1; }, // 先回彈,再加速 swingFrom: function (t) { return t * t * ((BACK_CONST + 1) * t - BACK_CONST); }, // 多走一段,再慢慢的回彈 swingTo: function (t) { return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1; }, //彈跳 bounce: function (t) { var s = 7.5625, r; if (t < (1 / 2.75)) { r = s * t * t; } else if (t < (2 / 2.75)) { r = s * (t -= (1.5 / 2.75)) * t + .75; } else if (t < (2.5 / 2.75)) { r = s * (t -= (2.25 / 2.75)) * t + .9375; } else { r = s * (t -= (2.625 / 2.75)) * t + .984375; } return r; } };
爲了實現更加複雜的動畫,咱們能夠將動畫進行 簡易 的封裝,要進行封裝,咱們先要抽象出動畫相關的要素:
動畫時長:T = duration
動畫進程:p = t/T
easing: e = f(p) (動畫算子:p的函數 )
動畫方程: x = g(e) y = g(e) (動畫的位移相對於動畫算子的方程)
動畫生命週期:開始、進程中、結束
<script type="text/javascript"> /* 參數1:動畫的執行時間 參數2:動畫執行的時候的回調函數(動畫執行的要乾的事情) 參數3:動畫算子. 若是沒有傳入動畫算子,則默認使用勻速算子 */ function Animator(duration, progress, easing) { this.duration = duration; this.progress = progress; this.easing = easing || function(p) { return p }; } Animator.prototype = { /*開始動畫的方法, 參數:一個布爾值 true表示動畫不循環執行。 */ start: function(finished) { /*動畫開始時間*/ var startTime = Date.now(); /*動畫執行時間*/ var duration = this.duration, self = this; /*定義動畫執行函數*/ requestAnimationFrame(function step() { /*獲得動畫執行進度*/ var p = (Date.now() - startTime) / duration; /*是否執行下一幀動畫*/ var next = true; /*判斷動畫進度是否完成*/ if(p < 1.0) { self.progress(self.easing(p), p); //執行動畫回調函數,並傳入動畫算子的結果和動畫進度。 } else { if(finished){ //判斷是否中止動畫。若是是true表明中止動畫。 next = false; }else{ startTime = Date.now(); } } // 若是next是true執行下一幀動畫 if(next) requestAnimationFrame(step); }); } }; block.onclick = function () { var self = this; new Animator(2000, function (p) { self.style.top = 500 * p +"px"; },Easing.bounce).start(false); }
有時候,咱們不但要支持元素的運動,還須要改變元素的外觀,好比飛翔的小鳥須要扇動翅膀,這類動畫咱們能夠用逐幀動畫來實現:
<style type="text/css"> .sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(http://res.h5jun.com/matrix/8PQEganHkhynPxk-CUyDcJEk.png);} .bird0 {width:86px; height:60px; background-position: -178px -2px} .bird1 {width:86px; height:60px; background-position: -90px -2px} .bird2 {width:86px; height:60px; background-position: -2px -2px} #bird{ position: absolute; left: 100px; top: 100px; zoom: 0.5; } </style> <div id="bird" class="sprite bird1"></div> <script> var i = 0; setInterval(function(){ bird.className = "sprite " + "bird" + ((i++) % 3); }, 1000/10); </script>