主要通过canvas上下文对象中drawImage方法,去复刻绘制一份图片(注意该canvas不做展示,最好使用display:none隐藏),然后利用getImageData方法获取图片数据,对获取到的图片数据进行一系列判断操作,得出图片的主色调,然后再进行灰度值计算,根据灰度值判断图片的明暗从而设置出合理的字体颜色。
语法:
drawImage(img, x, y)
drawImage(img, x, y, Width, Height)
drawImage(img, sx, sy, sWidth, sHeight, x, y, Width, Height)
值得注意的地方是:
参数 img,绘制到上下文的元素。允许任何的画布图像源,例如:HTMLImageElement、SVGImageElement(en-US)、HTMLVideoElement、HTMLCanvasElement、ImageBitmap、OffscreenCanvas 或 VideoFrame(en-US)。
常用的两种绘制手段:
1)HTMLImageElement , 直接使用dom流中的img元素。
const img = document.getElementById("flower-img"); //获取img元素
const canvas = document.getElementById("canvas-img"); //获取画布元素
const ctx = canvas.getContext("2d"); //获取画布元素上下文
ctx.drawImage(img, 200, 200, 10, 10); //(图片,宽,高,坐标x,坐标y)
这里的注意点:
在操作drawImage()函数时,经常会出现调取正常,但canvas绘制出现空白的情况:
这种情况,原因可以归为:
● 浏览器在加载图片时,图片尚未加载完毕,便开始绘图
● 主要原因为:drawImage()为异步函数
● drawImage()函数,要等到img标签里指定的图像加载完成后,再开始绘图,否则会出现无图的情况
解决方法
可以声明两种加载方式:
img.onload = function() {drawImage()}
window.onload = function() {drawImage()}
如下:
const img = document.getElementById("flower-img"); //获取img元素
console.log(img);
const canvas = document.getElementById("canvas-img"); //获取画布元素
const ctx = canvas.getContext("2d"); //获取画布元素上下文
img.onload = () => {
ctx.drawImage(img, 50, 50, 100, 100); //(图片源,坐标x,坐标y,宽,高)
}
2) 使用外链图片
const imgSrc =
"https://img.alicdn.com/imgextra/i2/O1CN01PAqb911xdw93IOrhS_!!6000000006467-0-tps-1200-220.jpg";
//创建图片
const img = new Image();
//设置图片支持跨域
img.crossOrigin = "Anonymous";
//异步绘制图片
img.onload = () => {
ctx.drawImage(img, 0, 0, 400, 400);
};
img.src = imgSrc;
为什么要设置img.crossOrigin = “Anonymous”?
mdn上写到,只要用了跨域外链图片,画布则会被污染,无法使用一些设计图片数据的api。
解决方法总结了一下:
步骤1: 首先,必须有一个可以对图片响应正确 Access-Control-Allow-Origin 响应头的服务器。简单的来说就是图床服务器得支持跨域
步骤2:在 HTMLImageElement 上设置 crossOrigin(en-US) 的 crossorigin 属性,这将允许浏览器在下载图像数据时允许跨域访问请求。
两者条件缺一不可,一旦外链服务器不支持跨域。则即使设置了crossOrigin = “Anonymous”,还是会提示跨域的错误。
例如把外链图片换成下面这个链接:
https://interactive-examples.mdn.mozilla.net/media/examples/plumeria.jpg
则就会导致跨域问题:
所以为了程序的健壮性以及避免报错,我们可以用异步Promise来优化加载图片这个功能:
return new Promise((resolve, reject) => {
img.onload = function () {
const width = img.width
const height = img.height
ctx.drawImage(img, 0, 0, width, height)
const { data } = ctx.getImageData(0, 0, width, height)
resolve(data)
}
// 错误处理
const errorHandler = () => reject(new Error('An error occurred attempting to load image'))
img.onerror = errorHandler
img.onabort = errorHandler //图片加载中止
img.src = src
})
getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。
对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:
R - 红色 (0-255)
G - 绿色 (0-255)
B - 蓝色 (0-255)
A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)
color/alpha 以数组形式存在,并存储于 ImageData 对象的 data 属性中。
提示:在操作完成数组中的 color/alpha 信息之后,可以使用 putImageData() 方法将图像数据拷贝回画布上。
例子:
以下代码可获得被返回的 ImageData 对象中第一个像素的 color/alpha 信息:
red=imgData.data[0];
green=imgData.data[1];
blue=imgData.data[2];
alpha=imgData.data[3];
data打印的结果为unit8ClampedArray(8 位无符号整型固定数组) 类型化数组
表示一个由值固定在 0-255 区间的 8 位无符号整型组成的数组;如果你指定一个在 [0,255] 区间外的值,它将被替换为 0 或 255;如果你指定一个非整数,那么它将被设置为最接近它的整数。(数组)内容被初始化为 0。一旦(数组)被创建,你可以使用对象的方法引用数组里的元素,或使用标准的数组索引语法(即使用方括号标记)。
/*
data 图像数据 :unit8ClampedArray
accuracyIndex 精准指数:number,用于性能优化,越低越精准
ignore 忽略的颜色数组: string[],填rgb值,用于性能优化
*/
const getColorCount = (data, accuracyIndex, ignore) => {
// 精准指数整数处理
accuracyIndex = Math.round(accuracyIndex)
//精准指数溢出处理
if (accuracyIndex > Math.round(data.length / 4.0)) {
accuracyIndex = Math.round(data.length / 4.0);
}
// 存储颜色以及数量的map集合
const colorCountMap = new Map();
for (let i = 0; i < data.length; i += 4 * accuracyIndex) {
let alpha = data[i + 3];
// 跳过透明度为0的像素点
if (alpha === 0) continue;
/* subarray 方法可以截取指定的字段返回与指定类型相同的类数组,与数组的slice方法相似
Array.from 可将unit8ClampedArray类数组转数组*/
let rgbArray = Array.from(data.subarray(i, i + 3));
// 过滤数据含undefined的点
if (rgbArray.indexOf(undefined) !== -1) continue;
// 将像素处理成rgba格式
let color =
alpha && alpha !== 255
? `rgba(${[...rgbArray, alpha].join(",")})`
: `rgb(${rgbArray.join(",")})`;
// 过滤ignore数组中指定的颜色
if (ignore.indexOf(color) !== -1) continue;
if (colorCountMap[color]) {
colorCountMap[color].count++;
} else {
colorCountMap[color] = { color, count: 1 };
}
}
//将map集合处理成数组
const countArray = Object.values(colorCountMap);
//利用数组的sort进行排序
return countArray.sort((a, b) => b.count - a.count);
};
补充:
subarray() 返回一个新的、基于相同 ArrayBuffer、元素类型也相同的的 TypedArray。开始的索引将会被包括,而结束的索引将不会被包括。TypedArray 是指 typed array types 的其中之一。
放一张计算出来的结果:
1)关于主色调的取法,可以去出现次数最多的颜色,当然也可以取出现次数前n的颜色进行中和。
// 中和的方法
const getMianColor = (colorArr, n) => {
const mainColorArr = colorArr.slice(0, n);
let r = 0,
g = 0,
b = 0,
a = 0;
mainColorArr.map((item) => {
const rgbStr = item?.color.substring(
item["color"].indexOf("(") + 1,
item["color"].indexOf(")")
);
const rgbArr = rgbStr.split(",");
r += Number(rgbArr[0]);
g += Number(rgbArr[1]);
b += Number(rgbArr[2]);
//当透明度存在时才计算
if (rgbArr.length > 3) {
a += rgbArr[3];
}
});
console.log(r, g, b);
const finalColor =
a === 0
? `rgb(${Math.round((r / n) * 1.0)},${Math.round(
(g / n) * 1.0
)},${Math.round((b / n) * 1.0)})`
: `rgb(${Math.round((r / n) * 1.0)},${Math.round(
(g / n) * 1.0
)},${Math.round((b / n) * 1.0)},${Math.round((a / n) * 1.0)})`;
//返回中和的颜色
return finalColor;
};
我们可以判断,当灰度值大于某个值,比如180时,我们断定这样图片色彩比较亮(偏白色),适合深色字体,反之则使用浅色字体。
如有收获,请点个免费的赞吧,谢谢!~