在我开发我的个人开源项目region-screenshot-js时,需要加一个图片马赛克绘制功能,在这里分享我的实现方式,希望对大家所有启发。本文要求你最好有一定的canvas知识储备,没有也没关系,我会详细介绍相关api。
示例(可使用鼠标在图片上绘制马赛克):
马赛克,指现行广为使用的一种图像处理手段,此手段将影像特定区域的色阶细节劣化并造成色块打乱的效果,因为这种模糊看上去有一个个的小格子组成,便形象的称这种画面为马赛克。其目的通常是使之无法辨认。
——摘自百度百科
通过上面的原图与马赛克图相比,可以很直观的看到马赛克图由一个个色块
构成。单个色块内仅有一种颜色
。算法的核心原理非常简单,通过循环的方式获取到当前色块位置所对应的所有原始像素点,将这些像素点的平均色值用于色块。
明白了马赛克制作原理,我们需要学习两个相关的api来帮助我们实现功能。
1.getImageData
该方法接收一个矩形区域,返回 ImageData 对象,该对象拷贝了指定矩形区域内的像素数据。
1let imageData = ctx.context.getImageData(x,y,width,height)
参数 | 描述 |
---|---|
x | 开始复制的左上角位置的 x 坐标。 |
y | 开始复制的左上角位置的 y 坐标。 |
width | 将要复制的矩形区域的宽度。 |
height | 将要复制的矩形区域的高度。 |
下图是getImageData
的返回值,其中data
属性为每个像素点的颜色值,数组中每四项可看为一组,分别代表像素点的R、G、B、A值。
2.putImageData
该方法将已有的ImageData 对象放回画布上。
1ctx.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
参数 | 描述 |
---|---|
imgData | 规定要放回画布的 ImageData 对象。 |
x | ImageData 对象左上角的 x 坐标,以像素计。 |
y | ImageData 对象左上角的 y 坐标,以像素计。 |
dirtyX | 可选。水平值(x),以像素计,在画布上放置图像的位置。 |
dirtyY | 可选。水平值(y),以像素计,在画布上放置图像的位置。 |
dirtyWidth | 可选。在画布上绘制图像所使用的宽度。 |
dirtyHeight | 可选。在画布上绘制图像所使用的高度。 |
3.举个栗子🌰
使用上面两个api来实现图片颜色反转。
1let canvas = document.querySelector('canvas');
2let ctx = canvas.getContext('2d');
3let img = new Image();
4img.src = "./loopy.png";
5img.onload = function () {
6 ctx.drawImage(img, 0, 0);
7 let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
8 for(let i=0;i<imageData.data.length;i+=4){
9 imageData.data[i] = 255 - imageData.data[i];
10 imageData.data[i+1] = 255 - imageData.data[i+1];
11 imageData.data[i+2] = 255 - imageData.data[i+2];
12 imageData.data[i+3] = imageData.data[i+3];
13 }
14 ctx.putImageData(imageData, 0, 0);
15}
原图:
处理后:
通过上面的学习,我们已经有能力来实现马赛克效果了。
1let canvas = document.querySelector('canvas');
2let ctx = canvas.getContext('2d');
3let img = new Image();
4img.src = "./1.webp";
5img.onload = function () {
6 ctx.drawImage(img, 0, 0);
7
8 let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
9
10 const suquareSize = 10;
11 let data = imageData.data;
12
13 for (let i = 0; i < canvas.height; i += suquareSize) {
14 for (let j = 0; j < canvas.width; j += suquareSize) {
15 let totalR = 0;
16 let totalG = 0;
17 let totalB = 0;
18 let totalA = 0;
19 let count = 0;
20
21 for (let y = i; y < i + suquareSize && y < canvas.height; y++) {
22 for (let x = j; x < j + suquareSize && x < canvas.width; x++) {
23
24
25
26 let pixelIndex = (y * canvas.width + x) * 4;
27 totalR += data[pixelIndex];
28 totalG += data[pixelIndex + 1];
29 totalB += data[pixelIndex + 2];
30 totalA += data[pixelIndex + 3];
31 count++;
32 }
33 }
34 let avgR = totalR / count;
35 let avgG = totalG / count;
36 let avgB = totalB / count;
37 let avgA = totalA / count;
38
39 for (let y = i; y < i + suquareSize && y < canvas.height; y++) {
40 for (let x = j; x < j + suquareSize && x < canvas.width; x++) {
41 let pixelIndex = (y * canvas.width + x) * 4;
42 data[pixelIndex] = avgR;
43 data[pixelIndex + 1] = avgG;
44 data[pixelIndex + 2] = avgB;
45 data[pixelIndex + 3] = avgA;
46 }
47 }
48 }
49 }
50
51 ctx.putImageData(imageData, 0, 0);
52}
大功告成✨
下面我们来实现一个进阶功能,使用鼠标绘制马赛克。在此之前我们还需要学习一个属性globalCompositeOperation
。
1.globalCompositeOperation 属性
globalCompositeOperation
属性设置如何将一个新的图像绘制到已有的图像上。单看这句话有点不知所云,没关系,结合下面的实例很容易理解。
实现一个画笔功能
1.设置globalCompositeOperation
为source-over
(默认值)
source-over:在原有图像上显示新绘制的图像
1let canvas = document.querySelector('canvas');
2let ctx = canvas.getContext('2d');
3let img = new Image();
4img.src = "./1.webp";
5img.onload = function () {
6 ctx.drawImage(img, 0, 0);
7}
8canvas.onmousedown = (e) => {
9 ctx.beginPath()
10 canvas.onmousemove = (e) => {
11 ctx.globalCompositeOperation = "source-over";
12 let { left, top } = canvas.getBoundingClientRect();
13 let mouseRelativeX = e.clientX - left;
14 let mouseRelativeY = e.clientY - top;
15 ctx.lineTo(mouseRelativeX,mouseRelativeY);
16 ctx.lineWidth = 10;
17 ctx.stroke();
18 }
19}
20canvas.onmouseup = () => {
21 canvas.onmousemove = null;
22}
2.设置globalCompositeOperation
为destination-out
destination-out:只会显示新绘制图像之外的原有图像,新绘制的图像是透明的。
1let canvas = document.querySelector('canvas');
2let ctx = canvas.getContext('2d');
3let img = new Image();
4img.src = "./1.webp";
5img.onload = function () {
6 ctx.drawImage(img, 0, 0);
7}
8canvas.onmousedown = (e) => {
9 ctx.beginPath()
10 canvas.onmousemove = (e) => {
11 ctx.globalCompositeOperation = "destination-out";
12 let { left, top } = canvas.getBoundingClientRect();
13 let mouseRelativeX = e.clientX - left;
14 let mouseRelativeY = e.clientY - top;
15 ctx.lineTo(mouseRelativeX,mouseRelativeY);
16 ctx.lineWidth = 10;
17 ctx.stroke();
18 }
19}
20canvas.onmouseup = () => {
21 canvas.onmousemove = null;
22}
3.设置globalCompositeOperation
为destination-in
destination-in:只会显示新绘制图像之内的原有图像,新绘制的图像是透明的。
1let canvas = document.querySelector('canvas');
2let ctx = canvas.getContext('2d');
3let img = new Image();
4img.src = "./1.webp";
5let isInit = true;
6canvas.onmousedown = (e) => {
7
8
9
10 canvas.onmousemove = (e) => {
11
12
13
14 ctx.globalCompositeOperation = "source-over";
15 ctx.drawImage(img, 0, 0);
16 ctx.globalCompositeOperation = "destination-in";
17 let { left, top } = canvas.getBoundingClientRect();
18 let mouseRelativeX = e.clientX - left;
19 let mouseRelativeY = e.clientY - top;
20
21 if (isInit) {
22 ctx.moveTo(mouseRelativeX, mouseRelativeY);
23 isInit = false;
24 }
25 ctx.lineTo(mouseRelativeX, mouseRelativeY);
26 ctx.lineWidth = 10;
27 ctx.stroke();
28 }
29}
30canvas.onmouseup = () => {
31 isInit = true;
32 canvas.onmousemove = null;
33}
2.功能实现
核心思路
1.再创建一个与现有画布大小相同的画布,填充上马赛克图像,不将其添加到dom中。
2.在鼠标涂抹页面上的canvas的过程中同时按照上文中destination-in
画笔演示的方式对新创建的马赛克画布进行绘制。
3.此时马赛克画布在鼠标经过的地方显示了马赛克,而其他部分是透明的,接着将它的内容叠加显示到页面的画布中去,我们就实现了鼠标涂抹绘制马赛克的功能。
完整代码:
1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Document</title>
8</head>
9
10<body>
11 <canvas width="499px" height="839px"></canvas>
12</body>
13<script>
14 let canvas = document.querySelector('canvas');
15 let ctx = canvas.getContext('2d');
16 let img = new Image();
17 img.src = "./1.webp";
18 img.onload = function () {
19 ctx.drawImage(img, 0, 0);
20
21 let canvasMosaic = document.createElement("canvas");
22 let ctxMosaic = canvasMosaic.getContext('2d');
23 canvasMosaic.width = canvas.width;
24 canvasMosaic.height = canvas.height;
25
26 let originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
27
28 let mosaicImageData = toMosaicImageData(originalImageData);
29
30
31
32 let isInit = true;
33 canvas.onmousedown = (e) => {
34 canvas.onmousemove = (e) => {
35
36
37
38
39 ctxMosaic.globalCompositeOperation = "source-over";
40 ctxMosaic.putImageData(mosaicImageData, 0, 0);
41 ctxMosaic.globalCompositeOperation = "destination-in";
42 let { left, top } = canvas.getBoundingClientRect();
43 let mouseRelativeX = e.clientX - left;
44 let mouseRelativeY = e.clientY - top;
45 if (isInit) {
46 ctxMosaic.moveTo(mouseRelativeX, mouseRelativeY);
47 isInit = false;
48 }
49 ctxMosaic.lineTo(mouseRelativeX, mouseRelativeY);
50 ctxMosaic.lineWidth = 10;
51 ctxMosaic.stroke();
52 ctx.drawImage(canvasMosaic, 0, 0);
53 }
54 }
55 canvas.onmouseup = () => {
56 isInit = true;
57 canvas.onmousemove = null;
58 }
59 }
60
61 function toMosaicImageData(imageData) {
62
63 const suquareSize = 10;
64 let data = imageData.data;
65
66 for (let i = 0; i < canvas.height; i += suquareSize) {
67 for (let j = 0; j < canvas.width; j += suquareSize) {
68 let totalR = 0;
69 let totalG = 0;
70 let totalB = 0;
71 let totalA = 0;
72 let count = 0;
73
74 for (let y = i; y < i + suquareSize && y < canvas.height; y++) {
75 for (let x = j; x < j + suquareSize && x < canvas.width; x++) {
76
77
78
79 let pixelIndex = (y * canvas.width + x) * 4;
80 totalR += data[pixelIndex];
81 totalG += data[pixelIndex + 1];
82 totalB += data[pixelIndex + 2];
83 totalA += data[pixelIndex + 3];
84 count++;
85 }
86 }
87 let avgR = totalR / count;
88 let avgG = totalG / count;
89 let avgB = totalB / count;
90 let avgA = totalA / count;
91
92 for (let y = i; y < i + suquareSize && y < canvas.height; y++) {
93 for (let x = j; x < j + suquareSize && x < canvas.width; x++) {
94 let pixelIndex = (y * canvas.width + x) * 4;
95 data[pixelIndex] = avgR;
96 data[pixelIndex + 1] = avgG;
97 data[pixelIndex + 2] = avgB;
98 data[pixelIndex + 3] = avgA;
99 }
100 }
101 }
102 }
103 return imageData;
104 }
105
106</script>
107
108</html>
以上是我在开发个人开源项目region-screenshot-js的部分技术总结,我们从0到1实现了一个绘制马赛克的功能,有任何问题或改进建议,欢迎评论区交流,互相学习。