feat: 使用OKLCH颜色空间重构色阶生成逻辑
- 将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生成浅色
This commit is contained in:
parent
ae7ed98808
commit
dc36b4c706
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user