package hexer import ( "errors" "fmt" "math" "strconv" "strings" ) type rgb struct{ r, g, b float64 } type hsl struct{ h, s, l float64 } func rgb2hex(c rgb) string { r := int(math.Round(c.r)) g := int(math.Round(c.g)) b := int(math.Round(c.b)) return fmt.Sprintf("#%.2x%.2x%.2x", r, g, b) } func hex2rgb(hex string) (rgb, error) { err := errors.New("invalid hex color") webc, ok := WebColors[strings.ToLower(hex)] if ok { hex = webc } hex = strings.TrimPrefix(hex, "#") if len(hex) != 6 { return rgb{}, err } r, errR := strconv.ParseUint(hex[:2], 16, 64) g, errG := strconv.ParseUint(hex[2:4], 16, 64) b, errB := strconv.ParseUint(hex[4:], 16, 64) if errR != nil || errG != nil || errB != nil { return rgb{}, err } return rgb{float64(r), float64(g), float64(b)}, nil } func hsl2hex(c hsl) string { return rgb2hex(hsl2rgb(c)) } func hex2hsl(hex string) (hsl, error) { rgb, err := hex2rgb(hex) return rgb2hsl(rgb), err } // https://en.wikipedia.org/wiki/HSL_and_HSV#General_approach func rgb2hsl(c rgb) hsl { r := c.r / 255 g := c.g / 255 b := c.b / 255 max := max(r, g, b) min := min(r, g, b) chroma := max - min var h, s, l float64 if chroma == 0 { h = 0 // by convention } else if max == r { // +6 to make sure the angle comes out positive h = math.Mod((g-b)/chroma+6, 6) } else if max == g { h = (b-r)/chroma + 2 } else { h = (r-g)/chroma + 4 } l = (max + min) / 2 if l == 1 || l == 0 { s = 0 } else { s = chroma / (1 - math.Abs(2*l-1)) } h = 60 * h s = 100 * s l = 100 * l return hsl{h, s, l} } // https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative func hsl2rgb(c hsl) rgb { h := c.h s := c.s / 100 l := c.l / 100 f := func(n float64) float64 { k := math.Mod(n+h/30, 12) a := s * min(l, 1-l) return l - a*max(-1, min(k-3, 9-k, 1)) } r := 255 * f(0) g := 255 * f(8) b := 255 * f(4) return rgb{r, g, b} }
// This source file contains the core functions behind hexer. All input and // output colors are in the hex format. package hexer import ( "errors" "math" ) // Luminance returns the relative Luminance of a color. // https://en.wikipedia.org/wiki/Relative_luminance func Luminance(c string) (float64, error) { rgb, err := hex2rgb(c) if err != nil { return 0, err } vr := rgb.r / 255 vg := rgb.g / 255 vb := rgb.b / 255 f := func(ch float64) float64 { if ch <= 0.04045 { return ch / 12.92 } return math.Pow((ch+0.055)/1.055, 2.4) } return 0.2126*f(vr) + 0.7152*f(vg) + 0.0722*f(vb), nil } // ContrastRatio returns the CR of 2 colors. func ContrastRatio(c1, c2 string) (float64, error) { l1, err := Luminance(c1) if err != nil { return 0, err } l2, err := Luminance(c2) if err != nil { return 0, err } if l1 > l2 { return (l1 + 0.05) / (l2 + 0.05), nil } return (l2 + 0.05) / (l1 + 0.05), nil } // Invert returns the inverse of a color and an error. func Invert(hex string) (string, error) { c, err := hex2rgb(hex) if err != nil { return "", err } c.r = 255 - c.r c.g = 255 - c.g c.b = 255 - c.b return rgb2hex(c), nil } // Red returns the red channel of a color and an error. func Red(hex string) (float64, error) { rgb, err := hex2rgb(hex) return rgb.r, err } // Green returns the green channel of a color and an error. func Green(hex string) (float64, error) { rgb, err := hex2rgb(hex) return rgb.g, err } // Blue returns the blue channel of a color and an error. func Blue(hex string) (float64, error) { rgb, err := hex2rgb(hex) return rgb.b, err } // Hue returns the hue channel of a color and an error. func Hue(hex string) (float64, error) { hsl, err := hex2hsl(hex) return hsl.h, err } // Saturation returns the saturation channel of a color and an error. func Saturation(hex string) (float64, error) { hsl, err := hex2hsl(hex) return hsl.s, err } // Lightness returns the lightness channel of a color and an error. func Lightness(hex string) (float64, error) { hsl, err := hex2hsl(hex) return hsl.l, err } // SetRed sets the red channel of a color to the provided value and returns the // result and an error. func SetRed(hex string, r float64) (string, error) { rgb, err := hex2rgb(hex) if err != nil { return "", err } if r < 0 || r > 255 { return "", errors.New("red out of range (0-255)") } rgb.r = r return rgb2hex(rgb), nil } // SetGreen sets the green channel of a color to the provided value and returns // the result and an error. func SetGreen(hex string, g float64) (string, error) { rgb, err := hex2rgb(hex) if err != nil { return "", err } if g < 0 || g > 255 { return "", errors.New("green out of range (0-255)") } rgb.g = g return rgb2hex(rgb), nil } // SetBlue sets the blue channel of a color to the provided value and returns // the result and an error. func SetBlue(hex string, b float64) (string, error) { rgb, err := hex2rgb(hex) if err != nil { return "", err } if b < 0 || b > 255 { return "", errors.New("blue out of range (0-255)") } rgb.b = b return rgb2hex(rgb), nil } // SetHue sets the hue channel of a color to the provided value and returns the // result and an error. func SetHue(hex string, h float64) (string, error) { hsl, err := hex2hsl(hex) if err != nil { return "", err } if h < 0 || h > 360 { return "", errors.New("hue out of range (0-360)") } hsl.h = h return hsl2hex(hsl), nil } // SetSaturation sets the saturation channel of a color to the provided value // and returns the result and an error. func SetSaturation(hex string, s float64) (string, error) { hsl, err := hex2hsl(hex) if err != nil { return "", err } if s < 0 || s > 100 { return "", errors.New("saturation out of range (0-100)") } hsl.s = s return hsl2hex(hsl), nil } // SetLightness sets the lightness channel of a color to the provided value and // returns the result and an error. func SetLightness(hex string, l float64) (string, error) { hsl, err := hex2hsl(hex) if err != nil { return "", err } if l < 0 || l > 100 { return "", errors.New("lightness out of range (0-100)") } hsl.l = l return hsl2hex(hsl), nil } // Lighten lightens a color by adding the provided value to its lightness // channel and returns the result and an error. func Lighten(hex string, a float64) (string, error) { if a < 0 { return "", errors.New("amount must be >= 0") } hsl, err := hex2hsl(hex) if err != nil { return "", err } l := hsl.l + a hsl.l = min(l, 100) return hsl2hex(hsl), nil } // Darken darkens a color by subtracting the provided value from its lightness // channel and returns the result and an error. func Darken(hex string, a float64) (string, error) { if a < 0 { return "", errors.New("amount must be >= 0") } hsl, err := hex2hsl(hex) if err != nil { return "", err } l := hsl.l - a hsl.l = max(l, 0) return hsl2hex(hsl), nil } // Saturate saturates a color by adding the provided value to its saturation // channel and returns the result and an error. func Saturate(hex string, a float64) (string, error) { if a < 0 { return "", errors.New("amount must be >= 0") } hsl, err := hex2hsl(hex) if err != nil { return "", err } s := hsl.s + a hsl.s = min(s, 100) return hsl2hex(hsl), nil } // Desaturate unsaturates a color by subtracting the provided value from its // saturation channel and returns the result and an error. func Desaturate(hex string, a float64) (string, error) { if a < 0 { return "", errors.New("amount must be >= 0") } hsl, err := hex2hsl(hex) if err != nil { return "", err } s := hsl.s - a hsl.s = max(s, 0) return hsl2hex(hsl), nil } // Mix mixes two colors using the given weight (of the first color) and returns // the result and an error. func Mix(c1, c2 string, w float64) (string, error) { rgb1, err := hex2rgb(c1) if err != nil { return "", err } rgb2, err := hex2rgb(c2) if err != nil { return "", err } if w < 0 || w > 1 { return "", errors.New("weight must be between 0 and 1") } rgbx := rgb{ w*rgb1.r + (1-w)*rgb2.r, w*rgb1.g + (1-w)*rgb2.g, w*rgb1.b + (1-w)*rgb2.b, } return rgb2hex(rgbx), nil } // MixPalette makes a slice of colors representing a palette of n equally // distanced colors between c1 and c2 (c1 and c2 included) and returns it with // an error. func MixPalette(c1, c2 string, n int) ([]string, error) { if n < 2 { return []string{}, errors.New("n must be >= 2") } palette := make([]string, n) for i := 0; i < n; i++ { w := float64(i) / float64(n-1) mix, err := Mix(c1, c2, w) if err != nil { return []string{}, err } palette[n-1-i] = mix } return palette, nil } // DarkPalette func DarkPalette(c string, n int) ([]string, error) { if n < 2 { return []string{}, errors.New("n must be >= 2") } hsl, err := hex2hsl(c) if err != nil { return []string{}, err } l0 := hsl.l palette := make([]string, n) for i := 0; i < n; i++ { l := l0 - float64(i)*l0/float64(n-1) hsl.l = l palette[i] = hsl2hex(hsl) } return palette, nil } // LightPalette func LightPalette(c string, n int) ([]string, error) { if n < 2 { return []string{}, errors.New("n must be >= 2") } hsl, err := hex2hsl(c) if err != nil { return []string{}, err } l0 := hsl.l palette := make([]string, n) for i := 0; i < n; i++ { l := l0 + float64(i)*(100-l0)/float64(n-1) hsl.l = l palette[i] = hsl2hex(hsl) } return palette, nil } // AdjustToContrastRatio adjusts the lightness of the color c1 so that the // contrast ratio between c1 and c2 is as close as possible to the given // contrast ratio cr0. func AdjustToContrastRatio(c1, c2 string, cr0 float64) (string, error) { const tolerance = 0.1 const adjustStep = 1 var adjust func(string, float64) (string, error) if cr0 < 1 || cr0 > 21 { return "", errors.New("invalid contrast ratio") } l1, err := Lightness(c1) if err != nil { return "", err } l2, err := Lightness(c2) if err != nil { return "", err } cr, _ := ContrastRatio(c1, c2) if (l1 > l2 && cr > cr0) || (l1 <= l2 && cr <= cr0) { adjust = Darken } else { adjust = Lighten } for math.Abs(cr0-cr) > tolerance { // fmt.Println(c1, c2, cr) // DEBUG: c1, _ = adjust(c1, adjustStep) prevCR := cr cr, _ = ContrastRatio(c1, c2) if prevCR == cr { break } } return c1, nil }
package hexer import ( "math" ) func rDist(c1, c2 rgb) float64 { return math.Abs(c1.r - c2.r) } func gDist(c1, c2 rgb) float64 { return math.Abs(c1.g - c2.g) } func bDist(c1, c2 rgb) float64 { return math.Abs(c1.b - c2.b) } func rgbDist(c1, c2 rgb) float64 { // normalize so that 100 becomes the max dist instead of 255 rdn := rDist(c1, c2) * 100 / 255 gdn := gDist(c1, c2) * 100 / 255 bdn := bDist(c1, c2) * 100 / 255 return math.Sqrt(rdn*rdn + gdn*gdn + bdn*bdn) } func sameRGB(c1, c2 rgb, eps float64) bool { return rgbDist(c1, c2) < eps } func hDist(c1, c2 hsl) float64 { return math.Abs(math.Mod(c1.h-c2.h, 360)) } func sDist(c1, c2 hsl) float64 { return math.Abs(c1.s - c2.s) } func lDist(c1, c2 hsl) float64 { return math.Abs(c1.l - c2.l) } func hslDist(c1, c2 hsl) float64 { // normalize so that 100 becomes the max dist instead of 360 hdn := hDist(c1, c2) * 100 / 360 sdn := sDist(c1, c2) ldn := lDist(c1, c2) return math.Sqrt(hdn*hdn + sdn*sdn + ldn*ldn) } func sameHSL(c1, c2 hsl, eps float64) bool { return hslDist(c1, c2) < eps } func sameHEX(c1, c2 string) bool { if c1 == c2 { return true } rgb1, err1 := hex2rgb(c1) rgb2, err2 := hex2rgb(c2) if err1 != nil || err2 != nil { return false } return sameRGB(rgb1, rgb2, 1) } func samePalette(p1, p2 []string) bool { l1, l2 := len(p1), len(p2) if l1 != l2 { return false } for i := 0; i < l1; i++ { if !sameHEX(p1[i], p2[i]) { return false } } return true }