package engine import ( "fmt" "math" "strings" ) // Color-related constants const ( // Alpha channel constants defaultAlphaValue = 255 // Default alpha value for opaque colors // RGB/HSL conversion constants rgbComponentMax = 255.0 // Maximum RGB component value rgbMaxValue = 255 // Maximum RGB value as integer hueCycle = 6.0 // Hue cycle length for HSL conversion hslMidpoint = 0.5 // HSL lightness midpoint // Grayscale detection threshold grayscaleToleranceThreshold = 0.01 // Threshold for detecting grayscale colors // Hue calculation constants hueSegmentCount = 6 // Number of hue segments for correction hueRounding = 0.5 // Rounding offset for hue indexing // Color theme lightness values (matches JavaScript implementation) colorThemeDarkLightness = 0.0 // Dark color lightness value colorThemeMidLightness = 0.5 // Mid color lightness value colorThemeFullLightness = 1.0 // Full lightness value // Hex color string buffer sizes hexColorLength = 7 // #rrggbb = 7 characters hexColorAlphaLength = 9 // #rrggbbaa = 9 characters ) // Lightness correctors for each hue segment (based on JavaScript implementation) // These values are carefully tuned to match the JavaScript reference implementation var correctors = []float64{ 0.55, // Red hues (0°-60°) 0.5, // Yellow hues (60°-120°) 0.5, // Green hues (120°-180°) 0.46, // Cyan hues (180°-240°) 0.6, // Blue hues (240°-300°) 0.55, // Magenta hues (300°-360°) 0.55, // Wrap-around for edge cases } // Color represents a color with HSL representation and on-demand RGB conversion type Color struct { H, S, L float64 // HSL values: H=[0,1], S=[0,1], L=[0,1] A uint8 // Alpha channel: [0,255] corrected bool // Whether to use corrected HSL to RGB conversion } // ToRGB returns the RGB values computed from HSL using appropriate conversion func (c Color) ToRGB() (r, g, b uint8, err error) { if c.corrected { return CorrectedHSLToRGB(c.H, c.S, c.L) } return HSLToRGB(c.H, c.S, c.L) } // ToRGBA returns the RGBA values computed from HSL using appropriate conversion func (c Color) ToRGBA() (r, g, b, a uint8, err error) { r, g, b, err = c.ToRGB() return r, g, b, c.A, err } // NewColorHSL creates a new Color from HSL values func NewColorHSL(h, s, l float64) Color { return Color{ H: h, S: s, L: l, A: defaultAlphaValue, corrected: false, } } // NewColorCorrectedHSL creates a new Color from HSL values with lightness correction func NewColorCorrectedHSL(h, s, l float64) Color { return Color{ H: h, S: s, L: l, A: defaultAlphaValue, corrected: true, } } // NewColorRGB creates a new Color from RGB values and calculates HSL func NewColorRGB(r, g, b uint8) Color { h, s, l := RGBToHSL(r, g, b) return Color{ H: h, S: s, L: l, A: defaultAlphaValue, corrected: false, } } // NewColorRGBA creates a new Color from RGBA values and calculates HSL func NewColorRGBA(r, g, b, a uint8) Color { h, s, l := RGBToHSL(r, g, b) return Color{ H: h, S: s, L: l, A: a, corrected: false, } } // String returns the hex representation of the color func (c Color) String() string { r, g, b, err := c.ToRGB() if err != nil { // Return a fallback color (black) if conversion fails // This maintains the string contract while indicating an error state r, g, b = 0, 0, 0 } if c.A == defaultAlphaValue { return RGBToHex(r, g, b) } // Use strings.Builder for RGBA format var buf strings.Builder buf.Grow(hexColorAlphaLength) buf.WriteByte('#') writeHexByte(&buf, r) writeHexByte(&buf, g) writeHexByte(&buf, b) writeHexByte(&buf, c.A) return buf.String() } // Equals compares two colors for equality func (c Color) Equals(other Color) bool { r1, g1, b1, err1 := c.ToRGB() r2, g2, b2, err2 := other.ToRGB() // If either color has a conversion error, they are not equal if err1 != nil || err2 != nil { return false } return r1 == r2 && g1 == g2 && b1 == b2 && c.A == other.A } // WithAlpha returns a new color with the specified alpha value func (c Color) WithAlpha(alpha uint8) Color { return Color{ H: c.H, S: c.S, L: c.L, A: alpha, corrected: c.corrected, } } // IsGrayscale returns true if the color is grayscale (saturation near zero) func (c Color) IsGrayscale() bool { return c.S < grayscaleToleranceThreshold } // Darken returns a new color with reduced lightness func (c Color) Darken(amount float64) Color { newL := clamp(c.L-amount, 0, 1) return Color{ H: c.H, S: c.S, L: newL, A: c.A, corrected: c.corrected, } } // Lighten returns a new color with increased lightness func (c Color) Lighten(amount float64) Color { newL := clamp(c.L+amount, 0, 1) return Color{ H: c.H, S: c.S, L: newL, A: c.A, corrected: c.corrected, } } // RGBToHSL converts RGB values to HSL // Returns H=[0,1], S=[0,1], L=[0,1] func RGBToHSL(r, g, b uint8) (h, s, l float64) { rf := float64(r) / rgbComponentMax gf := float64(g) / rgbComponentMax bf := float64(b) / rgbComponentMax max := math.Max(rf, math.Max(gf, bf)) min := math.Min(rf, math.Min(gf, bf)) // Calculate lightness l = (max + min) / 2.0 if max == min { // Achromatic (gray) h, s = 0, 0 } else { delta := max - min // Calculate saturation if l > hslMidpoint { s = delta / (2.0 - max - min) } else { s = delta / (max + min) } // Calculate hue switch max { case rf: h = (gf - bf) / delta if gf < bf { h += 6 } case gf: h = (bf-rf)/delta + 2 case bf: h = (rf-gf)/delta + 4 } h /= hueCycle } return h, s, l } // HSLToRGB converts HSL color values to RGB. // h: hue in range [0, 1] // s: saturation in range [0, 1] // l: lightness in range [0, 1] // Returns RGB values in range [0, 255] and an error if conversion fails func HSLToRGB(h, s, l float64) (r, g, b uint8, err error) { // Clamp input values to valid ranges h = math.Mod(h, 1.0) if h < 0 { h += 1.0 } s = clamp(s, 0, 1) l = clamp(l, 0, 1) // Handle grayscale case (saturation = 0) if s == 0 { // All RGB components are equal for grayscale gray := uint8(clamp(l*rgbComponentMax, 0, rgbComponentMax)) return gray, gray, gray, nil } // Calculate intermediate values for HSL to RGB conversion var m2 float64 if l <= hslMidpoint { m2 = l * (s + 1) } else { m2 = l + s - l*s } m1 := l*2 - m2 // Convert each RGB component rf := hueToRGB(m1, m2, h*hueCycle+2) * rgbComponentMax gf := hueToRGB(m1, m2, h*hueCycle) * rgbComponentMax bf := hueToRGB(m1, m2, h*hueCycle-2) * rgbComponentMax // Validate floating point results before conversion to uint8 if math.IsNaN(rf) || math.IsInf(rf, 0) || math.IsNaN(gf) || math.IsInf(gf, 0) || math.IsNaN(bf) || math.IsInf(bf, 0) { return 0, 0, 0, fmt.Errorf("jdenticon: engine: HSL to RGB conversion failed: non-finite value produced during conversion") } r = uint8(clamp(rf, 0, rgbComponentMax)) g = uint8(clamp(gf, 0, rgbComponentMax)) b = uint8(clamp(bf, 0, rgbComponentMax)) return r, g, b, nil } // CorrectedHSLToRGB converts HSL to RGB with lightness correction for better visual perception. // This function adjusts the lightness based on the hue to compensate for the human eye's // different sensitivity to different colors. func CorrectedHSLToRGB(h, s, l float64) (r, g, b uint8, err error) { // Defensive check: ensure correctors table is properly initialized if len(correctors) == 0 { return 0, 0, 0, fmt.Errorf("jdenticon: engine: corrected HSL to RGB conversion failed: color correctors table is empty or not initialized") } // Get the corrector for the current hue hueIndex := int((h*hueSegmentCount + hueRounding)) % len(correctors) corrector := correctors[hueIndex] // Adjust lightness relative to the corrector if l < hslMidpoint { l = l * corrector * 2 } else { l = corrector + (l-hslMidpoint)*(1-corrector)*2 } // Clamp the corrected lightness l = clamp(l, 0, 1) // Call HSLToRGB and propagate any error r, g, b, err = HSLToRGB(h, s, l) if err != nil { return 0, 0, 0, fmt.Errorf("jdenticon: engine: corrected HSL to RGB conversion failed: %w", err) } return r, g, b, nil } // hueToRGB converts a hue value to an RGB component value // Based on the W3C CSS3 color specification func hueToRGB(m1, m2, h float64) float64 { // Normalize hue to [0, 6) range if h < 0 { h += hueCycle } else if h > hueCycle { h -= hueCycle } // Calculate RGB component based on hue position if h < 1 { return m1 + (m2-m1)*h } else if h < 3 { return m2 } else if h < 4 { return m1 + (m2-m1)*(4-h) } else { return m1 } } // clamp constrains a value to the specified range [min, max] func clamp(value, min, max float64) float64 { if value < min { return min } if value > max { return max } return value } // writeHexByte writes a single byte as two hex characters to the builder func writeHexByte(buf *strings.Builder, b uint8) { const hexChars = "0123456789abcdef" buf.WriteByte(hexChars[b>>4]) buf.WriteByte(hexChars[b&0x0f]) } // RGBToHex converts RGB values to a hexadecimal color string func RGBToHex(r, g, b uint8) string { // Use a strings.Builder for more efficient hex formatting var buf strings.Builder buf.Grow(hexColorLength) buf.WriteByte('#') writeHexByte(&buf, r) writeHexByte(&buf, g) writeHexByte(&buf, b) return buf.String() } // GenerateColor creates a color with the specified hue and configuration-based saturation and lightness func GenerateColor(hue float64, config ColorConfig, lightnessValue float64) Color { // Restrict hue according to configuration restrictedHue := config.RestrictHue(hue) // Get lightness from configuration range lightness := config.ColorLightness.GetLightness(lightnessValue) // Use corrected HSL to RGB conversion return NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, lightness) } // GenerateGrayscale creates a grayscale color with configuration-based saturation and lightness func GenerateGrayscale(config ColorConfig, lightnessValue float64) Color { // For grayscale, hue doesn't matter, but we'll use 0 hue := colorThemeDarkLightness // Get lightness from grayscale configuration range lightness := config.GrayscaleLightness.GetLightness(lightnessValue) // Use grayscale saturation (typically 0) return NewColorCorrectedHSL(hue, config.GrayscaleSaturation, lightness) } // GenerateColorTheme generates a set of color candidates based on the JavaScript colorTheme function // This matches the JavaScript implementation that creates 5 colors: // 0: Dark gray, 1: Mid color, 2: Light gray, 3: Light color, 4: Dark color func GenerateColorTheme(hue float64, config ColorConfig) []Color { // Restrict hue according to configuration restrictedHue := config.RestrictHue(hue) return []Color{ // Dark gray (grayscale with lightness 0) NewColorCorrectedHSL(restrictedHue, config.GrayscaleSaturation, config.GrayscaleLightness.GetLightness(colorThemeDarkLightness)), // Mid color (normal color with lightness 0.5) NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, config.ColorLightness.GetLightness(colorThemeMidLightness)), // Light gray (grayscale with lightness 1) NewColorCorrectedHSL(restrictedHue, config.GrayscaleSaturation, config.GrayscaleLightness.GetLightness(colorThemeFullLightness)), // Light color (normal color with lightness 1) NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, config.ColorLightness.GetLightness(colorThemeFullLightness)), // Dark color (normal color with lightness 0) NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, config.ColorLightness.GetLightness(colorThemeDarkLightness)), } }