注册
web

玩转Canvas——给坤坤变个颜色

Canvas可以绘制出强大的效果,让我们给坤坤换个色。


先看看效果图:



9da749951067480ab24f2fc44c78ade9~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image

要怎么实现这样一个可以点击换色的效果呢?
话不多说,入正题。


第一步,创建基本元素,无须多言:


<body>
<canvas></canvas>
</body>

我们先加载坤坤的图片,然后给canvas添加基础事件:


const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true
});
//加载图片并绘制
const img = new Image();
img.src = './img/cxk.png';
img.onload = () => {
cvs.width = img.width;
cvs.height = img.height;
ctx.drawImage(img, 0, 0);
};

再给canvas注册一个点击事件:


//监听canvas点击
cvs.addEventListener('click', clickCb);

function clickCb(e) {
const x = e.offsetX;
const y = e.offsetY;
}

这样就拿到了点击的坐标,接下来的问题是,我们要如何拿到点击坐标的颜色值呢?


其实,canvas早就给我们提供了一个强大的api:getImageData


我们可以通过它获取整个canvas上每个像素点的颜色信息,一个像素点对应四个值,分别为rgba


ctx.getImageData返回数据结构如下:



a2d21048539e4647b163fbb8d3ee6207~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image?

所以我们便可以利用它拿到点击坐标的颜色值:


function clickCb(e) {
//省略之前代码
//...

const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
//获取点击坐标的rgba信息
const clickRgba = getColor(x, y, imgData.data);
}

//通过坐标获取rgba数组
function getColor(x, y, data) {
const i = getIndex(x, y);
return [data[i], data[i + 1], data[i + 2], data[i + 3]];
}

//通过坐标x,y获取对应imgData数组的索引
function getIndex(x, y) {
return (y * cvs.width + x) * 4;
}

接下来便是在点击处绘制我们的颜色值:


//为了方便,这里将变色值写死为原谅绿
const colorRgba = [0, 255, 0, 255];

function clickCb(e) {
//省略之前代码
//...

//坐标变色
function changeColor(x, y, imgData) {
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
}
changeColor(x, y, imgData);
}

此时如果点击坤坤的头发,会发现头发上仅仅带一点绿。要如何才能绿得彻底呢?


我们新增一个判断rgba值变化幅度的方法getDeff,当两者颜色相差过大,则视为不同区域。


//简单地根据绝对值之和判断是否为同颜色区域
function getDiff(rgba1, rgba2) {
return (
Math.abs(rgba2[0] - rgba1[0]) +
Math.abs(rgba2[1] - rgba1[1]) +
Math.abs(rgba2[2] - rgba1[2]) +
Math.abs(rgba2[3] - rgba1[3])
);
}

再新增一个判断坐标是否需要变色的方法:


function clickCb(e) {
//省略之前代码
//...

//判断该坐标是否无需变色
function stopChange(x, y, imgData) {
if (x < 0 || y < 0 || x > cvs.width || y > cvs.height) {
//超出canvas边界
return true
}
const rgba = getColor(x, y, imgData.data);
if (getDiff(rgba, clickRgba) >= 100) {
//色值差距过大
return true;
}
if (getDiff(rgba, colorRgba) === 0) {
//同颜色,不用改
return true;
}
}
}

我们更改changeColor方法,接下来便可以绿得彻底了:


function clickCb(e) {
//省略之前代码
//...

//坐标变色
function changeColor(x, y, imgData) {
if (stopChange(x, y, imgData)) {
return
}
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
//递归变色
changeColor(x - 1, y, imgData);
changeColor(x + 1, y, imgData);
changeColor(x, y + 1, imgData);
changeColor(x, y - 1, imgData);
}

//省略之前代码
//...
}

效果已然实现。但是上面通过递归调用函数去变色,如果变色区域过大,可能会导致栈溢出报错。


为了解决这个问题,我们得改用循环实现了。


这一步的实现需要一定的想象力。读者可以自己试试,看看能不能改用循环方式实现出来。


鉴于循环实现的代码略多,这里不再解释,直接上最终代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
.color-box {
margin-bottom: 20px;
}
.canvas-box {
text-align: center;
}
</style>
</head>
<body>
<div class="color-box">设置色值: <input type="color" /></div>
<div class="canvas-box">
<canvas></canvas>
</div>
<script>
let color = '#00ff00';
let colorRgba = getRGBAColor();
//hex转rgba数组
function getRGBAColor() {
const rgb = [color.slice(1, 3), color.slice(3, 5), color.slice(5)].map((item) =>
parseInt(item, 16)
);
return [...rgb, 255];
}

const input = document.querySelector('input[type=color]');
input.value = color;
input.addEventListener('change', function (e) {
color = e.target.value;
colorRgba = getRGBAColor();
});

const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true,
});
cvs.addEventListener('click', clickCb);

const img = new Image();
img.src = './img/cxk.png';
img.onload = () => {
cvs.width = 240;
cvs.height = (cvs.width * img.height) / img.width;
//图片缩放
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, cvs.width, cvs.height);
};

function clickCb(e) {
let x = e.offsetX;
let y = e.offsetY;
const pointMark = {};

const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
const clickRgba = getColor(x, y, imgData.data);
//坐标变色
function changeColor(x, y, imgData) {
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
markChange(x, y);
}

//判断该坐标是否无需变色
function stopChange(x, y, imgData) {
const rgba = getColor(x, y, imgData.data);
if (getDiff(rgba, clickRgba) >= 150) {
//色值差距过大
return true;
}
if (getDiff(rgba, colorRgba) === 0) {
//同颜色,不用改
return true;
}
if (hasChange(x, y)) {
//已变色
return true;
}
}
function hasChange(x, y) {
const pointKey = `${x}-${y}`;
return pointMark[pointKey];
}
function markChange(x, y) {
const pointKey = `${x}-${y}`;
pointMark[pointKey] = true;
}
//添加上下左右方向的点到等待变色的点数组
function addSurroundingPoint(x, y) {
if (y > 0) {
addWaitPoint(`${x}-${y - 1}`);
}
if (y < cvs.height - 1) {
addWaitPoint(`${x}-${y + 1}`);
}
if (x > 0) {
addWaitPoint(`${x - 1}-${y}`);
}
if (x < cvs.width - 1) {
addWaitPoint(`${x + 1}-${y}`);
}
}
function addWaitPoint(key) {
waitPoint[key] = true;
}
function deleteWaitPoint(key) {
delete waitPoint[key];
}
//本轮等待变色的点
const waitPoint = {
[`${x}-${y}`]: true,
};
while (Object.keys(waitPoint).length) {
const pointList = Object.keys(waitPoint);
for (let i = 0; i < pointList.length; i++) {
const key = pointList[i];
const list = key.split('-');
const x1 = +list[0];
const y1 = +list[1];

if (stopChange(x1, y1, imgData)) {
deleteWaitPoint(key);
continue;
}
changeColor(x1, y1, imgData);
deleteWaitPoint(key);
addSurroundingPoint(x1, y1);
}
}
}

//通过坐标x,y获取对应imgData数组的索引
function getIndex(x, y) {
return (y * cvs.width + x) * 4;
}

//通过坐标获取rgba数组
function getColor(x, y, data) {
const i = getIndex(x, y);
return [data[i], data[i + 1], data[i + 2], data[i + 3]];
}

////简单地根据绝对值之和判断是否为同颜色区域
function getDiff(rgba1, rgba2) {
return (
Math.abs(rgba2[0] - rgba1[0]) +
Math.abs(rgba2[1] - rgba1[1]) +
Math.abs(rgba2[2] - rgba1[2]) +
Math.abs(rgba2[3] - rgba1[3])
);
}
</script>
</body>
</html>

若有疑问,欢迎评论区讨论。


完整demo地址:canvas-change-color


作者:TRIS
来源:juejin.cn/post/7209226686372937789

0 个评论

要回复文章请先登录注册