本片文章主要涉及两个场景:
- 根据图片的平均色值获取文字的最佳显示颜色
- 提取图片的主题色
一、根据图片的平均色值获取文字的最佳显示颜色
登录页面由登录框,铺满背景的图片,位于背景图片上方的文字构成,背景图片可配置,如下图。如果这时候文字的颜色固定为白色,图片配置为白雪背景的图片,那么就会出现版权信息与白色背景太过相似,而显示不清晰的情况。思路是通过计算图片的平均色值,基于此判断文本应该为黑色或白色能够拥有更高的对比度。具体实现过程如下:
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 容器中,获取其像素数组
这里因为我获取的是局部图片而非整张图片的像素数组,所以针对截取哪部分,做了一下处理。即可以通过传入相应的比例系数截取指定部分,其实就是相当于对 canvas的 drawImage 方法的参数做了一下处理。如果截取整张图片就要简单的多,可以看一下该方法的文档
这里的参数含义如下图:
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(包含)的整数表示。 就像这样:
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'
}
像这样,总能找到合适的显示颜色:
二、提取图片的主题色
上传一张主题图片,提取该图片中的主题色。一开始我是打算直接使用 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
}
我们需要把获取到像素数组做一些处理,因为我们只是提取主题色,所以我们不需要过于透明的颜色。即 rgba 中 a 值不存在或小于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
}
效果如下:
结尾
两个场景就介绍完了,一开始本来只是讲显示文字颜色这个功能的,但是后面发现这两个场景有很大的共同点,所以就把之前的方法整理了一下,重新进行了一次封装,上述讲到的所有功能方法我已经封装完毕并发布到 npm 并上传至 github,感兴趣的朋友可以看代码,仓库地址:github.com/chanceyliu/…