diff --git a/image/color/hsv.go b/image/color/hsv.go new file mode 100644 index 0000000..a3c7957 --- /dev/null +++ b/image/color/hsv.go @@ -0,0 +1,177 @@ +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 +} +