做了一个没啥用的颜色提取器插件

247 阅读5分钟

大家好,我是前端林叔。

最近在看Chrome浏览器插件开发,大家都知道,学习这件事,光看不练是不能说自己学会了的,所以在看了几天官方文档之后,想要做点小玩意。正好以前一直心心念念想做一个专为前端同学服务的工具插件,这次就当练手Demo搞起来,还起了个好玩的名字「有个锤子」, github地址

今天开发的这个功能是颜色提取器,经常做前端开发的同学都会遇到过这个问题,有时候想要从某个设计稿或者页面中提取个色值,特别是从图片上取个颜色,以前是非常不方便的,不过现在很多截图工具都带了这个能力,所以这个工具也没啥用了,纯粹只是为了练手,如下图。

color-pick.gif

期望实现的效果:

  • 支持通过Ctrl/Command + V 快捷键粘贴图片,当然也支持上传图片
  • 上传图片后自动识别图片中的主色调,把颜色占比最高的10个颜色展示出来
  • 鼠标移入到图片上,展示鼠标所在位置的色值
  • 点击某个色值要能写入剪切板

好了,我们来看看这几个功能怎么实现

粘贴图片的实现

粘贴图片通常有两种方案。

方案一:利用dom的contenteditable能力

当一个dom设置属性contenteditable="true"之后,它就支持监听paste事件,在事件event中可以获取图片文件,然后利用FileReader读取图片文件。

<div id="pasteArea" contenteditable="true">
    粘贴到这里
</div>
<script>
    const pasteArea = document.getElementById('pasteArea');

    pasteArea.addEventListener('paste', function(event) {
        const items = (event.clipboardData || window.clipboardData).items;

        for (let i = 0; i < items.length; i++) {
            if (items[i].type.indexOf("image") !== -1) {
                const blob = items[i].getAsFile();
                const reader = new FileReader();
                reader.onload = function(event) {
                    const img = new Image();
                    img.src = event.target.result;
                    document.body.appendChild(img); 
                };

                reader.readAsDataURL(blob);
            }
        }
    });
</script>

不过我没有选择这个方案,有两个原因,第一就是这个能力快要被废弃了,第二就是设置了contenteditable="true"之后,这个dom就像是一个编辑器,用户可以随意输入内容,不一定是粘贴图片,这个交互无法接受。

方案二:navigator.clipboard

Navigator 接口的只读属性 clipboard 返回一个用于读写剪贴板内容的 Clipboard 对象,可用于在 Web 应用程序中实现剪切、复制和粘贴功能。

navigator.clipboard.read()可以用于读取剪切板,读取结果为一个对象数组,在读取图片之前,需要判断下内容的type是否为图片。

<script>
    navigator.clipboard.read().then(function (result) {
        for (let i = 0; i < result.length; i++) {
            let types = result[i].types;
            for (let j = 0; j < types.length; j++) {
                if (types[j].startsWith('image')) {
                    result[i].getType(types[j]).then(data => {
                        let reader = new FileReader();
                        reader.onload = function (event) {
                            const img = new Image();
                            img.src = event.target.result;
                            document.body.appendChild(img);
                        };
                        reader.readAsDataURL(data);
                    });
                    return;
                }
            }
        }
    }).catch(e => {
        console.log(e);
    });
</script>

clipboard.png

读取剪切板能力有了,还需要监听用户的粘贴动作,注意区分window和mac用户的习惯,windows用户一般使用ctrl + v 而mac用户一般使用 command + v。

 <script>
    document.addEventListener('keydown', listenKeyDown);
    function listenKeyDown(event){
        if ((event.metaKey || event.ctrlKey) && event.key === 'v') {
            //读取剪切板中的图片
        }
    }
</script>

获取图片的主色调实现

为了获取图片的颜色,我们需要借助canvas,在上一步中,我们获取到了图片,可以把图片绘制到canvas上,然后利用canvas的getImageData获取画布上的每个像素点,每个像素点由一个长度为4的数组组成,分别存储像素点颜色的r、g、b、a信息。

我们可以遍历所有的像素点,把每个色值对应的数量存下来,那出现次数最多的颜色不就是主色调了嘛,不过这里还要注意一点,有的颜色很相近,应该剔除。

rgb(255, 255, 255) 出现1000次,主色调
rgb(255, 255, 254) 出现800次 ,忽略,和上面的颜色很接近
rgb(100, 125, 200) 出现100次,主色调

怎么判断两个颜色相近呢,我没有采用什么高深的算法,两个颜色r、g、b的差值的绝对值加起来小于30,我就认为是相近色值,至于为什么是30,我拍脑袋加测试得来的。

//这两个颜色的差值为3,相近颜色
rgb(255, 255, 255) 
rgb(254, 254, 254)

//这两个颜色差值为300,不相近
rgb(255, 255, 255) 
rgb(155, 155, 155)

核心代码:

<script>
    //获取主色调
    function getMostCommonColors() {
        const numColors = 10;
        const ctx = canvas2d; //canvas的 getContext('2d')对象
        const imageData = ctx.getImageData(0, 0, width.value, height.value);
        const data = imageData.data;
        const colorCount = {};

        // 遍历像素数据
        for (let i = 0; i < data.length; i += 4) {
            const r = data[i];
            const g = data[i + 1];
            const b = data[i + 2];
            if (r > 250 && g > 250 && b > 250 || r < 5 && g < 5 && b < 5) {
                continue;
            }
            const key = `${r},${g},${b}`;
            if (!colorCount[key]) {
                colorCount[key] = 0;
            }
            colorCount[key]++;
        }

        // 将颜色计数转换为数组并排序
        const colors = Object.keys(colorCount).map(key => {
            let rgb = key.split(',').map(Number);
            return {
                rgb: rgb,
                hex: rgbToHex(...rgb),
                count: colorCount[key]
            };
        });
        colors.sort((a, b) => b.count - a.count);

        let result = [];

        for (let i = 0; i < colors.length; i++) {
            if (result.length >= numColors) {
                break;
            }
            if (result.find(item => diffColor(item, colors[i]) <= 30)) {
                continue;
            }
            result.push(colors[i]);
        }

        tableData[2].colors = result.map(item => ({
            rgb: `rgb(${item.rgb[0]}, ${item.rgb[1]}, ${item.rgb[2]})`,
            hex: item.hex
        }));
    }

    //计算两个颜色差值
    function diffColor(source, target) {
        return Math.max(Math.abs(source.rgb[0] - target.rgb[0]), Math.abs(source.rgb[1] - target.rgb[1]), Math.abs(source.rgb[2] - target.rgb[2]));
    }

    //rgb颜色转为16进制颜色
    function rgbToHex(r, g, b) {
        // 确保输入在0-255之间
        r = Math.floor(Math.max(Math.min(r, 255), 0));
        g = Math.floor(Math.max(Math.min(g, 255), 0));
        b = Math.floor(Math.max(Math.min(b, 255), 0));

        // 转换为16进制并填充前导零
        let hexR = r.toString(16).padStart(2, '0');
        let hexG = g.toString(16).padStart(2, '0');
        let hexB = b.toString(16).padStart(2, '0');

        // 拼接结果
        return `#${hexR}${hexG}${hexB}`;
    }
</script>

鼠标移入提取颜色实现

鼠标移动到图片上的某个位置,需要提取对应的颜色,这里我们首先要计算出鼠标在画布上的位置,鼠标在画布上的位置等于鼠标在屏幕中的位置减去canvas画布在屏幕中的位置。

通过canvas2d.getImageData,可以获取画布上某个位置的像素信息。

//canvas的鼠标move事件
function mousemove(event){
    //获取像素点信息
    const pixelData = getPixel(event);
    
    //提取像素点的颜色信息,赋给页面表格
    tableData[0].colors = [
        {
            rgb: `rgb(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]})`,
            hex: rgbToHex(pixelData[0], pixelData[1], pixelData[2])
        }
    ]
}

function getPixel(event){
    //canvas在屏幕上的矩形信息
    const rect = canvas.value.getBoundingClientRect();
    
    //计算鼠标在画布上的位置
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;
    
    // 检查坐标是否在画布范围内
    if (mouseX < 0 || mouseX >= canvas.width || mouseY < 0 || mouseY >= canvas.height) {
        return; // 坐标超出画布范围
    }

    // 获取该坐标的像素数据
    const imageData = canvas2d.getImageData(mouseX, mouseY, 1, 1);
    return  imageData.data;
}

复制色值信息的实现

好了,现在颜色已经提取出来了,为了方便用户复制颜色,我们在用户点击色值的时候,把颜色写入剪切板。这个实现就非常简单了,直接调用navigator.clipboard.writeText,返回的是Promise对象。

function writeTextToClipboard(text) {
    return navigator.clipboard.writeText(text);
}

总结

好了,这就是一个简单的图片颜色提取器的实现,虽然这个功能可能现在已经用处不大了,不过通过实现这个功能还是让我了解到了浏览器的剪切板功能,大家可以学习下 navigator.clipboard,非常不错,因为我是做Chrome插件,所以我不需要考虑浏览器兼容性。

canvas也是前端非常重要的一个能力,这次我们主要利用getImageData方法来获取画布的颜色信息。

具体实现见 github地址 , 感兴趣的同学可以下载下来安装到chrome浏览器试试效果。