jedmund-svelte/src/lib/server/color-utils.ts
Devin AI 6caf2651ac lint: remove unused imports and variables in server files (6 fixes)
Co-Authored-By: Justin Edmund <justin@jedmund.com>
2025-11-23 14:41:22 +00:00

236 lines
6.5 KiB
TypeScript

/**
* 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
}
}