From dc36b4c706748473070d7f6482bd257ad418d7ba Mon Sep 17 00:00:00 2001 From: jingrow Date: Wed, 21 Jan 2026 15:58:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8OKLCH=E9=A2=9C?= =?UTF-8?q?=E8=89=B2=E7=A9=BA=E9=97=B4=E9=87=8D=E6=9E=84=E8=89=B2=E9=98=B6?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将HSL颜色模型替换为OKLCH,实现感知均匀的颜色过渡 - 基于提取的主色调浅色版本生成11个色阶,浅色居中显示 - 左侧5个更浅色阶,右侧5个更深色阶,亮度间隔±0.08 OKLCH优势: - 感知上均匀的亮度过渡,避免视觉跳跃 - 保持色相(H)和色度(C)恒定,确保同色系 - 深色和浅色区域过渡更自然平滑 - 符合人眼对颜色差异的实际感知 色阶生成流程: 1. ColorThief提取图片主色调(RGB) 2. 转换为OKLCH,生成高亮度(L=0.92)浅色版本 3. 基于浅色背景生成11个感知均匀的色阶 4. 在页面标题栏中间显示,支持快速切换 技术实现: - 实现完整的Hex ↔ OKLCH双向转换 - 转换路径:Hex → sRGB → 线性RGB → OKLab → OKLCH - 更新rgbToLightPastel函数使用OKLCH生成浅色 --- .../tools/add_background/add_background.vue | 137 ++++++++++-------- 1 file changed, 79 insertions(+), 58 deletions(-) diff --git a/src/views/tools/add_background/add_background.vue b/src/views/tools/add_background/add_background.vue index 0a9b3ea..fb4ca44 100644 --- a/src/views/tools/add_background/add_background.vue +++ b/src/views/tools/add_background/add_background.vue @@ -267,54 +267,88 @@ const commonColors = ref([ '#A0A0A0' // Dark Gray ]) -// HSL color utilities - efficient and modern approach -const hexToHsl = (hex: string): { h: number; s: number; l: number } => { +// OKLCH color utilities - perceptually uniform color space +const hexToOklch = (hex: string): { l: number; c: number; h: number } => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) - if (!result) return { h: 0, s: 0, l: 100 } + if (!result) return { l: 1, c: 0, h: 0 } + // Convert hex to linear RGB let r = parseInt(result[1], 16) / 255 let g = parseInt(result[2], 16) / 255 let b = parseInt(result[3], 16) / 255 - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - let h = 0, s = 0 - const l = (max + min) / 2 + // Apply sRGB to linear RGB conversion + 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 - if (max !== min) { - const d = max - min - s = l > 0.5 ? d / (2 - max - min) : d / (max + min) - switch (max) { - case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break - case g: h = ((b - r) / d + 2) / 6; break - case b: h = ((r - g) / d + 4) / 6; break - } - } + // Convert 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 - return { h: h * 360, s: s * 100, l: l * 100 } + const l = Math.cbrt(l_) + const m = Math.cbrt(m_) + const s = Math.cbrt(s_) + + const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s + const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s + const bVal = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s + + // Convert OKLab to OKLCH + const C = Math.sqrt(a * a + bVal * bVal) + const H = Math.atan2(bVal, a) * 180 / Math.PI + + return { l: L, c: C, h: H >= 0 ? H : H + 360 } } -const hslToHex = (h: number, s: number, l: number): string => { - s /= 100 - l /= 100 - const a = s * Math.min(l, 1 - l) - const f = (n: number) => { - const k = (n + h / 30) % 12 - const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1) - return Math.round(255 * color).toString(16).padStart(2, '0') - } - return `#${f(0)}${f(8)}${f(4)}`.toUpperCase() +const oklchToHex = (l: number, c: number, h: number): string => { + // Convert OKLCH to OKLab + const hRad = h * Math.PI / 180 + const a = c * Math.cos(hRad) + const bVal = c * Math.sin(hRad) + + // Convert OKLab to linear RGB + const l_ = l + 0.3963377774 * a + 0.2158037573 * bVal + const m_ = l - 0.1055613458 * a - 0.0638541728 * bVal + const s_ = l - 0.0894841775 * a - 1.2914855480 * bVal + + const lCube = l_ * l_ * l_ + const mCube = m_ * m_ * m_ + const sCube = s_ * s_ * s_ + + let r = +4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube + let g = -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube + let bRgb = -0.0041960863 * lCube - 0.7034186147 * mCube + 1.7076147010 * sCube + + // Apply linear RGB to sRGB conversion + r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r + g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g + bRgb = bRgb > 0.0031308 ? 1.055 * Math.pow(bRgb, 1 / 2.4) - 0.055 : 12.92 * bRgb + + // Clamp values to [0, 1] + r = Math.max(0, Math.min(1, r)) + g = Math.max(0, Math.min(1, g)) + bRgb = Math.max(0, Math.min(1, bRgb)) + + // Convert to hex + const rHex = Math.round(r * 255).toString(16).padStart(2, '0') + const gHex = Math.round(g * 255).toString(16).padStart(2, '0') + const bHex = Math.round(bRgb * 255).toString(16).padStart(2, '0') + + return `#${rHex}${gHex}${bHex}`.toUpperCase() } -// Generate 11 color shades with current color in the middle (6th position) +// Generate 11 color shades with current color in the middle (6th position) using OKLCH const generateColorShades = (baseColor: string): string[] => { - const { h, s, l } = hexToHsl(baseColor) + const { l, c, h } = hexToOklch(baseColor) const shades: string[] = [] // Generate 5 lighter shades before current color + // Use perceptually uniform lightness steps (±0.08 intervals) for (let i = 5; i > 0; i--) { - const lighterL = Math.min(100, l + (i * 8)) - shades.push(hslToHex(h, s, lighterL)) + const lighterL = Math.min(1, l + (i * 0.08)) + shades.push(oklchToHex(lighterL, c, h)) } // Add current color in the middle (6th position) @@ -322,8 +356,8 @@ const generateColorShades = (baseColor: string): string[] => { // Generate 5 darker shades after current color for (let i = 1; i <= 5; i++) { - const darkerL = Math.max(0, l - (i * 8)) - shades.push(hslToHex(h, s, darkerL)) + const darkerL = Math.max(0, l - (i * 0.08)) + shades.push(oklchToHex(darkerL, c, h)) } return shades @@ -974,35 +1008,22 @@ const resetUpload = () => { } } -// Convert RGB to light pastel color based on the same hue +// Convert RGB to light pastel color using OKLCH const rgbToLightPastel = (r: number, g: number, b: number): string => { - // Convert RGB to HSL - const rNorm = r / 255 - const gNorm = g / 255 - const bNorm = b / 255 + // Convert RGB to hex first + const rHex = Math.round(r).toString(16).padStart(2, '0') + const gHex = Math.round(g).toString(16).padStart(2, '0') + const bHex = Math.round(b).toString(16).padStart(2, '0') + const hexColor = `#${rHex}${gHex}${bHex}` - const max = Math.max(rNorm, gNorm, bNorm) - const min = Math.min(rNorm, gNorm, bNorm) - let h = 0 - let s = 0 - const l = (max + min) / 2 + // Convert to OKLCH + const { l, c, h } = hexToOklch(hexColor) - if (max !== min) { - const d = max - min - s = l > 0.5 ? d / (2 - max - min) : d / (max + min) - switch (max) { - case rNorm: h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6; break - case gNorm: h = ((bNorm - rNorm) / d + 2) / 6; break - case bNorm: h = ((rNorm - gNorm) / d + 4) / 6; break - } - } + // Create light pastel version: high lightness (0.92), reduced chroma + const pastelL = 0.92 + const pastelC = Math.min(c, 0.08) // Reduce chroma for pastel effect - // Convert to light pastel: keep hue, reduce saturation to 25-35%, increase lightness to 90-95% - const pastelS = Math.min(s * 100, 40) // Cap saturation at 40% for pastel effect - const pastelL = 92 // High lightness for light background - - // Convert HSL back to RGB - return hslToHex(h * 360, pastelS, pastelL) + return oklchToHex(pastelL, pastelC, h) } // Extract dominant color from image using color-thief