1. 程式人生 > 實用技巧 >JavaScript動畫基礎:canvas繪製簡單動畫

JavaScript動畫基礎:canvas繪製簡單動畫

動畫是將靜止的畫面變為動態的藝術.實現由靜止到動態,主要是靠人眼的視覺殘留效應。視覺殘留也叫視覺暫留現象,物體在快速運動時, 當人眼所看到的影像消失後,人眼仍能繼續保留其影像0.1~0.4秒左右的影象,這種現象被稱為視覺暫留現象。利用人的這種視覺生理特性可製作出具有高度想象力和表現力的動畫影片。

電影的拍攝和放映就是視覺殘留效應的具體應用。

大家可能看過組成電影的實際膠片。從表面上看,它們像一堆畫面串在一條塑料膠片上。每一個畫面稱為一幀,代表電影中的一個時間片段。這些幀的內容總比前一幀有稍微的變化,這樣,當電影膠片在投影機上放映時就產生了運動的錯覺:每一幀都很短並且很快被另一個幀所代替,這樣就產生了運動。

通過迴圈繪製各幀的影象就可以實現動畫的效果。

在Canvas畫布中製作動畫相對來說很簡單,實際上就是繪製幀(圖形或影象)、擦除、重繪的過程。也就是說,在Canvas中模擬一個動畫過程就是每隔一定時間繪製圖形並且清除圖形,通過定時迴圈操作實現。

1.定時迴圈操作的三個函式

對於動畫,需要在一段時間內渲染不同的幀,各幀間隔一定的時間在畫布中依次被繪製。為完成定時迴圈操作幀,可以利用etInterval()、setTimeout()和requestAnimationFrame()這三個函式之一。

(1)setTimeout()方法。

setTimeout() 方法是HTML DOM Window物件的一個方法,它用於在指定的毫秒數後呼叫函式或計算表示式。其呼叫格式為:

setTimeout(code,millisec);

其中,引數code表示要呼叫的函式或要執行的程式碼串,millisec表示在執行程式碼前需等待的毫秒數。

例如,setTimeout(“draw()”,1000)表示延時1秒後執行函式draw中的程式碼。

編寫如下的HTML檔案。

<!DOCTYPE html>

<html>

<head>

<title>setTimeout方法的應用</title>

</head>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var ctx = canvas.getContext('2d');

function draw(x,y,len,color)

{

ctx.fillStyle = color;

ctx.fillRect(x,y,len,len);

}

setTimeout("draw(10,10,100,'red')",1000);

setTimeout("draw(110,110,200,'blue')",5000);

</script>

</body>

</html>

在瀏覽器中開啟儲存這段HTML程式碼的html檔案,則等待1秒後,會繪製一個邊長為100的紅色正方形,再等待5秒,繪製一個邊長為200的藍色正方形。

通過這個例子可以知道:(1)setTimeout()方法可以用於延時;(2)setTimeout()方法只執行code一次。如果要多次呼叫,則需要讓code 自身再次呼叫 setTimeout()。

為產生動畫效果,顯然得讓setTimeout()方法多次執行。修改上面的HTML程式碼如下。

<!DOCTYPE html>

<html>

<head>

<title>setTimeout方法的應用</title>

</head>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var ctx = canvas.getContext('2d');

var i=0;

function move()

{

ctx.fillStyle = 'red';

ctx.fillRect(i,i,50,50);

i++;

if (i==350)

{

i=0;

ctx.clearRect(0,0,400,400);

}

setTimeout("move()",10);

}

move();

</script>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到一個簡單的箭頭伸出動畫,如圖1所示。

圖1 簡單的動畫

(2)setInterval() 方法。

setInterval()也是HTML DOM Window物件的一個方法,它可按照指定的週期(以毫秒計)來呼叫函式或計算表示式。其呼叫格式為:

setInterval(code,millisec);

其中,引數code表示要呼叫的函式或要執行的程式碼串, millisec表示週期性執行或呼叫 code 之間的時間間隔(以毫秒計)。

setInterval() 方法會不停地呼叫函式,直到 clearInterval() 被呼叫或視窗被關閉。由 setInterval() 返回的 ID 值可用作 clearInterval() 方法的引數。

clearInterval() 方法可取消由 setInterval() 設定的 timeout。其呼叫形式為:

clearInterval(id_of_setinterval);

其中引數id_of_setinterval必須是由 setInterval() 返回的 ID 值。

若用setInterval() 方法實現圖1所示的動畫,則編寫的HTML檔案如下。

<!DOCTYPE html>

<html>

<head>

<title>setInterval()方法的應用</title>

</head>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var ctx = canvas.getContext('2d');

var i=0;

function move()

{

ctx.fillStyle = 'red';

ctx.fillRect(i,i,50,50);

i++;

if (i==350)

{

i=0;

ctx.clearRect(0,0,400,400);

}

}

setInterval("move()",10);

</script>

</body>

</html>

(3)requestAnimationFrame()方法。

requestAnimationFrame是瀏覽器用於定時迴圈操作的一個介面,類似於setTimeout,主要用途是按幀對網頁進行重繪。

編寫動畫迴圈的關鍵是要知道延遲時間多長合適。一方面,迴圈間隔必須足夠短,這樣才能保證不同的動畫效果顯得更平滑流暢;另一方面,迴圈間隔還要足夠長,這樣才能保證瀏覽器有能力渲染產生的變化。大多數顯示器的重新整理頻率是60Hz,相當於每秒鐘重繪60次。大多數瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率,因為即使超過了這個頻率,使用者體驗也不會有提升。

因此,最平滑動畫的最佳迴圈間隔是1000ms/60,約等於17ms。以這個迴圈間隔重繪的動畫是平滑的,因為這個速度最接近瀏覽器的最高限速。為了適應17ms的迴圈間隔,多重動畫可能需要加以節制,以便不會完成得太快。

雖然setTimeout()方法和setInterval()方法均可完成定時迴圈操作,但setTimeout()和setInterval() 都不十分精確。為它們傳入的第二個引數millisec,實際上只是指定了把動畫程式碼新增到瀏覽器UI執行緒佇列以等待執行的時間。如果佇列前面已經加入了其他任務,那動畫程式碼就要等前面的任務執行完成後再執行。如果UI執行緒繁忙,比如忙於處理使用者操作,那麼即使把程式碼加入佇列也不會立即執行。

確定什麼時候繪製下一幀是保證動畫平滑的關鍵。然而,面對不十分精確的 setTimeout()和setInterval(),開發人員至今都沒有辦法確保瀏覽器按時繪製下一幀。因此,採用setTimeout()和setInterval(),即使優化了迴圈間隔,可能仍然只能接近想要的效果。

引入requestAnimationFrame()方法的目的是為了讓各種網頁動畫效果(DOM動畫、Canvas動畫、SVG動畫、WebGL動畫)能夠有一個統一的重新整理機制,從而節省系統資源,提高系統效能,改善視覺效果。程式碼中使用requestAnimationFrame()方法,就是告訴瀏覽器希望執行一個動畫,讓瀏覽器在下一個動畫幀安排一次網頁重繪。

requestAnimationFrame的優勢在於充分利用顯示器的重新整理機制,比較節省系統資源。顯示器有固定的重新整理頻率(60Hz或75Hz),也就是說,每秒最多隻能重繪60次或75次,requestAnimationFrame的基本思想就是與這個重新整理頻率保持同步,利用這個重新整理頻率進行頁面重繪。

不過有一點需要注意,requestAnimationFrame是在主執行緒上完成。這意味著,如果主執行緒非常繁忙,requestAnimationFrame的動畫效果會大打折扣。

requestAnimationFrame使用一個回撥函式作為引數。這個回撥函式會在瀏覽器重繪之前呼叫。其呼叫格式為:

requestID = window.requestAnimationFrame(callback);

目前,主流瀏覽器(Firefox 23 / IE 10 / Chrome / Safari)都支援這個方法。可以用下面的方法,檢查瀏覽器是否支援requestAnimationFrame。如果不支援,則自行模擬部署該方法。

window.requestAnimFrame = (function(){

return window.requestAnimationFrame ||

window.webkitRequestAnimationFrame ||

window.mozRequestAnimationFrame ||

window.oRequestAnimationFrame ||

window.msRequestAnimationFrame ||

function( callback ){

window.setTimeout(callback, 1000 / 60);

};

})();

上面的程式碼按照1秒鐘60次(大約每16.7毫秒一次),來模擬requestAnimationFrame。

與 setTimeout() 和 setInterval() 方法不同,requestAnimationFrame( )不需要呼叫者指定幀速率,瀏覽器會自行決定最佳的幀效率。也就是說瀏覽器頁面每次要重繪,就會通知requestAnimationFrame。如果瀏覽器繪製間隔是16.7ms,它就按這個間隔繪製;如果瀏覽器繪製間隔是10ms,它就按10ms繪製。這樣就不會存在過度繪製的問題,動畫不會丟幀。

另外,使用requestAnimationFrame()方法,一旦頁面不處於瀏覽器的當前標籤,就會自動停止重新整理。例如,頁面最小化了,頁面是不會進行重繪的,requestAnimationFrame自然也不會觸發(因為沒有通知)。頁面繪製全部停止,資源高效利用,節省了CPU、GPU和電力。

和setTimeout類似,requestAnimationFrame的回撥函式只能被呼叫一次,並不能被重複呼叫(這點和setInterval不同)。因此,使用requestAnimationFrame的時候,同樣需要反覆呼叫它。

由於setTimeout可以自定義呼叫時間, requestAnimationFrame的呼叫時間則是跟著系統的重新整理頻率走的,所以在實現動畫的時候,setTimeout比requestAnimationFrame更加靈活, requestAnimationFrame比setTimeout表現效果更加優秀。

若用requestAnimationFrame() 方法實現圖1所示的動畫,則編寫的HTML檔案如下。

<!DOCTYPE html>

<html>

<head>

<title>requestAnimationFrame方法的應用</title>

</head>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var ctx = canvas.getContext('2d');

var i=0;

function move()

{

ctx.fillStyle = 'blue';

ctx.fillRect(i,i,50,50);

i++;

if (i==350)

{

i=0;

ctx.clearRect(0,0,400,400);

}

requestAnimationFrame(move);

}

move();

</script>

</body>

</html>

2.繪製簡單圖形實現動畫

圖1的動畫就是從左上角座標位置(0,0)開始,繪製一個邊長為50的紅色正方形,之後每隔10毫秒後將左上角座標位置的水平和垂直座標均增加1,再繪製一個正方形,從而得到一個簡單的箭頭伸出動畫效果。

通過在畫布中繪製簡單圖形,達到時間間隔後,擦除(有時候也可暫時不擦除)前次繪製的圖形,重新繪製一個位置或大小略有變化的圖形,這樣就可得到動畫效果。

例1 向中心交匯的箭頭。

仿照圖1動畫思想略作變化,編寫如下的HTML程式碼。

<!DOCTYPE html>

<html>

<head>

<title>向中心交匯的箭頭</title>

<script type="text/javascript">

var i=0;

function draw(id)

{

var canvas = document.getElementById(id);

ctx = canvas.getContext('2d');

setInterval(painting,10);

}

function painting()

{

ctx.fillStyle = "green";

ctx.fillRect(i,i,10,10);

ctx.fillRect(400-i,400-i,10,10);

ctx.fillRect(i,400-i,10,10);

ctx.fillRect(400-i,i,10,10);

i++;

if (i==200)

{

ctx.clearRect(0,0,400,400);

i=0;

}

}

</script>

</head>

<body onload="draw('myCanvas')">

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到如圖2所示的動畫。

圖2 向中心交匯的箭頭

例2 逐層向裡繪製的圓。

<!DOCTYPE html>

<html>

<head>

<title>層層向內畫的圓</title>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;"></canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var context = canvas.getContext('2d');

var flag=1;

var i=0;

var r=180;

function animate() {

window.requestAnimationFrame(animate);

draw();

}

function draw() {

var dig=Math.PI/120;

var x = Math.sin(i*dig)*r+200;

var y = Math.cos(i*dig)*r+200;

context.fillStyle = flag ? 'rgb(10,255,255)' : 'rgb(255,100,0)';

context.beginPath();

context.arc(x, y, 3, 0, Math.PI*2, true);

context.closePath();

context.fill();

i++;

if (i>240) {

i=0;

r=r-20;

flag = !flag;

if (r<=0) {

context.clearRect(0,0,canvas.width,canvas.height);

r=180;

}

}

}

animate();

</script>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到如圖3所示的動畫。

圖3 層層向內畫的圓

3.通過圖形變換實現動畫效果

在Canvas中,可以繪製一個基本圖形,然後通過對這個基本圖形使用平移、縮放和旋轉等圖形變換的方法實現動畫效果。

例3 放大縮小的五角星。

<!DOCTYPE html>

<html>

<head>

<title>放大縮小的五角星</title>

</head>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var ctx = canvas.getContext('2d');

var x=200;

var y=200;

var radius=30;

var rot=0;

var dr=5;

function draw()

{

ctx.clearRect(0,0,canvas.width,canvas.height);

ctx.save();

ctx.translate(x,y);

ctx.rotate(rot/180*Math.PI);

ctx.scale(radius,radius);

ctx.beginPath();

for(var i=0;i<5;i++) // 繪製標準五角星

{

ctx.lineTo(Math.cos((18+i*72)/180*Math.PI),-Math.sin((18+i*72)/180*Math.PI));

ctx.lineTo(Math.cos((54+i*72)/180*Math.PI)*0.5,-Math.sin((54+i*72)/180*Math.PI)*0.5);

}

ctx.closePath();

ctx.fillStyle="red";

ctx.fill();

ctx.restore();

radius+=dr;

if (radius>200 || radius<30)

dr=-dr;

}

setInterval("draw()",60);

</script>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到如圖4所示的動畫。這個動畫效果的變化核心是語句“ctx.scale(radius,radius);”在起作用。

圖4 放大縮小的五角星

若將上面程式段中,radius固定取值120,再修改變化語句

radius+=dr;

if (radius>200 || radius<30)

dr=-dr;

rot=(rot+2)%360;

則五角星會進行旋轉,呈現出如圖5所示的動畫效果。

圖5 旋轉的五角星

4.遮罩動畫

利用Canvas API提供的裁切方法clip(),可以用來實現遮罩動畫。

例4 圖片圓形展開後收縮。

<!DOCTYPE html>

<head>

<title>圓形展開後收縮</title>

</head>

<body>

<canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var r=10;

var dr=5;

var canvas=document.getElementById('myCanvas');

var ctx=canvas.getContext('2d');

var image = new Image();

image.src = 'aaa.jpg';

image.onload=function(){

ctx.drawImage(image,0,0);

}

setInterval("draw()",100);

function draw()

{

ctx.clearRect(0,0,400,400);

ctx.save();

ctx.beginPath();

ctx.arc(200,200,r,0,Math.PI*2,true);

ctx.closePath();

ctx.fillStyle="white";

ctx.fill();

ctx.clip();

ctx.drawImage(image,0,0);

ctx.restore();

r=r+dr;

if (r>280) dr=-5;

else if (r<=0) dr=5;

}

</script>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到如圖6所示的動畫。

圖6 圓形展開後收縮

5.多個同類物體同時運動實現動畫

有時候設計動畫時,畫布中會有多個同類物體按各自的規律進行運動,這時將各物體抽象為物件陣列比較方便處理。

例3中我們通過圖形變換的方法實現了五角星的放大和旋轉。下面我們繪製60個五角星在畫布上進行移動的動畫效果。

例5 60個五角星隨機移動。

為了描述60個五角星,抽象一個星星物件Star。為該物件定義五角星中心位置座標(x,y)、五角星外接圓半徑radius、水平方向移動速度speedX、垂直方向移動速度speedY和五角星旋轉角度deg等6個屬性。具體定義如下:

function Star()

{

this.x = randomNum(30,canvas.width-30);

this.y = randomNum(30,canvas.height-30);

this.radius=randomNum(8,12);

this.speedX = randomNum(-5,5);

this.speedY=randomNum(-5,5);

this.deg = randomNum(0,180);

}

為五角星物件定義兩個方法,一個是update方法,更新五角星的座標位置(x,y)並進行邊界碰撞檢查;一個方法是draw方法,按屬性設定繪製出五角星。具體定義如下:

Star.prototype.update = function()

{

this.x += this.speedX;

this.y += this.speedY;

if (this.x-this.radius<=0)

{

this.speedX=-this.speedX;

this.x=this.radius;

}

if (this.x+this.radius>canvas.width)

{

this.speedX=-this.speedX;

this.x=canvas.width-this.radius;

}

if (this.y-this.radius<=0)

{

this.speedY=-this.speedY;

this.y=this.radius;

}

if (this.y+this.radius>canvas.height)

{

this.speedY=-this.speedY;

this.y=canvas.height-this.radius;

}

}

Star.prototype.draw = function()

{

ctx.beginPath();

for (var i = 0; i < 5; i ++)

{

ctx.lineTo( Math.cos( (18 + i*72 - this.deg)/180 * Math.PI) *this.radius + this.x,

-Math.sin( (18 + i*72 - this.deg)/180 * Math.PI) * this.radius + this.y)

ctx.lineTo( Math.cos( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2+ this.x,

-Math.sin( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2 + this.y)

}

ctx.closePath();

ctx.lineWidth = 3;

ctx.fillStyle = "#ff0000";

ctx.strokeStyle = "#ffff00";

ctx.lineJoin = "round";

ctx.fill();

ctx.stroke();

}

定義好五角星物件後,定義一個陣列stars,儲存60個五角星並設定動畫過程。編寫完整的HTML檔案如下。

<!DOCTYPE html>

<html>

<head>

<title>滿天都是小星星</title>

</head>

<body>

<canvas id="myCanvas" width="500" height="400" style="border:3px double #996633;">

</canvas>

<script type="text/javascript">

var canvas = document.getElementById('myCanvas');

var ctx = canvas.getContext('2d');

function Star()

{

this.x = randomNum(30,canvas.width-30);

this.y = randomNum(30,canvas.height-30);

this.radius=randomNum(8,12);

this.speedX = randomNum(-5,5);

this.speedY=randomNum(-5,5);

this.deg = randomNum(0,180);

}

Star.prototype.update = function()

{

this.x += this.speedX;

this.y += this.speedY;

if (this.x-this.radius<=0)

{

this.speedX=-this.speedX;

this.x=this.radius;

}

if (this.x+this.radius>canvas.width)

{

this.speedX=-this.speedX;

this.x=canvas.width-this.radius;

}

if (this.y-this.radius<=0)

{

this.speedY=-this.speedY;

this.y=this.radius;

}

if (this.y+this.radius>canvas.height)

{

this.speedY=-this.speedY;

this.y=canvas.height-this.radius;

}

}

Star.prototype.draw = function()

{

ctx.beginPath();

for (var i = 0; i < 5; i ++)

{

ctx.lineTo( Math.cos( (18 + i*72 - this.deg)/180 * Math.PI) *this.radius + this.x,

-Math.sin( (18 + i*72 - this.deg)/180 * Math.PI) * this.radius + this.y)

ctx.lineTo( Math.cos( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2+ this.x,

-Math.sin( (54 + i*72 - this.deg)/180 * Math.PI) * this.radius/2 + this.y)

}

ctx.closePath();

ctx.lineWidth = 1;

ctx.fillStyle = "white";

ctx.strokeStyle = "#ffff00";

ctx.lineJoin = "round";

ctx.fill();

ctx.stroke();

}

function randomNum(min,max)

{

return Math.floor(Math.random()*(max-min+1)+min);

}

var stars = [];

for (var i = 0; i < 60; i++)

{

stars.push(new Star());

if (stars[i].speedX==0 && stars[i].speedY==0)

stars[i].speedX=stars[i].speedY=1;

}

function move()

{

ctx.clearRect(0,0,canvas.width,canvas.height);

ctx.fillStyle="blue";

ctx.fillRect(0,0,canvas.width,canvas.height);

for (var i = 0; i <60; i++)

{

stars[i].draw();

stars[i].update();

}

}

setInterval("move()",10);

</script>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到如圖7所示的動畫。

圖7 星星在運動

例6 下雪了。

簡單模擬下雪場景,編寫如下的HTML檔案。在螢幕中最多有100片雪花,每片雪花繪製一個小圓表示,從畫布頂端開始往下落。由於動畫過程一直在迴圈,因此每當一片雪花落出畫布之外後,隨機為其賦予水平座標、置垂直座標置為0、並隨機設定其圓半徑和下落速度,表示這是一片新雪花。這樣,用一個具有100個元素的陣列即可儲存螢幕中下落雪花的資訊。

<!DOCTYPE html>

<html>

<head>

<title>下雪了</title>

</head>

<body>

<canvas id="myCanvas" width="300" height="300" style="border:3px double #996633;">

</canvas>

<script>

var canvas=document.getElementById('myCanvas');

var ctx=canvas.getContext('2d');

var particles = [];

function loop()

{

createParticles();

downParticles();

drawParticles();

window.requestAnimationFrame(loop);

}

window.requestAnimationFrame(loop);

function createParticles()

{

if(particles.length <100)

{

particles.push({

x: Math.random()*canvas.width,

y: 0,

speed: 2+Math.random()*3,

radius: 3+Math.random()*4,

});

}

}

function downParticles()

{

for(var i in particles)

{

var part = particles[i];

part.y += part.speed;

if(part.y > canvas.height)

{

part.x=Math.random()*canvas.width;

part.y=0;

part.speed=2+Math.random()*3;

part.radius=3+Math.random()*4;

}

}

}

function drawParticles()

{

ctx.fillStyle = "black";

ctx.fillRect(0,0,canvas.width,canvas.height);

for(var i in particles)

{

var part = particles[i];

ctx.beginPath();

ctx.arc(part.x,part.y, part.radius, 0, Math.PI*2);

ctx.closePath();

ctx.fillStyle = "white";

ctx.fill();

}

}

</script>

</body>

</html>

在瀏覽器中開啟包含這段HTML程式碼的html檔案,可以在瀏覽器視窗中看到如圖8所示的動畫。

圖8 下雪了