概述
平时开发中可能会遇到一些图像处理的工作,本文就通过HTML5 Canvas实现一个类似油漆桶工具的颜色填充功能。
功能:将该区域中颜色相近的像素填充为目标颜色,实现类似Photoshop中油漆桶工具的效果。
实现原理
颜色填充算法的核心是种子填充算法 ,也称为"泛洪填充"。基本思路是从一个起始点(种子)开始,向四周扩散,将颜色相近的像素替换为目标颜色。

如上图,是一个3px,3px 像素的图片。如果鼠标点击到(1,1)坐标的像素,扩散算法就是向上下左右去扩散,被扩散的位置继续向上下左右去扩散,直到超出边界,或者颜色不相近,或者颜色和目标颜色完全相同为止。
实现步骤
- 需要先加载一张图片到canvas画布中。
- 给画布添加点击事件,获取到点击的坐标。
- 根据坐标获取到点击位置的颜色
- 修改当前像素点的位置颜色,并且扩散修改相邻位置 颜色相近的 像素点颜色,(递归)
代码结构
1.HTML结构
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas颜色填充工具</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
display: block;
border: 1px solid #ccc; /* 添加边框便于观察 */
}
</style>
</head>
<body>
<canvas></canvas>
<script src="main.js"></script>
</body>
</html>
2.初始化Canvas和图像加载
javascript
const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true // 优化性能,频繁读取图像数据
});
// 初始化:加载图片到Canvas
function init() {
const img = new Image();
img.onload = () => {
cvs.width = img.width;
cvs.height = img.height;
ctx.drawImage(img, 0, 0, img.width, img.height);
};
img.src = './img/image.png'; // 替换为你的图片路径
}
init();
核心算法:颜色填充
javascript
// 监听Canvas点击事件
cvs.addEventListener('click', (e) => {
// 1. 获取点击位置的像素信息
const x = e.offsetX;
const y = e.offsetY;
const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
const clickColor = getColor(x, y, imgData.data);
// 2. 改变颜色(目标颜色为半透明黑色)
changeColor(x, y, [0, 0, 0, 20], imgData.data, clickColor);
// 3. 将修改后的图像数据重新绘制到Canvas
ctx.putImageData(imgData, 0, 0);
});
/**
* 颜色填充算法 - 递归实现
* @param {number} x - 当前像素点的x坐标
* @param {number} y - 当前像素点的y坐标
* @param {Array} targetColor - 目标颜色 [R, G, B, A]
* @param {Uint8ClampedArray} imgData - 图像像素数据
* @param {Array} clickColor - 初始点击位置的颜色
* @returns {void}
*/
function changeColor(x, y, targetColor, imgData, clickColor) {
// 边界检查:确保坐标在Canvas范围内
if (x < 0 || x >= cvs.width || y < 0 || y >= cvs.height) {
return;
}
// 获取当前像素颜色
const curColor = getColor(x, y, imgData);
// 终止条件1:当前颜色与点击颜色差异过大(不是同一区域)
if (diff(clickColor, curColor) > 100) {
return;
}
// 终止条件2:当前颜色已经是目标颜色
if (diff(curColor, targetColor) === 0) {
return;
}
// 将当前像素颜色修改为目标颜色
const index = point2Index(x, y);
imgData.set(targetColor, index);
// 递归扩散:向上下左右四个方向继续填充
changeColor(x + 1, y, targetColor, imgData, clickColor);
changeColor(x - 1, y, targetColor, imgData, clickColor);
changeColor(x, y + 1, targetColor, imgData, clickColor);
changeColor(x, y - 1, targetColor, imgData, clickColor);
}
/**
* 计算两个颜色之间的差异
* @param {Array} color1 - 颜色数组 [R, G, B, A]
* @param {Array} color2 - 颜色数组 [R, G, B, A]
* @returns {number} 颜色差异值
*/
function diff(color1, color2) {
return Math.abs(color1[0] - color2[0]) +
Math.abs(color1[1] - color2[1]) +
Math.abs(color1[2] - color2[2]) +
Math.abs(color1[3] - color2[3]);
}
/**
* 将像素坐标转换为图像数据数组的索引
* @param {number} x - x坐标
* @param {number} y - y坐标
* @returns {number} 数组索引
*/
function point2Index(x, y) {
// 每个像素由4个值表示:R, G, B, A
return (y * cvs.width + x) * 4;
}
/**
* 获取指定位置的颜色
* @param {number} x - x坐标
* @param {number} y - y坐标
* @param {Uint8ClampedArray} imgData - 图像像素数据
* @returns {Array} 颜色数组 [R, G, B, A]
*/
function getColor(x, y, imgData) {
const index = point2Index(x, y);
return [
imgData[index], // R
imgData[index + 1], // G
imgData[index + 2], // B
imgData[index + 3] // A
];
}
算法优化
问题分析
上述递归实现在处理大面积区域时可能导致栈溢出,因为递归深度可能非常大。此外,性能也有优化空间。
优化方案:使用队列的迭代算法
javascript
/**
* 优化版颜色填充算法 - 使用队列迭代实现
*/
function changeColorOptimized(startX, startY, targetColor, imgData, clickColor) {
// 创建队列存储待处理的像素点
const queue = [];
queue.push({ x: startX, y: startY });
// 记录已访问的像素,避免重复处理
const visited = new Set();
while (queue.length > 0) {
const { x, y } = queue.shift();
const key = `${x},${y}`;
// 检查边界
if (x < 0 || x >= cvs.width || y < 0 || y >= cvs.height) {
continue;
}
// 检查是否已访问
if (visited.has(key)) {
continue;
}
visited.add(key);
// 获取当前像素颜色
const curColor = getColor(x, y, imgData);
// 检查颜色是否匹配
if (diff(clickColor, curColor) > 100) {
continue;
}
if (diff(curColor, targetColor) === 0) {
continue;
}
// 修改颜色
const index = point2Index(x, y);
imgData.set(targetColor, index);
// 将相邻像素加入队列
queue.push({ x: x + 1, y });
queue.push({ x: x - 1, y });
queue.push({ x, y: y + 1 });
queue.push({ x, y: y - 1 });
}
}
颜色过度不自然
前面采用的颜色比对算法,比较简单粗暴,color1和color2的rgba进行相减取绝对值,但是这种遇到极端情况下,效果不好。
这里比较科学的算法是实用欧几里得距离 也就是简单的勾股定理
javascript
function diff(color1,color2){
// 计算RGB差值(不考虑Alpha)
const rDiff = color1[0] - color2[0];
const gDiff = color1[1] - color2[1];
const bDiff = color1[2] - color2[2];
// 欧几里得距离
return Math.sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
}
总结
至此,我们实现了一个基于Canvas的颜色填充工具,并讲解了:
- Canvas图像数据处理的基本原理
- 种子填充算法的递归和迭代实现
- 性能优化技巧
这个工具可以作为图像处理应用的基础,进一步扩展可实现更复杂的功能,如图像编辑、滤镜应用等。
注意:实际使用中,建议使用优化版的队列迭代算法,避免递归可能导致的栈溢出问题。同时,对于大型图像,可以考虑分块处理或使用Web Worker进行多线程处理以提高性能。