《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>
由于篇幅限制,后面的内容将放到下一篇文章内

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容