大家好,我是前端林叔。
最近在看Chrome浏览器插件开发,大家都知道,学习这件事,光看不练是不能说自己学会了的,所以在看了几天官方文档之后,想要做点小玩意。正好以前一直心心念念想做一个专为前端同学服务的工具插件,这次就当练手Demo搞起来,还起了个好玩的名字「有个锤子」, github地址 。
今天开发的这个功能是颜色提取器,经常做前端开发的同学都会遇到过这个问题,有时候想要从某个设计稿或者页面中提取个色值,特别是从图片上取个颜色,以前是非常不方便的,不过现在很多截图工具都带了这个能力,所以这个工具也没啥用了,纯粹只是为了练手,如下图。
期望实现的效果:
- 支持通过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>
读取剪切板能力有了,还需要监听用户的粘贴动作,注意区分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浏览器试试效果。