package color import "fmt" import "image/color" // HSV represents a color with hue, saturation, and value components. Each // component C is in range 0 <= C <= 1. type HSV struct { H float64 S float64 V float64 } // HSVA is an HSV color with an added 8-bit alpha component. The alpha component // ranges from 0x0000 (fully transparent) to 0xFFFF (opaque), and has no bearing // on the other components. type HSVA struct { H float64 S float64 V float64 A uint16 } var ( HSVModel color.Model = color.ModelFunc(hsvModel) HSVAModel color.Model = color.ModelFunc(hsvaModel) ) func (hsv HSV) RGBA () (r, g, b, a uint32) { // Adapted from: // https://www.cs.rit.edu/~ncs/color/t_convert.html component := func (x float64) uint32 { return uint32(float64(0xFFFF) * x) } s := clamp01(hsv.S) v := clamp01(hsv.V) if s == 0 { light := component(v) return light, light, light, 0xFFFF } h := clamp01(hsv.H) * 360 sector := int(h / 60) // otherwise when given 1.0 for H, sector would overflow to 6 if sector > 5 { sector = 5 } offset := (h / 60) - float64(sector) p := component(v * (1 - s)) q := component(v * (1 - s * offset)) t := component(v * (1 - s * (1 - offset))) va := component(v) switch sector { case 0: return va, t, p, 0xFFFF case 1: return q, va, p, 0xFFFF case 2: return p, va, t, 0xFFFF case 3: return p, q, va, 0xFFFF case 4: return t, p, va, 0xFFFF default: return va, p, q, 0xFFFF } } func (hsva HSVA) RGBA () (r, g, b, a uint32) { r, g, b, a = HSV { H: hsva.H, S: hsva.S, V: hsva.V, }.RGBA() a = uint32(hsva.A) // alpha premultiplication r = (r * a) / 0xFFFF g = (g * a) / 0xFFFF b = (b * a) / 0xFFFF return } // Canon returns the color but with the H, S, and V fields are constrained to // the range 0.0-1.0 func (hsv HSV) Canon () HSV { hsv.H = clamp01(hsv.H) hsv.S = clamp01(hsv.S) hsv.V = clamp01(hsv.V) return hsv } // Canon returns the color but with the H, S, and V fields are constrained to // the range 0.0-1.0 func (hsva HSVA) Canon () HSVA { hsva.H = clamp01(hsva.H) hsva.S = clamp01(hsva.S) hsva.V = clamp01(hsva.V) return hsva } func clamp01 (x float64) float64 { if x > 1.0 { return 1.0 } if x < 0.0 { return 0.0 } return x } func hsvModel (c color.Color) color.Color { switch c := c.(type) { case HSV: return c case HSVA: return HSV { H: c.H, S: c.S, V: c.V } default: r, g, b, a := c.RGBA() // alpha unpremultiplication r = (r / a) * 0xFFFF g = (g / a) * 0xFFFF b = (b / a) * 0xFFFF return rgbToHSV(r, g, b) } } func hsvaModel (c color.Color) color.Color { switch c := c.(type) { case HSV: return HSVA { H: c.H, S: c.S, V: c.V, A: 0xFFFF } case HSVA: return c default: r, g, b, a := c.RGBA() hsv := rgbToHSV(r, g, b) return HSVA { H: hsv.H, S: hsv.S, V: hsv.V, A: uint16(a), } } } func rgbToHSV (r, g, b uint32) HSV { // Adapted from: // https://www.cs.rit.edu/~ncs/color/t_convert.html component := func (x uint32) float64 { return clamp01(float64(x) / 0xFFFF) } cr := component(r) cg := component(g) cb := component(b) var maxComponent float64 if cr > maxComponent { maxComponent = cr } if cg > maxComponent { maxComponent = cg } if cb > maxComponent { maxComponent = cb } var minComponent = 1.0 if cr < minComponent { minComponent = cr } if cg < minComponent { minComponent = cg } if cb < minComponent { minComponent = cb } hsv := HSV { V: maxComponent, } delta := maxComponent - minComponent if delta == 0 { // hsva.S is undefined, so hue doesn't matter return hsv } hsv.S = delta / maxComponent switch { case cr == maxComponent: hsv.H = (cg - cb) / delta case cg == maxComponent: hsv.H = 2 + (cb - cr) / delta case cb == maxComponent: hsv.H = 4 + (cr - cg) / delta } hsv.H *= 60 if hsv.H < 0 { hsv.H += 360 } hsv.H /= 360 return hsv } // FormatNRGBA formats an NRGBA value into a hex string. func FormatNRGBA (nrgba color.NRGBA) string { return fmt.Sprintf("%02X%02X%02X%02X", nrgba.R, nrgba.G, nrgba.B, nrgba.A) } // ParseNRGBA parses an NRGBA value from a hex string. It can be of the format: // - RGB // - RGBA // - RRGGBB // - RRGGBBAA // If none of these are specified, this function will return an opaque black // color. Hex digits may either be upper case or lower case. func ParseNRGBA (str string) color.NRGBA { runes := []rune(str) c := color.NRGBA { A: 255 } switch len(runes) { case 3: c.R = fillOctet(hexDigit(runes[0])) c.G = fillOctet(hexDigit(runes[1])) c.B = fillOctet(hexDigit(runes[2])) case 4: c.R = fillOctet(hexDigit(runes[0])) c.G = fillOctet(hexDigit(runes[1])) c.B = fillOctet(hexDigit(runes[2])) c.A = fillOctet(hexDigit(runes[3])) case 6: c.R = hexOctet(runes[0], runes[1]) c.G = hexOctet(runes[2], runes[3]) c.B = hexOctet(runes[4], runes[5]) case 8: c.R = hexOctet(runes[0], runes[1]) c.G = hexOctet(runes[2], runes[3]) c.B = hexOctet(runes[4], runes[5]) c.A = hexOctet(runes[6], runes[7]) } return c } func hexDigit (r rune) uint8 { switch { case r >= '0' && r <= '9': return uint8(r - '0') case r >= 'A' && r <= 'F': return uint8(r - 'A') + 10 case r >= 'a' && r <= 'f': return uint8(r - 'a') + 10 default: return 0 } } func fillOctet (low uint8) uint8 { return low << 4 | low } func hexOctet (high, low rune) uint8 { return hexDigit(high) << 4 | hexDigit(low) }