Skip to content

Commit fe2b388

Browse files
committed
Merge branch 'oklab' into 'main'
Add Oklab support See merge request Wacton/Unicolour!8
2 parents 63e1ab4 + 0e32db0 commit fe2b388

38 files changed

+794
-307
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A `Unicolour` encapsulates a single colour and its representation across differe
1414
- HSL
1515
- CIE XYZ
1616
- CIE LAB
17+
- Oklab
1718

1819
Unicolour uses sRGB as the default RGB model and standard illuminant D65 (2° observer) as the default white point of the XYZ colour space.
1920
These [can be overridden](#advanced-configuration-) using the `Configuration` parameter.
@@ -22,8 +23,6 @@ This library was initially written for personal projects since existing librarie
2223
The goal of this library is to be intuitive and easy to use; performance is not a priority.
2324
It is also [extensively tested](Unicolour.Tests) against known colour values and other .NET libraries.
2425

25-
More functionality will be added over time.
26-
2726
Targets .NET Standard 2.0 for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications.
2827

2928
## How to use 🎨
@@ -43,6 +42,7 @@ var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.0);
4342
var unicolour = Unicolour.FromHsl(327.6, 1.0, 0.539);
4443
var unicolour = Unicolour.FromXyz(0.47, 0.24, 0.3);
4544
var unicolour = Unicolour.FromLab(55.96, +84.54, -5.7);
45+
var unicolour = Unicolour.FromOklab(0.65, 0.26, -0.01);
4646
```
4747

4848
3. Get representation of colour in different colour spaces:
@@ -52,6 +52,7 @@ var hsb = unicolour.Hsb;
5252
var hsl = unicolour.Hsl;
5353
var xyz = unicolour.Xyz;
5454
var lab = unicolour.Lab;
55+
var oklab = unicolour.Oklab;
5556
```
5657

5758
4. Interpolate between colours:
@@ -61,6 +62,7 @@ var interpolated = unicolour1.InterpolateHsb(unicolour2, 0.5);
6162
var interpolated = unicolour1.InterpolateHsl(unicolour2, 0.5);
6263
var interpolated = unicolour1.InterpolateXyz(unicolour2, 0.5);
6364
var interpolated = unicolour1.InterpolateLab(unicolour2, 0.5);
65+
var interpolated = unicolour1.InterpolateOklab(unicolour2, 0.5);
6466
```
6567

6668
5. Compare colours:
@@ -69,7 +71,8 @@ var contrast = unicolour1.Contrast(unicolour2);
6971
var difference = unicolour1.DeltaE76(unicolour2);
7072
```
7173

72-
See also the [example code](Unicolour.Example/Program.cs), which uses `Unicolour` to generate gradients through different colour spaces.
74+
See also the [example code](Unicolour.Example/Program.cs), which uses `Unicolour` to generate gradients through different colour spaces:
75+
![Gradients generate from Unicolour](Unicolour.Example/gradients.png)
7376

7477
## Advanced configuration 💡
7578
A `Configuration` parameter can be used to change the RGB model (e.g. Adobe RGB, wide-gamut RGB)

Unicolour.Example/Program.cs

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,66 @@
55
using SixLabors.ImageSharp.Processing;
66
using Wacton.Unicolour;
77

8-
var startColour = Unicolour.FromHsb(260, 1.0, 0.33);
9-
var endColour = Unicolour.FromHsb(30, 0.66, 1.0);
10-
var backgroundRgba32 = AsRgba32(Unicolour.FromHex("#404046"));
11-
var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF"));
8+
const int gradientWidth = 800;
9+
const int gradientHeight = 100;
1210

1311
FontCollection collection = new();
1412
var fontFamily = collection.Add("Inconsolata-Regular.ttf");
15-
var font = fontFamily.CreateFont(32);
13+
var font = fontFamily.CreateFont(24);
14+
var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF"));
1615

17-
var gradientWidth = 1600;
18-
var gradientHeight = 200;
16+
var labels = new List<string> {"RGB", "HSB", "HSL", "XYZ", "LAB", "OKLAB"};
17+
var purple = Unicolour.FromHsb(260, 1.0, 0.33);
18+
var orange = Unicolour.FromHsb(30, 0.66, 1.0);
19+
var black = Unicolour.FromRgb(0, 0, 0);
20+
var cyan = Unicolour.FromRgb255(0, 255, 255);
1921

20-
var image = new Image<Rgba32>(gradientWidth, gradientHeight * 5);
21-
image.Mutate(x => x.BackgroundColor(backgroundRgba32));
22+
var image = new Image<Rgba32>(gradientWidth * 2, gradientHeight * labels.Count);
23+
Draw(purple, orange, 0);
24+
Draw(black, cyan, 1);
2225

23-
for (var x = 0; x < gradientWidth; x++)
26+
void Draw(Unicolour start, Unicolour end, int column)
2427
{
25-
var distance = x / (double)(gradientWidth - 1);
26-
var viaRgb = startColour.InterpolateRgb(endColour, distance);
27-
var viaHsb = startColour.InterpolateHsb(endColour, distance);
28-
var viaHsl = startColour.InterpolateHsl(endColour, distance);
29-
var viaXyz = startColour.InterpolateXyz(endColour, distance);
30-
var viaLab = startColour.InterpolateLab(endColour, distance);
31-
SetPixels(x, viaRgb, viaHsb, viaHsl, viaXyz, viaLab);
28+
for (var pixelIndex = 0; pixelIndex < gradientWidth; pixelIndex++)
29+
{
30+
var distance = pixelIndex / (double)(gradientWidth - 1);
31+
var unicolours = new List<Unicolour>
32+
{
33+
start.InterpolateRgb(end, distance),
34+
start.InterpolateHsb(end, distance),
35+
start.InterpolateHsl(end, distance),
36+
start.InterpolateXyz(end, distance),
37+
start.InterpolateLab(end, distance),
38+
start.InterpolateOklab(end, distance)
39+
};
40+
41+
SetPixels(column, pixelIndex, unicolours);
42+
}
43+
44+
for (var i = 0; i < labels.Count; i++)
45+
{
46+
var label = labels[i];
47+
var textLocation = TextLocation(column, i);
48+
image.Mutate(context => context.DrawText(label, font, textRgba32, textLocation));
49+
}
3250
}
3351

3452
image.Save("gradients.png");
3553

36-
void SetPixels(int x, Unicolour viaRgb, Unicolour viaHsb, Unicolour viaHsl, Unicolour viaXyz, Unicolour viaLab)
54+
void SetPixels(int column, int pixelIndex, List<Unicolour> unicolours)
3755
{
3856
for (var y = 0; y < gradientHeight; y++)
3957
{
40-
image[x, y] = AsRgba32(viaRgb);
41-
image[x, y + 200] = AsRgba32(viaHsb);
42-
image[x, y + 400] = AsRgba32(viaHsl);
43-
image[x, y + 600] = AsRgba32(viaXyz);
44-
image[x, y + 800] = AsRgba32(viaLab);
58+
for (var i = 0; i < unicolours.Count; i++)
59+
{
60+
var x = gradientWidth * column + pixelIndex;
61+
image[x, y + gradientHeight * i] = AsRgba32(unicolours[i]);
62+
}
4563
}
46-
47-
PointF TextLocation(float targetY) => new(16, targetY + 16);
48-
image.Mutate(context => context.DrawText("RGB", font, textRgba32, TextLocation(0)));
49-
image.Mutate(context => context.DrawText("HSB", font, textRgba32, TextLocation(200)));
50-
image.Mutate(context => context.DrawText("HSL", font, textRgba32, TextLocation(400)));
51-
image.Mutate(context => context.DrawText("XYZ", font, textRgba32, TextLocation(600)));
52-
image.Mutate(context => context.DrawText("LAB", font, textRgba32, TextLocation(800)));
5364
}
5465

66+
PointF TextLocation(float column, float row) => new(gradientWidth * column + 16, gradientHeight * row + 16);
67+
5568
Rgba32 AsRgba32(Unicolour unicolour)
5669
{
5770
var rgb = unicolour.Rgb;

Unicolour.Example/gradients.png

22 KB
Loading

Unicolour.Tests/ConfigurationTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ public static class ConfigurationTests
2121
[Test]
2222
public static void StandardRgbD65ToXyzD65()
2323
{
24-
var rgbToXyzMatrix = Configuration.Default.RgbToXyzMatrix;
25-
2624
// https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
2725
var expectedMatrixA = new[,]
2826
{
@@ -39,6 +37,7 @@ public static void StandardRgbD65ToXyzD65()
3937
{0.0193339, 0.1191920, 0.9503041}
4038
};
4139

40+
var rgbToXyzMatrix = Matrices.RgbToXyzMatrix(Configuration.Default);
4241
var unicolourNoConfig = Unicolour.FromRgb(0.5, 0.25, 0.75);
4342
var unicolourWithConfig = Unicolour.FromRgb(Configuration.Default, 0.5, 0.25, 0.75);
4443
var expectedColour = new TestColour
@@ -57,8 +56,6 @@ public static void StandardRgbD65ToXyzD65()
5756
[Test]
5857
public static void XyzD65ToStandardRgbD65()
5958
{
60-
var xyzToRgbMatrix = Configuration.Default.XyzToRgbMatrix;
61-
6259
// https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
6360
var expectedMatrixA = new[,]
6461
{
@@ -75,6 +72,7 @@ public static void XyzD65ToStandardRgbD65()
7572
{0.0556434, -0.2040259, 1.0572252}
7673
};
7774

75+
var xyzToRgbMatrix = Matrices.RgbToXyzMatrix(Configuration.Default).Inverse();
7876
var unicolourXyzNoConfig = Unicolour.FromXyz(0.200757, 0.119618, 0.506757);
7977
var unicolourXyzWithConfig = Unicolour.FromXyz(Configuration.Default, 0.200757, 0.119618, 0.506757);
8078
var unicolourLabNoConfig = Unicolour.FromLab(41.1553, 51.4108, -56.4485);
@@ -109,6 +107,7 @@ public static void StandardRgbD65ToXyzD50()
109107
{0.0139322, 0.0971045, 0.7141733}
110108
};
111109

110+
var rgbToXyzMatrix = Matrices.RgbToXyzMatrix(configuration);
112111
var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75);
113112
var expectedColour = new TestColour
114113
{
@@ -117,7 +116,7 @@ public static void StandardRgbD65ToXyzD50()
117116
// Luv = new(40.5359, 18.7523, -78.2057)
118117
};
119118

120-
Assert.That(configuration.RgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
119+
Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
121120
AssertColour(unicolour, expectedColour);
122121
}
123122

@@ -141,11 +140,12 @@ public static void XyzD50ToStandardRgbD65()
141140
{ 0.0719453, -0.2289914, 1.4052427}
142141
};
143142

143+
var xyzToRgbMatrix = Matrices.RgbToXyzMatrix(configuration).Inverse();
144144
var unicolourXyz = Unicolour.FromXyz(configuration, 0.187691, 0.115771, 0.381093);
145145
var unicolourLab = Unicolour.FromLab(configuration, 40.5359, 46.0847, -57.1158);
146146
var expectedColour = new TestColour { Rgb = new(0.5, 0.25, 0.75) };
147147

148-
Assert.That(configuration.XyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
148+
Assert.That(xyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
149149
AssertColour(unicolourXyz, expectedColour);
150150
AssertColour(unicolourLab, expectedColour);
151151
}
@@ -324,8 +324,8 @@ public static void XyzD50ToWideGamutRgbD50()
324324

325325
private static void AssertColour(Unicolour unicolour, TestColour expected)
326326
{
327-
if (expected.Rgb != null) AssertUtils.AssertColourTuple(unicolour.Rgb.Tuple, expected.Rgb!, 0.01);
328-
if (expected.Xyz != null) AssertUtils.AssertColourTuple(unicolour.Xyz.Tuple, expected.Xyz!, 0.001);
329-
if (expected.Lab != null) AssertUtils.AssertColourTuple(unicolour.Lab.Tuple, expected.Lab!, 0.05);
327+
if (expected.Rgb != null) AssertUtils.AssertColourTriplet(unicolour.Rgb.Triplet, expected.Rgb!, 0.01);
328+
if (expected.Xyz != null) AssertUtils.AssertColourTriplet(unicolour.Xyz.Triplet, expected.Xyz!, 0.001);
329+
if (expected.Lab != null) AssertUtils.AssertColourTriplet(unicolour.Lab.Triplet, expected.Lab!, 0.05);
330330
}
331331
}

Unicolour.Tests/ConversionTests.cs

Lines changed: 32 additions & 21 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 OklabTolerance = 0.000001;
1516

1617
[Test]
1718
// no point doing this test starting with Wikipedia's HSB / HSL values since they're rounded
@@ -44,6 +45,9 @@ public void HslSameAfterDeconversion()
4445

4546
[Test]
4647
public void LabSameAfterDeconversion() => AssertUtils.AssertRandomLabColours(AssertLabDeconversion);
48+
49+
[Test]
50+
public void OklabSameAfterDeconversion() => AssertUtils.AssertRandomOklabColours(AssertOklabDeconversion);
4751

4852
private static void AssertRgbConversion(TestColour namedColour)
4953
{
@@ -65,66 +69,73 @@ private static void AssertRgbConversion(TestColour namedColour)
6569
Assert.That(Math.Round(hsl.L, 2), Is.EqualTo(expectedRoundedHsl.Third).Within(0.02), namedColour.Name!);
6670
}
6771

68-
private static void AssertRgbDeconversion(TestColour namedColour) => AssertRgbDeconversion(GetRgbTupleFromHex(namedColour.Hex!));
69-
private static void AssertRgb255Deconversion(ColourTuple tuple) => AssertRgbDeconversion(GetNormalisedRgb255Tuple(tuple));
70-
private static void AssertRgbDeconversion(ColourTuple tuple) => AssertRgbDeconversion(new Rgb(tuple.First, tuple.Second, tuple.Third, Configuration.Default));
72+
private static void AssertRgbDeconversion(TestColour namedColour) => AssertRgbDeconversion(GetRgbTripletFromHex(namedColour.Hex!));
73+
private static void AssertRgb255Deconversion(ColourTriplet triplet) => AssertRgbDeconversion(GetNormalisedRgb255Triplet(triplet));
74+
private static void AssertRgbDeconversion(ColourTriplet triplet) => AssertRgbDeconversion(new Rgb(triplet.First, triplet.Second, triplet.Third, Configuration.Default));
7175
private static void AssertRgbDeconversion(Rgb original)
7276
{
7377
var deconvertedViaHsb = Conversion.HsbToRgb(Conversion.RgbToHsb(original), Configuration.Default);
74-
AssertUtils.AssertColourTuple(deconvertedViaHsb.Tuple, original.Tuple, RgbTolerance);
75-
AssertUtils.AssertColourTuple(deconvertedViaHsb.TupleLinear, original.TupleLinear, RgbTolerance);
76-
AssertUtils.AssertColourTuple(deconvertedViaHsb.Tuple255, original.Tuple255, RgbTolerance);
78+
AssertUtils.AssertColourTriplet(deconvertedViaHsb.Triplet, original.Triplet, RgbTolerance);
79+
AssertUtils.AssertColourTriplet(deconvertedViaHsb.TripletLinear, original.TripletLinear, RgbTolerance);
80+
AssertUtils.AssertColourTriplet(deconvertedViaHsb.Triplet255, original.Triplet255, RgbTolerance);
7781

7882
var deconvertedViaXyz = Conversion.XyzToRgb(Conversion.RgbToXyz(original, Configuration.Default), Configuration.Default);
79-
AssertUtils.AssertColourTuple(deconvertedViaXyz.Tuple, original.Tuple, RgbTolerance);
80-
AssertUtils.AssertColourTuple(deconvertedViaXyz.TupleLinear, original.TupleLinear, RgbTolerance);
81-
AssertUtils.AssertColourTuple(deconvertedViaXyz.Tuple255, original.Tuple255, RgbTolerance);
83+
AssertUtils.AssertColourTriplet(deconvertedViaXyz.Triplet, original.Triplet, RgbTolerance);
84+
AssertUtils.AssertColourTriplet(deconvertedViaXyz.TripletLinear, original.TripletLinear, RgbTolerance);
85+
AssertUtils.AssertColourTriplet(deconvertedViaXyz.Triplet255, original.Triplet255, RgbTolerance);
8286
}
8387

8488
private static void AssertHsbDeconversion(TestColour namedColour) => AssertHsbDeconversion(namedColour.Hsb!);
85-
private static void AssertHsbDeconversion(ColourTuple tuple) => AssertHsbDeconversion(new Hsb(tuple.First, tuple.Second, tuple.Third));
89+
private static void AssertHsbDeconversion(ColourTriplet triplet) => AssertHsbDeconversion(new Hsb(triplet.First, triplet.Second, triplet.Third));
8690
private static void AssertHsbDeconversion(Hsb original)
8791
{
8892
var deconvertedViaRgb = Conversion.RgbToHsb(Conversion.HsbToRgb(original, Configuration.Default));
89-
AssertUtils.AssertColourTuple(deconvertedViaRgb.Tuple, original.Tuple, HsbTolerance, true);
93+
AssertUtils.AssertColourTriplet(deconvertedViaRgb.Triplet, original.Triplet, HsbTolerance, true);
9094

9195
var deconvertedViaHsl = Conversion.HslToHsb(Conversion.HsbToHsl(original));
92-
AssertUtils.AssertColourTuple(deconvertedViaHsl.Tuple, original.Tuple, HsbTolerance, true);
96+
AssertUtils.AssertColourTriplet(deconvertedViaHsl.Triplet, original.Triplet, HsbTolerance, true);
9397
}
9498

9599
private static void AssertHslDeconversion(TestColour namedColour) => AssertHslDeconversion(namedColour.Hsl!);
96-
private static void AssertHslDeconversion(ColourTuple tuple) => AssertHslDeconversion(new Hsl(tuple.First, tuple.Second, tuple.Third));
100+
private static void AssertHslDeconversion(ColourTriplet triplet) => AssertHslDeconversion(new Hsl(triplet.First, triplet.Second, triplet.Third));
97101
private static void AssertHslDeconversion(Hsl original)
98102
{
99103
var deconverted = Conversion.RgbToHsl(Conversion.HsbToRgb(Conversion.HslToHsb(original), Configuration.Default));
100-
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, HslTolerance, true);
104+
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, HslTolerance, true);
101105
}
102106

103-
private static void AssertXyzDeconversion(ColourTuple tuple) => AssertXyzDeconversion(new Xyz(tuple.First, tuple.Second, tuple.Third));
107+
private static void AssertXyzDeconversion(ColourTriplet triplet) => AssertXyzDeconversion(new Xyz(triplet.First, triplet.Second, triplet.Third));
104108
private static void AssertXyzDeconversion(Xyz original)
105109
{
106110
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
107111
var deconverted = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default);
108-
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, XyzTolerance);
112+
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, XyzTolerance);
109113
}
110114

111-
private static void AssertLabDeconversion(ColourTuple tuple) => AssertLabDeconversion(new Lab(tuple.First, tuple.Second, tuple.Third));
115+
private static void AssertLabDeconversion(ColourTriplet triplet) => AssertLabDeconversion(new Lab(triplet.First, triplet.Second, triplet.Third));
112116
private static void AssertLabDeconversion(Lab original)
113117
{
114118
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
115119
var deconverted = Conversion.XyzToLab(Conversion.LabToXyz(original, Configuration.Default), Configuration.Default);
116-
AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, LabTolerance);
120+
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, LabTolerance);
121+
}
122+
123+
private static void AssertOklabDeconversion(ColourTriplet triplet) => AssertOklabDeconversion(new Oklab(triplet.First, triplet.Second, triplet.Third));
124+
private static void AssertOklabDeconversion(Oklab original)
125+
{
126+
var deconverted = Conversion.XyzToOklab(Conversion.OklabToXyz(original, Configuration.Default), Configuration.Default);
127+
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, OklabTolerance);
117128
}
118129

119-
private static ColourTuple GetRgbTupleFromHex(string hex)
130+
private static ColourTriplet GetRgbTripletFromHex(string hex)
120131
{
121132
var (r255, g255, b255, _) = Wacton.Unicolour.Utils.ParseColourHex(hex);
122133
return new(r255 / 255.0, g255 / 255.0, b255 / 255.0);
123134
}
124135

125-
private static ColourTuple GetNormalisedRgb255Tuple(ColourTuple tuple)
136+
private static ColourTriplet GetNormalisedRgb255Triplet(ColourTriplet triplet)
126137
{
127-
var (r255, g255, b255) = tuple;
138+
var (r255, g255, b255) = triplet;
128139
return new(r255 / 255.0, g255 / 255.0, b255 / 255.0);
129140
}
130141
}

0 commit comments

Comments
 (0)