diff --git a/oklab.go b/oklab.go index 9ede5f3..8f47fc9 100644 --- a/oklab.go +++ b/oklab.go @@ -149,6 +149,14 @@ func Distance(a, b Color) float64 { return math.Sqrt(max(sqr(dL), sqr(dL+dA)) + max(sqr(da), sqr(da+dA)) + max(sqr(db), sqr(db+dA))) } +// NRGBAColor represent a color that can give us non pre-multiplied RGBA components. +// +// This isn't a standard interface, but we implement it and check for it regardless. +type NRGBAColor interface { + color.Color + NRGBA() (r, g, b, a uint32) +} + func okLabModel(c color.Color) color.Color { switch c := c.(type) { case Color: @@ -161,8 +169,7 @@ func okLabModel(c color.Color) color.Color { case color.NRGBA64: return FromNRGBA(uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)) - // This isn't a standard interface, but I'm going to check for it regardless. - case interface{ NRGBA() (r, g, b, a uint32) }: + case NRGBAColor: return FromNRGBA(c.NRGBA()) default: @@ -170,5 +177,10 @@ func okLabModel(c color.Color) color.Color { } } -// Implements a color model for converting arbitrary colors to OKLab. -var Model = color.ModelFunc(okLabModel) +var ( + // A color model for converting arbitrary colors to OKLab. + Model = color.ModelFunc(okLabModel) + + // Type assertions. + _ NRGBAColor = Color{} +) diff --git a/oklab_test.go b/oklab_test.go index c52f9cd..c3b4210 100644 --- a/oklab_test.go +++ b/oklab_test.go @@ -59,7 +59,7 @@ func Test_delinearize(t *testing.T) { // 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.(interface{ NRGBA() (r, g, b, a uint32) }); ok { + 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)} } @@ -68,7 +68,7 @@ func fixedNRGBAModel(c color.Color) color.Color { } func fixedNRGBA64Model(c color.Color) color.Color { - if c, ok := c.(interface{ NRGBA() (r, g, b, a uint32) }); ok { + 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)} } @@ -149,6 +149,57 @@ func Test_Model(t *testing.T) { } } +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), @@ -159,37 +210,9 @@ func TestDistance(t *testing.T) { FromNRGBA(0x0000, 0x0000, 0x0000, 0x0000), } - for i, c0 := range colours { - for j, c1 := range colours { - d := Distance(c0, c1) - - if i == j || (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) = %v, want 0", c0, c1, d) - } - } else { - // otherwise, there should be some kind of difference between them. - if d <= 0 { - t.Errorf("Distance(%v, %v) = %v, want > 0", c0, c1, d) - } - } - - 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) - } + for _, c0 := range colours { + for _, c1 := range colours { + testDistance(t, c0, c1) } } }