UNPKG

autohue.js

Version:

一个自动提取图片主题色让图片和背景融为一体的工具

213 lines (206 loc) 6.31 kB
/** * @name: autohue.js * @author: Larry Zhu * @version: 1.0.3 * @description: 一个自动提取图片主题色让图片和背景融为一体的工具 * @license: MIT */ //#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 export { colorPicker as default };