feat: move artwork feature

-move artwork feature
-some bug fixes
-extracted artwork reprocessing
This commit is contained in:
MiraclePatrickLumowa
2025-09-21 15:57:57 +08:00
committed by GitHub
parent 3a2129ebb7
commit 23c3bb9747
8 changed files with 1524 additions and 19 deletions
+576 -2
View File
@@ -26,6 +26,13 @@ class ImageProcessor {
// Color cache for performance optimization
this._colorCache = new Map();
this._labCache = new Map();
this._hsvCache = new Map();
this._hslCache = new Map();
this._xyzCache = new Map();
this._luvCache = new Map();
this._yuvCache = new Map();
this._oklabCache = new Map();
this._lchCache = new Map();
// Configuration constants
this.TRANSPARENCY_THRESHOLD = 128;
@@ -196,6 +203,403 @@ class ImageProcessor {
return v;
}
/**
* Convert RGB to HSV color space
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} HSV values [H (0-360), S (0-1), V (0-1)]
* @private
*/
_rgbToHsv(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = 0;
const s = max === 0 ? 0 : delta / max;
const v = max;
if (delta !== 0) {
if (max === r) {
h = ((g - b) / delta) % 6;
} else if (max === g) {
h = (b - r) / delta + 2;
} else {
h = (r - g) / delta + 4;
}
h *= 60;
if (h < 0) h += 360;
}
return [h, s, v];
}
/**
* Convert HSV to RGB color space
* @param {number} h - Hue value (0-360)
* @param {number} s - Saturation value (0-1)
* @param {number} v - Value/Brightness (0-1)
* @returns {number[]} RGB values [R, G, B] (0-255)
* @private
*/
_hsvToRgb(h, s, v) {
const c = v * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = v - c;
let r = 0, g = 0, b = 0;
if (h >= 0 && h < 60) {
r = c; g = x; b = 0;
} else if (h >= 60 && h < 120) {
r = x; g = c; b = 0;
} else if (h >= 120 && h < 180) {
r = 0; g = c; b = x;
} else if (h >= 180 && h < 240) {
r = 0; g = x; b = c;
} else if (h >= 240 && h < 300) {
r = x; g = 0; b = c;
} else if (h >= 300 && h < 360) {
r = c; g = 0; b = x;
}
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
/**
* Convert RGB to HSL color space
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} HSL values [H (0-360), S (0-1), L (0-1)]
* @private
*/
_rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (delta !== 0) {
s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
if (max === r) {
h = ((g - b) / delta) % 6;
} else if (max === g) {
h = (b - r) / delta + 2;
} else {
h = (r - g) / delta + 4;
}
h *= 60;
if (h < 0) h += 360;
}
return [h, s, l];
}
/**
* Convert HSL to RGB color space
* @param {number} h - Hue value (0-360)
* @param {number} s - Saturation value (0-1)
* @param {number} l - Lightness value (0-1)
* @returns {number[]} RGB values [R, G, B] (0-255)
* @private
*/
_hslToRgb(h, s, l) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h >= 0 && h < 60) {
r = c; g = x; b = 0;
} else if (h >= 60 && h < 120) {
r = x; g = c; b = 0;
} else if (h >= 120 && h < 180) {
r = 0; g = c; b = x;
} else if (h >= 180 && h < 240) {
r = 0; g = x; b = c;
} else if (h >= 240 && h < 300) {
r = x; g = 0; b = c;
} else if (h >= 300 && h < 360) {
r = c; g = 0; b = x;
}
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
/**
* Convert RGB to XYZ color space
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} XYZ values [X, Y, Z]
* @private
*/
_rgbToXyz(r, g, b) {
// Normalize RGB values
let x = r / 255.0;
let y = g / 255.0;
let z = b / 255.0;
// Apply gamma correction
x = x > 0.04045 ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92;
y = y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92;
z = z > 0.04045 ? Math.pow((z + 0.055) / 1.055, 2.4) : z / 12.92;
// Convert to XYZ using sRGB matrix
const X = x * 0.4124564 + y * 0.3575761 + z * 0.1804375;
const Y = x * 0.2126729 + y * 0.7151522 + z * 0.0721750;
const Z = x * 0.0193339 + y * 0.1191920 + z * 0.9503041;
return [X, Y, Z];
}
/**
* Convert XYZ to RGB color space
* @param {number} x - X value
* @param {number} y - Y value
* @param {number} z - Z value
* @returns {number[]} RGB values [R, G, B] (0-255)
* @private
*/
_xyzToRgb(x, y, z) {
// Convert XYZ to linear RGB
let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560;
let b = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
// Apply gamma correction
r = r > 0.0031308 ? 1.055 * Math.pow(r, 1.0 / 2.4) - 0.055 : 12.92 * r;
g = g > 0.0031308 ? 1.055 * Math.pow(g, 1.0 / 2.4) - 0.055 : 12.92 * g;
b = b > 0.0031308 ? 1.055 * Math.pow(b, 1.0 / 2.4) - 0.055 : 12.92 * b;
// Clamp and convert to 0-255 range
return [
Math.round(Math.max(0, Math.min(1, r)) * 255),
Math.round(Math.max(0, Math.min(1, g)) * 255),
Math.round(Math.max(0, Math.min(1, b)) * 255)
];
}
/**
* Convert RGB to LUV color space
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} LUV values [L, u, v]
* @private
*/
_rgbToLuv(r, g, b) {
const [X, Y, Z] = this._rgbToXyz(r, g, b);
// Reference white D65
const Xn = 0.95047;
const Yn = 1.00000;
const Zn = 1.08883;
const yr = Y / Yn;
const L = yr > 0.008856 ? 116 * Math.pow(yr, 1/3) - 16 : 903.3 * yr;
const denom = X + 15 * Y + 3 * Z;
const denomN = Xn + 15 * Yn + 3 * Zn;
const up = denom === 0 ? 0 : (4 * X) / denom;
const vp = denom === 0 ? 0 : (9 * Y) / denom;
const upN = (4 * Xn) / denomN;
const vpN = (9 * Yn) / denomN;
const u = 13 * L * (up - upN);
const v = 13 * L * (vp - vpN);
return [L, u, v];
}
/**
* Convert LUV to RGB color space
* @param {number} l - L value
* @param {number} u - u value
* @param {number} v - v value
* @returns {number[]} RGB values [R, G, B] (0-255)
* @private
*/
_luvToRgb(l, u, v) {
// Reference white D65
const Xn = 0.95047;
const Yn = 1.00000;
const Zn = 1.08883;
const upN = (4 * Xn) / (Xn + 15 * Yn + 3 * Zn);
const vpN = (9 * Yn) / (Xn + 15 * Yn + 3 * Zn);
const Y = l > 8 ? Yn * Math.pow((l + 16) / 116, 3) : Yn * l / 903.3;
const up = u / (13 * l) + upN;
const vp = v / (13 * l) + vpN;
const X = Y * 9 * up / (4 * vp);
const Z = Y * (12 - 3 * up - 20 * vp) / (4 * vp);
return this._xyzToRgb(X, Y, Z);
}
/**
* Convert RGB to YUV color space
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} YUV values [Y, U, V]
* @private
*/
_rgbToYuv(r, g, b) {
const Y = 0.299 * r + 0.587 * g + 0.114 * b;
const U = -0.14713 * r - 0.28886 * g + 0.436 * b;
const V = 0.615 * r - 0.51499 * g - 0.10001 * b;
return [Y, U, V];
}
/**
* Convert YUV to RGB color space
* @param {number} y - Y (luminance) value
* @param {number} u - U (chrominance) value
* @param {number} v - V (chrominance) value
* @returns {number[]} RGB values [R, G, B] (0-255)
* @private
*/
_yuvToRgb(y, u, v) {
const r = y + 1.13983 * v;
const g = y - 0.39465 * u - 0.58060 * v;
const b = y + 2.03211 * u;
return [
Math.round(Math.max(0, Math.min(255, r))),
Math.round(Math.max(0, Math.min(255, g))),
Math.round(Math.max(0, Math.min(255, b)))
];
}
/**
* Convert RGB to Oklab color space (improved LAB)
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} Oklab values [L, a, b]
* @private
*/
_rgbToOklab(r, g, b) {
// Normalize RGB
r /= 255;
g /= 255;
b /= 255;
// Apply gamma correction
r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
// Linear RGB to Oklab
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
const l_ = Math.cbrt(l);
const m_ = Math.cbrt(m);
const s_ = Math.cbrt(s);
return [
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
];
}
/**
* Convert Oklab to RGB color space
* @param {number} l - L value
* @param {number} a - a value
* @param {number} b - b value
* @returns {number[]} RGB values [R, G, B] (0-255)
* @private
*/
_oklabToRgb(l, a, b) {
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
const l3 = l_ * l_ * l_;
const m3 = m_ * m_ * m_;
const s3 = s_ * s_ * s_;
let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
let b2 = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
// Apply gamma correction
r = r > 0.0031308 ? 1.055 * Math.pow(r, 1.0 / 2.4) - 0.055 : 12.92 * r;
g = g > 0.0031308 ? 1.055 * Math.pow(g, 1.0 / 2.4) - 0.055 : 12.92 * g;
b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1.0 / 2.4) - 0.055 : 12.92 * b2;
return [
Math.round(Math.max(0, Math.min(1, r)) * 255),
Math.round(Math.max(0, Math.min(1, g)) * 255),
Math.round(Math.max(0, Math.min(1, b2)) * 255)
];
}
/**
* Convert LAB to LCH color space (cylindrical representation)
* @param {number} l - L value
* @param {number} a - a value
* @param {number} b - b value
* @returns {number[]} LCH values [L, C, H]
* @private
*/
_labToLch(l, a, b) {
const c = Math.sqrt(a * a + b * b);
let h = Math.atan2(b, a) * 180 / Math.PI;
if (h < 0) h += 360;
return [l, c, h];
}
/**
* Convert LCH to LAB color space
* @param {number} l - L value
* @param {number} c - C (chroma) value
* @param {number} h - H (hue) value (0-360)
* @returns {number[]} LAB values [L, a, b]
* @private
*/
_lchToLab(l, c, h) {
const hRad = h * Math.PI / 180;
const a = c * Math.cos(hRad);
const b = c * Math.sin(hRad);
return [l, a, b];
}
/**
* Find closest color in palette using specified algorithm
* @param {number} r - Red value
@@ -235,7 +639,170 @@ class ImageProcessor {
return cor;
}
// LAB algorithm
// HSV algorithm
if (algorithm === 'hsv') {
const [ht, st, vt] = this._rgbToHsv(r, g, b);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [hp, sp, vp] = this._rgbToHsv(pr, pg, pb);
const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)) / 360; // Normalize hue distance
const ds = st - sp;
const dv = vt - vp;
const dist = dh * dh + ds * ds + dv * dv;
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// HSL algorithm
if (algorithm === 'hsl') {
const [ht, st, lt] = this._rgbToHsl(r, g, b);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [hp, sp, lp] = this._rgbToHsl(pr, pg, pb);
const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)) / 360; // Normalize hue distance
const ds = st - sp;
const dl = lt - lp;
const dist = dh * dh + ds * ds + dl * dl;
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// XYZ algorithm
if (algorithm === 'xyz') {
const [xt, yt, zt] = this._rgbToXyz(r, g, b);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [xp, yp, zp] = this._rgbToXyz(pr, pg, pb);
const dx = xt - xp;
const dy = yt - yp;
const dz = zt - zp;
const dist = dx * dx + dy * dy + dz * dz;
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// LUV algorithm
if (algorithm === 'luv') {
const [lt, ut, vt] = this._rgbToLuv(r, g, b);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [lp, up, vp] = this._rgbToLuv(pr, pg, pb);
const dl = lt - lp;
const du = ut - up;
const dv = vt - vp;
const dist = dl * dl + du * du + dv * dv;
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// YUV algorithm
if (algorithm === 'yuv') {
const [yt, ut, vt] = this._rgbToYuv(r, g, b);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [yp, up, vp] = this._rgbToYuv(pr, pg, pb);
const dy = yt - yp;
const du = ut - up;
const dv = vt - vp;
const dist = dy * dy + du * du + dv * dv;
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// Oklab algorithm
if (algorithm === 'oklab') {
const [lt, at, bt] = this._rgbToOklab(r, g, b);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [lp, ap, bp] = this._rgbToOklab(pr, pg, pb);
const dl = lt - lp;
const da = at - ap;
const db = bt - bp;
const dist = dl * dl + da * da + db * db;
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// LCH algorithm
if (algorithm === 'lch') {
const [Lt, at, bt] = this._getLab(r, g, b);
const [lt, ct, ht] = this._labToLch(Lt, at, bt);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [Lp, ap, bp] = this._getLab(pr, pg, pb);
const [lp, cp, hp] = this._labToLch(Lp, ap, bp);
const dl = lt - lp;
const dc = ct - cp;
const dh = Math.min(Math.abs(ht - hp), 360 - Math.abs(ht - hp)); // Circular hue distance
const dist = dl * dl + dc * dc + (dh * dh) / 360; // Normalize hue
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
// LAB algorithm (default)
const [Lt, at, bt] = this._getLab(r, g, b);
const targetChroma = Math.sqrt(at * at + bt * bt);
let best = null;
@@ -710,6 +1277,13 @@ class ImageProcessor {
clearCaches() {
this._colorCache.clear();
this._labCache.clear();
this._hsvCache?.clear();
this._hslCache?.clear();
this._xyzCache?.clear();
this._luvCache?.clear();
this._yuvCache?.clear();
this._oklabCache?.clear();
this._lchCache?.clear();
this._ditherWorkBuf = null;
this._ditherEligibleBuf = null;
}
@@ -738,4 +1312,4 @@ window.globalImageProcessor = new ImageProcessor();
// Legacy compatibility - expose key methods globally for backward compatibility
window.ImageProcessor = ImageProcessor;
console.log('✅ WPlace Image Processor loaded and ready');
console.log('✅ WPlace Image Processor loaded and ready');