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

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


基礎知識

canvas元素

canvas元素的能力是通過Canvas的context對象表現(xiàn)出來的哩簿。該環(huán)境變量可以從canvas元素身上獲取。

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

  1. 使用document.getElementById()方法來獲取指向canvas的引用胞锰。
  2. 在canvas對象上調(diào)用getContext('2d')方法厨姚,獲取繪圖環(huán)境變量。
  3. 使用繪圖環(huán)境對象在canvas元素上進行繪制猎醇。

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

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

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

易錯點及提示小結

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

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

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

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

Canvas的繪圖環(huán)境

canvas元素僅僅是為了充當繪圖環(huán)境對象的容器而存在的橄镜,該環(huán)境對象提供了全部的繪制功能偎快。

2d繪圖環(huán)境

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

  • CanvasRenderingContext2D對象所含的屬性

屬性 | 簡介
-|
canvas | 指向該繪圖環(huán)境所屬的canvas對象翔横。最常見的用途是獲取canvas的寬度(context.canvas.width)和高度(context.canvas.height)
fillstyle | 指向該繪圖環(huán)境在后續(xù)的圖形填充操作中所使用的顏色读跷、漸變色或圖案
font | 設定在調(diào)用繪圖環(huán)境對象的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 | 該值決定了瀏覽器該如何延伸陰影效果。值越高祝辣,陰影效果延伸得就越遠贴妻。該值不是指陰影的像素長度,而是代表高斯模糊方程式中的參數(shù)值蝙斜。它必須是一個非負且非無窮量的double值名惩,默認值是0
shadowColor | 該值告訴瀏覽器使用何種顏色來繪制陰影(通常采用半透明色作為該屬性的值)
shadowOffsetX | 以像素為單位,指定了陰影效果的水平方向偏移量
shadowOffsetY | 以像素為單位孕荠,指定了陰影效果的垂直方向偏移量
strokeStyle | 指定了對路徑進行描邊時所用的繪制風格娩鹉。該值可被設定為某個顏色、漸變色或圖案
textAlign | 決定了以fillText()或stroText()方法進行繪制時稚伍,所畫文本的水平對齊方式
textBaseline | 決定了以fillText()或stroText()方法進行繪制時弯予,所畫文本的垂直對齊方式

在Canvas中,有一個與2d繪圖環(huán)境相對應的3d繪圖環(huán)境个曙,叫做WebGL锈嫩,它完全符合OpenGL ES2.0的API
Canvas狀態(tài)的保存與恢復

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

Canvas的API提供了save()和restore()兩個方法呼寸,用于保存及恢復當前canvas繪圖環(huán)境的所有屬性。
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()方法可以嵌套式調(diào)用
繪圖環(huán)境的save()方法會將當前的繪圖環(huán)境壓入堆棧頂部猴贰。對應的restore()方法則會從堆棧頂部彈出一組狀態(tài)信息对雪,并據(jù)此恢復當前繪圖環(huán)境的各個狀態(tài)。這意味著可以嵌套式地調(diào)用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中增加一個事件監(jiān)聽器慌植。
例如,要監(jiān)聽“按下鼠標事件”义郑,可以:

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

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

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

將鼠標坐標轉換為Canvas坐標
瀏覽器通過事件對象傳遞給監(jiān)聽器的鼠標坐標蝶柿,是窗口坐標,而不是相對于canvas自身的坐標非驮。所以需要坐標轉換交汤。

例子:精靈表坐標查看器
該應用程序向canvas注冊了一個mousemove事件監(jiān)聽器,等到瀏覽器回調(diào)這個監(jiān)聽時,應用程序會將相對于窗口的鼠標坐標轉換為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對象上調(diào)用getBoundingClientRect()方法星岗,來獲取canvas元素的邊界框(bounding box),該邊界框的坐標是相對于整個窗口的戒洼。然后俏橘,windowToCanvas()方法返回了一個對象,其x與y屬性分別對應于鼠標在canvas之中的坐標圈浇。

精靈表坐標查看器

Tips

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

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

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

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

注意:canvas是一個不可獲取焦點的元素。
所以掉伏,在canvas元素上新增鍵盤事件監(jiān)聽器是徒勞的缝呕。

一共有三種鍵盤事件:

  • keydown
  • keypress
  • keyup
觸摸事件

用于智能手機與平板上。

繪制表面的保存與恢復

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

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

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

立即模式繪圖系統(tǒng)
canvas元素采用“立即模式”繪制圖形,即它會立即繪制箍镜,然后源祈,立即忘記剛才繪制的內(nèi)容。(SVG等繪圖系統(tǒng)則是“保留模式”繪圖系統(tǒng))

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

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控件結合起來使用色迂,以便讓用戶可以通過輸入數(shù)值或其他方式來控制程序香缺。
為了讓HTML控件看上去好像是出現(xiàn)在canvas范圍內(nèi),可以使用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規(guī)范書中規(guī)定:采用絕對定位方式的元素將被繪制在采用相對定位方式的元素之上。
示例代碼中canvas元素的position屬性值為默認值relative,玻璃窗格采用絕對定位祸轮,所以玻璃窗格會顯示在canvas之上兽埃。
如果兩個元素都采用相對或絕對定位,那么适袜,改變這兩個元素的順序也可以達到同樣的效果柄错,或者調(diào)整其CSS中的z-index屬性。

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

JavaScript代碼片段:

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

let paused = true;
...

// 根據(jù)應用程序當前的狀態(tài)來啟動或暫停動畫效果
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元素

動畫效果展示

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

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

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

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

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

JavaScript代碼略

打印Canvas的內(nèi)容

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

Canvas的API提供了一個toDataURL()方法脯爪,該方法返回的引用,指向了某個給定canvas元素的數(shù)據(jù)地址矿微。接下來痕慢,將img元素的src屬性值設置為這個數(shù)據(jù)地址,就可以創(chuàng)建一幅表示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。
作用:提高性能名秀;于幕后完成顯示模式的切換励负。

基礎數(shù)學知識

需回顧的數(shù)學內(nèi)容

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


</br>

繪制

坐標系統(tǒng)

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

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

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

Canvas的繪制模型

瀏覽器起初會將圖形或圖像繪制到一張“無限大的位圖”上,在繪制時會使用Canvas繪圖環(huán)境對象中與圖形的填充及描邊有關的那些屬性。
接下來闹啦,如果啟用了陰影效果的話,那么瀏覽器將會把陰影渲染到另外一張位圖上苛秕。并將陰影中每個像素alpha值乘以globalAlpha屬性颖杏,把運算結果設置為該陰影像素的透明度,并將陰影與canvas元素進行圖像合成政溃。操作時采用當前的合成設定趾访,并按照當前的剪輯區(qū)域對合成之后的位圖進行剪切。
最后董虱,瀏覽器會根據(jù)當前的合成設定與剪輯區(qū)域扼鞋,將圖形或位圖與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規(guī)范描述了繪制這些圓角的詳細過程溃槐,沒有留下什么自由發(fā)揮的余地。如果想要控制諸如圓角半徑之類的一些屬性科吭,那么必須自己來繪制這些圓角昏滴。

顏色與透明度

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

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

漸變色與圖案

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

線性(linear)漸變
通過調(diào)用createLinearGradient()方法來創(chuàng)建猖闪。
調(diào)用之后鲜棠,該方法會返回一個CanvasGradient實例。

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

在創(chuàng)建好漸變色之后表鳍,通過調(diào)用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)漸變
通過調(diào)用createRadialGradient()方法來創(chuàng)建祥诽。

接下來與線性漸變類似譬圣。

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()方法來創(chuàng)建圖案厘熟。

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>



陰影

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

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

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

  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繪圖環(huán)境對象也可以在對文本或路徑進行描邊時繪制陰影效果份招。

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

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

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

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()方法的調(diào)用,使得后續(xù)被調(diào)用的stroke()方法以及此方法所生成的陰影涩盾,
       都被局限在這個圓形的范圍之內(nèi) */
    drawingContext.clip();
    drawingContext.stroke();

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

路徑十气、描邊與填充

大多數(shù)繪制系統(tǒng),如SVG等春霍,都是基于路徑的砸西。使用這些繪制系統(tǒng)時,需要先定義一個路徑址儒,然后再對其進行描邊或填充芹枷,也可以在描邊的同時進行填充。


圖形的描邊與填充效果

該應用程序創(chuàng)建了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規(guī)范將其稱為“當前路徑”(current path)熄攘。然而兽愤,這條路徑卻可以包含許多子路徑(subpath)。而子路徑挪圾,又是由兩個或更多的點組成的浅萧。

二次調(diào)用beginPath()方法,會清除上一次調(diào)用某繪圖方法時所創(chuàng)建的子路徑哲思。如果沒有調(diào)用beginPath()方法來清除原有的子路徑洼畅,則第二次對某繪圖方法的調(diào)用,會向當前路徑中增加一條子路徑棚赔。

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

剪紙效果

運用路徑、陰影以及非零環(huán)繞原則等知識壳快,實現(xiàn)如下圖所示的剪紙(cutout)效果纸巷。


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

這段代碼創(chuàng)建了一條路徑,它由兩個圓形組成眶痰,其中一個圓形在另一個的內(nèi)部瘤旨,通過設定arc()方法的最后一個參數(shù)值,分別以順竖伯、逆時針方向繪制內(nèi)存哲、外部的圓形。

在創(chuàng)建好路徑之后黔夭,應用程序就對該路徑進行了填充宏胯。瀏覽器運用“非零環(huán)繞規(guī)則”,對外圍圓形的內(nèi)部進行了填充本姥,而填充的范圍并不包括里面的圓肩袍,這就產(chǎn)生了一種剪紙圖案的效果。

代碼核心部分

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()方法可以讓調(diào)用者控制圓弧的繪制方向,然而rect()方法則總是按照順時針方向來創(chuàng)建路徑艰管。
在本例中需要一條逆時針的矩形路徑滓侍,所以需要自己創(chuàng)建一個rect()方法,使得它像arc()一樣牲芋,可以讓調(diào)用者控制矩形路徑的方向:

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();
    }
}

內(nèi)部的矩形剪紙圖形:

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

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

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

</br>

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

線段

Canvas繪圖環(huán)境提供了兩個可以用來創(chuàng)建路徑的方法:moveTo()與lineTo()裂逐。在創(chuàng)建路徑之后調(diào)用stroke()方法歹鱼,才能使線性路徑出現(xiàn)在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() | 向當前路徑中增加一條子路徑卜高,該子路徑只包含一個點(由參數(shù)傳入)弥姻。該方法并不會從當前路徑中清除任何子路徑。
lineTo() | 如果當前路徑中沒有子路徑掺涛,則這個方法的行為與moveTo()方法一樣庭敦;如果當前路徑中存在子路徑,那么該方法會將所指定的那個點加入子路徑中薪缆。

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

    左為在像素邊界處繪制線段诞外,右為在某個像素范圍內(nèi)繪制線段

  • 網(wǎng)格的繪制


    繪制網(wǎng)格
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);
  • 虛線的繪制


    虛線的繪制

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

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();
  • 以橡皮筋式輔助線來協(xié)助用戶畫圓
    可以讓用戶以拖動鼠標的方式畫圓。當拖動鼠標時正什,該應用程序會持續(xù)地繪制圓形啥纸。


    以橡皮筋式輔助線來協(xié)助用戶畫圓

代碼核心部分

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()方法創(chuàng)建一條代表三次方貝塞爾曲線的路徑荣暮。
這段代碼除了繪制曲線本身庭惜,還填充了曲線控制點與錨點的小圓圈。

三次方貝塞爾曲線

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()方法穗酥,再結合一些簡單的三角函數(shù)护赊,就可以繪制出任意邊數(shù)的多邊形。


多邊形的繪制

代碼核心部分

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);
    }
}

其所實現(xiàn)的多邊形對象包含以下方法:

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

在創(chuàng)建多邊形時,需要指定其位置蜓席。該位置指的是多邊形外接圓的圓心器一,同時需要指定外接圓的半徑、多邊形的邊數(shù)厨内、多邊形第一個頂點的起始角度祈秕、多邊形的描邊與填充風格,以及該多邊形是否需要被填充雏胃。

Polygon對象可以生成一個用以表示其頂點的數(shù)組请毛,它可以根據(jù)這些點來創(chuàng)建代表此多邊形的路徑,也可以對該路徑進行描邊或填充操作瞭亮》椒拢可以調(diào)用其move()方法來移動它的位置。

高級路徑操作

為了追蹤所畫的內(nèi)容统翩,諸如畫圖應用程序仙蚜、CAD系統(tǒng)以及游戲等應用程序,都會維護一份包含當前顯示對象的列表厂汗。通常來說委粉,這些應用程序都允許用戶對當前顯示在屏幕上的物體進行操作(選擇、移動娶桦、縮放等)贾节。

拖動多邊形對象

繪制(draw)模式

編輯(edit)模式

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

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

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

坐標變換

將坐標原點從其默認位置屏幕左上角照卦,移動到其他地方式矫,通常是非常有用的。
在計算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

剪輯區(qū)域

  • 通過剪輯區(qū)域來擦除圖像

  • 利用剪輯區(qū)域來制作伸縮式動畫

</br>
由于篇幅限制察绷,后面的內(nèi)容將放到下一篇文章內(nèi)

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市津辩,隨后出現(xiàn)的幾起案子拆撼,更是在濱河造成了極大的恐慌,老刑警劉巖喘沿,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闸度,死亡現(xiàn)場離奇詭異,居然都是意外死亡蚜印,警方通過查閱死者的電腦和手機筋岛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晒哄,“玉大人,你說我怎么就攤上這事肪获∏蘖瑁” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵孝赫,是天一觀的道長较木。 經(jīng)常有香客問我,道長青柄,這世上最難降的妖魔是什么伐债? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任预侯,我火速辦了婚禮,結果婚禮上峰锁,老公的妹妹穿的比我還像新娘萎馅。我一直安慰自己,他們只是感情好虹蒋,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布糜芳。 她就那樣靜靜地躺著,像睡著了一般魄衅。 火紅的嫁衣襯著肌膚如雪峭竣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天晃虫,我揣著相機與錄音皆撩,去河邊找鬼。 笑死哲银,一個胖子當著我的面吹牛扛吞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播盘榨,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼喻粹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了草巡?” 一聲冷哼從身側響起守呜,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎山憨,沒想到半個月后查乒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡郁竟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年玛迄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棚亩。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡蓖议,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出讥蟆,到底是詐尸還是另有隱情勒虾,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布瘸彤,位于F島的核電站修然,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜愕宋,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一玻靡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧中贝,春花似錦囤捻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至老厌,卻和暖如春瘟则,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背枝秤。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工醋拧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人淀弹。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓丹壕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親薇溃。 傳聞我的和親對象是個殘疾皇子菌赖,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容