變形
在了解變形之前,先了解狀態(tài)。
- 狀態(tài)
canvas 的狀態(tài)就是當(dāng)前畫面應(yīng)用的所有樣式和變形的一個(gè)快照,用來操作這個(gè)狀態(tài)有兩個(gè)繪制復(fù)雜圖形時(shí)必不可少的方法:save()和restore() 。save()用來保存當(dāng)前狀態(tài),restore()用來恢復(fù)剛才保存的狀態(tài)。他們都可以多次調(diào)用。
ctx.fillStyle = 'black';
ctx.fillRect(20, 20, 150, 150);
ctx.save(); //保存當(dāng)前狀態(tài)
ctx.fillStyle= '#fff';
ctx.fillRect(45, 45, 100, 100);
ctx.restore(); //恢復(fù)到剛才保存的狀態(tài)
ctx.fillRect(70, 70, 50, 50);
- 位移
translate(x, y)
translate方法,它用來移動(dòng) canvas 和它的原點(diǎn)到一個(gè)不同的位置。
demo:
var ctx = document.getElementById('canvas').getContext('2d');
for(var i = 1; i< 4; i++) {
ctx.save(); //使用save方法保存狀態(tài),讓每次位移時(shí)都針對(duì)(0,0)移動(dòng)。
ctx.translate(100*i, 0);
ctx.fillRect(0, 50, 50, 50);
ctx.restore();
}
-
旋轉(zhuǎn)
rotate(angle)rotate它用于以原點(diǎn)為中心旋轉(zhuǎn): 旋轉(zhuǎn)的角度(angle),它是順時(shí)針方向的,以弧度為單位的值。
ctx.rotate(Math.PI * 2) //參照原點(diǎn)順時(shí)針旋轉(zhuǎn)360度
demo:
ctx.translate(75,75); //把原點(diǎn)移動(dòng)到(75, 75);
for (var i=1; i<6; i++){ // 從里到外一共6圈
ctx.save();
ctx.fillStyle = 'rgb('+(50*i)+','+(255-50*i)+',255)';
for (var j=0; j<i*6; j++){ // 每一圈有i*6個(gè)圓點(diǎn)
ctx.rotate(Math.PI*2/(i*6));
ctx.beginPath();
ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
ctx.fill();
}
ctx.restore();
}
- 縮放
scale
ctx.scale(x, y); //基于原點(diǎn)縮放,x、y是兩個(gè)軸的縮放倍數(shù)
demo:
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillStyle = 'red';
ctx.scale(0.8, 1.2);
ctx.beginPath();
ctx.arc(75, 75, 60, 0, Math.PI * 2);
ctx.fill();
- 變形
transforms
transform(m11, m12, m21, m22, dx, dy)
m11:水平方向的縮放
m12:水平方向的偏移
m21:豎直方向的偏移
m22:豎直方向的縮放
dx:水平方向的移動(dòng)
dy:豎直方向的移動(dòng)
動(dòng)畫
一幀一幀的來渲染這個(gè)元素,而且這個(gè)元素每一幀的位置都不一樣,我們的眼睛看到的就是動(dòng)畫了。實(shí)現(xiàn)起來也很方便,js提供了兩個(gè)方法:setTimeout 和setInterval都可以實(shí)現(xiàn),但是一個(gè)有逼格的程序員實(shí)現(xiàn)動(dòng)畫是不會(huì)用這兩個(gè)方法的,而是用requestAnimationFrame這個(gè)方法。有什么區(qū)別呢?下面簡(jiǎn)單做個(gè)比較。
-
setInterval(myFun, 10);意思是隔一毫秒執(zhí)行一個(gè)myFun函數(shù),但是這樣就有一個(gè)問題了,比如我myFun函數(shù)里面繪制的東西比較耗時(shí),而10ms之內(nèi)還沒有完全繪制出來,但是這段代碼強(qiáng)制1ms之后又開始繪制下一幀了,所以就會(huì)出現(xiàn)丟幀的問題;反之,如果時(shí)間設(shè)置太長(zhǎng),就會(huì)出現(xiàn)畫面不流暢、視覺卡頓的問題。 -
requestAnimationFrame(myFun);如果我們這樣寫,又是什么意思呢?意思是根據(jù)一定的時(shí)間間隔,會(huì)自動(dòng)執(zhí)行myFun函數(shù)來進(jìn)行繪制。這個(gè)”一定的時(shí)間間隔”就是根據(jù)瀏覽器的性能或者網(wǎng)速快慢來決定了,總之,它會(huì)保證你繪制完這一幀,才會(huì)繪制下一幀,保證性能的同時(shí),也保證動(dòng)畫的流暢
基本步驟:
- 清空 canvas
除非接下來要畫的內(nèi)容會(huì)完全充滿 canvas (例如背景圖),否則你需要清空所有。最簡(jiǎn)單的做法就是用 clearRect 方法。
- 保存 canvas 狀態(tài)
如果你要改變一些會(huì)改變 canvas 狀態(tài)的設(shè)置(樣式,變形之類的),又要在每畫一幀之時(shí)都是原始狀態(tài)的話,你需要先保存一下。
- 繪制動(dòng)畫圖形(animated shapes)
這一步才是重繪動(dòng)畫幀。
- 恢復(fù) canvas 狀態(tài)
如果已經(jīng)保存了 canvas 的狀態(tài),可以先恢復(fù)它,然后重繪下一幀。
高級(jí)動(dòng)畫
動(dòng)畫主要是requestAnimationFrame方法,現(xiàn)在我們來實(shí)現(xiàn)一個(gè)在畫布內(nèi)滾動(dòng)的實(shí)例
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var ball = { //小球?qū)傩?,原點(diǎn)位置,速度,半徑等。
x: 100,
y: 100,
vx: 4,
vy: 2,
radius: 20,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function draw() {
ctx.clearRect(0,0, canvas.width, canvas.height); //繪制之前清除整個(gè)畫布
ball.draw(); //在畫布中繪制小球
ball.x += ball.vx; //改變小球位置坐標(biāo)
ball.y += ball.vy; //改變小球位置坐標(biāo)
if (ball.y + ball.vy > canvas.height-15 || ball.y + ball.vy < 15) { //邊界判斷
ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width-15 || ball.x + ball.vx < 15) { //邊界判斷
ball.vx = -ball.vx;
}
window.requestAnimationFrame(draw); //循環(huán)執(zhí)行
}
draw();
簡(jiǎn)單概括一下這個(gè)實(shí)例的實(shí)現(xiàn)思想:
創(chuàng)建一個(gè)小球?qū)ο?,包含一個(gè)繪制自己的方法。在整個(gè)畫布中繪制這個(gè)小球,然后在下一次繪制之前,先清除整個(gè)畫布,改變小球的各個(gè)屬性(包含了邏輯,比如邊界的判斷),然后重新繪制一遍,從而達(dá)到了動(dòng)起來的效果。
如果你把上面代碼中的ctx.clearRect(0,0, canvas.width, canvas.height);換成下面這樣:
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
就可以得到漸變尾巴的效果,大概意思就是使用半透明的白色背景填充畫布來代替直接清除這個(gè)畫布,從而實(shí)現(xiàn)了想要的效果。
像素操作
如果我們想對(duì)一個(gè)canvas畫布進(jìn)行如下操作:獲取每一個(gè)點(diǎn)的信息,對(duì)每一個(gè)坐標(biāo)點(diǎn)進(jìn)行操作。那我們就需要了解一下ImageData對(duì)象了。
ImageData對(duì)象(由getImageData方法獲取的)中存儲(chǔ)著canvas對(duì)象真實(shí)的像素?cái)?shù)據(jù),它包含以下幾個(gè)只讀屬性:
- width
圖片寬度,單位是像素。
- height
圖片高度,單位是像素。
- data
Uint8ClampedArray類型的一維數(shù)組,包含著RGBA格式的整型數(shù)據(jù),范圍在0至255之間(包括255)。簡(jiǎn)單講,就是一個(gè)數(shù)組,每四個(gè)元素存儲(chǔ)一個(gè)點(diǎn)的顏色信息,這四個(gè)元素分別對(duì)應(yīng)為R、G、B、A的值(知道顏色取值的一眼就明白了,不知道的也沒關(guān)系,后面有實(shí)例,一看就明白)。
創(chuàng)建ImageData對(duì)象
去創(chuàng)建一個(gè)新的,空白的ImageData對(duì)像,你應(yīng)該會(huì)使用createImageData()方法:
var myImageData = ctx.createImageData(width, height);
上面代碼創(chuàng)建了一個(gè)新的具體特定尺寸的ImageData對(duì)像。所有像素被預(yù)設(shè)為透明黑。
獲取像素?cái)?shù)據(jù)
為了獲得一個(gè)包含畫布場(chǎng)景像素?cái)?shù)據(jù)的ImageData對(duì)像,你可以用getImageData()方法:
var myImageData = ctx.getImageData(left, top, width, height);
創(chuàng)建的myImageData對(duì)象就有width、height、data三個(gè)屬性的值了??聪旅孢@個(gè)實(shí)例:
html:
<div id="color">hover處的顏色</div>
<canvas id="myCanvas" width="300" height="150"></canvas>
js:
var can = document.getElementById('myCanvas');
var ctx = can.getContext('2d');
var img = new Image();
img.src = "***.jpg";
ctx.drawImage(img, 0, 0);
var color = document.getElementById('color');
function pick(event) {
var x = event.layerX;
var y = event.layerY;
var area = ctx.getImageData(x, y, 1, 1); //創(chuàng)建ImageData對(duì)象
var data = area.data; //獲取data屬性(一個(gè)存儲(chǔ)顏色rgba值的數(shù)組)
var rgba = 'rgba(' + data[0] + ',' + data[1] + ',' + data[2] + ',' + data[3] + ')';
color.style.color = rgba;
color.textContent = rgba;
}
can.addEventListener('mousemove', pick);
在場(chǎng)景中寫入像素?cái)?shù)據(jù)
你可以用putImageData()方法去對(duì)場(chǎng)景進(jìn)行像素?cái)?shù)據(jù)的寫入
ctx.putImageData(myImageData, x, y); //在畫布的(x, y)點(diǎn)開始繪制myImageData所存儲(chǔ)的像素信息。
所以我們可以把獲取到的像素信息進(jìn)行處理,然后再重新繪制,就得到了新的圖形。看看下面這個(gè)實(shí)例:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = '***.jpg';
ctx.drawImage(img, 0, 0);
var imageData = ctx.getImageData(0,0,canvas.width, canvas.height); //獲取ImageData
var colors = imageData.data; //獲取像素信息
function invert() {
for (var i = 0; i < colors.length; i += 4) { //四個(gè)為一組
colors[i] = 225 - colors[i]; // red
colors[i+1] = 225 - colors[i+1]; // green
colors[i+2] = 225 - colors[i+2]; // blue
colors[i+3] = 255; //alpha
}
ctx.putImageData(imageData, 220, 0); //從(220, 0)開始繪制改變過的顏色
}
function toGray() {
for (var i = 0; i < colors.length; i += 4) {
var avg = (colors[i] + colors[i+1] + colors[i+2]) / 3;
colors[i] = avg; // red
colors[i+1] = avg; // green
colors[i+2] = avg; // blue
colors[i+3] = 255; //alpha
}
ctx.putImageData(imageData, 440, 0); //從(440, 0)開始繪制改變過的顏色
}
invert(); //反轉(zhuǎn)色
toGray(); //變灰色
性能優(yōu)化
- 坐標(biāo)點(diǎn)盡量用整數(shù)
瀏覽器為了達(dá)到抗鋸齒的效果會(huì)做額外的運(yùn)算。為了避免這種情況,請(qǐng)保證使用canvas的繪制函數(shù)時(shí),盡量用Math.floor()函數(shù)對(duì)所有的坐標(biāo)點(diǎn)取整。比如:
ctx.drawImage(myImage, 0.3, 0.5); //不提倡這樣寫,應(yīng)該像下面這樣處理
ctx.drawImage(myImage, Math.floor(0.3), Math.floor(0.5));
- 使用多個(gè)畫布繪制復(fù)雜場(chǎng)景
比如做一個(gè)游戲,有幾個(gè)層面:背景層(簡(jiǎn)單變化)、游戲?qū)樱〞r(shí)刻變化)。這個(gè)時(shí)候,我們就可以創(chuàng)建兩個(gè)畫布,一個(gè)專門用來繪制不變的背景(少量繪制),另一個(gè)用來繪制游戲動(dòng)態(tài)部分(大量繪制),就像這樣:
<canvas id="background-can" width="480" height="320"></canvas>
<canvas id="game-can" width="480" height="320"></canvas>
- 用CSS設(shè)置靜態(tài)大圖
如果有一層是永遠(yuǎn)不變的,比如一張靜態(tài)的背景圖,最好使用div+css的方法去替代ctx.drawimage(),這么做可以避免在每一幀在畫布上繪制大圖。簡(jiǎn)單講,dom渲染肯定比canvas的操作性能更高。
- 盡量少操作canvas的縮放
如果要對(duì)一個(gè)畫布進(jìn)行縮放,如果可以的話,盡量使用CSS3的transform來實(shí)現(xiàn)??傊?,記住一個(gè)原則,能用html+div實(shí)現(xiàn)的盡量不用js對(duì)canvas進(jìn)行操作。
- more
- 將畫布的函數(shù)調(diào)用集合到一起(例如,畫一條折線,而不要畫多條分開的直線)
- 使用不同的辦法去清除畫布(clearRect()、fillRect()、調(diào)整canvas大小)
- 盡可能避免 shadowBlur特性
- 有動(dòng)畫,請(qǐng)使用window.requestAnimationFrame() 而非window.setInterval()