現如今,許多頁面上均有一些動畫效果。適當的動畫效果可以在一定程度上提高頁面的美觀度,具有提示效果的動畫可以增強頁面的易用性。
實現頁面動畫的途徑一般有兩種。
- 一種是通過操作JavaScript間接操作CSS樣式,每隔一段時間更新一次;
- 一種是直接通過CSS定義動畫。第二種方法在CSS3成熟之后被廣泛采用。
我們今天只講第一種實現方式。
一、JavaScript中動畫原理
所謂的動畫,就是通過一些列的運動形成的動的畫面。在網頁中,我們可以通過不斷的改變元素的css值,來達到動的效果。
JavaScript的動畫用的最多的3個api就是setInterval()、setTimeout()和requestAnimationFrame()
據說,普通人眼能看到1/24秒,就是說1秒至少24幀,每次移位間隔需要小於1000/24=41.7毫秒,也就說setInterval要每隔至少40毫秒執行一次,一般地,我們采用10毫秒,當然間隔時間越短,客戶端執行計算次數就越多,如果你code計算量大則可以適當調長些。
1.1 setTimeout()和setInterval ()
1.2 requestAnimationFrame(回調函數)
像setTimeout、setInterval一樣,requestAnimationFrame是一個全局函數。調用requestAnimationFrame后,它會要求瀏覽器根據自己的頻率進行一次重繪,它接收一個回調函數作為參數,在即將開始的瀏覽器重繪時,會調用這個函數,並會給這個函數傳入調用回調函數時的時間作為參數。由於requestAnimationFrame的功效只是一次性的,所以若想達到動畫效果,則必須連續不斷的調用requestAnimationFrame,就像我們使用setTimeout來實現動畫所做的那樣。requestAnimationFrame函數會返回一個資源標識符,可以把它作為參數傳入cancelAnimationFrame函數來取消requestAnimationFrame的回調。跟setTimeout的clearTimeout很相似啊。
可以這么說,requestAnimationFrame是setTimeout的性能增強版。
有一點需要注意的是,requestAnimationFrame不能自行指定函數運行頻率,而是有瀏覽器決定刷新頻率。所以這個更能達到瀏覽器所能達到的最佳動畫效果了。
這個方法不是所有的瀏覽器都兼容。
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>
1.3 簡單動畫的問題
1.3.1 setTimeout和setInterval深入理解
我們知道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) {}
}
//輸出結果是什么?
1.3.2 簡單動畫的變慢問題
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);
1.4 使用動畫的正確姿勢
動畫其實是 “位移”關於“時間”的函數: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);
}
});
});
二、常見動畫效果實現
2.1 勻速水平運動
用時間來控制進度
$$
s = S*p
$$
2.2 勻加速(減速)運動
加速度恆定,速度從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);
});
});
課堂練習:小球的自由落體運動
2.3 水平拋物運動
勻速水平運動和自由落體運動的組合。
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);
});
});
2.4 正弦曲線運動
正弦運動: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);
});
});
2.5 圓周運動
圓周運動公式:
$$
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);
});
});
三、動畫算子(easing)
對於一些比較復雜的變化,算法也比較復雜,就要用到動畫算子。動畫算子 是一個函數,可以把進度轉化成另外一個值。其實也就是一種算法。
我們總結一下上面的各類動畫,發現它們是非常相似的,勻速運動、勻加速運動、勻減速運動、圓周運動唯一的區別僅僅在於位移方程:
-
勻速運動:
$$
s = S *p
$$ -
勻加速運動:
$$
s = S *p^2
$$
- 勻減速運動:
$$
s = Sp(2-p)
$$
- 圓周運動x軸:
$$
x = Rsin(2PI*p)
$$
- 圓周運動y軸:
$$
y = Rcos(2PI*p)
$$
我們把共同的部分 S 或R 去掉,得到一個關於 p 的方程 ,這個方程我們稱為動畫的算子(easing),它決定了動畫的性質。
- 勻速算子:
$$
e = p
$$
- 勻加速算子:
$$
e = p^2
$$
- 勻減速算子:
$$
e = p*(2 - p)
$$
- 圓周算子x軸:
$$
e = sin(2PIp)
$$
- 圓周算子y軸:
$$
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>