在我开发我的个人开源项目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 对象。
xImageData 对象左上角的 x 坐标,以像素计。
yImageData 对象左上角的 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.设置globalCompositeOperationsource-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.设置globalCompositeOperationdestination-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.设置globalCompositeOperationdestination-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实现了一个绘制马赛克的功能,有任何问题或改进建议,欢迎评论区交流,互相学习。

个人笔记记录 2021 ~ 2025