前言
最近在逛论坛的时候发现了一个新API:EyeDropper
,仅需创建一个实例,然后调用open方法,就可以取到你屏幕内所有可以取到的颜色,可惜兼容性不太行,只有Chrome,Edge,Opera支持,MDN文档
知道了这个API后我也有了一个想实现取色器的想法,工作摸鱼期间👻折腾了几天搞了出来,实现步骤大致以下几步
- 所需页面创建实例,初始化所需属性
- 需开启时调用open方法开启取色器,网页截屏生成canvas,初始化监听事件和浮动元素(放大镜)
- 鼠标移动时根据坐标获取颜色数据修改放大镜颜色
- 鼠标点击或者按Esc键后销毁
预览
预览地址:https://songlh.top/page-color-picker/
github:https://github.com/LHRUN/page-color-picker
初始化方法
初始化方法没什么说的,就是把需要的属性和方法赋值一遍初始值,然后接收一个鼠标点击时的回调
export class ColorPicker {
canvasContainer: HTMLDivElement | null = null // canvas容器元素
canvas: HTMLCanvasElement | null = null // 截屏canvas
context: CanvasRenderingContext2D | null = null // 截屏canvas[context]
floatContainer: HTMLDivElement | null = null // 鼠标移动时的浮动容器元素
onChange?: (color: string) => void // 点击鼠标后的回调
color = '' // 颜色值
elementId = '' // 元素唯一id
colorArr: {
el: HTMLDivElement
row: number
col: number
}[] = [] // 放大镜颜色数组
constructor(
onChange?: (color: string) => void // 点击后回调
) {
this.onChange = onChange
}
// ...
}
开启取色器
开启取色器分为4步
- 初始化canvas容器
- 生成canvas,我使用的是html2canvas
- 初始化监听事件
- 创建浮动元素
/**
* 开启取色器
*/
open() {
// 获取随机id
this.elementId = getId()
// 初始化canvas容器
this.initContainer()
html2canvas(document.body).then((canvas) => {
if (canvas && this.canvasContainer) {
// 初始化事件
this.initEvent(canvas)
this.canvasContainer.style.display = 'block'
this.canvasContainer.appendChild(canvas)
this.canvas = canvas
this.context = canvas.getContext('2d')
// 创建浮动元素
this.initFloatContainer()
}
})
}
- 初始化canvas容器
initContainer() {
// 创建元素我封装了一个方法
const canvasContainer = createDocument(
'div',
styleObj.canvasContainer,
document.body
)
this.canvasContainer = canvasContainer
return canvasContainer
}
/**
* 创建元素
* @param elType 元素类型
* @param styleObj 样式对象
* @param parent 父级元素
* @returns element
*/
export const createDocument = <T extends keyof HTMLElementTagNameMap>(
elType: T,
styleObj: Record<string, string | number>,
parent: HTMLElement | DocumentFragment
): HTMLElementTagNameMap[T] => {
const el = document.createElement(elType)
Object.keys(styleObj).forEach((key) => {
if (isValidKey(key, styleObj)) {
Reflect.set(el.style, key, styleObj[key])
}
})
parent.appendChild(el)
return el
}
- 初始化事件
/**
* 初始化事件
* @param canvas
*/
initEvent(canvas: HTMLCanvasElement) {
canvas.addEventListener('mousemove', this.canvasMouseMove)
canvas.addEventListener('mousedown', this.canvasMouseDown)
window.addEventListener('keydown', this.onKeyDown)
}
- 创建浮动元素容器
initFloatCOntainer() {
if (this.canvasContainer) {
// 创建浮动元素容器
const floatContainer = createDocument(
'div',
styleObj.floatContainer,
this.canvasContainer
)
// 创建放大镜的小颜色块
const fragment = document.createDocumentFragment()
for (let i = 1; i <= COLOR_ITEM_SIZE * COLOR_ITEM_SIZE; i++) {
const row = Math.ceil(i / COLOR_ITEM_SIZE)
const col = i - (row - 1) * COLOR_ITEM_SIZE
const style: Record<string, string | number> = {
...styleObj.colorItem
}
if (row === 6 && col === 6) {
style.borderColor = '#000000'
}
const itemEl = createDocument('div', style, fragment)
itemEl.setAttribute('id', `${this.elementId}${i}`)
this.colorArr.push({
el: itemEl,
row,
col
})
}
floatContainer.appendChild(fragment)
const textEl = createDocument('div', styleObj.text, floatContainer)
textEl.setAttribute('id', `${this.elementId}text`)
this.floatContainer = floatContainer
}
}
鼠标移动
- 根据鼠标移动时的坐标,计算需要处理的颜色区域,然后调用CanvasRenderingContext2D.getImageData()方法,这个方法会返回一个ImageData对象,这个对象里就包含RGBA数据,然后把这些数据展示到放大镜元素上,就有了放大的效果
canvasMouseMove = (e: MouseEvent) => {
if (this.context) {
const x = e.pageX * window.devicePixelRatio
const y = e.pageY * window.devicePixelRatio
// 获取放大镜所需区域颜色
const colors = this.getColors(x, y)
if (this.floatContainer && colors) {
// 根据坐标改变放大镜位置
this.floatContainer.style.transform = `translate(${e.pageX - 82.5}px, ${
e.pageY - 82.5
}px )`
if (this.floatContainer.style.visibility === 'hidden') {
this.floatContainer.style.visibility = 'visible'
}
const textEl = document.getElementById(`${this.elementId}text`)
// 遍历每个颜色块,修改颜色
for (
let i = 0;
i < COLOR_ITEM_SIZE * COLOR_ITEM_SIZE;
i++
) {
const { el, row, col } = this.colorArr[i]
const [r, g, b, a] = colors[i]
// toHexString rgba转16进制
const hexStr = toHexString({ r, g, b, a: a / 255 })
// 最中间的颜色保存起来
if (row === 6 && col === 6 && textEl) {
textEl.textContent = hexStr
textEl.style.color = hexStr
this.color = hexStr
}
el.style.backgroundColor = hexStr
}
}
}
}
/**
* 获取放大镜所需区域颜色
* @param x
* @param y
* @returns
*/
getColors(x: number, y: number) {
if (this.context) {
const { data } = this.context.getImageData(
x - 5,
y - 5,
COLOR_ITEM_SIZE,
COLOR_ITEM_SIZE
)
const colors = []
for (let i = 0; i < data.length; i += 4) {
colors.push([data[i], data[i + 1], data[i + 2], data[i + 3]])
}
return colors
}
}
鼠标点击
- 鼠标点击触发回调,销毁元素
canvasMouseDown = () => {
this?.onChange?.(this.color)
this.destroy()
}
destroy() {
if (this.canvas) {
this.canvas.removeEventListener('mousemove', this.canvasMouseMove)
this.canvas.removeEventListener('mousedown', this.canvasMouseDown)
}
if (this.canvasContainer) {
document.body.removeChild(this.canvasContainer)
}
window.removeEventListener('keydown', this.onKeyDown)
}
总结
有问题欢迎讨论👻