Skip to content

Commit 15f8027

Browse files
committed
Merge branch 'luv' into 'main'
Add LUV support See merge request Wacton/Unicolour!9
2 parents fe2b388 + df8bfd5 commit 15f8027

32 files changed

+941
-411
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ A `Unicolour` encapsulates a single colour and its representation across differe
1414
- HSL
1515
- CIE XYZ
1616
- CIE LAB
17+
- CIE LUV
18+
- ~~CIE LCHab~~ _(coming soon)_
19+
- ~~CIE LCHuv~~ _(coming soon)_
1720
- Oklab
1821

1922
Unicolour uses sRGB as the default RGB model and standard illuminant D65 (2° observer) as the default white point of the XYZ colour space.
@@ -42,6 +45,7 @@ var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.0);
4245
var unicolour = Unicolour.FromHsl(327.6, 1.0, 0.539);
4346
var unicolour = Unicolour.FromXyz(0.47, 0.24, 0.3);
4447
var unicolour = Unicolour.FromLab(55.96, +84.54, -5.7);
48+
var unicolour = Unicolour.FromLuv(55.96, +131.47, -24.35);
4549
var unicolour = Unicolour.FromOklab(0.65, 0.26, -0.01);
4650
```
4751

@@ -52,6 +56,7 @@ var hsb = unicolour.Hsb;
5256
var hsl = unicolour.Hsl;
5357
var xyz = unicolour.Xyz;
5458
var lab = unicolour.Lab;
59+
var luv = unicolour.Luv;
5560
var oklab = unicolour.Oklab;
5661
```
5762

@@ -62,6 +67,7 @@ var interpolated = unicolour1.InterpolateHsb(unicolour2, 0.5);
6267
var interpolated = unicolour1.InterpolateHsl(unicolour2, 0.5);
6368
var interpolated = unicolour1.InterpolateXyz(unicolour2, 0.5);
6469
var interpolated = unicolour1.InterpolateLab(unicolour2, 0.5);
70+
var interpolated = unicolour1.InterpolateLuv(unicolour2, 0.5);
6571
var interpolated = unicolour1.InterpolateOklab(unicolour2, 0.5);
6672
```
6773

Unicolour.Example/Program.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@
1313
var font = fontFamily.CreateFont(24);
1414
var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF"));
1515

16-
var labels = new List<string> {"RGB", "HSB", "HSL", "XYZ", "LAB", "OKLAB"};
16+
var labels = new List<string> {"RGB", "HSB", "HSL", "XYZ", "LAB", "LUV", "OKLAB"};
1717
var purple = Unicolour.FromHsb(260, 1.0, 0.33);
1818
var orange = Unicolour.FromHsb(30, 0.66, 1.0);
19-
var black = Unicolour.FromRgb(0, 0, 0);
19+
var pink = Unicolour.FromHex("#FF1493");
2020
var cyan = Unicolour.FromRgb255(0, 255, 255);
21+
var black = Unicolour.FromRgb(0, 0, 0);
22+
var green = Unicolour.FromRgb(0, 1, 0);
2123

22-
var image = new Image<Rgba32>(gradientWidth * 2, gradientHeight * labels.Count);
24+
var image = new Image<Rgba32>(gradientWidth * 3, gradientHeight * labels.Count);
2325
Draw(purple, orange, 0);
24-
Draw(black, cyan, 1);
26+
Draw(pink, cyan, 1);
27+
Draw(black, green, 2);
28+
image.Save("gradients.png");
2529

2630
void Draw(Unicolour start, Unicolour end, int column)
2731
{
@@ -35,6 +39,7 @@ void Draw(Unicolour start, Unicolour end, int column)
3539
start.InterpolateHsl(end, distance),
3640
start.InterpolateXyz(end, distance),
3741
start.InterpolateLab(end, distance),
42+
start.InterpolateLuv(end, distance),
3843
start.InterpolateOklab(end, distance)
3944
};
4045

@@ -49,8 +54,6 @@ void Draw(Unicolour start, Unicolour end, int column)
4954
}
5055
}
5156

52-
image.Save("gradients.png");
53-
5457
void SetPixels(int column, int pixelIndex, List<Unicolour> unicolours)
5558
{
5659
for (var y = 0; y < gradientHeight; y++)

Unicolour.Example/gradients.png

15 KB
Loading

Unicolour.Tests/ConfigurationTests.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static void StandardRgbD65ToXyzD65()
4444
{
4545
Xyz = new(0.200757, 0.119618, 0.506757),
4646
Lab = new(41.1553, 51.4108, -56.4485),
47-
// Luv = new(41.1553, 16.3709, -86.7190)
47+
Luv = new(41.1553, 16.3709, -86.7190)
4848
};
4949

5050
Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrixA).Within(0.0005));
@@ -113,7 +113,7 @@ public static void StandardRgbD65ToXyzD50()
113113
{
114114
Xyz = new(0.187691, 0.115771, 0.381093),
115115
Lab = new(40.5359, 46.0847, -57.1158),
116-
// Luv = new(40.5359, 18.7523, -78.2057)
116+
Luv = new(40.5359, 18.7523, -78.2057)
117117
};
118118

119119
Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
@@ -167,7 +167,7 @@ public static void AdobeRgbD65ToXyzD65()
167167
{
168168
Xyz = new(0.234243, 0.134410, 0.535559),
169169
Lab = new(43.4203, 57.3600, -55.4259),
170-
// Luv = new(43.4203, 25.4480, -87.3268)
170+
Luv = new(43.4203, 25.4480, -87.3268)
171171
};
172172

173173
AssertColour(unicolour, expectedColour);
@@ -210,7 +210,7 @@ public static void AdobeRgbD65ToXyzD50()
210210
{
211211
Xyz = new(0.221673, 0.130920, 0.402670),
212212
Lab = new(42.9015, 52.4152, -55.9013),
213-
// Luv = new(42.9015, 29.0751, -78.5576)
213+
Luv = new(42.9015, 29.0751, -78.5576)
214214
};
215215

216216
AssertColour(unicolour, expectedColour);
@@ -253,7 +253,7 @@ public static void WideGamutRgbD50ToXyzD65()
253253
{
254254
Xyz = new(0.251993, 0.102404, 0.550393),
255255
Lab = new(38.2704, 87.2838, -65.7493),
256-
// Luv = new(38.2704, 47.3837, -99.6819)
256+
Luv = new(38.2704, 47.3837, -99.6819)
257257
};
258258

259259
AssertColour(unicolour, expectedColour);
@@ -296,7 +296,7 @@ public static void WideGamutRgbD50ToXyzD50()
296296
{
297297
Xyz = new(0.238795, 0.099490, 0.413181),
298298
Lab = new(37.7508, 82.3084, -66.1402),
299-
// Luv = new(37.7508, 55.1488, -91.6044)
299+
Luv = new(37.7508, 55.1488, -91.6044)
300300
};
301301

302302
AssertColour(unicolour, expectedColour);
@@ -327,5 +327,6 @@ private static void AssertColour(Unicolour unicolour, TestColour expected)
327327
if (expected.Rgb != null) AssertUtils.AssertColourTriplet(unicolour.Rgb.Triplet, expected.Rgb!, 0.01);
328328
if (expected.Xyz != null) AssertUtils.AssertColourTriplet(unicolour.Xyz.Triplet, expected.Xyz!, 0.001);
329329
if (expected.Lab != null) AssertUtils.AssertColourTriplet(unicolour.Lab.Triplet, expected.Lab!, 0.05);
330+
if (expected.Luv != null) AssertUtils.AssertColourTriplet(unicolour.Luv.Triplet, expected.Luv!, 0.1);
330331
}
331332
}

Unicolour.Tests/ConversionTests.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class ConversionTests
1212
private const double HslTolerance = 0.00000001;
1313
private const double XyzTolerance = 0.00000001;
1414
private const double LabTolerance = 0.00000001;
15+
private const double LuvTolerance = 0.00000001;
1516
private const double OklabTolerance = 0.000001;
1617

1718
[Test]
@@ -46,6 +47,9 @@ public void HslSameAfterDeconversion()
4647
[Test]
4748
public void LabSameAfterDeconversion() => AssertUtils.AssertRandomLabColours(AssertLabDeconversion);
4849

50+
[Test]
51+
public void LuvSameAfterDeconversion() => AssertUtils.AssertRandomLuvColours(AssertLuvDeconversion);
52+
4953
[Test]
5054
public void OklabSameAfterDeconversion() => AssertUtils.AssertRandomOklabColours(AssertOklabDeconversion);
5155

@@ -54,7 +58,7 @@ private static void AssertRgbConversion(TestColour namedColour)
5458
var systemColour = ColorTranslator.FromHtml(namedColour.Hex!);
5559
var rgb = new Rgb(systemColour.R / 255.0, systemColour.G / 255.0, systemColour.B / 255.0, Configuration.Default);
5660
var hsb = Conversion.RgbToHsb(rgb);
57-
var hsl = Conversion.RgbToHsl(rgb);
61+
var hsl = Conversion.HsbToHsl(hsb);
5862

5963
var expectedRoundedHsb = namedColour.Hsb;
6064
var expectedRoundedHsl = namedColour.Hsl;
@@ -100,16 +104,22 @@ private static void AssertHsbDeconversion(Hsb original)
100104
private static void AssertHslDeconversion(ColourTriplet triplet) => AssertHslDeconversion(new Hsl(triplet.First, triplet.Second, triplet.Third));
101105
private static void AssertHslDeconversion(Hsl original)
102106
{
103-
var deconverted = Conversion.RgbToHsl(Conversion.HsbToRgb(Conversion.HslToHsb(original), Configuration.Default));
107+
var deconverted = Conversion.HsbToHsl(Conversion.HslToHsb(original));
104108
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, HslTolerance, true);
105109
}
106110

107111
private static void AssertXyzDeconversion(ColourTriplet triplet) => AssertXyzDeconversion(new Xyz(triplet.First, triplet.Second, triplet.Third));
108112
private static void AssertXyzDeconversion(Xyz original)
109113
{
110114
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
111-
var deconverted = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default);
112-
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, XyzTolerance);
115+
var deconvertedViaLab = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default);
116+
AssertUtils.AssertColourTriplet(deconvertedViaLab.Triplet, original.Triplet, XyzTolerance);
117+
118+
var deconvertedViaLuv = Conversion.LuvToXyz(Conversion.XyzToLuv(original, Configuration.Default), Configuration.Default);
119+
AssertUtils.AssertColourTriplet(deconvertedViaLuv.Triplet, original.Triplet, XyzTolerance);
120+
121+
var deconvertedViaOklab = Conversion.OklabToXyz(Conversion.XyzToOklab(original, Configuration.Default), Configuration.Default);
122+
AssertUtils.AssertColourTriplet(deconvertedViaOklab.Triplet, original.Triplet, XyzTolerance);
113123
}
114124

115125
private static void AssertLabDeconversion(ColourTriplet triplet) => AssertLabDeconversion(new Lab(triplet.First, triplet.Second, triplet.Third));
@@ -120,6 +130,14 @@ private static void AssertLabDeconversion(Lab original)
120130
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, LabTolerance);
121131
}
122132

133+
private static void AssertLuvDeconversion(ColourTriplet triplet) => AssertLuvDeconversion(new Luv(triplet.First, triplet.Second, triplet.Third));
134+
private static void AssertLuvDeconversion(Luv original)
135+
{
136+
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
137+
var deconverted = Conversion.XyzToLuv(Conversion.LuvToXyz(original, Configuration.Default), Configuration.Default);
138+
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, LuvTolerance);
139+
}
140+
123141
private static void AssertOklabDeconversion(ColourTriplet triplet) => AssertOklabDeconversion(new Oklab(triplet.First, triplet.Second, triplet.Third));
124142
private static void AssertOklabDeconversion(Oklab original)
125143
{
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
namespace Wacton.Unicolour.Tests;
2+
3+
using System;
4+
using NUnit.Framework;
5+
using Wacton.Unicolour.Tests.Utils;
6+
7+
/*
8+
* Conversions that are just translations to/from cylindrical coordinate spaces
9+
* should have input values clamped to the range of the source coordinate space
10+
* otherwise they will be mapped to incorrect values
11+
* e.g. RGB with negative -> HSB should produce the same value as RGB with zero -> HSB
12+
*/
13+
public class CoordinateSpaceTests
14+
{
15+
[Test]
16+
public void CartesianRgbToCylindricalHsb()
17+
{
18+
ColourTriplet upperOutRange = new(1.00001, 100, double.PositiveInfinity);
19+
ColourTriplet upperInRange = new(1, 1, 1);
20+
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
21+
ColourTriplet lowerInRange = new(0, 0, 0);
22+
AssertRgbToHsb(upperInRange, upperOutRange);
23+
AssertRgbToHsb(lowerInRange, lowerOutRange);
24+
}
25+
26+
[Test]
27+
public void CylindricalHsbToCartesianRgb()
28+
{
29+
ColourTriplet upperOutRange = new(360.00001, 100, double.PositiveInfinity);
30+
ColourTriplet upperInRange = new(0.00001, 1, 1);
31+
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
32+
ColourTriplet lowerInRange = new(359.99999, 0, 0);
33+
AssertHsbToRgb(upperInRange, upperOutRange);
34+
AssertHsbToRgb(lowerInRange, lowerOutRange);
35+
}
36+
37+
[Test]
38+
public void CylindricalHsbToCylindricalHsl()
39+
{
40+
ColourTriplet upperOutRange = new(360.00001, 100, double.PositiveInfinity);
41+
ColourTriplet upperInRange = new(0.00001, 1, 1);
42+
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
43+
ColourTriplet lowerInRange = new(359.99999, 0, 0);
44+
AssertHsbToHsl(upperInRange, upperOutRange);
45+
AssertHsbToHsl(lowerInRange, lowerOutRange);
46+
}
47+
48+
[Test]
49+
public void CylindricalHslToCylindricalHsb()
50+
{
51+
ColourTriplet upperOutRange = new(360.00001, 100, double.PositiveInfinity);
52+
ColourTriplet upperInRange = new(0.00001, 1, 1);
53+
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
54+
ColourTriplet lowerInRange = new(359.99999, 0, 0);
55+
AssertHslToHsb(upperInRange, upperOutRange);
56+
AssertHslToHsb(lowerInRange, lowerOutRange);
57+
}
58+
59+
private static void AssertRgbToHsb(ColourTriplet inRange, ColourTriplet outRange)
60+
{
61+
Rgb GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third, Configuration.Default);
62+
var inRangeInput = GetInput(inRange);
63+
var outRangeInput = GetInput(outRange);
64+
65+
Hsb GetOutput(Rgb rgb) => Conversion.RgbToHsb(rgb);
66+
var inRangeOutput = GetOutput(inRangeInput);
67+
var outRangeOutput = GetOutput(outRangeInput);
68+
69+
AssertTriplets(
70+
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
71+
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
72+
}
73+
74+
private static void AssertHsbToRgb(ColourTriplet inRange, ColourTriplet outRange)
75+
{
76+
Hsb GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third);
77+
var inRangeInput = GetInput(inRange);
78+
var outRangeInput = GetInput(outRange);
79+
80+
Rgb GetOutput(Hsb hsb) => Conversion.HsbToRgb(hsb, Configuration.Default);
81+
var inRangeOutput = GetOutput(inRangeInput);
82+
var outRangeOutput = GetOutput(outRangeInput);
83+
84+
AssertTriplets(
85+
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
86+
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
87+
}
88+
89+
private static void AssertHsbToHsl(ColourTriplet inRange, ColourTriplet outRange)
90+
{
91+
Hsb GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third);
92+
var inRangeInput = GetInput(inRange);
93+
var outRangeInput = GetInput(outRange);
94+
95+
Hsl GetOutput(Hsb hsb) => Conversion.HsbToHsl(hsb);
96+
var inRangeOutput = GetOutput(inRangeInput);
97+
var outRangeOutput = GetOutput(outRangeInput);
98+
99+
AssertTriplets(
100+
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
101+
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
102+
}
103+
104+
private static void AssertHslToHsb(ColourTriplet inRange, ColourTriplet outRange)
105+
{
106+
Hsl GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third);
107+
var inRangeInput = GetInput(inRange);
108+
var outRangeInput = GetInput(outRange);
109+
110+
Hsb GetOutput(Hsl hsl) => Conversion.HslToHsb(hsl);
111+
var inRangeOutput = GetOutput(inRangeInput);
112+
var outRangeOutput = GetOutput(outRangeInput);
113+
114+
AssertTriplets(
115+
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
116+
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
117+
}
118+
119+
private static void AssertTriplets(Triplets inRangeInput, Triplets outRangeInput, Triplets inRangeOutput, Triplets outRangeOutput)
120+
{
121+
AssertUtils.AssertColourTriplet(outRangeInput.Constrained, inRangeInput.Unconstrained, 0.00001);
122+
AssertUtils.AssertColourTriplet(outRangeOutput.Unconstrained, inRangeOutput.Unconstrained, 0.00001);
123+
AssertUtils.AssertColourTriplet(outRangeOutput.Constrained, inRangeOutput.Unconstrained, 0.00001);
124+
}
125+
126+
private static Triplets AsTriplets(Rgb rgb) => new(rgb.Triplet, rgb.ConstrainedTriplet);
127+
private static Triplets AsTriplets(Hsb hsb) => new(hsb.Triplet, hsb.ConstrainedTriplet);
128+
private static Triplets AsTriplets(Hsl hsl) => new(hsl.Triplet, hsl.ConstrainedTriplet);
129+
130+
private record Triplets(ColourTriplet Unconstrained, ColourTriplet Constrained);
131+
}

Unicolour.Tests/EqualityTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ public void EqualLabGivesEqualObjects()
4646
AssertUnicoloursEqual(unicolour1, unicolour2);
4747
}
4848

49+
[Test]
50+
public void EqualLuvGivesEqualObjects()
51+
{
52+
var unicolour1 = GetRandomLuvUnicolour();
53+
var unicolour2 = Unicolour.FromLuv(unicolour1.Luv.L, unicolour1.Luv.U, unicolour1.Luv.V, unicolour1.Alpha.A);
54+
AssertUnicoloursEqual(unicolour1, unicolour2);
55+
}
56+
4957
[Test]
5058
public void EqualOklabGivesEqualObjects()
5159
{
@@ -99,6 +107,15 @@ public void NotEqualLabGivesNotEqualObjects()
99107
AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Lab.Triplet);
100108
}
101109

110+
[Test]
111+
public void NotEqualLuvGivesNotEqualObjects()
112+
{
113+
var unicolour1 = GetRandomLuvUnicolour();
114+
var differentTuple = GetDifferent(unicolour1.Luv.Triplet, 1.0).Tuple;
115+
var unicolour2 = Unicolour.FromLuv(differentTuple, unicolour1.Alpha.A + 0.1);
116+
AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Luv.Triplet);
117+
}
118+
102119
[Test]
103120
public void NotEqualOklabGivesNotEqualObjects()
104121
{
@@ -144,6 +161,7 @@ public void DifferentConfigurationObjects()
144161
private static Unicolour GetRandomHslUnicolour() => Unicolour.FromHsl(TestColours.GetRandomHsl().Tuple, TestColours.GetRandomAlpha());
145162
private static Unicolour GetRandomXyzUnicolour() => Unicolour.FromXyz(TestColours.GetRandomXyz().Tuple, TestColours.GetRandomAlpha());
146163
private static Unicolour GetRandomLabUnicolour() => Unicolour.FromLab(TestColours.GetRandomLab().Tuple, TestColours.GetRandomAlpha());
164+
private static Unicolour GetRandomLuvUnicolour() => Unicolour.FromLuv(TestColours.GetRandomLuv().Tuple, TestColours.GetRandomAlpha());
147165
private static Unicolour GetRandomOklabUnicolour() => Unicolour.FromOklab(TestColours.GetRandomOklab().Tuple, TestColours.GetRandomAlpha());
148166
private static ColourTriplet GetDifferent(ColourTriplet triplet, double diff = 0.1) => new(triplet.First + diff, triplet.Second + diff, triplet.Third + diff);
149167

@@ -154,6 +172,7 @@ private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicol
154172
AssertEqual(unicolour1.Hsl, unicolour2.Hsl);
155173
AssertEqual(unicolour1.Xyz, unicolour2.Xyz);
156174
AssertEqual(unicolour1.Lab, unicolour2.Lab);
175+
AssertEqual(unicolour1.Luv, unicolour2.Luv);
157176
AssertEqual(unicolour1.Oklab, unicolour2.Oklab);
158177
AssertEqual(unicolour1.Alpha, unicolour2.Alpha);
159178
AssertEqual(unicolour1.Luminance, unicolour2.Luminance);

0 commit comments

Comments
 (0)