詳解Canvas動畫部分

基礎篇: Html5中Canvas繪製、樣式詳解(不包含動畫部分)

此篇爲後續

目錄

1. 狀態的保存和恢復

2. translate移動

3. 旋轉Rotating

4. 縮放Scaling

5. 圖形相互交叉顯示規則

6. 裁切路徑

7. 動畫基本步驟

8. canvas相關的動畫js框架


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.動畫基本步驟

你可以通過以下的步驟來畫出一幀:

  1. 清空 canvas
    除非接下來要畫的內容會完全充滿 canvas (例如背景圖),否則你需要清空所有。最簡單的做法就是用 clearRect 方法。
  2. 保存 canvas 狀態
    如果你要改變一些會改變 canvas 狀態的設置(樣式,變形之類的),又要在每畫一幀之時都是原始狀態的話,你需要先保存一下。
  3. 繪製動畫圖形(animated shapes)
    這一步纔是重繪動畫幀。(重繪是相當費時的,而且性能很依賴於電腦的速度。)
  4. 恢復 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設計哲學一樣,每個工具只幹一件事並把這件事做好,你可以組合很多工具完成複雜的功能。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章