summaryrefslogtreecommitdiff
path: root/internal/colors
diff options
context:
space:
mode:
Diffstat (limited to 'internal/colors')
-rw-r--r--internal/colors/cmyk.go9
-rw-r--r--internal/colors/colors_test.go142
-rw-r--r--internal/colors/hsl.go108
-rw-r--r--internal/colors/precise.go6
-rw-r--r--internal/colors/rgb.go9
5 files changed, 271 insertions, 3 deletions
diff --git a/internal/colors/cmyk.go b/internal/colors/cmyk.go
index 3460c4e..b5ef223 100644
--- a/internal/colors/cmyk.go
+++ b/internal/colors/cmyk.go
@@ -1,6 +1,9 @@
package colors
-import "math"
+import (
+ "fmt"
+ "math"
+)
type CMYK struct {
C int // 0-100
@@ -9,6 +12,10 @@ type CMYK struct {
K int // 0-100
}
+func (c CMYK) String() string {
+ return fmt.Sprintf("cmyk(%d, %d, %d, %d)", c.C, c.M, c.Y, c.K)
+}
+
func (c CMYK) ToPrecise() PreciseColor {
return PreciseColor{
R: (1 - float64(c.C)/100) * (1 - float64(c.K)/100),
diff --git a/internal/colors/colors_test.go b/internal/colors/colors_test.go
new file mode 100644
index 0000000..569d7e2
--- /dev/null
+++ b/internal/colors/colors_test.go
@@ -0,0 +1,142 @@
+package colors
+
+import (
+ "math"
+ "testing"
+)
+
+const (
+ PCmaxDelta = 1e-8
+ AssertTemplate = "Testing '%v'. Expected %v to become: %v Got: %v"
+)
+
+type equivalentColors struct {
+ name string
+ pc PreciseColor
+ rgb RGB
+ cmyk CMYK
+ hsl HSL
+}
+
+func pcDeltaOk(a, b PreciseColor) bool {
+ return math.Abs(a.R-b.R) < PCmaxDelta &&
+ math.Abs(a.G-b.G) < PCmaxDelta &&
+ math.Abs(a.B-b.B) < PCmaxDelta
+}
+
+func getEquivalents() []equivalentColors {
+ return []equivalentColors{
+ // Black & White {{{
+ {
+ "Pure White",
+ PreciseColor{1, 1, 1},
+ RGB{255, 255, 255},
+ CMYK{0, 0, 0, 0},
+ HSL{0, 0, 100},
+ },
+ {
+ "Pure Black",
+ PreciseColor{0, 0, 0},
+ RGB{0, 0, 0},
+ CMYK{0, 0, 0, 100},
+ HSL{0, 0, 0},
+ },
+ // }}}
+ // Pure RGB {{{
+ {
+ "Red",
+ PreciseColor{1, 0, 0},
+ RGB{255, 0, 0},
+ CMYK{0, 100, 100, 0},
+ HSL{0, 100, 50},
+ },
+ {
+ "Green",
+ PreciseColor{0, 1, 0},
+ RGB{0, 255, 0},
+ CMYK{100, 0, 100, 0},
+ HSL{120, 100, 50},
+ },
+ {
+ "Blue",
+ PreciseColor{0, 0, 1},
+ RGB{0, 0, 255},
+ CMYK{100, 100, 0, 0},
+ HSL{240, 100, 50},
+ },
+ // }}}
+ // Pure CMYK {{{
+ {
+ "Cyan",
+ PreciseColor{0, 1, 1},
+ RGB{0, 255, 255},
+ CMYK{100, 0, 0, 0},
+ HSL{180, 100, 50},
+ },
+ {
+ "Magenta",
+ PreciseColor{1, 0, 1},
+ RGB{255, 0, 255},
+ CMYK{0, 100, 0, 0},
+ HSL{300, 100, 50},
+ },
+ {
+ "Yellow",
+ PreciseColor{1, 1, 0},
+ RGB{255, 255, 0},
+ CMYK{0, 0, 100, 0},
+ HSL{60, 100, 50},
+ },
+ // note: Black is already tested
+ // }}}
+ // TODO: add less pure colors to test luminance and saturation better
+ }
+}
+
+func TestToPreciseColor(t *testing.T) {
+ for _, ce := range getEquivalents() {
+ target := ce.pc
+ for _, cs := range []ColorSpace{ce.rgb, ce.cmyk, ce.hsl} {
+ pc := cs.ToPrecise()
+ if !pcDeltaOk(pc, target) {
+ t.Errorf(AssertTemplate, ce.name, cs, target, pc)
+ }
+ }
+ }
+}
+
+func TestToRgb(t *testing.T) {
+ for _, ce := range getEquivalents() {
+ target := ce.rgb
+ for _, cs := range []ColorSpace{ce.pc, ce.cmyk, ce.hsl} {
+ rgb := RGB{}.FromPrecise(cs.ToPrecise()).(RGB)
+ if rgb != target {
+ t.Errorf(AssertTemplate, ce.name, cs, target, rgb)
+ }
+ }
+ }
+}
+
+func TestToCmyk(t *testing.T) {
+ for _, ce := range getEquivalents() {
+ target := ce.cmyk
+ for _, cs := range []ColorSpace{ce.pc, ce.rgb, ce.hsl} {
+ cmyk := CMYK{}.FromPrecise(cs.ToPrecise()).(CMYK)
+ if cmyk != target {
+ t.Errorf(AssertTemplate, ce.name, cs, target, cmyk)
+ }
+ }
+ }
+}
+
+func TestToHsl(t *testing.T) {
+ for _, ce := range getEquivalents() {
+ target := ce.hsl
+ for _, cs := range []ColorSpace{ce.pc, ce.rgb, ce.cmyk} {
+ hsl := HSL{}.FromPrecise(cs.ToPrecise()).(HSL)
+ if hsl != target {
+ t.Errorf(AssertTemplate, ce.name, cs, target, hsl)
+ }
+ }
+ }
+}
diff --git a/internal/colors/hsl.go b/internal/colors/hsl.go
index 7477042..2a31f81 100644
--- a/internal/colors/hsl.go
+++ b/internal/colors/hsl.go
@@ -1 +1,109 @@
package colors
+
+import (
+ "fmt"
+ "math"
+)
+
+type HSL struct {
+ H int // 0-360
+ S int // 0-100
+ L int // 0-100
+}
+
+func (h HSL) String() string {
+ return fmt.Sprintf("hsl(%d, %d, %d)", h.H, h.S, h.L)
+}
+
+func (h HSL) ToPrecise() PreciseColor {
+ // Normalize H, S, L
+ hue := float64(h.H) / 360.0
+ sat := float64(h.S) / 100.0
+ light := float64(h.L) / 100.0
+
+ var r, g, b float64
+
+ if sat == 0 {
+ // Achromatic case
+ r, g, b = light, light, light
+ } else {
+ var q float64
+ if light < 0.5 {
+ q = light * (1 + sat)
+ } else {
+ q = light + sat - (light * sat)
+ }
+ p := 2*light - q
+ r = hueToRGB(p, q, hue+1.0/3.0)
+ g = hueToRGB(p, q, hue)
+ b = hueToRGB(p, q, hue-1.0/3.0)
+ }
+
+ return PreciseColor{R: r, G: g, B: b}
+}
+
+func hueToRGB(p, q, t float64) float64 {
+ if t < 0 {
+ t += 1
+ }
+ if t > 1 {
+ t -= 1
+ }
+ if t < 1.0/6.0 {
+ return p + (q-p)*6*t
+ }
+ if t < 1.0/2.0 {
+ return q
+ }
+ if t < 2.0/3.0 {
+ return p + (q-p)*(2.0/3.0-t)*6
+ }
+ return p
+}
+
+func (h HSL) FromPrecise(p PreciseColor) ColorSpace {
+ r := p.R
+ g := p.G
+ b := p.B
+
+ max := math.Max(math.Max(r, g), b)
+ min := math.Min(math.Min(r, g), b)
+ delta := max - min
+
+ light := (max + min) / 2
+ var sat, hue float64
+
+ if delta == 0 {
+ // Achromatic case
+ hue, sat = 0, 0
+ } else {
+ if light < 0.5 {
+ sat = delta / (max + min)
+ } else {
+ sat = delta / (2 - max - min)
+ }
+
+ switch max {
+ case r:
+ hue = (g-b)/delta + (6 * boolToFloat64(g < b))
+ case g:
+ hue = (b-r)/delta + 2
+ case b:
+ hue = (r-g)/delta + 4
+ }
+ hue /= 6
+ }
+
+ return HSL{
+ H: int(math.Round(hue * 360)),
+ S: int(math.Round(sat * 100)),
+ L: int(math.Round(light * 100)),
+ }
+}
+
+func boolToFloat64(b bool) float64 {
+ if b {
+ return 1
+ }
+ return 0
+}
diff --git a/internal/colors/precise.go b/internal/colors/precise.go
index fafa104..a58d7c5 100644
--- a/internal/colors/precise.go
+++ b/internal/colors/precise.go
@@ -11,6 +11,10 @@ type ColorSpace interface {
FromPrecise(PreciseColor) ColorSpace
}
+func (c PreciseColor) String() string {
+ return fmt.Sprintf("PC(%.4f, %.4f, %.4f)", c.R, c.G, c.B)
+}
+
// PreciseColor is a color with floating point values for red, green, and blue.
// The extra precision minimizes rounding errors when converting between different
// color spaces. It is used as an intermediate representation when converting between
@@ -27,7 +31,7 @@ func (c PreciseColor) FromPrecise(p PreciseColor) ColorSpace {
return p
}
-func ToHexString(cs ColorSpace) string {
+func Hex(cs ColorSpace) string {
p := cs.ToPrecise()
return strings.ToUpper(fmt.Sprintf("#%02x%02x%02x",
diff --git a/internal/colors/rgb.go b/internal/colors/rgb.go
index e81aefc..0cc48da 100644
--- a/internal/colors/rgb.go
+++ b/internal/colors/rgb.go
@@ -1,6 +1,9 @@
package colors
-import "math"
+import (
+ "fmt"
+ "math"
+)
type RGB struct {
R int // 0-255
@@ -8,6 +11,10 @@ type RGB struct {
B int // 0-255
}
+func (c RGB) String() string {
+ return fmt.Sprintf("rgb(%d, %d, %d)", c.R, c.G, c.B)
+}
+
func (c RGB) ToPrecise() PreciseColor {
return PreciseColor{
R: float64(c.R) / 255,