Added packages for linear grayscale colour varients.
This commit is contained in:
		| @ -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,44 +1,57 @@ | |||||||
| 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 { | 			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 b := uint32(0); b <= a; b += cStep { | ||||||
| 					for g := uint32(0); g <= a; g += cStep { | 					for g := uint32(0); g <= a; g += cStep { | ||||||
| 						for r := uint32(0); r <= a; r += cStep { | 						for r := uint32(0); r <= a; r += cStep { | ||||||
| 						if !yield(color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}) { | 							if !yield(_color.RGBA64{uint16(r), uint16(g), uint16(b), uint16(a)}) { | ||||||
| 								return | 								return | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
| @ -48,10 +61,23 @@ func Enum(alpha, slow bool) iter.Seq[color.RGBA64] { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return func(yield func(_color.RGBA64) bool) { | ||||||
|  | 		for a := aStart; a <= 0xffff; a += aStep { | ||||||
|  | 			cStep := max(1, a/cDiv) | ||||||
|  |  | ||||||
|  | 			for c := uint32(0); c <= a; c += cStep { | ||||||
|  | 				if !yield(_color.RGBA64{uint16(c), uint16(c), uint16(c), uint16(a)}) { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| // 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) | ||||||
| } | } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user