《Core HTML5 Canvas:Graphics, Animation, and Game Development》學習筆記

書中代碼示例效果展示:Core HTML5 Canvas Examples


基礎知識

canvas元素

canvas元素的能力是通過Canvas的context對象表現出來的。該環境變量可以從canvas元素身上獲取。

在開發基于Canvas的應用程序時可以這樣做:

  1. 使用document.getElementById()方法來獲取指向canvas的引用。
  2. 在canvas對象上調用getContext('2d')方法,獲取繪圖環境變量。
  3. 使用繪圖環境對象在canvas元素上進行繪制。

canvas元素并未提供很多API,它只提供了兩個屬性和三個方法。

  • canvas元素的屬性
屬性 描述 類型 取值范圍 默認值
width canvas元素繪圖表面的寬度 非負整數 在有效范圍內的任意非負整數 300
height canvas元素繪圖表面的高度 非負整數 在有效范圍內的任意非負整數 150
  • canvas元素的方法

屬性 | 描述
-|
getContext() | 返回與該canvas元素相關的繪圖環境對象
toDataURL(type, quality) | 返回一個數據地址(data URL),可以設定為img元素的src屬性值。第一個參數指定了圖像的類型(默認是“image/png”);第二個參數必須是0~1.0之間的double值,表示JPEG圖像的顯示質量。
toBlob(callback, type, args...) | 創建一個用于表示此canvas元素圖像文件的Blob。第一個參數是一個回調函數,瀏覽器會以一個指向blob的引用作為參數,去調用該回調函數;第二個參數以“image/png”這樣的形式來指定圖像類型(默認是“image/png”);最后一個參數是介于0.0~1.0之間的值,表示JPEG圖像的質量。將來可能加入其他參數。

易錯點及提示小結

  1. 在設置canvas的寬度和高度時,不能使用px后綴(不符合規范)
  2. 可以通過指定width和height屬性值來修改canvas元素的大小,如:
<canvas id='canvas' width='600' height='300'></canvas>

這種方法實際上同時修改了該元素本身的大小與元素繪圖表面的大小。
而如果是通過CSS來設定canvas元素的大小,如:

#canvas {
    width: 600px;
    height: 300px;
}

那么只會改變元素本身的大小,而不會影響到繪圖表面(還是默認的300×150像素)。當canvas元素的大小不符合其繪圖表面的大小時,瀏覽器就會對繪圖表面進行縮放,使其符合元素的大小。

Canvas的繪圖環境

canvas元素僅僅是為了充當繪圖環境對象的容器而存在的,該環境對象提供了全部的繪制功能。

2d繪圖環境

在JavaScript代碼中,很少會用到canvas元素本身(獲取canvas的寬度、高度或某個數據地址)。 還可以通過canvas元素來獲取指向canvas繪圖環境對象的引用,這個繪圖環境對象提供功能強大的API,可以用來繪制圖形與文本,顯示并修改圖像等等。

  • CanvasRenderingContext2D對象所含的屬性

屬性 | 簡介
-|
canvas | 指向該繪圖環境所屬的canvas對象。最常見的用途是獲取canvas的寬度(context.canvas.width)和高度(context.canvas.height)
fillstyle | 指向該繪圖環境在后續的圖形填充操作中所使用的顏色、漸變色或圖案
font | 設定在調用繪圖環境對象的fillText()或strokeText()方法時,所使用的字型
globalAlpha | 全局透明度設定,它可以取0(完全透明)~1.0(完全不透明)之間的值。瀏覽器會將每個像素的alpha值與該值相乘,在繪制圖像時也是如此
globalCompsiteOperation | 該值決定了瀏覽器將某個物體繪制在其他物體之上時,所采用的繪制方式。
lineCap | 該值告訴瀏覽器如何繪制線段的端點。可取的值有:butt、round及square。默認值是butt
lineWidth | 該值決定了在canvas中繪制線段的屏幕像素寬度。它必須是個非負、非無窮的double值。默認值是1.0
lineJoin | 告訴瀏覽器在兩條線段相交時如何繪制焦點。可取的值是:bevel、round、miter。默認值是miter
miterLimit | 告訴瀏覽器如何繪制miter形式的線段焦點
shadowBlur | 該值決定了瀏覽器該如何延伸陰影效果。值越高,陰影效果延伸得就越遠。該值不是指陰影的像素長度,而是代表高斯模糊方程式中的參數值。它必須是一個非負且非無窮量的double值,默認值是0
shadowColor | 該值告訴瀏覽器使用何種顏色來繪制陰影(通常采用半透明色作為該屬性的值)
shadowOffsetX | 以像素為單位,指定了陰影效果的水平方向偏移量
shadowOffsetY | 以像素為單位,指定了陰影效果的垂直方向偏移量
strokeStyle | 指定了對路徑進行描邊時所用的繪制風格。該值可被設定為某個顏色、漸變色或圖案
textAlign | 決定了以fillText()或stroText()方法進行繪制時,所畫文本的水平對齊方式
textBaseline | 決定了以fillText()或stroText()方法進行繪制時,所畫文本的垂直對齊方式

在Canvas中,有一個與2d繪圖環境相對應的3d繪圖環境,叫做WebGL,它完全符合OpenGL ES2.0的API
Canvas狀態的保存與恢復

在進行繪圖操作時,很多時候只是想臨時性地改變一些屬性值。

Canvas的API提供了save()和restore()兩個方法,用于保存及恢復當前canvas繪圖環境的所有屬性。
CanvasRenderingContext2D.save()
CanvasRenderingContext2D.restore()

function drawGrid(strokeStyle, fillStyle) {
    controlContext.save(); // Save the context on a stack

    controlContext.fillStyle = fillStyle;
    controlContext.strokeStyle = strokeStyle;

    // Draw the grid...

    controlContext.restore(); // Restore the contex from the stack
}

save()與restore()方法可以嵌套式調用
繪圖環境的save()方法會將當前的繪圖環境壓入堆棧頂部。對應的restore()方法則會從堆棧頂部彈出一組狀態信息,并據此恢復當前繪圖環境的各個狀態。這意味著可以嵌套式地調用save()與restore()方法。

基本的繪制操作

示例:時鐘程序
它用到了如下的Canvas繪圖API:

  • arc()
  • beginPath()
  • clearPath()
  • fill()
  • fillText()
  • lineTo()
  • moveTo()
  • stroke()

// JavaScript

const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    FONT_HEIGHT = 15,
    MARGIN = 35,
    HAND_TRUNCATION = canvas.width / 25,
    HOUR_HAND_TRUNCATION = canvas.width / 10,
    NUMERAL_SPACING = 20,
    RADIUS = canvas.width / 2 - MARGIN,
    HAND_RADIUS = RADIUS + NUMERAL_SPACING;

// Functions.....................................................

function drawCircle() {
    context.beginPath();
    context.arc(canvas.width / 2, canvas.height / 2,
        RADIUS, 0, Math.PI * 2, true);
    context.stroke();
}

function drawNumerals() {
    let numerals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
        angle = 0,
        numeralWidth = 0;

    numerals.forEach(function(numeral) {
        angle = Math.PI / 6 * (numeral - 3);
        numeralWidth = context.measureText(numeral).width;
        context.fillText(numeral,
            canvas.width / 2 + Math.cos(angle) * (HAND_RADIUS) -
            numeralWidth / 2,
            canvas.height / 2 + Math.sin(angle) * (HAND_RADIUS) +
            FONT_HEIGHT / 3);
    });
}

function drawCenter() {
    context.beginPath();
    context.arc(canvas.width / 2, canvas.height / 2, 5, 0, Math.PI * 2, true);
    context.fill();
}

function drawHand(loc, isHour) {
    let angle = (Math.PI * 2) * (loc / 60) - Math.PI / 2,
        handRadius = isHour ? 
                     RADIUS - HAND_TRUNCATION - HOUR_HAND_TRUNCATION :
                     RADIUS - HAND_TRUNCATION;

    context.moveTo(canvas.width / 2, canvas.height / 2);
    context.lineTo(canvas.width / 2 + Math.cos(angle) * handRadius,
        canvas.height / 2 + Math.sin(angle) * handRadius);
    context.stroke();
}

function drawHands() {
    let date = new Date,
        hour = date.getHours();
    hour = hour > 12 ? hour - 12 : hour;
    drawHand(hour * 5 + (date.getMinutes() / 60) * 5, true, 0.5);
    drawHand(date.getMinutes(), false, 0.5);
    drawHand(date.getSeconds(), false, 0.2);
}

function drawClock() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    drawCircle();
    drawCenter();
    drawHands();
    drawNumerals();
}

// Initialization................................................

context.font = FONT_HEIGHT + 'px Arial';
loop = setInterval(drawClock, 1000);

// HTML

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Clock</title>

    <style>
        body {
            background: #dddddd;
        }

        #canvas {
            position: absolute;
            left: 0px;
            top: 0px;
            margin: 20px;
            background: #ffffff;
            border: thin solid #aaaaaa;
        }
    </style>
</head>

<body>
    <canvas id='canvas' width='400' height='400'>
      Canvas not supported
    </canvas>

    <script src='example.js'></script>
</body>

</html>
效果截圖

事件處理

鼠標事件

在canvas中檢測鼠標事件:在canvas中增加一個事件監聽器。
例如,要監聽“按下鼠標事件”,可以:

canvas.onmousedown = function(e) {
    // React to the mouse down event
};

也可以使用更為通用的addEventListener()方法來注冊監聽器:

canvas.addEventListener('mousedown', function(e){
    // React to the mouse down event
});

將鼠標坐標轉換為Canvas坐標
瀏覽器通過事件對象傳遞給監聽器的鼠標坐標,是窗口坐標,而不是相對于canvas自身的坐標。所以需要坐標轉換。

例子:精靈表坐標查看器
該應用程序向canvas注冊了一個mousemove事件監聽器,等到瀏覽器回調這個監聽時,應用程序會將相對于窗口的鼠標坐標轉換為canvas坐標。轉換工作是通過類似下面這樣的windowToCanvas()方法來完成的:

function windowToCanvas(canvas, x, y) {
    let bbox = canvas.getBoundingClientRect();
    return {
        x: x - bbox.left * (canvas.width / bbox.width),
        y: y - bbox.top * (canvas.height / bbox.height)
    };
}

canvas.onmousemove = function(e) {
    let loc = windowToCanvas(canvas, e.clientX, e.clientY);

    drawBackground();
    drawSpritesheet();
    drawGuidelines(loc.x, loc.y);
    updateReadout(loc.x, loc.y);
};
...
/* 完整代碼略 */

上述windowToCanvas()方法在canvas對象上調用getBoundingClientRect()方法,來獲取canvas元素的邊界框(bounding box),該邊界框的坐標是相對于整個窗口的。然后,windowToCanvas()方法返回了一個對象,其x與y屬性分別對應于鼠標在canvas之中的坐標。

精靈表坐標查看器

Tips

  1. 在HTML5規范出現后,通過瀏覽器傳給事件監聽器的事件對象,來獲取鼠標事件發生的窗口坐標,當前支持HTML5的瀏覽器都支持clientX與clientY屬性了。詳見http://www.quirksmode.org/js/events_mouse.html
  2. 讓瀏覽器不再干預事件處理
    在編寫的event handler代碼中調用preventDefault()方法即可
  3. Canvas繪圖環境對象的drawImage()方法
    該方法可以將某圖像的全部或者一部分從某個地方復制到一個canvas中,另外還可以對圖像進行縮放。

示例代碼:(最簡單的形式)將存放于Image對象中的全部圖像內容,未經縮放地繪制到應用程序的canvas中。

function drawSpritesheet() {
    context.drawImage(spritesheet, 0, 0);
}
鍵盤事件

當在瀏覽器窗口按下某鍵時,瀏覽器會生成鍵盤事件。這些事件發生在當前擁有焦點的HTML元素身上。
假如沒有元素擁有焦點,那么事件的發生地將會上移至window與document對象。

注意:canvas是一個不可獲取焦點的元素。
所以,在canvas元素上新增鍵盤事件監聽器是徒勞的。

一共有三種鍵盤事件:

  • keydown
  • keypress
  • keyup
觸摸事件

用于智能手機與平板上。

繪制表面的保存與恢復

繪制表面的保存與恢復功能,可以讓開發者在繪圖表面上進行一些臨時性的繪制動作,諸如繪制橡皮帶線條、輔助線或注解。

檢測到鼠標按下的事件之后,應用程序就將繪圖表面保存起來......

使用以下兩個方法來操作圖像
CanvasRenderingContext2D.getImageData()
CanvasRenderingContext2D.putImageData()

立即模式繪圖系統
canvas元素采用“立即模式”繪制圖形,即它會立即繪制,然后,立即忘記剛才繪制的內容。(SVG等繪圖系統則是“保留模式”繪圖系統)

示例:通過保存與恢復繪圖表面來繪制輔助線

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
...

// Save and restore drawing surface.............................

function saveDrawingSurface() {
    drawingSurfaceImageData = context.getImageData(0, 0,
        canvas.width, canvas.height);
}

function restoreDrawingSurface() {
    context.putImageData(drawingSurfaceImageData, 0, 0);
}

// Event handlers...............................................

canvas.onmousedown = function(e) {
    ...
    saveDrawingSurface();
    ...
};

canvas.onmousemove = function(e) {
    let loc = windowToCanvas(e);

    if (dragging) {
        restoreDrawingSurface();
        ...

        if (guidewires) {
            drawGuidewires(mousedown.x, mousedown.y);
        }
    }
};

canvas.onmouseup = function(e) {
    ...
    restoreDrawingSurface();
}

在Canvas中使用HTML元素

將一個或更多的canvas元素與其他HTML控件結合起來使用,以便讓用戶可以通過輸入數值或其他方式來控制程序。
為了讓HTML控件看上去好像是出現在canvas范圍內,可以使用CSS將這些控件放置在canvas之上。

示例:用于在canvas中顯示HTML控件的HTML代碼片段

// 通過CSS來確定玻璃窗格的絕對位置,讓其顯示在canvas之上
<style>
    #canvas {
        margin-left: 10px;
        margin-top: 10px;
        background: #ffffff;
        border: thin solid #aaaaaa;
    }

    #glasspane {
        position: absolute;
        left: 50px;
        top: 50px;
        ...
    }

    ...
</style>

CSS規范書中規定:采用絕對定位方式的元素將被繪制在采用相對定位方式的元素之上。
示例代碼中canvas元素的position屬性值為默認值relative,玻璃窗格采用絕對定位,所以玻璃窗格會顯示在canvas之上。
如果兩個元素都采用相對或絕對定位,那么,改變這兩個元素的順序也可以達到同樣的效果,或者調整其CSS中的z-index屬性。

除了放置好需要顯示的HTML控件,還需要在JavaScript代碼中獲取指向這些控件的引用,以便訪問并修改它們的屬性值。

JavaScript代碼片段:

const context = document.getElementById('canvas').getContext('2d'),
    startButton = document.getElementById('startButton'),
    glasspane = document.getElementById('glasspane');

let paused = true;
...

// 根據應用程序當前的狀態來啟動或暫停動畫效果
startButton.onclick = function(e) {
    e.preventDefault();
    e.stopPropagation();
    paused = !paused;
    startButton.innerText = paused ? 'Start' : 'Stop';
};

// 阻止瀏覽器對于鼠標點擊的默認反應,以避免用戶無意間選中了玻璃窗格控件
glasspane.onmousedown = function(e) {
    e.preventDefault();
    e.stopPropagation();
}
...

顯示在canvas之上的HTML元素

動畫效果展示

進階:在用戶拖動鼠標時動態地修改DIV元素的大小

示例:使用浮動的DIV元素來實現橡皮筋式選取框
效果展示
HTML代碼片段:

<!-- 包含按鈕,如果點擊那個按鈕,程序會像剛啟動那樣,將整幅圖像繪制出來 -->
<div id='controls'>
    <input type='button' id='resetButton' value='Reset' />
</div>

<!-- 用于實現橡皮筋式選取框。一開始是不可見的,當用戶拖動鼠標時設置為可見 -->
<div id='rubberbandDiv'></div>

<canvas id='canvas' width='800' height='520'>
    Canvas not supported
</canvas>

JavaScript代碼略

打印Canvas的內容

在默認情況下,盡管每個canvas對象都是一幅位圖,但是,它并不是HTML的img元素,所以,用戶不能對其進行某些操作。

Canvas的API提供了一個toDataURL()方法,該方法返回的引用,指向了某個給定canvas元素的數據地址。接下來,將img元素的src屬性值設置為這個數據地址,就可以創建一幅表示canvas的圖像了。


使用toDataURL()方法來保存Canvas的圖像

它提供了一個控件,讓用戶通過該控件來抓取canvas的快照。

核心代碼片段:

snapshotButton.onclick = function(e) {
    let dataUrl;

    if (snapshotButton.value === 'Take snapshot') {
        dataUrl = canvas.toDataURL();
        clearInterval(loop);
        snapshotImageElement.src = dataUrl;
        snapshotImageElement.style.display = 'inline';
        canvas.style.display = 'none';
        snapshotButton.value = 'Return to Canvas';
    } else {
        snapshotButton.value = 'Take snapshot';
        canvas.style.display = 'inline';
        snapshotImageElement.style.display = 'none';
        loop = setInterval(drawClock, 1000);
    }
};

離屏canvas

離屏canvas,也叫緩沖canvas、幕后canvas。
作用:提高性能;于幕后完成顯示模式的切換。

基礎數學知識

需回顧的數學內容

  • 求解代數方程
  • 三角函數
  • 向量運算
  • 根據計量單位來推導等式


</br>

繪制

坐標系統

它以canvas的左上角為原點,X坐標向右方增長,而Y坐標則向下方延伸。

Canvas的坐標系并不是固定的。可以采用如下方式來變換坐標系統:

  • 平移(translate)
  • 旋轉(rotate)
  • 縮放(scale)
  • 創建自定義的變換方式,例如切變(shear),也叫“錯切”,詳見維基百科

Canvas的繪制模型

瀏覽器起初會將圖形或圖像繪制到一張“無限大的位圖”上,在繪制時會使用Canvas繪圖環境對象中與圖形的填充及描邊有關的那些屬性。
接下來,如果啟用了陰影效果的話,那么瀏覽器將會把陰影渲染到另外一張位圖上。并將陰影中每個像素alpha值乘以globalAlpha屬性,把運算結果設置為該陰影像素的透明度,并將陰影與canvas元素進行圖像合成。操作時采用當前的合成設定,并按照當前的剪輯區域對合成之后的位圖進行剪切。
最后,瀏覽器會根據當前的合成設定與剪輯區域,將圖形或位圖與canvas元素進行圖像合成。

矩形的繪制

Canvas的API提供了如下三個方法,分別用于矩形的清除、描邊及填充:

  • clearRect(double x, double y, double w, double h)
  • strokeRect(double x, double y, double w, double h)
    使用以下屬性,為指定的矩形描邊:
    • strokeStyle
    • lineWidth
    • lineJoin
    • miterLimit
  • fillRect(double x, double y, double w, double h)

Tip:圓角矩形的繪制

// 示例代碼,通過lineJoin屬性繪制
const context = canvas.getContext('2d');
context.lineJoin = 'round';
context.lineWidth = 30; // 還需考慮lineWidth屬性
context.strokeRect(75, 100, 200, 200);

Canvas規范描述了繪制這些圓角的詳細過程,沒有留下什么自由發揮的余地。如果想要控制諸如圓角半徑之類的一些屬性,那么必須自己來繪制這些圓角。

顏色與透明度

對矩形進行描邊與填充的顏色可通過繪圖環境的strokeStyle與fillStyle屬性來設置。strokeStyle與fillStyle的屬性值可以是任意有效的CSS顏色字串。
詳見CSS Color Module Level 3
除此之外,還可以使用SVG1.0規范中的顏色名稱

瀏覽器可能并不支持全部SVG1.0標準的顏色名稱

漸變色與圖案

除了顏色值之外,也可以為strokeStyle與fillStyle屬性指定漸變色與圖案。

線性(linear)漸變
通過調用createLinearGradient()方法來創建。
調用之后,該方法會返回一個CanvasGradient實例。

最后,應用程序將該漸變色設置為fillStyle屬性的值,接下來調用fill()方法時,都會使用此漸變色進行填充,直到將填充屬性設置成其他值為止。

在創建好漸變色之后,通過調用addColorStop()方法來向漸變色中增加“顏色停止點”(color stop)。

const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    gradient = context.createLinearGradient(0, 0, 0, canvas.height / 2);

gradient.addColorStop(0, 'blue');
gradient.addColorStop(0.25, 'white');
gradient.addColorStop(0.5, 'purple');
gradient.addColorStop(0.75, 'red');
gradient.addColorStop(1, 'yellow');

context.fillStyle = gradient;
context.rect(0, 0, canvas.width, canvas.height);
context.fill();
線性漸變

放射(radial)漸變
通過調用createRadialGradient()方法來創建。

接下來與線性漸變類似。

const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    gradient = context.createRadialGradient(
        canvas.width / 2, canvas.height, 10, canvas.width / 2, 0, 100);

gradient.addColorStop(0, 'blue');
gradient.addColorStop(0.25, 'white');
gradient.addColorStop(0.5, 'purple');
gradient.addColorStop(0.75, 'red');
gradient.addColorStop(1, 'yellow');

context.fillStyle = gradient;
context.rect(0, 0, canvas.width, canvas.height);
context.fill();
放射漸變

圖案
可以是以下三種之一:image元素、canvas元素或vedio元素。
可以用createPattern()方法來創建圖案。

const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    repeatRadio = document.getElementById('repeatRadio'),
    noRepeatRadio = document.getElementById('noRepeatRadio'),
    repeatXRadio = document.getElementById('repeatXRadio'),
    repeatYRadio = document.getElementById('repeatYRadio'),
    image = new Image();

function fillCanvasWithPattern(repeatString) {
    let pattern = context.createPattern(image, repeatString);
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.fillStyle = pattern;
    context.fillRect(0, 0, canvas.width, canvas.height);
    context.fill();
};

repeatRadio.onclick = function(e) {
    fillCanvasWithPattern('repeat');
};

repeatXRadio.onclick = function(e) {
    fillCanvasWithPattern('repeat-x');
};

repeatYRadio.onclick = function(e) {
    fillCanvasWithPattern('repeat-y');
};

noRepeatRadio.onclick = function(e) {
    fillCanvasWithPattern('no-repeat');
};

image.src = 'redball.png';
image.onload = function(e) {
    fillCanvasWithPattern('repeat');
};

HTML主要作用部分

<body>
    <div id='radios'>
        <input type='radio' id='repeatRadio' name='patternRadio' checked/>repeat
        <input type='radio' id='repeatXRadio' name='patternRadio' />repeat-x
        <input type='radio' id='repeatYRadio' name='patternRadio' />repeat-y
        <input type='radio' id='noRepeatRadio' name='patternRadio' />no repeat
    </div>

    <canvas id="canvas" width="450" height="275">
        Canvas not supported
    </canvas>
</body>



陰影

可以通過修改繪圖環境中的如下4個屬性值來指定陰影效果:

  • shadowColor: CSS3格式的顏色。
  • shadowOffsetX: 從圖形或文本到陰影的水平像素偏移。
  • shadowOffsetY: 從圖形或文本到陰影的垂直像素偏移。
  • shadowBlur: 一個與像素無關的值。該值被用于高斯模糊方程中,以便對陰影對象進行模糊化處理。

如果滿足以下條件,那么使用Canvas的繪圖環境對象就可以繪制出陰影效果了:

  1. 指定的shadowColor值不是全透明的。
  2. 在其余的陰影屬性中,存在一個非0的值。
let SHADOW_COLOR = 'rgba(0,0,0,0.7)';
...
function setIconShadow() {
    iconContext.shadowColor = SHADOW_COLOR;
    iconContext.shadowOffsetX = 1;
    iconContext.shadowOffsetY = 1;
    iconContext.shadowBlur = 2;
}

// 對被選中的按鈕圖標使用了與其余圖標不同的陰影屬性
function setSelectedIconShadow() {
    iconContext.shadowColor = SHADOW_COLOR;
    iconContext.shadowOffsetX = 4;
    iconContext.shadowOffsetY = 4;
    iconContext.shadowBlur = 5;
}
使用陰影效果制作具有深度感的按鈕
Canvas繪圖環境對象也可以在對文本或路徑進行描邊時繪制陰影效果。

Tip:使用半透明色來繪制陰影
通常來說,使用半透明色來繪制陰影是個不錯的選擇,因為這樣一來,背景就可以透過陰影顯示出來了。

負偏移量可以用來制作內嵌陰影(inset shadow)效果。

示例:畫圖程序里的橡皮擦工具(它有一個淡淡的內嵌陰影,使得橡皮擦的表面看上去有種凹陷的效果)

const drawingCanvas = document.getElementById('drawingCanvas'),
    drawingContext = drawingCanvas.getContext('2d'),
    ERASER_LINE_WIDTH = 1,
    ERASER_SHADOW_STYLE = 'blue',
    ERASER_STROKE_STYLE = 'rgba(0,0,255,0.6)',
    ERASER_SHADOW_OFFSET = -5,
    ERASER_SHADOW_BLUR = 20,
    ERASER_RADIUS = 40;

// Eraser......................................................

function setEraserAttributes() {
    drawingContext.lineWidth = ERASER_LINE_WIDTH;
    drawingContext.shadowColor = ERASER_SHADOW_STYLE;
    drawingContext.shadowOffsetX = ERASER_SHADOW_OFFSET;
    drawingContext.shadowOffsetY = ERASER_SHADOW_OFFSET;
    drawingContext.shadowBlur = ERASER_SHADOW_BLUR;
    drawingContext.strokeStyle = ERASER_STROKE_STYLE;
}

function drawEraser(loc) {
    drawingContext.save();
    setEraserAttributes();

    drawingContext.beginPath();
    drawingContext.arc(loc.x, loc.y, ERASER_RADIUS, 0, Math.PI * 2, false);

    /* clip()方法的調用,使得后續被調用的stroke()方法以及此方法所生成的陰影,
       都被局限在這個圓形的范圍之內 */
    drawingContext.clip();
    drawingContext.stroke();

    drawingContext.restore();
}
畫圖程序中所用的內嵌陰影效果

路徑、描邊與填充

大多數繪制系統,如SVG等,都是基于路徑的。使用這些繪制系統時,需要先定義一個路徑,然后再對其進行描邊或填充,也可以在描邊的同時進行填充。


圖形的描邊與填充效果

該應用程序創建了9個不同的路徑,第一列只描邊,第二列只填充,第三列同時進行描邊與填充;第一行的矩形路徑與第三行的圓弧路徑都是封閉路徑,而中間一行的弧形路徑是開放路徑,但不論一個路徑是開放或是封閉,都可以對其進行填充。

JavaScript代碼

const context = document.getElementById('canvas').getContext('2d');

// Functions..........................................................

function drawGrid(context, color, stepx, stepy) {
    context.save()

    context.strokeStyle = color;
    context.lineWidth = 0.5;

    for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
        context.beginPath();
        context.moveTo(i, 0);
        context.lineTo(i, context.canvas.height);
        context.stroke();
        context.closePath();
    }
    for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
        context.beginPath();
        context.moveTo(0, i);
        context.lineTo(context.canvas.width, i);
        context.stroke();
        context.closePath();
    }
    context.restore();
}

// Initialization.....................................................

drawGrid(context, 'lightgray', 10, 10);

// Drawing attributes.................................................

context.font = '48pt Helvetica';
context.strokeStyle = 'blue';
context.fillStyle = 'red';
context.lineWidth = '2'; // line width set to 2 for text

// Text...............................................................

context.strokeText('Stroke', 60, 110);
context.fillText('Fill', 440, 110);

context.strokeText('Stroke & Fill', 650, 110);
context.fillText('Stroke & Fill', 650, 110);

// Rectangles.........................................................

context.lineWidth = '5'; // line width set to 5 for shapes
context.beginPath();
context.rect(80, 150, 150, 100);
context.stroke();

context.beginPath();
context.rect(400, 150, 150, 100);
context.fill();

context.beginPath();
context.rect(750, 150, 150, 100);
context.stroke();
context.fill();

// Open arcs..........................................................

context.beginPath();
context.arc(150, 370, 60, 0, Math.PI * 3 / 2);
context.stroke();

context.beginPath();
context.arc(475, 370, 60, 0, Math.PI * 3 / 2);
context.fill();

context.beginPath();
context.arc(820, 370, 60, 0, Math.PI * 3 / 2);
context.stroke();
context.fill();

// Closed arcs........................................................

context.beginPath();
context.arc(150, 550, 60, 0, Math.PI * 3 / 2);
context.closePath();
context.stroke();

context.beginPath();
context.arc(475, 550, 60, 0, Math.PI * 3 / 2);
context.closePath();
context.fill();

context.beginPath();
context.arc(820, 550, 60, 0, Math.PI * 3 / 2);
context.closePath();
context.stroke();
context.fill();
路徑與子路徑

在某一時刻,canvas中只能有一條路徑存在,Canvas規范將其稱為“當前路徑”(current path)。然而,這條路徑卻可以包含許多子路徑(subpath)。而子路徑,又是由兩個或更多的點組成的。

二次調用beginPath()方法,會清除上一次調用某繪圖方法時所創建的子路徑。如果沒有調用beginPath()方法來清除原有的子路徑,則第二次對某繪圖方法的調用,會向當前路徑中增加一條子路徑。

填充路徑時所使用的“非零環繞規則”
如果當前路徑是循環的,或是包含多個相交的子路徑,那么Canvas的繪圖環境變量就必須要判斷,當fill()方法被調用時,應該如何對當前路徑進行填充。Canvas在填充那種互相有交叉的路徑時,使用“非零環繞規則”(nonzero winding rule)來進行判斷。
非零環繞規則參考解釋

剪紙效果

運用路徑、陰影以及非零環繞原則等知識,實現如下圖所示的剪紙(cutout)效果。


用兩個圓形做出的剪紙效果

這段代碼創建了一條路徑,它由兩個圓形組成,其中一個圓形在另一個的內部,通過設定arc()方法的最后一個參數值,分別以順、逆時針方向繪制內、外部的圓形。

在創建好路徑之后,應用程序就對該路徑進行了填充。瀏覽器運用“非零環繞規則”,對外圍圓形的內部進行了填充,而填充的范圍并不包括里面的圓,這就產生了一種剪紙圖案的效果。

代碼核心部分

const context = document.getElementById('canvas').getContext('2d');

// Functions.....................................................
...
function drawTwoArcs(sameDirection) {
    context.beginPath();
    context.arc(300, 170, 150, 0, Math.PI * 2, false); // outer: CCW
    context.arc(300, 170, 100, 0, Math.PI * 2, !sameDirection); // innner: CW

    context.fill();
    context.shadowColor = undefined;
    context.shadowOffsetX = 0;
    context.shadowOffsetY = 0;
    context.stroke();
}

function draw(sameDirection) {
    context.clearRect(0, 0, context.canvas.width,
        context.canvas.height);
    drawGrid('lightgray', 10, 10);

    context.save();

    context.shadowColor = 'rgba(0, 0, 0, 0.8)';
    context.shadowOffsetX = 12;
    context.shadowOffsetY = 12;
    context.shadowBlur = 15;

    drawTwoArcs(directionCheckbox.checked);

    context.restore();

    ...
}
...

// Initialization................................................

context.fillStyle = 'rgba(100, 140, 230, 0.5)';
context.strokeStyle = context.fillStyle; 
draw(...);

采用完全不透明的顏色來填充這個包含剪紙圖形的矩形:
(可以用任意形狀的路徑來包圍剪紙圖形)


各種剪紙圖形

該程序建立剪紙圖形所用的代碼如下:

function drawCutouts() {
    context.beginPath();
    addOuterRectanglePath(); // CW

    addCirclePath(); // CCW
    addRectanglePath(); // CCW
    addTrianglePath(); // CCW

    context.fill(); // Cut out shapes
}

addOuterRectanglePath()、addCirclePath()、addRectanglePath()及addTrianglePath()方法分別向當前路徑中添加了表示剪紙圖形的子路徑。

arc()方法可以讓調用者控制圓弧的繪制方向,然而rect()方法則總是按照順時針方向來創建路徑。
在本例中需要一條逆時針的矩形路徑,所以需要自己創建一個rect()方法,使得它像arc()一樣,可以讓調用者控制矩形路徑的方向:

function rect(x, y, w, h, direction) {
    if (direction) { // CCW
        context.moveTo(x, y);
        context.lineTo(x, y + h);
        context.lineTo(x + w, y + h);
        context.lineTo(x + w, y);
        context.closePath();
    } else {
        context.moveTo(x, y);
        context.lineTo(x + w, y);
        context.lineTo(x + w, y + h);
        context.lineTo(x, y + h);
        context.closePath();
    }
}

內部的矩形剪紙圖形:

function addRectanglePath() {
    rect(310, 55, 70, 35, true);
}

外部的矩形,使用繪圖環境對象的rect()方法,此方法總是按照順時針方向來繪制矩形:

function addOuterRectanglePath() {
    context.rect(110, 25, 370, 335);
}

</br>

Tip: 去掉arc()方法所產生的那條不太美觀的連接線
可以在調用arc()方法來繪制圓弧之前,先調用beginPath()方法。【調用此方法會將當前路徑下的所有子路徑都清除掉】

線段

Canvas繪圖環境提供了兩個可以用來創建路徑的方法:moveTo()與lineTo()。在創建路徑之后調用stroke()方法,才能使線性路徑出現在canvas中。


線段的繪制
const context = document.getElementById('canvas').getContext('2d');

context.lineWidth = 1;
context.beginPath();
context.moveTo(50, 10);
context.lineTo(450, 10);
context.stroke();

context.beginPath();
context.moveTo(50.5, 50.5);
context.lineTo(450.5, 50.5);
context.stroke();

方法 | 描述
-|
moveTo() | 向當前路徑中增加一條子路徑,該子路徑只包含一個點(由參數傳入)。該方法并不會從當前路徑中清除任何子路徑。
lineTo() | 如果當前路徑中沒有子路徑,則這個方法的行為與moveTo()方法一樣;如果當前路徑中存在子路徑,那么該方法會將所指定的那個點加入子路徑中。

  • 線段與像素邊界
    如果在某2個像素的邊界處繪制一條1像素寬的線段,那么該線段實際上會占據2個像素的寬度;如果將線段繪制在某2個像素之間的那個像素中,中線左右兩端的那半個像素就不會再延伸了,合起來恰好占據1個像素的寬度。

    左為在像素邊界處繪制線段,右為在某個像素范圍內繪制線段

  • 網格的繪制


    繪制網格
const context = document.getElementById('canvas').getContext('2d');

// Functions.....................................................

function drawGrid(context, color, stepx, stepy) {
    context.strokeStyle = color;
    context.lineWidth = 0.5;

    for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
        context.beginPath();
        context.moveTo(i, 0);
        context.lineTo(i, context.canvas.height);
        context.stroke();
    }

    for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
        context.beginPath();
        context.moveTo(0, i);
        context.lineTo(context.canvas.width, i);
        context.stroke();
    }
}

// Initialization................................................

drawGrid(context, 'lightgray', 10, 10);
  • 坐標軸的繪制


    繪制坐標軸
const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),

    AXIS_MARGIN = 40,
    AXIS_ORIGIN = {
        x: AXIS_MARGIN,
        y: canvas.height - AXIS_MARGIN
    },

    AXIS_TOP = AXIS_MARGIN,
    AXIS_RIGHT = canvas.width - AXIS_MARGIN,

    HORIZONTAL_TICK_SPACING = 10,
    VERTICAL_TICK_SPACING = 10,

    AXIS_WIDTH = AXIS_RIGHT - AXIS_ORIGIN.x,
    AXIS_HEIGHT = AXIS_ORIGIN.y - AXIS_TOP,

    NUM_VERTICAL_TICKS = AXIS_HEIGHT / VERTICAL_TICK_SPACING,
    NUM_HORIZONTAL_TICKS = AXIS_WIDTH / HORIZONTAL_TICK_SPACING,

    TICK_WIDTH = 10,
    TICKS_LINEWIDTH = 0.5,
    TICKS_COLOR = 'navy',

    AXIS_LINEWIDTH = 1.0,
    AXIS_COLOR = 'blue';

// Functions..........................................................

function drawGrid(color, stepx, stepy) {
    context.save()

    context.fillStyle = 'white';
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);

    context.lineWidth = 0.5;
    context.strokeStyle = color;

    for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
        context.beginPath();
        context.moveTo(i, 0);
        context.lineTo(i, context.canvas.height);
        context.stroke();
    }

    for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
        context.beginPath();
        context.moveTo(0, i);
        context.lineTo(context.canvas.width, i);
        context.stroke();
    }

    context.restore();
}

function drawAxes() {
    context.save();
    context.strokeStyle = AXIS_COLOR;
    context.lineWidth = AXIS_LINEWIDTH;

    drawHorizontalAxis();
    drawVerticalAxis();

    context.lineWidth = 0.5;
    context.lineWidth = TICKS_LINEWIDTH;
    context.strokeStyle = TICKS_COLOR;

    drawVerticalAxisTicks();
    drawHorizontalAxisTicks();

    context.restore();
}

function drawHorizontalAxis() {
    context.beginPath();
    context.moveTo(AXIS_ORIGIN.x, AXIS_ORIGIN.y);
    context.lineTo(AXIS_RIGHT, AXIS_ORIGIN.y)
    context.stroke();
}

function drawVerticalAxis() {
    context.beginPath();
    context.moveTo(AXIS_ORIGIN.x, AXIS_ORIGIN.y);
    context.lineTo(AXIS_ORIGIN.x, AXIS_TOP);
    context.stroke();
}

function drawVerticalAxisTicks() {
    let deltaY;

    for (let i = 1; i < NUM_VERTICAL_TICKS; ++i) {
        context.beginPath();

        if (i % 5 === 0) deltaX = TICK_WIDTH;
        else deltaX = TICK_WIDTH / 2;

        context.moveTo(AXIS_ORIGIN.x - deltaX,
            AXIS_ORIGIN.y - i * VERTICAL_TICK_SPACING);

        context.lineTo(AXIS_ORIGIN.x + deltaX,
            AXIS_ORIGIN.y - i * VERTICAL_TICK_SPACING);

        context.stroke();
    }
}

function drawHorizontalAxisTicks() {
    let deltaY;

    for (let i = 1; i < NUM_HORIZONTAL_TICKS; ++i) {
        context.beginPath();

        if (i % 5 === 0) deltaY = TICK_WIDTH;
        else deltaY = TICK_WIDTH / 2;

        context.moveTo(AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING,
            AXIS_ORIGIN.y - deltaY);

        context.lineTo(AXIS_ORIGIN.x + i * HORIZONTAL_TICK_SPACING,
            AXIS_ORIGIN.y + deltaY);

        context.stroke();
    }
}

// Initialization................................................

drawGrid('lightgray', 10, 10);
drawAxes();
  • 橡皮筋式的線條繪制


    橡皮筋式的線條繪制

用戶可以通過拖拽鼠標的方式在canvas的背景上互動式地畫線。

const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    eraseAllButton = document.getElementById('eraseAllButton'),
    strokeStyleSelect = document.getElementById('strokeStyleSelect'),
    guidewireCheckbox = document.getElementById('guidewireCheckbox');

let drawingSurfaceImageData,
    mousedown = {},
    rubberbandRect = {},
    dragging = false,
    guidewires = guidewireCheckbox.checked;

// Functions..........................................................

function drawGrid(color, stepx, stepy) {
    context.save()

    context.strokeStyle = color;
    context.lineWidth = 0.5;
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);

    for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
        context.beginPath();
        context.moveTo(i, 0);
        context.lineTo(i, context.canvas.height);
        context.stroke();
    }

    for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
        context.beginPath();
        context.moveTo(0, i);
        context.lineTo(context.canvas.width, i);
        context.stroke();
    }

    context.restore();
}

function windowToCanvas(x, y) {
    let bbox = canvas.getBoundingClientRect();
    return {
        x: x - bbox.left * (canvas.width / bbox.width),
        y: y - bbox.top * (canvas.height / bbox.height)
    };
}

// Save and restore drawing surface...................................

function saveDrawingSurface() {
    drawingSurfaceImageData = context.getImageData(0, 0,
        canvas.width,
        canvas.height);
}

function restoreDrawingSurface() {
    context.putImageData(drawingSurfaceImageData, 0, 0);
}

// Rubberbands........................................................

function updateRubberbandRectangle(loc) {
    rubberbandRect.width = Math.abs(loc.x - mousedown.x);
    rubberbandRect.height = Math.abs(loc.y - mousedown.y);

    if (loc.x > mousedown.x) rubberbandRect.left = mousedown.x;
    else rubberbandRect.left = loc.x;

    if (loc.y > mousedown.y) rubberbandRect.top = mousedown.y;
    else rubberbandRect.top = loc.y;

    context.save();
    context.strokeStyle = 'red';
    context.restore();
}

function drawRubberbandShape(loc) {
    context.beginPath();
    context.moveTo(mousedown.x, mousedown.y);
    context.lineTo(loc.x, loc.y);
    context.stroke();
}

function updateRubberband(loc) {
    updateRubberbandRectangle(loc);
    drawRubberbandShape(loc);
}

// Guidewires.........................................................

function drawHorizontalLine(y) {
    context.beginPath();
    context.moveTo(0, y + 0.5);
    context.lineTo(context.canvas.width, y + 0.5);
    context.stroke();
}

function drawVerticalLine(x) {
    context.beginPath();
    context.moveTo(x + 0.5, 0);
    context.lineTo(x + 0.5, context.canvas.height);
    context.stroke();
}

function drawGuidewires(x, y) {
    context.save();
    context.strokeStyle = 'rgba(0,0,230,0.4)';
    context.lineWidth = 0.5;
    drawVerticalLine(x);
    drawHorizontalLine(y);
    context.restore();
}

// Canvas event handlers..............................................

canvas.onmousedown = function(e) {
    let loc = windowToCanvas(e.clientX, e.clientY);

    e.preventDefault(); // prevent cursor change

    saveDrawingSurface();
    mousedown.x = loc.x;
    mousedown.y = loc.y;
    dragging = true;
};

canvas.onmousemove = function(e) {
    let loc;

    if (dragging) {
        e.preventDefault(); // prevent selections

        loc = windowToCanvas(e.clientX, e.clientY);
        restoreDrawingSurface();
        updateRubberband(loc);

        if (guidewires) {
            drawGuidewires(loc.x, loc.y);
        }
    }
};

canvas.onmouseup = function(e) {
    loc = windowToCanvas(e.clientX, e.clientY);
    restoreDrawingSurface();
    updateRubberband(loc);
    dragging = false;
};

// Controls event handlers.......................................

eraseAllButton.onclick = function(e) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    drawGrid('lightgray', 10, 10);
    saveDrawingSurface();
};

strokeStyleSelect.onchange = function(e) {
    context.strokeStyle = strokeStyleSelect.value;
};

guidewireCheckbox.onchange = function(e) {
    guidewires = guidewireCheckbox.checked;
};

// Initialization................................................

context.strokeStyle = strokeStyleSelect.value;
drawGrid('lightgray', 10, 10);
  • 虛線的繪制


    虛線的繪制

這段代碼計算虛線的總長度,然后根據其中每條短劃線(dash)的長度,算出整個虛線中應該含有多少這樣的短劃線。代碼根據計算出的短劃線數量,通過反復繪制多條很短的線段來畫出整個虛線。

const context = document.querySelector('#canvas').getContext('2d');

function drawDashedLine(context, x1, y1, x2, y2, dashLength) {
    dashLength = dashLength === undefined ? 5 : dashLength;

    let deltaX = x2 - x1;
    let deltaY = y2 - y1;
    let numDashes = Math.floor(Math.sqrt(deltaX * deltaX + deltaY * deltaY) / dashLength);

    for (let i = 0; i < numDashes; ++i) {
        context[i % 2 === 0 ? 'moveTo' : 'lineTo'](x1 + (deltaX / numDashes) * i, y1 + (deltaY / numDashes) * i);
    }

    context.stroke();
};

context.lineWidth = 3;
context.strokeStyle = 'blue';

drawDashedLine(context, 20, 20, context.canvas.width - 20, 20);
drawDashedLine(context, context.canvas.width - 20, 20, context.canvas.width - 20, context.canvas.height - 20, 10);
drawDashedLine(context, context.canvas.width - 20, context.canvas.height - 20, 20, context.canvas.height - 20, 15);
drawDashedLine(context, 20, context.canvas.height - 20, 20, 20, 2);

圓弧與圓形的繪制

arc()方法的用法
在清除已有子路徑后繪制圓弧

不清除子路徑即繪制圓弧
const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d');

context.beginPath();
// context.moveTo(8, 28);
context.arc(canvas.width / 2, canvas.height / 4, 80, Math.PI / 4, Math.PI, false);
context.stroke();
  • 以橡皮筋式輔助線來協助用戶畫圓
    可以讓用戶以拖動鼠標的方式畫圓。當拖動鼠標時,該應用程序會持續地繪制圓形。


    以橡皮筋式輔助線來協助用戶畫圓

代碼核心部分

function drawRubberbandShape(loc) {
    let angle, radius;

    if (mousedown.y === loc.y) { // horizontal line
        // Horizontal lines are a special case. See the else
        // block for an explanation

        radius = Math.abs(loc.x - mousedown.x);
    } else {
        // For horizontal lines, the angle is 0, and Math.sin(0)
        // is 0, which means we would be dividing by 0 here to get NaN
        // for radius. The if block above catches horizontal lines.

        angle = Math.atan(rubberbandRect.height / rubberbandRect.width),
        radius = rubberbandRect.height / Math.sin(angle);
    }

    context.beginPath();
    context.arc(mousedown.x, mousedown.y, radius, 0, Math.PI * 2, false);
    context.stroke();

    if (fillCheckbox.checked)
        context.fill();
}
arcTo()方法的用法
圓角矩形的繪制
const context = document.getElementById('canvas').getContext('2d');

function roundedRect(cornerX, cornerY, width, height, cornerRadius) {
    if (width > 0) context.moveTo(cornerX + cornerRadius, cornerY);
    else context.moveTo(cornerX - cornerRadius, cornerY);

    context.arcTo(cornerX + width, cornerY, cornerX + width, cornerY + height, cornerRadius);
    context.arcTo(cornerX + width, cornerY + height, cornerX, cornerY + height, cornerRadius);
    context.arcTo(cornerX, cornerY + height, cornerX, cornerY, cornerRadius);

    if (width > 0) {
        context.arcTo(cornerX, cornerY, cornerX + cornerRadius, cornerY, cornerRadius);
    } else {
        context.arcTo(cornerX, cornerY, cornerX - cornerRadius, cornerY, cornerRadius);
    }
}

function drawRoundedRect(strokeStyle, fillStyle, cornerX, cornerY, width, height, cornerRadius) {
    context.beginPath();
    roundedRect(cornerX, cornerY, width, height, cornerRadius);

    context.strokeStyle = strokeStyle;
    context.fillStyle = fillStyle;
    context.stroke();
    context.fill();
}

drawRoundedRect('blue', 'yellow', 50, 40, 100, 100, 10);
drawRoundedRect('purple', 'green', 275, 40, -100, 100, 20);
drawRoundedRect('red', 'white', 300, 140, 100, -100, 30);
drawRoundedRect('white', 'blue', 525, 140, -100, -100, 40);
  • 刻度儀表盤的繪制


    儀表盤的繪制

代碼核心部分

function drawDial() {
    let loc = {
        x: circle.x,
        y: circle.y
    };

    drawCentroid();
    drawCentroidGuidewire(loc);

    drawRing();
    drawTickInnerCircle();
    drawTicks();
    drawAnnotations();
}

貝塞爾曲線

貝塞爾曲線原理
  • 二次方貝塞爾曲線
    由三個點來定義:兩個錨點(anchor point)及一個控制點(control point)。
    二次方貝塞爾曲線是那種只向一個方向彎曲的簡單二維曲線。

示例:用三條二次方貝塞爾曲線拼合而成的一個復選框(checkbox)標記。


使用二次方貝塞爾曲線來繪制復選框標記
CanvasRenderingContext2D.quadraticCurveTo()
const context = document.getElementById('canvas').getContext('2d');

context.fillStyle = 'cornflowerblue';
context.strokeStyle = 'yellow';

context.shadowColor = 'rgba(50, 50, 50, 1.0)';
context.shadowOffsetX = 2;
context.shadowOffsetY = 2;
context.shadowBlur = 4;

context.lineWidth = 20;
context.lineCap = 'round';

context.beginPath();
context.moveTo(120.5, 130);
context.quadraticCurveTo(150.8, 130, 160.6, 150.5);
context.quadraticCurveTo(190, 250.0, 210.5, 160.5);
context.quadraticCurveTo(240, 100.5, 290, 70.5);
context.stroke();
  • 三次方貝塞爾曲線
    由四個點來定義:兩個錨點及兩個控制點。
    三次方貝塞爾曲線是能夠向兩個方向彎曲的三次曲線。

示例:使用bezierCurveTo()方法創建一條代表三次方貝塞爾曲線的路徑。
這段代碼除了繪制曲線本身,還填充了曲線控制點與錨點的小圓圈。

三次方貝塞爾曲線

const canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    endPoints = [{
            x: 130,
            y: 70
        },
        {
            x: 430,
            y: 270
        },
    ],
    controlPoints = [{
            x: 130,
            y: 250
        },
        {
            x: 450,
            y: 70
        },
    ];

function drawGrid(color, stepx, stepy) {
    context.save()

    context.strokeStyle = color;
    context.fillStyle = '#ffffff';
    context.lineWidth = 0.5;
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);

    for (let i = stepx + 0.5; i < context.canvas.width; i += stepx) {
        context.beginPath();
        context.moveTo(i, 0);
        context.lineTo(i, context.canvas.height);
        context.stroke();
    }

    for (let i = stepy + 0.5; i < context.canvas.height; i += stepy) {
        context.beginPath();
        context.moveTo(0, i);
        context.lineTo(context.canvas.width, i);
        context.stroke();
    }

    context.restore();
}

function drawBezierCurve() {
    context.strokeStyle = 'blue';
    context.fillStyle = 'yellow';

    context.beginPath();
    context.moveTo(endPoints[0].x, endPoints[0].y);
    context.bezierCurveTo(controlPoints[0].x, controlPoints[0].y,
        controlPoints[1].x, controlPoints[1].y,
        endPoints[1].x, endPoints[1].y);
    context.stroke();
}

function drawEndPoints() {
    context.strokeStyle = 'blue';
    context.fillStyle = 'red';

    endPoints.forEach(function(point) {
        context.beginPath();
        context.arc(point.x, point.y, 5, 0, Math.PI * 2, false);
        context.stroke();
        context.fill();
    });
}

function drawControlPoints() {
    context.strokeStyle = 'yellow';
    context.fillStyle = 'blue';

    controlPoints.forEach(function(point) {
        context.beginPath();
        context.arc(point.x, point.y, 5, 0, Math.PI * 2, false);
        context.stroke();
        context.fill();
    });
}

drawGrid('lightgray', 10, 10);

drawControlPoints();
drawEndPoints();
drawBezierCurve();

多邊形的繪制

使用moveTo()與lineTo()方法,再結合一些簡單的三角函數,就可以繪制出任意邊數的多邊形。


多邊形的繪制

代碼核心部分

function getPolygonPoints(centerX, centerY, radius, sides, startAngle) {
    let points = [],
        angle = startAngle || 0;

    for (let i = 0; i < sides; ++i) {
        points.push(new Point(centerX + radius * Math.sin(angle),
            centerY - radius * Math.cos(angle)));
        angle += 2 * Math.PI / sides;
    }

    return points;
}

function createPolygonPath(centerX, centerY, radius, sides, startAngle) {
    let points = getPolygonPoints(centerX, centerY, radius, sides, startAngle);

    context.beginPath();
    context.moveTo(points[0].x, points[0].y);

    for (let i = 1; i < sides; ++i) {
        context.lineTo(points[i].x, points[i].y);
    }
    context.closePath();
}

function drawRubberbandShape(loc, sides, startAngle) {
    createPolygonPath(mousedown.x, mousedown.y, rubberbandRect.width,
        parseInt(sidesSelect.value), (Math.PI / 180) * parseInt(startAngleSelect.value));
    context.stroke();

    if (fillCheckbox.checked) {
        context.fill();
    }
}
多邊形對象

修改以上應用程序,讓其維護一份多邊形對象的列表。

function drawRubberbandShape(loc, sides, startAngle) {
    let polygon = new Polygon(mousedown.x, mousedown.y,
        rubberbandRect.width,
        parseInt(sidesSelect.value),
        (Math.PI / 180) * parseInt(startAngleSelect.value),
        context.strokeStyle,
        context.fillStyle,
        fillCheckbox.checked);

    context.beginPath();
    polygon.createPath(context);
    polygon.stroke(context);

    if (fillCheckbox.checked) {
        polygon.fill(context);
    }

    if (!dragging) {
        polygons.push(polygon);
    }
}

其所實現的多邊形對象包含以下方法:

  • points[] getPoints()
  • void createPath(context)
  • void stroke(context)
  • void fill(context)
  • void move(x,y)

在創建多邊形時,需要指定其位置。該位置指的是多邊形外接圓的圓心,同時需要指定外接圓的半徑、多邊形的邊數、多邊形第一個頂點的起始角度、多邊形的描邊與填充風格,以及該多邊形是否需要被填充。

Polygon對象可以生成一個用以表示其頂點的數組,它可以根據這些點來創建代表此多邊形的路徑,也可以對該路徑進行描邊或填充操作。可以調用其move()方法來移動它的位置。

高級路徑操作

為了追蹤所畫的內容,諸如畫圖應用程序、CAD系統以及游戲等應用程序,都會維護一份包含當前顯示對象的列表。通常來說,這些應用程序都允許用戶對當前顯示在屏幕上的物體進行操作(選擇、移動、縮放等)。

拖動多邊形對象

繪制(draw)模式

編輯(edit)模式

該應用程序維護一份含有Polygon對象的數組。當在編輯模式下檢測到鼠標按下事件時,應用程序會遍歷這個數組,為每個多邊形都創建一條路徑,然后檢測鼠標按下的位置是否在路徑內。如果是的話,應用程序就會將指向該多邊形的引用保存起來,同時還會保存多邊形左上角與鼠標按下位置之間的X、Y坐標偏移量。
從這時起,應用程序中的鼠標事件處理器就會根據鼠標的移動來同時移動被選中的那個多邊形。

編輯貝塞爾曲線
編輯貝塞爾曲線,拖動貝塞爾曲線的端點與控制點
自動滾動網頁,使某段路徑所對應的元素顯示在視窗中

scrollPathIntoView()方法主要用于(在小屏幕的手機上)開發移動應用程序。開發者可以使用這個方法讓網頁自行滾動,從而將屏幕外的某部分canvas內容顯示到視窗之內。
目前大多數瀏覽器并未支持此方法。

坐標變換

將坐標原點從其默認位置屏幕左上角,移動到其他地方,通常是非常有用的。
在計算canvas之中的圖形與文本位置時,通過移動坐標原點,可以簡化計算過程。

  • 坐標系的平移、縮放與旋轉



    坐標系的平移與旋轉

繪制具有某一給定旋轉角度的多邊形

function drawPolygon(polygon, angle) {
    let tx = polygon.x,
        ty = polygon.y;

    context.save();

    context.translate(tx, ty);

    if (angle) {
        context.rotate(angle);
    }

    polygon.x = 0;
    polygon.y = 0;

    polygon.createPath(context);
    context.stroke();

    if (fillCheckbox.checked) {
        context.fill();
    }

    context.restore();

    polygon.x = tx;
    polygon.y = ty;
}
  • 自定義的坐標變換
    無法直接通過組合運用scale()、rotate()或translate()方法來達成想要的效果時,就必須直接操作變換矩陣。
CanvasRenderingContext2D.transform()
CanvasRenderingContext2D.setTransform()

圖像合成

組合 Compositing
CanvasRenderingContext2D.globalCompositeOperation

剪輯區域

  • 通過剪輯區域來擦除圖像

  • 利用剪輯區域來制作伸縮式動畫

</br>
由于篇幅限制,后面的內容將放到下一篇文章內

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容