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

5,663 阅读3分钟

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

先看看效果图:

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

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

<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返回数据结构如下:

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

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