react获取图片色值

2,221 阅读4分钟

本片文章主要涉及两个场景:

  1. 根据图片的平均色值获取文字的最佳显示颜色
  2. 提取图片的主题色

一、根据图片的平均色值获取文字的最佳显示颜色

登录页面由登录框,铺满背景的图片,位于背景图片上方的文字构成,背景图片可配置,如下图。如果这时候文字的颜色固定为白色,图片配置为白雪背景的图片,那么就会出现版权信息与白色背景太过相似,而显示不清晰的情况。思路是通过计算图片的平均色值,基于此判断文本应该为黑色或白色能够拥有更高的对比度。具体实现过程如下:

image-20210925165445682

1. 创建图片标签
export const createImage = (url: string): Promise<HTMLImageElement> => {
  return new Promise((resolve, reject) => {
    const image = new Image()
    image.addEventListener('load', () => resolve(image))
    image.addEventListener('error', (error) => reject(error))
    // 图片添加跨域
    image.setAttribute('crossOrigin', 'anonymous')
    image.src = url
  })
}
2. 将图片绘制到 canvas 容器中,获取其像素数组

这里因为我获取的是局部图片而非整张图片的像素数组,所以针对截取哪部分,做了一下处理。即可以通过传入相应的比例系数截取指定部分,其实就是相当于对 canvasdrawImage 方法的参数做了一下处理。如果截取整张图片就要简单的多,可以看一下该方法的文档

这里的参数含义如下图:

export const getImgData = async (
  params: GetImgDataParams
): Promise<GetImgDataRes> => {
  const {
    imgSrc,
    xMultiple = 0,
    yMultiple = 0,
    wMultiple = 1,
    hMultiple = 1
  } = params
​
  const multipleArr = [xMultiple, yMultiple, wMultiple, hMultiple]
  const isVerify = multipleArr.every((item) => item >= 0 && item <= 1)
  if (!isVerify) {
    throw new Error('请输入合法的比例系数,即大于0小于1的数字')
  }
​
  const myCanvas = document.createElement('canvas')
  const bgImg = await createImage(imgSrc)
​
  const iHeight = bgImg.height
  const iWidth = bgImg.width
  const canvasWidth = iWidth * wMultiple
  const canvasHeight = iHeight * hMultiple
  const canvasSize = canvasWidth * canvasHeight
​
  myCanvas.width = iWidth * wMultiple
  myCanvas.height = iHeight * hMultiple
​
  const ctx = myCanvas.getContext('2d')
  if (!ctx) {
    throw new Error('Canvas创建失败')
  }
  ctx.drawImage(
    bgImg,
    iWidth * xMultiple,
    iHeight * yMultiple,
    canvasWidth,
    canvasHeight,
    0,
    0,
    canvasWidth,
    canvasHeight
  )
​
  // 获取canvas中图像的像素数据
  const data = ctx.getImageData(0, 0, canvasWidth, canvasHeight).data
  return { data: data, canvasSize: canvasSize }
}
3. 根据像素数组计算出平均像素
export const getAverageColor = (params: GetImgDataRes): number[] => {
  const { data, canvasSize } = params
  let r = 0
  let g = 0
  let b = 0
​
  for (let i = 0, offset; i < canvasSize; i++) {
    offset = i * 4
    r = data[offset + 0]
    g = data[offset + 1]
    b = data[offset + 2]
  }
  // 求取平均值
  r += Math.round(r / canvasSize)
  g += Math.round(g / canvasSize)
  b += Math.round(b / canvasSize)
​
  return [r, g, b]
}

这里解释一下这个为什么要这样写,看下面这张图你就明白了。我们通过 getImageData 得到的像素数组是一个一维数组,包含以 RGBA 顺序的数据,数据使用 0 至 255(包含)的整数表示。 就像这样: preview

4. 使用平均像素计算 YIQ 值,通过该值判断文本显示颜色。

YIQ值具体指什么感兴趣的可以查一下,我这里直接解释为色系,就是通过这个值判断色彩是偏黑色系还是白色系。从而判断文本应该是黑色还是白色,基于哪个会具有更高的对比度,以此来提供最佳的可读性。

export const getContrastYIQ = (r: number, g: number, b: number) => {
  const yiq = (r * 299 + g * 587 + b * 114) / 1000
  return yiq >= 128 ? 'black' : 'white'
}

像这样,总能找到合适的显示颜色:

localhost_3001_theme-color


二、提取图片的主题色

上传一张主题图片,提取该图片中的主题色。一开始我是打算直接使用 color-thief 的,参照官方文档中的使用 ES6 方式引入行不通,我就去看了一下源代码,一通研究之下发现其实挺简单,最核心的代码是引入 quantize 包处理颜色数组的一段,下面我会讲到,于是我就基于他的源码做了一下处理。以下是实现过程:

1. 绘制图片到canvas中,提取颜色数组

提取主题色的过程前半段与我们上面的场景一致,我们都是需要先获取到图片的像素数组,代码可以参考上面,这里不再赘述。

2. 整理有效像素数组

imgData 就是我们要提取图片的像素数组;pixelCount 是像素点的数量,也就是图片的尺寸;quality 是精度,因为很多时候其实我们没必要挨着去将每个像素点取出来,从下面的方法中我们能看出,该值越大我们就会跳过更多的像素点,即获取到的色值就会越不准确,但是同时处理速度也会有所上升,所以需要做权衡

export const createPixelArray = ({
  imgData,
  pixelCount,
  quality
}: CreatePixelArrayParams) => {
  const pixels = imgData
  const pixelArray = []
​
  // 以适合量化函数的数组格式存储 RGB 值
  for (let i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
    offset = i * 4
    r = pixels[offset + 0]
    g = pixels[offset + 1]
    b = pixels[offset + 2]
    a = pixels[offset + 3]
​
    // 像素要是不透明的和半透明以上的
    if (typeof a === 'undefined' || a >= 125) {
      // 像素不能是太贴近白色的
      if (!(r > 250 && g > 250 && b > 250)) {
        pixelArray.push([r, g, b])
      }
    }
  }
​
  return pixelArray
}

我们需要把获取到像素数组做一些处理,因为我们只是提取主题色,所以我们不需要过于透明的颜色。即 rgbaa 值不存在或小于125的。rgb 三个值同时大于250的我们也不需要,因为我们认为他过于贴近白色,去除掉这部分不会影响提取效果,还可以提高处理效率。

3.量化颜色数组,并返回调色板

有关颜色提取的算法主要有:最小差值法中位切分法八叉树算法 等。这里使用了 quantize 包来处理颜色,这个包使用的是中位切分法

export const getPalette = async ({
  imgSrc,
  colorCount = 10,
  quality = 10
}: GetPaletteParams) => {
  if (
    typeof colorCount === 'undefined' ||
    !Number.isInteger(colorCount) ||
    colorCount < 2 ||
    colorCount > 20
  ) {
    colorCount = 10
  }
​
  if (
    typeof quality === 'undefined' ||
    !Number.isInteger(quality) ||
    quality < 1
  ) {
    quality = 10
  }
​
  const { data: imgData, canvasSize: pixelCount } = await getImgData({
    imgSrc
  })
  const pixelArray = createPixelArray({
    imgData,
    pixelCount,
    quality: quality
  })
​
  // quantize将像素数组进行量化,聚类,最终返回面板数组
  // 使用中位切分法
  const cmap = quantize(pixelArray, colorCount)
  const palette = cmap ? cmap.palette() : null
​
  return palette
}

效果如下:localhost_3000_theme-color (3)

结尾

两个场景就介绍完了,一开始本来只是讲显示文字颜色这个功能的,但是后面发现这两个场景有很大的共同点,所以就把之前的方法整理了一下,重新进行了一次封装,上述讲到的所有功能方法我已经封装完毕并发布到 npm 并上传至 github,感兴趣的朋友可以看代码,仓库地址:github.com/chanceyliu/…