网页拾色器(颜色吸管工具)可以吸取网页上每个像素的颜色值。
一、介绍和使用
图例
使用
import ColorPipette from './color-pipette';
// 初始化
const pipette = new ColorPipette({
container: document.body,
scale: 2,
listener: {
onOk({ color, colors }) => {
console.log(color, colors);
},
}
});
// 开始取色
pipette.start();
参数说明
参数名 | 含义 | 默认值 | 备注 |
---|---|---|---|
container | 想截图的页面dom元素 | document.body | - |
scale | 显示像素比例 | 1 | 绘制截图时的比例,范围为1-4,数字越大越清晰但是速度越慢 |
listener | 事件监听 | - | onOk: ({color: string; colors: string[][]}) => void |
listener.onOk | 监听完成 | - | onOk: ({color: string; colors: string[][]}) => void |
listener.onChange | 监听变化 | - | onChange: ({color: string; colors: string[][]}) => void |
二、实现方法
实现拾色器主要分为一下几个步骤
- 网页截图;
- 绘制截图到canvas,并获取指定区域像素点颜色值;
- 获取鼠标定位,显示放大镜和颜色值;
1. 网页截图
想要实现获取到页面的像素点颜色,首先要对整个网页进行截图,为了兼容性考虑我们使用dom-to-image
来实现截图功能。源码在此:dom-to-image
使用过程中发现dom-to-image
截图很模糊,因此复制了源码(只有几百行),支持设置分辨率。
- 使用截图
import domtoimage from './dom-to-image';
async getPagePng() {
const base64 = await domtoimage.toPng(this.targetDom, { scale: 2 });
return base64;
}
- 只修改了绘制函数这里,增加了
scale
的设置
function draw(domNode, options) {
const { scale = 1 } = options;
return toSvg(domNode, options)
.then(util.makeImage)
.then(util.delay(100))
.then(function (image) {
const { canvas, ctx } = newCanvas(domNode, options);
console.log(canvas, ctx, canvas.width, canvas.height, scale );
ctx.drawImage(image, 0, 0, canvas.width / scale, canvas.height / scale);
return canvas;
});
function newCanvas(domNode, options) {
const canvas = document.createElement('canvas');
const { scale = 1 } = options;
canvas.width = (options.width || util.width(domNode)) * scale;
canvas.height = (options.height || util.height(domNode)) * scale;
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
if (options.bgcolor) {
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return { canvas, ctx };
}
}
dom-to-image.js
完整代码如下(将draw函数替换成上面即可):
2. 绘制&获取像素点颜色
- 截图获取base64,然后绘制到canvas上
/**
* 加载base64图片
* @param base64
* @returns
*/
export const loadImage = (base64: string) => {
return new Promise((resolve) => {
const img = new Image();
img.src = base64;
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
});
};
/**
* 将dom节点画到canvas里
*/
async drawCanvas() {
const base64 = await domtoimage.toPng(this.container, { scale: this.scale }).catch(() => '');
if (!base64) {
return;
}
const img = await loadImage(base64);
if (!img) {
return;
}
this.ctx.drawImage(img, 0, 0, this.rect.width, this.rect.height);
}
- 获取画布上一点的颜色值
/**
* rbga对象转化为16进制颜色字符串
* @param rgba
* @returns
*/
export const rbgaObjToHex = (rgba: IRgba) => {
let { r, g, b } = rgba;
const { a } = rgba;
r = Math.floor(r * a);
g = Math.floor(g * a);
b = Math.floor(b * a);
return `#${hex(r)}${hex(g)}${hex(b)}`;
};
/**
* 获取鼠标点的颜色
*/
getPointColor(x: number, y: number) {
const { scale } = this;
const { data } = this.ctx.getImageData(x * scale, y * scale, 1, 1);
const r = data[0];
const g = data[1];
const b = data[2];
const a = data[3] / 255;
const rgba = { r, g, b, a };
return rbgaObjToHex(rgba);
}
- 获取画布上某一矩形区域的颜色值
/**
* 获取将canvas输出的数据转化为二位数组
* @param data
* @param rect
* @param scale
* @returns
*/
const getImageColor = (data: any[], rect: IRect, scale: number = 1) => {
const colors: any[][] = [];
const { width, height } = rect;
for (let row = 0; row < height; row += 1) {
if (!colors[row]) {
colors[row] = [];
}
const startIndex = row * width * 4 * scale * scale;
for (let column = 0; column < width; column += 1) {
const i = startIndex + column * 4 * scale;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3] / 255;
const color = rbgaObjToHex({ r, g, b, a });
colors[row][column] = color;
}
}
return colors;
};
/**
* 获取canvas某一区域的颜色值二位数组
* @param ctx
* @param rect
* @param scale
* @returns
*/
export const getCanvasRectColor = (ctx: any, rect: IRect, scale: number = 1) => {
const { x, y, width, height } = rect;
const image = ctx.getImageData(x * scale, y * scale, width * scale, height * scale);
const { data } = image;
const colors = getImageColor(data, rect, scale);
return colors;
}
3. 绘制放大镜和颜色值
- 监听鼠标移动和点击事件,进行交互操作
- 绘制放大镜
- 绘制颜色值和选中颜色
/**
* 绘制放大镜canvas
* @param colors
* @param size
* @returns
*/
export function drawPipetteCanvas(colors: IColors, size: number) {
const count = colors.length;
const diameter = size * count;
const radius = diameter / 2;
const { canvas, ctx } = getCanvas({
width: diameter,
height: diameter,
scale: 2,
attrs: {
style: `border-radius: 50%;`,
},
});
if (!ctx) {
return;
}
// 画像素点
colors.forEach((row, i) => row.forEach((color, j) => {
ctx.fillStyle = color;
ctx.fillRect(j * size, i * size, size, size);
}));
// 画水平线
for (let i = 0; i < count; i += 1) {
ctx.beginPath();
ctx.strokeStyle = '#eee';
ctx.lineWidth = 0.6;
ctx.moveTo(0, i * size);
ctx.lineTo(diameter, i * size);
ctx.stroke();
}
// 画垂直线
for (let j = 0; j < count; j += 1) {
ctx.beginPath();
ctx.strokeStyle = '#eee';
ctx.lineWidth = 0.6;
ctx.moveTo(j * size, 0);
ctx.lineTo(j * size, diameter);
ctx.stroke();
}
// 画圆形边框
ctx.beginPath();
ctx.strokeStyle = '#ddd';
ctx.arc(radius, radius, radius, 0, 2 * Math.PI);
ctx.stroke();
// 画中心像素点
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.strokeRect(radius - size / 2, radius - size / 2, size, size);
return canvas;
}
/**
* 绘制放大镜dom
* @param colors 颜色二位数组
* @param size 单个像素点显示大小
* @returns
*/
export function drawPipette(colors: IColors, size = 8) {
const scale = 2;
const canvasContainer: any = document.createElement('div');
const canvasContent: any = document.createElement('div');
const pipetteCanvas: any = drawPipetteCanvas(colors, size);
canvasContainer.style = `position: relative;`;
canvasContent.style = `
position: absolute;
top: 0;
left: 0;
width: ${pipetteCanvas.width / scale}px;
height: ${pipetteCanvas.height / scale}px;
border-radius: 50%;
box-shadow: 0 0 10px 10px rgba(150,150,150,0.2) inset;
`;
canvasContainer.appendChild(pipetteCanvas);
canvasContainer.appendChild(canvasContent);
return canvasContainer;
}
/**
* 颜色方块和颜色值显示
* @param color
* @returns
*/
export function drawColorBlock(color: string) {
const colorBlock: any = document.createElement('div');
colorBlock.style = `
display: flex;
align-items: center;
background-color: rgba(0,0,0,0.4);
padding: 2px 4px;
border-radius: 3px;
`;
colorBlock.innerHTML = `
<div style="
width: 20px;
height: 20px;
background-color: ${color};
border-radius: 3px;
border: 1px solid #eee;
"></div>
<div style="
width: 65px;
border-radius: 3px;
color: #fff;
margin-left: 4px;
">${color}</div>
`;
return colorBlock;
}
三、完整源码
取色器类
/**
* 网页颜色吸管工具【拾色器】
* date: 2021.10.31
* author: alanyf
*/
import domtoimage from './dom-to-image.js';
import { drawTooltip, getCanvas, getCanvasRectColor, loadImage, rbgaObjToHex, renderColorInfo } from './helper';
import type { IProps, IRect } from './interface';
export * from './interface';
/**
* 网页拾色器【吸管工具】
*/
class ColorPipette {
container: any = {};
listener: Record<string, (e: any) => void> = {};
rect: IRect = { x: 0, y: 0, width: 0, height: 0 };
canvas: any = {};
ctx: any;
scale = 1;
magnifier: any = null;
colorContainer: any = null;
colors: string[][] = [];
tooltipVisible = true;
useMagnifier = false;
constructor(props: IProps) {
try {
const { container, listener, scale = 1, useMagnifier = false } = props;
this.container = container || document.body;
this.listener = listener || {};
this.rect = this.container.getBoundingClientRect();
this.scale = scale > 4 ? 4 : scale;
this.useMagnifier = useMagnifier;
// 去除noscript标签,可能会导致
const noscript = document.body.querySelector('noscript');
noscript?.parentNode?.removeChild(noscript);
this.initCanvas();
} catch (err) {
console.error(err);
this.destroy();
}
}
/**
* 初始化canvas
*/
initCanvas() {
const { rect, scale } = this;
const { x, y, width, height } = rect;
const { canvas, ctx } = getCanvas({
width: rect.width,
height: rect.height,
scale,
attrs: {
class: 'color-pipette-canvas-container',
style: `
position: fixed;
left: ${x}px;
top: ${y}px;
z-index: 10000;
cursor: pointer;
width: ${width}px;
height: ${height}px;
`,
},
});
this.canvas = canvas;
this.ctx = ctx;
}
/**
* 开始
*/
async start() {
try {
await this.drawCanvas();
document.body.appendChild(this.canvas);
const tooltip = drawTooltip('按Esc可退出');
document.body.appendChild(tooltip);
setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000);
// 添加监听
this.canvas.addEventListener('mousemove', this.handleMove);
this.canvas.addEventListener('mousedown', this.handleDown);
document.addEventListener('keydown', this.handleKeyDown);
} catch (err) {
console.error(err);
this.destroy();
}
}
/**
* 结束销毁dom,清除事件监听
*/
destroy() {
this.canvas.removeEventListener('mousemove', this.handleMove);
this.canvas.removeEventListener('mousedown', this.handleDown);
document.removeEventListener('keydown', this.handleKeyDown);
this.canvas?.parentNode?.removeChild(this.canvas);
this.colorContainer?.parentNode?.removeChild(this.colorContainer);
}
/**
* 将dom节点画到canvas里
*/
async drawCanvas() {
const base64 = await domtoimage.toPng(this.container, { scale: this.scale }).catch(() => '');
if (!base64) {
return;
}
const img = await loadImage(base64);
if (!img) {
return;
}
this.ctx.drawImage(img, 0, 0, this.rect.width, this.rect.height);
}
/**
* 处理鼠标移动
*/
handleMove = (e: any) => {
const { color, colors } = this.getPointColors(e);
const { onChange = () => '' } = this.listener;
const point = { x: e.pageX + 15, y: e.pageY + 15 };
const colorContainer = renderColorInfo({
containerDom: this.colorContainer,
color,
colors,
point,
});
if (!this.colorContainer) {
this.colorContainer = colorContainer;
document.body.appendChild(colorContainer);
}
onChange({ color, colors });
}
/**
* 处理鼠标按下
*/
handleDown = (e: any) => {
const { onOk = () => '' } = this.listener;
const res = this.getPointColors(e);
console.log(JSON.stringify(res.colors, null, 4));
onOk(res);
this.destroy();
}
/**
* 处理键盘按下Esc退出拾色
*/
handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Escape') {
this.destroy();
}
};
/**
* 获取鼠标点周围的颜色整列
*/
getPointColors(e: any) {
const { ctx, rect, scale } = this;
let { pageX: x, pageY: y } = e;
x -= rect.x;
y -= rect.y;
const color = this.getPointColor(x, y);
const size = 19;
const half = Math.floor(size / 2);
const info = { x: x - half, y: y - half, width: size, height: size };
const colors = getCanvasRectColor(ctx, info, scale);
return { color, colors };
}
/**
* 获取鼠标点的颜色
*/
getPointColor(x: number, y: number) {
const { scale } = this;
const { data } = this.ctx.getImageData(x * scale, y * scale, 1, 1);
const r = data[0];
const g = data[1];
const b = data[2];
const a = data[3] / 255;
const rgba = { r, g, b, a };
return rbgaObjToHex(rgba);
}
}
export default ColorPipette;
工具方法
import type { IColors, IRect, IRgba } from './interface';
/**
* 加载base64图片
* @param base64
* @returns
*/
export const loadImage = (base64: string) => {
return new Promise((resolve) => {
const img = new Image();
img.src = base64;
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
});
};
// 十进制转化为16进制
export function hex(n: number){
return `0${n.toString(16)}`.slice(-2);
}
/**
* rbga对象转化为16进制颜色字符串
* @param rgba
* @returns
*/
export const rbgaObjToHex = (rgba: IRgba) => {
let { r, g, b } = rgba;
const { a } = rgba;
r = Math.floor(r * a);
g = Math.floor(g * a);
b = Math.floor(b * a);
return `#${hex(r)}${hex(g)}${hex(b)}`;
};
/**
* rbga对象转化为rgba css颜色字符串
* @param rgba
* @returns
*/
export const rbgaObjToRgba = (rgba: IRgba) => {
const { r, g, b, a } = rgba;
return `rgba(${r},${g},${b},${a})`;
};
/**
* 显示颜色信息,包括放大镜和颜色值
* @param params
* @returns
*/
export const renderColorInfo = (params: any) => {
const { containerDom, color, colors, point } = params;
let container = containerDom;
const pos = point;
const n = 7;
const count = colors[0].length;
const size = count * (n + 0) + 2;
if (!container) {
const magnifier: any = document.createElement('div');
container = magnifier;
}
if (pos.x + size + 25 > window.innerWidth) {
pos.x -= size + 25;
}
if (pos.y + size + 40 > window.innerHeight) {
pos.y -= size + 40;
}
container.style = `
position: fixed;
left: ${pos.x + 5}px;
top: ${pos.y}px;
z-index: 10001;
pointer-events: none;
`;
container.innerHTML = '';
const pipette = drawPipette(colors, n);
const colorBlock = drawColorBlock(color);
const padding: any = document.createElement('div');
padding.style = 'height: 3px;';
container.appendChild(pipette);
container.appendChild(padding);
container.appendChild(colorBlock);
return container;
}
/**
* 绘制放大镜
* @param colors 颜色二位数组
* @param size 单个像素点显示大小
* @returns
*/
export function drawPipette(colors: IColors, size = 8) {
const scale = 2;
const canvasContainer: any = document.createElement('div');
const canvasContent: any = document.createElement('div');
const pipetteCanvas: any = drawPipetteCanvas(colors, size);
canvasContainer.style = `position: relative;`;
canvasContent.style = `
position: absolute;
top: 0;
left: 0;
width: ${pipetteCanvas.width / scale}px;
height: ${pipetteCanvas.height / scale}px;
border-radius: 50%;
box-shadow: 0 0 10px 10px rgba(150,150,150,0.2) inset;
`;
canvasContainer.appendChild(pipetteCanvas);
canvasContainer.appendChild(canvasContent);
return canvasContainer;
}
/**
* 颜色方块和颜色值显示
* @param color
* @returns
*/
export function drawColorBlock(color: string) {
const colorBlock: any = document.createElement('div');
colorBlock.style = `
display: flex;
align-items: center;
background-color: rgba(0,0,0,0.4);
padding: 2px 4px;
border-radius: 3px;
`;
colorBlock.innerHTML = `
<div style="
width: 20px;
height: 20px;
background-color: ${color};
border-radius: 3px;
border: 1px solid #eee;
"></div>
<div style="
width: 65px;
border-radius: 3px;
color: #fff;
margin-left: 4px;
">${color}</div>
`;
return colorBlock;
}
/**
* 显示提示
* @param content
* @param tooltipVisible
* @returns
*/
export function drawTooltip(content: string, tooltipVisible = true) {
const tooltip: any = document.createElement('div');
tooltip.id = 'color-pipette-tooltip-container';
tooltip.innerHTML = content;
tooltip.style = `
position: fixed;
left: 50%;
top: 30%;
z-index: 10002;
display: ${tooltipVisible ? 'flex' : 'none'};
align-items: center;
background-color: rgba(0,0,0,0.4);
padding: 4px 10px;
border-radius: 3px;
color: #fff;
font-size: 20px;
pointer-events: none;
`;
return tooltip;
}
/**
* 绘制放大镜canvas
* @param colors
* @param size
* @returns
*/
export function drawPipetteCanvas(colors: IColors, size: number) {
const count = colors.length;
const diameter = size * count;
const radius = diameter / 2;
const { canvas, ctx } = getCanvas({
width: diameter,
height: diameter,
scale: 2,
attrs: {
style: `border-radius: 50%;`,
},
});
if (!ctx) {
return;
}
// 画像素点
colors.forEach((row, i) => row.forEach((color, j) => {
ctx.fillStyle = color;
ctx.fillRect(j * size, i * size, size, size);
}));
// 画水平线
for (let i = 0; i < count; i += 1) {
ctx.beginPath();
ctx.strokeStyle = '#eee';
ctx.lineWidth = 0.6;
ctx.moveTo(0, i * size);
ctx.lineTo(diameter, i * size);
ctx.stroke();
}
// 画垂直线
for (let j = 0; j < count; j += 1) {
ctx.beginPath();
ctx.strokeStyle = '#eee';
ctx.lineWidth = 0.6;
ctx.moveTo(j * size, 0);
ctx.lineTo(j * size, diameter);
ctx.stroke();
}
// 画圆形边框
ctx.beginPath();
ctx.strokeStyle = '#ddd';
ctx.arc(radius, radius, radius, 0, 2 * Math.PI);
ctx.stroke();
// 画中心像素点
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.strokeRect(radius - size / 2, radius - size / 2, size, size);
return canvas;
}
/**
* 生成canvas
* @param param0
* @returns
*/
export function getCanvas({ width = 0, height = 0, scale = 1, attrs = {} as Record<string, any> }) {
const canvas: any = document.createElement('canvas');
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
canvas.setAttribute(key, value);
});
canvas.setAttribute('width', `${width * scale}`);
canvas.setAttribute('height', `${height * scale}`);
canvas.style = `${attrs.style || ''};width: ${width}px;height: ${height}px;`;
const ctx = canvas.getContext('2d');
ctx?.scale(scale, scale);
return { canvas, ctx };
}
/**
* 获取将canvas输出的数据转化为二位数组
* @param data
* @param rect
* @param scale
* @returns
*/
const getImageColor = (data: any[], rect: IRect, scale: number = 1) => {
const colors: any[][] = [];
const { width, height } = rect;
for (let row = 0; row < height; row += 1) {
if (!colors[row]) {
colors[row] = [];
}
const startIndex = row * width * 4 * scale * scale;
for (let column = 0; column < width; column += 1) {
const i = startIndex + column * 4 * scale;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3] / 255;
const color = rbgaObjToHex({ r, g, b, a });
colors[row][column] = color;
}
}
return colors;
};
/**
* 获取canvas某一区域的颜色值二位数组
* @param ctx
* @param rect
* @param scale
* @returns
*/
export const getCanvasRectColor = (ctx: any, rect: IRect, scale: number = 1) => {
const { x, y, width, height } = rect;
// console.log(x, y, width, height);
const image = ctx.getImageData(x * scale, y * scale, width * scale, height * scale);
const { data } = image;
const colors = getImageColor(data, rect, scale);
return colors;
}
interface接口
export interface Point {
x: number;
y: number;
}
export interface IRect {
x: number;
y: number;
width: number;
height: number;
}
export type IColors = string[][];
export interface IRgba {
r: number;
g: number;
b: number;
a: number;
}
export interface IProps {
container: any;
listener?: Record<string, (e: any) => void>;
scale?: number;
useMagnifier?: boolean;
}