预热
💋Ladies and gentlemen, welcome to my makeup show!
从零开始筹备一场令人难忘的化妆变装秀,打造一场时空跨越灵魂的艺术与技术的流动对话,遵从
code hard, play hard的宗旨,敬请期待!!!
场地
🖌 布置画布
首先, 我们需要想两个问题:第一:如何来改变图片里面的像素信息,比如颜色?第二:提前如何判断周边是否是相同颜色呢?
这里我们用图像画到canvas里,然后接下来所有的变颜色,就只需拿到canvas的图像信息,然后进行更改。
核心魔法原理: 一个canvas,一个颜色的选择器。
1️⃣ 画布:我们将把时尚图片"摊开"在Canvas这个数字画布上;
2️⃣ 调色盘:通过操纵Canvas的像素数据,实现精准换色法术;
3️⃣ 雷达: 智能色彩雷达,在施法前自动检测周边色域,防止颜色越界。
<body>
<p>
<input type="color" />
</p>
<canvas></canvas>
<script src="./index.js"></script>
</body>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
canvas {
display: block;
margin: 1em auto;
border: 2px solid #000;
}
p {
text-align: center;
}
👷👷♀️人员standby
// 🎨 获取颜色选择器 - "魔法颜料盒"
const inpColor = document.querySelector('input[type="color"]');
// 🖼️ 获取Canvas画布 - "T台"
const cvs = document.querySelector('canvas');
// ✨ 获取2D渲染上下文 - "魔法画笔"
const ctx = cvs.getContext('2d');
🌟布置画布
首先,我们要把canvas里面画上我们的图,以后呢方便咱们好拿图里面的数据做文章。
// 🎬 初始化画布 - 舞台
function init() {
// 👗 创一个Image对象 - 虚拟の"穿衣模特"
const img = new Image();
// 📌 设置图片加载完成的回调 - 模特就位
img.onload = () => {
// 📏 调整画布尺寸匹配图片原始尺寸(确保像素级精确)
cvs.width = img.width; // 设置画布实际宽度
cvs.height = img.height; // 设置画布实际高度
// 🖼️ 同步设置CSS显示尺寸(保持视觉一致性)
cvs.style.width = `${img.width}px`;
cvs.style.height = `${img.height}px`;
// ✨ 将模特图片绘制到画布上(让模特登上T台)
ctx.drawImage(img, 0, 0);
};
// 📌 设置图片路径 - 模特形象
img.src = './model.png';
}
init();
这里详细说一下drawImage,以及怎么用它:
这个drawImage在canvas中非常核心,跟ps(photoshop)贴图差不多意思。
ctx.drawImage(image, x, y);
参数说明:
image: 图像源x: 目标位置的x坐标y: 目标位置的y坐标
换装
换装预备工作
舞台好了,模特到位了。剩下就可以看看换装了。
那么前提我们先监听一下先换哪里:
// 同色块变装,监听点了画布哪个坐标点
cvs.addEventListener("click", (e) => {
const x = e.offsetX;
const y = e.offsetY;
});
知道坐标之后,就可以改颜色了。
换点
接下来看看改变点击的那个点的颜色:
canvas上下文有个getImageData的函数,返回画布的像素数据:
{
width: 区域宽度(像素数),
height: 区域高度(像素数),
data: Uint8ClampedArray // 核心像素数据数组
}
像素数据的data数组是是按rgba顺序的一维数组,每4个元素代表一个像素:
[ R, G, B, A, R, G, B, A, R, G, B, A, ... ]
↑ 第一个像素 ↑ 第二个像素 ↑ 第三个像素
rgb元素值的范围: 0-255。
a: 0(完全透明) - 255(完全不透明)
像素索引公式: index = (y * width + x) * 4
-
y*width:一行有width个像素,所以,第y行的起始位置是y*width; -
+x:定位到具体的第x列,也就是当前行的横向偏移量。 -
*4:每个像素占用4个数组元素(rgba)。所以这里需要乘以4才能找到该像素的起始位置。
以此公式可以得到一个辅助函数,也就是通过点击的x坐标,y坐标去换索引的辅助函数:
function points2Index(x, y) {
return (y * cvs.width + x) * 4;
}
打辅助函数
function changeColor(x, y) {
const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
const pointColor = getColor(x, y, imgData); // 获得点击的那个点的颜色
const targetColor = hexToRgb(inpColor.value);
...
}
getColor:
这其中辅助函数是getColor——获取像素颜色:
function getColor(x, y, cvsColors) {
const index = points2Index(x, y); // 返回该RGBA数据在数组中
return cvsColors.data.slice(index, index + 4); // 返回4个元素,也就是[R, G, B, A]
}
hexToRgb —— 十六进制颜色值转换成rgb数组:
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return [r, g, b, 255]; // 固定透明度为255(不透明)
}
hex to rgb:
十六进制颜色值转换成rgb数组
转换过程
红色分量提取
hex.slice(1, 3)获取1-2位字符parseInt(..., 16)将十六进制字符串转为十进制数值
绿色是3-4位字符;蓝色是5-6位字符。
const targetColor = hexToRgb(inpColor.value);
// 如果inpColor.value是“#ff3366”,则返回的是“[255, 51, 102, 255]”
ps - 油漆桶
接着到了最重要的环节了,万事俱备只欠东风。
该函数会从指定点(x, y)开始,向四周扩散填充目标颜色,但只会替换与起点颜色相似的相邻像素。
function changeColor(x, y) {
// 辅助函数
const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height); // 获取整个画布的像素数据
const centerColor = getColor(x, y, imgData); // 获取点击点的原始颜色
const targetColor = hexToRgb(inpColor.value); // 获取要填充的目标颜色
// 内部递归
function _changeColor(x, y) {
const stack = [[x, y]]; // 初始化栈,放入起始点
while (stack.length > 0) { // 使用栈代替递归
const [x, y] = stack.pop(); // 取出栈顶坐标
const i = point2Index(x, y); // 转换为像素索引
const color = getColor(x, y, imgData); // 获取当前像素颜色
// 边界检查
if (x < 0 || x >= cvs.width || y < 0 || y >= cvs.height) {
continue;
}
// 跳过已经是目标颜色的像素
if (diff(color, targetColor) === 0) {
continue;
}
// 颜色相似度检查(差异>50就跳过)
if (diff(color, centerColor) > 50) {
continue;
}
// 设置新颜色
imgData.data.set(targetColor, i);
// 将相邻像素压入栈
stack.push(
[x - 1, y], // 左
[x + 1, y], // 右
[x, y - 1], // 上
[x, y + 1] // 下
);
}
}
_changeColor(x, y); // 设置
ctx.putImageData(imgData, 0, 0); // 将修改后的像素数据写回画布
}
重难点:
第一点:四向扩散 —— 检查上下左右四个方向的相邻像素。
第二点:边界控制 —— 画布边界检查、颜色差异阈值控制(50)。
第三点:避免重复处理 —— 跳过已经是目标的像素。
第四点:非递归实现 —— 使用栈结构避免递归深度限制。
这个实现是跟ps软件中的“油漆桶工具”有异曲同工之妙。