autohue.js
Version:
一个自动提取图片主题色让图片和背景融为一体的工具
217 lines (208 loc) • 6.34 kB
JavaScript
/**
* @name: autohue.js
* @author: Larry Zhu
* @version: 1.0.3
* @description: 一个自动提取图片主题色让图片和背景融为一体的工具
* @license: MIT
*/var autohue = (function() {
;
//#region src/index.ts
function rgbToLab(r, g, b) {
let R = r / 255, G = g / 255, B = b / 255;
R = R > .04045 ? Math.pow((R + .055) / 1.055, 2.4) : R / 12.92;
G = G > .04045 ? Math.pow((G + .055) / 1.055, 2.4) : G / 12.92;
B = B > .04045 ? Math.pow((B + .055) / 1.055, 2.4) : B / 12.92;
let X = R * .4124 + G * .3576 + B * .1805;
let Y = R * .2126 + G * .7152 + B * .0722;
let Z = R * .0193 + G * .1192 + B * .9505;
X = X / .95047;
Y = Y / 1;
Z = Z / 1.08883;
const f = (t) => t > .008856 ? Math.pow(t, .3333333333333333) : 7.787 * t + .13793103448275862;
const fx = f(X);
const fy = f(Y);
const fz = f(Z);
const L = 116 * fy - 16;
const a = 500 * (fx - fy);
const bVal = 200 * (fy - fz);
return [
L,
a,
bVal
];
}
function labDistance(lab1, lab2) {
const dL = lab1[0] - lab2[0];
const da = lab1[1] - lab2[1];
const db = lab1[2] - lab2[2];
return Math.sqrt(dL * dL + da * da + db * db);
}
function rgbToHex(rgb) {
return "#" + rgb.map((v) => {
const hex = Math.round(v).toString(16);
return hex.length === 1 ? "0" + hex : hex;
}).join("");
}
function loadImage(imageSource) {
return new Promise((resolve, reject) => {
let img;
if (typeof imageSource === "string") {
img = new Image();
img.crossOrigin = "Anonymous";
img.src = imageSource;
} else img = imageSource;
if (img.complete) resolve(img);
else {
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
}
});
}
function getImageDataFromImage(img, maxSize = 100) {
const canvas = document.createElement("canvas");
let width = img.naturalWidth;
let height = img.naturalHeight;
if (width > maxSize || height > maxSize) {
const scale = Math.min(maxSize / width, maxSize / height);
width = Math.floor(width * scale);
height = Math.floor(height * scale);
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("无法获取 Canvas 上下文");
ctx.drawImage(img, 0, 0, width, height);
return ctx.getImageData(0, 0, width, height);
}
/**
* 对满足条件的像素进行聚类
* @param imageData 图片像素数据
* @param condition 判断像素是否属于指定区域的条件函数(参数 x, y)
* @param threshold Lab 距离阈值,低于此值的颜色归为同一簇,建议 8~12
*/
function clusterPixelsByCondition(imageData, condition, threshold = 10) {
const clusters = [];
const data = imageData.data;
const width = imageData.width;
const height = imageData.height;
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
if (!condition(x, y)) continue;
const index = (y * width + x) * 4;
if (data[index + 3] === 0) continue;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const lab = rgbToLab(r, g, b);
let added = false;
for (const cluster of clusters) {
const d = labDistance(lab, cluster.averageLab);
if (d < threshold) {
cluster.count++;
cluster.sumRgb[0] += r;
cluster.sumRgb[1] += g;
cluster.sumRgb[2] += b;
cluster.sumLab[0] += lab[0];
cluster.sumLab[1] += lab[1];
cluster.sumLab[2] += lab[2];
cluster.averageRgb = [
cluster.sumRgb[0] / cluster.count,
cluster.sumRgb[1] / cluster.count,
cluster.sumRgb[2] / cluster.count
];
cluster.averageLab = [
cluster.sumLab[0] / cluster.count,
cluster.sumLab[1] / cluster.count,
cluster.sumLab[2] / cluster.count
];
added = true;
break;
}
}
if (!added) clusters.push({
count: 1,
sumRgb: [
r,
g,
b
],
sumLab: [
lab[0],
lab[1],
lab[2]
],
averageRgb: [
r,
g,
b
],
averageLab: [
lab[0],
lab[1],
lab[2]
]
});
}
return clusters;
}
function __handleAutoHueOptions(options) {
if (!options) options = {};
const { maxSize = 100 } = options;
let threshold = options.threshold || 10;
if (typeof threshold === "number") threshold = {
primary: threshold,
left: threshold,
right: threshold,
top: threshold,
bottom: threshold
};
else threshold = {
primary: threshold.primary || 10,
left: threshold.left || 10,
right: threshold.right || 10,
top: threshold.top || 10,
bottom: threshold.bottom || 10
};
return {
maxSize,
threshold
};
}
async function colorPicker(imageSource, options) {
const { maxSize, threshold } = __handleAutoHueOptions(options);
const img = await loadImage(imageSource);
const imageData = getImageDataFromImage(img, maxSize);
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary);
clusters.sort((a, b) => b.count - a.count);
const primaryCluster = clusters[0];
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0];
const primaryColor = rgbToHex(primaryCluster.averageRgb);
const secondaryColor = rgbToHex(secondaryCluster.averageRgb);
const margin = 10;
const width = imageData.width;
const height = imageData.height;
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top);
topClusters.sort((a, b) => b.count - a.count);
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor;
const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom);
bottomClusters.sort((a, b) => b.count - a.count);
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor;
const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left);
leftClusters.sort((a, b) => b.count - a.count);
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor;
const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right);
rightClusters.sort((a, b) => b.count - a.count);
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor;
return {
primaryColor,
secondaryColor,
backgroundColor: {
top: topColor,
right: rightColor,
bottom: bottomColor,
left: leftColor
}
};
}
//#endregion
return colorPicker;
})();