基礎篇: Html5中Canvas繪製、樣式詳解(不包含動畫部分)
此篇爲後續
目錄
1.狀態的保存和恢復
save()
保存畫布(canvas)的所有狀態
restore()
save 和 restore 方法是用來保存和恢復 canvas 狀態的,都沒有參數。Canvas 的狀態就是當前畫面應用的所有樣式和變形的一個快照。
Canvas狀態存儲在棧中,每當save()方法被調用後,當前的狀態就被推送到棧中保存。一個繪畫狀態包括:
當前應用的變形(即移動,旋轉和縮放,見下)
以及下面這些屬性:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
當前的裁切路徑(clipping path)
你可以調用任意多次 save方法。每一次調用 restore 方法,上一個保存的狀態就從棧中彈出,所有設定都恢復。
save
和 restore
的應用例子
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,150,150); // 使用默認設置繪製一個矩形
ctx.save(); // 保存默認狀態
ctx.fillStyle = '#09F' // 在原有配置基礎上對顏色做改變
ctx.fillRect(15,15,120,120); // 使用新的設置繪製一個矩形
ctx.save(); // 保存當前狀態
ctx.fillStyle = '#FFF' // 再次改變顏色配置
ctx.globalAlpha = 0.5;
ctx.fillRect(30,30,90,90); // 使用新的配置繪製一個矩形
ctx.restore(); // 重新加載之前的顏色狀態
ctx.fillRect(45,45,60,60); // 使用上一次的配置繪製一個矩形
ctx.restore(); // 加載默認顏色配置
ctx.fillRect(60,60,30,30); // 使用加載的配置繪製一個矩形
}
第一步是用默認設置畫一個大四方形,然後保存一下狀態。改變填充顏色畫第二個小一點的藍色四方形,然後再保存一下狀態。再次改變填充顏色繪製更小一點的半透明的白色四方形。
一旦我們調用 restore
,狀態棧中最後的狀態會彈出,並恢復所有設置。如果不是之前用 save
保存了狀態,那麼我們就需要手動改變設置來回到前一個狀態,這個對於兩三個屬性的時候還是適用的,一旦多了,我們的代碼將會猛漲。
當第二次調用 restore
時,已經恢復到最初的狀態,因此最後是再一次繪製出一個黑色的四方形。
2. translate移動
translate
方法,它用來移動 canvas 和它的原點到一個不同的位置。
translate(x, y)
translate
方法接受兩個參數。x 是左右偏移量,y 是上下偏移量
例子 平移之後畫個圓,然後恢復狀態,再平移再畫個圓,再恢復狀態,每次都畫在原點上,每次原點都不在一個位置上
function draw3() {
var ctx = document.getElementById('canvas').getContext('2d');
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
ctx.save();
ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)';
ctx.translate(10 + j * 50, 10 + i * 50);
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI*2, true);
ctx.fill();
ctx.restore();
}
}
}
效果
3. 旋轉Rotating
rotate
方法,它用於以原點爲中心旋轉 canvas
rotate(angle)
這個方法只接受一個參數:旋轉的角度(angle),它是順時針方向的,以弧度爲單位的值。
旋轉的中心點始終是 canvas 的原點,如果要改變它,我們需要用到 translate
方法。
來個例子,記住是座標系旋轉,想問題的時候要想清楚 此案例先把中心移到了圓的中心
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.translate(75,75);
for (var i=1;i<6;i++){ // Loop through rings (from inside to out)
ctx.save();
ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
for (var j=0;j<i*6;j++){ // draw individual dots
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();
}
}
效果:
4. 縮放Scaling
我們用它來增減圖形在 canvas 中的像素數目,對形狀,位圖進行縮小或者放大
scale(x, y)
scale
方法可以縮放畫布的水平和垂直的單位。兩個參數都是實數,可以爲負數,x 爲水平縮放因子,y 爲垂直縮放因子,如果比1小,會比縮放圖形, 如果比1大會放大圖形。默認值爲1, 爲實際大小。
畫布初始情況下, 是以左上角座標爲原點的第一象限。如果參數爲負實數, 相當於以x 或 y軸作爲對稱軸鏡像反轉(例如, 使用translate(0,canvas.height); scale(1,-1);
以y軸作爲對稱軸鏡像反轉, 就可得到著名的笛卡爾座標系,左下角爲原點)。
默認情況下,canvas 的 1 個單位爲 1 個像素。舉例說,如果我們設置縮放因子是 0.5,1 個單位就變成對應 0.5 個像素,這樣繪製出來的形狀就會是原先的一半。同理,設置爲 2.0 時,1 個單位就對應變成了 2 像素,繪製的結果就是圖形放大了 2 倍。
scale
的例子
function draw6() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.save();
ctx.scale(10, 3);
ctx.fillRect(1, 10, 10, 10);
ctx.restore();
// 鏡像x軸
ctx.scale(-1, 1);
ctx.font = '48px serif';
ctx.fillText('海軍', -135, 120);
}
效果圖
5. 圖形相互交叉顯示規則
ctx.globalCompositeOperation = type
這個屬性設定了在畫新圖形時採用的遮蓋策略,其值是一個標識12種遮蓋方式的字符串。
source-over
這是默認設置,並在現有畫布上下文之上繪製新圖形。
source-in
新圖形只在新圖形和目標畫布重疊的地方繪製。其他的都是透明的。
source-out
在不與現有畫布內容重疊的地方繪製新圖形。
source-atop
新圖形只在與現有畫布內容重疊的地方繪製。
destination-over
在現有的畫布內容後面繪製新的圖形。
destination-in
現有的畫布內容保持在新圖形和現有畫布內容重疊的位置。其他的都是透明的。
destination-out
現有內容保持在新圖形不重疊的地方。
destination-atop
現有的畫布只保留與新圖形重疊的部分,新的圖形是在畫布內容後面繪製的。
lighter
兩個重疊圖形的顏色是通過顏色值相加來確定的。
copy
只顯示新圖形。
xor
圖像中,那些重疊和正常繪製之外的其他地方是透明的。
multiply
將頂層像素與底層相應像素相乘,結果是一幅更黑暗的圖片。
screen
像素被倒轉,相乘,再倒轉,結果是一幅更明亮的圖片。
overlay
multiply和screen的結合,原本暗的地方更暗,原本亮的地方更亮。
darken
保留兩個圖層中最暗的像素。
lighten
保留兩個圖層中最亮的像素。
color-dodge
將底層除以頂層的反置。
color-burn
將反置的底層除以頂層,然後將結果反過來。
hard-light
屏幕相乘(A combination of multiply and screen)類似於疊加,但上下圖層互換了。
soft-light
用頂層減去底層或者相反來得到一個正值。
difference
一個柔和版本的強光(hard-light)。純黑或純白不會導致純黑或純白。
exclusion
和difference相似,但對比度較低。
hue
保留了底層的亮度(luma)和色度(chroma),同時採用了頂層的色調(hue)。
saturation
保留底層的亮度(luma)和色調(hue),同時採用頂層的色度(chroma)。
color
保留了底層的亮度(luma),同時採用了頂層的色調(hue)和色度(chroma)。
luminosity
保持底層的色調(hue)和色度(chroma),同時採用頂層的亮度(luma)。
6. 裁切路徑
裁切路徑和普通的 canvas 圖形差不多,不同的是它的作用是遮罩,用來隱藏不需要的部分。如上圖所示。紅邊五角星就是裁切路徑,所有在路徑以外的部分都不會在 canvas 上繪製出來。
與globalCompositeOperation
屬性作一比較,它可以實現與 source-in
和 source-atop
差不多的效果。最重要的區別是裁切路徑不會在 canvas 上繪製東西,而且它永遠不受新圖形的影響。這些特性使得它在特定區域裏繪製圖形時相當好用。
clip()
方法
將當前正在構建的路徑轉換爲當前的裁剪路徑。
看案例
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,150,150);
ctx.translate(75,75);
// Create a circular clipping path
ctx.beginPath();
ctx.arc(0,0,60,0,Math.PI*2,true);
ctx.clip();
// draw background
var lingrad = ctx.createLinearGradient(0,-75,0,75);
lingrad.addColorStop(0, '#232256');
lingrad.addColorStop(1, '#143778');
ctx.fillStyle = lingrad;
ctx.fillRect(-75,-75,150,150);
// draw stars
for (var j=1;j<50;j++){
ctx.save();
ctx.fillStyle = '#fff';
ctx.translate(75-Math.floor(Math.random()*150),
75-Math.floor(Math.random()*150));
drawStar(ctx,Math.floor(Math.random()*4)+2);
ctx.restore();
}
}
function drawStar(ctx,r){
ctx.save();
ctx.beginPath()
ctx.moveTo(r,0);
for (var i=0;i<9;i++){
ctx.rotate(Math.PI/5);
if(i%2 == 0) {
ctx.lineTo((r/0.525731)*0.200811,0);
} else {
ctx.lineTo(r,0);
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
}
用了一個圓形的裁切路徑來限制隨機星星的繪製區域。
首先,畫了一個與 canvas 一樣大小的黑色方形作爲背景,然後移動原點至中心點。然後用 clip
方法創建一個弧形的裁切路徑。裁切路徑也屬於 canvas 狀態的一部分,可以被保存起來。如果我們在創建新裁切路徑時想保留原來的裁切路徑,我們需要做的就是保存一下 canvas 的狀態。
裁切路徑創建之後所有出現在它裏面的東西纔會畫出來。在畫線性漸變時我們就會注意到這點。然後會繪製出50 顆隨機位置分佈(經過縮放)的星星,當然也只有在裁切路徑裏面的星星纔會繪製出來。
效果圖
7.動畫基本步驟
你可以通過以下的步驟來畫出一幀:
- 清空 canvas
除非接下來要畫的內容會完全充滿 canvas (例如背景圖),否則你需要清空所有。最簡單的做法就是用clearRect
方法。 - 保存 canvas 狀態
如果你要改變一些會改變 canvas 狀態的設置(樣式,變形之類的),又要在每畫一幀之時都是原始狀態的話,你需要先保存一下。 - 繪製動畫圖形(animated shapes)
這一步纔是重繪動畫幀。(重繪是相當費時的,而且性能很依賴於電腦的速度。) - 恢復 canvas 狀態
如果已經保存了 canvas 的狀態,可以先恢復它,然後重繪下一幀。
爲了實現動畫,我們需要一些可以定時執行重繪的方法。有兩種方法可以實現這樣的動畫操控。首先可以通過 setInterval
和 setTimeout
方法來控制在設定的時間點上執行重繪。
requestAnimationFrame(draw)
告訴瀏覽器你希望執行一個動畫,並在重繪之前,請求瀏覽器執行一個特定的函數來更新動畫。
如果你並不需要與用戶互動,你可以使用setInterval()方法,它就可以定期執行指定代碼。如果我們需要做一個遊戲,我們可以使用鍵盤或者鼠標事件配合上setTimeout()方法來實現。通過設置事件監聽,我們可以捕捉用戶的交互,並執行相應的動作。
簡答的案例
function draw9() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.globalCompositeOperation = 'destination-over';
ctx.clearRect(0,0,500,500); // clear canvas
ctx.fillStyle = 'green';
ctx.strokeStyle = 'blueviolet';
ctx.save();
ctx.translate(180,180);
ctx.beginPath();
ctx.arc(0,0,150,0,Math.PI*2,false);
ctx.stroke();
var time = new Date();
ctx.rotate( ((2*Math.PI)/60)*time.getSeconds() + ((2*Math.PI)/60000)*time.getMilliseconds() )
ctx.beginPath();
ctx.arc(0,150,20,0,Math.PI*2,false);
ctx.fill();
ctx.closePath();
ctx.restore();
window.requestAnimationFrame(draw9)
}
window.requestAnimationFrame(draw9);
效果就是一個小圓繞着大圓旋轉
案例2模擬太陽系
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init(){
sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
window.requestAnimationFrame(draw);
}
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.globalCompositeOperation = 'destination-over';
ctx.clearRect(0,0,300,300); // clear canvas
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.strokeStyle = 'rgba(0,153,255,0.4)';
ctx.save();
ctx.translate(150,150);
// Earth
var time = new Date();
ctx.rotate( ((2*Math.PI)/60)*time.getSeconds() + ((2*Math.PI)/60000)*time.getMilliseconds() );
ctx.translate(105,0);
ctx.fillRect(0,-12,50,24); // Shadow
ctx.drawImage(earth,-12,-12);
// Moon
ctx.save();
ctx.rotate( ((2*Math.PI)/6)*time.getSeconds() + ((2*Math.PI)/6000)*time.getMilliseconds() );
ctx.translate(0,28.5);
ctx.drawImage(moon,-3.5,-3.5);
ctx.restore();
ctx.restore();
ctx.beginPath();
ctx.arc(150,150,105,0,Math.PI*2,false); // Earth orbit
ctx.stroke();
ctx.drawImage(sun,0,0,300,300);
window.requestAnimationFrame(draw);
}
init();
案例3動畫時鐘
function clock(){
var now = new Date();
var ctx = document.getElementById('canvas').getContext('2d');
ctx.save();
ctx.clearRect(0,0,150,150);
ctx.translate(75,75);
ctx.scale(0.4,0.4);
ctx.rotate(-Math.PI/2);
ctx.strokeStyle = "black";
ctx.fillStyle = "white";
ctx.lineWidth = 8;
ctx.lineCap = "round";
// Hour marks
ctx.save();
for (var i=0;i<12;i++){
ctx.beginPath();
ctx.rotate(Math.PI/6);
ctx.moveTo(100,0);
ctx.lineTo(120,0);
ctx.stroke();
}
ctx.restore();
// Minute marks
ctx.save();
ctx.lineWidth = 5;
for (i=0;i<60;i++){
if (i%5!=0) {
ctx.beginPath();
ctx.moveTo(117,0);
ctx.lineTo(120,0);
ctx.stroke();
}
ctx.rotate(Math.PI/30);
}
ctx.restore();
var sec = now.getSeconds();
var min = now.getMinutes();
var hr = now.getHours();
hr = hr>=12 ? hr-12 : hr;
ctx.fillStyle = "black";
// write Hours
ctx.save();
ctx.rotate( hr*(Math.PI/6) + (Math.PI/360)*min + (Math.PI/21600)*sec )
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-20,0);
ctx.lineTo(80,0);
ctx.stroke();
ctx.restore();
// write Minutes
ctx.save();
ctx.rotate( (Math.PI/30)*min + (Math.PI/1800)*sec )
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(-28,0);
ctx.lineTo(112,0);
ctx.stroke();
ctx.restore();
// Write seconds
ctx.save();
ctx.rotate(sec * Math.PI/30);
ctx.strokeStyle = "#D40000";
ctx.fillStyle = "#D40000";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(-30,0);
ctx.lineTo(83,0);
ctx.stroke();
ctx.beginPath();
ctx.arc(0,0,10,0,Math.PI*2,true);
ctx.fill();
ctx.beginPath();
ctx.arc(95,0,10,0,Math.PI*2,true);
ctx.stroke();
ctx.fillStyle = "rgba(0,0,0,0)";
ctx.arc(0,0,3,0,Math.PI*2,true);
ctx.fill();
ctx.restore();
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = '#325FA2';
ctx.arc(0,0,142,0,Math.PI*2,true);
ctx.stroke();
ctx.restore();
window.requestAnimationFrame(clock);
}
window.requestAnimationFrame(clock);
關於動畫要了解的可不止這些,包括速率,加速度,邊界,拖尾效果,鼠標控制等等
舉個栗子
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;
var running = false;
var ball = {
x: 100,
y: 100,
vx: 5,
vy: 1,
radius: 25,
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 clear() {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
}
function draw() {
clear();
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
ball.vx = -ball.vx;
}
raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener('mousemove', function(e){
if (!running) {
clear();
ball.x = e.offsetX;
ball.y = e.offsetY;
ball.draw();
}
});
canvas.addEventListener('click',function(e){
if (!running) {
raf = window.requestAnimationFrame(draw);
running = true;
}
});
canvas.addEventListener('mouseout', function(e){
window.cancelAnimationFrame(raf);
running = false;
});
ball.draw();
用你的鼠標移動小球,點擊可以釋放。
8. canvas相關的動畫js框架
Three.js
這個流行的庫提供了非常多的3D顯示功能,以一種直觀的方式使用 WebGL。這個庫提供了<canvas>、 <svg>、CSS3D 和 WebGL渲染器,讓我們在設備和瀏覽器之間創建豐富的交互體驗。該庫於2010年4月首次推出
pixi.js
雖然只是個渲染框架而非遊戲引擎,但是pixi.js挺適合寫遊戲的,你可以看下netent上的博彩遊戲,上萬款遊戲全是pixi.js寫的。pixi.js剛接觸的時候覺得很簡陋,文檔都是英文的,有時候還需要看源碼(源碼寫的非常的優秀)才能理解怎麼回事,這對很多人是個很高的門檻,但是如果你能克服這些,會發現用着真的很舒服。
周邊插件也挺豐富的,動畫(spine/dragonbones),粒子系統,物理引擎等等,想用什麼就引入什麼,和unix設計哲學一樣,每個工具只幹一件事並把這件事做好,你可以組合很多工具完成複雜的功能。