oklab/oklab_test.go
2025-02-23 14:57:27 -05:00

238 lines
6.6 KiB
Go

package oklab
import (
"image/color"
"math"
"reflect"
"testing"
)
func TestColor_NRGBA(t *testing.T) {
// Not checking the whole space because that would be slow, and alpha is given less resolution in this test.
// The increments were chosen because they're factors of 0xffff, so we'll end on the maximum value.
for a := uint32(0); a < 0x10000; a += 4369 {
for b := uint32(0); b < 0x10000; b += 1285 {
for g := uint32(0); g < 0x10000; g += 1285 {
for r := uint32(0); r < 0x10000; r += 1285 {
_r, _g, _b, _a := FromNRGBA(r, g, b, a).NRGBA()
if r != _r || g != _g || b != _b || a != _a {
t.Errorf("NRGBA(0x%04x,0x%04x,0x%04x,0x%04x) -> OkLab -> NRGBA(0x%04x,0x%04x,0x%04x,0x%04x)", r, g, b, a, _r, _g, _b, _a)
return
}
}
}
}
}
}
func TestColor_RGBA(t *testing.T) {
// This is similar to the NRGBA test, except the components step by a/17 instead of a fixed 1285.
// a/17 was chosen because 17 is a factor of 4369, so the components will end equalling a.
for a := uint32(0); a < 0x10000; a += 4369 {
step := max(1, a/17)
for b := uint32(0); b <= a; b += step {
for g := uint32(0); g <= a; g += step {
for r := uint32(0); r <= a; r += step {
_r, _g, _b, _a := FromRGBA(r, g, b, a).RGBA()
if r != _r || g != _g || b != _b || a != _a {
t.Errorf("RGBA(0x%04x,0x%04x,0x%04x,0x%04x) -> OkLab -> RGBA(0x%04x,0x%04x,0x%04x,0x%04x)", r, g, b, a, _r, _g, _b, _a)
return
}
}
}
}
}
}
func Test_delinearize(t *testing.T) {
for c := uint32(0); c < 0x10000; c++ {
got := delinearize(linearize(c))
if got != c {
t.Errorf("delinearize(linearize(0x%04x)) != 0x%04x", c, got)
return
}
}
}
// The NRGBA and NRGBA64 models don't have sensible ways to recover transparent colors from types it
// doesn't know about, so I'm going to help them out.
func fixedNRGBAModel(c color.Color) color.Color {
if c, ok := c.(NRGBAColor); ok {
r, g, b, a := c.NRGBA()
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
}
return color.NRGBAModel.Convert(c)
}
func fixedNRGBA64Model(c color.Color) color.Color {
if c, ok := c.(NRGBAColor); ok {
r, g, b, a := c.NRGBA()
return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
return color.NRGBA64Model.Convert(c)
}
type testNRGBAColor struct {
color.NRGBA64
}
func (c testNRGBAColor) NRGBA() (r, g, b, a uint32) {
return uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
}
func testNRGBAModel(c color.Color) color.Color {
return testNRGBAColor{fixedNRGBA64Model(c).(color.NRGBA64)}
}
func Test_Model(t *testing.T) {
// test to make sure we can reproduce some test colors.
// In particular, I want to make sure the colors can be recovered from transparent NRGBA and NRGBA64.
for _, tt := range [...]struct {
name string
color color.Color
model color.Model
}{
{
"OkLab: nop",
Color{math.Inf(1), math.Inf(-1), math.MaxFloat64, 0},
Model,
}, {
"NRGBA: opaque Sapphire",
color.NRGBA{R: 0x0f, G: 0x52, B: 0xba, A: 0xff},
color.ModelFunc(fixedNRGBAModel),
}, {
"NRGBA: translucent Orchid",
color.NRGBA{R: 0xda, G: 0x70, B: 0xd6, A: 0x7f},
color.ModelFunc(fixedNRGBAModel),
}, {
"NRGBA: invisible Floral White",
color.NRGBA{R: 0xff, G: 0xfa, B: 0xf0, A: 0x00},
color.ModelFunc(fixedNRGBAModel),
}, {
"NRGBA64: opaque Amethyst",
color.NRGBA64{R: 0x9999, G: 0x6666, B: 0xcccc, A: 0xffff},
color.ModelFunc(fixedNRGBA64Model),
}, {
"NRGBA64: translucent Smoke",
color.NRGBA64{R: 0xf5f5, G: 0xf5f5, B: 0xf5f5, A: 0x7fff},
color.ModelFunc(fixedNRGBA64Model),
}, {
"NRGBA64: invisible Emerald",
color.NRGBA64{R: 0x5050, G: 0xC8C8, B: 0x7878, A: 0x0000},
color.ModelFunc(fixedNRGBA64Model),
}, {
"RGBA64: opaque Chartreuse",
color.RGBA64{R: 0xb2b2, G: 0xd6d6, B: 0x3f3f, A: 0xffff},
color.RGBA64Model,
}, {
"RGBA64: transparent Mauveine",
color.RGBA64{R: 0x577c, G: 0x013e, B: 0x602b, A: 0x9e37},
color.RGBA64Model,
}, {
"RGBA64: invisible nothing",
color.RGBA64{R: 0x0, G: 0x0, B: 0x0, A: 0x0},
color.RGBA64Model,
}, {
"RGBA: opaque Chartreuse",
color.RGBA{R: 0xb2, G: 0xd6, B: 0x3f, A: 0xff},
color.RGBAModel,
}, {
"RGBA: transparent Mauveine",
color.RGBA{R: 0x57, G: 0x01, B: 0x60, A: 0x9e},
color.RGBAModel,
}, {
"RGBA: invisible nothing",
color.RGBA{R: 0x0, G: 0x0, B: 0x0, A: 0x0},
color.RGBAModel,
}, {
// test the handling of the NRGBAColor interface; as
// all the types that support it have special handling
// that would take precedence.
"testNRGBAColor: invisible Floral White",
testNRGBAColor{color.NRGBA64{R: 0xff, G: 0xfa, B: 0xf0, A: 0x00}},
color.ModelFunc(testNRGBAModel),
},
} {
t.Run(tt.name, func(t *testing.T) {
c := Model.Convert(tt.color).(Color)
if got := tt.model.Convert(c); !reflect.DeepEqual(got, tt.color) {
t.Errorf("%#+v -> %#+v -> %#+v, want %#+v", tt.color, c, got, tt.color)
return
}
})
}
}
func testDistance(t *testing.T, c0, c1 Color) {
d := Distance(c0, c1)
if math.IsNaN(d) || math.IsInf(d, 0) {
t.Errorf("Distance(%v, %v) = %f, want finite", c0, c1, d)
return
}
if d < 0 {
t.Errorf("Distance(%v, %v) = %f, want %f >= 0", c0, c1, d, d)
return
}
if d2 := Distance(c1, c0); d2 != d {
t.Errorf("Distance(%v, %v) != Distance(%v, %v), want %f == %f", c1, c0, c0, c1, d, d2)
return
}
if c0 == c1 || (c0.A == 0 && c1.A == 0) {
// if they're the same color, or both are completely transparent,
// they should be perceived as identical.
if d != 0 {
t.Errorf("Distance(%v, %v) = %f, want %f == 0", c0, c1, d, d)
return
}
} else {
// otherwise, there should be some kind of difference between them.
if d <= 0 {
t.Errorf("Distance(%v, %v) = %f, want %f > 0", c0, c1, d, d)
return
}
}
mid := Color{
Lightness: (c0.Lightness + c1.Lightness) * 0.5,
ChromaA: (c0.ChromaA + c1.ChromaA) * 0.5,
ChromaB: (c0.ChromaB + c1.ChromaB) * 0.5,
A: (c0.A + c1.A) * 0.5,
}
// traveling from c0 to c1 via mid can't possibly be
// shorter than traveling from c0 to c1 directly.
d2 := Distance(c0, mid) + Distance(mid, c1)
if d2 < d {
t.Errorf("Distance(%v, %v)+Distance(%v, %v) < Distance(%v, %v), want %f >= %f", c0, mid, mid, c1, c0, c1, d2, d)
return
}
}
func TestDistance(t *testing.T) {
colours := []Color{
FromNRGBA(0xffff, 0xffff, 0xffff, 0xffff),
FromNRGBA(0xffff, 0xffff, 0xffff, 0x7fff),
FromNRGBA(0xffff, 0xffff, 0xffff, 0x0000),
FromNRGBA(0x0000, 0x0000, 0x0000, 0xffff),
FromNRGBA(0x0000, 0x0000, 0x0000, 0x7fff),
FromNRGBA(0x0000, 0x0000, 0x0000, 0x0000),
}
for _, c0 := range colours {
for _, c1 := range colours {
testDistance(t, c0, c1)
}
}
}