Added packages for linear grayscale colour varients.
This commit is contained in:
parent
a0c520c3ed
commit
2410cce342
@ -22,3 +22,6 @@ You can find the documentation at [pkg.go.dev](https://pkg.go.dev/smariot.com/co
|
|||||||
* *[smariot.com/color/oklab](https://pkg.go.dev/smariot.com/color/oklab)*: OkLab colour, no alpha.
|
* *[smariot.com/color/oklab](https://pkg.go.dev/smariot.com/color/oklab)*: OkLab colour, no alpha.
|
||||||
* *[smariot.com/color/oklaba](https://pkg.go.dev/smariot.com/color/oklaba)*: Premultiplied OkLab+Alpha color.
|
* *[smariot.com/color/oklaba](https://pkg.go.dev/smariot.com/color/oklaba)*: Premultiplied OkLab+Alpha color.
|
||||||
* *[smariot.com/color/noklaba](https://pkg.go.dev/smariot.com/color/noklaba)*: Non-premultiplied OkLab+Alpha colour.
|
* *[smariot.com/color/noklaba](https://pkg.go.dev/smariot.com/color/noklaba)*: Non-premultiplied OkLab+Alpha colour.
|
||||||
|
* *[smariot.com/color/lgray](https://pkg.go.dev/smariot.com/color/lgray)*: Linear grayscale colour, no alpha.
|
||||||
|
* *[smariot.com/color/lgraya](https://pkg.go.dev/smariot.com/color/lgraya)*: Premultiplied linear grayscale+alpha colour.
|
||||||
|
* *[smariot.com/color/nlgraya](https://pkg.go.dev/smariot.com/color/nlgraya)*: Non-premultiplied linear grayscale+alpha colour.
|
||||||
|
@ -6,8 +6,8 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDistance[T tester[T], C color.Color](t T, alpha bool, midpoint func(c0, c1 C) C, f func(c0, c1 C) float64, m color.Model) {
|
func TestDistance[T tester[T], C color.Color](t T, color, alpha bool, midpoint func(c0, c1 C) C, f func(c0, c1 C) float64, m color.Model) {
|
||||||
colors := slices.Collect(EnumColor[C](alpha, false, m))
|
colors := slices.Collect(EnumColor[C](color, alpha, false, m))
|
||||||
|
|
||||||
for i, c0 := range colors {
|
for i, c0 := range colors {
|
||||||
// a colour should have a distance of zero to itself.
|
// a colour should have a distance of zero to itself.
|
||||||
|
@ -20,13 +20,13 @@ func TestTestDistance(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mt.run("non-zero distance for identical colours", func(t *mockTest) {
|
mt.run("non-zero distance for identical colours", func(t *mockTest) {
|
||||||
TestDistance(t, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
TestDistance(t, true, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
||||||
return 1
|
return 1
|
||||||
}, color.RGBAModel)
|
}, color.RGBAModel)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("NaN distance", func(t *mockTest) {
|
mt.run("NaN distance", func(t *mockTest) {
|
||||||
TestDistance(t, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
TestDistance(t, true, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
||||||
if c0 == c1 {
|
if c0 == c1 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ func TestTestDistance(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mt.run("negative distance", func(t *mockTest) {
|
mt.run("negative distance", func(t *mockTest) {
|
||||||
TestDistance(t, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
TestDistance(t, true, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
||||||
if c0 == c1 {
|
if c0 == c1 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ func TestTestDistance(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mt.run("asymmetric distance", func(t *mockTest) {
|
mt.run("asymmetric distance", func(t *mockTest) {
|
||||||
TestDistance(t, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
TestDistance(t, true, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
||||||
if c0 == c1 {
|
if c0 == c1 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ func TestTestDistance(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mt.run("triangle inequality", func(t *mockTest) {
|
mt.run("triangle inequality", func(t *mockTest) {
|
||||||
TestDistance(t, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
TestDistance(t, true, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
||||||
dR := int(c0.R) - int(c1.R)
|
dR := int(c0.R) - int(c1.R)
|
||||||
dG := int(c0.G) - int(c1.G)
|
dG := int(c0.G) - int(c1.G)
|
||||||
dB := int(c0.B) - int(c1.B)
|
dB := int(c0.B) - int(c1.B)
|
||||||
@ -73,7 +73,7 @@ func TestTestDistance(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mt.run("euclidean distance", func(t *mockTest) {
|
mt.run("euclidean distance", func(t *mockTest) {
|
||||||
TestDistance(t, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
TestDistance(t, true, true, midpoint, func(c0, c1 color.RGBA) float64 {
|
||||||
dR := int(c0.R) - int(c1.R)
|
dR := int(c0.R) - int(c1.R)
|
||||||
dG := int(c0.G) - int(c1.G)
|
dG := int(c0.G) - int(c1.G)
|
||||||
dB := int(c0.B) - int(c1.B)
|
dB := int(c0.B) - int(c1.B)
|
||||||
|
@ -1,47 +1,73 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
_color "image/color"
|
||||||
"iter"
|
"iter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enum iterates over a sparse sample of the RGBA colour space.
|
// Enum iterates over a sparse sample of the RGBA colour space.
|
||||||
//
|
//
|
||||||
|
// If color is true, the colours will have distinct RGB components.
|
||||||
|
// otherwise, the colours will have identical RGB components.
|
||||||
|
//
|
||||||
// If alpha is true, the colours will include transparency,
|
// If alpha is true, the colours will include transparency,
|
||||||
// otherwise the returned colours will be fully opaque.
|
// otherwise the returned colours will be fully opaque.
|
||||||
//
|
//
|
||||||
// If slow is false, an even smaller number of samples will be returned
|
// If slow is false, an even smaller number of samples will be returned
|
||||||
// making this suitable for use in a nested loop.
|
// making this suitable for use in a nested loop.
|
||||||
//
|
//
|
||||||
// alpha=true slow=true: 87481 samples.
|
// color=true alpha=true slow=true: 87481 samples.
|
||||||
// alpha=false slow=true: 140608 samples.
|
// color=true alpha=false slow=true: 140608 samples.
|
||||||
// alpha=true slow=false: 649 samples.
|
// color=true alpha=true slow=false: 649 samples.
|
||||||
// alpha=false slow=false: 216 samples.
|
// color=true alpha=false slow=false: 216 samples.
|
||||||
func Enum(alpha, slow bool) iter.Seq[color.RGBA64] {
|
func Enum(color, alpha, slow bool) iter.Seq[_color.RGBA64] {
|
||||||
var aStart, aStep, cDiv uint32
|
var aStart, aStep, cDiv uint32
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case alpha && slow:
|
case color && alpha && slow:
|
||||||
aStart, aStep, cDiv = 0, 0xffff/15, 17
|
aStart, aStep, cDiv = 0, 0xffff/15, 17
|
||||||
case alpha: // alpha && !slow
|
case color && alpha: // color && alpha && !slow
|
||||||
aStart, aStep, cDiv = 0, 0xffff/3, 5
|
aStart, aStep, cDiv = 0, 0xffff/3, 5
|
||||||
case slow: // !alpha && slow
|
case color && slow: // color && !alpha && slow
|
||||||
aStart, aStep, cDiv = 0xffff, 1, 51
|
aStart, aStep, cDiv = 0xffff, 1, 51
|
||||||
default: // !alpha && !slow
|
case color: // color && !alpha && !slow
|
||||||
aStart, aStep, cDiv = 0xffff, 1, 5
|
aStart, aStep, cDiv = 0xffff, 1, 5
|
||||||
|
|
||||||
|
case alpha && slow: // !color && alpha && slow
|
||||||
|
aStart, aStep, cDiv = 0, 0xffff/15, 4369
|
||||||
|
case alpha: // !color && alpha && !slow
|
||||||
|
aStart, aStep, cDiv = 0, 0xffff/15, 17
|
||||||
|
case slow: // !color && !alpha && slow
|
||||||
|
aStart, aStep, cDiv = 0xffff, 1, 65535
|
||||||
|
default: // !color && !alpha && !slow
|
||||||
|
aStart, aStep, cDiv = 0xffff, 1, 257
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(yield func(color.RGBA64) bool) {
|
if color {
|
||||||
|
return func(yield func(_color.RGBA64) bool) {
|
||||||
|
for a := aStart; a <= 0xffff; a += aStep {
|
||||||
|
cStep := max(1, a/cDiv)
|
||||||
|
|
||||||
|
for b := uint32(0); b <= a; b += cStep {
|
||||||
|
for g := uint32(0); g <= a; g += cStep {
|
||||||
|
for r := uint32(0); r <= a; r += cStep {
|
||||||
|
if !yield(_color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(yield func(_color.RGBA64) bool) {
|
||||||
for a := aStart; a <= 0xffff; a += aStep {
|
for a := aStart; a <= 0xffff; a += aStep {
|
||||||
cStep := max(1, a/cDiv)
|
cStep := max(1, a/cDiv)
|
||||||
|
|
||||||
for b := uint32(0); b <= a; b += cStep {
|
for c := uint32(0); c <= a; c += cStep {
|
||||||
for g := uint32(0); g <= a; g += cStep {
|
if !yield(_color.RGBA64{uint16(c), uint16(c), uint16(c), uint16(a)}) {
|
||||||
for r := uint32(0); r <= a; r += cStep {
|
return
|
||||||
if !yield(color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,9 +75,9 @@ func Enum(alpha, slow bool) iter.Seq[color.RGBA64] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EnumColor is identical to [Enum], but invokes a [color.Model] to return a concrete colour type.
|
// EnumColor is identical to [Enum], but invokes a [color.Model] to return a concrete colour type.
|
||||||
func EnumColor[C color.Color](alpha, slow bool, m color.Model) iter.Seq[C] {
|
func EnumColor[C _color.Color](color, alpha, slow bool, m _color.Model) iter.Seq[C] {
|
||||||
return func(yield func(C) bool) {
|
return func(yield func(C) bool) {
|
||||||
for rgba := range Enum(alpha, slow) {
|
for rgba := range Enum(color, alpha, slow) {
|
||||||
if !yield(m.Convert(rgba).(C)) {
|
if !yield(m.Convert(rgba).(C)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -24,35 +24,39 @@ func eqRGBA64(a, b color.RGBA64) bool {
|
|||||||
|
|
||||||
func TestEnum(t *testing.T) {
|
func TestEnum(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
alpha, slow bool
|
color, alpha, slow bool
|
||||||
expectedCount int
|
expectedCount int
|
||||||
}{
|
}{
|
||||||
{true, true, 87481},
|
{true, true, true, 87481},
|
||||||
{false, true, 140608},
|
{true, false, true, 140608},
|
||||||
{true, false, 649},
|
{true, true, false, 649},
|
||||||
{false, false, 216},
|
{true, false, false, 216},
|
||||||
|
{false, true, true, 65551},
|
||||||
|
{false, false, true, 65536},
|
||||||
|
{false, true, false, 271},
|
||||||
|
{false, false, false, 258},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(fmt.Sprintf("alpha=%v, slow=%v", tt.alpha, tt.slow), func(t *testing.T) {
|
t.Run(fmt.Sprintf("color=%v, alpha=%v, slow=%v", tt.color, tt.alpha, tt.slow), func(t *testing.T) {
|
||||||
t.Run("sequence meets expected criteria", func(t *testing.T) {
|
t.Run("sequence meets expected criteria", func(t *testing.T) {
|
||||||
list := slices.Collect(Enum(tt.alpha, tt.slow))
|
list := slices.Collect(Enum(tt.color, tt.alpha, tt.slow))
|
||||||
gotCount := len(list)
|
gotCount := len(list)
|
||||||
if gotCount != tt.expectedCount {
|
if gotCount != tt.expectedCount {
|
||||||
t.Errorf("Enum(%v, %v) returned %d items, wanted %d", tt.alpha, tt.slow, gotCount, tt.expectedCount)
|
t.Errorf("Enum(%v, %v, %v) returned %d items, wanted %d", tt.color, tt.alpha, tt.slow, gotCount, tt.expectedCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(list, cmpRGBA64)
|
slices.SortFunc(list, cmpRGBA64)
|
||||||
list = slices.CompactFunc(list, eqRGBA64)
|
list = slices.CompactFunc(list, eqRGBA64)
|
||||||
|
|
||||||
if len(list) != gotCount {
|
if len(list) != gotCount {
|
||||||
t.Errorf("Enum(%v, %v) returned %d duplicate items", tt.alpha, tt.slow, gotCount-len(list))
|
t.Errorf("Enum(%v, %v, %v) returned %d duplicate items", tt.color, tt.alpha, tt.slow, gotCount-len(list))
|
||||||
}
|
}
|
||||||
|
|
||||||
listHasAlpha := false
|
listHasAlpha := false
|
||||||
for _, c := range list {
|
for _, c := range list {
|
||||||
if c.A != 0xffff {
|
if c.A != 0xffff {
|
||||||
if !tt.alpha {
|
if !tt.alpha {
|
||||||
t.Errorf("Enum(%v, %v) returned non-opaque color: %v", tt.alpha, tt.slow, c)
|
t.Errorf("Enum(%v, %v, %v) returned non-opaque color: %v", tt.color, tt.alpha, tt.slow, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
listHasAlpha = true
|
listHasAlpha = true
|
||||||
@ -61,14 +65,14 @@ func TestEnum(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !listHasAlpha && tt.alpha {
|
if !listHasAlpha && tt.alpha {
|
||||||
t.Errorf("Enum(%v, %v) didn't return non-opaque colors", tt.alpha, tt.slow)
|
t.Errorf("Enum(%v, %v, %v) didn't return non-opaque colors", tt.color, tt.alpha, tt.slow)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cancel", func(t *testing.T) {
|
t.Run("cancel", func(t *testing.T) {
|
||||||
// make sure cancelling the iteration doesn't panic.
|
// make sure cancelling the iteration doesn't panic.
|
||||||
// But mostly, we want that sweet, sweet test coverage.
|
// But mostly, we want that sweet, sweet test coverage.
|
||||||
next, stop := iter.Pull(Enum(tt.alpha, tt.slow))
|
next, stop := iter.Pull(Enum(tt.color, tt.alpha, tt.slow))
|
||||||
// need to invoke next to actually have the generated be started.
|
// need to invoke next to actually have the generated be started.
|
||||||
if _, ok := next(); !ok {
|
if _, ok := next(); !ok {
|
||||||
t.Error("iteration stopped before we could cancel it")
|
t.Error("iteration stopped before we could cancel it")
|
||||||
@ -82,19 +86,19 @@ func TestEnum(t *testing.T) {
|
|||||||
|
|
||||||
func TestEnumColor(t *testing.T) {
|
func TestEnumColor(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
alpha, slow bool
|
color, alpha, slow bool
|
||||||
}{
|
}{
|
||||||
{true, true},
|
{true, true, true},
|
||||||
{false, true},
|
{true, false, true},
|
||||||
{true, false},
|
{true, true, false},
|
||||||
{false, false},
|
{true, false, false},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(fmt.Sprintf("alpha=%v, slow=%v", tt.alpha, tt.slow), func(t *testing.T) {
|
t.Run(fmt.Sprintf("color=%v, alpha=%v, slow=%v", tt.color, tt.alpha, tt.slow), func(t *testing.T) {
|
||||||
t.Run("sequence equivalence", func(t *testing.T) {
|
t.Run("sequence equivalence", func(t *testing.T) {
|
||||||
nextRGBA64, stop1 := iter.Pull(Enum(tt.alpha, tt.slow))
|
nextRGBA64, stop1 := iter.Pull(Enum(tt.color, tt.alpha, tt.slow))
|
||||||
defer stop1()
|
defer stop1()
|
||||||
nextNRGBA, stop2 := iter.Pull(EnumColor[color.NRGBA](tt.alpha, tt.slow, color.NRGBAModel))
|
nextNRGBA, stop2 := iter.Pull(EnumColor[color.NRGBA](tt.color, tt.alpha, tt.slow, color.NRGBAModel))
|
||||||
defer stop2()
|
defer stop2()
|
||||||
|
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
@ -119,7 +123,7 @@ func TestEnumColor(t *testing.T) {
|
|||||||
t.Run("cancel", func(t *testing.T) {
|
t.Run("cancel", func(t *testing.T) {
|
||||||
// make sure cancelling the iteration doesn't panic.
|
// make sure cancelling the iteration doesn't panic.
|
||||||
// But mostly, we want that sweet, sweet test coverage.
|
// But mostly, we want that sweet, sweet test coverage.
|
||||||
next, stop := iter.Pull(EnumColor[color.NRGBA](tt.alpha, tt.slow, color.NRGBAModel))
|
next, stop := iter.Pull(EnumColor[color.NRGBA](tt.color, tt.alpha, tt.slow, color.NRGBAModel))
|
||||||
// need to invoke next to actually have the generated be started.
|
// need to invoke next to actually have the generated be started.
|
||||||
if _, ok := next(); !ok {
|
if _, ok := next(); !ok {
|
||||||
t.Error("iteration stopped before we could cancel it")
|
t.Error("iteration stopped before we could cancel it")
|
||||||
|
@ -4,8 +4,11 @@ import "math"
|
|||||||
|
|
||||||
// Linearize converts an sRGB component in the range [0, 0xffff] to a linearRGB component in the range [0, 1].
|
// Linearize converts an sRGB component in the range [0, 0xffff] to a linearRGB component in the range [0, 1].
|
||||||
func Linearize(c uint32) float64 {
|
func Linearize(c uint32) float64 {
|
||||||
l := float64(c) / 0xffff
|
return LinearizeF(float64(c) / 0xffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinearizeF converts an sRGB component in the range [0, 1] to a linearRGB component in the range [0, 1].
|
||||||
|
func LinearizeF(l float64) float64 {
|
||||||
if l <= 0.039285714285714285714285714285714 {
|
if l <= 0.039285714285714285714285714285714 {
|
||||||
return l / 12.923210180787861094641554898407
|
return l / 12.923210180787861094641554898407
|
||||||
}
|
}
|
||||||
@ -26,3 +29,12 @@ func Delinearize(l float64) uint32 {
|
|||||||
return uint32(69139.425*math.Pow(l, 1/2.4) - 3603.925)
|
return uint32(69139.425*math.Pow(l, 1/2.4) - 3603.925)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delinearize converts a linearRGB component in the range [0, 1] to an sRGB component in the range [0, 1].
|
||||||
|
func DelinearizeF(l float64) float64 {
|
||||||
|
if l <= 0.0030399346397784299969770436366690 {
|
||||||
|
return l * 12.923210180787861094641554898407
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.055*math.Pow(l, 1/2.4) - 0.055
|
||||||
|
}
|
||||||
|
@ -46,6 +46,30 @@ func TestLinearize(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLinearizeF(t *testing.T) {
|
||||||
|
// the positive finites were indirectly tested by TestLinearize, so we'll just test the
|
||||||
|
// negative and non-finite inputs.
|
||||||
|
tests := []struct {
|
||||||
|
value float64
|
||||||
|
want float64
|
||||||
|
}{
|
||||||
|
{-1, -1 / 12.923210180787861094641554898407},
|
||||||
|
{0, 0},
|
||||||
|
{1, 1},
|
||||||
|
{2, 4.9538457515920408157613451180477},
|
||||||
|
{math.Inf(-1), math.Inf(-1)},
|
||||||
|
{math.Inf(1), math.Inf(1)},
|
||||||
|
{math.NaN(), math.NaN()},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%x", tt.value), func(t *testing.T) {
|
||||||
|
if got := LinearizeF(tt.value); !EqFloat64Fuzzy(got, tt.want) {
|
||||||
|
t.Errorf("LinearizeF(%x) = %x: want %x", tt.value, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDelinearize(t *testing.T) {
|
func TestDelinearize(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
value float64
|
value float64
|
||||||
@ -82,3 +106,35 @@ func TestDelinearize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDelinearizeF(t *testing.T) {
|
||||||
|
// values that would have been impossible for Delinearize to represent.
|
||||||
|
tests := []struct {
|
||||||
|
value float64
|
||||||
|
want float64
|
||||||
|
}{
|
||||||
|
{-1, -12.923210180787861094641554898407},
|
||||||
|
{math.Inf(-1), math.Inf(-1)},
|
||||||
|
{math.Inf(1), math.Inf(1)},
|
||||||
|
{math.NaN(), math.NaN()},
|
||||||
|
{2, 1.3532560461493862548965276346496},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%x", tt.value), func(t *testing.T) {
|
||||||
|
if got := DelinearizeF(tt.value); !EqFloat64Fuzzy(got, tt.want) {
|
||||||
|
t.Errorf("DelinearizeF(%x) = %x: want %x", tt.value, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("roundtrip conversions", func(t *testing.T) {
|
||||||
|
for c := -0x1000; c < 0x11000; c++ {
|
||||||
|
f := float64(c) / 0x10000
|
||||||
|
got := DelinearizeF(LinearizeF(f))
|
||||||
|
if !EqFloat64Fuzzy(got, f) {
|
||||||
|
t.Errorf("DelinearizeF(LinearizeF(%x)) != %x", c, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
_color "image/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Model[C color.Color](fromColor func(color.Color) C) color.Model {
|
func Model[C _color.Color](fromColor func(_color.Color) C) _color.Model {
|
||||||
return color.ModelFunc(func(c color.Color) color.Color {
|
return _color.ModelFunc(func(c _color.Color) _color.Color {
|
||||||
return fromColor(c)
|
return fromColor(c)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@ func Model[C color.Color](fromColor func(color.Color) C) color.Model {
|
|||||||
// Interface that the colours used in this package are expected to implement.
|
// Interface that the colours used in this package are expected to implement.
|
||||||
type Color interface {
|
type Color interface {
|
||||||
comparable
|
comparable
|
||||||
color.Color
|
_color.Color
|
||||||
NRGBA() (r, g, b, a uint32)
|
NRGBA() (r, g, b, a uint32)
|
||||||
NLRGBA() (r, g, b, a float64)
|
NLRGBA() (r, g, b, a float64)
|
||||||
NXYZA() (x, y, z, a float64)
|
NXYZA() (x, y, z, a float64)
|
||||||
@ -22,13 +22,13 @@ type Color interface {
|
|||||||
|
|
||||||
type ConvertTest[C Color] struct {
|
type ConvertTest[C Color] struct {
|
||||||
Name string
|
Name string
|
||||||
In color.Color
|
In _color.Color
|
||||||
Out C
|
Out C
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestModel[T tester[T], C Color](t T, alpha bool, m color.Model, eq func(c0, c1 C) bool, extra []ConvertTest[C]) {
|
func TestModel[T tester[T], C Color](t T, color, alpha bool, m _color.Model, eq func(c0, c1 C) bool, extra []ConvertTest[C]) {
|
||||||
t.Run("legal colours", func(t T) {
|
t.Run("legal colours", func(t T) {
|
||||||
for wantRGBA := range Enum(alpha, true) {
|
for wantRGBA := range Enum(color, alpha, true) {
|
||||||
_gotC := m.Convert(wantRGBA)
|
_gotC := m.Convert(wantRGBA)
|
||||||
gotC, ok := _gotC.(C)
|
gotC, ok := _gotC.(C)
|
||||||
|
|
||||||
@ -38,16 +38,16 @@ func TestModel[T tester[T], C Color](t T, alpha bool, m color.Model, eq func(c0,
|
|||||||
}
|
}
|
||||||
|
|
||||||
r, g, b, a := gotC.RGBA()
|
r, g, b, a := gotC.RGBA()
|
||||||
gotRGBA := color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}
|
gotRGBA := _color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}
|
||||||
|
|
||||||
if gotRGBA != wantRGBA {
|
if gotRGBA != wantRGBA {
|
||||||
t.Errorf("%#+v.RGBA() = %v, want %v", gotC, gotRGBA, wantRGBA)
|
t.Errorf("%#+v.RGBA() = %v, want %v", gotC, gotRGBA, wantRGBA)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wantNRGBA := color.NRGBA64Model.Convert(wantRGBA)
|
wantNRGBA := _color.NRGBA64Model.Convert(wantRGBA)
|
||||||
r, g, b, a = gotC.NRGBA()
|
r, g, b, a = gotC.NRGBA()
|
||||||
gotNRGBA := color.NRGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}
|
gotNRGBA := _color.NRGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}
|
||||||
if gotNRGBA != wantNRGBA {
|
if gotNRGBA != wantNRGBA {
|
||||||
t.Errorf("%#+v.NRGBA() = %v, want %v", gotC, gotNRGBA, wantNRGBA)
|
t.Errorf("%#+v.NRGBA() = %v, want %v", gotC, gotNRGBA, wantNRGBA)
|
||||||
return
|
return
|
||||||
|
@ -90,31 +90,31 @@ func TestTestModel(t *testing.T) {
|
|||||||
mt := mockTester{t: t}
|
mt := mockTester{t: t}
|
||||||
|
|
||||||
mt.run("wrong colour type", func(t *mockTest) {
|
mt.run("wrong colour type", func(t *mockTest) {
|
||||||
TestModel(t, true, color.RGBAModel, eq[nrgba64], nil)
|
TestModel(t, true, true, color.RGBAModel, eq[nrgba64], nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("bad RGBA", func(t *mockTest) {
|
mt.run("bad RGBA", func(t *mockTest) {
|
||||||
TestModel(t, false, Model(convert[nrgba64BadRGBA]), eq[nrgba64BadRGBA], nil)
|
TestModel(t, true, false, Model(convert[nrgba64BadRGBA]), eq[nrgba64BadRGBA], nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("bad NRGBA", func(t *mockTest) {
|
mt.run("bad NRGBA", func(t *mockTest) {
|
||||||
TestModel(t, false, Model(convert[nrgba64BadNRGBA]), eq[nrgba64BadNRGBA], nil)
|
TestModel(t, true, false, Model(convert[nrgba64BadNRGBA]), eq[nrgba64BadNRGBA], nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("bad NLRGBA", func(t *mockTest) {
|
mt.run("bad NLRGBA", func(t *mockTest) {
|
||||||
TestModel(t, false, Model(convert[nrgba64BadNLRGBA]), eq[nrgba64BadNLRGBA], nil)
|
TestModel(t, true, false, Model(convert[nrgba64BadNLRGBA]), eq[nrgba64BadNLRGBA], nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("bad NXYZA", func(t *mockTest) {
|
mt.run("bad NXYZA", func(t *mockTest) {
|
||||||
TestModel(t, false, Model(convert[nrgba64BadNXYZA]), eq[nrgba64BadNXYZA], nil)
|
TestModel(t, true, false, Model(convert[nrgba64BadNXYZA]), eq[nrgba64BadNXYZA], nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("bad NOkLabA", func(t *mockTest) {
|
mt.run("bad NOkLabA", func(t *mockTest) {
|
||||||
TestModel(t, false, Model(convert[nrgba64BadNOkLabA]), eq[nrgba64BadNOkLabA], nil)
|
TestModel(t, true, false, Model(convert[nrgba64BadNOkLabA]), eq[nrgba64BadNOkLabA], nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
mt.run("working model", func(t *mockTest) {
|
mt.run("working model", func(t *mockTest) {
|
||||||
TestModel(t, true, Model(convert[nrgba64]), eq[nrgba64], []ConvertTest[nrgba64]{
|
TestModel(t, true, true, Model(convert[nrgba64]), eq[nrgba64], []ConvertTest[nrgba64]{
|
||||||
{"good", color.NRGBA64{0x0123, 0x4567, 0x89ab, 0xcdef}, nrgba64{color.NRGBA64{0x0123, 0x4567, 0x89ab, 0xcdef}}},
|
{"good", color.NRGBA64{0x0123, 0x4567, 0x89ab, 0xcdef}, nrgba64{color.NRGBA64{0x0123, 0x4567, 0x89ab, 0xcdef}}},
|
||||||
{"bad", color.NRGBA64{0xcafe, 0xf00d, 0x54ac, 0xce55}, nrgba64{color.NRGBA64{0x0123, 0x4567, 0x89ab, 0xcdef}}},
|
{"bad", color.NRGBA64{0xcafe, 0xf00d, 0x54ac, 0xce55}, nrgba64{color.NRGBA64{0x0123, 0x4567, 0x89ab, 0xcdef}}},
|
||||||
})
|
})
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLMSToXYZ(t *testing.T) {
|
func TestLMSToXYZ(t *testing.T) {
|
||||||
for c := range Enum(false, true) {
|
for c := range Enum(true, false, true) {
|
||||||
want := collect3(LRGBtoXYZ(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B))))
|
want := collect3(LRGBtoXYZ(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B))))
|
||||||
|
|
||||||
if got := collect3(LMStoXYZ(XYZtoLMS(want[0], want[1], want[2]))); !EqFloat64SliceFuzzy(want[:], got[:]) {
|
if got := collect3(LMStoXYZ(XYZtoLMS(want[0], want[1], want[2]))); !EqFloat64SliceFuzzy(want[:], got[:]) {
|
||||||
@ -19,7 +19,7 @@ func TestLMSToXYZ(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLMSToLRGB(t *testing.T) {
|
func TestLMSToLRGB(t *testing.T) {
|
||||||
for c := range Enum(false, true) {
|
for c := range Enum(true, false, true) {
|
||||||
want := collect3(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B)))
|
want := collect3(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B)))
|
||||||
|
|
||||||
l, m, s := LRGBtoLMS(want[0], want[1], want[2])
|
l, m, s := LRGBtoLMS(want[0], want[1], want[2])
|
||||||
@ -39,7 +39,7 @@ func TestLMSToLRGB(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOKLabToLMS(t *testing.T) {
|
func TestOKLabToLMS(t *testing.T) {
|
||||||
for c := range Enum(false, true) {
|
for c := range Enum(true, false, true) {
|
||||||
want := collect3(LRGBtoLMS(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B))))
|
want := collect3(LRGBtoLMS(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B))))
|
||||||
if got := collect3(OkLabToLMS(LMStoOkLab(want[0], want[1], want[2]))); !EqFloat64SliceFuzzy(want[:], got[:]) {
|
if got := collect3(OkLabToLMS(LMStoOkLab(want[0], want[1], want[2]))); !EqFloat64SliceFuzzy(want[:], got[:]) {
|
||||||
t.Errorf("OkLabToLMS(LMStoOKLab(%v)) = %v, want unchanged", want, got)
|
t.Errorf("OkLabToLMS(LMStoOKLab(%v)) = %v, want unchanged", want, got)
|
||||||
|
@ -61,12 +61,6 @@ func (s *testStatus) setPanic(v any) {
|
|||||||
s.panicValue = v
|
s.panicValue = v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *testStatus) getPanic() any {
|
|
||||||
s.m.Lock()
|
|
||||||
defer s.m.Unlock()
|
|
||||||
return s.panicValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *testStatus) setHandled() {
|
func (s *testStatus) setHandled() {
|
||||||
s.m.Lock()
|
s.m.Lock()
|
||||||
defer s.m.Unlock()
|
defer s.m.Unlock()
|
||||||
@ -217,22 +211,6 @@ func (m *mockTester) expectFailedChildren(name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// panic if the named test doesn't exist or didn't panic (and returns the panic value)
|
|
||||||
func (m *mockTester) expectPanic(name string) any {
|
|
||||||
if s := m.get(name); s != nil {
|
|
||||||
s.setHandled()
|
|
||||||
|
|
||||||
if r := s.getPanic(); r != nil {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
m.t.Errorf("%s: didn't panic", name)
|
|
||||||
s.log(m.t, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// panic if the named test doesn't exist or has failed.
|
// panic if the named test doesn't exist or has failed.
|
||||||
func (m *mockTester) expectSuccess(name string) {
|
func (m *mockTester) expectSuccess(name string) {
|
||||||
if s := m.get(name); s != nil {
|
if s := m.get(name); s != nil {
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestXYZtoLRGB(t *testing.T) {
|
func TestXYZtoLRGB(t *testing.T) {
|
||||||
for c := range Enum(false, true) {
|
for c := range Enum(true, false, true) {
|
||||||
want := collect3(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B)))
|
want := collect3(RGBtoLRGB(uint32(c.R), uint32(c.G), uint32(c.B)))
|
||||||
if got := collect3(XYZtoLRGB(LRGBtoXYZ(want[0], want[1], want[2]))); !EqFloat64SliceFuzzy(want[:], got[:]) {
|
if got := collect3(XYZtoLRGB(LRGBtoXYZ(want[0], want[1], want[2]))); !EqFloat64SliceFuzzy(want[:], got[:]) {
|
||||||
t.Errorf("XYZtoLRGB(LRGBtoXYZ(%v)) = %v, want unchanged", want, got)
|
t.Errorf("XYZtoLRGB(LRGBtoXYZ(%v)) = %v, want unchanged", want, got)
|
||||||
|
82
lgray/lgray.go
Normal file
82
lgray/lgray.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Provides a [color.Color] type for dealing with linear grayscale colours without alpha.
|
||||||
|
package lgray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"smariot.com/color/internal/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color is a linear grayscale [color.Color].
|
||||||
|
type Color struct {
|
||||||
|
Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceSqr returns the euclidean distance squared between two colours.
|
||||||
|
func DistanceSqr(a, b Color) float64 {
|
||||||
|
dY := a.Y - b.Y
|
||||||
|
return dY * dY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance returns the euclidean distance between two colours,
|
||||||
|
//
|
||||||
|
// If you just want to compare relative distances, use [DistanceSqr] instead.
|
||||||
|
func Distance(a, b Color) float64 {
|
||||||
|
return math.Abs(a.Y - b.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA converts to premultiplied RGBA, implementing the [color.Color] interface.
|
||||||
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
||||||
|
_y := helper.Delinearize(c.Y)
|
||||||
|
return _y, _y, _y, 0xffff
|
||||||
|
}
|
||||||
|
|
||||||
|
// NRGBA converts to non-premultiplied RGBA.
|
||||||
|
func (c Color) NRGBA() (r, g, b, a uint32) {
|
||||||
|
_y := helper.Delinearize(c.Y)
|
||||||
|
return _y, _y, _y, 0xffff
|
||||||
|
}
|
||||||
|
|
||||||
|
// NLRGBA converts to non-premultiplied linear RGBA.
|
||||||
|
func (c Color) NLRGBA() (r, g, b, a float64) {
|
||||||
|
return c.Y, c.Y, c.Y, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// NXYZA converts to non-premultiplied XYZ+Alpha.
|
||||||
|
func (c Color) NXYZA() (x, y, z, a float64) {
|
||||||
|
x, y, z = helper.LRGBtoXYZ(c.Y, c.Y, c.Y)
|
||||||
|
return x, y, z, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOkLabA converts to non-premultiplied OkLab+Alpha.
|
||||||
|
func (c Color) NOkLabA() (lightness, chromaA, chromaB, a float64) {
|
||||||
|
lightness, chromaA, chromaB = helper.LMStoOkLab(helper.LRGBtoLMS(c.Y, c.Y, c.Y))
|
||||||
|
return lightness, chromaA, chromaB, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert converts an arbitrary colour type to a linear RGB [Color].
|
||||||
|
func Convert(c color.Color) Color {
|
||||||
|
if c, ok := c.(Color); ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
r, g, b, _ := helper.ColorToNLRGBA(c)
|
||||||
|
|
||||||
|
// the color.Gray16Model documents that it uses the coefficients 0.299, 0.5867, and 0.114.
|
||||||
|
// however, it does this using integer arithmetic, so the actual coefficients are effectively rounded to the nearest 1/0x10000.
|
||||||
|
|
||||||
|
return Color{Y: helper.DelinearizeF(
|
||||||
|
helper.LinearizeF(r)*0x0.4c8bp0 +
|
||||||
|
helper.LinearizeF(g)*0x0.9646p0 +
|
||||||
|
helper.LinearizeF(b)*0x0.1d2fp0,
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A [color.Model] for converting arbitrary colours to a linear RGB [Color].
|
||||||
|
//
|
||||||
|
// Wraps the [Convert] function, returning a [color.Color] interface rather than the [Color] type.
|
||||||
|
var Model = helper.Model(Convert)
|
||||||
|
|
||||||
|
// Type assertion.
|
||||||
|
var _ color.Color = Color{}
|
47
lgray/lgray_test.go
Normal file
47
lgray/lgray_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package lgray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"smariot.com/color/internal/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func eq(c0, c1 Color) bool {
|
||||||
|
return helper.EqFloat64Fuzzy(c0.Y, c1.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func midpoint(c0, c1 Color) Color {
|
||||||
|
return Color{(c0.Y + c1.Y) / 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel(t *testing.T) {
|
||||||
|
helper.TestModel(t, false, false, Model, eq, []helper.ConvertTest[Color]{
|
||||||
|
{
|
||||||
|
Name: "passthrough +inf",
|
||||||
|
In: Color{math.Inf(1)},
|
||||||
|
Out: Color{math.Inf(1)},
|
||||||
|
}, {
|
||||||
|
Name: "passthrough -inf",
|
||||||
|
In: Color{math.Inf(-1)},
|
||||||
|
Out: Color{math.Inf(-1)},
|
||||||
|
}, {
|
||||||
|
Name: "passthrough nan",
|
||||||
|
In: Color{math.NaN()},
|
||||||
|
Out: Color{math.NaN()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(a, b Color) float64 {
|
||||||
|
d := Distance(a, b)
|
||||||
|
dSqr := DistanceSqr(a, b)
|
||||||
|
if !helper.EqFloat64Fuzzy(d*d, dSqr) {
|
||||||
|
panic("Distance and DistanceSqr are not equivalent")
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDistance(t *testing.T) {
|
||||||
|
helper.TestDistance(t, false, false, midpoint, distance, Model)
|
||||||
|
}
|
123
lgraya/lgraya.go
Normal file
123
lgraya/lgraya.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Provides a [color.Color] type for dealing with premultiplied linear grayscale+alpha colours.
|
||||||
|
package lgraya
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"smariot.com/color/internal/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color is a pre-multiplied linear grayscale+alpha [color.Color].
|
||||||
|
type Color struct {
|
||||||
|
Y, A float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqr(a float64) float64 {
|
||||||
|
return a * a
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceSqr returns the maximum possible euclidean distance squared between two colours,
|
||||||
|
// accounting for the possible backgrounds they might be composited over.
|
||||||
|
func DistanceSqr(a, b Color) float64 {
|
||||||
|
dY := a.Y - b.Y
|
||||||
|
dA := a.A - b.A
|
||||||
|
return max(sqr(dY), sqr(dY+dA))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance returns the maximum possible euclidean distance between two colours,
|
||||||
|
// accounting for the possible backgrounds they might be composited over.
|
||||||
|
func Distance(a, b Color) float64 {
|
||||||
|
dY := a.Y - b.Y
|
||||||
|
dA := a.A - b.A
|
||||||
|
return max(math.Abs(dY), math.Abs(dY+dA))
|
||||||
|
}
|
||||||
|
|
||||||
|
// YA converts to premultiplied sRGB grayscale+alpha.
|
||||||
|
func (c Color) YA() (y, a uint32) {
|
||||||
|
y, a = c.NYA()
|
||||||
|
y = y * a / 0xffff
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NYA converts to non-premultiplied sRGB grayscale+alpha.
|
||||||
|
func (c Color) NYA() (y, a uint32) {
|
||||||
|
_y, _a := c.NLYA()
|
||||||
|
return helper.Delinearize(_y), uint32(_a*0xffff + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NLYA converts to non-premultiplied linear grayscale+alpha.
|
||||||
|
func (c Color) NLYA() (y, a float64) {
|
||||||
|
if c.A <= 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Y / c.A, c.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA converts to premultiplied RGBA, implementing the [color.Color] interface.
|
||||||
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
||||||
|
y, a := c.YA()
|
||||||
|
return y, y, y, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// NRGBA converts to non-premultiplied RGBA.
|
||||||
|
func (c Color) NRGBA() (r, g, b, a uint32) {
|
||||||
|
y, a := c.NYA()
|
||||||
|
return y, y, y, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// NLRGBA converts to non-premultiplied linear RGBA.
|
||||||
|
func (c Color) NLRGBA() (r, g, b, a float64) {
|
||||||
|
y, a := c.NLYA()
|
||||||
|
return y, y, y, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// NXYZA converts to non-premultiplied XYZ+Alpha.
|
||||||
|
func (c Color) NXYZA() (x, y, z, a float64) {
|
||||||
|
if c.A <= 0 {
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y, z = helper.LRGBtoXYZ(c.Y/c.A, c.Y/c.A, c.Y/c.A)
|
||||||
|
return x, y, z, c.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOkLabA converts to non-premultiplied OkLab+Alpha.
|
||||||
|
func (c Color) NOkLabA() (lightness, chromaA, chromaB, a float64) {
|
||||||
|
if c.A <= 0 {
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lightness, chromaA, chromaB = helper.LMStoOkLab(helper.LRGBtoLMS(c.Y/c.A, c.Y/c.A, c.Y/c.A))
|
||||||
|
return lightness, chromaA, chromaB, c.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert converts an arbitrary colour type to a premultiplied linear RGBA [Color].
|
||||||
|
func Convert(c color.Color) Color {
|
||||||
|
if c, ok := c.(Color); ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
r, g, b, a := helper.ColorToNLRGBA(c)
|
||||||
|
|
||||||
|
// the color.Gray16Model documents that it uses the coefficients 0.299, 0.5867, and 0.114.
|
||||||
|
// however, it does this using integer arithmetic, so the actual coefficients are effectively rounded to the nearest 1/0x10000.
|
||||||
|
|
||||||
|
return Color{
|
||||||
|
Y: helper.DelinearizeF(
|
||||||
|
helper.LinearizeF(r)*0x0.4c8bp0+
|
||||||
|
helper.LinearizeF(g)*0x0.9646p0+
|
||||||
|
helper.LinearizeF(b)*0x0.1d2fp0,
|
||||||
|
) * a,
|
||||||
|
A: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A [color.Model] for converting arbitrary colours to a premultiplied linear RGBA [Color].
|
||||||
|
//
|
||||||
|
// Wraps the [Convert] function, returning a [color.Color] interface rather than the [Color] type.
|
||||||
|
var Model = helper.Model(Convert)
|
||||||
|
|
||||||
|
// Type assertion.
|
||||||
|
var _ color.Color = Color{}
|
53
lgraya/lgraya_test.go
Normal file
53
lgraya/lgraya_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package lgraya
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"smariot.com/color/internal/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func eq(c0, c1 Color) bool {
|
||||||
|
return helper.EqFloat64SliceFuzzy(
|
||||||
|
[]float64{c0.Y, c0.A},
|
||||||
|
[]float64{c1.Y, c1.A},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func midpoint(c0, c1 Color) Color {
|
||||||
|
return Color{(c0.Y + c1.Y) / 2, (c0.A + c1.A) / 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel(t *testing.T) {
|
||||||
|
helper.TestModel(t, false, true, Model, eq, []helper.ConvertTest[Color]{
|
||||||
|
// These is a very illegal colour. If it makes it through
|
||||||
|
// unchanged, we can be reasonably confident no colour space conversions were
|
||||||
|
// attempted.
|
||||||
|
{
|
||||||
|
Name: "passthrough +inf",
|
||||||
|
In: Color{math.Inf(1), 0},
|
||||||
|
Out: Color{math.Inf(1), 0},
|
||||||
|
}, {
|
||||||
|
Name: "passthrough +inf",
|
||||||
|
In: Color{math.Inf(-1), math.NaN()},
|
||||||
|
Out: Color{math.Inf(-1), math.NaN()},
|
||||||
|
}, {
|
||||||
|
Name: "passthrough nan",
|
||||||
|
In: Color{math.NaN(), math.Inf(1)},
|
||||||
|
Out: Color{math.NaN(), math.Inf(1)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(a, b Color) float64 {
|
||||||
|
d := Distance(a, b)
|
||||||
|
dSqr := DistanceSqr(a, b)
|
||||||
|
if !helper.EqFloat64Fuzzy(d*d, dSqr) {
|
||||||
|
panic("Distance and DistanceSqr are not equivalent")
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDistance(t *testing.T) {
|
||||||
|
helper.TestDistance(t, false, true, midpoint, distance, Model)
|
||||||
|
}
|
@ -19,7 +19,7 @@ func midpoint(c0, c1 Color) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModel(t *testing.T) {
|
func TestModel(t *testing.T) {
|
||||||
helper.TestModel(t, false, Model, eq, []helper.ConvertTest[Color]{
|
helper.TestModel(t, true, false, Model, eq, []helper.ConvertTest[Color]{
|
||||||
{
|
{
|
||||||
Name: "passthrough",
|
Name: "passthrough",
|
||||||
In: Color{math.Inf(1), math.Inf(-1), math.NaN()},
|
In: Color{math.Inf(1), math.Inf(-1), math.NaN()},
|
||||||
@ -29,5 +29,5 @@ func TestModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDistance(t *testing.T) {
|
func TestDistance(t *testing.T) {
|
||||||
helper.TestDistance(t, false, midpoint, Distance, Model)
|
helper.TestDistance(t, true, false, midpoint, Distance, Model)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ func midpoint(c0, c1 Color) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModel(t *testing.T) {
|
func TestModel(t *testing.T) {
|
||||||
helper.TestModel(t, true, Model, eq, []helper.ConvertTest[Color]{
|
helper.TestModel(t, true, true, Model, eq, []helper.ConvertTest[Color]{
|
||||||
{
|
{
|
||||||
Name: "passthrough",
|
Name: "passthrough",
|
||||||
// These is a very illegal colour. If it makes it through
|
// These is a very illegal colour. If it makes it through
|
||||||
@ -32,5 +32,5 @@ func TestModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDistance(t *testing.T) {
|
func TestDistance(t *testing.T) {
|
||||||
helper.TestDistance(t, true, midpoint, Distance, Model)
|
helper.TestDistance(t, true, true, midpoint, Distance, Model)
|
||||||
}
|
}
|
||||||
|
111
nlgraya/nlgraya.go
Normal file
111
nlgraya/nlgraya.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// Provides a [color.Color] type for dealing with non-premultiplied linear grayscale+alpha colours.
|
||||||
|
package nlgraya
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"smariot.com/color/internal/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color is a non-premultiplied linear grayscale+alpha [color.Color].
|
||||||
|
type Color struct {
|
||||||
|
Y, A float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqr(a float64) float64 {
|
||||||
|
return a * a
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceSqr returns the maximum possible euclidean distance squared between two colours,
|
||||||
|
// accounting for the possible backgrounds they might be composited over.
|
||||||
|
func DistanceSqr(a, b Color) float64 {
|
||||||
|
dY := a.Y*a.A - b.Y*b.A
|
||||||
|
dA := a.A - b.A
|
||||||
|
return max(sqr(dY), sqr(dY+dA))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance returns the maximum possible euclidean distance between two colours,
|
||||||
|
// accounting for the possible backgrounds they might be composited over.
|
||||||
|
func Distance(a, b Color) float64 {
|
||||||
|
dY := a.Y*a.A - b.Y*b.A
|
||||||
|
dA := a.A - b.A
|
||||||
|
return max(math.Abs(dY), math.Abs(dY+dA))
|
||||||
|
}
|
||||||
|
|
||||||
|
// YA converts to premultiplied sRGB grayscale+alpha.
|
||||||
|
func (c Color) YA() (y, a uint32) {
|
||||||
|
y, a = c.NYA()
|
||||||
|
y = y * a / 0xffff
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NYA converts to non-premultiplied sRGB grayscale+alpha.
|
||||||
|
func (c Color) NYA() (y, a uint32) {
|
||||||
|
_y, _a := c.NLYA()
|
||||||
|
return helper.Delinearize(_y), uint32(_a*0xffff + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NLYA converts to non-premultiplied linear grayscale+alpha.
|
||||||
|
func (c Color) NLYA() (y, a float64) {
|
||||||
|
return c.Y, c.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA converts to premultiplied RGBA, implementing the [color.Color] interface.
|
||||||
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
||||||
|
y, a := c.YA()
|
||||||
|
return y, y, y, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// NRGBA converts to non-premultiplied RGBA.
|
||||||
|
func (c Color) NRGBA() (r, g, b, a uint32) {
|
||||||
|
y, a := c.NYA()
|
||||||
|
return y, y, y, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// NLRGBA converts to non-premultiplied linear RGBA.
|
||||||
|
func (c Color) NLRGBA() (r, g, b, a float64) {
|
||||||
|
y, a := c.NLYA()
|
||||||
|
return y, y, y, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// NXYZA converts to non-premultiplied XYZ+Alpha.
|
||||||
|
func (c Color) NXYZA() (x, y, z, a float64) {
|
||||||
|
x, y, z = helper.LRGBtoXYZ(c.Y, c.Y, c.Y)
|
||||||
|
return x, y, z, c.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOkLabA converts to non-premultiplied OkLab+Alpha.
|
||||||
|
func (c Color) NOkLabA() (lightness, chromaA, chromaB, a float64) {
|
||||||
|
lightness, chromaA, chromaB = helper.LMStoOkLab(helper.LRGBtoLMS(c.Y, c.Y, c.Y))
|
||||||
|
return lightness, chromaA, chromaB, c.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert converts an arbitrary colour type to a premultiplied linear RGBA [Color].
|
||||||
|
func Convert(c color.Color) Color {
|
||||||
|
if c, ok := c.(Color); ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
r, g, b, a := helper.ColorToNLRGBA(c)
|
||||||
|
|
||||||
|
// the color.Gray16Model documents that it uses the coefficients 0.299, 0.5867, and 0.114.
|
||||||
|
// however, it does this using integer arithmetic, so the actual coefficients are effectively rounded to the nearest 1/0x10000.
|
||||||
|
|
||||||
|
return Color{
|
||||||
|
Y: helper.DelinearizeF(
|
||||||
|
helper.LinearizeF(r)*0x0.4c8bp0 +
|
||||||
|
helper.LinearizeF(g)*0x0.9646p0 +
|
||||||
|
helper.LinearizeF(b)*0x0.1d2fp0,
|
||||||
|
),
|
||||||
|
A: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A [color.Model] for converting arbitrary colours to a premultiplied linear RGBA [Color].
|
||||||
|
//
|
||||||
|
// Wraps the [Convert] function, returning a [color.Color] interface rather than the [Color] type.
|
||||||
|
var Model = helper.Model(Convert)
|
||||||
|
|
||||||
|
// Type assertion.
|
||||||
|
var _ color.Color = Color{}
|
53
nlgraya/nlgraya_test.go
Normal file
53
nlgraya/nlgraya_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package nlgraya
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"smariot.com/color/internal/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func eq(c0, c1 Color) bool {
|
||||||
|
return helper.EqFloat64SliceFuzzy(
|
||||||
|
[]float64{c0.Y, c0.A},
|
||||||
|
[]float64{c1.Y, c1.A},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func midpoint(c0, c1 Color) Color {
|
||||||
|
return Color{(c0.Y + c1.Y) / 2, (c0.A + c1.A) / 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModel(t *testing.T) {
|
||||||
|
helper.TestModel(t, false, true, Model, eq, []helper.ConvertTest[Color]{
|
||||||
|
// These is a very illegal colour. If it makes it through
|
||||||
|
// unchanged, we can be reasonably confident no colour space conversions were
|
||||||
|
// attempted.
|
||||||
|
{
|
||||||
|
Name: "passthrough +inf",
|
||||||
|
In: Color{math.Inf(1), 0},
|
||||||
|
Out: Color{math.Inf(1), 0},
|
||||||
|
}, {
|
||||||
|
Name: "passthrough +inf",
|
||||||
|
In: Color{math.Inf(-1), math.NaN()},
|
||||||
|
Out: Color{math.Inf(-1), math.NaN()},
|
||||||
|
}, {
|
||||||
|
Name: "passthrough nan",
|
||||||
|
In: Color{math.NaN(), math.Inf(1)},
|
||||||
|
Out: Color{math.NaN(), math.Inf(1)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func distance(a, b Color) float64 {
|
||||||
|
d := Distance(a, b)
|
||||||
|
dSqr := DistanceSqr(a, b)
|
||||||
|
if !helper.EqFloat64Fuzzy(d*d, dSqr) {
|
||||||
|
panic("Distance and DistanceSqr are not equivalent")
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDistance(t *testing.T) {
|
||||||
|
helper.TestDistance(t, false, true, midpoint, distance, Model)
|
||||||
|
}
|
@ -19,7 +19,7 @@ func midpoint(c0, c1 Color) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModel(t *testing.T) {
|
func TestModel(t *testing.T) {
|
||||||
helper.TestModel(t, true, Model, eq, []helper.ConvertTest[Color]{
|
helper.TestModel(t, true, true, Model, eq, []helper.ConvertTest[Color]{
|
||||||
{
|
{
|
||||||
Name: "passthrough",
|
Name: "passthrough",
|
||||||
// These is a very illegal colour. If it makes it through
|
// These is a very illegal colour. If it makes it through
|
||||||
@ -32,5 +32,5 @@ func TestModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDistance(t *testing.T) {
|
func TestDistance(t *testing.T) {
|
||||||
helper.TestDistance(t, true, midpoint, Distance, Model)
|
helper.TestDistance(t, true, true, midpoint, Distance, Model)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ func midpoint(c0, c1 Color) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModel(t *testing.T) {
|
func TestModel(t *testing.T) {
|
||||||
helper.TestModel(t, true, Model, eq, []helper.ConvertTest[Color]{
|
helper.TestModel(t, true, true, Model, eq, []helper.ConvertTest[Color]{
|
||||||
{
|
{
|
||||||
Name: "passthrough",
|
Name: "passthrough",
|
||||||
// These is a very illegal colour. If it makes it through
|
// These is a very illegal colour. If it makes it through
|
||||||
@ -32,5 +32,5 @@ func TestModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDistance(t *testing.T) {
|
func TestDistance(t *testing.T) {
|
||||||
helper.TestDistance(t, true, midpoint, Distance, Model)
|
helper.TestDistance(t, true, true, midpoint, Distance, Model)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ func midpoint(c0, c1 Color) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModel(t *testing.T) {
|
func TestModel(t *testing.T) {
|
||||||
helper.TestModel(t, false, Model, eq, []helper.ConvertTest[Color]{
|
helper.TestModel(t, true, false, Model, eq, []helper.ConvertTest[Color]{
|
||||||
{
|
{
|
||||||
Name: "passthrough",
|
Name: "passthrough",
|
||||||
In: Color{math.Inf(1), math.Inf(-1), math.NaN()},
|
In: Color{math.Inf(1), math.Inf(-1), math.NaN()},
|
||||||
@ -29,5 +29,5 @@ func TestModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDistance(t *testing.T) {
|
func TestDistance(t *testing.T) {
|
||||||
helper.TestDistance(t, false, midpoint, Distance, Model)
|
helper.TestDistance(t, true, false, midpoint, Distance, Model)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ func midpoint(c0, c1 Color) Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModel(t *testing.T) {
|
func TestModel(t *testing.T) {
|
||||||
helper.TestModel(t, true, Model, eq, []helper.ConvertTest[Color]{
|
helper.TestModel(t, true, true, Model, eq, []helper.ConvertTest[Color]{
|
||||||
{
|
{
|
||||||
Name: "passthrough",
|
Name: "passthrough",
|
||||||
// These is a very illegal colour. If it makes it through
|
// These is a very illegal colour. If it makes it through
|
||||||
@ -32,5 +32,5 @@ func TestModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDistance(t *testing.T) {
|
func TestDistance(t *testing.T) {
|
||||||
helper.TestDistance(t, true, midpoint, Distance, Model)
|
helper.TestDistance(t, true, true, midpoint, Distance, Model)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user