HTML5遊戲開發(十)
一、動畫
1、動畫循環
在Canvas中這病動畫效果很簡單:只需要在播放動畫時持續更新並繪製就行了。這種持續更新與重繪叫就動畫循環。
(1)通過requestAnimationFrame()方法讓瀏覽器來自行決定幀速率:
使用window.setInterval()或window.setTimeout()製作出的動畫,其效果可能並不如預期般流暢,而且還可能會佔用額外的資源。這是因爲setInterval()與setTimeout方法具有如下特徵:
==它們都是通過法,並不是專爲製作動畫而用的。
即使向其傳遞以毫秒爲單位的參數值,它們也達不到毫秒級的精確性。
沒有對調用動畫循環的機制作優化。
不考慮繪製動畫的最佳時機,而只會一味地以某個大致的時間時隔來調用動畫循環。==
(2)Ployfill
window.requestNextAnimationFrame =
(function () {
var originalWebkitRequestAnimationFrame = undefined,
wrapper = undefined,
callback = undefined,
geckoVersion = 0,
userAgent = navigator.userAgent,
index = 0,
self = this;
//chrome瀏覽器
if (window.webkitRequestAnimationFrame) {
wrapper = function (time) {
if (time === undefined) {
time = +new Date();
}
self.callback(time);
};
//原生
originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;
window.webkitRequestAnimationFrame = function (callback, element) {
self.callback = callback;
originalWebkitRequestAnimationFrame(wrapper, element);
}
}
//火狐瀏覽器
if (window.mozRequestAnimationFrame) {
index = userAgent.indexOf('rv:');
if (userAgent.indexOf('Gecko') != -1) {
geckoVersion = userAgent.substr(index + 3, 3);
if (geckoVersion === '2.0') {
//原生
window.mozRequestAnimationFrame = undefined;
}
}
}
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
//回調函數
function (callback, element) {
var start,
finish;
window.setTimeout( function () {
start = +new Date();
callback(start);
finish = +new Date();
self.timeout = 1000 / 60 - (finish - start);
}, self.timeout);
};
}
)();
2.幀速率計算
&ems;動畫是由一系列叫做幀的圖像組成的,這些圖像的顯示頻率就叫做幀速率。通常來說,有必要計算一下幀速率。在基於時間的運動效果是地,可能會用到動畫的幀速率,或是有時爲了保證動畫能夠播放得足夠流暢,我們也需要知道幀速率。 #### (1) 基本功能
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>幀速率計算</title>
<style>
body {
background: #dddddd;
}
#canvas {
position: absolute;
left: 0px;
top: 20px;
margin: 20px;
background: #ffffff;
border: thin inset rgba(100, 150, 230, 0.5);
}
#controls {
margin-top: 10px;
margin-left: 15px;
}
</style>
</head>
<body>
<div id='controls'>
<input id='animateButton' type='button' value='開始' />
</div>
<canvas id='canvas' width='500' height='300'>
</canvas>
<script src='js/Frame.js'>
</script>
</body>
</html>
JS腳本
var canvas = document.querySelector('#canvas'),
context = canvas.getContext('2d'),
paused = true,
//定義三個小球數據
discs = [{
x: 150,
y: 50,
lastX: 150,
lastY: 50,
velocityX: 3.2,
velocityY: 3.5,
radius: 25,
innerColor: 'rgba(0,255,255,0.3)',
middleColor: 'rgba(0,255,255,0.9)',
outerColor: 'rgba(0,255,255,0.3)',
strokeStyle: 'slateblue',
}, {
x: 75,
y: 200,
lastX: 75,
lastY: 200,
velocityX: 2.2,
velocityY: 2.5,
radius: 25,
innerColor: 'rgba(225,225,225,0.1)',
middleColor: 'rgba(225,225,225,0.9)',
outerColor: 'rgba(225,225,225,0.3)',
strokeStyle: 'gray'
}, {
x: 100,
y: 150,
lastX: 150,
lastY: 50,
velocityX: 1.2,
velocityY: 1.5,
radius: 25,
innerColor: 'orange',
middleColor: 'yellow',
outerColor: 'gold',
shadowColor: 'rgba(255,0,0,0.7)',
strokeStyle: 'orange'
}, ],
//小球數量
numDiscs = discs.length,
//時間
lastTime = 0,
//
lastFpsUpdateTime = 0,
frameCount = 0,
animateButton = document.querySelector('#animateButton');
//擦除背景
function eraseBackground() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
//更新小球位置,進行座標計算
function update() {
var i = numDiscs,
disc = null;
while(i--) {
disc = discs[i];
// 座標計算
if(disc.x + disc.velocityX + disc.radius > context.canvas.width ||
disc.x + disc.velocityX - disc.radius < 0)
//橫座標
disc.velocityX = -disc.velocityX;
if(disc.y + disc.velocityY + disc.radius > context.canvas.height ||
disc.y + disc.velocityY - disc.radius < 0)
//縱座標
disc.velocityY = -disc.velocityY;
// 默認爲相加 每次移動距離
disc.x += disc.velocityX;
disc.y += disc.velocityY;
}
}
//繪製小球
function drawDisc(disc) {
var gradient = context.createRadialGradient(disc.x, disc.y, 0,
disc.x, disc.y, disc.radius);
gradient.addColorStop(0.3, disc.innerColor);
gradient.addColorStop(0.7, disc.middleColor);
gradient.addColorStop(1.0, disc.outerColor);
context.save();
context.beginPath();
context.arc(disc.x, disc.y, disc.radius, 0, Math.PI * 2, false);
context.clip();
context.fillStyle = gradient;
context.strokeStyle = disc.strokeStyle;
context.lineWidth = 2;
context.fill();
context.stroke();
context.restore();
}
//繪製小球
function draw() {
var i = numDiscs,
disc;
i = numDiscs;
while(i--) {
disc = discs[i];
//小球繪製
drawDisc(disc);
disc.lastX = disc.x;
disc.lastY = disc.y;
}
if(frameCount === 100) {
frameCount = -1;
}
if(frameCount !== -1 && frameCount < 100) {
frameCount++;
}
}
//計算幀率
function calculateFps() {
//每秒的播發幀數
var now = (+new Date),
fps = 1000 / (now - lastTime);
lastTime = now;
return fps;
}
//動畫
function animate() {
//獲取當前時間
var now = (+new Date),
fps = 0;
//啓動動畫
if(!paused) {
//擦除重繪
eraseBackground();
//更新
update();
//繪製
draw();
//計算幀率
fps = calculateFps();
//如果當前時間與最後更新時間大於1000
if(now - lastFpsUpdateTime > 1000) {
lastFpsUpdateTime = now;
//最後幀率
lastFpsUpdate = fps;
}
//設置幀率顯示樣式
context.fillStyle = 'cornflowerblue';
//四捨五入取值
context.fillText(lastFpsUpdate.toFixed() + ' fps', 45, 50);
}
//chrome可以使用標準的requestAnimationFrame
window.requestAnimationFrame(animate);
}
//加載就調用
window.requestAnimationFrame(animate);
(2)事件處理
//-------------------------------事件處理
canvas.onclick = function(e) {
paused = paused ? false : true;
};
animateButton.onclick = function(e) {
paused = paused ? false : true;
if(paused) {
animateButton.value = '開始';
} else {
animateButton.value = '停止';
}
};
context.canvas.width = canvas.width;
context.canvas.height = canvas.height;
//設定字體
context.font = '48px Helvetica';
顯示效果:
3.恢復動畫背景
從來質上來看,恢復動畫背景無外乎三種方法:
1、將所有內容擦除,並重繪製;
2、僅繪內容發生變化的區域;
3、從離屏緩衝區中將內容發生變化的那部分背景圖像恢復到屏幕上。
4.利用剪輯區域來重繪背景
//重繪背景
function drawBackground(){
context.fillStyle="#99CC99";
context.fillRect(0,0,canvas.width,canvas.height);
}
//重繪小球區域
function drawDiscBackground(disc){
//保存原來
context.save();
context.beginPath();
context.arc(disc.lastX,disc.lastY,disc.radius+1,0,Math.PI*2,false);
//剪輯
context.clip();
//擦除背景
eraseBackground();
//重繪背景
drawBackground();
//恢復
context.restore();
}
//繪製小球
function draw() {
var i = numDiscs,
disc;
i = numDiscs;
while(i--) {
drawDiscBackground(discs[i]);
disc = discs[i];
//小球繪製
drawDisc(disc);
disc.lastX = disc.x;
disc.lastY = disc.y;
}
if(frameCount === 100) {
frameCount = -1;
}
if(frameCount !== -1 && frameCount < 100) {
frameCount++;
}
}
5.利用圖塊複製技術重繪背景
將整個背景一次性地繪製到離屏canvas中,稍後從離屏canvas中只將修復動畫背景所需的那一塊圖像複製到屏幕上即可。
//創建離屏對象
var offscreenCanvas = document.createElement('canvas');
offscreenContext = offscreenCanvas.getContext('2d');
//離屏的大小設定
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
//將原畫布複製給離屏
function drawBackground() {
//將當前畫布給離屏
offscreenContext.drawImage(canvas, 0, 0,
canvas.width, canvas.height);
}
//重繪小球區域
function drawDiscBackground(context, disc) {
var x = disc.lastX,
y = disc.lastY,
r = disc.radius,
w = r * 2,
h = r * 2;
//保存原來
context.save();
context.beginPath();
context.arc(x, y, r + 1, 0, Math.PI * 2, false);
//剪輯
context.clip();
//擦除背景
eraseBackground();
//重繪背景--使用離屏技術
context.drawImage(offscreenCanvas,x-r,y-r,w,h,x-r,y-r,w,h);
//恢復
context.restore();
}
默認情況下:瀏覽器使用了雙緩衝技術繪製動畫
6.基於時間的運動
例:每秒移動100個像素,那麼每毫秒移動的像素爲:100/1000=0.1px,計算每一幀移動多少像素:假設這一幀對於上一幀過去的時間爲200ms,那麼這一幀移動的像素即爲:200ms*0.1px=20px
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>幀速率計算</title>
<style>
body {
background: #dddddd;
}
#canvas {
position: absolute;
left: 0px;
top: 20px;
margin: 20px;
background: #ffffff;
border: thin inset rgba(100, 150, 230, 0.5);
}
#controls {
margin-top: 10px;
margin-left: 15px;
}
</style>
</head>
<body>
<div id='controls'>
<input id='animateButton' type='button' value='開始' />
</div>
<canvas id='canvas' width='500' height='300'>
</canvas>
<script src='js/TimeFrame.js'>
</script>
</body>
</html>
JS腳本
var canvas = document.querySelector('#canvas'),
context = canvas.getContext('2d'),
paused = true,
discs = [{
x: 215,
y: 175,
lastX: 150,
lastY: 75,
velocityX: -3.2,
velocityY: 4.5,
radius: 25,
innerColor: 'rgba(255,0,0,1.0)',
middleColor: 'rgba(255,0,0,0.7)',
outerColor: 'rgba(255,0,0,0.5)',
shadowColor: 'rgba(255,0,0,0.7)',
strokeStyle: 'orange'
}, {
x: 250,
y: 150,
lastX: 150,
lastY: 80,
velocityX: 2.2,
velocityY: -4.5,
radius: 25,
innerColor: 'rgba(255,255,0,1)',
middleColor: 'rgba(255,255,0,0.7)',
outerColor: 'rgba(255,255,0,0.5)',
shadowColor: 'rgba(175,175,175,0.7)',
strokeStyle: 'gray',
},
{
x: 150,
y: 75,
lastX: 50,
lastY: 150,
velocityX: 2.2,
velocityY: -1.5,
radius: 25,
innerColor: 'rgba(100,145,230,1.0)',
middleColor: 'rgba(100,145,230,0.7)',
outerColor: 'rgba(100,145,230,0.5)',
shadowColor: 'rgba(100,145,230,0.8)',
strokeStyle: 'blue'
},
{
x: 100,
y: 100,
lastX: 150,
lastY: 75,
velocityX: -5.9,//X軸速度
velocityY: -0.2,//Y軸速度
radius: 25,
innerColor: 'rgba(255,0,0,1.0)',
middleColor: 'rgba(255,0,0,0.7)',
outerColor: 'rgba(255,0,0,0.5)',
shadowColor: 'rgba(255,0,0,0.7)',
strokeStyle: 'orange'
},
],
numDiscs = discs.length,
startTime = 0, //開始時間
lastTime = 0, //結束時間
elapsedTime = 0, //運行時間,每幀時間差
fps = 0, //幀率
lastFpsUpdate = {
time: 0,
value: 0
},
animateButton = document.querySelector('#animateButton');
//擦除背影
function eraseBackground() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
//更新小球
function update() {
var i = numDiscs,
disc = null;
while(i--) {
disc = discs[i];
if(disc.x + disc.velocityX + disc.radius > context.canvas.width ||
disc.x + disc.velocityX - disc.radius < 0)
disc.velocityX = -disc.velocityX;
if(disc.y + disc.velocityY + disc.radius > context.canvas.height ||
disc.y + disc.velocityY - disc.radius < 0)
disc.velocityY = -disc.velocityY;
disc.x += disc.velocityX;
disc.y += disc.velocityY;
}
}
//基於時間更新
function updateTimeBased(time) {
var i = numDiscs,
disc = null;
if(fps == 0)
return;
while(i--) {
disc = discs[i];
deltaX = disc.velocityX
//elapsedTime爲每幀時間差,速度(像素)X每幀秒數,移動座標像素
deltaX = disc.velocityX * (elapsedTime / 1000);
deltaY = disc.velocityY * (elapsedTime / 1000);
if(disc.x + deltaX + disc.radius > context.canvas.width ||
disc.x + deltaX - disc.radius < 0) {
disc.velocityX = -disc.velocityX;
deltaX = -deltaX;
}
if(disc.y + deltaY + disc.radius > context.canvas.height ||
disc.y + deltaY - disc.radius < 0) {
disc.velocityY = -disc.velocityY;
deltaY = -deltaY;
}
disc.x = disc.x + deltaX;
disc.y = disc.y + deltaY;
}
}
//繪製小球
function draw() {
var i = numDiscs,
disc = discs[i];
while(i--) {
disc = discs[i];
//
gradient = context.createRadialGradient(disc.x, disc.y, 0,
disc.x, disc.y, disc.radius);
gradient.addColorStop(0.3, disc.innerColor);
gradient.addColorStop(0.5, disc.middleColor);
gradient.addColorStop(1.0, disc.outerColor);
context.beginPath();
context.arc(disc.x, disc.y, disc.radius, 0, Math.PI * 2, false);
context.save();
context.fillStyle = gradient;
context.strokeStyle = disc.strokeStyle;
context.fill();
context.stroke();
context.restore();
}
}
//計算幀率
function calculateFps(now) {
//第一幀所用時間單位爲毫秒
elapsedTime = now - lastTime;
//計算幀率,每一秒多少幀
fps = 1000 / elapsedTime;
lastTime = now;
}
//更新幀率
function updateFps() {
var now = (+new Date);
//計算幀率
calculateFps(now);
if(now - startTime < 2000) {
return;
}
if(now - lastFpsUpdate.time > 1000) {
lastFpsUpdate.time = now;
lastFpsUpdate.value = fps;
}
if(!paused) {
context.fillStyle = 'cornflowerblue';
context.fillText(lastFpsUpdate.value.toFixed() + ' fps', 50, 48);
}
}
//基於時間運行動畫
function animate(time) {
if(time === undefined) {
time = +new Date;
}
//console.log(time)
if(!paused) {
//擦除重繪背景
eraseBackground();
//drawBackground();
//基於時間
updateTimeBased(time);
//非基於時間
//update();
draw();
}
//更新幀率
updateFps();
}
//按鈕事件
animateButton.addEventListener('click', function(e) {
paused = paused ? false : true;
if(paused) {
animateButton.value = '開始';
} else {
for(var i = 0; i < discs.length; ++i) {
discs[i].velocityX *= 20;
discs[i].velocityY *= 50;
}
animateButton.value = '停止';
}
});
context.font = '36px Helvetica';
//獲取開始是境
startTime = +new Date;
//計算每一幀所用時長爲多少毫秒,以下爲每秒60幀動畫
setInterval(animate, 1000 / 60);
6.視差動畫
視差動畫,就是動畫製作者讓各個動畫圖層以不同的速度滾動,這樣就實現了視差效果。近的移動快,遠的移動慢。
(1)初始設置
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>視差動畫</title>
<style>
body {
background: #dddddd;
}
#canvas {
position: absolute;
left: 20px;
top: 30px;
background: #ffffff;
margin-left: 10px;
margin-top: 10px;
box-shadow: 4px 4px 8px rgba(0,0,0,0.5);
}
margin-left: 15px;
}
</style>
</head>
<body>
<input id='animateButton' type='button' value='動畫'/>
<canvas id='canvas' width='1000' height='440'>
</canvas>
<script src='js/requestNextAnimationFrame.js'></script>
<script src='js/Parallax.js'></script>
</body>
</html>
JS腳本
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d'),
animateButton = document.getElementById('animateButton'),
//樹
tree = new Image(),
//大數
nearTree = new Image(),
//草
grass = new Image(),
grass2 = new Image(),
//天空
sky = new Image(),
paused = true,
lastTime = 0,
lastFpsUpdate = { time: 0, value: 0 },
fps=60,
//天空偏移
skyOffset = 0,
//草地偏移
grassOffset = 0,
//樹偏移
treeOffset = 0,
//大樹偏移
nearTreeOffset = 0,
//速度
TREE_VELOCITY = 20,
FAST_TREE_VELOCITY = 40,
SKY_VELOCITY = 8,
GRASS_VELOCITY = 75;
//----------------------初始設置
context.font = '48px Helvetica';
tree.src = 'img/smalltree.png';
nearTree.src = 'img/tree-twotrunks.png';
grass.src = 'img/grass.png';
grass2.src = 'img/grass2.png';
sky.src = 'img/sky.png';
//擦除背景
function erase() {
context.clearRect(0,0,canvas.width,canvas.height);
}
//繪製圖像
function draw() {
//天空
skyOffset = skyOffset < canvas.width ?
skyOffset + SKY_VELOCITY/fps : 0;
//草地
grassOffset = grassOffset < canvas.width ?
grassOffset + GRASS_VELOCITY/fps : 0;
//樹
treeOffset = treeOffset < canvas.width ?
treeOffset + TREE_VELOCITY/fps : 0;
//大樹
nearTreeOffset = nearTreeOffset < canvas.width ?
nearTreeOffset + FAST_TREE_VELOCITY/fps : 0;
//保存畫布
context.save();
//平移
context.translate(-skyOffset, 0);
//繪製兩個天空,實現背景滾動
context.drawImage(sky, 0, 0);
context.drawImage(sky, sky.width-2, 0);
//恢復畫布
context.restore();
//保存畫布
context.save();
context.translate(-treeOffset, 0);
context.drawImage(tree, 100, 240);
context.drawImage(tree, 1100, 240);
context.drawImage(tree, 400, 240);
context.drawImage(tree, 1400, 240);
context.drawImage(tree, 700, 240);
context.drawImage(tree, 1700, 240);
//恢復畫布
context.restore();
context.save();
context.translate(-nearTreeOffset, 0);
context.drawImage(nearTree, 250, 220);
context.drawImage(nearTree, 1250, 220);
context.drawImage(nearTree, 800, 220);
context.drawImage(nearTree, 1800, 220);
context.restore();
context.save();
context.translate(-grassOffset, 0);
context.drawImage(grass, 0, canvas.height-grass.height);
context.drawImage(grass, grass.width-5,
canvas.height-grass.height);
context.drawImage(grass2, 0, canvas.height-grass2.height);
context.drawImage(grass2, grass2.width,
canvas.height-grass2.height);
context.restore();
}
(2)動畫實現
//--------------------幀率
//幀率
function calculateFps(now) {
var fps = 1000 / (now - lastTime);
lastTime = now;
return fps;
}
//動畫
function animate(now) {
if (now === undefined) {
now = +new Date;
}
fps = calculateFps(now);
if (!paused) {
erase();
draw();
}
requestNextAnimationFrame(animate);
}
animateButton.onclick = function (e) {
paused = paused ? false : true;
if (paused) {
animateButton.value = '動畫';
}
else {
animateButton.value = '暫停';
}
};
//加載繪製
sky.onload = function (e) {
draw();
};
//動畫加載
requestNextAnimationFrame(animate);
顯示效果: