html5: Canvas 绘制基本图形
从绘制直线 到 路径
1. Canvas 相关概念
1.1 什么是Canvas
Canvas是 HTML5 的 2D图形技术之一,是一门纯JavaScript操作的图形技术。另一个是2D图形技术为 SVG。这两者的区别在于:
Canvas |
SVG |
Canvas是基于位图的,放大图形会导致失真,适用于像素处理和动态渲染 | SVG是基于矢量图的,放大图形不会导致失真,但不适用于像素处理和适合静态描述 |
Canvas是使用JavaScript动态生成的 | SVG是使用XML静态描述的 |
若发生修改,使用Canvas需要重绘 | 使用SVG需要重绘 |
1.2 Canvas坐标系
Canvas使用的坐标系是 W3C坐标系 这与数学中的直角坐标系中的区别在于,数学直角坐标系y轴
正方向向上,而W3C坐标系中的y轴
正方向向下:
1.3 Canvas元素
1.3.1 基本用法
Canvas 使用一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式,以下是 HTML 中的一个Canvas元素:
<canvas id="canvas" width="300" height="200"></canvas>
要操作Canvas绘图首先要获取 HTML <canvas> 元素的引用,然后获取这个元素的context,需要依次使用以下方法:
方法 | 描述 |
Document.getElementById() |
取HTML 元素的引用 |
getContext() |
获取这个元素的context,图像稍后将在此被渲染。 |
如获取上面id
属性值为 canvas
的上下文对象(context):
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d');
在 HTML 中,你需要指定canvas
元素的宽度和高度作为你的画布大小,宽度和高度都为纯数字。
后文默认使用变量名 cts 表示一个已经获取的Canvas上下文对象(CanvasRenderingContext2D) |
1.3.2 在 TypeScript 中的用法
在TypeScript中你需要指定类型。在这里,getElementById()
方法返回的类型应该被断言为 HTMLCanvasElement 而非直接断言成普通的 HTMLElement。
HTMLCanvasElement 接口提供用于操作元素布局和表示的属性和方法。HTMLCanvasElement 接口还继承了HTMLElement 接口的属性和方法。
<canvas id="canvas" width="300" height="200" ></canvas> <script lang="ts"> let canvas = document.getElementById("canvas") as HTMLCanvasElement; let ctx = canvas.getContext("2d") as CanvasRenderingContext2D; </script>
2. Canvas 绘制直线
2.1 直线与路径:从绘制基本线段开始
不同于 SVG, 只支持两种形式的图形绘制:矩形 和 路径(由一系列点连成的直线段)。所有其他类型的图形都是通过一条或者多条路径组合而成的。不过,我们拥有众多路径生成的方法让复杂图形的绘制成为了可能。
我们可以使用以下两个方法来配合绘制直线:
方法 | 描述 | 说明 |
moveTo (x, y) |
将一个新的子路径的起始点移动到(x,y)坐标 | - |
lineTO (x, y) |
使用直线连接子路径的终点到x,y坐标的方法 | 并不会真正地绘制 |
var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); ctx.beginPath(); ctx.moveTo(50,50); ctx.lineTo(200, 50); ctx.stroke();
2.1.1 CanvasRenderingContext2D 对象的 moveTo()
方法
将一个 新的子路径的起始点
移动到(x,y)坐标的方法。
语法
void ctx.moveTo(x, y);
2.1.2 CanvasRenderingContext2D 对象的 lineTo()
方法
使用直线连接子路径的终点到x,y坐标的方法(并不会真正地绘制)
语法
void ctx.lineTo(x, y);
参数
参数 | 描述 |
x |
点的 x 轴。 |
y |
点的 y 轴。 |
2.1.3 CanvasRenderingContext2D 对象的 stroke()
方法
使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。
语法
void ctx.stroke(); void ctx.stroke(path);
参数
参数 | 描述 | 说明 |
path | 绘制Path2D 的路径 |
Path2D 对象 用于声明路径 |
2.2. Canvas 绘制矩形
2.2.1 通过绘制直线的方法绘制矩形
ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.moveTo(0,0); ctx.lineTo(160,0); ctx.lineTo(160,100); ctx.lineTo(0,100); ctx.lineTo(0,0); ctx.stroke();
效果如下:
2.2.2 通过 CanvasRenderingContext2D 对象的矩形方法
2.2.2.1 rect
(x, y, width, height) 方法
创建矩形路径的方法,矩形的起点位置是 (x, y) ,尺寸为 width 和 height。矩形的4个点通过直线连接,子路径做为闭合的标记,所以你可以填充或者描边矩形。
语法
void ctx.rect(x, y, width, height);
例如
ctx.beginPath(); ctx.rect(10, 30, 200, 100); ctx.fill();
效果如下:
2.2.2.2 fillRect
(x, y, width, height)
CanvasRenderingContext2D.fillRect()
函数是Canvas 2D API 绘制填充矩形的方法。当前渲染上下文中的fillStyle 属性决定了对这个矩形对的填充样式。
这个方法是直接在画布上绘制填充,并不修改当前路径,所以在这个方法后面调用 fill() 或者stroke()方法并不会对这个方法有什么影响。
语法
void ctx.fillRect(x, y, width, height);
参数:
x
:矩形起始点的 x 轴坐标。y
:矩形起始点的 y 轴坐标。width
:矩形的宽度。height
:矩形的高度。
fillRect()
方法绘制一个填充了内容的矩形,这个矩形的开始点(左上点)在(x, y) ,它的宽度和高度分别由width
和 height
确定,填充样式由当前的fillStyle 决定。
例如
ctx.fillStyle = 'yellow'; ctx.fillRect(60, 20, 200, 100);
效果如下:
2.2.2.3 strokeRect
(x, y, width, height)
该方法使用当前的绘画样式,描绘一个起点在 (x, y) 、宽度为 w 、高度为 h 的矩形的方法。
语法
void ctx.strokeRect(x, y, width, height);
例如
ctx.shadowColor = '#5F9CFA'; ctx.shadowBlur = 30; ctx.lineJoin = 'bevel'; ctx.lineWidth = 15; ctx.strokeStyle = '#5FFA7A'; ctx.strokeRect(30, 30, 90, 90);
效果如下:
2.2.2.4 clearRect
(x, y, width, height)
该方法通过把像素设置为透明以达到擦除一个矩形区域的目的。
语法
void ctx.clearRect(x, y, width, height);
参数 | 描述 |
x | 矩形起点的 x 轴坐标。 |
y | 矩形起点的 y 轴坐标。 |
width | 矩形的宽度。 |
height | 矩形的高度。 |
例如
除整个画布:
ctx.clearRect(0, 0, canvas.width, canvas.height);
清除画布一个区域显现出矩形:
// 绘制背景色 ctx.beginPath(); ctx.fillStyle = '#eca601'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制矩形清除区域 ctx.clearRect(10, 10, 120, 100);
效果如下:
3 Canvas 路径
3.1 什么是 Canvas 路径
在Canvas中除了下一节中的矩形提供了直接绘制方法,其它的所有Canvas基本图形,包括上一小节中的直线,以及圆形、弧线、贝塞尔曲线,都是以路径为基础进行绘制的。
3.2 操作 Canvas 路径的方法
1. 操作路径
方法 | 描述 | 说明 |
beginPath() | 清空子路径列表开始一个新的路径。 | 当你想创建一个新的路径时,调用此方法。 |
closePath() | 使笔点返回到当前子路径的起始点。它尝试从当前点到起始点绘制一条直线。 | 如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。 |
moveTo() | 将一个新的子路径的起始点移动到(x,y)坐标。 | 参见 2.1.1 节 |
lineTo() | 使用直线连接子路径的最后的点到x,y坐标。 | 参见 2.1.2 节 |
bezierCurveTo() | 添加一个3次贝赛尔曲线路径。该方法需要三个点。 第一、第二个点是控制点,第三个点是结束点。起始点是当前路径的最后一个点,绘制贝赛尔曲线前,可以通过调用 moveTo() 进行修改。 | |
quadraticCurveTo() | 添加一个2次贝赛尔曲线路径。 | |
arc() | 绘制一段圆弧路径, 圆弧路径的圆心在 (x, y) 位置,半径为 r ,根据anticlockwise (默认为顺时针)指定的方向从 startAngle 开始绘制,到 endAngle 结束。 | |
arcTo() | 根据控制点和半径绘制圆弧路径,使用当前的描点(前一个moveTo或lineTo等函数的止点)。根据当前描点与给定的控制点1连接的直线,和控制点1与控制点2连接的直线,作为使用指定半径的圆的切线,画出两条切线之间的弧线路径。 | |
ellipse() | 添加一个椭圆路径,椭圆的圆心在(x,y)位置,半径分别是radiusX 和 radiusY ,按照anticlockwise (默认顺时针)指定的方向,从 startAngle 开始绘制,到 endAngle 结束。 | 实验中的功能 |
rect() | 创建一个矩形路径,矩形的起点位置是 (x, y) ,尺寸为 width 和 height。 | |
stroke() | 使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。 | 参见 2.1.3节 |
2. 绘制路径
方法 | 描述 | 说明 |
fill() | 使用当前的样式填充子路径。 | |
stroke() | 使用当前的样式描边子路径。 | |
drawFocusIfNeeded() | 如果给定的元素获取了焦点,那么此方法会在当前的路径绘制一个焦点。 | |
scrollPathIntoView() | 将当前或给定的路径滚动到窗口。 | |
clip() | 从当前路径创建一个剪切路径。在 clip() 调用之后,绘制的所有信息只会出现在剪切路径内部。 | |
isPointInPath() | 判断当前路径是否包含检测点。 | |
isPointInStroke() | 判断检测点是否在路径的描边线上。 |
3.2.1 beginPath
() 方法
清空子路径列表开始一个新的路径。当你想创建一个新的路径时,调用此方法。
语法
void ctx.beginPath();
例如
// 第一条路径 ctx.beginPath(); ctx.strokeStyle = 'blue'; ctx.moveTo(30,30); ctx.lineTo(0,200); ctx.stroke(); // 第二条路径 ctx.beginPath(); ctx.strokeStyle = '#FE5B5C'; ctx.moveTo(30,30); ctx.lineTo(120,120); ctx.stroke();
效果如下:
效果图(其中边框是为了展示canvas画布区域有意添加上去的)
3.2.2 closePath
() 方法
该方法 将笔点返回到当前子路径起始点 。它尝试从当前点到起始点绘制一条直线。 如果图形已经是封闭的或者只有一个点,那么此方法不会做任何操作。
语法
void ctx.closePath();
例子
不使用该方法时:
ctx.beginPath(); ctx.strokeStyle = 'blue'; ctx.moveTo(0,0); ctx.lineTo(200,20); ctx.lineTo(120,120); ctx.stroke();
效果如下:
效果图(其中边框是为了展示canvas画布区域有意添加上去的)
使用该方法返回到当前子路径起始点后:
ctx.beginPath(); ctx.strokeStyle = 'blue'; ctx.moveTo(0,0); ctx.lineTo(200,20); ctx.lineTo(120,120); ctx.closePath(); // 绘制三角形的最后一条线 ctx.stroke();
效果如下:
效果图(其中边框是为了展示canvas画布区域有意添加上去的)
3.2.3 bezierCurveTo
(cp1x, cp1y, cp2x, cp2y, x, y) 方法
该方法用于绘制三次贝赛尔曲线路径。
贝塞尔曲线它是依据四个位置任意的点坐标绘制出的一条光滑曲线。它通过控制 曲线上的四个点 (起始点、终止点 以及 两个相互分离的中间点 )来创造、编辑图形。
其中起重要作用的是位于曲线中央的控制线。这条线是虚拟的,中间与贝塞尔曲线交叉,两端是控制端点。移动两端的端点时贝塞尔曲线改变曲线的曲率(弯曲的程度);移动中间点(也就是移动虚拟的控制线)时,贝塞尔曲线在起始点和终止点锁定的情况下做均匀移动。
语法
void ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
参数 | 描述 |
cp1x |
第一个控制点的 x 轴坐标。 |
cp1y |
第一个控制点的 y 轴坐标。 |
cp2x |
第二个控制点的 x 轴坐标。 |
cp2y |
第二个控制点的 y 轴坐标。 |
x |
结束点的 x 轴坐标。 |
y |
结束点的 y 轴坐标。 |
例如:
ctx.beginPath(); ctx.moveTo(20,20); ctx.bezierCurveTo(230, 30, 150, 60, 50, 100); ctx.stroke(); // 绘制相关点 ctx.fillStyle = 'blue'; ctx.fillRect(20, 20, 10, 10);// 起点 ctx.fillRect(50, 100, 10, 10);// 终点 ctx.fillStyle = 'red'; ctx.fillRect(230, 30, 10, 10);// 控制点1 ctx.fillRect(150, 70, 10, 10);// 控制点2
效果如下:
3.2.4 quadraticCurveTo
(cpx, cpy, x, y) 方法
该方法是 Canvas 2D API 新增 二次贝塞尔曲线路径 的方法。它需要2个点。 第一个点是控制点
,第二个点是终点
。 起始点
是 当前路径最新的点,当创建二次贝赛尔曲线之前,可以使用 moveTo()
方法进行改变。
语法
void ctx.quadraticCurveTo(cpx, cpy, x, y);
参数 | 描述 |
cpx | 控制点的 x 轴坐标。 |
cpy | 控制点的 y 轴坐标。 |
x | 终点的 x 轴坐标。 |
y | 终点的 y 轴坐标。 |
例如
ctx.beginPath(); ctx.moveTo(50, 20); ctx.quadraticCurveTo(230, 30, 50, 100); ctx.stroke(); ctx.fillStyle = 'blue'; ctx.beginPath(); ctx.arc(50, 20, 5, 0, 2 * Math.PI); // 起始点 ctx.arc(50, 100, 5, 0, 2 * Math.PI); // 终点 ctx.fill(); ctx.fillStyle = 'red'; ctx.beginPath(); ctx.arc(230, 30, 5, 0, 2 * Math.PI); // 控制点 ctx.fill();
效果如下:
3.2.5 arc
(x, y, radius, startAngle, endAngle, anticlockwise) 方法
该方法用于绘制圆弧路径。 圆弧路径的圆心在 (x, y) 位置,半径为 r ,根据anticlockwise (默认为顺时针)指定的方向从 startAngle 开始绘制,到 endAngle 结束。
语法
void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
参数 | 描述 |
x |
圆弧中心(圆心)的 x 轴坐标。 |
y |
圆弧中心(圆心)的 y 轴坐标。 |
radius |
圆弧的半径。 |
startAngle |
圆弧的起始点, x轴方向开始计算,单位以弧度表示。 |
endAngle |
圆弧的终点, 单位以弧度表示。 |
anticlockwise |
可选的Boolean 值 ,如果为 true,逆时针绘制圆弧,反之,顺时针绘制。 |
例如:
ctx.beginPath(); ctx.arc(50, 50, 50, 0, 2 * Math.PI); ctx.stroke();
效果如下:
3.2.6 arcTo
(x1, y1, x2, y2, radius) 方法
该方法根据控制点和半径绘制圆弧路径,使用当前的描点(前一个moveTo或lineTo等函数的止点)。根据当前描点与给定的控制点1连接的直线,和控制点1与控制点2连接的直线,作为使用指定半径的圆的切线,画出两条切线之间的弧线路径。
语法
void ctx.arcTo(x1, y1, x2, y2, radius);
参数 | 描述 |
x1 |
第一个控制点的 x 轴坐标。 |
y1 |
第一个控制点的 y 轴坐标。 |
x2 |
第二个控制点的 x 轴坐标。 |
y2 |
第二个控制点的 y 轴坐标。 |
radius |
圆弧的半径。 |
例如
ctx.setLineDash([]) ctx.beginPath(); ctx.moveTo(150, 20); ctx.arcTo(150,100,50,20,30); ctx.stroke(); ctx.fillStyle = 'blue'; ctx.fillRect(150, 20, 10, 10); // 基本点 ctx.fillStyle = 'red'; ctx.fillRect(150, 100, 10, 10); // 控制点1 ctx.fillRect(50, 20, 10, 10); // 控制点2 ctx.setLineDash([5,5]) ctx.moveTo(150, 20); ctx.lineTo(150,100); ctx.lineTo(50, 20); ctx.stroke(); ctx.beginPath(); ctx.arc(120,38,30,0,2*Math.PI); ctx.stroke();
效果如下:
3.2.7 ellipse
(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) 方法
语法
void ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
参数 | 描述 |
x |
椭圆圆心的 x 轴坐标。 |
y |
椭圆圆心的 y 轴坐标。 |
radiusX |
椭圆长轴的半径。 |
radiusY |
椭圆短轴的半径。 |
rotation |
椭圆的旋转角度,以弧度表示(非角度度数)。 |
startAngle |
将要绘制的起始点角度,从 x 轴测量,以弧度表示(非角度度数)。 |
endAngle |
椭圆将要绘制的结束点角度,以弧度表示(非角度度数)。 |
anticlockwise |
可选的 Boolean 选项,如果为 true,逆时针方向绘制椭圆 (逆时针), 反之顺时针方向绘制。 |
3.2.8 fill
() 方法
该方法根据当前的填充样式,填充当前或已存在的路径的方法。采取非零环绕或者奇偶环绕规则。
语法
void ctx.fill(); void ctx.fill(fillRule); void ctx.fill(path, fillRule);
参数
参数 | 描述 | 说明 |
fillRule | 一种算法,决定点是在路径内还是在路径外。 | 允许的值:"nonzero" : 非零环绕规则, 默认的规则。"evenodd" : 奇偶环绕规则。 |
path | 需要填充的Path2D 路径。 | - |
例子
ctx.beginPath(); ctx.strokeStyle = 'blue'; ctx.moveTo(0,0); ctx.lineTo(200,90); ctx.lineTo(120,120); ctx.closePath(); ctx.stroke(); ctx.fill();
效果如下:
3.2.10 drawFocusIfNeeded
() 方法
该方法用来给 当前路径 或 特定路径 绘制焦点
的方法,如果给定的元素获取了焦点。
语法
void ctx.drawFocusIfNeeded(element); void ctx.drawFocusIfNeeded(path, element);
参数 | 描述 |
element |
是否需要设置焦点的元素。 |
path |
Path2D 路径。 |
例如
<canvas id="canvas" width="300" height="200" > <input id="button" type="range" min="1" max="12"> </canvas> <script lang="ts"> let canvas = document.getElementById("canvas") as HTMLCanvasElement; let ctx = canvas.getContext("2d") as CanvasRenderingContext2D; let button = document.getElementById("button") as HTMLElement ; </script>
效果如下:
3.2.11 scrollPathIntoView
() 方法
该方法将当前或给定的路径滚动到窗口的方法。类似于 Element.scrollIntoView()
。
语法
void ctx.scrollPathIntoView(); void ctx.scrollPathIntoView(path);
参数 | 描述 |
path | 使用的Path2D 路径。 |
例如
ctx.beginPath(); ctx.fillRect(10, 10, 30, 30); ctx.scrollPathIntoView();
3.2.12 clip
() 方法
语法
void ctx.clip(); void ctx.clip(fillRule); void ctx.clip(path, fillRule);
参数 | 描述 | 说明 |
fillRule | 这个算法判断一个点是在路径内还是在路径外。 | 允许的值: “nonzero”: 非零环绕原则,默认的原则。 “evenodd”: 奇偶环绕原则。 |
path | 需要剪切的 Path2D 路径。 | - |
例如
ctx.beginPath(); ctx.arc(100, 100, 50, 0, Math.PI*2, false); ctx.clip(); ctx.fillRect(0, 0, 100,100);
效果如下:
3.2.13 isPointInPath
() 方法
语法
boolean ctx.isPointInPath(x, y); boolean ctx.isPointInPath(x, y, fillRule); boolean ctx.isPointInPath(path, x, y); boolean ctx.isPointInPath(path, x, y, fillRule);
参数 | 描述 | 说明 |
x |
检测点的X坐标 | |
y |
检测点的Y坐标 | |
fillRule |
用来决定点在路径内还是在路径外的算法。 | 允许的值: “nonzero”: 非零环绕规则 ,默认的规则。 “evenodd”: 奇偶环绕原则 。 |
path |
Path2D应用的路径。 |
返回值
一个Boolean值:
返回值 | 描述 |
true | 当检测点包含在当前或指定的路径内时 |
false | 没有检测到时 |
例子
ctx.rect(50, 50, 50, 50); ctx.stroke(); let _ = ctx.isPointInPath(10, 10) console.log(_); // true
控制台输出为:true
效果如下:
3.2.14 isPointInStroke
() 方法
语法
boolean ctx.isPointInStroke(x, y); boolean ctx.isPointInStroke(path, x, y);
参数 | 描述 |
x |
检测点的X坐标。 |
y |
检测点的Y坐标。 |
path |
Path2D 路径。 |
返回值
一个Boolean值:
返回值 | 描述 |
true | 当这个点在路径的描边线上时 |
false | 不在路径的描边线上时 |
例子
ctx.rect(50, 50, 50, 50); ctx.stroke(); let _ = ctx.isPointInStroke(10, 10) console.log(_); // false
控制台输出为:false
效果如下:
4. Canvas实战:多边形与组合图形的绘制
4.1 绘制五边形
ctx.beginPath(); ctx.lineWidth = 5; ctx.strokeStyle = 'blue'; ctx.moveTo(100, 0); ctx.lineTo(195, 69); ctx.lineTo(159, 181); ctx.lineTo(41, 181); ctx.lineTo(5, 69); ctx.closePath(); ctx.stroke();
效果如下:
4.2 绘制一个五角星
ctx.beginPath(); ctx.lineWidth = 5; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo(100, 0); ctx.lineTo(159, 181); ctx.lineTo(5, 69); ctx.lineTo(195, 69); ctx.lineTo(41, 181); ctx.closePath(); ctx.stroke();
效果如下:
4.3 绘制房屋
// 线条宽度 ctx.lineWidth = 5; ctx.beginPath(); // 绘制屋顶 ctx.moveTo(50, 95); ctx.lineTo(150, 20); ctx.lineTo(250, 95); ctx.closePath(); ctx.stroke(); ctx.strokeRect(100, 95, 100, 100); // 绘制墙 ctx.fillRect(130, 135, 40, 60); // 绘制门
效果如下: