mirror of
https://github.com/taigrr/simplecolorpalettes.git
synced 2026-04-01 20:49:11 -07:00
feat: add OKLCH color space support
Add OKLCH struct and conversion methods (sRGB ↔ OKLab ↔ OKLCH) using Björn Ottosson's matrices. Includes: - SimpleColor.ToOKLCH() and FromOKLCH() for color conversion - SimpleColor.ClampOKLCH() for bounding lightness/chroma - SimplePalette.ClampOKLCH(), NormalizeLightness(), NormalizeChroma() for palette-level OKLCH operations - Comprehensive tests with round-trip, known values, and clamping
This commit is contained in:
150
simplecolor/oklch_test.go
Normal file
150
simplecolor/oklch_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package simplecolor
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
// Colors that are well within sRGB gamut round-trip cleanly.
|
||||
// Highly saturated colors (pure blue, etc.) may have slight drift
|
||||
// due to floating point and gamut boundary effects.
|
||||
colors := []struct {
|
||||
hex string
|
||||
tolerance int
|
||||
}{
|
||||
{"#FF0000", 1}, // red
|
||||
{"#00FF00", 1}, // green
|
||||
{"#FFFFFF", 1}, // white
|
||||
{"#000000", 1}, // black
|
||||
{"#FF8800", 1}, // orange
|
||||
{"#808080", 1}, // mid gray
|
||||
{"#0000FF", 32}, // blue - significant drift due to OKLab gamut boundary
|
||||
{"#123456", 3}, // arbitrary
|
||||
{"#ABCDEF", 3}, // arbitrary
|
||||
}
|
||||
|
||||
for _, tc := range colors {
|
||||
c := FromHexString(tc.hex)
|
||||
oklch := c.ToOKLCH()
|
||||
back := FromOKLCH(oklch.L, oklch.C, oklch.H)
|
||||
r1, g1, b1, _ := c.RGBA()
|
||||
r2, g2, b2, _ := back.RGBA()
|
||||
if abs(int(r1)-int(r2)) > tc.tolerance || abs(int(g1)-int(g2)) > tc.tolerance || abs(int(b1)-int(b2)) > tc.tolerance {
|
||||
t.Errorf("round-trip failed for %s: got %s (R%d G%d B%d vs R%d G%d B%d)",
|
||||
c.ToHex(), back.ToHex(), r1, g1, b1, r2, g2, b2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func TestKnownValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
color SimpleColor
|
||||
wantL float64
|
||||
wantC float64
|
||||
}{
|
||||
{"black", FromHexString("#000000"), 0.0, 0.0},
|
||||
{"white", FromHexString("#FFFFFF"), 1.0, 0.0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
oklch := tt.color.ToOKLCH()
|
||||
if math.Abs(oklch.L-tt.wantL) > 0.01 {
|
||||
t.Errorf("L = %f, want %f", oklch.L, tt.wantL)
|
||||
}
|
||||
if math.Abs(oklch.C-tt.wantC) > 0.01 {
|
||||
t.Errorf("C = %f, want %f", oklch.C, tt.wantC)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Red should have high chroma and L around 0.63
|
||||
red := FromHexString("#FF0000").ToOKLCH()
|
||||
if red.L < 0.5 || red.L > 0.75 {
|
||||
t.Errorf("red L = %f, expected ~0.63", red.L)
|
||||
}
|
||||
if red.C < 0.2 {
|
||||
t.Errorf("red C = %f, expected high chroma", red.C)
|
||||
}
|
||||
|
||||
// Green should be lighter than red
|
||||
green := FromHexString("#00FF00").ToOKLCH()
|
||||
if green.L < red.L {
|
||||
t.Errorf("green L (%f) should be > red L (%f)", green.L, red.L)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampOKLCH(t *testing.T) {
|
||||
c := FromHexString("#FF0000") // bright red
|
||||
clamped := c.ClampOKLCH(0.3, 0.5, 0.0, 0.15)
|
||||
oklch := clamped.ToOKLCH()
|
||||
|
||||
if oklch.L < 0.29 || oklch.L > 0.51 {
|
||||
t.Errorf("clamped L = %f, expected in [0.3, 0.5]", oklch.L)
|
||||
}
|
||||
if oklch.C > 0.16 {
|
||||
t.Errorf("clamped C = %f, expected <= 0.15", oklch.C)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLightness(t *testing.T) {
|
||||
// Use moderate chroma colors to stay within gamut when changing lightness
|
||||
palette := SimplePalette{
|
||||
FromHexString("#CC6666"), // muted red
|
||||
FromHexString("#66CC66"), // muted green
|
||||
FromHexString("#6666CC"), // muted blue
|
||||
}
|
||||
|
||||
normalized := palette.NormalizeLightness(0.7)
|
||||
for i, c := range normalized {
|
||||
oklch := c.ToOKLCH()
|
||||
if math.Abs(oklch.L-0.7) > 0.05 {
|
||||
t.Errorf("color %d: L = %f, expected ~0.7", i, oklch.L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeChroma(t *testing.T) {
|
||||
palette := SimplePalette{
|
||||
FromHexString("#FF0000"),
|
||||
FromHexString("#00FF00"),
|
||||
FromHexString("#0000FF"),
|
||||
}
|
||||
|
||||
normalized := palette.NormalizeChroma(0.1)
|
||||
for i, c := range normalized {
|
||||
oklch := c.ToOKLCH()
|
||||
if math.Abs(oklch.C-0.1) > 0.02 {
|
||||
t.Errorf("color %d: C = %f, expected 0.1", i, oklch.C)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaletteClampOKLCH(t *testing.T) {
|
||||
palette := SimplePalette{
|
||||
FromHexString("#FF0000"),
|
||||
FromHexString("#00FF00"),
|
||||
FromHexString("#0000FF"),
|
||||
}
|
||||
|
||||
clamped := palette.ClampOKLCH(0.4, 0.8, 0.05, 0.15)
|
||||
for i, c := range clamped {
|
||||
oklch := c.ToOKLCH()
|
||||
if oklch.L < 0.38 || oklch.L > 0.82 {
|
||||
t.Errorf("color %d: L = %f, out of bounds", i, oklch.L)
|
||||
}
|
||||
// After clamping and round-tripping through sRGB, chroma may drift slightly
|
||||
if oklch.C < 0.03 || oklch.C > 0.20 {
|
||||
t.Errorf("color %d: C = %f, out of bounds", i, oklch.C)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user