/** * Color utility functions for selecting better dominant colors */ interface ColorInfo { hex: string percentage: number } /** * Calculate color vibrance/saturation * Returns a value between 0 (grey) and 1 (fully saturated) */ function getColorVibrance(hex: string): number { // Convert hex to RGB const r = parseInt(hex.slice(1, 3), 16) / 255 const g = parseInt(hex.slice(3, 5), 16) / 255 const b = parseInt(hex.slice(5, 7), 16) / 255 const max = Math.max(r, g, b) const min = Math.min(r, g, b) // Calculate saturation const delta = max - min const lightness = (max + min) / 2 if (delta === 0) return 0 // Grey const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min) return saturation } /** * Calculate color brightness * Returns a value between 0 (black) and 1 (white) */ function getColorBrightness(hex: string): number { const r = parseInt(hex.slice(1, 3), 16) / 255 const g = parseInt(hex.slice(3, 5), 16) / 255 const b = parseInt(hex.slice(5, 7), 16) / 255 // Using perceived brightness formula return r * 0.299 + g * 0.587 + b * 0.114 } /** * Score a color based on its visual interest * Higher scores mean more visually interesting colors */ function scoreColor(color: ColorInfo, preferBrighter: boolean = false): number { const vibrance = getColorVibrance(color.hex) const brightness = getColorBrightness(color.hex) // Apply brightness penalties with a smoother curve let brightnessPenalty = 0 if (brightness < 0.15) { // Heavy penalty for very dark colors (below 15%) brightnessPenalty = (0.15 - brightness) * 6 } else if (brightness < 0.3 && preferBrighter) { // Moderate penalty for dark colors (15-30%) when preferBrighter is true brightnessPenalty = (0.3 - brightness) * 2 } else if (brightness > 0.85) { // Penalty for very light colors brightnessPenalty = (brightness - 0.85) * 2 } // Ideal brightness range is 0.3-0.7 for most use cases const idealBrightness = brightness >= 0.3 && brightness <= 0.7 // Weight factors const vibranceWeight = 2.5 // Prefer colorful over grey const percentageWeight = 0.4 // Slightly higher weight for prevalence const brightnessWeight = 2.0 // Important to avoid too dark/light // Calculate base score let score = vibrance * vibranceWeight + (color.percentage / 100) * percentageWeight + Math.max(0, 1 - brightnessPenalty) * brightnessWeight // Apply bonuses for ideal colors if (idealBrightness && vibrance > 0.5) { // Bonus for colors in ideal brightness range with good vibrance score *= 1.3 } else if (vibrance > 0.8 && brightness > 0.25 && brightness < 0.75) { // Smaller bonus for very vibrant colors that aren't too dark/light score *= 1.15 } return score } /** * Select the best dominant color from Cloudinary's color array * * @param colors - Array of [hex, percentage] tuples from Cloudinary * @param options - Configuration options * @returns The selected dominant color hex string */ export function selectBestDominantColor( colors: Array<[string, number]>, options: { minPercentage?: number preferVibrant?: boolean excludeGreys?: boolean preferBrighter?: boolean } = {} ): string { const { minPercentage = 2, // Ignore colors below this percentage excludeGreys = false, preferBrighter = true // Avoid very dark colors } = options if (!colors || colors.length === 0) { return '#888888' // Default grey } // Convert to our format and filter let colorCandidates: ColorInfo[] = colors .map(([hex, percentage]) => ({ hex, percentage })) .filter((color) => color.percentage >= minPercentage) // Exclude greys if requested if (excludeGreys) { colorCandidates = colorCandidates.filter((color) => { const vibrance = getColorVibrance(color.hex) return vibrance > 0.1 // Keep colors with at least 10% saturation }) } // If no candidates after filtering, use the original dominant color if (colorCandidates.length === 0) { return colors[0][0] } // Score and sort colors const scoredColors = colorCandidates.map((color) => ({ ...color, score: scoreColor(color, preferBrighter) })) scoredColors.sort((a, b) => b.score - a.score) // If we're still getting a darker color than ideal, look for better alternatives if (preferBrighter && scoredColors.length > 1) { const bestColor = scoredColors[0] const bestBrightness = getColorBrightness(bestColor.hex) // If the best color is darker than ideal (< 45%), check alternatives if (bestBrightness < 0.45) { // Look through top candidates for significantly brighter alternatives for (let i = 1; i < Math.min(5, scoredColors.length); i++) { const candidate = scoredColors[i] const candidateBrightness = getColorBrightness(candidate.hex) const candidateVibrance = getColorVibrance(candidate.hex) // Select a brighter alternative if: // 1. It's at least 15% brighter than current best // 2. It still has good vibrance (> 0.5) // 3. Its score is at least 80% of the best score if ( candidateBrightness > bestBrightness + 0.15 && candidateVibrance > 0.5 && candidate.score >= bestColor.score * 0.8 ) { return candidate.hex } } // If still very dark and we can lower the threshold, try again if (bestBrightness < 0.25 && minPercentage > 0.5) { return selectBestDominantColor(colors, { ...options, minPercentage: Math.max(0.5, minPercentage * 0.5) }) } } } // Return the best scoring color return scoredColors[0].hex } /** * Get a color palette excluding greys and very dark/light colors */ export function getVibrantPalette( colors: Array<[string, number]>, maxColors: number = 5 ): string[] { const vibrantColors = colors .map(([hex, percentage]) => ({ hex, percentage })) .filter((color) => { const vibrance = getColorVibrance(color.hex) const brightness = getColorBrightness(color.hex) return vibrance > 0.2 && brightness > 0.15 && brightness < 0.85 }) .slice(0, maxColors) .map((color) => color.hex) return vibrantColors } /** * Determine if a color is considered "grey" or neutral */ export function isGreyColor(hex: string): boolean { const vibrance = getColorVibrance(hex) return vibrance < 0.1 } /** * Debug function to analyze a color */ export function analyzeColor(hex: string): { hex: string vibrance: number brightness: number isGrey: boolean isDark: boolean isBright: boolean } { const vibrance = getColorVibrance(hex) const brightness = getColorBrightness(hex) return { hex, vibrance, brightness, isGrey: vibrance < 0.1, isDark: brightness < 0.2, isBright: brightness > 0.9 } }