canvas API 整理 II

變形

在了解變形之前,先了解狀態(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)畫的流暢

基本步驟:

  1. 清空 canvas

除非接下來要畫的內(nèi)容會(huì)完全充滿 canvas (例如背景圖),否則你需要清空所有。最簡(jiǎn)單的做法就是用 clearRect 方法。

  1. 保存 canvas 狀態(tài)

如果你要改變一些會(huì)改變 canvas 狀態(tài)的設(shè)置(樣式,變形之類的),又要在每畫一幀之時(shí)都是原始狀態(tài)的話,你需要先保存一下。

  1. 繪制動(dòng)畫圖形(animated shapes)

這一步才是重繪動(dòng)畫幀。

  1. 恢復(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
  1. 將畫布的函數(shù)調(diào)用集合到一起(例如,畫一條折線,而不要畫多條分開的直線)
  2. 使用不同的辦法去清除畫布(clearRect()、fillRect()、調(diào)整canvas大小)
  3. 盡可能避免 shadowBlur特性
  4. 有動(dòng)畫,請(qǐng)使用window.requestAnimationFrame() 而非window.setInterval()
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容