初識canvas

ZG發表於2017-10-13

HTML5 也出來一段時間了,帶來了一些新的技術標準,例如 HTML5 裡的 Canvas 標籤,<canvas></canvas>,允許我們使用 Javascript 在 canvas 標籤裡繪製 2D 或 3D 圖形和動畫。下面就談談對 canvas 繪圖的理解以及介紹其中一些使用過的方法,用一個條形進度條和一個仿科幻文字做展示例子。

<canvas> 元素不被一些老的瀏覽器所支援,但是所有的主流瀏覽器的新近版本都支援,相容性問題尚好。使用 canvas 標籤來繪製圖形或動畫需要掌握一些基本的 HTML 和 JavaScript 知識,通過 canvas 的 api 和了解 canvas 繪圖的原理即可。其實 canvas 上的動作的原理和顯示器中動作原理是一樣的,都是一幀一幀繪製靜態圖形,繪製重新整理的速度超過了我們的人眼識別速度,所以看上去就像真的在動一樣。不同點是顯示器的重新整理是實時的,而 canvas 的重新整理需要我們通過 js 控制來手動進行觸發,如果只是繪製簡單的靜態圖形,那麼這一步就不需要我們手動控制進行重新整理了。比如說我們想要一個矩形從左邊移到右邊,開始時可以讓矩形在最左邊,然後下一秒時再往右一些繪製相同的矩形,但是前提是先把原來的矩形給去掉,再重新繪製。通過不斷的擦出和重繪,就會出現動作效果,通過加快速度不被人眼識破。

canvas 在 html 文件中通過標籤建立後是空白的,是一個固定大小的畫布,可以通過 width 和 height 控制它的寬高,預設大小為300畫素×150畫素(px)。為了展示,首先需要找到渲染上下文,然後通過 js 在 canvas 上進行繪製。<canvas> 元素有一個叫做 getContext() 的方法,這個方法是用來獲得渲染上下文和它的繪畫功能。getContext()只有一個引數,上下文的格式,對於 2D 影像而言,填入 `2d` 字串。

var canvas = document.getElementByTagName(`canvas`)[0];
var ctx = canvas.getContext(`2d`);
// 清除畫布
context.clearRect(0, 0, canvas.width, canvas.height);

通過 document.getElementByTagName() 方法來為 <canvas> 元素得到 DOM 物件。通過 DOM 元素上的getContext() 方法來訪問繪畫上下文。看一個條形進度條例子 https://codepen.io/zgfrank/pe…

var canvas = document.getElementById(`canvas`),
    context = canvas.getContext(`2d`),
    centerX = canvas.width/2,
    centerY = canvas.height/2,
    speed = 0.1;

function text(n){
  context.save();
  context.fillStyle = "#F47C7C";
  context.font = "40px Arial";
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.fillText(n.toFixed(0)+"%", centerX, centerY);
  context.restore();
}

function rectfixed(){
  context.beginPath();
  context.rect(50, 140, 400, 20);
  
  context.lineWidth = 1;
  context.strokeStyle = `white`;
  context.fillStyle = `white`;

  context.fill();
  context.stroke();
}

function rectmove(speed){
  context.beginPath();
  context.rect(50, 140, speed*4, 20);

  context.lineWidth = 1;
  context.fillStyle = `#F47C7C`;

  context.fill();
  context.stroke();
}


(function drawFrame(){
  window.requestAnimationFrame(drawFrame, canvas);
  context.clearRect(0, 0, canvas.width, canvas.height);

  text(speed);
  rectfixed()
  rectmove(speed)
  if(speed > 100) speed = 0;
  speed += 0.2;
}());

從程式碼上看,首先繪製矩形的 api 是 context.rect(x, y, width, height),分別是x/y座標,寬和高。第一個條形是固定不動的,第二個條形的寬隨著speed的變動而變動,通過window.requestAnimationFrame迴圈調動drawFrame函式達到動畫的效果。requestAnimationFrame是一個新的API,作用與setTimeInterval一樣,不同的是它會根據瀏覽器的重新整理頻率自動調整動畫的時間間隔。也別忘了要對畫布呼叫clearRect方法清除上一幀的圖案才能產生正確效果。繪製文字的 api 是,給圖形上色有兩個重要的屬性可以做到,分別是 fillStyle 和 strokeStyle。fillStyle 是設定圖形的填充顏色,strokestyle 是設定圖形的輪廓顏色,通過 fill() 和stroke() 方法,還有更多樣式設定可以參考 MDN 中的 canvas API。

第二個例子是仿科幻文字效果,https://codepen.io/zgfrank/pe…

var canvas = document.querySelector(`#test`),
    ctx = canvas.getContext(`2d`);
    canvas.width = window.innerWidth,
    canvas.height = window.innerHeight;

var shadowColor = `rgba(0, 0, 0, 0.08)`,  //用於覆蓋文字的漸變陰影
    textColor = "#33ff33",                //文字顏色
    words = "0123456789qwertyuiopasdfghjklzxcvbnm,./;`[]QWERTYUIOP{}ASDFGHJHJKL:ZXCVBBNM<>?",      //隨機文字
    wordsArray = words.split(``),         //將文字拆分進一個陣列
    fontSize = 18,                        //字型大小
    columns = canvas.width / fontSize,    //文字降落的列數
    drops = [];                           //文字行數

for(var i = 0; i < columns; i++){
  //從第一行開始
  drops[i] = 1;
}

function drawText(){
  ctx.save();
  ctx.fillStyle = textColor;
  ctx.font = fontSize + "px arial";
  
  for (var i = 0; i < drops.length; i++){
    // 隨機取文字
    var text = wordsArray[Math.floor(Math.random() * wordsArray.length)];
    // 繪製第n行文字
    ctx.fillText(text, i * fontSize, drops[i] * fontSize);
    // 差異,數字越小差異越小
    if (drops[i] * fontSize > canvas.height && Math.random() > 0.99){
      drops[i] = 0;
    }
    drops[i]++;
  }
  ctx.restore();
}

(function drawFrame(){
  window.requestAnimationFrame(drawFrame, canvas);
  // 陰影覆蓋文字
  ctx.fillStyle = shadowColor;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  drawText();
}())

上面的程式碼就是實現該效果的關鍵:通過 fillText() 方法來繪製隨機的字元,通過 fillRect() 方法覆蓋一個帶有透明度的黑色矩形實現文字被覆蓋形成陰影的效果。繪製列數是 canvas 寬度 / 字型大小。在繪製函式中,最開始是一行行繪製,所以建立drop陣列一開始都為1,為了從第一行開始繪製,因此剛重新整理的時候是從第一行繪製到最後一行。

context.fillText(text, i * fontSize, drops[i] * fontSize);

fillText() 有三個引數,第一個是繪製的隨機文字,第二是文字的 X 起始座標,三是文字的 Y 起始座標。所以第一行每個文字的 X 軸座標是 0, 18, 36…,而 Y 軸座標為 18, 18, 18, …。繪製第二行則將 drops 陣列的值都加 1 ,即第二行的 Y 軸座標為(32,32,32…)。依次類推,從第一行到最後一行就繪製了滿屏的文字,通過 fillRect() 加上覆蓋的黑色透明矩形,就有點類似實現了漸變陰影的文字向下運動動畫。為了顯示重複繪製的效果,需要設定當繪製到最後一行了,需要從第一行開始繪製:

if (drops[i] * fontSize > canvas.height ){
    drops[i] = 0;
}

但是這樣我們會發現,每一行文字都是同時繪製的,沒有差異性,這樣不能達到不同列錯落有致的效果。為了顯示出每一行文字的差異性,就必須讓一些列先從第一行開始繪製,一些列後面再從第一行開始繪製,怎麼達到呢,可以增加隨機數,同時判斷隨機數和是否繪製到了最後一行,同時滿足的列才開始從第一行繪製:

if (drops[i] * fontSize > canvas.height && Math.random() > 0.99){
    drops[i] = 0;
}