From f75ff8da5a12c3ebd4f1543da416aaf120e78695 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Mar 2025 14:24:41 +0100 Subject: [PATCH] Added HSL and HSV colors --- Benchmarks_test.go | 8 ++++---- Color.go | 14 +++++++------- Color_test.go | 6 ++++++ HSL.go | 29 +++++++++++++++++++++++++++++ HSL_test.go | 26 ++++++++++++++++++++++++++ HSV.go | 29 +++++++++++++++++++++++++++++ HSV_test.go | 26 ++++++++++++++++++++++++++ LCH.go | 7 +------ LCH_test.go | 10 ++++++---- README.md | 14 +++++++++----- RGB.go | 8 ++++++-- RGB_test.go | 13 +------------ go.mod | 5 +---- go.sum | 6 ++---- 14 files changed, 153 insertions(+), 48 deletions(-) create mode 100644 HSL.go create mode 100644 HSL_test.go create mode 100644 HSV.go create mode 100644 HSV_test.go diff --git a/Benchmarks_test.go b/Benchmarks_test.go index 64ee417..c811429 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -7,13 +7,13 @@ import ( ) func BenchmarkRGB(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { color.RGB(1.0, 1.0, 1.0) } } func BenchmarkLCH(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { color.LCH(0.5, 0.5, 0.0) } } @@ -22,7 +22,7 @@ func BenchmarkPrint(b *testing.B) { color.Terminal = true c := color.RGB(1.0, 1.0, 1.0) - for i := 0; i < b.N; i++ { + for b.Loop() { c.Print("") } } @@ -31,7 +31,7 @@ func BenchmarkPrintRaw(b *testing.B) { color.Terminal = false c := color.RGB(1.0, 1.0, 1.0) - for i := 0; i < b.N; i++ { + for b.Loop() { c.Print("") } } diff --git a/Color.go b/Color.go index cb83a0a..5b007c9 100644 --- a/Color.go +++ b/Color.go @@ -4,11 +4,11 @@ import ( "fmt" ) -// Color represents an RGB color. +// Color represents an sRGB color. type Color struct { - R Value - G Value - B Value + R byte + G byte + B byte } // Print writes the text in the given color to standard output. @@ -18,7 +18,7 @@ func (c Color) Print(args ...any) { return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.R, c.G, c.B, fmt.Sprint(args...)) } // Printf formats according to a format specifier and writes the text in the given color to standard output. @@ -28,7 +28,7 @@ func (c Color) Printf(format string, args ...any) { return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprintf(format, args...)) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.R, c.G, c.B, fmt.Sprintf(format, args...)) } // Println writes the text in the given color to standard output and appends a newline. @@ -38,5 +38,5 @@ func (c Color) Println(args ...any) { return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\n\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\n\x1b[0m", c.R, c.G, c.B, fmt.Sprint(args...)) } diff --git a/Color_test.go b/Color_test.go index da84c14..1abfa15 100644 --- a/Color_test.go +++ b/Color_test.go @@ -8,12 +8,14 @@ import ( func TestPrint(t *testing.T) { color.Terminal = true + color.TrueColor = true color.RGB(1, 0, 0).Print("red\n") color.RGB(0, 1, 0).Print("green\n") color.RGB(0, 0, 1).Print("blue\n") color.Terminal = false + color.TrueColor = false color.RGB(1, 0, 0).Print("red\n") color.RGB(0, 1, 0).Print("green\n") @@ -22,12 +24,14 @@ func TestPrint(t *testing.T) { func TestPrintf(t *testing.T) { color.Terminal = true + color.TrueColor = true color.RGB(1, 0, 0).Printf("%s\n", "red") color.RGB(0, 1, 0).Printf("%s\n", "green") color.RGB(0, 0, 1).Printf("%s\n", "blue") color.Terminal = false + color.TrueColor = false color.RGB(1, 0, 0).Printf("%s\n", "red") color.RGB(0, 1, 0).Printf("%s\n", "green") @@ -36,12 +40,14 @@ func TestPrintf(t *testing.T) { func TestPrintln(t *testing.T) { color.Terminal = true + color.TrueColor = true color.RGB(1, 0, 0).Println("red") color.RGB(0, 1, 0).Println("green") color.RGB(0, 0, 1).Println("blue") color.Terminal = false + color.TrueColor = false color.RGB(1, 0, 0).Println("red") color.RGB(0, 1, 0).Println("green") diff --git a/HSL.go b/HSL.go new file mode 100644 index 0000000..f5cda9a --- /dev/null +++ b/HSL.go @@ -0,0 +1,29 @@ +package color + +import "math" + +// HSL represents a color using hue, saturation and lightness. +func HSL(hue Value, saturation Value, lightness Value) Color { + hue = math.Mod(hue, 360) + c := (1 - math.Abs(2*lightness-1)) * saturation + x := c * (1 - math.Abs(math.Mod(hue/60, 2)-1)) + var r, g, b Value + + switch { + case hue >= 0 && hue < 60: + r, g, b = c, x, 0 + case hue >= 60 && hue < 120: + r, g, b = x, c, 0 + case hue >= 120 && hue < 180: + r, g, b = 0, c, x + case hue >= 180 && hue < 240: + r, g, b = 0, x, c + case hue >= 240 && hue < 300: + r, g, b = x, 0, c + case hue >= 300 && hue < 360: + r, g, b = c, 0, x + } + + m := lightness - c/2 + return RGB(r+m, g+m, b+m) +} diff --git a/HSL_test.go b/HSL_test.go new file mode 100644 index 0000000..0c3e654 --- /dev/null +++ b/HSL_test.go @@ -0,0 +1,26 @@ +package color_test + +import ( + "fmt" + "testing" + + "git.urbach.dev/go/color" +) + +func TestHSLSpectrum(t *testing.T) { + color.Terminal = true + color.TrueColor = true + + for lightness := range 21 { + for hue := range 80 { + h := color.Value(hue) * 4.4 + s := color.Value(1.0) + l := color.Value(lightness) * 0.05 + + c := color.HSL(h, s, l) + c.Print("█") + } + + fmt.Println() + } +} diff --git a/HSV.go b/HSV.go new file mode 100644 index 0000000..2efa5fd --- /dev/null +++ b/HSV.go @@ -0,0 +1,29 @@ +package color + +import "math" + +// HSV represents a color using hue, saturation and value. +func HSV(hue Value, saturation Value, value Value) Color { + hue = math.Mod(hue, 360) + c := value * saturation + x := c * (1 - math.Abs(math.Mod(hue/60, 2)-1)) + var r, g, b Value + + switch { + case hue >= 0 && hue < 60: + r, g, b = c, x, 0 + case hue >= 60 && hue < 120: + r, g, b = x, c, 0 + case hue >= 120 && hue < 180: + r, g, b = 0, c, x + case hue >= 180 && hue < 240: + r, g, b = 0, x, c + case hue >= 240 && hue < 300: + r, g, b = x, 0, c + case hue >= 300 && hue < 360: + r, g, b = c, 0, x + } + + m := value - c + return RGB(r+m, g+m, b+m) +} diff --git a/HSV_test.go b/HSV_test.go new file mode 100644 index 0000000..c2b4bd3 --- /dev/null +++ b/HSV_test.go @@ -0,0 +1,26 @@ +package color_test + +import ( + "fmt" + "testing" + + "git.urbach.dev/go/color" +) + +func TestHSVSpectrum(t *testing.T) { + color.Terminal = true + color.TrueColor = true + + for value := range 21 { + for hue := range 80 { + h := color.Value(hue) * 4.4 + s := color.Value(1.0) + v := color.Value(value) * 0.05 + + c := color.HSV(h, s, v) + c.Print("█") + } + + fmt.Println() + } +} diff --git a/LCH.go b/LCH.go index fb2b662..c27d4df 100644 --- a/LCH.go +++ b/LCH.go @@ -19,12 +19,7 @@ func LCH(lightness Value, chroma Value, hue Value) Color { b *= chroma r, g, b := oklabToLinearRGB(lightness, a, b) - - r = sRGB(r) - g = sRGB(g) - b = sRGB(b) - - return Color{r, g, b} + return RGB(r, g, b) } // findChromaInSRGB tries to find the closest chroma that can be represented in sRGB color space. diff --git a/LCH_test.go b/LCH_test.go index 3cd09e0..59e9097 100644 --- a/LCH_test.go +++ b/LCH_test.go @@ -25,20 +25,22 @@ func TestLCH(t *testing.T) { } for name, c := range lchColors { - testColorRange(t, c) c.Println("█ " + name) } } func TestLCHSpectrum(t *testing.T) { color.Terminal = true + color.TrueColor = true for chroma := range 4 { for lightness := range 21 { for hue := range 80 { - c := color.LCH(color.Value(lightness)*0.05, color.Value(chroma)*0.05, color.Value(hue)*4.4) - testColorRange(t, c) - c.Print("█") + l := color.Value(lightness) * 0.05 + c := color.Value(chroma) * 0.05 + h := color.Value(hue) * 4.4 + col := color.LCH(l, c, h) + col.Print("█") } fmt.Println() diff --git a/README.md b/README.md index 40cd44e..56fe49a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Adds color to your terminal output. ## Features - ANSI colors +- HSL colors +- HSV colors - LCH colors - RGB colors @@ -35,6 +37,8 @@ blue.Println("blue text") PASS: TestPrint PASS: TestPrintf PASS: TestPrintln +PASS: TestHSLSpectrum +PASS: TestHSVSpectrum PASS: TestLCH PASS: TestLCHSpectrum PASS: TestRGB @@ -44,10 +48,10 @@ coverage: 100.0% of statements ## Benchmarks ``` -BenchmarkRGB-12 1000000000 0.3141 ns/op 0 B/op 0 allocs/op -BenchmarkLCH-12 4780176 250.9 ns/op 0 B/op 0 allocs/op -BenchmarkPrint-12 375394 3343 ns/op 0 B/op 0 allocs/op -BenchmarkPrintRaw-12 3105022 387.3 ns/op 0 B/op 0 allocs/op +BenchmarkRGB-20 100000000 14.88 ns/op 0 B/op 0 allocs/op +BenchmarkLCH-20 5075756 227.8 ns/op 0 B/op 0 allocs/op +BenchmarkPrint-20 1587134 755.5 ns/op 0 B/op 0 allocs/op +BenchmarkPrintRaw-20 3166090 361.5 ns/op 0 B/op 0 allocs/op ``` ## License @@ -56,4 +60,4 @@ Please see the [license documentation](https://urbach.dev/license). ## Copyright -© 2024 Eduard Urbach +© 2024 Eduard Urbach \ No newline at end of file diff --git a/RGB.go b/RGB.go index f882f07..3d94abc 100644 --- a/RGB.go +++ b/RGB.go @@ -2,9 +2,13 @@ package color import "math" -// RGB creates a new color with red, green and blue values in the range of 0.0 to 1.0. +// RGB creates a new sRGB color. func RGB(r Value, g Value, b Value) Color { - return Color{r, g, b} + return Color{ + byte(sRGB(r) * 255), + byte(sRGB(g) * 255), + byte(sRGB(b) * 255), + } } // inSRGB indicates whether the given color can be mapped to the sRGB color space. diff --git a/RGB_test.go b/RGB_test.go index 56ed903..a1f4481 100644 --- a/RGB_test.go +++ b/RGB_test.go @@ -3,12 +3,12 @@ package color_test import ( "testing" - "git.urbach.dev/go/assert" "git.urbach.dev/go/color" ) func TestRGB(t *testing.T) { color.Terminal = true + color.TrueColor = true rgbColors := map[string]color.Color{ "black": color.RGB(0, 0, 0), @@ -24,17 +24,6 @@ func TestRGB(t *testing.T) { } for name, c := range rgbColors { - testColorRange(t, c) c.Println("█ " + name) } } - -func testColorRange(t *testing.T, c color.Color) { - assert.True(t, c.R >= 0.0) - assert.True(t, c.G >= 0.0) - assert.True(t, c.B >= 0.0) - - assert.True(t, c.R <= 1.0) - assert.True(t, c.G <= 1.0) - assert.True(t, c.B <= 1.0) -} diff --git a/go.mod b/go.mod index f5b705c..c3152ec 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module git.urbach.dev/go/color go 1.24 -require ( - git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf - golang.org/x/sys v0.30.0 -) +require golang.org/x/sys v0.31.0 diff --git a/go.sum b/go.sum index c5592cb..c55261f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE= -git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=