diff --git a/Benchmarks_test.go b/Benchmarks_test.go index fdbb35c..630f902 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -13,7 +13,23 @@ func BenchmarkRGB(b *testing.B) { } } -func BenchmarkFprint(b *testing.B) { +func BenchmarkLCH(b *testing.B) { + for i := 0; i < b.N; i++ { + color.LCH(0.5, 0.5, 0.0) + } +} + +func BenchmarkFprintColorized(b *testing.B) { + color.Terminal = true + c := color.RGB(1.0, 1.0, 1.0) + + for i := 0; i < b.N; i++ { + c.Fprint(io.Discard, "") + } +} + +func BenchmarkFprintRaw(b *testing.B) { + color.Terminal = false c := color.RGB(1.0, 1.0, 1.0) for i := 0; i < b.N; i++ { diff --git a/Color.go b/Color.go index 81b7e32..3ffa1dc 100644 --- a/Color.go +++ b/Color.go @@ -5,18 +5,18 @@ import ( "io" ) -// Component is a type definition for the data type of a single color component. -type Component float32 +// Value is a type definition for the data type of a single color component. +type Value = float64 // Color represents an RGB color. type Color struct { - R Component - G Component - B Component + R Value + G Value + B Value } // RGB creates a new color with red, green and blue values in the range of 0.0 to 1.0. -func RGB(r Component, g Component, b Component) Color { +func RGB(r Value, g Value, b Value) Color { return Color{r, g, b} } diff --git a/Color_test.go b/Color_test.go index c61fcaa..8f259a8 100644 --- a/Color_test.go +++ b/Color_test.go @@ -1,6 +1,7 @@ package color_test import ( + "fmt" "io" "testing" @@ -8,36 +9,73 @@ import ( "git.akyoto.dev/go/color" ) -func TestColors(t *testing.T) { - color.Terminal = true - testColors(t) -} - func TestNoColors(t *testing.T) { color.Terminal = false - testColors(t) + testRGBColors(t) + testLCHColors(t) } -func testColors(t *testing.T) { - colors := map[string]color.Color{ - "black": color.RGB(0.0, 0.0, 0.0), - "white": color.RGB(1.0, 1.0, 1.0), - "red": color.RGB(1.0, 0.0, 0.0), - "green": color.RGB(0.0, 1.0, 0.0), - "blue": color.RGB(0.0, 0.0, 1.0), - } +func TestColors(t *testing.T) { + color.Terminal = true + testRGBColors(t) + testLCHColors(t) +} - for name, value := range colors { - assert.True(t, value.R >= 0.0) - assert.True(t, value.G >= 0.0) - assert.True(t, value.B >= 0.0) +func TestRainbow(t *testing.T) { + color.Terminal = true - assert.True(t, value.R <= 1.0) - assert.True(t, value.G <= 1.0) - assert.True(t, value.B <= 1.0) + for chroma := range 5 { + 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.5) + c.Print("█") + } - value.Fprint(io.Discard, name) - value.Print(name) - value.Println(name) + fmt.Println() + } + + fmt.Println() } } + +func testRGBColors(t *testing.T) { + rgbColors := map[string]color.Color{ + "RGB.black": color.RGB(0.0, 0.0, 0.0), + "RGB.white": color.RGB(1.0, 1.0, 1.0), + "RGB.red": color.RGB(1.0, 0.0, 0.0), + "RGB.green": color.RGB(0.0, 1.0, 0.0), + "RGB.blue": color.RGB(0.0, 0.0, 1.0), + } + + for name, c := range rgbColors { + testColor(t, c, name) + } +} + +func testLCHColors(t *testing.T) { + lchColors := map[string]color.Color{ + "LCH.black": color.LCH(0.0, 0.0, 0.0), + "LCH.white": color.LCH(1.0, 0.0, 0.0), + "LCH.red": color.LCH(0.6, 0.5, 20.0), + "LCH.green": color.LCH(0.6, 0.5, 161.0), + "LCH.blue": color.LCH(0.6, 0.5, 240.0), + } + + for name, c := range lchColors { + testColor(t, c, name) + } +} + +func testColor(t *testing.T, c color.Color, name string) { + 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) + + c.Fprint(io.Discard, name) + c.Print(name) + c.Println(name) +} diff --git a/LCH.go b/LCH.go new file mode 100644 index 0000000..f8565ef --- /dev/null +++ b/LCH.go @@ -0,0 +1,56 @@ +package color + +import ( + "math" +) + +// LCH represents a color using lightness, chroma and hue (oklch). +func LCH(lightness Value, chroma Value, hue Value) Color { + hue *= math.Pi / 180.0 + a := chroma * math.Cos(hue) + b := chroma * math.Sin(hue) + + r, g, b := labToRGB(lightness, a, b) + + r = gammaCorrection(r) + g = gammaCorrection(g) + b = gammaCorrection(b) + + return Color{r, g, b} +} + +// LAB to linear sRGB +// Source: https://bottosson.github.io/posts/oklab/ +func labToRGB(lightness Value, a Value, b Value) (Value, Value, Value) { + l := lightness + 0.3963377773761749*a + 0.21580375730991364*b + m := lightness - 0.10556134581565857*a - 0.0638541728258133*b + s := lightness - 0.08948418498039246*a - 1.2914855480194092*b + + l = l * l * l + m = m * m * m + s = s * s * s + + red := 4.0767416621*l - 3.3077115913*m + 0.2309699292*s + green := -1.2684380046*l + 2.6097574011*m - 0.3413193965*s + blue := -0.0041960863*l - 0.7034186147*m + 1.7076147010*s + + return red, green, blue +} + +// Gamma correction +// Source: IEC 61966-2-2 +func gammaCorrection(x Value) Value { + if x < 0 { + return 0 + } + + if x <= 0.0031308 { + return 12.92 * x + } + + if x < 1.0 { + return 1.055*math.Pow(x, 1/2.4) - 0.055 + } + + return 1.0 +} diff --git a/README.md b/README.md index 2a72784..f817888 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Adds color to your terminal output. ## Features -- Truecolor output +- RGB color space +- OKLCH color space +- Truecolor terminal output ## Installation @@ -22,15 +24,19 @@ red.Println("red text") ## Tests ``` -PASS: TestColor +PASS: TestNoColors +PASS: TestColors +PASS: TestRainbow coverage: 100.0% of statements ``` ## Benchmarks ``` -BenchmarkRGB-12 1000000000 0.3139 ns/op 0 B/op 0 allocs/op -BenchmarkFprint-12 25758495 45.38 ns/op 0 B/op 0 allocs/op +BenchmarkRGB-12 1000000000 0.3132 ns/op 0 B/op 0 allocs/op +BenchmarkLCH-12 11214220 107.1 ns/op 0 B/op 0 allocs/op +BenchmarkFprintColorized-12 6356535 188.4 ns/op 0 B/op 0 allocs/op +BenchmarkFprintRaw-12 27374659 43.76 ns/op 0 B/op 0 allocs/op ``` ## License