众所周知,QQ截图是一个非常好用的功能,我经常拿它来精准取色,直接复制就能使用,非常方便。如果你也是QQ截图忠实用户,那你绝对熟悉下方操作:
- 使用快捷键(
Ctrl + Alt + A
)激活 QQ 截图; - 鼠标拖出一个截图框;
- 鼠标移动时,左下角实时显示该点的 RGB/HEX 颜色;
- 取色结束后可以复制、保存或贴图。

好巧不巧,之前有朋友问我这样的问题:用什么软件取色。我就回答的qq截图。现在想到这件事,我很好奇这些截图软件取色原理是什么?用的什么API。我能不能用前端技术实现一个类似功能呢?带着这样的疑问,我去网上搜了又搜,下面来看看我的结果吧。
取色的原理
作为程序员,我们都知道,屏幕是由成千上万的像素组成的,而每个像素都有自己独立的颜色信息。我们看到的图像,实际上是显示器在快速刷新一帧帧的 RGB 值阵列,这非常好理解。所以不用想都能明白,取色这个操作实际上就是在屏幕的某个位置上读取到该像素的颜色值。
既然图像是由像素组成的颜色阵列,那么如果我们想在代码里获取某个屏幕位置的颜色值(也就是取色) ,本质上就要知道:
- 它的颜色值存储在哪?
- 我怎么从那里读出来?
接下来我们一步步解开这个谜团。
第一个问题是,我肉眼是读取不出来这个值的,放大我也只能告诉你是蓝色还是绿色。所以只有屏幕自己才知道这块地方到底显示的什么。而这个数据存放在哪里呢?
我是比较喜欢打游戏的。如果你也爱玩一些吃配置的游戏,那你一定对显卡这个东西比较了解,比如我们通常喜欢讨论的你这显卡打什么游戏能跑多少帧,FPS是多少。我们在游戏中看到的每一帧图像,其实在被显示出来前,都会被暂存在一块叫做**帧缓冲(Frame Buffer)的区域中。这些数据一般是保存在显卡的显存(VRAM)**中。
但是很显然,这部分数据我们程序员平时是接触不到的。我们写代码通常也不会直接和显卡打交道,而是通过操作系统来完成。至于操作系统是怎么知道屏幕上每个像素的颜色信息?放心,有一种东西叫做驱动程序,你的所有硬件不是都有驱动程序嘛,不然操作系统怎么知道如何使用。对于个人来说只管调用 API 就行了。如果你想深入了解可以再去搜索驱动相关的知识。这里就不跑题了。
我们现在只需要知道OK了,我肯定是可以向系统要求我要获得像素值的信息的了。以QQ截图为例,整个流程大致可以分为三步:
- 全屏截图
- 通过系统 API(如 Windows 下的
BitBlt
)截取整个屏幕并暂存在内存中。 - 这一步对应的视觉就是你按下了快捷键,然后屏幕变灰并且保持不变化。实际上你可以认为操作系统把你截图时刻的整个屏幕的像素信息缓存下来加上遮罩显示出来。这里如果你不框选,qq截图默认你选中了整个屏幕。
- 监听鼠标并获取坐标
- 鼠标移动时监听当前位置(全局坐标);
- 与截图图像中的像素坐标一一对应。
- 移动鼠标,会有一个放大镜,还提示你当前坐标。
3.读取指定像素颜色
- 根据坐标,在截图图像中提取当前点的 RGB 值;
- 鼠标移动的时候不断获取当前坐标的像素值
windows系统 API 的关键角色
BitBlt
:抓屏:learn.microsoft.com/zh-cn/windo...GetDC
:获取桌面设备上下文:learn.microsoft.com/zh-cn/windo...GetPixel
:读取某点颜色:learn.microsoft.com/zh-cn/windo...
不过非常遗憾的是,这些都是操作系统接口,浏览器环境里是用不了的!
那如何通过前端技术来实现呢?想解决这个问题,得先知道浏览器都有哪些渲染图像的方式,分别做处理。我所了解到的有这么几个,DOM渲染、canvas渲染、webgl渲染。我们来分别聊聊。
1. DOM渲染
MDN 是这么描述的:
浏览器在接收到 HTML 和 CSS 后,会分别构建 DOM(文档对象模型)树和 CSSOM(样式规则)树。它们组合生成渲染树,浏览器再根据渲染树进行布局、绘制,最终通过光栅化呈现在屏幕上。
你可以理解成:HTML 是页面的骨架、CSS 是它的衣服,浏览器则是造型师 + 摄影师,负责把这些"穿戴整齐"的元素排版好,并"拍成一张图"。
但问题来了:
这些操作对开发者是不可见的。你只能操作结构(DOM 节点)你知道的有一套操作DOM的API,却无法拿到浏览器"画出来"的那张最终成图------像素数据根本不会暴露。
简单说:你看到的网页内容,是浏览器自己内部通过显卡绘制的,就像 Photoshop 最终输出了一张图,但不给你 PSD 源文件,也不让你动它的像素。
所以DOM 渲染是没法直接截图或取色的。不过别急,后面有办法"曲线救国"。卖个关子
2. canvas渲染
canvasAPI的出现为浏览器提供了更自由的绘图模式,你想画什么就画什么。和画笔一样。因为由你来决定哪个位置填充哪个像素值。甚至直接提供了我们想要的API,获取像素值、输出图片等
js
ctx.getImageData(x, y, 1, 1); // 获取某像素的 RGBA 值
canvas.toDataURL(); // 导出为图片
聪明的你肯定想到了,太好了,那我们是不是可以把DOM解析通过canvas渲染的方式做出来,不接可以截图、取色了嘛?没错,html2canvas这个库的作者也是这么想的。于是前面卖的关子这里就透露出来了。我们通过这个库可以对html、css重新解析然后通过canvas渲染出来。这样就可以暴露像素数据了。
3. webgl渲染
近些年搞3D是一个挺火的方向,比如著名库ThreeJS,Babylon等,他们都依赖了webgl这个技术。webgl本质是通过着色器程序告诉显卡如何绘制图形,因为三维领域相比二维,数据几何式增长,因此为了保障体验流畅,必须请显卡出山。
还记得之前说我们不能直接跟显卡交互吗,但是webgl可以啊。webgl提供的API可以直接读取帧缓冲区的信息,然后你就可以随便取色了,至于截图那更是不用太容易,因为你已经知道了,截图就是把截图那一刻屏幕的像素信息全部拿到然后缓存下来。
js
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);//获取对应坐标的像素信息
这里贴一个GPT生成的demo,通过html2canvas库来解析对DOM进行截图操作。不过我发现有bug,但是无所谓了。毕竟能说明是什么意思就可以了。
js
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>网页版截图 + 取色器 Demo</title>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 0;
}
.toolbar {
padding: 10px;
background: #333;
color: white;
}
#screenshotCanvas {
display: none;
position: absolute;
top: 0;
left: 0;
cursor: crosshair;
z-index: 10;
}
#colorInfo {
position: fixed;
bottom: 10px;
left: 10px;
background: white;
border: 1px solid #ccc;
padding: 8px;
font-size: 14px;
z-index: 20;
display: none;
}
#magnifier {
position: absolute;
width: 100px;
height: 100px;
border: 2px solid #aaa;
border-radius: 50%;
overflow: hidden;
z-index: 30;
display: none;
pointer-events: none;
}
</style>
</head>
<body>
<div class="toolbar">
<button onclick="takeScreenshot()">📸 开始截图 + 取色</button>
</div>
<div style="padding: 30px">
<h1>网页截图 + 取色器 Demo</h1>
<p>点击上方按钮开始截图,鼠标移动查看颜色。点击取色后复制 HEX。</p>
<img src="https://picsum.photos/600/400" alt="测试图片" />
</div>
<canvas id="screenshotCanvas"></canvas>
<div id="colorInfo"></div>
<canvas id="magnifier"></canvas>
<script>
let canvas, ctx, imageData;
const colorInfo = document.getElementById('colorInfo');
const magnifier = document.getElementById('magnifier');
function takeScreenshot() {
html2canvas(document.body).then(canvasResult => {
canvas = document.getElementById('screenshotCanvas');
ctx = canvas.getContext('2d');
canvas.width = canvasResult.width;
canvas.height = canvasResult.height;
ctx.drawImage(canvasResult, 0, 0);
canvas.style.display = 'block';
colorInfo.style.display = 'block';
canvas.addEventListener('mousemove', handleMove);
canvas.addEventListener('click', handleClick);
});
}
function handleMove(e) {
const rect = canvas.getBoundingClientRect();
const x = Math.floor(e.clientX - rect.left);
const y = Math.floor(e.clientY - rect.top);
const pixel = ctx.getImageData(x, y, 1, 1).data;
const r = pixel[0], g = pixel[1], b = pixel[2];
const hex = rgbToHex(r, g, b);
colorInfo.innerHTML = `📍 坐标: (${x}, ${y})<br/>🎨 RGB: (${r}, ${g}, ${b})<br/>💎 HEX: <b>${hex}</b>`;
colorInfo.style.background = hex;
colorInfo.style.color = r + g + b < 380 ? '#fff' : '#000';
// magnifier
const magnifierCtx = magnifier.getContext('2d');
magnifier.width = 100;
magnifier.height = 100;
magnifier.style.left = `${e.pageX + 20}px`;
magnifier.style.top = `${e.pageY + 20}px`;
magnifier.style.display = 'block';
magnifierCtx.imageSmoothingEnabled = false;
magnifierCtx.drawImage(canvas, x - 5, y - 5, 10, 10, 0, 0, 100, 100);
}
function handleClick(e) {
const rect = canvas.getBoundingClientRect();
const x = Math.floor(e.clientX - rect.left);
const y = Math.floor(e.clientY - rect.top);
const pixel = ctx.getImageData(x, y, 1, 1).data;
const hex = rgbToHex(pixel[0], pixel[1], pixel[2]);
navigator.clipboard.writeText(hex);
alert(`已复制颜色值: ${hex}`);
}
// 将远程图片转为 base64,再设置为 img.src
function loadImageAsBase64(url, callback) {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => {
const c = document.createElement('canvas');
c.width = img.width;
c.height = img.height;
c.getContext('2d').drawImage(img, 0, 0);
callback(c.toDataURL('image/png'));
};
img.src = url;
}
function rgbToHex(r, g, b) {
return "#" + [r, g, b].map(x =>
x.toString(16).padStart(2, "0")
).join('');
}
</script>
</body>
</html>
WebGL 如果你感兴趣,也可以尝试使用,它支持直接读取帧缓冲数据,截图和取色都是没问题的。不过需要注意的是,WebGL 学起来门槛稍高,有不少底层图形学概念需要理解。
最后回过头来看,其实你会发现:截图和取色其实是一回事。截图是拿到整张"图",取色是取其中一个点的像素值而已。只要搞清楚像素从哪儿来、怎么读取,问题就迎刃而解了。