二维码

图形开发学院(GraphAnywhere.com)

第五章:渲染效果

  在前几章的内容中,我们讲述了各种几何图形的绘制,并学习了使用颜色填充几何图形或者给几何图形描边,本章将会讲解在绘制几何图形的时候使用各种渲染效果。本章的内容包括:

  • 边框样式
  • 阴影效果
  • 渐变效果
  • 图案/纹理效果

1. 边框样式

在图形系统开发实战-基础篇:绘制基本图形中讲述了在绘制矩形、直线、多边形、圆等内容时我们学习到了以下几类边框样式:

  • 通过strokeStyle属性可以指定边框的颜色
  • 通过lineWidth属性可以指定描边的粗细
  • 通过setLineDash()方法可以指定虚线风格

本节我们还将学习:

  • 线条末端样式
  • 线条连接风格
  • 斜接面限制比例
  • 多边形

线条末端样式

通过画布的渲染上下文对象lineCap属性可指定线路末端样式,其API如下所示:

属性值 说明
butt 线段末端以方形结束
round 线段末端以圆形结束
square 线段末端以方形结束
1
2
3
ctx.lineCap = "butt";
ctx.lineCap = "round";
ctx.lineCap = "square";

运行效果如下图所示:

运行效果

线段的末端样式规则为:

  • round: 增加了一个半径等于线路宽度一半的半圆
  • square: 增加了一个宽度和线段宽度相同,长度是线段宽度一半的矩形区域

其完整源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html lang="cn">
<head>
<title>渲染效果(stroke)</title>
<meta charset="UTF-8">
<script src="./js/helper.js"></script>
</head>

<body style="overflow: hidden; margin:10px; background-color: white;">
<canvas id="canvas" width="750" height="300" style="border:solid 1px #CCCCCC;"></canvas>
</body>
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 指定线宽
ctx.lineWidth = 20;

// ctx.lineCap 默认值(butt);
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(450, 50);
ctx.stroke();

// ctx.lineCap = "butt";
ctx.beginPath();
ctx.moveTo(50, 110);
ctx.lineTo(450, 110);
ctx.lineCap = "butt";
ctx.stroke();

// ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(50, 170);
ctx.lineTo(450, 170);
ctx.lineCap = "round";
ctx.stroke();

// ctx.lineCap = "square";
ctx.beginPath();
ctx.moveTo(50, 230);
ctx.lineTo(450, 230);
ctx.lineCap = "square";
ctx.stroke();
</script>

</html>

线条连接风格

通过画布的渲染上下文对象lineJoin属性可指定线路末端样式,其API如下所示:

1
2
3
ctx.lineJoin = "bevel";
ctx.lineJoin = "round";
ctx.lineJoin = "miter";
属性值 说明
bevel 在相连部分的末端填充一个额外的以三角形为底的区域,每个部分都有各自独立的矩形拐角。
round 通过填充一个额外的,圆心在相连部分末端的扇形,绘制拐角的形状。圆角的半径是线段的宽度。
miter 通过延伸相连部分的外边缘,使其相交于一点,形成一个额外的菱形区域。

运行效果如下图所示:

运行效果

其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 指定线宽
ctx.lineWidth = 40;

// ctx.lineJoin = "bevel";
ctx.beginPath();
ctx.moveTo(50, 70);
ctx.lineTo(220, 120);
ctx.lineTo(50, 170);
ctx.lineJoin = "bevel";
ctx.stroke();

// ctx.lineJoin默认值(miter)
ctx.beginPath();
ctx.moveTo(320, 70);
ctx.lineTo(490, 120);
ctx.lineTo(320, 170);
ctx.lineJoin = "round";
ctx.stroke();

// ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(590, 70);
ctx.lineTo(760, 120);
ctx.lineTo(590, 170);
ctx.lineJoin = "miter";
ctx.stroke();
</script>

闭合多边形

在讲解绘制曲线和路径时我们提到过,在绘制多边形时,可通过ctx.closePath()闭合一个多边形,有时也会通过ctx.moveTo()到起始点的坐标闭合一个多边形。这两种方法均可实现闭合一个多边形,但在设置了lineJoin属性后,其运行结果会有所差异,如下图所示:

运行效果

从上图可以看出,使用ctx.closePath()闭合多边形才会更符合我们所希望的效果。

2. 阴影

阴影颜色和模糊程度

通过画布的渲染上下文对象shadowBlur属性和shadowColor可指定线路末端样式,其API如下所示:

1
2
ctx.shadowBlur = level;
ctx.shadowColor = color;
属性 说明
shadowBlur 描述模糊效果程度的,float 类型的值。注意:只有设置 shadowColor 属性值为不透明,阴影才会被绘制。
shadowColor 可以转换成 CSS <color> 值的DOMString 字符串。默认值是 fully-transparent black.。

下图是设置了该属性的运行效果:

运行效果

其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 设置阴影效果
ctx.shadowBlur = 30;
ctx.shadowColor = "red";

// 开始绘制路径
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 2 * Math.PI)
ctx.ellipse(300, 100, 75, 50, 0, 0, 2 * Math.PI);
ctx.rect(450, 50, 200, 100);
ctx.fillStyle = "green";
ctx.fill();
</script>

阴影偏移

在设置阴影效果时,除了阴影颜色和模糊程度外,还可设置阴影的偏移距离,其API如下:

1
2
ctx.shadowOffsetX = offset;
ctx.shadowOffsetY = offset;
属性 说明
shadowOffsetX 阴影水平偏移距离的 float 类型的值。默认值是 0。
shadowOffsetY 阴影垂直偏移距离的 float 类型的值。默认值是 0。

下图是设置了该属性的运行效果:

运行效果

其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 设置阴影效果
ctx.shadowBlur = 30;
ctx.shadowColor = "red";

// 开始绘制路径
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 2 * Math.PI)
ctx.ellipse(300, 100, 75, 50, 0, 0, 2 * Math.PI);
ctx.rect(450, 50, 200, 100);
ctx.fillStyle = "green";
ctx.fill();

ctx.translate(0, 160);
// 设置阴影效果
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
// 开始绘制路径
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 2 * Math.PI)
ctx.ellipse(300, 100, 75, 50, 0, 0, 2 * Math.PI);
ctx.rect(450, 50, 200, 100);
ctx.fillStyle = "green";
ctx.fill();

ctx.translate(0, 160);
// 设置阴影效果
ctx.shadowOffsetX = -10;
ctx.shadowOffsetY = 10;
// 开始绘制路径
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 2 * Math.PI)
ctx.ellipse(300, 100, 75, 50, 0, 0, 2 * Math.PI);
ctx.rect(450, 50, 200, 100);
ctx.fillStyle = "green";
ctx.fill();
</script>

3. 渐变

  在之前的内容中,我们使用过ctx.strokeStyle=colorctx.fillStyle=color设置绘制图形时的边框颜色和填充颜色,其实这两个属性除了可以指定颜色之外,还可以指定为渐变对象填充图案对象,这一节我们讲解设置渐变风格的实现过程。

  渐变风格又包括了“线性渐变”和“径向渐变”两种不同的风格。

线性渐变

  线性渐变是一种颜色过渡方式,它以一条直线(水平或垂直)为轴线,从起点到终点颜色进行顺序渐变。渲染上下文对象提供了建立线性渐变的方法,其定义如下:

1
CanvasGradient ctx.createLinearGradient(x0, y0, x1, y1);
参数 说明
x0 起点的 x 轴坐标
y0 起点的 y 轴坐标
x1 终点的 x 轴坐标
y1 终点的 y 轴坐标

  该方法的返回值是一个CanvasGradient对象,该对象包含了一个方法:

1
void gradient.addColorStop(offset, color);
参数 说明
offset 偏移位置,0到1之间的值
color 颜色值

  我们通过一个例子熟悉一下线性渐变的用法,运行效果如下:

运行效果

  这个例子使用了线性渐变填充在圆形、矩形和文本。起始颜色是金色(gold),终止颜色是红色(red),其源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 建立线性渐变,其坐标相对于画布的坐标
const gradient = ctx.createLinearGradient(0, 50, 0, 200);
gradient.addColorStop(0.05, "gold");
gradient.addColorStop(0.95, "red");
ctx.fillStyle = gradient;

// 绘制圆
ctx.beginPath();
ctx.arc(125, 125, 75, 0, 2 * Math.PI);
ctx.fill();

// 绘制矩形
ctx.translate(200, 0);
ctx.beginPath();
ctx.rect(50, 50, 150, 150);
ctx.fill();

// 绘制文字
ctx.translate(250, 0);
ctx.font = "150px 黑体"
ctx.textBaseline = "top";
ctx.fillText("图形", 0, 50);
</script>

  在这个例子中建立的线性渐变对象,其坐标为(0,50)至(0,200),表示其渐变颜色的方向为从上往下垂直渐变。改变坐标就能改变颜色逐渐变换的方向,例如可以改为水平方向,也可以改为沿着斜线的方向。我们看看下面这个示例:

运行效果

  渐变对象的addColorStop()方法用于指定渐变颜色,该方法可以调用n次,第一次表示渐变开始的颜色,第二次表示渐变结束的颜色,第三次会以第二次为开始颜色渐变,以此类推。我们看看下面这个示例:

运行效果

其源代码如下:

1
2
3
4
5
6
// 建立线性渐变,其坐标相对于画布的坐标
const gradient = ctx.createLinearGradient(0, 50, 0, 200);
gradient.addColorStop(0.05, "red");
gradient.addColorStop(0.5, "gold");
gradient.addColorStop(0.95, "green");
ctx.fillStyle = gradient;

径向渐变

径向渐变是指从起点到终点颜色从内到外进行圆形渐变(从中间向外拉)。渲染上下文对象提供了建立径向渐变的方法,其定义如下:

1
CanvasGradient ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);
参数 说明
x0 开始圆形的 x 轴坐标
y0 开始圆形的 y 轴坐标
r0 开始圆形的半径
x1 结束圆形的 x 轴坐标
y1 结束圆形的 y 轴坐标
r1 结束圆形的半径

  该方法的返回值是一个CanvasGradient对象,该对象包含了方法addColorStop()与线性渐变的该方法完全一样。接下来看一个径向渐变的例子,运行效果如下:

运行效果

  径向渐变同样可填充在圆形、矩形和文本等图形中。从上图可知径向渐变是一种颜色从内到外的渐变,从一个起点向所有方向进行的渐变。完整源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 建立径向渐变,其坐标相对于画布的坐标
const gradient = ctx.createRadialGradient(125, 125, 0, 125, 125, 160);
gradient.addColorStop(0.05, "gold");
gradient.addColorStop(0.95, "red");
ctx.fillStyle = gradient;

// 绘制圆
ctx.beginPath();
ctx.arc(125, 125, 75, 0, 2 * Math.PI);
ctx.fill();

// 绘制矩形
ctx.translate(200, 0);
ctx.beginPath();
ctx.rect(50, 50, 150, 150);
ctx.fill();

// 绘制文字
ctx.translate(250, 0);
ctx.font = "150px 黑体"
ctx.textBaseline = "top";
ctx.fillText("图形", 0, 50);
</script>

重要提示:渐变对象可同时应用于多个形状或文字,该示例中的圆形、矩形和文字均使用了该渐变对象。在绘制了圆形之后,由于使用了translate()将画布坐标系进行了偏移,在绘制矩形和文字时的坐标仍与绘制圆的坐标位置基本一致,因此该渐变对象的坐标可同时适用于此三个形状/文字。关于translate()的用法我们将在后续的章节中进行介绍。

  使用createRadialGradient()创建径向渐变对象时可指定开始圆的位置和半径,也可指定结束圆的位置和半径,下面的效果图片就是改变了这几个参数后的运行效果,如下图:

运行效果

4. 图案/纹理

  除了渐变对象外,fillStyle还可以使用图案/纹理方式填充各类图形,使得画布可以很方面的实现类似于CAD等绘图软件提供的图案填充功能。渲染上下文对象提供了创建图案的方法createPattern(),其定义如下:

1
CanvasPattern ctx.createPattern(image, repetition);
参数 说明
image 填充图案源
repetition 重复方式
  • 填充图案源可以是位图、也可以是的画布对象
  • 重复方式包括:
    • “repeat” :水平和垂直方向均重复
    • “repeat-x” :仅水平方向重复
    • “repeat-y” :仅垂直方向重复
    • “no-repeat” :不重复
      默认值是:repeat

使用位图作为填充图案

  填充图案亦可应用于路径、矩形和文字中,下面这个示例将在圆形、矩形和文本绘制中使用图案填充,其效果如下所示:

运行效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 装载图案后,渲染图形
loadImage("./images/15.jpg", function (image) {
let pattern = ctx.createPattern(image, "repeat");
ctx.save();

// 设置图案填充样式
ctx.fillStyle = pattern;
// 设置描边时的线宽
ctx.lineWidth = 2;

// 绘制带图案填充的圆
ctx.beginPath();
ctx.arc(125, 125, 75, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();

// 绘制带图案填充的矩形
ctx.translate(200, 0);
ctx.beginPath();
ctx.rect(50, 50, 150, 150);
ctx.fill();
ctx.stroke();

// 绘制带图案填充的文字
ctx.translate(250, 0);
ctx.font = "150px 黑体"
ctx.textBaseline = "top";
ctx.fillText("图形", 0, 50);
ctx.strokeText("图形", 0, 50);
ctx.restore();
})

function loadImage(src, callback) {
// 加载并绘制图片
let image = new Image();
image.onload = function () {
callback(image);
}
image.src = src
}
</script>

  在图形系统中,经常应用“填充图案”描述图形中一些特殊的区域或者材质,如下图所示:

运行效果

使用画布作为填充图案

  除了直接使用位图作为填充图案之外,还可以使用画布的内容作为填充图案,比较适合一些使用简易图案作为填充图案的场景,下面这个示例展示了使用画板作为填充图案,运行效果如下图所示:

运行效果

  在这个示例中,使用document.createElement("canvas")创建了一个临时画布,在这个画布中绘制了两个线条,并使其重复填充至主画布的圆形和矩形中,就实现了上面这个效果,其完整源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);
// 设置样式
ctx.fillStyle = getParrent(25, 25);
ctx.lineWidth = 2;

// 绘制带图案填充的圆
ctx.beginPath();
ctx.arc(130, 125, 100, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();

// 绘制带图案填充的矩形
ctx.translate(220, 0);
ctx.beginPath();
ctx.rect(50, 50, 500, 180);
ctx.fill();
ctx.stroke();

/**
* 建立画布填充图案对象
*/
function getParrent(width, height) {
// 创建临时画布
let pcanvas = document.createElement("canvas");
pcanvas.width = width;
pcanvas.height = height;
pctx = pcanvas.getContext('2d');
// 绘制线条
pctx.beginPath();
pctx.moveTo(0, 0);
pctx.lineTo(width, height);
pctx.moveTo(width, 0);
pctx.lineTo(0, height);
pctx.strokeStyle = "blue";
pctx.lineWidth = 0.5;
pctx.stroke();

return ctx.createPattern(pcanvas, "repeat");
}
</script>

掌握了使用渐变和图案作为填充效果之后,接下来我们看一个“围棋”实际案例,该围棋棋盘和棋子的绘制效果非常逼真,如下图所示:

运行效果

在该示例中,使用“图案填充”技术填充了围棋棋盘,使用“径向渐变”实现了棋子的光影效果,这两种技术都是我们刚刚讲述过的,因此其实现的代码也很容易理解,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 棋盘格子大小
let size = 100;

// 使用木质纹理图案作为围棋棋盘
let image = new Image();
image.onload = function () {
draw();
}
image.src = "./images/wood2.png";

/**
* 绘制棋盘和棋子
*/
function draw() {
let pattern = ctx.createPattern(image, "repeat");
ctx.save();
// 绘制带图案填充的圆和矩形
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();

// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 绘制棋盘
drawBoards();

// 绘制棋子(第3个参数为棋子类型: 1是为黑子,0为白子)
drawPiece(2, 1, 1);
drawPiece(3, 0, 1);
drawPiece(3, 1, 1);
drawPiece(2, 1, 1);
drawPiece(0, 2, 1);
drawPiece(1, 2, 1);
drawPiece(2, 2, 1);
drawPiece(3, 2, 0);
drawPiece(4, 0, 0);
drawPiece(4, 1, 0);
drawPiece(0, 3, 0);
drawPiece(1, 3, 0);
drawPiece(2, 3, 0);
drawPiece(3, 3, 0);
}

/**
* 绘制棋盘函数
*/
function drawBoards() {
ctx.save();
ctx.beginPath();
ctx.lineCap = "square";
for (let i = size; i < canvas.width; i += size) {
ctx.moveTo(i, size);
ctx.lineTo(i, canvas.height)
}
for (let i = size; i < canvas.width; i += size) {
ctx.moveTo(size, i);
ctx.lineTo(canvas.width, i)
}
ctx.lineWidth = 6;
ctx.stroke();
ctx.restore();
}

/**
* 绘制棋子函数
*/
function drawPiece(x, y, type) {
// 计算棋子的坐标
x = (x + 1) * 100;
y = (y + 1) * 100;
let radius = size/2 - 2;

// 建立径向渐变,其坐标相对于画布的坐标
const gradient = ctx.createRadialGradient(x + radius / 2, y - radius / 2, 0, x, y, radius);
// type == 1是为黑子,否则为白子
gradient.addColorStop(0, type == 1 ? "#B9B9B9" : "#FFFFFF");
gradient.addColorStop(0.95, type == 1 ? "#000000" : "#DCDCDC");
gradient.addColorStop(0.97, type == 1 ? "#000000" : "#DCDCDC00");
gradient.addColorStop(1, type == 1 ? "#00000000" : "#DCDCDC00");

// 绘制圆
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
}
</script>

5. 本章小结

  本章讲解了Canvas所提供的的一些渲染效果的用法,这些效果不仅仅是增强了图形的立体感和视觉效果,更重要的是让图形更具有真实感,使图形看起来更接近现实世界,因此这些这些效果在图形系统中得到了广泛应用。

本章内容使用了Canvas 2D API以下属性和方法:

属性

属性值 说明
ctx.lineCap 线条末端样式
ctx.lineJoin 线条连接风格
ctx.shadowBlur 模糊程度
ctx.shadowColor 阴影颜色
ctx.shadowOffsetX 阴影水平偏移距离
ctx.shadowOffsetY 阴影垂直偏移距离

方法

方法名 说明
ctx.createLinearGradient(x0, y0, x1, y1) 创建线性渐变样式
ctx.createRadialGradient(x0, y0, r0, x1, y1, r1) 创建径向渐变样式
gradient.addColorStop(offset, color) 添加一个由偏移值和颜色值指定的断点到渐变
ctx.createPattern(image, repetition) 创建填充图像样式

练习一下

阴影

按照以下格式要求在Canvas中绘制你喜欢的一本书的名字:

  • 垂直居中
  • 水平居中
  • 字体大小:50px
  • 字型:黑体
  • 颜色:blue
  • 添加阴影效果

渐变和图案

  在本章渐变与图案的示例中,均是将这两种风格应用在了填充样式ctx.fillStyle中,而且示例中的几何对象也是几个简单的几何对象。其实渐变与图案是可以应用于更复杂的路径,同时还可以应用于描边样式ctx.strokeStyle的,下面这张图片就是将渐变和图案应用到了贝塞尔曲线中了。

运行效果

你也动手试一试吧。

本文为“图形开发学院”(graphanywhere.com)网站原创文章,遵循CC BY-NC-ND 4.0版权协议,商业转载请联系作者获得授权,非商业转载请附上原文出处链接及本声明。