From 2546c338ad8d0cc9ab950b319d99423493f7fdcf Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 15 Aug 2024 16:41:22 -0400 Subject: [PATCH] Separate HSVA color into HSV, HSVA, fix alpha premultiplication --- internal/color.go | 127 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 29 deletions(-) diff --git a/internal/color.go b/internal/color.go index 32b9845..3c7ac2f 100644 --- a/internal/color.go +++ b/internal/color.go @@ -1,13 +1,31 @@ package internal +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 uint8 + A uint16 } -func (hsva HSVA) RGBA () (r, g, b, a uint32) { +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 @@ -15,34 +33,55 @@ func (hsva HSVA) RGBA () (r, g, b, a uint32) { return uint32(float64(0xFFFF) * x) } - ca := uint32(hsva.A) << 8 - s := clamp01(hsva.S) - v := clamp01(hsva.V) + s := clamp01(hsv.S) + v := clamp01(hsv.V) if s == 0 { light := component(v) - return light, light, light, ca + return light, light, light, 0xFFFF } - h := clamp01(hsva.H) * 360 + h := clamp01(hsv.H) * 360 sector := int(h / 60) offset := (h / 60) - float64(sector) - fac := float64(hsva.A) / 255 - p := component(fac * v * (1 - s)) - q := component(fac * v * (1 - s * offset)) - t := component(fac * v * (1 - s * (1 - offset))) + 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, ca - case 1: return q, va, p, ca - case 2: return p, va, t, ca - case 3: return p, q, va, ca - case 4: return t, p, va, ca - default: return va, p, q, ca + 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 { @@ -58,7 +97,38 @@ func clamp01 (x float64) float64 { return x } -func RGBAToHSVA (r, g, b, a uint32) HSVA { +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 @@ -78,27 +148,26 @@ func RGBAToHSVA (r, g, b, a uint32) HSVA { if cg < minComponent { minComponent = cg } if cb < minComponent { minComponent = cb } - hsva := HSVA { + hsv := HSV { V: maxComponent, - A: uint8(a >> 8), } delta := maxComponent - minComponent if delta == 0 { // hsva.S is undefined, so hue doesn't matter - return hsva + return hsv } - hsva.S = delta / maxComponent + hsv.S = delta / maxComponent switch { - case cr == maxComponent: hsva.H = (cg - cb) / delta - case cg == maxComponent: hsva.H = 2 + (cb - cr) / delta - case cb == maxComponent: hsva.H = 4 + (cr - cg) / delta + 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 } - hsva.H *= 60 - if hsva.H < 0 { hsva.H += 360 } - hsva.H /= 360 + hsv.H *= 60 + if hsv.H < 0 { hsv.H += 360 } + hsv.H /= 360 - return hsva + return hsv }