package engine import ( "fmt" "math" "strconv" ) // Lightness correctors for each hue segment (based on JavaScript implementation) var correctors = []float64{0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55} // Color represents a color with both HSL and RGB representations type Color struct { H, S, L float64 // HSL values: H=[0,1], S=[0,1], L=[0,1] R, G, B uint8 // RGB values: [0,255] A uint8 // Alpha channel: [0,255] } // NewColorHSL creates a new Color from HSL values func NewColorHSL(h, s, l float64) Color { r, g, b := HSLToRGB(h, s, l) return Color{ H: h, S: s, L: l, R: r, G: g, B: b, A: 255, } } // NewColorCorrectedHSL creates a new Color from HSL values with lightness correction func NewColorCorrectedHSL(h, s, l float64) Color { r, g, b := CorrectedHSLToRGB(h, s, l) return Color{ H: h, S: s, L: l, R: r, G: g, B: b, A: 255, } } // 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, R: r, G: g, B: b, A: 255, } } // 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, R: r, G: g, B: b, A: a, } } // String returns the hex representation of the color func (c Color) String() string { if c.A == 255 { return RGBToHex(c.R, c.G, c.B) } return fmt.Sprintf("#%02x%02x%02x%02x", c.R, c.G, c.B, c.A) } // Equals compares two colors for equality func (c Color) Equals(other Color) bool { return c.R == other.R && c.G == other.G && c.B == other.B && 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, R: c.R, G: c.G, B: c.B, A: alpha, } } // IsGrayscale returns true if the color is grayscale (saturation near zero) func (c Color) IsGrayscale() bool { return c.S < 0.01 // Small tolerance for floating point comparison } // Darken returns a new color with reduced lightness func (c Color) Darken(amount float64) Color { newL := clamp(c.L-amount, 0, 1) return NewColorCorrectedHSL(c.H, c.S, newL) } // Lighten returns a new color with increased lightness func (c Color) Lighten(amount float64) Color { newL := clamp(c.L+amount, 0, 1) return NewColorCorrectedHSL(c.H, c.S, newL) } // 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) / 255.0 gf := float64(g) / 255.0 bf := float64(b) / 255.0 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 > 0.5 { s = delta / (2.0 - max - min) } else { s = delta / (max + min) } // Calculate hue switch max { case rf: h = (gf-bf)/delta + (func() float64 { if gf < bf { return 6 } return 0 })() case gf: h = (bf-rf)/delta + 2 case bf: h = (rf-gf)/delta + 4 } h /= 6.0 } 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] func HSLToRGB(h, s, l float64) (r, g, b uint8) { // 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*255, 0, 255)) return gray, gray, gray } // Calculate intermediate values for HSL to RGB conversion var m2 float64 if l <= 0.5 { m2 = l * (s + 1) } else { m2 = l + s - l*s } m1 := l*2 - m2 // Convert each RGB component r = uint8(clamp(hueToRGB(m1, m2, h*6+2)*255, 0, 255)) g = uint8(clamp(hueToRGB(m1, m2, h*6)*255, 0, 255)) b = uint8(clamp(hueToRGB(m1, m2, h*6-2)*255, 0, 255)) return r, g, b } // 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) { // Get the corrector for the current hue hueIndex := int((h*6 + 0.5)) % len(correctors) corrector := correctors[hueIndex] // Adjust lightness relative to the corrector if l < 0.5 { l = l * corrector * 2 } else { l = corrector + (l-0.5)*(1-corrector)*2 } // Clamp the corrected lightness l = clamp(l, 0, 1) return HSLToRGB(h, s, l) } // 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 += 6 } else if h > 6 { h -= 6 } // 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 } // RGBToHex converts RGB values to a hexadecimal color string func RGBToHex(r, g, b uint8) string { return fmt.Sprintf("#%02x%02x%02x", r, g, b) } // ParseHexColor parses a hexadecimal color string and returns RGB values // Supports formats: #RGB, #RRGGBB, #RRGGBBAA // Returns error if the format is invalid func ParseHexColor(color string) (r, g, b, a uint8, err error) { if len(color) == 0 || color[0] != '#' { return 0, 0, 0, 255, fmt.Errorf("invalid color format: %s", color) } hex := color[1:] // Remove '#' prefix a = 255 // Default alpha // Helper to parse a component and chain errors parse := func(target *uint8, hexStr string) { if err != nil { return // Don't parse if a previous component failed } *target, err = hexToByte(hexStr) } switch len(hex) { case 3: // #RGB parse(&r, hex[0:1]+hex[0:1]) parse(&g, hex[1:2]+hex[1:2]) parse(&b, hex[2:3]+hex[2:3]) case 6: // #RRGGBB parse(&r, hex[0:2]) parse(&g, hex[2:4]) parse(&b, hex[4:6]) case 8: // #RRGGBBAA parse(&r, hex[0:2]) parse(&g, hex[2:4]) parse(&b, hex[4:6]) parse(&a, hex[6:8]) default: return 0, 0, 0, 255, fmt.Errorf("invalid hex color length: %s", color) } if err != nil { // Return zero values for color components on error, but keep default alpha return 0, 0, 0, 255, fmt.Errorf("failed to parse color '%s': %w", color, err) } return r, g, b, a, nil } // hexToByte converts a 2-character hex string to a byte value func hexToByte(hex string) (uint8, error) { if len(hex) != 2 { return 0, fmt.Errorf("invalid hex string length: expected 2 characters, got %d", len(hex)) } n, err := strconv.ParseUint(hex, 16, 8) if err != nil { return 0, fmt.Errorf("invalid hex value '%s': %w", hex, err) } return uint8(n), nil } // 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 := 0.0 // 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(0)), // Mid color (normal color with lightness 0.5) NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, config.ColorLightness.GetLightness(0.5)), // Light gray (grayscale with lightness 1) NewColorCorrectedHSL(restrictedHue, config.GrayscaleSaturation, config.GrayscaleLightness.GetLightness(1)), // Light color (normal color with lightness 1) NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, config.ColorLightness.GetLightness(1)), // Dark color (normal color with lightness 0) NewColorCorrectedHSL(restrictedHue, config.ColorSaturation, config.ColorLightness.GetLightness(0)), } }