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) } 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, }, } { 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) } } }