package engine import ( "fmt" "math" "strings" ) // Default configuration constants matching JavaScript implementation const ( // Default saturation values defaultColorSaturation = 0.5 // Default saturation for colored shapes defaultGrayscaleSaturation = 0.0 // Default saturation for grayscale shapes // Default lightness range boundaries defaultColorLightnessMin = 0.4 // Default minimum lightness for colors defaultColorLightnessMax = 0.8 // Default maximum lightness for colors defaultGrayscaleLightnessMin = 0.3 // Default minimum lightness for grayscale defaultGrayscaleLightnessMax = 0.9 // Default maximum lightness for grayscale // Default padding defaultIconPadding = 0.08 // Default padding as percentage of icon size // Hue calculation constants hueIndexNormalizationFactor = 0.999 // Factor to normalize hue to [0,1) range for indexing degreesToTurns = 360.0 // Conversion factor from degrees to turns ) // ColorConfig represents the configuration for color generation type ColorConfig struct { // Saturation settings ColorSaturation float64 // Saturation for normal colors [0, 1] GrayscaleSaturation float64 // Saturation for grayscale colors [0, 1] // Lightness ranges ColorLightness LightnessRange // Lightness range for normal colors GrayscaleLightness LightnessRange // Lightness range for grayscale colors // Hue restrictions Hues []float64 // Allowed hues in degrees [0, 360] or range [0, 1]. Empty means no restriction // Background color BackColor *Color // Background color (nil for transparent) // Icon padding IconPadding float64 // Padding as percentage of icon size [0, 1] } // LightnessRange represents a range of lightness values type LightnessRange struct { Min float64 // Minimum lightness [0, 1] Max float64 // Maximum lightness [0, 1] } // GetLightness returns a lightness value for the given position in range [0, 1] // where 0 returns Min and 1 returns Max func (lr LightnessRange) GetLightness(value float64) float64 { // Clamp value to [0, 1] range value = clamp(value, 0, 1) // Linear interpolation between min and max result := lr.Min + value*(lr.Max-lr.Min) // Clamp result to valid lightness range return clamp(result, 0, 1) } // DefaultColorConfig returns the default configuration matching the JavaScript implementation func DefaultColorConfig() ColorConfig { return ColorConfig{ ColorSaturation: defaultColorSaturation, GrayscaleSaturation: defaultGrayscaleSaturation, ColorLightness: LightnessRange{Min: defaultColorLightnessMin, Max: defaultColorLightnessMax}, GrayscaleLightness: LightnessRange{Min: defaultGrayscaleLightnessMin, Max: defaultGrayscaleLightnessMax}, Hues: nil, // No hue restriction BackColor: nil, // Transparent background IconPadding: defaultIconPadding, } } // RestrictHue applies hue restrictions to the given hue value // Returns the restricted hue in range [0, 1] func (c ColorConfig) RestrictHue(originalHue float64) float64 { // Normalize hue to [0, 1) range hue := math.Mod(originalHue, 1.0) if hue < 0 { hue += 1.0 } // If no hue restrictions, return original if len(c.Hues) == 0 { return hue } // Find the closest allowed hue // originalHue is in range [0, 1], multiply by 0.999 to get range [0, 1) // then truncate to get index index := int((hueIndexNormalizationFactor * hue * float64(len(c.Hues)))) if index >= len(c.Hues) { index = len(c.Hues) - 1 } restrictedHue := c.Hues[index] // Convert from degrees to turns in range [0, 1) // Handle any turn - e.g. 746° is valid result := math.Mod(restrictedHue/degreesToTurns, 1.0) if result < 0 { result += 1.0 } return result } // Validate validates a ColorConfig to ensure all values are within valid ranges // Returns an error if any validation issues are found without correcting the values func (c *ColorConfig) Validate() error { var validationErrors []string // Validate saturation values if c.ColorSaturation < 0 || c.ColorSaturation > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: color saturation out of range: value %f not in [0, 1]", c.ColorSaturation)) } if c.GrayscaleSaturation < 0 || c.GrayscaleSaturation > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: grayscale saturation out of range: value %f not in [0, 1]", c.GrayscaleSaturation)) } // Validate lightness ranges if c.ColorLightness.Min < 0 || c.ColorLightness.Min > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: color lightness minimum out of range: value %f not in [0, 1]", c.ColorLightness.Min)) } if c.ColorLightness.Max < 0 || c.ColorLightness.Max > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: color lightness maximum out of range: value %f not in [0, 1]", c.ColorLightness.Max)) } if c.ColorLightness.Min > c.ColorLightness.Max { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: color lightness range invalid: minimum %f greater than maximum %f", c.ColorLightness.Min, c.ColorLightness.Max)) } if c.GrayscaleLightness.Min < 0 || c.GrayscaleLightness.Min > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: grayscale lightness minimum out of range: value %f not in [0, 1]", c.GrayscaleLightness.Min)) } if c.GrayscaleLightness.Max < 0 || c.GrayscaleLightness.Max > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: grayscale lightness maximum out of range: value %f not in [0, 1]", c.GrayscaleLightness.Max)) } if c.GrayscaleLightness.Min > c.GrayscaleLightness.Max { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: grayscale lightness range invalid: minimum %f greater than maximum %f", c.GrayscaleLightness.Min, c.GrayscaleLightness.Max)) } // Validate icon padding if c.IconPadding < 0 || c.IconPadding > 1 { validationErrors = append(validationErrors, fmt.Sprintf("jdenticon: engine: validation failed: icon padding out of range: value %f not in [0, 1]", c.IconPadding)) } if len(validationErrors) > 0 { return fmt.Errorf("jdenticon: engine: validation failed: configuration invalid: %s", strings.Join(validationErrors, "; ")) } return nil } // Normalize validates and corrects a ColorConfig to ensure all values are within valid ranges // This method provides backward compatibility by applying corrections for invalid values func (c *ColorConfig) Normalize() { // Clamp saturation values c.ColorSaturation = clamp(c.ColorSaturation, 0, 1) c.GrayscaleSaturation = clamp(c.GrayscaleSaturation, 0, 1) // Validate and fix lightness ranges c.ColorLightness.Min = clamp(c.ColorLightness.Min, 0, 1) c.ColorLightness.Max = clamp(c.ColorLightness.Max, 0, 1) if c.ColorLightness.Min > c.ColorLightness.Max { c.ColorLightness.Min, c.ColorLightness.Max = c.ColorLightness.Max, c.ColorLightness.Min } c.GrayscaleLightness.Min = clamp(c.GrayscaleLightness.Min, 0, 1) c.GrayscaleLightness.Max = clamp(c.GrayscaleLightness.Max, 0, 1) if c.GrayscaleLightness.Min > c.GrayscaleLightness.Max { c.GrayscaleLightness.Min, c.GrayscaleLightness.Max = c.GrayscaleLightness.Max, c.GrayscaleLightness.Min } // Clamp icon padding c.IconPadding = clamp(c.IconPadding, 0, 1) }