Skip to content

Commit

Permalink
Add support for OkLab and OkLch (#66)
Browse files Browse the repository at this point in the history
* Add support for OkLab and OkLch

* Add color constructors for OkLab and OkLch
  • Loading branch information
stephenafamo authored Mar 5, 2024
1 parent 6e6f2cd commit fae0ace
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 6 deletions.
89 changes: 83 additions & 6 deletions colors.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ func (c1 Color) DistanceLinearRGB(c2 Color) float64 {
//
// Sources:
//
// https://www.compuphase.com/cmetric.htm
// https://github.com/lucasb-eyer/go-colorful/issues/52
// https://www.compuphase.com/cmetric.htm
// https://github.com/lucasb-eyer/go-colorful/issues/52
func (c1 Color) DistanceRiemersma(c2 Color) float64 {
rAvg := (c1.R + c2.R) / 2.0
// Deltas
Expand All @@ -136,9 +136,11 @@ func (c1 Color) AlmostEqualRgb(c2 Color) bool {

// You don't really want to use this, do you? Go for BlendLab, BlendLuv or BlendHcl.
func (c1 Color) BlendRgb(c2 Color, t float64) Color {
return Color{c1.R + t*(c2.R-c1.R),
return Color{
c1.R + t*(c2.R-c1.R),
c1.G + t*(c2.G-c1.G),
c1.B + t*(c2.B-c1.B)}
c1.B + t*(c2.B-c1.B),
}
}

// Utility used by Hxx color-spaces for interpolating between two angles in [0,360].
Expand Down Expand Up @@ -408,7 +410,7 @@ func linearize_fast(v float64) float64 {
v2 := v1 * v1
v3 := v2 * v1
v4 := v2 * v2
//v5 := v3*v2
// v5 := v3*v2
return -0.248750514614486 + 0.925583310193438*v + 1.16740237321695*v2 + 0.280457026598666*v3 - 0.0757991963780179*v4 //+ 0.0437040411548932*v5
}

Expand Down Expand Up @@ -828,7 +830,7 @@ func LuvToXyz(l, u, v float64) (x, y, z float64) {
}

func LuvToXyzWhiteRef(l, u, v float64, wref [3]float64) (x, y, z float64) {
//y = wref[1] * lab_finv((l + 0.16) / 1.16)
// y = wref[1] * lab_finv((l + 0.16) / 1.16)
if l <= 0.08 {
y = wref[1] * l * 100.0 * 3.0 / 29.0 * 3.0 / 29.0 * 3.0 / 29.0
} else {
Expand Down Expand Up @@ -1028,3 +1030,78 @@ func (col1 Color) BlendLuvLCh(col2 Color, t float64) Color {
// We know that h are both in [0..360]
return LuvLCh(l1+t*(l2-l1), c1+t*(c2-c1), interp_angle(h1, h2, t))
}

/// OkLab ///
///////////

func (col Color) OkLab() (l, a, b float64) {
return XyzToOkLab(col.Xyz())
}

func OkLab(l, a, b float64) Color {
return Xyz(OkLabToXyz(l, a, b))
}

func XyzToOkLab(x, y, z float64) (l, a, b float64) {
l_ := math.Cbrt(0.8189330101*x + 0.3618667424*y - 0.1288597137*z)
m_ := math.Cbrt(0.0329845436*x + 0.9293118715*y + 0.0361456387*z)
s_ := math.Cbrt(0.0482003018*x + 0.2643662691*y + 0.6338517070*z)
l = 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_
a = 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_
b = 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_
return
}

func OkLabToXyz(l, a, b float64) (x, y, z float64) {
l_ := 0.9999999984505196*l + 0.39633779217376774*a + 0.2158037580607588*b
m_ := 1.0000000088817607*l - 0.10556134232365633*a - 0.0638541747717059*b
s_ := 1.0000000546724108*l - 0.08948418209496574*a - 1.2914855378640917*b

ll := math.Pow(l_, 3)
m := math.Pow(m_, 3)
s := math.Pow(s_, 3)

x = 1.2268798733741557*ll - 0.5578149965554813*m + 0.28139105017721594*s
y = -0.04057576262431372*ll + 1.1122868293970594*m - 0.07171106666151696*s
z = -0.07637294974672142*ll - 0.4214933239627916*m + 1.5869240244272422*s

return
}

/// OkLch ///
///////////

func (col Color) OkLch() (l, c, h float64) {
return OkLabToOkLch(col.OkLab())
}

func OkLch(l, c, h float64) Color {
return Xyz(OkLchToXyz(l, c, h))
}

func XyzToOkLch(x, y, z float64) (float64, float64, float64) {
l, c, h := OkLabToOkLch(XyzToOkLab(x, y, z))
return l, c, h
}

func OkLchToXyz(l, c, h float64) (float64, float64, float64) {
x, y, z := OkLabToXyz(OkLchToOkLab(l, c, h))
return x, y, z
}

func OkLabToOkLch(l, a, b float64) (float64, float64, float64) {
c := math.Sqrt((a * a) + (b * b))
h := math.Atan2(b, a)
if h < 0 {
h += 2 * math.Pi
}

return l, c, h * 180 / math.Pi
}

func OkLchToOkLab(l, c, h float64) (float64, float64, float64) {
h *= math.Pi / 180
a := c * math.Cos(h)
b := c * math.Sin(h)
return l, a, b
}
123 changes: 123 additions & 0 deletions colors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,129 @@ func TestHclWhiteRefConversion(t *testing.T) {
}
}

// / Oklab ///
// ///////////

func TestRgbToOkLab(t *testing.T) {
for i, tCase := range []struct {
R, G, B float64
l, a, b float64
}{
{1, 1, 1, 1.000, 0.000, 0.000}, // white
{1, 0, 0, 0.627955, 0.224863, 0.125846}, // red
{0, 1, 0, 0.86644, -0.233888, 0.179498}, // lime
{0, 0, 1, 0.452014, -0.032457, -0.311528}, // blue
{0, 1, 1, 0.905399, -0.149444, -0.039398}, // cyan
{1, 0, 1, 0.701674, 0.274566, -0.169156}, // magenta
{1, 1, 0, 0.967983, -0.071369, 0.198570}, // yellow
{0, 0, 0, 0.000000, 0.000000, 0.000000}, // black
} {
l, a, b := XyzToOkLab(LinearRgb(tCase.R, tCase.G, tCase.B).Xyz())
if !almosteq(l, tCase.l) {
t.Errorf("%v. RgbToOklab => (%v), want %v (l)", i, l, tCase.l)
}
if !almosteq(a, tCase.a) {
t.Errorf("%v. RgbToOklab => (%v), want %v (a)", i, a, tCase.a)
}
if !almosteq(b, tCase.b) {
t.Errorf("%v. RgbToOklab => (%v), want %v (b)", i, b, tCase.b)
}
}
}

// https://bottosson.github.io/posts/oklab/#table-of-example-xyz-and-oklab-pairs
var xyzOklabPairs = []struct {
x, y, z float64
l, a, b float64
}{
{0.950, 1.000, 1.089, 1.000, 0.000, 0.000},
{1.000, 0.000, 0.000, 0.450, 1.236, -0.019},
{0.000, 1.000, 0.000, 0.922, -0.671, 0.263},
{0.000, 0.000, 1.000, 0.153, -1.415, -0.449},
}

func TestXyzToOkLab(t *testing.T) {
for i, tCase := range xyzOklabPairs {
l, a, b := XyzToOkLab(tCase.x, tCase.y, tCase.z)
if !almosteq(l, tCase.l) {
t.Errorf("%v. XyzToOklab => (%v), want %v (l)", i, l, tCase.l)
}
if !almosteq(a, tCase.a) {
t.Errorf("%v. XyzToOklab => (%v), want %v (a)", i, a, tCase.a)
}
if !almosteq(b, tCase.b) {
t.Errorf("%v. XyzToOklab => (%v), want %v (b)", i, b, tCase.b)
}
}
}

func TestOklabToXyz(t *testing.T) {
for i, tCase := range xyzOklabPairs {
x, y, z := OkLabToXyz(tCase.l, tCase.a, tCase.b)
if !almosteq(x, tCase.x) {
t.Errorf("%v. OklabToXyz => (%v), want %v (x)", i, x, tCase.x)
}
if !almosteq(y, tCase.y) {
t.Errorf("%v. OklabToXyz => (%v), want %v (y)", i, y, tCase.y)
}
if !almosteq(z, tCase.z) {
t.Errorf("%v. OklabToXyz => (%v), want %v (z)", i, z, tCase.z)
}
}
}

var OkPairs = []struct {
lab [3]float64
lch [3]float64
}{
{
[3]float64{55.0, 0.17, -0.14}, // oklab
[3]float64{55.0, 0.22, 320.528}, // oklch
},
{
[3]float64{90.0, 0.32, 0.00}, // oklab
[3]float64{90.0, 0.32, 0.0}, // oklch
},
{
[3]float64{10.0, 0.00, -0.40}, // oklab
[3]float64{10.0, 0.40, 270.0}, // oklch
},
}

func TestOkLabToOkLch(t *testing.T) {
for i, tc := range OkPairs {
l, c, h := OkLabToOkLch(tc.lab[0], tc.lab[1], tc.lab[2])
if !almosteq(l, tc.lch[0]) {
t.Errorf("%d. l returned %v, expected %v", i, l, tc.lch[0])
}

if !almosteq(c, tc.lch[1]) {
t.Errorf("%d. c returned %v, expected %v", i, c, tc.lch[1])
}

if !almosteq(h, tc.lch[2]) {
t.Errorf("%d. h returned %v, expected %v", i, h, tc.lch[2])
}
}
}

func TestOkLchToOkLab(t *testing.T) {
for i, tc := range OkPairs {
l, a, b := OkLchToOkLab(tc.lch[0], tc.lch[1], tc.lch[2])
if !almosteq(l, tc.lab[0]) {
t.Errorf("%d. l returned %v, expected %v", i, l, tc.lab[0])
}

if !almosteq(a, tc.lab[1]) {
t.Errorf("%d. a returned %v, expected %v", i, a, tc.lab[1])
}

if !almosteq(b, tc.lab[2]) {
t.Errorf("%d. b returned %v, expected %v", i, b, tc.lab[2])
}
}
}

/// Test distances ///
//////////////////////

Expand Down

0 comments on commit fae0ace

Please sign in to comment.