feat: 封装获取图片主色调指令
This commit is contained in:
@ -109,4 +109,174 @@ export function downloadByUrl(url: string, filename?: string) {
|
||||
|
||||
export function genRandomId() {
|
||||
return `id_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getImageMainColor(imageUrl: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous'; // 处理跨域图片
|
||||
img.src = imageUrl;
|
||||
|
||||
img.onload = () => {
|
||||
// 创建画布缩小图片尺寸以提高性能
|
||||
const canvas = document.createElement('canvas');
|
||||
const maxDimension = 100; // 最大尺寸为100px
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
// 按比例缩小图片
|
||||
if (width > height) {
|
||||
if (width > maxDimension) {
|
||||
height *= maxDimension / width;
|
||||
width = maxDimension;
|
||||
}
|
||||
} else {
|
||||
if (height > maxDimension) {
|
||||
width *= maxDimension / height;
|
||||
height = maxDimension;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Canvas context not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 绘制缩小后的图片
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// 获取图片数据
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const data = imageData.data;
|
||||
|
||||
// 使用中位数切分法提取主色调
|
||||
const colorGroups = medianCut(data, 8); // 分成8组
|
||||
|
||||
// 找出最大的颜色组
|
||||
let maxGroup = colorGroups[0];
|
||||
for (let i = 1; i < colorGroups.length; i++) {
|
||||
if (colorGroups[i].count > maxGroup.count) {
|
||||
maxGroup = colorGroups[i];
|
||||
}
|
||||
}
|
||||
|
||||
// 计算组内平均颜色
|
||||
const avgColor = {
|
||||
r: Math.round(maxGroup.sumR / maxGroup.count),
|
||||
g: Math.round(maxGroup.sumG / maxGroup.count),
|
||||
b: Math.round(maxGroup.sumB / maxGroup.count)
|
||||
};
|
||||
|
||||
resolve(`rgb(${avgColor.r},${avgColor.g},${avgColor.b})`);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load image'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 中位数切分法进行色彩量化
|
||||
* @param data 图像数据
|
||||
* @param levels 切分级别
|
||||
* @returns 颜色组
|
||||
*/
|
||||
function medianCut(data: Uint8ClampedArray, levels: number): any[] {
|
||||
const colors = [];
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const a = data[i + 3];
|
||||
|
||||
// 跳过透明像素
|
||||
if (a < 128) continue;
|
||||
|
||||
colors.push({
|
||||
r, g, b,
|
||||
count: 1,
|
||||
sumR: r, sumG: g, sumB: b
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有颜色数据,返回默认白色
|
||||
if (colors.length === 0) {
|
||||
return [{
|
||||
count: 1,
|
||||
sumR: 255, sumG: 255, sumB: 255
|
||||
}];
|
||||
}
|
||||
|
||||
// 开始中位数切分
|
||||
let colorGroups = [{
|
||||
colors,
|
||||
count: colors.length,
|
||||
sumR: colors.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: colors.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: colors.reduce((sum, c) => sum + c.b, 0)
|
||||
}];
|
||||
|
||||
for (let i = 0; i < levels; i++) {
|
||||
const newGroups = [];
|
||||
|
||||
for (const group of colorGroups) {
|
||||
if (group.colors.length <= 1) {
|
||||
newGroups.push(group);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 找出颜色范围最大的通道
|
||||
const rMin = Math.min(...group.colors.map(c => c.r));
|
||||
const rMax = Math.max(...group.colors.map(c => c.r));
|
||||
const gMin = Math.min(...group.colors.map(c => c.g));
|
||||
const gMax = Math.max(...group.colors.map(c => c.g));
|
||||
const bMin = Math.min(...group.colors.map(c => c.b));
|
||||
const bMax = Math.max(...group.colors.map(c => c.b));
|
||||
|
||||
const rRange = rMax - rMin;
|
||||
const gRange = gMax - gMin;
|
||||
const bRange = bMax - bMin;
|
||||
|
||||
let sortChannel = 'r';
|
||||
if (gRange > rRange && gRange > bRange) {
|
||||
sortChannel = 'g';
|
||||
} else if (bRange > rRange && bRange > gRange) {
|
||||
sortChannel = 'b';
|
||||
}
|
||||
|
||||
// 按最大范围通道排序
|
||||
group.colors.sort((a, b) => a[sortChannel] - b[sortChannel]);
|
||||
|
||||
// 切分中位数
|
||||
const mid = Math.floor(group.colors.length / 2);
|
||||
const group1 = group.colors.slice(0, mid);
|
||||
const group2 = group.colors.slice(mid);
|
||||
|
||||
// 添加新组
|
||||
newGroups.push({
|
||||
colors: group1,
|
||||
count: group1.length,
|
||||
sumR: group1.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: group1.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: group1.reduce((sum, c) => sum + c.b, 0)
|
||||
});
|
||||
|
||||
newGroups.push({
|
||||
colors: group2,
|
||||
count: group2.length,
|
||||
sumR: group2.reduce((sum, c) => sum + c.r, 0),
|
||||
sumG: group2.reduce((sum, c) => sum + c.g, 0),
|
||||
sumB: group2.reduce((sum, c) => sum + c.b, 0)
|
||||
});
|
||||
}
|
||||
|
||||
colorGroups = newGroups;
|
||||
}
|
||||
|
||||
return colorGroups;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user