颜色选择器的纯JS实现

前言

最近在逛论坛的时候发现了一个新API:EyeDropper,仅需创建一个实例,然后调用open方法,就可以取到你屏幕内所有可以取到的颜色,可惜兼容性不太行,只有Chrome,Edge,Opera支持,MDN文档
知道了这个API后我也有了一个想实现取色器的想法,工作摸鱼期间👻折腾了几天搞了出来,实现步骤大致以下几步

  1. 所需页面创建实例,初始化所需属性
  2. 需开启时调用open方法开启取色器,网页截屏生成canvas,初始化监听事件和浮动元素(放大镜)
  3. 鼠标移动时根据坐标获取颜色数据修改放大镜颜色
  4. 鼠标点击或者按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步

  1. 初始化canvas容器
  2. 生成canvas,我使用的是html2canvas
  3. 初始化监听事件
  4. 创建浮动元素
/**
* 开启取色器
*/
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)
}

总结

有问题欢迎讨论👻

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值