本文系筆者學習原生javascript動效的筆記。內容基於某非著名培訓機構的視頻教程。並重新做了歸類整理。刪除了一些過時的內容。並重做了GIF圖,加上了自己的一些分析。 一. 運動學基礎 引子:從左到右的div 點擊按鈕,一個紅色div從左向右運動! 運動的要素在於一個絕對定位的主體,一個定時器。和 ...
本文系筆者學習原生javascript動效的筆記。內容基於某非著名培訓機構的視頻教程。並重新做了歸類整理。刪除了一些過時的內容。並重做了GIF圖,加上了自己的一些分析。
一. 運動學基礎
引子:從左到右的div
<input id="btn1" type="button" value="開始運動!" onclick="startMove();" />
<div id="div1" style="width:100px;height:100px;background:red;position:absolute;left:0;"></div>
點擊按鈕,一個紅色div從左向右運動!
運動的要素在於一個絕對定位的主體,一個定時器。和一個不斷變化的量度值(比如說style.left)。
因此js代碼應該是:
function startMove(){
var oBtn=document.getElementById('btn1');
var oDiv=document.getElementById('div1');
setInterval(function(){
oDiv.style.left=oDiv.offsetLeft+4+'px';
},30);
}
第一個startMove就完成了。
改進
當前這個運動有太多的需要改進。
改進1:運動終點
用戶像看到的效果是基於就實際生活經驗,而不是太空運動狀態。運動應該有個終點。
比如說,我讓運動到屏幕偏移左側300px停下來:
只需要判斷offsetLeft到300px就停掉定時器:
function startMove(){
var oBtn=document.getElementById('btn1');
var oDiv=document.getElementById('div1');
setInterval(function(){
if(oDiv.offsetLeft==300){
clearInterval();
}else{
oDiv.style.left=oDiv.offsetLeft+4+'px';
}
},30);
}
好了。那麼到了目標位置,這個定時器就停下來了。
改進二: 初步封裝
為了讓運動函數可擴展,我把這個速度值4改為一個變數iSpeed.
function startMove(){
var iSpeed=4;
var oBtn=document.getElementById('btn1');
var oDiv=document.getElementById('div1');
setInterval(function(){
if(oDiv.offsetLeft==300){
clearInterval();
}else{
oDiv.style.left=oDiv.offsetLeft+iSpeed+'px';
}
},30);
}
改進三:永不停下的定時器
定義了速度值,給startMove提供了很大的靈活性,以至於當我onclick="startMove(7);
由於不能整除。定時器跳過了300px的運動終點。
暫時這樣處理吧:把判斷條件改為:
if(oDiv.offsetLeft>=300)
不管怎樣,都能停下來了。事實上這隻是暫時的處理方法。
改進四:事件累加
如果我連續點擊button,就會觸發多次startMove。div的運動速度會成倍加快。不可接受。
解決方法很簡單,在下次觸發定時器時,不管三七二十一先把div身上的定時器清了再說。為了方便,我定義了一個全局變數timer
,startMove執行時,他將作為定時器的載體:
所以最終完善的版本是這樣:
var timer=null;
function startMove(){
var oDiv=document.getElementById('div1');
clearInterval(timer);
timer=setInterval(function (){
var iSpeed=5;
if(oDiv.offsetLeft>=300){
clearInterval(timer);
}else{
oDiv.style.left=oDiv.offsetLeft+iSpeed+'px';
}
}, 30);
}
小結:
正如初中物理課的流程——現在我們已經擁有了一個勻速直線運動
的框架。麻雀雖小,五臟俱全。
- 一個運動終點
- 一個運動的速度值
- 停止事件疊加的機制
但是封裝星還是不足,那還是來做一個框架吧。
二. 運動框架
為了說明這個框架的靠譜和種種不靠譜,先看一個案例
案例2.1:左側伸縮導航條
項目需求:有一個導航條縮在屏幕左側。當滑鼠懸停,出現在左邊。
佈局:
<div id="div1">
<span>分享到</span>
</div>
#div1{
background: #ccc;
width: 100px;height: 200px;
position: absolute;
top:100px;left: -100px;
}
span{
width: 20px;height: 60px;
line-height: 20px;
text-align:center;
position: absolute;
left: 100px;
top: 70px;
background: red;
}
- 這裡有兩個事件(滑鼠一入一齣),要改的就是div的絕對定位的left值。事件對象應該用span的父級容器div,而不是span!
- 運動對象(obj):
#div1
- 運動終點(iTarget):#div1的寬度(0和-100px),
根據運動框架,可以很快寫出js。
window.onload=function(){
var oDiv=document.getElementById('div1');
var oSpan=oDiv.getElementsByTagName('span')[0];
var timer=null;
oDiv.onmouseover=function(){
clearInterval(timer);
timer=setInterval(function(){
if(oDiv.offsetLeft>=0){
clearInterval(timer);
}else{
oDiv.style.left=oDiv.offsetLeft+5+'px';
}
},30);
};
oDiv.onmouseout=function(){
clearInterval(timer);
timer=setInterval(function(){
if(oDiv.offsetLeft<=-100){
clearInterval(timer);
}else{
oDiv.style.left=oDiv.offsetLeft-5+'px';
}
},30);
};
};
效果已經出來了。但是重覆代碼太多。
因此,有三個可以作為參數:運動終點,運動對象,速度是可設置的。根據這個可以封裝函數了。
var timer=null;
function startMove(obj,iTarget,iSpeed){
clearInterval(timer);
timer=setInterval(function(){
if(obj.offsetLeft==iTarget){
clearInterval(timer);
}else{
obj.style.left=obj.offsetLeft+iSpeed+'px';
}
},30);
}
在這個框架里,可以設置速度值,可以設置對象,也可以設置運動終點。在調用時:
window.onload=function(){
var oDiv=document.getElementById('div1');
oDiv.onmouseover=function(){
startMove(this,0,5);
};
oDiv.onmouseout=function(){
startMove(this,-100,-5);
};
};
看起來大大精簡了代碼冗餘。效果如下:
透明度的勻速運動框架
有了演算法,意味著網頁所有可見的量度都可以進行運算。在這裡把透明度加進運動框架中:
案例2.2 淡入淡出的圖片。
假設有一張圖片:
<img id="img1" src="images/1.jpg" />
預設透明度為0.3
#img1{
filter: alpha(opacity:30);/*相容萬惡的ie*/
opacity: 0.3;/*相容火狐。chrome*/
}
實現懸停後透明度為1!
【思路】如果我們不用任何運動框架,代碼應該是這樣的:
window.onload=function(){
var oImg=document.getElementById('img1');
var timer=null;
var alpha=30;
oImg.onmouseover=function(){
var iSpeed=5;
timer=setInterval(function(){
if(oImg.style.opacity==1){
clearInterval(timer);
}else{
alpha+=iSpeed;
oImg.style.opacity=(alpha)/100;
oImg.style.filter='alpha(opacity:'+alpha+')';
}
},30);
};
oImg.onmouseout=function(){
//console.log('heh');
clearInterval(timer);
var iSpeed=5;
timer=setInterval(function(){
if(oImg.style.opacity==0.3){
clearInterval(timer);
}else{
alpha-=iSpeed;
oImg.style.opacity=(alpha)/100;
oImg.style.filter='alpha(opacity:'+alpha+')';
}
},30);
};
};
好長好長。
在這個透明度運動中,oImg的運動終點是100和30;
所以這個透明度框架應該是:
var alpha=30;
var timer=null;
function startMoveOpacity(obj,iTarget,iSpeed){
clearInterval(timer);
timer=setInterval(function(){
if(obj.style.opacity==iTarget/100){
clearInterval(timer);
}else{
alpha+=iSpeed;
obj.style.filter='alpha(opacity:'+alpha+')';
obj.style.opacity=alpha/100;
}
},30);
}
在調用時應該是:
window.onload=function(){
var oImg=document.getElementById('img1');
oImg.onmouseover=function(){
startMoveOpacity(oImg,100,5);
};
oImg.onmouseout=function(){
startMoveOpacity(oImg,30,-5);
};
};
效果:
運動研究方法
回到點擊按鈕div從左往右運動的場景。併在300px處加上一條黑線:
<input id="btn1" type="button" value="開始運動!" onclick="startMove(300);" />
<div id="div1" style="width:100px;height:100px;background:red;position:absolute;left:500px;"></div>
<span style="width:1px;height:300px;background:black;left:300px;position:absolute;top:0;left:300px;"></span>
<textarea></textarea>
在勻速運動框架內做速度分析。用一個textarea記錄每次執行函數的運動速度。
這個div在300px線的右邊,所以速度應該為負。
var timer=null;
function startMove(obj,iTarget,iSpeed){
clearInterval(timer);
var oTxt=document.getElementsByTagName('textarea')[0];
timer=setInterval(function(){
if(obj.offsetLeft==iTarget){
clearInterval(timer);
}else{
obj.style.left=obj.offsetLeft+iSpeed+'px';
oTxt.value+=iSpeed+'\n';//記錄速度!
}
},30);
}
window.onload=function(){
var oDiv=document.getElementById('div1');
var oBtn=document.getElementById('btn1');
oBtn.onclick=function(){
startMove(oDiv,300,-10);
};
};
在這個運動框架內做速度分析。用一個textarea記錄每次執行函數的運動速度。
運行:
速度值就打到了多行文本框上面。
對該數據做圖表分析:
勻速運動顯然是條水平線。好像沒什麼說的。
以後會藉助此方法進行運動分析。
三.緩衝運動
所謂緩衝運動就是緩動。比如手風琴效果——逐級變慢——越接近終點,速度越小。距離越大速度越大。相對於勻速運動,緩動更能接近實際效果。
定義速度
每當我看到運動方向相關的資料時,都會想起這個句子:
I find the great thing in this world is not so much where we stand, as in what direction we are moving. - Oliver Wendell Holmes
“我認為現代世界里最重要的不是我們所處的位置,而是我們前進的方向。”——老奧利弗·溫德爾·霍姆斯
對於緩動框架,我們似乎不必再設置iSpeed參數,直接給他設定速度值計算方法就行了。至於運動方向是正是負,全部有iTarget決定!
在框架中,iTarget-iSpeed就是到右邊終點的距離。讓它和速度關聯起來:
var timer=null;
function startMove(obj,iTarget){
clearInterval(timer);
var oTxt=document.getElementsByTagName('textarea')[0];
timer=setInterval(function(){
var iSpeed=(iTarget-obj.offsetLeft)/8;//關鍵語句:實現了速動緩停。正負完全交給演算法決定!
if(obj.offsetLeft==iTarget){
clearInterval(timer);
}else{
obj.style.left=obj.offsetLeft+iSpeed+'px';
oTxt.value+=iSpeed+'\n';//運動分析
}
},30);
}
window.onload=function(){
var oDiv=document.getElementById('div1');
var oBtn=document.getElementById('btn1');
oBtn.onclick=function(){
startMove(oDiv,300);
};
};
看看效果:
效果似乎不錯,不必定義速度方向的正負,就可以實現div向指定的目標運動。但是發現了一個很大的問題:沒到300px線就停下來了。
運動分析:
這是之前速度框架留下的一個天坑。
取整問題
我們發現速度應該是無限接近於0.但是瀏覽器只識別整數單位的px,結果速度到了-0.5px,就再也無法前進了。由於無法到達運動終點,實際上定時器還沒停。
1像素是電腦所能處理的最小單位。計算出來的offsetLeft值在瀏覽器解析後,全部經過了整處理。200.9px,轉化為200px。
所以停下來累積誤差為297px。ispeed必須做人工取整處理。
取整方法:
Math.cell()向上取整。
Math.ceil(-12.5)//12
Math.ceil(12.3)//13
Math.floor()向下取整。
Math.floor(12.9)//12
那麼究竟是向上取整還是向下取整呢?由於取整的特殊性。應當明確:
- 如果運動速度方向為負值,向上取整
速度為正:向下取整。
所以iSpeed的表達式可以這麼寫:var iSpeed=(iTarget-obj.offsetLeft)/8;//實現了速動緩停。 if(iSpeed>0){ iSpeed=Math.ceil(iSpeed); }else { iSpeed=Math.floor(iSpeed); }
看看效果:
當速度為-1時定時器已經停了。
而且左右自如:
迄今為止我們的運動框架是這樣:
var timer=null;
function startMove(obj,iTarget){
clearInterval(timer);
//var oTxt=document.getElementsByTagName('textarea')[0];
timer=setInterval(function(){
var iSpeed=(iTarget-obj.offsetLeft)/8;//實現了速動緩停。
if(iSpeed>0){
iSpeed=Math.ceil(iSpeed);
}else {
iSpeed=Math.floor(iSpeed);
}
if(obj.offsetLeft==iTarget){
clearInterval(timer);
}else{
obj.style.left=obj.offsetLeft+iSpeed+'px';
//oTxt.value+=iSpeed+'\n';//運動分析
}
},30);
}
解決無法判斷終點問題
現在到了填坑的時候了。勻速運動解決速度值不准的問題。
function startMove(obj,iTarget){
clearInterval(timer);
timer=setInterval(function (){
//var iSpeed=(iTarget-obj.offsetLeft)/8;
var iSpeed=iSpeed>0?iSpeed=7:iSpeed=-7;
if(obj.offsetLeft==iTarget){
clearInterval(timer);
}else{
obj.style.left=obj.offsetLeft+iSpeed+'px';
}
}, 30);
}
window.onload=function(){
var oDiv=document.getElementById('div1');
var oBtn=document.getElementById('btn1');
oBtn.onclick=function(){
startMove(oDiv,300);
};
};
如果你讓div停在300px,絕對不停。這時候可以在定時器里做一個判斷:
怎麼辦呢?給定一個範圍,進入到範圍附近時,把距離人工改為0
用到Math.abs()函數。這就是絕對值。
判斷語句為:
if(Math.abs(obj.offsetLeft-iTarget)<Math.abs(iSpeed)){//認為到達終點,再讓物體撞線。
obj.style.left=iTarget+'px';
}
緩動應用:上下滑動的側邊欄
在ie6時代,右邊居中固定的“廣告視窗”是可以通過js來實現的。當用戶滾動滾動條,顯然通過運動的方式使廣告窗居中是最合適的。
在做滾動之前應該瞭解這張圖
如果你無從下手,那麼再次回想之前的話:重要的是運動目標!
滾動目標位置(iTarget)=設備高度的一半-滾動物體高度的一半+滾動高度+滾動距離
在這裡需要註意一個問題:偏移值不能是小數。所以需要用parseInt取整。
結構和樣式:
<div id="div1"></div>
<textarea id="txt1" style="position:fixed;top:0;"/><!--運動分析-->
css
#div1{
width: 100px;height: 100px;
background: red;
position: absolute;
right: 0;
}
body{
height: 5000px;
}/*滾動空間*/
我們把startMove改寫為下麵的樣子:
var timer=null;
function startMove(obj,iTarget){
clearInterval(timer);
timer=setInterval(function(){
var iSpeed=(iTarget-obj.offsetTop)/8;
iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
if(obj.offsetTop==iTarget){
clearInterval(timer);
}else{
obj.style.top=iSpeed+obj.offsetTop+'px';
}
txt1.value=obj.offsetTop+' iTarget:'+iTarget;//運動分析:
},30);
}
其實就是把offsetLfet
改成了offsetTop
,而已。
接下來調用就是
window.onscroll=function(){
var oDiv=document.getElementById('div1');
var scrolltop=document.documentElement.scrollTop||document.body.scrollTop;//滾動距離
var target=(document.documentElement.clientHeight-oDiv.offsetHeight)/2+scrolltop;//滾動目標偏移(offsetTop)=設備高度的一半-滾動物體高度的一半+滾動高度+滾動距離
target=parseInt(target);
startMove(oDiv,target);
};
效果
四. 多物體運動框架
先看一個小案例:變寬的div
當滑鼠移入div,div由100變寬為300,移出滑鼠後消失。
樣式如下
<div id="div1" style="width:100px;height:100px;background:red;"></div>
只是框架的量度值變為offsetWidth就可以了。把框架默寫出來吧!
var timer=null;
function startMove(obj,iTarget){
clearInterval(timer);
timer=setInterval(function(){
var iSpeed=(iTarget-obj.offsetWidth)/8;
iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
if(iSpeed==iTarget){
clearInterval(timer);
}else{
obj.style.width=obj.offsetWidth+iSpeed+'px';
}
},30);
}
window.onload=function(){
var oDiv=document.getElementById('div1');
oDiv.onmouseover=function(){
startMove(this,300);
};
oDiv.onmouseout=function(){
startMove(this,100);
};
};
效果挺好,但錄製效果有卡頓:
擴展到多個div
假設我的頁面有五個div:
<div id="div1"></div>
<div id="div2"></div>
<div id="div3"></div>
<div id="div4"></div>
<div id="div5"></div>
樣式為:
div{
width: 100px;
height: 50px;
}
#div1{
background: red;
}
#div2{
background: yellow;
}
#div3{
background: tomato;
}
#div4{
background: pink;
}
#div5{
background: orange;
}
對5個div實現懸停變寬效果!
照著寫就是了:
window.onload=function(){
var aDiv=document.getElementsByTagName('div');
for(var i=0;i<aDiv.length;i++){
aDiv[i].onmouseover=function(){
startMove(this,300);
};
aDiv[i].onmouseout=function(){
startMove(this,100);
};
}
};
然而效果令人奔潰:
多個事件同時進行的時候,因為共用同一個定時器timer,各種事件頻繁發生。有的定時器就停在半路上了。
解決思路:把timer綁定到事件對象身上。(私有化),也趁早解決那個令人反感的全局變數timer
function startMove(obj,iTarget){
clearInterval(obj.timer);
obj.timer=setInterval(function(){
var iSpeed=(iTarget-obj.offsetWidth)/8;
iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
if(iSpeed==iTarget){
clearInterval(obj.timer);
}else{
obj.style.width=obj.offsetWidth+iSpeed+'px';
}
},30);
}
效果就正常了。反正定時器又不要錢,多搞幾個吧。
多個物體的淡入淡出:
同樣的把它運用到透明度上面去:
還是5個#div1-5——樣式:
div{
width: 100px;
height: 50px;
opacity: 0.3;
filter: opacity(0.3);
}
#div1{
background: red;
}
#div2{
background: yellow;
}
#div3{
background: tomato;
}
#div4{
background: pink;
}
#div5{
background: orange;
}
於是想當然地這麼寫:
var alpha=30;//初始化透明度!
function startMoveOpacity(obj,iTarget){
clearInterval(obj.timer);
obj.timer=setInterval(function(){
var iSpeed=(iTarget-alpha)/8;
iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
if(alpha==iTarget){
clearInterval(obj.timer);
}else{
alpha+=iSpeed;
obj.style.filter='alpha(opacity:'+alpha+')';
obj.style.opacity=alpha/100;
}
},30);
}
在這個例子中,定義了又一個全局變數alpha。alpha存在的特殊性,但所有的div都用這個alpha就亂了。
那麼就把alpha也私有了吧。反正又不要錢。
function startMoveOpacity(obj,iTarget){
clearInterval(obj.timer);
//obj.alpha=30;
obj.timer=setInterval(function(){
var iSpeed=(iTarget-obj.alpha)/8;
iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
if(obj.alpha==iTarget){
clearInterval(obj.timer);
}else{
obj.alpha+=iSpeed;
obj.style.