Improved LCH support
This commit is contained in:
parent
30838ca0b6
commit
dae9993a96
@ -1,7 +1,6 @@
|
|||||||
package color_test
|
package color_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -9,64 +8,71 @@ import (
|
|||||||
"git.akyoto.dev/go/color"
|
"git.akyoto.dev/go/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNoColors(t *testing.T) {
|
func TestFprint(t *testing.T) {
|
||||||
|
color.Terminal = true
|
||||||
|
|
||||||
|
color.RGB(1, 0, 0).Fprint(io.Discard, "red")
|
||||||
|
color.RGB(0, 1, 0).Fprint(io.Discard, "green")
|
||||||
|
color.RGB(0, 0, 1).Fprint(io.Discard, "blue")
|
||||||
|
|
||||||
color.Terminal = false
|
color.Terminal = false
|
||||||
testRGBColors(t)
|
|
||||||
testLCHColors(t)
|
color.RGB(1, 0, 0).Fprint(io.Discard, "red")
|
||||||
|
color.RGB(0, 1, 0).Fprint(io.Discard, "green")
|
||||||
|
color.RGB(0, 0, 1).Fprint(io.Discard, "blue")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColors(t *testing.T) {
|
func TestPrint(t *testing.T) {
|
||||||
color.Terminal = true
|
|
||||||
testRGBColors(t)
|
|
||||||
testLCHColors(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRainbow(t *testing.T) {
|
|
||||||
color.Terminal = true
|
color.Terminal = true
|
||||||
|
|
||||||
for chroma := range 5 {
|
color.RGB(1, 0, 0).Print("red\n")
|
||||||
for lightness := range 21 {
|
color.RGB(0, 1, 0).Print("green\n")
|
||||||
for hue := range 80 {
|
color.RGB(0, 0, 1).Print("blue\n")
|
||||||
c := color.LCH(color.Value(lightness)*0.05, color.Value(chroma)*0.05, color.Value(hue)*4.5)
|
|
||||||
c.Print("█")
|
color.Terminal = false
|
||||||
|
|
||||||
|
color.RGB(1, 0, 0).Print("red\n")
|
||||||
|
color.RGB(0, 1, 0).Print("green\n")
|
||||||
|
color.RGB(0, 0, 1).Print("blue\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
func TestPrintln(t *testing.T) {
|
||||||
|
color.Terminal = 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.RGB(1, 0, 0).Println("red")
|
||||||
|
color.RGB(0, 1, 0).Println("green")
|
||||||
|
color.RGB(0, 0, 1).Println("blue")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
func TestRGB(t *testing.T) {
|
||||||
}
|
color.Terminal = true
|
||||||
}
|
|
||||||
|
|
||||||
func testRGBColors(t *testing.T) {
|
|
||||||
rgbColors := map[string]color.Color{
|
rgbColors := map[string]color.Color{
|
||||||
"RGB.black": color.RGB(0.0, 0.0, 0.0),
|
"black": color.RGB(0, 0, 0),
|
||||||
"RGB.white": color.RGB(1.0, 1.0, 1.0),
|
"white": color.RGB(1, 1, 1),
|
||||||
"RGB.red": color.RGB(1.0, 0.0, 0.0),
|
"gray": color.RGB(0.5, 0.5, 0.5),
|
||||||
"RGB.green": color.RGB(0.0, 1.0, 0.0),
|
"red": color.RGB(1, 0, 0),
|
||||||
"RGB.blue": color.RGB(0.0, 0.0, 1.0),
|
"green": color.RGB(0, 1, 0),
|
||||||
|
"blue": color.RGB(0, 0, 1),
|
||||||
|
"cyan": color.RGB(0, 1, 1),
|
||||||
|
"yellow": color.RGB(1, 1, 0),
|
||||||
|
"orange": color.RGB(1, 0.5, 0),
|
||||||
|
"magenta": color.RGB(1, 0, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, c := range rgbColors {
|
for name, c := range rgbColors {
|
||||||
testColor(t, c, name)
|
testColorRange(t, c)
|
||||||
|
c.Println("█ " + name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLCHColors(t *testing.T) {
|
func testColorRange(t *testing.T, c color.Color) {
|
||||||
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.R >= 0.0)
|
||||||
assert.True(t, c.G >= 0.0)
|
assert.True(t, c.G >= 0.0)
|
||||||
assert.True(t, c.B >= 0.0)
|
assert.True(t, c.B >= 0.0)
|
||||||
@ -74,8 +80,4 @@ func testColor(t *testing.T, c color.Color, name string) {
|
|||||||
assert.True(t, c.R <= 1.0)
|
assert.True(t, c.R <= 1.0)
|
||||||
assert.True(t, c.G <= 1.0)
|
assert.True(t, c.G <= 1.0)
|
||||||
assert.True(t, c.B <= 1.0)
|
assert.True(t, c.B <= 1.0)
|
||||||
|
|
||||||
c.Fprint(io.Discard, name)
|
|
||||||
c.Print(name)
|
|
||||||
c.Println(name)
|
|
||||||
}
|
}
|
||||||
|
65
LCH.go
65
LCH.go
@ -4,24 +4,51 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LCH represents a color using lightness, chroma and hue (oklch).
|
// LCH represents a color using lightness, chroma and hue.
|
||||||
func LCH(lightness Value, chroma Value, hue Value) Color {
|
func LCH(lightness Value, chroma Value, hue Value) Color {
|
||||||
hue *= math.Pi / 180.0
|
lightness = min(max(lightness, 0), 1)
|
||||||
a := chroma * math.Cos(hue)
|
hue *= math.Pi / 180
|
||||||
b := chroma * math.Sin(hue)
|
a := math.Cos(hue)
|
||||||
|
b := math.Sin(hue)
|
||||||
|
|
||||||
r, g, b := labToRGB(lightness, a, b)
|
if !inSRGB(lightness, a, b, chroma) {
|
||||||
|
chroma = findChromaInSRGB(chroma, lightness, a, b, 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
r = gammaCorrection(r)
|
a *= chroma
|
||||||
g = gammaCorrection(g)
|
b *= chroma
|
||||||
b = gammaCorrection(b)
|
|
||||||
|
r, g, b := oklabToLinearRGB(lightness, a, b)
|
||||||
|
|
||||||
|
r = sRGB(r)
|
||||||
|
g = sRGB(g)
|
||||||
|
b = sRGB(b)
|
||||||
|
|
||||||
return Color{r, g, b}
|
return Color{r, g, b}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LAB to linear sRGB
|
// findChromaInSRGB tries to find the closest chroma that can be represented in sRGB color space.
|
||||||
|
func findChromaInSRGB(chroma Value, lightness Value, a Value, b Value, precision Value) Value {
|
||||||
|
high := chroma
|
||||||
|
low := 0.0
|
||||||
|
chroma *= 0.5
|
||||||
|
|
||||||
|
for high-low > precision {
|
||||||
|
if inSRGB(lightness, a, b, chroma) {
|
||||||
|
low = chroma
|
||||||
|
} else {
|
||||||
|
high = chroma
|
||||||
|
}
|
||||||
|
|
||||||
|
chroma = (high + low) * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
return chroma
|
||||||
|
}
|
||||||
|
|
||||||
|
// OKLAB to linear RGB.
|
||||||
// Source: https://bottosson.github.io/posts/oklab/
|
// Source: https://bottosson.github.io/posts/oklab/
|
||||||
func labToRGB(lightness Value, a Value, b Value) (Value, Value, Value) {
|
func oklabToLinearRGB(lightness Value, a Value, b Value) (Value, Value, Value) {
|
||||||
l := lightness + 0.3963377773761749*a + 0.21580375730991364*b
|
l := lightness + 0.3963377773761749*a + 0.21580375730991364*b
|
||||||
m := lightness - 0.10556134581565857*a - 0.0638541728258133*b
|
m := lightness - 0.10556134581565857*a - 0.0638541728258133*b
|
||||||
s := lightness - 0.08948418498039246*a - 1.2914855480194092*b
|
s := lightness - 0.08948418498039246*a - 1.2914855480194092*b
|
||||||
@ -36,21 +63,3 @@ func labToRGB(lightness Value, a Value, b Value) (Value, Value, Value) {
|
|||||||
|
|
||||||
return red, green, blue
|
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
|
|
||||||
}
|
|
||||||
|
49
LCH_test.go
Normal file
49
LCH_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package color_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.akyoto.dev/go/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLCH(t *testing.T) {
|
||||||
|
color.Terminal = true
|
||||||
|
|
||||||
|
lchColors := map[string]color.Color{
|
||||||
|
"black": color.LCH(0.0, 0.0, 0),
|
||||||
|
"white": color.LCH(1.0, 0.0, 0),
|
||||||
|
"gray": color.LCH(0.5, 0.0, 0),
|
||||||
|
"pink": color.LCH(0.75, 1.0, 0),
|
||||||
|
"red": color.LCH(0.75, 1.0, 40),
|
||||||
|
"orange": color.LCH(0.75, 1.0, 60),
|
||||||
|
"yellow": color.LCH(0.9, 1.0, 100),
|
||||||
|
"green": color.LCH(0.75, 1.0, 150),
|
||||||
|
"blue": color.LCH(0.75, 1.0, 260),
|
||||||
|
"cyan": color.LCH(0.75, 1.0, 210),
|
||||||
|
"magenta": color.LCH(0.75, 1.0, 320),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range lchColors {
|
||||||
|
testColorRange(t, c)
|
||||||
|
c.Println("█ " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLCHSpectrum(t *testing.T) {
|
||||||
|
color.Terminal = 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("█")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
16
README.md
16
README.md
@ -5,7 +5,7 @@ Adds color to your terminal output.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- RGB color space
|
- RGB color space
|
||||||
- OKLCH color space
|
- LCH color space (oklch)
|
||||||
- Truecolor terminal output
|
- Truecolor terminal output
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@ -19,14 +19,20 @@ go get git.akyoto.dev/go/color
|
|||||||
```go
|
```go
|
||||||
red := color.RGB(1.0, 0.0, 0.0)
|
red := color.RGB(1.0, 0.0, 0.0)
|
||||||
red.Println("red text")
|
red.Println("red text")
|
||||||
|
|
||||||
|
orange := color.LCH(0.7, 1.0, 65)
|
||||||
|
orange.Println("orange text")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```
|
```
|
||||||
PASS: TestNoColors
|
PASS: TestFprint
|
||||||
PASS: TestColors
|
PASS: TestPrint
|
||||||
PASS: TestRainbow
|
PASS: TestPrintln
|
||||||
|
PASS: TestRGB
|
||||||
|
PASS: TestLCH
|
||||||
|
PASS: TestLCHSpectrum
|
||||||
coverage: 100.0% of statements
|
coverage: 100.0% of statements
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -34,7 +40,7 @@ coverage: 100.0% of statements
|
|||||||
|
|
||||||
```
|
```
|
||||||
BenchmarkRGB-12 1000000000 0.3132 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
|
BenchmarkLCH-12 4802006 249.8 ns/op 0 B/op 0 allocs/op
|
||||||
BenchmarkFprintColorized-12 6356535 188.4 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
|
BenchmarkFprintRaw-12 27374659 43.76 ns/op 0 B/op 0 allocs/op
|
||||||
```
|
```
|
||||||
|
27
sRGB.go
Normal file
27
sRGB.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package color
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// inSRGB indicates whether the given color can be mapped to the sRGB color space.
|
||||||
|
func inSRGB(l Value, a Value, b Value, chroma Value) bool {
|
||||||
|
r, g, b := oklabToLinearRGB(l, a*chroma, b*chroma)
|
||||||
|
return r >= 0 && g >= 0 && b >= 0 && r <= 1 && g <= 1 && b <= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// sRGB performs gamma correction to convert linear RGB to sRGB.
|
||||||
|
// Source: IEC 61966-2-2
|
||||||
|
func sRGB(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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user