二维码

第六章 画布操作

本章摘要

在图形开发中,Canvas不仅是创作的画布,更是实现创意的强大工具。本章深入探讨了Canvas画布操作的核心技术,包括状态管理、裁切、像素处理和图像转换。通过save()和restore()方法,我们能精确控制绘图状态,确保作品连贯性和准确性。clip()方法则允许我们定义精细的裁切区域,让渲染更可控。ImageData对象则让我们能进行像素级别的图像处理,打造独特视觉效果。这些技术不仅提升了作品质量,还激发了创作灵感。无论是实现探照灯、橡皮擦功能,还是创建马赛克和刮刮乐效果,Canvas画布操作都展现了其强大的灵活性和实用性。

图形系统开发实战课程 - 基础篇 渲染效果

第六章 画布操作

本章的内容包括:

  • 画布状态的保存与恢复
  • 画布的裁切
  • 对画布中的内容进行像素操作
  • 将绘制到画布的内容转换为图片

1. 状态保存与恢复

  回顾一下前面章节中学习的基本几何形状的绘制过程,基本上都包括“设置渲染样式、绘制路径、填充或描边”几个过程,这其中设置渲染样式指的是改变画布之后绘制图形所使用的样式,如果没有指定新的样式,之后绘制的内容一直会使用这个新的样式。如下代码所示:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 第一次设置样式
ctx.strokeStyle = "blue";
ctx.lineWidth = 4;
ctx.lineCap = "round";

// 绘制线条
ctx.beginPath();
ctx.moveTo(50, 70);
ctx.lineTo(500, 70);
ctx.stroke();

// 绘制矩形1
ctx.strokeRect(50, 120, 200, 120);

// 第二次设置样式
ctx.strokeStyle = "red";

// 绘制矩形2
ctx.strokeRect(300, 120, 200, 120);
</script>

  上述代码执行后,线条和第一次绘制的矩形均会是蓝颜色,而第二次绘制的矩形将会是红颜色,且三个图形的边框宽度均是4px,如下图所示:

运行效果

  而经常我们只想临时性改变一下样式,绘制某个形状之后,又想将画布恢复之前的状态,这就需要在设置样式之前,将当前渲染上下文的状态保存起来,在绘制图形之后,将当前渲染上下文的状态恢复至保存前的状态。Canvas提供了以下两个api接口,用于保存和恢复渲染上下文状态。

1
2
ctx.save();    // 保存当前的绘图状态。
ctx.restore(); // 恢复之前保存的绘图状态

save()restore()常被误认为是用来保存所绘制的图形和还原之前保存的图形,其实不然。save()和restore()保存和恢复的是绘图状态,而不是图形本身。

  save()restore()方法是用来保存和恢复 Canvas 状态的,都没有参数。Canvas 的状态指的是Canvas渲染上下文对象设置的各种样式属性,包括:

参数名 说明
strokeStyle 描述画笔(绘制图形)颜色或者样式的属性
fillStyle 描述填充时颜色和样式的属性
lineWidth 线段宽度的属性
lineCap 线条末端样式
lineJoin 线条连接风格
miterLimit 斜接面限制比例
lineDashOffset 虚线偏移量
shadowOffsetX 阴影水平偏移距离
shadowOffsetY 阴影垂直偏移距离
shadowBlur 模糊程度
shadowColor 阴影颜色
globalAlpha 透明度
font 字体样式的属性
textAlign 文本的水平对齐方式
textBaseline 文本的垂直对齐方式
wordSpacing 单词之间的间距
letterSpacing 中文的字与字之间,英文单词的字母与字母之间的距离
direction 文本方向(从左向右,从右向左)
imageSmoothingEnabled 图片是否平滑(消除锯齿)
globalCompositeOperation 绘制新形状时应用的合成操作的类型

此外画布状态还包括:

  • 当前的变形操作,即平移ctx.translate(),旋转ctx.rotate()和缩放ctx.scale()
  • 当前的裁切路径,即ctx.clip()

接下来我们通过一个实例熟悉这个方法的使用,先看看需要实现的效果图:

运行效果

  在这个示例中需要多次更改字体颜色,其中正文的文本是黑色,关键字的字体颜色是红色或蓝色。在绘制这些文字时可使用save()保存渲染上下文的样式,在绘制完成后使用restore()恢复之前样式。其源代码如下:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 渲染文字定义
let textArray = [
{"text":"通过"},
{"text":"技术创新", "color":"red"},
{"text":"促动"},
{"text":"管理变革", "color":"red"},
{"text":",建成"},
{"text":"“规建运、营配调”一体化管控", "color":"blue"},
{"text":"系统。"}
];

// 根据定义,逐个渲染文字
let x = 40, y=50;
ctx.save();
ctx.font = "24px 黑体";
ctx.textBaseline = "top";
textArray.forEach(textObj => {
x+= drawText(textObj, x, y);
})
ctx.restore();

/**
* 绘制文字函数
*/
function drawText(textObj, x, y) {
ctx.save();
if(textObj.color != null) {
ctx.fillStyle = textObj.color;
}
ctx.fillText(textObj.text, x, y);
ctx.restore();
return ctx.measureText(textObj.text).width;
}
</script>

状态栈

  Canvas 状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。你可以调用任意多次 save方法。每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
上面这个示例中其实已经使用了状态栈。

  • 第一次save()状态是是在开始渲染文本的时候,在保存了渲染上下文状态之后,设置了字体和对齐方式,系统默认的颜色是黑色,如果需改变默认的字体颜色,可在此时进行设置。
  • 第二次save()状态是在绘制文字函数中,此时根据文字定义改变了绘制字体的颜色,在字体绘制之后立即使用restore()恢复了状态。
  • 在所有文字绘制完成之后,再次使用restore()恢复到了绘制文字之前的状态。

2. 裁切

  裁切指的是在Canvas之中由路径所定义的一块区域,浏览器会将所有的绘图操作都限制在本区域内执行。在默认情况下,剪辑区域的大小与Canvas画布大小一致。当你通过创建路径并调用clip()方法来设定剪辑区域后,你在Canvas之中绘制的所有内容都将局限在该区域内。这也意味着在剪辑区域以外进行绘制是没有任何效果的。先看一张效果图片,如下所示:

运行效果

  在该示例中建立一个裁切区域,因此画布仅仅渲染了该裁切区域内的内容。渲染上下文提供了clip()方法实现该功能。

1
ctx.clip();

  该方法和stroke()fill()用法相似,均需先定义一个路径,stroke()fill()是对路径进行描边或填充,而fill()是裁切该路径,在之后绘制的图形,只有在裁切范围内的内容才会被画布渲染出来。上述效果的源代码如下:

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

// 建立裁切区域
ctx.beginPath();
ctx.arc(200, 220, 150, 0, 2 * Math.PI);
ctx.moveTo(450, 220);
ctx.arc(450, 220, 150, 0, 2 * Math.PI);
ctx.clip();

// 加载并绘制图片
let image = new Image();
image.onload = function () {
// 在画布中绘制图像,由于在绘制图像之前定义了裁切路径,因此该图片只有部分内容会被渲染出来。
ctx.drawImage(image, 10, 10, 780, 430);
}
image.src = "./images/j20.jpg";
</script>

探照灯

  在状态保存与恢复中我们讲过,裁切区域是可以保存在状态中的,这也意味着我们可以在save()渲染上下文状态之后使用clip()建立裁切区域,此后绘制图像将会受裁切区域的影响,在restore()恢复渲染上下文之后,之后在绘制的图形就不会收到裁切区域的影响了。

接下我们通过一个示例来理解这个特性,这是一个‘探照灯’的示例,其效果如下:

运行效果

  在这个示例中将会根据鼠标的位置建立裁切区域,此时只能看到该裁切区域内的图像,当鼠标移动时,清除现有内容,重新建立裁切区域并重新绘制图像,因此渲染出了新的图像,其运行结果就好比是拿着一个探照灯,照到哪里就能显示哪里的图像。其源代码如下:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
let ready = false;

// 加载并绘制图片
let image = new Image();
image.onload = function () {
ready = true;
reDraw(150, 150);
}
image.src = "./images/j20.jpg";

// 增加鼠标移动事件
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
reDraw(e.offsetX, e.offsetY);
}, false);

// 重绘图像
function reDraw(x, y) {
// 清除背景
ctx.fillRect(0, 0, canvas.width, canvas.height);

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

ctx.save();

// 建立裁切区域
ctx.beginPath();
ctx.arc(x, y, 100, 0, 2 * Math.PI);
ctx.clip();

// 在画布中绘制图像
if (ready == true) {
ctx.drawImage(image, 10, 10, 780, 430);
}

// 十字光标
ctx.beginPath();
ctx.moveTo(x-20, y);
ctx.lineTo(x+20, y);
ctx.moveTo(x, y-20);
ctx.lineTo(x, y+20);
ctx.strokeStyle="white";
ctx.stroke();
ctx.restore();
}
</script>

橡皮檫

  在图形系统中,橡皮擦是一个很常用的功能,其使用方式很简单,在Canvas中按下并拖动鼠标左键就会擦除鼠标所在位置周围圆形区域内的内容。

  前文已经讲述过Canvas渲染上下文对象提供了ctx.clearRect(x, y, width, height)方法,可以擦除Canvas中指定矩形范围内的内容,然而我们在使用橡皮檫时擦除的范围往往不是矩形,这时候就可以发挥裁剪功能的效果了。先看看下图的运行效果:

运行效果

  鼠标移动时,根据鼠标位置建立一个裁剪区域(在本例中是建立一个半径大小为40的圆形),然后调用ctx.clearRect(0,0,canvas.width, canvas.height)擦除画布的内容,由于建立了裁剪区域,因此只会擦除裁剪区域内的内容。刚刚我们学习过了画布状态的保存和恢复的用法,在建立裁切区域之前我们保存现有状态,在执行擦除画布内容后在恢复画布状态,每移动一点我们就重复一次这个动作,从而实现鼠标移到哪就清除那个范围的内容。其源代码如下:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
let beginDrag = false;
let ready = false;

// 绘制图像
let image = new Image();
image.onload = function () {
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
drawGrid('lightgray', 0, 0, ctx);
}
image.src = "./images/j20.jpg";

// 橡皮檫功能实现
function erase(x, y) {
// 当鼠标键按下时,移动鼠标箭头,即可擦除覆盖物
if (beginDrag === true) {
ctx.save();
// 建立裁切区域
ctx.beginPath();
ctx.arc(x, y, 40, 0, 2 * Math.PI);
ctx.clip();

// 清除画面
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 补齐背景网格
drawGrid('lightgray', 10, 10);
ctx.restore();
}
}


// 鼠标键按下事件
canvas.addEventListener('mousedown', function (e) {
beginDrag = true;
}, false);

// 鼠标键抬起事件
canvas.addEventListener('mouseup', function (e) {
beginDrag = false;
}, false);

// 增加鼠标移动事件
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
erase(e.offsetX, e.offsetY, false);
}, false);
</script>

3. 像素操作

  Canvas除了提供基本绘图功能,还提供了对图片数据的处理功能,能够从像素级别操作位图。画布上下文渲染对象提供以下三个像素操作的api:

  • ImageData对象,ImageData 是图片的数据化,它具备以下属性
属性 说明
data 图像数据
width 图像宽度
height 图像宽度

  其中data属于Uint8ClampedArray类型,存储了像素数据,每个像素包含了4个byte的值,分别是该像素对应的红,绿,蓝和透明值(r,g,b,a)。各颜色值的取值范围为:0~255,data总共包含了height × width × 4 字节数据,索引值从 0 到 (height × width × 4) - 1。左上角像素在数组的索引 0 位置。像素从左到右被处理,然后往下,遍历整个数组。

  下图显示了imageData中data的数组结构,每个像素均占了data数组中的4个元素,第一个像素存储在data数组的0至3个元素,第二个像素存储在data数组的4至7个元素。

效果

  • 获取画布中的像素数据
1
let myImageData = ctx.getImageData(left, top, width, height);
参数名 说明
left 起始横坐标位置
top 起始纵坐标位置
width 图像宽度
height 图像宽度

  这个方法会返回一个ImageData对象,它代表了画布区域的对象数据,此画布的四个角落分别表示为 (left, top), (left + width, top), (left, top + height), 以及 (left + width, top + height) 四个点。

  • 在画布中写入像素数据
1
ctx.putImageData(myImageData, dx, dy);
参数名 说明
dx 起始横坐标位置
dy 起始纵坐标位置

  这个方法会在画布中写入一个ImageData对象,(dx、dy)为写入的横坐标和纵坐标位置,宽和高由ImageData数据中的宽和高来确定。

  • 创建一个 ImageData 对象
1
2
let myImageData = ctx.createImageData(width, height);
let myImageData = ctx.createImageData(ImageData);

  这个方法可以直接建立一个空的ImageData 对象。

参数名 说明
width 图像宽度
height 图像宽度

或者

参数名 说明
ImageData 已有ImageData数据

  像素操作可以实现很灵活很复杂的图像操作功能,例如图像的裁切、图像的美化、图像的保存等等,下面通过两方面示例介绍一些它的用法。

访问像素值

  在讲述ImageData数据结构时,我们谈到ImageData包含了一个data属性,该属性包含了各个像素的颜色信息,那我们是否可以使用该功能获取到canvas中任何位置的颜色呢?

那我们就来实现这个功能吧,其实现思路包含以下几点:

  • (1) 绘制图像
  • (2) 给canvas增加了鼠标click事件
  • (3) 使用 ctx.getImageData()获取鼠标click位置imageData中的颜色
  • (4) 使用该颜色绘制一个矩形

实现的效果如下:

运行效果

其源代码如下:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d', { willReadFrequently: true });

// (1)加载图像
let image = new Image();
image.onload = function () {
draw();
}
image.src = "./images/square_150.png"

// (2)为画布增加click事件
canvas.addEventListener("click", function (e) {
let imageData = ctx.getImageData(e.offsetX, e.offsetY, 1, 1);
// (3)获取鼠标click位置imageData中的颜色
let color = "rgba(" + imageData.data[0] + "," + imageData.data[1] + "," + imageData.data[2] + "," + imageData.data[3] + ")";
draw(color);
})

// 绘制图像
function draw(color) {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);

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

// 在画布中绘制图像
ctx.drawImage(image, 50, 50);

// (4) 使用该颜色绘制一个矩形
if (color) {
ctx.save();
ctx.fillStyle = color;
ctx.fillRect(300, 50, 350, 150);
ctx.restore();
}
ctx.strokeRect(300, 50, 350, 150);
}
</script>

像素遍历

  • 逐像素遍历

  ImageData中的data数组中每个像素占了数组的4个位置,每隔4个数据遍历一次,即可实现对像素的遍历。其代码如下:

1
2
3
4
5
6
7
8
let data = imageData.data;
for (let i = 0, len = data.length; i < len; i += 4) {
let r = data[i + 0];
let g = data[i + 1];
let b = data[i + 2];
let a = data[i + 3];
console.log(r, g, b, a)
}
  • 逐行遍历像素

  ImageData中的data数组中的像素按照从左右向右,从上至下的顺序排列,结合ImageData的width和height属性即可实现逐行逐点遍历所有像素。其代码如下:

1
2
3
4
5
6
7
8
9
10
11
let data = imageData.data;
for (let y = 0, height = imageData.height; y < height; y++) {
for (let x = 0, width = imageData.width; x < width; x++) {
let idx = (y * w + x) * 4;
let r = data[idx];
let g = data[idx + 1];
let b = data[idx + 2];
let a = data[idx + 3];
console.log(r, g, b, a)
}
}

  通过像素遍历可以实现像素级的图像处理功能,例如给图像增加滤镜处理功能。下面通过一个示例实现对图像的灰度滤镜处理。

  灰度滤镜的实现思路是:使用getImageData()获取画布中某个区域的的ImageData数据,逐个像素进行灰度变换操作,然后使用putImageData()至画布中就可实现灰度滤镜的效果。灰度变换操作也比较简单,使用加权平均计算各个像素的r、g、b值,替换原r、g、b值即可。加权平均公式为:x=0.34r + 0.5g + 0.16*b。运行效果如下图:

运行效果

源代码如下:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
let imageSize = [360, 340];
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 加载并绘制图片
let image = new Image();
image.onload = function () {
// 在画布中绘制图像
ctx.drawImage(image, 20, 40, imageSize[0], imageSize[1]);
// 复制图像
copyImage();

ctx.font = "16px 黑体";
ctx.textAlign = "center";
ctx.fillText("(原图)", 20 + imageSize[0]/2, 400);
ctx.fillText("(使用灰度滤镜后产生的图像)", 400 + imageSize[0]/2, 400)
}
image.src = "./images/ma.png";

/**
* 复制图像函数
*/
function copyImage() {
// 获取imageData
let imageData = ctx.getImageData(20, 40, imageSize[0], imageSize[1]);
// 像素处理(滤镜)
grayscaleFilter(imageData);
// 显示在某位置
ctx.putImageData(imageData, 400, 40);
}

/**
* 灰度滤镜
*/
function grayscaleFilter(imageData) {
let data = imageData.data,
len = data.length,
brightness;
// 对逐个像素值进行处理
for (let i = 0; i < len; i += 4) {
brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2];
data[i] = brightness;
data[i + 1] = brightness;
data[i + 2] = brightness;
}
}
</script>

绘制橡皮线

  许多图形系统中提供了图形交互操作功能,橡皮线就是一种常见的操作,例如要实现图像的裁切功能,则需要通过鼠标在图形中绘制矩形框,随着鼠标的移动,这个矩形框不断变大或变小,好像橡皮一样有弹性,因此简称橡皮线或橡皮框。

  由于当我们在图形中绘图了任何几何形状后,这些形状都将变成图形的一部分不可分割,因此要实现这个功能,我们得先将现有画布中的图形保存起来,在绘制矩形的时候先清除原有内容,然后绘制保存的图形,最后绘制矩形框,每移动一点鼠标就重复这个过程,由于计算机处理速度极快,我们的眼睛看到的仅仅是矩形大小在发生变化。

  像素操作可以实现图形的保存和重绘。使用getImageData()将当前图形内容存储至变量中,通过putImageData()重绘至画布中实现图像的重绘。看看下面示例的运行效果:

运行效果

  在这个示例中,可以使用鼠标对图形进行拉框操作,图中的矩形不断变换,每次变化其实是重绘了整个图形。原理正如上讲述的那样,在背景绘制完成之后使用getImageData()将图像保存到了变量backgroundImageData中,鼠标移动过程中清除原图形,使用putImageData()backgroundImageData重新绘制到画布中,并绘制拉框操作产生的矩形,鼠标移动后再次重复这个过程。其源代码如下:

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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格
drawGrid('lightgray', 10, 10);

// 背景图像数据,重绘时将使用该数据重绘背景
let backgroundImageData = drawRandomRound();
let beginDraw = false; // 是否开始绘制矩形
let startX, startY; // 鼠标按下去时的坐标

/**
* 随机生成背景,并返回该背景imageData
*/
function drawRandomRound() {
// 定义圆的半径和颜色
let colors = ["153,0,0,", "204,0,0,", "255,0,0,", "255,64,64,", "255,128,128,"];
let radius = [15, 30, 45, 60, 75];

// 随机绘制100个圆
for (let i = 0; i < 100; i++) {
let x = getRandomNum(0, canvas.width);
let y = getRandomNum(0, canvas.height);
ctx.beginPath();
ctx.arc(x, y, radius[i % 5], 0, 2 * Math.PI);
ctx.fillStyle = "rgba(" + colors[i % 5] + getRandomNum(1, 5) / 10 + ")";
ctx.fill();
}

// 获取imageData
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}

// 增加鼠标移动事件
canvas.addEventListener('mouseup', function (e) {
e.preventDefault();
e.stopPropagation();
beginDraw = false;
}, false);

// 增加鼠标移动事件
canvas.addEventListener('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
beginDraw = true;
startX = e.offsetX;
startY = e.offsetY;
}, false);

// 增加鼠标移动事件
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
if (beginDraw == true) {
doDrawRect(e.offsetX, e.offsetY);
}
}, false);

/**
* 重新绘制背景+矩形
*/
function doDrawRect(x, y) {
ctx.putImageData(backgroundImageData, 0, 0);
ctx.strokeRect(startX, startY, x - startX, y - startY);
}
</script>

4. 转换为图片

  画布提供了toDataURL()方法,可将当前画布的内容转换为图片。其定义如下:

1
canvas.toDataURL(type, encoderOptions)
参数名 说明
type 图片格式,默认为 image/png
encoderOptions 图片质量
  • 图片格式:MIME类型,例如 image/png, image/jpg, image/webp, image/gif 等等,如果参数为空,则为默认值image/png。
  • 图片质量:仅当指定type为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。

  toDataURL()返回一个包含图片展示的 data URI(一种经过base64编码格式的数据URI),在<html>中可将该值赋值给<img>对象的src属性,网页即可直接渲染出图像结果。

说明:
1、toDataURL()方法是画布提供的方法,而非画布上下文渲染对象的方法。
2、如果画布的高度或宽度是0,那么会返回字符串“data:,
3、Chrome支持“image/webp”类型

下面通过一个示例来熟悉其用法:效果图如下:

运行效果

  在这个示例中,依旧是使用了getImageData()从画布中获取图像数据,在应用滤镜对ImageData进行处理后,使用putImageData()将其写入一个新的Canvas中,并在网页中创建一个img对象显示转换后的图像。其完整源代码如下:

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<!DOCTYPE html>
<html>

<head>
<title>画布操作(toDataUrl)</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="图形系统开发实战:基础篇 示例">
<meta name="author" content="hjq">
<meta name="keywords" content="canvas,ladder,javascript">
<style>
img { margin-left: 10px }
</style>
</head>

<body style="overflow: hidden;">
<div style="margin:0 auto; width:900px;">
<div style="margin:0 auto; width:600px;">
<canvas id="canvas" width="800" height="600" style="border:solid 10px goldenrod;"></canvas>
</div>
<div id="toolbar" style="text-align:center;"></div>
</div>
</body>
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');

// 加载并绘制图片
let image = new Image();
image.onload = function () {
canvas.width = image.width;
canvas.height = image.height;
canvas.parentElement.style.width = (canvas.width + 20)+ "px";
ctx.drawImage(image, 0, 0);
filterImage(sepia, "怀旧");
filterImage(grayscaleFilter, "灰度");
filterImage(brighten, "亮度调节");
filterImage(noise, "噪声调节");
filterImage(invert, "反色");
// 显示文字
drawText("原 图", ctx);
}
image.src = "./images/girl.png";

/**
* 使用滤镜处理图像,并显示在html页面中
*/
function filterImage(filter, text) {
// 使用滤镜处理原canvas中的图像
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
filter(imageData)

// 创建临时画布,用于渲染经过滤镜处理后的图像
let tempCanvas = document.createElement("canvas");
tempCanvas.width = imageData.width;
tempCanvas.height = imageData.height;
let context = tempCanvas.getContext("2d");
context.putImageData(imageData, 0, 0);

// 显示文字
drawText(text, context);

// 将裁切的图像转换为图片
let base64url = tempCanvas.toDataURL();

// 在html中建立img对象,显示转换后的图片
let img = document.createElement("img");
img.width = 160;
img.src = base64url;
document.getElementById("toolbar").appendChild(img);

// 清除临时画布
tempCanvas.width = 0;
tempCanvas.height = 0;
tempCanvas = null;
}

/**
* 绘制文字
*/
function drawText(text, context) {
context.save();
context.font = "24px 黑体"
context.textAlign = "center";
context.strokeStyle="white";
context.lineWidth = 6;
context.strokeText(text, context.canvas.width/2, 30);
context.fillText(text, context.canvas.width/2, 30);
context.restore();
}

/**
* 灰度滤镜
*/
function grayscaleFilter(imageData) {
let data = imageData.data, brightness;
for (let i = 0; i < data.length; i += 4) {
brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2];
data[i] = brightness;
data[i + 1] = brightness;
data[i + 2] = brightness;
}
}

/**
* 亮度调节滤镜
*/
function brighten(imageData, options = {}) {
let data = imageData.data, brightness = (options.brightness || 0.2) * 255;
for (let i = 0; i < data.length; i += 4) {
data[i] += brightness;
data[i + 1] += brightness;
data[i + 2] += brightness;
}
}

/**
* 反色滤镜
*/
function invert(imageData) {
let data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
}

/**
* 怀旧滤镜
*/
function sepia(imageData) {
let data = imageData.data, i, r, g, b;
for (i = 0; i < data.length; i += 4) {
r = data[i + 0];
g = data[i + 1];
b = data[i + 2];
data[i + 0] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
}
}

/**
* 噪声调节滤镜
*/
function noise(imageData, options = {}) {
let noise = options.noise || 0.4;
let amount = noise * 255, data = imageData.data, half = amount / 2;
for (let i = 0; i < data.length; i += 4) {
data[i + 0] += half - 2 * half * Math.random();
data[i + 1] += half - 2 * half * Math.random();
data[i + 2] += half - 2 * half * Math.random();
}
}
</script>

</html>

5. 本章小结

  本节讲解了如何在画布中进行状态保存和恢复的操作,讲述了通过建立裁切区域进行图形渲染,还讲述了针对画布的像素操作和将画布图像转换为图片的功能及应用。本节内容使用了Canvas 2D API以下属性和方法:

方法

方法名 说明
ctx.save() 保存当前的绘图状态
ctx.restore() 恢复之前保存的绘图状态
ctx.clip() 建立一个裁切区域
ctx.getImageData(left, top, width, height) 获取画布中的像素数据
ctx.putImageData(myImageData, dx, dy) 在画布中写入像素数据
ctx.createImageData(width, height) 创建ImageData对象
canvas.toDataURL(type, encoderOptions) 将当前画布的内容转换为图片

练习一下

画布状态

  刚刚我们已经学习了save()restore()这两个方法用于保存和恢复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
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');

ctx.fillStyle = "red";
ctx.save();
// 第一个矩形
ctx.fillRect(50, 50, 100, 100);

ctx.fillStyle = "blue";
ctx.save();
// 第二个矩形
ctx.fillRect(200, 50, 100, 100);
ctx.restore();

// 第三个矩形
ctx.fillRect(50, 200, 100, 100);

ctx.restore();
// 第四个矩形
ctx.fillRect(200, 200, 100, 100);
</script>

像素操作

  马赛克是一种图像(视频)处理手段,它通过对图像进行像素化处理来降低图像的细节清晰度,这种模糊看上去由一个个的小格子组成,便形象的称这种画面为马赛克。其处理过程分为两个步骤,首先是图片分隔成指定大小的网格,然后给网格填充某个颜色。

下图是通过这两个步骤处理的结果,你也动手试一试吧。

运行效果

裁切

  刮刮乐图片通过在原始图像上添加一层特殊的涂层来隐藏图像内容。这层涂层通常是不透明的,因此我们无法直接看到底层的图像。当我们使用刮刮乐工具轻轻刮擦这层涂层时,就可以逐渐揭示出隐藏在背后的原始图像。这种技术常被用于制作互动游戏、促销活动或者数字艺术作品中,为用户带来独特的视觉体验和乐趣。

  如果是在图形系统实现这样的功能,那么使用裁切技术就必不可少,类似下图这样的效果,你也动手试一试吧。

运行效果

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

历史发布版本

第1版发布时间:2023-11-01
第2版发布时间:2024-06-08