diff --git a/README.md b/README.md index 0c59c0bc..db277933 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A `Unicolour` encapsulates a single colour and its representation across differe - HSL - CIE XYZ - CIE LAB +- Oklab Unicolour uses sRGB as the default RGB model and standard illuminant D65 (2° observer) as the default white point of the XYZ colour space. 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 The goal of this library is to be intuitive and easy to use; performance is not a priority. It is also [extensively tested](Unicolour.Tests) against known colour values and other .NET libraries. -More functionality will be added over time. - Targets .NET Standard 2.0 for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications. ## How to use 🎨 @@ -43,6 +42,7 @@ var unicolour = Unicolour.FromHsb(327.6, 0.922, 1.0); var unicolour = Unicolour.FromHsl(327.6, 1.0, 0.539); var unicolour = Unicolour.FromXyz(0.47, 0.24, 0.3); var unicolour = Unicolour.FromLab(55.96, +84.54, -5.7); +var unicolour = Unicolour.FromOklab(0.65, 0.26, -0.01); ``` 3. Get representation of colour in different colour spaces: @@ -52,6 +52,7 @@ var hsb = unicolour.Hsb; var hsl = unicolour.Hsl; var xyz = unicolour.Xyz; var lab = unicolour.Lab; +var oklab = unicolour.Oklab; ``` 4. Interpolate between colours: @@ -61,6 +62,7 @@ var interpolated = unicolour1.InterpolateHsb(unicolour2, 0.5); var interpolated = unicolour1.InterpolateHsl(unicolour2, 0.5); var interpolated = unicolour1.InterpolateXyz(unicolour2, 0.5); var interpolated = unicolour1.InterpolateLab(unicolour2, 0.5); +var interpolated = unicolour1.InterpolateOklab(unicolour2, 0.5); ``` 5. Compare colours: @@ -69,7 +71,8 @@ var contrast = unicolour1.Contrast(unicolour2); var difference = unicolour1.DeltaE76(unicolour2); ``` -See also the [example code](Unicolour.Example/Program.cs), which uses `Unicolour` to generate gradients through different colour spaces. +See also the [example code](Unicolour.Example/Program.cs), which uses `Unicolour` to generate gradients through different colour spaces: +![Gradients generate from Unicolour](Unicolour.Example/gradients.png) ## Advanced configuration 💡 A `Configuration` parameter can be used to change the RGB model (e.g. Adobe RGB, wide-gamut RGB) diff --git a/Unicolour.Example/Program.cs b/Unicolour.Example/Program.cs index 501b8c49..6a27fe60 100644 --- a/Unicolour.Example/Program.cs +++ b/Unicolour.Example/Program.cs @@ -5,53 +5,66 @@ using SixLabors.ImageSharp.Processing; using Wacton.Unicolour; -var startColour = Unicolour.FromHsb(260, 1.0, 0.33); -var endColour = Unicolour.FromHsb(30, 0.66, 1.0); -var backgroundRgba32 = AsRgba32(Unicolour.FromHex("#404046")); -var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF")); +const int gradientWidth = 800; +const int gradientHeight = 100; FontCollection collection = new(); var fontFamily = collection.Add("Inconsolata-Regular.ttf"); -var font = fontFamily.CreateFont(32); +var font = fontFamily.CreateFont(24); +var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF")); -var gradientWidth = 1600; -var gradientHeight = 200; +var labels = new List {"RGB", "HSB", "HSL", "XYZ", "LAB", "OKLAB"}; +var purple = Unicolour.FromHsb(260, 1.0, 0.33); +var orange = Unicolour.FromHsb(30, 0.66, 1.0); +var black = Unicolour.FromRgb(0, 0, 0); +var cyan = Unicolour.FromRgb255(0, 255, 255); -var image = new Image(gradientWidth, gradientHeight * 5); -image.Mutate(x => x.BackgroundColor(backgroundRgba32)); +var image = new Image(gradientWidth * 2, gradientHeight * labels.Count); +Draw(purple, orange, 0); +Draw(black, cyan, 1); -for (var x = 0; x < gradientWidth; x++) +void Draw(Unicolour start, Unicolour end, int column) { - var distance = x / (double)(gradientWidth - 1); - var viaRgb = startColour.InterpolateRgb(endColour, distance); - var viaHsb = startColour.InterpolateHsb(endColour, distance); - var viaHsl = startColour.InterpolateHsl(endColour, distance); - var viaXyz = startColour.InterpolateXyz(endColour, distance); - var viaLab = startColour.InterpolateLab(endColour, distance); - SetPixels(x, viaRgb, viaHsb, viaHsl, viaXyz, viaLab); + for (var pixelIndex = 0; pixelIndex < gradientWidth; pixelIndex++) + { + var distance = pixelIndex / (double)(gradientWidth - 1); + var unicolours = new List + { + start.InterpolateRgb(end, distance), + start.InterpolateHsb(end, distance), + start.InterpolateHsl(end, distance), + start.InterpolateXyz(end, distance), + start.InterpolateLab(end, distance), + start.InterpolateOklab(end, distance) + }; + + SetPixels(column, pixelIndex, unicolours); + } + + for (var i = 0; i < labels.Count; i++) + { + var label = labels[i]; + var textLocation = TextLocation(column, i); + image.Mutate(context => context.DrawText(label, font, textRgba32, textLocation)); + } } image.Save("gradients.png"); -void SetPixels(int x, Unicolour viaRgb, Unicolour viaHsb, Unicolour viaHsl, Unicolour viaXyz, Unicolour viaLab) +void SetPixels(int column, int pixelIndex, List unicolours) { for (var y = 0; y < gradientHeight; y++) { - image[x, y] = AsRgba32(viaRgb); - image[x, y + 200] = AsRgba32(viaHsb); - image[x, y + 400] = AsRgba32(viaHsl); - image[x, y + 600] = AsRgba32(viaXyz); - image[x, y + 800] = AsRgba32(viaLab); + for (var i = 0; i < unicolours.Count; i++) + { + var x = gradientWidth * column + pixelIndex; + image[x, y + gradientHeight * i] = AsRgba32(unicolours[i]); + } } - - PointF TextLocation(float targetY) => new(16, targetY + 16); - image.Mutate(context => context.DrawText("RGB", font, textRgba32, TextLocation(0))); - image.Mutate(context => context.DrawText("HSB", font, textRgba32, TextLocation(200))); - image.Mutate(context => context.DrawText("HSL", font, textRgba32, TextLocation(400))); - image.Mutate(context => context.DrawText("XYZ", font, textRgba32, TextLocation(600))); - image.Mutate(context => context.DrawText("LAB", font, textRgba32, TextLocation(800))); } +PointF TextLocation(float column, float row) => new(gradientWidth * column + 16, gradientHeight * row + 16); + Rgba32 AsRgba32(Unicolour unicolour) { var rgb = unicolour.Rgb; diff --git a/Unicolour.Example/gradients.png b/Unicolour.Example/gradients.png new file mode 100644 index 00000000..92cdc7e2 Binary files /dev/null and b/Unicolour.Example/gradients.png differ diff --git a/Unicolour.Tests/ConfigurationTests.cs b/Unicolour.Tests/ConfigurationTests.cs index 8599aa9b..63c73453 100644 --- a/Unicolour.Tests/ConfigurationTests.cs +++ b/Unicolour.Tests/ConfigurationTests.cs @@ -21,8 +21,6 @@ public static class ConfigurationTests [Test] public static void StandardRgbD65ToXyzD65() { - var rgbToXyzMatrix = Configuration.Default.RgbToXyzMatrix; - // https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ var expectedMatrixA = new[,] { @@ -39,6 +37,7 @@ public static void StandardRgbD65ToXyzD65() {0.0193339, 0.1191920, 0.9503041} }; + var rgbToXyzMatrix = Matrices.RgbToXyzMatrix(Configuration.Default); var unicolourNoConfig = Unicolour.FromRgb(0.5, 0.25, 0.75); var unicolourWithConfig = Unicolour.FromRgb(Configuration.Default, 0.5, 0.25, 0.75); var expectedColour = new TestColour @@ -57,8 +56,6 @@ public static void StandardRgbD65ToXyzD65() [Test] public static void XyzD65ToStandardRgbD65() { - var xyzToRgbMatrix = Configuration.Default.XyzToRgbMatrix; - // https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB var expectedMatrixA = new[,] { @@ -75,6 +72,7 @@ public static void XyzD65ToStandardRgbD65() {0.0556434, -0.2040259, 1.0572252} }; + var xyzToRgbMatrix = Matrices.RgbToXyzMatrix(Configuration.Default).Inverse(); var unicolourXyzNoConfig = Unicolour.FromXyz(0.200757, 0.119618, 0.506757); var unicolourXyzWithConfig = Unicolour.FromXyz(Configuration.Default, 0.200757, 0.119618, 0.506757); var unicolourLabNoConfig = Unicolour.FromLab(41.1553, 51.4108, -56.4485); @@ -109,6 +107,7 @@ public static void StandardRgbD65ToXyzD50() {0.0139322, 0.0971045, 0.7141733} }; + var rgbToXyzMatrix = Matrices.RgbToXyzMatrix(configuration); var unicolour = Unicolour.FromRgb(configuration, 0.5, 0.25, 0.75); var expectedColour = new TestColour { @@ -117,7 +116,7 @@ public static void StandardRgbD65ToXyzD50() // Luv = new(40.5359, 18.7523, -78.2057) }; - Assert.That(configuration.RgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001)); + Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001)); AssertColour(unicolour, expectedColour); } @@ -141,11 +140,12 @@ public static void XyzD50ToStandardRgbD65() { 0.0719453, -0.2289914, 1.4052427} }; + var xyzToRgbMatrix = Matrices.RgbToXyzMatrix(configuration).Inverse(); var unicolourXyz = Unicolour.FromXyz(configuration, 0.187691, 0.115771, 0.381093); var unicolourLab = Unicolour.FromLab(configuration, 40.5359, 46.0847, -57.1158); var expectedColour = new TestColour { Rgb = new(0.5, 0.25, 0.75) }; - Assert.That(configuration.XyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001)); + Assert.That(xyzToRgbMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001)); AssertColour(unicolourXyz, expectedColour); AssertColour(unicolourLab, expectedColour); } @@ -324,8 +324,8 @@ public static void XyzD50ToWideGamutRgbD50() private static void AssertColour(Unicolour unicolour, TestColour expected) { - if (expected.Rgb != null) AssertUtils.AssertColourTuple(unicolour.Rgb.Tuple, expected.Rgb!, 0.01); - if (expected.Xyz != null) AssertUtils.AssertColourTuple(unicolour.Xyz.Tuple, expected.Xyz!, 0.001); - if (expected.Lab != null) AssertUtils.AssertColourTuple(unicolour.Lab.Tuple, expected.Lab!, 0.05); + if (expected.Rgb != null) AssertUtils.AssertColourTriplet(unicolour.Rgb.Triplet, expected.Rgb!, 0.01); + if (expected.Xyz != null) AssertUtils.AssertColourTriplet(unicolour.Xyz.Triplet, expected.Xyz!, 0.001); + if (expected.Lab != null) AssertUtils.AssertColourTriplet(unicolour.Lab.Triplet, expected.Lab!, 0.05); } } \ No newline at end of file diff --git a/Unicolour.Tests/ConversionTests.cs b/Unicolour.Tests/ConversionTests.cs index 5d4e482c..b6c9bb3b 100644 --- a/Unicolour.Tests/ConversionTests.cs +++ b/Unicolour.Tests/ConversionTests.cs @@ -12,6 +12,7 @@ public class ConversionTests private const double HslTolerance = 0.00000001; private const double XyzTolerance = 0.00000001; private const double LabTolerance = 0.00000001; + private const double OklabTolerance = 0.000001; [Test] // no point doing this test starting with Wikipedia's HSB / HSL values since they're rounded @@ -44,6 +45,9 @@ public void HslSameAfterDeconversion() [Test] public void LabSameAfterDeconversion() => AssertUtils.AssertRandomLabColours(AssertLabDeconversion); + + [Test] + public void OklabSameAfterDeconversion() => AssertUtils.AssertRandomOklabColours(AssertOklabDeconversion); private static void AssertRgbConversion(TestColour namedColour) { @@ -65,66 +69,73 @@ private static void AssertRgbConversion(TestColour namedColour) Assert.That(Math.Round(hsl.L, 2), Is.EqualTo(expectedRoundedHsl.Third).Within(0.02), namedColour.Name!); } - private static void AssertRgbDeconversion(TestColour namedColour) => AssertRgbDeconversion(GetRgbTupleFromHex(namedColour.Hex!)); - private static void AssertRgb255Deconversion(ColourTuple tuple) => AssertRgbDeconversion(GetNormalisedRgb255Tuple(tuple)); - private static void AssertRgbDeconversion(ColourTuple tuple) => AssertRgbDeconversion(new Rgb(tuple.First, tuple.Second, tuple.Third, Configuration.Default)); + private static void AssertRgbDeconversion(TestColour namedColour) => AssertRgbDeconversion(GetRgbTripletFromHex(namedColour.Hex!)); + private static void AssertRgb255Deconversion(ColourTriplet triplet) => AssertRgbDeconversion(GetNormalisedRgb255Triplet(triplet)); + private static void AssertRgbDeconversion(ColourTriplet triplet) => AssertRgbDeconversion(new Rgb(triplet.First, triplet.Second, triplet.Third, Configuration.Default)); private static void AssertRgbDeconversion(Rgb original) { var deconvertedViaHsb = Conversion.HsbToRgb(Conversion.RgbToHsb(original), Configuration.Default); - AssertUtils.AssertColourTuple(deconvertedViaHsb.Tuple, original.Tuple, RgbTolerance); - AssertUtils.AssertColourTuple(deconvertedViaHsb.TupleLinear, original.TupleLinear, RgbTolerance); - AssertUtils.AssertColourTuple(deconvertedViaHsb.Tuple255, original.Tuple255, RgbTolerance); + AssertUtils.AssertColourTriplet(deconvertedViaHsb.Triplet, original.Triplet, RgbTolerance); + AssertUtils.AssertColourTriplet(deconvertedViaHsb.TripletLinear, original.TripletLinear, RgbTolerance); + AssertUtils.AssertColourTriplet(deconvertedViaHsb.Triplet255, original.Triplet255, RgbTolerance); var deconvertedViaXyz = Conversion.XyzToRgb(Conversion.RgbToXyz(original, Configuration.Default), Configuration.Default); - AssertUtils.AssertColourTuple(deconvertedViaXyz.Tuple, original.Tuple, RgbTolerance); - AssertUtils.AssertColourTuple(deconvertedViaXyz.TupleLinear, original.TupleLinear, RgbTolerance); - AssertUtils.AssertColourTuple(deconvertedViaXyz.Tuple255, original.Tuple255, RgbTolerance); + AssertUtils.AssertColourTriplet(deconvertedViaXyz.Triplet, original.Triplet, RgbTolerance); + AssertUtils.AssertColourTriplet(deconvertedViaXyz.TripletLinear, original.TripletLinear, RgbTolerance); + AssertUtils.AssertColourTriplet(deconvertedViaXyz.Triplet255, original.Triplet255, RgbTolerance); } private static void AssertHsbDeconversion(TestColour namedColour) => AssertHsbDeconversion(namedColour.Hsb!); - private static void AssertHsbDeconversion(ColourTuple tuple) => AssertHsbDeconversion(new Hsb(tuple.First, tuple.Second, tuple.Third)); + private static void AssertHsbDeconversion(ColourTriplet triplet) => AssertHsbDeconversion(new Hsb(triplet.First, triplet.Second, triplet.Third)); private static void AssertHsbDeconversion(Hsb original) { var deconvertedViaRgb = Conversion.RgbToHsb(Conversion.HsbToRgb(original, Configuration.Default)); - AssertUtils.AssertColourTuple(deconvertedViaRgb.Tuple, original.Tuple, HsbTolerance, true); + AssertUtils.AssertColourTriplet(deconvertedViaRgb.Triplet, original.Triplet, HsbTolerance, true); var deconvertedViaHsl = Conversion.HslToHsb(Conversion.HsbToHsl(original)); - AssertUtils.AssertColourTuple(deconvertedViaHsl.Tuple, original.Tuple, HsbTolerance, true); + AssertUtils.AssertColourTriplet(deconvertedViaHsl.Triplet, original.Triplet, HsbTolerance, true); } private static void AssertHslDeconversion(TestColour namedColour) => AssertHslDeconversion(namedColour.Hsl!); - private static void AssertHslDeconversion(ColourTuple tuple) => AssertHslDeconversion(new Hsl(tuple.First, tuple.Second, tuple.Third)); + private static void AssertHslDeconversion(ColourTriplet triplet) => AssertHslDeconversion(new Hsl(triplet.First, triplet.Second, triplet.Third)); private static void AssertHslDeconversion(Hsl original) { var deconverted = Conversion.RgbToHsl(Conversion.HsbToRgb(Conversion.HslToHsb(original), Configuration.Default)); - AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, HslTolerance, true); + AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, HslTolerance, true); } - private static void AssertXyzDeconversion(ColourTuple tuple) => AssertXyzDeconversion(new Xyz(tuple.First, tuple.Second, tuple.Third)); + private static void AssertXyzDeconversion(ColourTriplet triplet) => AssertXyzDeconversion(new Xyz(triplet.First, triplet.Second, triplet.Third)); private static void AssertXyzDeconversion(Xyz original) { // note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1 var deconverted = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default); - AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, XyzTolerance); + AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, XyzTolerance); } - private static void AssertLabDeconversion(ColourTuple tuple) => AssertLabDeconversion(new Lab(tuple.First, tuple.Second, tuple.Third)); + private static void AssertLabDeconversion(ColourTriplet triplet) => AssertLabDeconversion(new Lab(triplet.First, triplet.Second, triplet.Third)); private static void AssertLabDeconversion(Lab original) { // note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1 var deconverted = Conversion.XyzToLab(Conversion.LabToXyz(original, Configuration.Default), Configuration.Default); - AssertUtils.AssertColourTuple(deconverted.Tuple, original.Tuple, LabTolerance); + AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, LabTolerance); + } + + private static void AssertOklabDeconversion(ColourTriplet triplet) => AssertOklabDeconversion(new Oklab(triplet.First, triplet.Second, triplet.Third)); + private static void AssertOklabDeconversion(Oklab original) + { + var deconverted = Conversion.XyzToOklab(Conversion.OklabToXyz(original, Configuration.Default), Configuration.Default); + AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, OklabTolerance); } - private static ColourTuple GetRgbTupleFromHex(string hex) + private static ColourTriplet GetRgbTripletFromHex(string hex) { var (r255, g255, b255, _) = Wacton.Unicolour.Utils.ParseColourHex(hex); return new(r255 / 255.0, g255 / 255.0, b255 / 255.0); } - private static ColourTuple GetNormalisedRgb255Tuple(ColourTuple tuple) + private static ColourTriplet GetNormalisedRgb255Triplet(ColourTriplet triplet) { - var (r255, g255, b255) = tuple; + var (r255, g255, b255) = triplet; return new(r255 / 255.0, g255 / 255.0, b255 / 255.0); } } \ No newline at end of file diff --git a/Unicolour.Tests/EqualityTests.cs b/Unicolour.Tests/EqualityTests.cs index 04e99319..a3bbc33f 100644 --- a/Unicolour.Tests/EqualityTests.cs +++ b/Unicolour.Tests/EqualityTests.cs @@ -1,5 +1,6 @@ namespace Wacton.Unicolour.Tests; +using System; using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; @@ -45,40 +46,66 @@ public void EqualLabGivesEqualObjects() AssertUnicoloursEqual(unicolour1, unicolour2); } + [Test] + public void EqualOklabGivesEqualObjects() + { + var unicolour1 = GetRandomOklabUnicolour(); + var unicolour2 = Unicolour.FromOklab(unicolour1.Oklab.L, unicolour1.Oklab.A, unicolour1.Oklab.B, unicolour1.Alpha.A); + AssertUnicoloursEqual(unicolour1, unicolour2); + } + [Test] public void NotEqualRgbGivesNotEqualObjects() { var unicolour1 = GetRandomRgbUnicolour(); - var unicolour2 = Unicolour.FromRgb( - (unicolour1.Rgb.R + 0.1).Modulo(1), - (unicolour1.Rgb.G + 0.1).Modulo(1), - (unicolour1.Rgb.B + 0.1).Modulo(1), - (unicolour1.Alpha.A + 0.1).Modulo(1)); - AssertUnicoloursNotEqual(unicolour1, unicolour2); + var differentTuple = GetDifferent(unicolour1.Rgb.Triplet).Tuple; + var unicolour2 = Unicolour.FromRgb(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Rgb.Triplet); } [Test] public void NotEqualHsbGivesNotEqualObjects() { var unicolour1 = GetRandomHsbUnicolour(); - var unicolour2 = Unicolour.FromHsb( - (unicolour1.Hsb.H + 0.1).Modulo(360), - (unicolour1.Hsb.S + 0.1).Modulo(1), - (unicolour1.Hsb.B + 0.1).Modulo(1), - (unicolour1.Alpha.A + 0.1).Modulo(1)); - AssertUnicoloursNotEqual(unicolour1, unicolour2); + var differentTuple = GetDifferent(unicolour1.Hsb.Triplet).Tuple; + var unicolour2 = Unicolour.FromHsb(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Hsb.Triplet); } [Test] public void NotEqualHslGivesNotEqualObjects() { var unicolour1 = GetRandomHslUnicolour(); - var unicolour2 = Unicolour.FromHsl( - (unicolour1.Hsl.H + 0.1).Modulo(360), - (unicolour1.Hsl.S + 0.1).Modulo(1), - (unicolour1.Hsl.L + 0.1).Modulo(1), - (unicolour1.Alpha.A + 0.1).Modulo(1)); - AssertUnicoloursNotEqual(unicolour1, unicolour2); + var differentTuple = GetDifferent(unicolour1.Hsl.Triplet).Tuple; + var unicolour2 = Unicolour.FromHsl(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Hsl.Triplet); + } + + [Test] + public void NotEqualXyzGivesNotEqualObjects() + { + var unicolour1 = GetRandomXyzUnicolour(); + var differentTuple = GetDifferent(unicolour1.Xyz.Triplet).Tuple; + var unicolour2 = Unicolour.FromXyz(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Xyz.Triplet); + } + + [Test] + public void NotEqualLabGivesNotEqualObjects() + { + var unicolour1 = GetRandomLabUnicolour(); + var differentTuple = GetDifferent(unicolour1.Lab.Triplet, 1.0).Tuple; + var unicolour2 = Unicolour.FromLab(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Lab.Triplet); + } + + [Test] + public void NotEqualOklabGivesNotEqualObjects() + { + var unicolour1 = GetRandomOklabUnicolour(); + var differentTuple = GetDifferent(unicolour1.Oklab.Triplet).Tuple; + var unicolour2 = Unicolour.FromOklab(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Oklab.Triplet); } [Test] @@ -117,6 +144,8 @@ public void DifferentConfigurationObjects() private static Unicolour GetRandomHslUnicolour() => Unicolour.FromHsl(TestColours.GetRandomHsl().Tuple, TestColours.GetRandomAlpha()); private static Unicolour GetRandomXyzUnicolour() => Unicolour.FromXyz(TestColours.GetRandomXyz().Tuple, TestColours.GetRandomAlpha()); private static Unicolour GetRandomLabUnicolour() => Unicolour.FromLab(TestColours.GetRandomLab().Tuple, TestColours.GetRandomAlpha()); + private static Unicolour GetRandomOklabUnicolour() => Unicolour.FromOklab(TestColours.GetRandomOklab().Tuple, TestColours.GetRandomAlpha()); + private static ColourTriplet GetDifferent(ColourTriplet triplet, double diff = 0.1) => new(triplet.First + diff, triplet.Second + diff, triplet.Third + diff); private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicolour2) { @@ -125,17 +154,15 @@ private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicol AssertEqual(unicolour1.Hsl, unicolour2.Hsl); AssertEqual(unicolour1.Xyz, unicolour2.Xyz); AssertEqual(unicolour1.Lab, unicolour2.Lab); + AssertEqual(unicolour1.Oklab, unicolour2.Oklab); AssertEqual(unicolour1.Alpha, unicolour2.Alpha); AssertEqual(unicolour1.Luminance, unicolour2.Luminance); AssertEqual(unicolour1, unicolour2); } - private static void AssertUnicoloursNotEqual(Unicolour unicolour1, Unicolour unicolour2) + private static void AssertUnicoloursNotEqual(Unicolour unicolour1, Unicolour unicolour2, Func getTriplet) { - AssertNotEqual(unicolour1.Rgb, unicolour2.Rgb); - AssertNotEqual(unicolour1.Hsb, unicolour2.Hsb); - AssertNotEqual(unicolour1.Xyz, unicolour2.Xyz); - AssertNotEqual(unicolour1.Lab, unicolour2.Lab); + AssertNotEqual(getTriplet(unicolour1), getTriplet(unicolour2)); AssertNotEqual(unicolour1.Alpha, unicolour2.Alpha); AssertNotEqual(unicolour1.Luminance, unicolour2.Luminance); AssertNotEqual(unicolour1, unicolour2); diff --git a/Unicolour.Tests/InterpolateHsbTests.cs b/Unicolour.Tests/InterpolateHsbTests.cs index e91d9ad6..0e6f85a9 100644 --- a/Unicolour.Tests/InterpolateHsbTests.cs +++ b/Unicolour.Tests/InterpolateHsbTests.cs @@ -14,10 +14,10 @@ public void SameColour() var interpolated3 = unicolour1.InterpolateHsb(unicolour2, 0.75); var interpolated4 = unicolour2.InterpolateHsb(unicolour1, 0.25); - AssertHsba(interpolated1, (180, 0.25, 0.75, 0.5)); - AssertHsba(interpolated2, (180, 0.25, 0.75, 0.5)); - AssertHsba(interpolated3, (180, 0.25, 0.75, 0.5)); - AssertHsba(interpolated4, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated1, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated2, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated3, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated4, (180, 0.25, 0.75, 0.5)); } [Test] @@ -28,8 +28,8 @@ public void Equidistant() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 0.5); - AssertHsba(interpolated1, (90, 0.5, 0.5, 0.5)); - AssertHsba(interpolated2, (90, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated1, (90, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated2, (90, 0.5, 0.5, 0.5)); } [Test] @@ -40,8 +40,8 @@ public void EquidistantViaRed() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 0.5); - AssertHsba(interpolated1, (350, 0.25, 0.4, 0.1)); - AssertHsba(interpolated2, (350, 0.25, 0.4, 0.1)); + AssertInterpolated(interpolated1, (350, 0.25, 0.4, 0.1)); + AssertInterpolated(interpolated2, (350, 0.25, 0.4, 0.1)); } [Test] @@ -52,8 +52,8 @@ public void CloserToEndColour() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 0.75); - AssertHsba(interpolated1, (135, 0.25, 0.75, 0.625)); - AssertHsba(interpolated2, (45, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated1, (135, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (45, 0.75, 0.25, 0.875)); } [Test] @@ -64,8 +64,8 @@ public void CloserToEndColourViaRed() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 0.75); - AssertHsba(interpolated1, (30, 0.25, 0.75, 0.625)); - AssertHsba(interpolated2, (330, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated1, (30, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (330, 0.75, 0.25, 0.875)); } [Test] @@ -76,8 +76,8 @@ public void CloserToStartColour() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 0.25); - AssertHsba(interpolated1, (45, 0.75, 0.25, 0.875)); - AssertHsba(interpolated2, (135, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated1, (45, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (135, 0.25, 0.75, 0.625)); } [Test] @@ -88,8 +88,8 @@ public void CloserToStartColourViaRed() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 0.25); - AssertHsba(interpolated1, (330, 0.75, 0.25, 0.875)); - AssertHsba(interpolated2, (30, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated1, (330, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (30, 0.25, 0.75, 0.625)); } [Test] @@ -100,8 +100,8 @@ public void BeyondEndColour() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, 1.5); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, 1.5); - AssertHsba(interpolated1, (135, 0.7, 0.3, 0.95)); - AssertHsba(interpolated2, (315, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated1, (135, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated2, (315, 0.3, 0.7, 0.75)); } [Test] @@ -112,11 +112,11 @@ public void BeyondStartColour() var interpolated1 = unicolour1.InterpolateHsb(unicolour2, -0.5); var interpolated2 = unicolour2.InterpolateHsb(unicolour1, -0.5); - AssertHsba(interpolated1, (315, 0.3, 0.7, 0.75)); - AssertHsba(interpolated2, (135, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated1, (315, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated2, (135, 0.7, 0.3, 0.95)); } - private static void AssertHsba(Unicolour unicolour, (double h, double s, double b, double alpha) expected) + private static void AssertInterpolated(Unicolour unicolour, (double h, double s, double b, double alpha) expected) { var actualHsb = unicolour.Hsb; var actualAlpha = unicolour.Alpha; diff --git a/Unicolour.Tests/InterpolateHslTests.cs b/Unicolour.Tests/InterpolateHslTests.cs index 854a3fde..8fc17e43 100644 --- a/Unicolour.Tests/InterpolateHslTests.cs +++ b/Unicolour.Tests/InterpolateHslTests.cs @@ -14,10 +14,10 @@ public void SameColour() var interpolated3 = unicolour1.InterpolateHsl(unicolour2, 0.75); var interpolated4 = unicolour2.InterpolateHsl(unicolour1, 0.25); - AssertHsla(interpolated1, (180, 0.25, 0.75, 0.5)); - AssertHsla(interpolated2, (180, 0.25, 0.75, 0.5)); - AssertHsla(interpolated3, (180, 0.25, 0.75, 0.5)); - AssertHsla(interpolated4, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated1, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated2, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated3, (180, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated4, (180, 0.25, 0.75, 0.5)); } [Test] @@ -28,8 +28,8 @@ public void Equidistant() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 0.5); - AssertHsla(interpolated1, (90, 0.5, 0.5, 0.5)); - AssertHsla(interpolated2, (90, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated1, (90, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated2, (90, 0.5, 0.5, 0.5)); } [Test] @@ -40,8 +40,8 @@ public void EquidistantViaRed() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 0.5); - AssertHsla(interpolated1, (350, 0.25, 0.4, 0.1)); - AssertHsla(interpolated2, (350, 0.25, 0.4, 0.1)); + AssertInterpolated(interpolated1, (350, 0.25, 0.4, 0.1)); + AssertInterpolated(interpolated2, (350, 0.25, 0.4, 0.1)); } [Test] @@ -52,8 +52,8 @@ public void CloserToEndColour() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 0.75); - AssertHsla(interpolated1, (135, 0.25, 0.75, 0.625)); - AssertHsla(interpolated2, (45, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated1, (135, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (45, 0.75, 0.25, 0.875)); } [Test] @@ -64,8 +64,8 @@ public void CloserToEndColourViaRed() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 0.75); - AssertHsla(interpolated1, (30, 0.25, 0.75, 0.625)); - AssertHsla(interpolated2, (330, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated1, (30, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (330, 0.75, 0.25, 0.875)); } [Test] @@ -76,8 +76,8 @@ public void CloserToStartColour() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 0.25); - AssertHsla(interpolated1, (45, 0.75, 0.25, 0.875)); - AssertHsla(interpolated2, (135, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated1, (45, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (135, 0.25, 0.75, 0.625)); } [Test] @@ -88,8 +88,8 @@ public void CloserToStartColourViaRed() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 0.25); - AssertHsla(interpolated1, (330, 0.75, 0.25, 0.875)); - AssertHsla(interpolated2, (30, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated1, (330, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (30, 0.25, 0.75, 0.625)); } [Test] @@ -100,8 +100,8 @@ public void BeyondEndColour() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, 1.5); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, 1.5); - AssertHsla(interpolated1, (135, 0.7, 0.3, 0.95)); - AssertHsla(interpolated2, (315, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated1, (135, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated2, (315, 0.3, 0.7, 0.75)); } [Test] @@ -112,11 +112,11 @@ public void BeyondStartColour() var interpolated1 = unicolour1.InterpolateHsl(unicolour2, -0.5); var interpolated2 = unicolour2.InterpolateHsl(unicolour1, -0.5); - AssertHsla(interpolated1, (315, 0.3, 0.7, 0.75)); - AssertHsla(interpolated2, (135, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated1, (315, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated2, (135, 0.7, 0.3, 0.95)); } - private static void AssertHsla(Unicolour unicolour, (double h, double s, double l, double alpha) expected) + private static void AssertInterpolated(Unicolour unicolour, (double h, double s, double l, double alpha) expected) { var actualHsl = unicolour.Hsl; var actualAlpha = unicolour.Alpha; diff --git a/Unicolour.Tests/InterpolateLabTests.cs b/Unicolour.Tests/InterpolateLabTests.cs index 9cbb5572..46928166 100644 --- a/Unicolour.Tests/InterpolateLabTests.cs +++ b/Unicolour.Tests/InterpolateLabTests.cs @@ -14,10 +14,10 @@ public void SameColour() var interpolated3 = unicolour1.InterpolateLab(unicolour2, 0.75); var interpolated4 = unicolour2.InterpolateLab(unicolour1, 0.25); - AssertLaba(interpolated1, (50, -64, 64, 0.5)); - AssertLaba(interpolated2, (50, -64, 64, 0.5)); - AssertLaba(interpolated3, (50, -64, 64, 0.5)); - AssertLaba(interpolated4, (50, -64, 64, 0.5)); + AssertInterpolated(interpolated1, (50, -64, 64, 0.5)); + AssertInterpolated(interpolated2, (50, -64, 64, 0.5)); + AssertInterpolated(interpolated3, (50, -64, 64, 0.5)); + AssertInterpolated(interpolated4, (50, -64, 64, 0.5)); } [Test] @@ -28,8 +28,8 @@ public void Equidistant() var interpolated1 = unicolour1.InterpolateLab(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateLab(unicolour1, 0.5); - AssertLaba(interpolated1, (25, 0, 0, 0.5)); - AssertLaba(interpolated2, (25, 0, 0, 0.5)); + AssertInterpolated(interpolated1, (25, 0, 0, 0.5)); + AssertInterpolated(interpolated2, (25, 0, 0, 0.5)); } [Test] @@ -40,8 +40,8 @@ public void CloserToEndColour() var interpolated1 = unicolour1.InterpolateLab(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateLab(unicolour1, 0.75); - AssertLaba(interpolated1, (60, -64, 64, 0.625)); - AssertLaba(interpolated2, (20, 64, -64, 0.875)); + AssertInterpolated(interpolated1, (60, -64, 64, 0.625)); + AssertInterpolated(interpolated2, (20, 64, -64, 0.875)); } [Test] @@ -52,8 +52,8 @@ public void CloserToStartColour() var interpolated1 = unicolour1.InterpolateLab(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateLab(unicolour1, 0.25); - AssertLaba(interpolated1, (20, 64, -64, 0.875)); - AssertLaba(interpolated2, (60, -64, 64, 0.625)); + AssertInterpolated(interpolated1, (20, 64, -64, 0.875)); + AssertInterpolated(interpolated2, (60, -64, 64, 0.625)); } [Test] @@ -64,8 +64,8 @@ public void BeyondEndColour() var interpolated1 = unicolour1.InterpolateLab(unicolour2, 1.5); var interpolated2 = unicolour2.InterpolateLab(unicolour1, 1.5); - AssertLaba(interpolated1, (35, 51.2, -51.2, 0.95)); - AssertLaba(interpolated2, (15, -51.2, 51.2, 0.75)); + AssertInterpolated(interpolated1, (35, 51.2, -51.2, 0.95)); + AssertInterpolated(interpolated2, (15, -51.2, 51.2, 0.75)); } [Test] @@ -76,11 +76,11 @@ public void BeyondStartColour() var interpolated1 = unicolour1.InterpolateLab(unicolour2, -0.5); var interpolated2 = unicolour2.InterpolateLab(unicolour1, -0.5); - AssertLaba(interpolated1, (15, -51.2, 51.2, 0.75)); - AssertLaba(interpolated2, (35, 51.2, -51.2, 0.95)); + AssertInterpolated(interpolated1, (15, -51.2, 51.2, 0.75)); + AssertInterpolated(interpolated2, (35, 51.2, -51.2, 0.95)); } - private static void AssertLaba(Unicolour unicolour, (double l, double a, double b, double alpha) expected) + private static void AssertInterpolated(Unicolour unicolour, (double l, double a, double b, double alpha) expected) { var actualLab = unicolour.Lab; var actualAlpha = unicolour.Alpha; diff --git a/Unicolour.Tests/InterpolateMonochromeTests.cs b/Unicolour.Tests/InterpolateMonochromeHsbTests.cs similarity index 78% rename from Unicolour.Tests/InterpolateMonochromeTests.cs rename to Unicolour.Tests/InterpolateMonochromeHsbTests.cs index 839e9ff8..0ff2b6c1 100644 --- a/Unicolour.Tests/InterpolateMonochromeTests.cs +++ b/Unicolour.Tests/InterpolateMonochromeHsbTests.cs @@ -2,7 +2,7 @@ namespace Wacton.Unicolour.Tests; using NUnit.Framework; -public class InterpolateMonochromeTests +public class InterpolateMonochromeHsbTests { [Test] // monochrome RGB has no hue - shouldn't assume to start at red (0 degrees) when interpolating @@ -13,19 +13,19 @@ public void MonochromeStartColour() var rgbWhite = Unicolour.FromRgb255(255, 255, 255); var hsbBlack = Unicolour.FromHsb(180, 1, 0); // no brightness = black var hsbWhite = Unicolour.FromHsb(180, 0, 1); // no saturation = greyscale - var green = Unicolour.FromHsb(120, 1, 1); - var interpolatedFromRgbBlack = rgbBlack.InterpolateHsb(green, 0.5); - var interpolatedFromRgbWhite = rgbWhite.InterpolateHsb(green, 0.5); - var interpolatedFromHsbBlack = hsbBlack.InterpolateHsb(green, 0.5); - var interpolatedFromHsbWhite = hsbWhite.InterpolateHsb(green, 0.5); + var green = Unicolour.FromHsb(120, 1, 1); + var fromRgbBlack = rgbBlack.InterpolateHsb(green, 0.5); + var fromRgbWhite = rgbWhite.InterpolateHsb(green, 0.5); + var fromHsbBlack = hsbBlack.InterpolateHsb(green, 0.5); + var fromHsbWhite = hsbWhite.InterpolateHsb(green, 0.5); // note that "RGB black" interpolates differently to the "HSB black" // since monochrome RGB assumes saturation of 0 (but saturation can be any value) - AssertHsb(interpolatedFromRgbBlack.Hsb, (120, 0.5, 0.5)); - AssertHsb(interpolatedFromRgbWhite.Hsb, (120, 0.5, 1)); - AssertHsb(interpolatedFromHsbBlack.Hsb, (150, 1, 0.5)); - AssertHsb(interpolatedFromHsbWhite.Hsb, (150, 0.5, 1)); + AssertHsb(fromRgbBlack.Hsb, (120, 0.5, 0.5)); + AssertHsb(fromRgbWhite.Hsb, (120, 0.5, 1)); + AssertHsb(fromHsbBlack.Hsb, (150, 1, 0.5)); + AssertHsb(fromHsbWhite.Hsb, (150, 0.5, 1)); } [Test] @@ -37,19 +37,19 @@ public void MonochromeEndColour() var rgbWhite = Unicolour.FromRgb255(255, 255, 255); var hsbBlack = Unicolour.FromHsb(180, 1, 0); // no brightness = black var hsbWhite = Unicolour.FromHsb(180, 0, 1); // no saturation = greyscale - var blue = Unicolour.FromHsb(240, 1, 1); - var interpolatedToRgbBlack = blue.InterpolateHsb(rgbBlack, 0.5); - var interpolatedToRgbWhite = blue.InterpolateHsb(rgbWhite, 0.5); - var interpolatedToHsbBlack = blue.InterpolateHsb(hsbBlack, 0.5); - var interpolatedToHsbWhite = blue.InterpolateHsb(hsbWhite, 0.5); + var blue = Unicolour.FromHsb(240, 1, 1); + var toRgbBlack = blue.InterpolateHsb(rgbBlack, 0.5); + var toRgbWhite = blue.InterpolateHsb(rgbWhite, 0.5); + var toHsbBlack = blue.InterpolateHsb(hsbBlack, 0.5); + var toHsbWhite = blue.InterpolateHsb(hsbWhite, 0.5); // note that "RGB black" interpolates differently to the "HSB black" // since monochrome RGB assumes saturation of 0 (but saturation can be any value) - AssertHsb(interpolatedToRgbBlack.Hsb, (240, 0.5, 0.5)); - AssertHsb(interpolatedToRgbWhite.Hsb, (240, 0.5, 1)); - AssertHsb(interpolatedToHsbBlack.Hsb, (210, 1, 0.5)); - AssertHsb(interpolatedToHsbWhite.Hsb, (210, 0.5, 1)); + AssertHsb(toRgbBlack.Hsb, (240, 0.5, 0.5)); + AssertHsb(toRgbWhite.Hsb, (240, 0.5, 1)); + AssertHsb(toHsbBlack.Hsb, (210, 1, 0.5)); + AssertHsb(toHsbWhite.Hsb, (210, 0.5, 1)); } [Test] diff --git a/Unicolour.Tests/InterpolateMonochromeHslTests.cs b/Unicolour.Tests/InterpolateMonochromeHslTests.cs new file mode 100644 index 00000000..a5994d40 --- /dev/null +++ b/Unicolour.Tests/InterpolateMonochromeHslTests.cs @@ -0,0 +1,112 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; + +public class InterpolateMonochromeHslTests +{ + [Test] + // monochrome RGB has no hue - shouldn't assume to start at red (0 degrees) when interpolating + // monochrome HSL has a hue so it should be used (it just can't be seen until there is some saturation & lightness) + public void MonochromeStartColour() + { + var rgbBlack = Unicolour.FromRgb255(0, 0, 0); + var rgbWhite = Unicolour.FromRgb255(255, 255, 255); + var hslBlack = Unicolour.FromHsl(180, 1, 0); // no lightness = black + var hslWhite = Unicolour.FromHsl(180, 0, 1); // no saturation = greyscale + + var green = Unicolour.FromHsl(120, 1, 0.5); + var fromRgbBlack = rgbBlack.InterpolateHsl(green, 0.5); + var fromRgbWhite = rgbWhite.InterpolateHsl(green, 0.5); + var fromHslBlack = hslBlack.InterpolateHsl(green, 0.5); + var fromHslWhite = hslWhite.InterpolateHsl(green, 0.5); + + // note that "RGB black" interpolates differently to the "HSL black" + // since monochrome RGB assumes saturation of 0 (but saturation can be any value) + AssertHsl(fromRgbBlack.Hsl, (120, 0.5, 0.25)); + AssertHsl(fromRgbWhite.Hsl, (120, 0.5, 0.75)); + AssertHsl(fromHslBlack.Hsl, (150, 1, 0.25)); + AssertHsl(fromHslWhite.Hsl, (150, 0.5, 0.75)); + } + + [Test] + // monochrome RGB has no hue - shouldn't assume to end at red (0 degrees) when interpolating + // monochrome HSL has a hue so it should be used (it just can't be seen until there is some saturation & brightness) + public void MonochromeEndColour() + { + var rgbBlack = Unicolour.FromRgb255(0, 0, 0); + var rgbWhite = Unicolour.FromRgb255(255, 255, 255); + var hslBlack = Unicolour.FromHsl(180, 1, 0); // no brightness = black + var hslWhite = Unicolour.FromHsl(180, 0, 1); // no saturation = greyscale + + var blue = Unicolour.FromHsl(240, 1, 0.5); + var toRgbBlack = blue.InterpolateHsl(rgbBlack, 0.5); + var toRgbWhite = blue.InterpolateHsl(rgbWhite, 0.5); + var toHslBlack = blue.InterpolateHsl(hslBlack, 0.5); + var toHslWhite = blue.InterpolateHsl(hslWhite, 0.5); + + // note that "RGB black" interpolates differently to the "HSL black" + // since monochrome RGB assumes saturation of 0 (but saturation can be any value) + AssertHsl(toRgbBlack.Hsl, (240, 0.5, 0.25)); + AssertHsl(toRgbWhite.Hsl, (240, 0.5, 0.75)); + AssertHsl(toHslBlack.Hsl, (210, 1, 0.25)); + AssertHsl(toHslWhite.Hsl, (210, 0.5, 0.75)); + } + + [Test] + // monochrome RGB has no hue, so it should be ignored when interpolating + public void MonochromeBothRgbColours() + { + var black = Unicolour.FromRgb(0.0, 0.0, 0.0); + var white = Unicolour.FromRgb(1.0, 1.0, 1.0); + var grey = Unicolour.FromRgb(0.5, 0.5, 0.5); + + var blackToWhite = black.InterpolateHsl(white, 0.5); + var blackToGrey = black.InterpolateHsl(grey, 0.5); + var whiteToGrey = white.InterpolateHsl(grey, 0.5); + + AssertRgb(blackToWhite.Rgb, (0.5, 0.5, 0.5)); + AssertRgb(blackToGrey.Rgb, (0.25, 0.25, 0.25)); + AssertRgb(whiteToGrey.Rgb, (0.75, 0.75, 0.75)); + + // colours created from RGB therefore hue does not change + AssertHsl(blackToWhite.Hsl, (0, 0, 0.5)); + AssertHsl(blackToGrey.Hsl, (0, 0, 0.25)); + AssertHsl(whiteToGrey.Hsl, (0, 0, 0.75)); + } + + [Test] + // monochrome HSL has a hue so it should be used when interpolating + public void MonochromeBothHslColours() + { + var black = Unicolour.FromHsl(0, 0, 0); + var white = Unicolour.FromHsl(300, 0, 1.0); + var grey = Unicolour.FromHsl(100, 0, 0.5); + + var blackToWhite = black.InterpolateHsl(white, 0.5); + var blackToGrey = black.InterpolateHsl(grey, 0.5); + var whiteToGrey = white.InterpolateHsl(grey, 0.5); + + AssertRgb(blackToWhite.Rgb, (0.5, 0.5, 0.5)); + AssertRgb(blackToGrey.Rgb, (0.25, 0.25, 0.25)); + AssertRgb(whiteToGrey.Rgb, (0.75, 0.75, 0.75)); + + // colours created from HSL therefore hue changes + AssertHsl(blackToWhite.Hsl, (330, 0, 0.5)); + AssertHsl(blackToGrey.Hsl, (50, 0, 0.25)); + AssertHsl(whiteToGrey.Hsl, (20, 0, 0.75)); + } + + private static void AssertRgb(Rgb actualRgb, (double r, double g, double b) expectedRgb) + { + Assert.That(actualRgb.R, Is.EqualTo(expectedRgb.r).Within(0.00000000005)); + Assert.That(actualRgb.G, Is.EqualTo(expectedRgb.g).Within(0.00000000005)); + Assert.That(actualRgb.B, Is.EqualTo(expectedRgb.b).Within(0.00000000005)); + } + + private static void AssertHsl(Hsl actualHsl, (double h, double s, double l) expectedHsl) + { + Assert.That(actualHsl.H, Is.EqualTo(expectedHsl.h).Within(0.00000000005)); + Assert.That(actualHsl.S, Is.EqualTo(expectedHsl.s).Within(0.00000000005)); + Assert.That(actualHsl.L, Is.EqualTo(expectedHsl.l).Within(0.00000000005)); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/InterpolateOklabTests.cs b/Unicolour.Tests/InterpolateOklabTests.cs new file mode 100644 index 00000000..73916195 --- /dev/null +++ b/Unicolour.Tests/InterpolateOklabTests.cs @@ -0,0 +1,93 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; + +public class InterpolateOklabTests +{ + [Test] + public void SameColour() + { + var unicolour1 = Unicolour.FromOklab(0.5, 0.25, 0.75, 0.5); + var unicolour2 = Unicolour.FromOklab(0.5, 0.25, 0.75, 0.5); + var interpolated1 = unicolour1.InterpolateOklab(unicolour2, 0.25); + var interpolated2 = unicolour2.InterpolateOklab(unicolour1, 0.75); + var interpolated3 = unicolour1.InterpolateOklab(unicolour2, 0.75); + var interpolated4 = unicolour2.InterpolateOklab(unicolour1, 0.25); + + AssertInterpolated(interpolated1, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated2, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated3, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated4, (0.5, 0.25, 0.75, 0.5)); + } + + [Test] + public void Equidistant() + { + var unicolour1 = Unicolour.FromOklab(0.0, 0.0, 0.0, 0.0); + var unicolour2 = Unicolour.FromOklab(0.5, 1.0, 1.0); + var interpolated1 = unicolour1.InterpolateOklab(unicolour2, 0.5); + var interpolated2 = unicolour2.InterpolateOklab(unicolour1, 0.5); + + AssertInterpolated(interpolated1, (0.25, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated2, (0.25, 0.5, 0.5, 0.5)); + } + + [Test] + public void CloserToEndColour() + { + var unicolour1 = Unicolour.FromOklab(0, 1, 0); + var unicolour2 = Unicolour.FromOklab(0.8, 0.0, 1.0, 0.5); + var interpolated1 = unicolour1.InterpolateOklab(unicolour2, 0.75); + var interpolated2 = unicolour2.InterpolateOklab(unicolour1, 0.75); + + AssertInterpolated(interpolated1, (0.6, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (0.2, 0.75, 0.25, 0.875)); + } + + [Test] + public void CloserToStartColour() + { + var unicolour1 = Unicolour.FromOklab(0.0, 1.0, 0.0); + var unicolour2 = Unicolour.FromOklab(0.8, 0.0, 1.0, 0.5); + var interpolated1 = unicolour1.InterpolateOklab(unicolour2, 0.25); + var interpolated2 = unicolour2.InterpolateOklab(unicolour1, 0.25); + + AssertInterpolated(interpolated1, (0.2, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (0.6, 0.25, 0.75, 0.625)); + } + + [Test] + public void BeyondEndColour() + { + var unicolour1 = Unicolour.FromOklab(0.2, 0.4, 0.6, 0.8); + var unicolour2 = Unicolour.FromOklab(0.3, 0.6, 0.4, 0.9); + var interpolated1 = unicolour1.InterpolateOklab(unicolour2, 1.5); + var interpolated2 = unicolour2.InterpolateOklab(unicolour1, 1.5); + + AssertInterpolated(interpolated1, (0.35, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated2, (0.15, 0.3, 0.7, 0.75)); + } + + [Test] + public void BeyondStartColour() + { + var unicolour1 = Unicolour.FromOklab(0.2, 0.4, 0.6, 0.8); + var unicolour2 = Unicolour.FromOklab(0.3, 0.6, 0.4, 0.9); + var interpolated1 = unicolour1.InterpolateOklab(unicolour2, -0.5); + var interpolated2 = unicolour2.InterpolateOklab(unicolour1, -0.5); + + AssertInterpolated(interpolated1, (0.15, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated2, (0.35, 0.7, 0.3, 0.95)); + } + + private static void AssertInterpolated(Unicolour unicolour, (double x, double y, double z, double alpha) expected) + { + var actualOklab = unicolour.Oklab; + var actualAlpha = unicolour.Alpha; + + Assert.That(actualOklab.L, Is.EqualTo(expected.x).Within(0.00000000005)); + Assert.That(actualOklab.A, Is.EqualTo(expected.y).Within(0.00000000005)); + Assert.That(actualOklab.B, Is.EqualTo(expected.z).Within(0.00000000005)); + Assert.That(actualAlpha.A, Is.EqualTo(expected.alpha).Within(0.00000000005)); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/InterpolateRgbTests.cs b/Unicolour.Tests/InterpolateRgbTests.cs index 8ff98a77..bae61a9c 100644 --- a/Unicolour.Tests/InterpolateRgbTests.cs +++ b/Unicolour.Tests/InterpolateRgbTests.cs @@ -14,10 +14,10 @@ public void SameColour() var interpolated3 = unicolour1.InterpolateRgb(unicolour2, 0.75); var interpolated4 = unicolour2.InterpolateRgb(unicolour1, 0.25); - AssertRgba(interpolated1, (0.5, 0.25, 0.75, 0.5)); - AssertRgba(interpolated2, (0.5, 0.25, 0.75, 0.5)); - AssertRgba(interpolated3, (0.5, 0.25, 0.75, 0.5)); - AssertRgba(interpolated4, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated1, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated2, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated3, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated4, (0.5, 0.25, 0.75, 0.5)); } [Test] @@ -28,8 +28,8 @@ public void Equidistant() var interpolated1 = unicolour1.InterpolateRgb(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateRgb(unicolour1, 0.5); - AssertRgba(interpolated1, (0.25, 0.5, 0.5, 0.5)); - AssertRgba(interpolated2, (0.25, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated1, (0.25, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated2, (0.25, 0.5, 0.5, 0.5)); } [Test] @@ -40,8 +40,8 @@ public void CloserToEndColour() var interpolated1 = unicolour1.InterpolateRgb(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateRgb(unicolour1, 0.75); - AssertRgba(interpolated1, (0.6, 0.25, 0.75, 0.625)); - AssertRgba(interpolated2, (0.2, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated1, (0.6, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (0.2, 0.75, 0.25, 0.875)); } [Test] @@ -52,8 +52,8 @@ public void CloserToStartColour() var interpolated1 = unicolour1.InterpolateRgb(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateRgb(unicolour1, 0.25); - AssertRgba(interpolated1, (0.2, 0.75, 0.25, 0.875)); - AssertRgba(interpolated2, (0.6, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated1, (0.2, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (0.6, 0.25, 0.75, 0.625)); } [Test] @@ -64,8 +64,8 @@ public void BeyondEndColour() var interpolated1 = unicolour1.InterpolateRgb(unicolour2, 1.5); var interpolated2 = unicolour2.InterpolateRgb(unicolour1, 1.5); - AssertRgba(interpolated1, (0.35, 0.7, 0.3, 0.95)); - AssertRgba(interpolated2, (0.15, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated1, (0.35, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated2, (0.15, 0.3, 0.7, 0.75)); } [Test] @@ -76,11 +76,11 @@ public void BeyondStartColour() var interpolated1 = unicolour1.InterpolateRgb(unicolour2, -0.5); var interpolated2 = unicolour2.InterpolateRgb(unicolour1, -0.5); - AssertRgba(interpolated1, (0.15, 0.3, 0.7, 0.75)); - AssertRgba(interpolated2, (0.35, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated1, (0.15, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated2, (0.35, 0.7, 0.3, 0.95)); } - private static void AssertRgba(Unicolour unicolour, (double r, double g, double b, double alpha) expected) + private static void AssertInterpolated(Unicolour unicolour, (double r, double g, double b, double alpha) expected) { var actualRgb = unicolour.Rgb; var actualAlpha = unicolour.Alpha; diff --git a/Unicolour.Tests/InterpolateXyzTests.cs b/Unicolour.Tests/InterpolateXyzTests.cs index 8b96ac3a..727fefe6 100644 --- a/Unicolour.Tests/InterpolateXyzTests.cs +++ b/Unicolour.Tests/InterpolateXyzTests.cs @@ -14,10 +14,10 @@ public void SameColour() var interpolated3 = unicolour1.InterpolateXyz(unicolour2, 0.75); var interpolated4 = unicolour2.InterpolateXyz(unicolour1, 0.25); - AssertXyza(interpolated1, (0.5, 0.25, 0.75, 0.5)); - AssertXyza(interpolated2, (0.5, 0.25, 0.75, 0.5)); - AssertXyza(interpolated3, (0.5, 0.25, 0.75, 0.5)); - AssertXyza(interpolated4, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated1, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated2, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated3, (0.5, 0.25, 0.75, 0.5)); + AssertInterpolated(interpolated4, (0.5, 0.25, 0.75, 0.5)); } [Test] @@ -28,8 +28,8 @@ public void Equidistant() var interpolated1 = unicolour1.InterpolateXyz(unicolour2, 0.5); var interpolated2 = unicolour2.InterpolateXyz(unicolour1, 0.5); - AssertXyza(interpolated1, (0.25, 0.5, 0.5, 0.5)); - AssertXyza(interpolated2, (0.25, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated1, (0.25, 0.5, 0.5, 0.5)); + AssertInterpolated(interpolated2, (0.25, 0.5, 0.5, 0.5)); } [Test] @@ -40,8 +40,8 @@ public void CloserToEndColour() var interpolated1 = unicolour1.InterpolateXyz(unicolour2, 0.75); var interpolated2 = unicolour2.InterpolateXyz(unicolour1, 0.75); - AssertXyza(interpolated1, (0.6, 0.25, 0.75, 0.625)); - AssertXyza(interpolated2, (0.2, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated1, (0.6, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated2, (0.2, 0.75, 0.25, 0.875)); } [Test] @@ -52,8 +52,8 @@ public void CloserToStartColour() var interpolated1 = unicolour1.InterpolateXyz(unicolour2, 0.25); var interpolated2 = unicolour2.InterpolateXyz(unicolour1, 0.25); - AssertXyza(interpolated1, (0.2, 0.75, 0.25, 0.875)); - AssertXyza(interpolated2, (0.6, 0.25, 0.75, 0.625)); + AssertInterpolated(interpolated1, (0.2, 0.75, 0.25, 0.875)); + AssertInterpolated(interpolated2, (0.6, 0.25, 0.75, 0.625)); } [Test] @@ -64,8 +64,8 @@ public void BeyondEndColour() var interpolated1 = unicolour1.InterpolateXyz(unicolour2, 1.5); var interpolated2 = unicolour2.InterpolateXyz(unicolour1, 1.5); - AssertXyza(interpolated1, (0.35, 0.7, 0.3, 0.95)); - AssertXyza(interpolated2, (0.15, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated1, (0.35, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated2, (0.15, 0.3, 0.7, 0.75)); } [Test] @@ -76,11 +76,11 @@ public void BeyondStartColour() var interpolated1 = unicolour1.InterpolateXyz(unicolour2, -0.5); var interpolated2 = unicolour2.InterpolateXyz(unicolour1, -0.5); - AssertXyza(interpolated1, (0.15, 0.3, 0.7, 0.75)); - AssertXyza(interpolated2, (0.35, 0.7, 0.3, 0.95)); + AssertInterpolated(interpolated1, (0.15, 0.3, 0.7, 0.75)); + AssertInterpolated(interpolated2, (0.35, 0.7, 0.3, 0.95)); } - private static void AssertXyza(Unicolour unicolour, (double x, double y, double z, double alpha) expected) + private static void AssertInterpolated(Unicolour unicolour, (double x, double y, double z, double alpha) expected) { var actualXyz = unicolour.Xyz; var actualAlpha = unicolour.Alpha; diff --git a/Unicolour.Tests/OklabTests.cs b/Unicolour.Tests/OklabTests.cs new file mode 100644 index 00000000..f73fc5f6 --- /dev/null +++ b/Unicolour.Tests/OklabTests.cs @@ -0,0 +1,75 @@ +namespace Wacton.Unicolour.Tests; + +using System; +using System.Collections.Generic; +using NUnit.Framework; + +// Oklab is the only colour space so far that provides test values (https://bottosson.github.io/posts/oklab/#table-of-example-xyz-and-oklab-pairs) +// so it has its own dedicated set of tests based on those +public static class OklabTests +{ + private static readonly List<(ColourTriplet xyz, ColourTriplet expectedOklab)> TestData = new() + { + (new(0.950, 1.000, 1.089), new(1.000, 0.000, 0.000)), + (new(1.000, 0.000, 0.000), new(0.450, 1.236, -0.019)), + (new(0.000, 1.000, 0.000), new(0.922, -0.671, 0.263)), + (new(0.000, 0.000, 1.000), new(0.153, -1.415, -0.449)) + }; + + [Test] + public static void FromXyzD65() + { + foreach (var (xyz, expectedOklab) in TestData) + { + AssertFromXyzD65(xyz, expectedOklab); + } + } + + [Test] + public static void FromXyzD50() + { + foreach (var (xyz, expectedOklab) in TestData) + { + AssertFromXyzD50(xyz, expectedOklab); + } + } + + private static void AssertFromXyzD65(ColourTriplet xyz, ColourTriplet expectedOklab) + { + var (x, y, z) = xyz; + var oklab = Conversion.XyzToOklab(new Xyz(x, y, z), Configuration.Default); + AssertOklab(oklab, expectedOklab); + } + + private static void AssertFromXyzD50(ColourTriplet xyz, ColourTriplet expectedOklab) + { + // create unicolour from default D65 XYZ whitepoint + var fromXyzD65 = Unicolour.FromXyz(xyz.Tuple); + var rgb = fromXyzD65.Rgb; + + // using the D65 RGB, create a unicolour based in D50 XYZ + var configXyzD50 = new Configuration( + Chromaticity.StandardRgbR, + Chromaticity.StandardRgbG, + Chromaticity.StandardRgbB, + Companding.StandardRgb, + Companding.InverseStandardRgb, + WhitePoint.From(Illuminant.D65), + WhitePoint.From(Illuminant.D50)); + + var toXyzD50 = Unicolour.FromRgb(configXyzD50, rgb.Triplet.Tuple); + var oklabFromXyzD50 = toXyzD50.Oklab; + + // since Oklab specifically uses a D65 whitepoint + // ensure Oklab results are still as expected, despite starting in D50 XYZ + AssertOklab(oklabFromXyzD50, expectedOklab); + Assert.That(fromXyzD65.Xyz, Is.Not.EqualTo(toXyzD50.Xyz)); + } + + private static void AssertOklab(Oklab oklab, ColourTriplet expected) + { + Assert.That(Math.Round(oklab.L, 3), Is.EqualTo(expected.First)); + Assert.That(Math.Round(oklab.A, 3), Is.EqualTo(expected.Second)); + Assert.That(Math.Round(oklab.B, 3), Is.EqualTo(expected.Third)); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/OtherLibraryTests.cs b/Unicolour.Tests/OtherLibraryTests.cs index f8f3daf2..d09c23ed 100644 --- a/Unicolour.Tests/OtherLibraryTests.cs +++ b/Unicolour.Tests/OtherLibraryTests.cs @@ -21,10 +21,10 @@ public void OpenCvWindows() Assume.That(IsWindows()); AssertUtils.AssertNamedColours(namedColour => AssertFromHex(namedColour.Hex!, OpenCvFactory)); AssertUtils.AssertRandomHexColours(hex => AssertFromHex(hex, OpenCvFactory)); - AssertUtils.AssertRandomRgb255Colours(tuple => AssertFromRgb255(tuple, OpenCvFactory)); - AssertUtils.AssertRandomRgbColours(tuple => AssertFromRgb(tuple, OpenCvFactory)); - AssertUtils.AssertRandomHsbColours(tuple => AssertFromHsb(tuple, OpenCvFactory)); - AssertUtils.AssertRandomHslColours(tuple => AssertFromHsl(tuple, OpenCvFactory)); + AssertUtils.AssertRandomRgb255Colours(triplet => AssertFromRgb255(triplet, OpenCvFactory)); + AssertUtils.AssertRandomRgbColours(triplet => AssertFromRgb(triplet, OpenCvFactory)); + AssertUtils.AssertRandomHsbColours(triplet => AssertFromHsb(triplet, OpenCvFactory)); + AssertUtils.AssertRandomHslColours(triplet => AssertFromHsl(triplet, OpenCvFactory)); } [Test] @@ -40,8 +40,8 @@ public void Colourful() // no asserting random HSB colours because Colourful doesn't support HSB/HSL AssertUtils.AssertNamedColours(namedColour => AssertFromHex(namedColour.Hex!, ColourfulFactory)); AssertUtils.AssertRandomHexColours(hex => AssertFromHex(hex, ColourfulFactory)); - AssertUtils.AssertRandomRgb255Colours(tuple => AssertFromRgb255(tuple, ColourfulFactory)); - AssertUtils.AssertRandomRgbColours(tuple => AssertFromRgb(tuple, ColourfulFactory)); + AssertUtils.AssertRandomRgb255Colours(triplet => AssertFromRgb255(triplet, ColourfulFactory)); + AssertUtils.AssertRandomRgbColours(triplet => AssertFromRgb(triplet, ColourfulFactory)); } [Test] @@ -50,9 +50,9 @@ public void ColorMine() // no asserting random RGB 0-1 colours because ColorMine only accepts RGB 255 AssertUtils.AssertNamedColours(namedColour => AssertFromHex(namedColour.Hex!, ColorMineFactory)); AssertUtils.AssertRandomHexColours(hex => AssertFromHex(hex, ColorMineFactory)); - AssertUtils.AssertRandomRgb255Colours(tuple => AssertFromRgb255(tuple, ColorMineFactory)); - AssertUtils.AssertRandomHsbColours(tuple => AssertFromHsb(tuple, ColorMineFactory)); - AssertUtils.AssertRandomHslColours(tuple => AssertFromHsl(tuple, ColorMineFactory)); + AssertUtils.AssertRandomRgb255Colours(triplet => AssertFromRgb255(triplet, ColorMineFactory)); + AssertUtils.AssertRandomHsbColours(triplet => AssertFromHsb(triplet, ColorMineFactory)); + AssertUtils.AssertRandomHslColours(triplet => AssertFromHsl(triplet, ColorMineFactory)); } [Test] @@ -60,10 +60,10 @@ public void SixLabors() { AssertUtils.AssertNamedColours(namedColour => AssertFromHex(namedColour.Hex!, SixLaborsFactory)); AssertUtils.AssertRandomHexColours(hex => AssertFromHex(hex, SixLaborsFactory)); - AssertUtils.AssertRandomRgb255Colours(tuple => AssertFromRgb255(tuple, SixLaborsFactory)); - AssertUtils.AssertRandomRgbColours(tuple => AssertFromRgb(tuple, SixLaborsFactory)); - AssertUtils.AssertRandomHsbColours(tuple => AssertFromHsb(tuple, SixLaborsFactory)); - AssertUtils.AssertRandomHslColours(tuple => AssertFromHsl(tuple, SixLaborsFactory)); + AssertUtils.AssertRandomRgb255Colours(triplet => AssertFromRgb255(triplet, SixLaborsFactory)); + AssertUtils.AssertRandomRgbColours(triplet => AssertFromRgb(triplet, SixLaborsFactory)); + AssertUtils.AssertRandomHsbColours(triplet => AssertFromHsb(triplet, SixLaborsFactory)); + AssertUtils.AssertRandomHslColours(triplet => AssertFromHsl(triplet, SixLaborsFactory)); } private static void AssertFromHex(string hex, ITestColourFactory testColourFactory) @@ -75,36 +75,36 @@ private static void AssertFromHex(string hex, ITestColourFactory testColourFacto AssertTestColour(unicolour, testColour, $"HEX [{hex}]"); } - private static void AssertFromRgb255(ColourTuple tuple, ITestColourFactory testColourFactory) + private static void AssertFromRgb255(ColourTriplet triplet, ITestColourFactory testColourFactory) { - var (first, second, third) = tuple; + var (first, second, third) = triplet; var r255 = (int)(first / 255.0); var g255 = (int)(second / 255.0); var b255 = (int)(third / 255.0); var unicolour = Unicolour.FromRgb255(r255, g255, b255); var testColour = testColourFactory.FromRgb255(r255, g255, b255); - AssertTestColour(unicolour, testColour, $"RGB [{unicolour.Rgb.Tuple255}]"); + AssertTestColour(unicolour, testColour, $"RGB [{unicolour.Rgb.Triplet255}]"); } - private static void AssertFromRgb(ColourTuple tuple, ITestColourFactory testColourFactory) + private static void AssertFromRgb(ColourTriplet triplet, ITestColourFactory testColourFactory) { - var (r, g, b) = tuple; + var (r, g, b) = triplet; var unicolour = Unicolour.FromRgb(r, g, b); var testColour = testColourFactory.FromRgb(r, g, b); AssertTestColour(unicolour, testColour, $"RGB [{unicolour.Rgb}]"); } - private static void AssertFromHsb(ColourTuple tuple, ITestColourFactory testColourFactory) + private static void AssertFromHsb(ColourTriplet triplet, ITestColourFactory testColourFactory) { - var (h, s, b) = tuple; + var (h, s, b) = triplet; var unicolour = Unicolour.FromHsb(h, s, b); var testColour = testColourFactory.FromHsb(h, s, b); AssertTestColour(unicolour, testColour, $"HSB [{unicolour.Hsb}]"); } - private static void AssertFromHsl(ColourTuple tuple, ITestColourFactory testColourFactory) + private static void AssertFromHsl(ColourTriplet triplet, ITestColourFactory testColourFactory) { - var (h, s, l) = tuple; + var (h, s, l) = triplet; var unicolour = Unicolour.FromHsl(h, s, l); var testColour = testColourFactory.FromHsl(h, s, l); AssertTestColour(unicolour, testColour, $"HSL [{unicolour.Hsl}]"); @@ -134,10 +134,10 @@ private static void AssertTestColour(Unicolour unicolour, TestColour testColour, if (colourName == null) throw new ArgumentException("Malformed test colour: no name"); if (tolerances == null) throw new ArgumentException("Malformed test colour: no tolerances"); - AssertColourTuple(unicolour.Rgb.Tuple, testColour.Rgb, tolerances.Rgb, $"{source} -> RGB"); - AssertColourTuple(unicolour.Rgb.TupleLinear, testColour.RgbLinear, tolerances.RgbLinear, $"{source} -> RGB Linear"); - AssertColourTuple(unicolour.Xyz.Tuple, testColour.Xyz, tolerances.Xyz, $"{source} -> XYZ"); - AssertColourTuple(unicolour.Lab.Tuple, testColour.Lab, tolerances.Lab, $"{source} -> LAB"); + AssertColourTriplet(unicolour.Rgb.Triplet, testColour.Rgb, tolerances.Rgb, $"{source} -> RGB"); + AssertColourTriplet(unicolour.Rgb.TripletLinear, testColour.RgbLinear, tolerances.RgbLinear, $"{source} -> RGB Linear"); + AssertColourTriplet(unicolour.Xyz.Triplet, testColour.Xyz, tolerances.Xyz, $"{source} -> XYZ"); + AssertColourTriplet(unicolour.Lab.Triplet, testColour.Lab, tolerances.Lab, $"{source} -> LAB"); if (testColour.ExcludeFromHueBasedTest) { @@ -146,13 +146,13 @@ private static void AssertTestColour(Unicolour unicolour, TestColour testColour, return; } - AssertColourTuple(unicolour.Hsb.Tuple, testColour.Hsb, tolerances.Hsb, $"{source} -> HSB", true); - AssertColourTuple(unicolour.Hsl.Tuple, testColour.Hsl, tolerances.Hsl, $"{source} -> HSL", true); + AssertColourTriplet(unicolour.Hsb.Triplet, testColour.Hsb, tolerances.Hsb, $"{source} -> HSB", true); + AssertColourTriplet(unicolour.Hsl.Triplet, testColour.Hsl, tolerances.Hsl, $"{source} -> HSL", true); } - private static void AssertColourTuple(ColourTuple unicolourTuple, ColourTuple? testTuple, double tolerance, string details, bool hasHue = false) + private static void AssertColourTriplet(ColourTriplet actual, ColourTriplet? expected, double tolerance, string details, bool hasHue = false) { - if (testTuple == null) return; - AssertUtils.AssertColourTuple(unicolourTuple, testTuple, tolerance, hasHue, details); + if (expected == null) return; + AssertUtils.AssertColourTriplet(actual, expected, tolerance, hasHue, details); } } \ No newline at end of file diff --git a/Unicolour.Tests/RangeClampTests.cs b/Unicolour.Tests/RangeClampTests.cs index 5e4b8946..3a27ca61 100644 --- a/Unicolour.Tests/RangeClampTests.cs +++ b/Unicolour.Tests/RangeClampTests.cs @@ -12,8 +12,8 @@ public static void RgbRange() Range bRange = new(0.0, 1.0); var beyondMax = new Rgb(rRange.BeyondMax, gRange.BeyondMax, bRange.BeyondMax, Configuration.Default); var beyondMin = new Rgb(rRange.BeyondMin, gRange.BeyondMin, bRange.BeyondMin, Configuration.Default); - AssertClamped(beyondMax.ClampedTuple, beyondMax.Tuple); - AssertClamped(beyondMin.Tuple, beyondMin.ClampedTuple); + AssertClamped(beyondMax.ClampedTriplet, beyondMax.Triplet); + AssertClamped(beyondMin.Triplet, beyondMin.ClampedTriplet); } [Test] @@ -24,8 +24,8 @@ public static void HsbRange() Range bRange = new(0.0, 1.0); var beyondMax = new Hsb(hRange.BeyondMax, sRange.BeyondMax, bRange.BeyondMax); var beyondMin = new Hsb(hRange.BeyondMin, sRange.BeyondMin, bRange.BeyondMin); - AssertClamped(beyondMax.ClampedTuple, beyondMax.Tuple); - AssertClamped(beyondMin.Tuple, beyondMin.ClampedTuple); + AssertClamped(beyondMax.ClampedTriplet, beyondMax.Triplet); + AssertClamped(beyondMin.Triplet, beyondMin.ClampedTriplet); } [Test] @@ -36,11 +36,11 @@ public static void HslRange() Range lRange = new(0.0, 1.0); var beyondMax = new Hsl(hRange.BeyondMax, sRange.BeyondMax, lRange.BeyondMax); var beyondMin = new Hsl(hRange.BeyondMin, sRange.BeyondMin, lRange.BeyondMin); - AssertClamped(beyondMax.ClampedTuple, beyondMax.Tuple); - AssertClamped(beyondMin.Tuple, beyondMin.ClampedTuple); + AssertClamped(beyondMax.ClampedTriplet, beyondMax.Triplet); + AssertClamped(beyondMin.Triplet, beyondMin.ClampedTriplet); } - private static void AssertClamped(ColourTuple lesser, ColourTuple greater) + private static void AssertClamped(ColourTriplet lesser, ColourTriplet greater) { Assert.That(lesser.First, Is.LessThan(greater.First)); Assert.That(lesser.Second, Is.LessThan(greater.Second)); diff --git a/Unicolour.Tests/SmokeTests.cs b/Unicolour.Tests/SmokeTests.cs index 45da016c..50e3dd14 100644 --- a/Unicolour.Tests/SmokeTests.cs +++ b/Unicolour.Tests/SmokeTests.cs @@ -59,12 +59,21 @@ public static void UnicolourLab() AssertLab((100, 128, 128)); AssertLab((50, -1, 1)); } + + [Test] + public static void UnicolourOklab() + { + AssertOklab((0, 0, 0)); + AssertOklab((1, 1, 1)); + AssertOklab((0.4, 0.5, 0.6)); + } private static void AssertRgb((double, double, double) tuple) => AssertInit(tuple, Unicolour.FromRgb, Unicolour.FromRgb, Unicolour.FromRgb, Unicolour.FromRgb); private static void AssertHsb((double, double, double) tuple) => AssertInit(tuple, Unicolour.FromHsb, Unicolour.FromHsb, Unicolour.FromHsb, Unicolour.FromHsb); private static void AssertHsl((double, double, double) tuple) => AssertInit(tuple, Unicolour.FromHsl, Unicolour.FromHsl, Unicolour.FromHsl, Unicolour.FromHsl); private static void AssertXyz((double, double, double) tuple) => AssertInit(tuple, Unicolour.FromXyz, Unicolour.FromXyz, Unicolour.FromXyz, Unicolour.FromXyz); private static void AssertLab((double, double, double) tuple) => AssertInit(tuple, Unicolour.FromLab, Unicolour.FromLab, Unicolour.FromLab, Unicolour.FromLab); + private static void AssertOklab((double, double, double) tuple) => AssertInit(tuple, Unicolour.FromOklab, Unicolour.FromOklab, Unicolour.FromOklab, Unicolour.FromOklab); private delegate Unicolour FromValues(double first, double second, double third, double alpha = 1.0); private delegate Unicolour FromValuesWithConfig(Configuration config, double first, double second, double third, double alpha = 1.0); diff --git a/Unicolour.Tests/Utils/AssertUtils.cs b/Unicolour.Tests/Utils/AssertUtils.cs index ba4e3d2c..cebc9d76 100644 --- a/Unicolour.Tests/Utils/AssertUtils.cs +++ b/Unicolour.Tests/Utils/AssertUtils.cs @@ -8,12 +8,13 @@ internal static class AssertUtils { public static void AssertNamedColours(Action action) => AssertItems(TestColours.NamedColours, action); public static void AssertRandomHexColours(Action action) => AssertItems(TestColours.RandomHexColours, action); - public static void AssertRandomRgb255Colours(Action action) => AssertItems(TestColours.RandomRgb255Colours, action); - public static void AssertRandomRgbColours(Action action) => AssertItems(TestColours.RandomRgbColours, action); - public static void AssertRandomHsbColours(Action action) => AssertItems(TestColours.RandomHsbColours, action); - public static void AssertRandomHslColours(Action action) => AssertItems(TestColours.RandomHslColours, action); - public static void AssertRandomXyzColours(Action action) => AssertItems(TestColours.RandomXyzColours, action); - public static void AssertRandomLabColours(Action action) => AssertItems(TestColours.RandomLabColours, action); + public static void AssertRandomRgb255Colours(Action action) => AssertItems(TestColours.RandomRgb255Colours, action); + public static void AssertRandomRgbColours(Action action) => AssertItems(TestColours.RandomRgbColours, action); + public static void AssertRandomHsbColours(Action action) => AssertItems(TestColours.RandomHsbColours, action); + public static void AssertRandomHslColours(Action action) => AssertItems(TestColours.RandomHslColours, action); + public static void AssertRandomXyzColours(Action action) => AssertItems(TestColours.RandomXyzColours, action); + public static void AssertRandomLabColours(Action action) => AssertItems(TestColours.RandomLabColours, action); + public static void AssertRandomOklabColours(Action action) => AssertItems(TestColours.RandomOklabColours, action); private static void AssertItems(List itemsToAssert, Action assertAction) { @@ -23,7 +24,7 @@ private static void AssertItems(List itemsToAssert, Action assertAction } } - public static void AssertColourTuple(ColourTuple actual, ColourTuple expected, double tolerance, bool hasHue = false, string? details = null) + public static void AssertColourTriplet(ColourTriplet actual, ColourTriplet expected, double tolerance, bool hasHue = false, string? details = null) { double NormalisedFirst(double value) => hasHue ? value / 360.0 : value; diff --git a/Unicolour.Tests/Utils/TestColour.cs b/Unicolour.Tests/Utils/TestColour.cs index 8d9782c6..dd569c73 100644 --- a/Unicolour.Tests/Utils/TestColour.cs +++ b/Unicolour.Tests/Utils/TestColour.cs @@ -7,12 +7,12 @@ internal class TestColour { public string? Name { get; init; } public string? Hex { get; init; } - public ColourTuple? Rgb { get; init; } - public ColourTuple? RgbLinear { get; init; } - public ColourTuple? Hsl { get; init; } - public ColourTuple? Hsb { get; init; } - public ColourTuple? Xyz { get; init; } - public ColourTuple? Lab { get; init; } + public ColourTriplet? Rgb { get; init; } + public ColourTriplet? RgbLinear { get; init; } + public ColourTriplet? Hsl { get; init; } + public ColourTriplet? Hsb { get; init; } + public ColourTriplet? Xyz { get; init; } + public ColourTriplet? Lab { get; init; } public Tolerances? Tolerances { get; init; } /* diff --git a/Unicolour.Tests/Utils/TestColours.cs b/Unicolour.Tests/Utils/TestColours.cs index 83c47e8c..e2f5ec54 100644 --- a/Unicolour.Tests/Utils/TestColours.cs +++ b/Unicolour.Tests/Utils/TestColours.cs @@ -12,12 +12,13 @@ internal static class TestColours public static readonly List NamedColours; public static readonly List RandomHexColours = new(); - public static readonly List RandomRgb255Colours = new(); - public static readonly List RandomRgbColours = new(); - public static readonly List RandomHsbColours = new(); - public static readonly List RandomHslColours = new(); - public static readonly List RandomXyzColours = new(); - public static readonly List RandomLabColours = new(); + public static readonly List RandomRgb255Colours = new(); + public static readonly List RandomRgbColours = new(); + public static readonly List RandomHsbColours = new(); + public static readonly List RandomHslColours = new(); + public static readonly List RandomXyzColours = new(); + public static readonly List RandomLabColours = new(); + public static readonly List RandomOklabColours = new(); static TestColours() { @@ -33,15 +34,17 @@ static TestColours() RandomHslColours.Add(GetRandomHsl()); RandomXyzColours.Add(GetRandomXyz()); RandomLabColours.Add(GetRandomLab()); + RandomOklabColours.Add(GetRandomOklab()); } } - internal static ColourTuple GetRandomRgb255() => new(Random.Next(256), Random.Next(256), Random.Next(256)); - internal static ColourTuple GetRandomRgb() => new(Random.NextDouble(), Random.NextDouble(), Random.NextDouble()); - internal static ColourTuple GetRandomHsb() => new(Random.NextDouble() * 360, Random.NextDouble(), Random.NextDouble()); - internal static ColourTuple GetRandomHsl() => new(Random.NextDouble() * 360, Random.NextDouble(), Random.NextDouble()); - internal static ColourTuple GetRandomXyz() => new(Random.NextDouble(), Random.NextDouble(), Random.NextDouble()); - internal static ColourTuple GetRandomLab() => new(Random.NextDouble() * 100, Random.NextDouble() * 256 - 128, Random.NextDouble() * 256 - 128); + internal static ColourTriplet GetRandomRgb255() => new(Random.Next(256), Random.Next(256), Random.Next(256)); + internal static ColourTriplet GetRandomRgb() => new(Random.NextDouble(), Random.NextDouble(), Random.NextDouble()); + internal static ColourTriplet GetRandomHsb() => new(Random.NextDouble() * 360, Random.NextDouble(), Random.NextDouble()); + internal static ColourTriplet GetRandomHsl() => new(Random.NextDouble() * 360, Random.NextDouble(), Random.NextDouble()); + internal static ColourTriplet GetRandomXyz() => new(Random.NextDouble(), Random.NextDouble(), Random.NextDouble()); + internal static ColourTriplet GetRandomLab() => new(Random.NextDouble() * 100, Random.NextDouble() * 256 - 128, Random.NextDouble() * 256 - 128); + internal static ColourTriplet GetRandomOklab() => new(Random.NextDouble(), Random.NextDouble(), Random.NextDouble()); internal static double GetRandomAlpha() => Random.NextDouble(); diff --git a/Unicolour.Tests/UtilsTests.cs b/Unicolour.Tests/UtilsTests.cs index 21e97f02..8e6740aa 100644 --- a/Unicolour.Tests/UtilsTests.cs +++ b/Unicolour.Tests/UtilsTests.cs @@ -1,19 +1,42 @@ namespace Wacton.Unicolour.Tests; using NUnit.Framework; +using static Wacton.Unicolour.Utils; public class UtilsTests { [Test] - public void Clamp() + public void ClampWithinRange() { Assert.That(0.5.Clamp(0, 1), Is.EqualTo(0.5)); - Assert.That(1.00001.Clamp(0, 1), Is.EqualTo(1.0)); Assert.That(1.0.Clamp(0, 1), Is.EqualTo(1.0)); Assert.That(0.0.Clamp(0, 1), Is.EqualTo(0.0)); + } + + [Test] + public void ClampOutwithRange() + { + Assert.That(1.00001.Clamp(0, 1), Is.EqualTo(1.0)); Assert.That((-0.00001).Clamp(0, 1), Is.EqualTo(0.0)); } + [Test] + public void CubeRootPositive() + { + Assert.That(CubeRoot(8), Is.EqualTo(2)); + Assert.That(CubeRoot(0.027), Is.EqualTo(0.3).Within(0.0000000000000001)); + } + + [Test] + public void CubeRootNegative() + { + Assert.That(CubeRoot(-8), Is.EqualTo(-2)); + Assert.That(CubeRoot(-0.027), Is.EqualTo(-0.3).Within(0.0000000000000001)); + } + + [Test] + public void CubeRootZero() => Assert.That(CubeRoot(0), Is.EqualTo(0)); + [Test] public void ModuloSameAsDividend([Values(-10, -1, -0.1, 0.1, 1, 10)] double dividend) { diff --git a/Unicolour/ColourTuple.cs b/Unicolour/ColourTriplet.cs similarity index 76% rename from Unicolour/ColourTuple.cs rename to Unicolour/ColourTriplet.cs index e08dfbbb..e02dd548 100644 --- a/Unicolour/ColourTuple.cs +++ b/Unicolour/ColourTriplet.cs @@ -1,6 +1,6 @@ namespace Wacton.Unicolour; -public record ColourTuple(double First, double Second, double Third) +public record ColourTriplet(double First, double Second, double Third) { public double First { get; } = First; public double Second { get; } = Second; diff --git a/Unicolour/Configuration.cs b/Unicolour/Configuration.cs index 1d98cdb2..59b7e6a7 100644 --- a/Unicolour/Configuration.cs +++ b/Unicolour/Configuration.cs @@ -10,9 +10,6 @@ public class Configuration public Func Compand { get; } public Func InverseCompand { get; } - internal Matrix RgbToXyzMatrix { get; } - internal Matrix XyzToRgbMatrix { get; } - // default is sRGB model (defined by these chromaticities, illuminant/observer, and sRGB linear correction) // and will transform into D65-based XYZ colour space public static readonly Configuration Default = new( @@ -35,8 +32,6 @@ public Configuration(Chromaticity chromaticityR, Chromaticity chromaticityG, Chr InverseCompand = inverseCompand; RgbWhitePoint = rgbWhitePoint; XyzWhitePoint = xyzWhitePoint; - RgbToXyzMatrix = Matrices.RgbToXyzMatrix(this); - XyzToRgbMatrix = RgbToXyzMatrix.Inverse(); } public override string ToString() => $"RGB {RgbWhitePoint} {ChromaticityR} {ChromaticityG} {ChromaticityB} -> XYZ {XyzWhitePoint} "; diff --git a/Unicolour/Conversion.cs b/Unicolour/Conversion.cs index 29fa6471..68156bec 100644 --- a/Unicolour/Conversion.cs +++ b/Unicolour/Conversion.cs @@ -1,5 +1,7 @@ namespace Wacton.Unicolour; +using static Wacton.Unicolour.Utils; + internal static class Conversion { // https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB @@ -115,7 +117,7 @@ public static Xyz RgbToXyz(Rgb rgb, Configuration config) {rgb.BLinear} }); - var transformationMatrix = config.RgbToXyzMatrix; + var transformationMatrix = Matrices.RgbToXyzMatrix(config); var xyzMatrix = transformationMatrix.Multiply(rgbLinearMatrix); var x = xyzMatrix[0, 0]; @@ -135,7 +137,7 @@ public static Rgb XyzToRgb(Xyz xyz, Configuration config) {xyz.Z} }); - var transformationMatrix = config.XyzToRgbMatrix; + var transformationMatrix = Matrices.RgbToXyzMatrix(config).Inverse(); var rgbLinearMatrix = transformationMatrix.Multiply(xyzMatrix); var rLinear = rgbLinearMatrix[0, 0]; @@ -162,7 +164,7 @@ public static Lab XyzToLab(Xyz xyz, Configuration config) var zRatio = z * 100 / referenceWhite.Z; var delta = 6.0 / 29.0; - double F(double t) => t > Math.Pow(delta, 3) ? Math.Pow(t, 1 / 3.0) : t * (1 / 3.0) * Math.Pow(delta, -2) + 4.0 / 29.0; + double F(double t) => t > Math.Pow(delta, 3) ? CubeRoot(t) : t * (1 / 3.0) * Math.Pow(delta, -2) + 4.0 / 29.0; var l = 116 * F(yRatio) - 16; var a = 500 * (F(xRatio) - F(yRatio)); var b = 200 * (F(yRatio) - F(zRatio)); @@ -186,4 +188,58 @@ public static Xyz LabToXyz(Lab lab, Configuration config) return new Xyz(x, y, z); } + + // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab + public static Oklab XyzToOklab(Xyz xyz, Configuration config) + { + var xyzMatrix = new Matrix(new[,] + { + {xyz.X}, + {xyz.Y}, + {xyz.Z} + }); + + var adaptedXyz = Matrices.AdaptForWhitePoint(xyzMatrix, config.XyzWhitePoint, WhitePoint.From(Illuminant.D65)); + var lmsMatrix = Matrices.OklabM1.Multiply(adaptedXyz); + var lmsNonLinearMatrix = new Matrix(new[,] + { + {CubeRoot(lmsMatrix[0, 0])}, + {CubeRoot(lmsMatrix[1, 0])}, + {CubeRoot(lmsMatrix[2, 0])} + }); + + var labMatrix = Matrices.OklabM2.Multiply(lmsNonLinearMatrix); + + var l = labMatrix[0, 0]; + var a = labMatrix[1, 0]; + var b = labMatrix[2, 0]; + return new Oklab(l, a, b); + } + + // https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab + public static Xyz OklabToXyz(Oklab oklab, Configuration config) + { + var labMatrix = new Matrix(new[,] + { + {oklab.L}, + {oklab.A}, + {oklab.B} + }); + + var lmsNonLinearMatrix = Matrices.OklabM2.Inverse().Multiply(labMatrix); + var lmsMatrix = new Matrix(new[,] + { + {Math.Pow(lmsNonLinearMatrix[0, 0], 3)}, + {Math.Pow(lmsNonLinearMatrix[1, 0], 3)}, + {Math.Pow(lmsNonLinearMatrix[2, 0], 3)} + }); + + var xyzMatrix = Matrices.OklabM1.Inverse().Multiply(lmsMatrix); + var adaptedXyz = Matrices.AdaptForWhitePoint(xyzMatrix, WhitePoint.From(Illuminant.D65), config.XyzWhitePoint); + + var x = adaptedXyz[0, 0]; + var y = adaptedXyz[1, 0]; + var z = adaptedXyz[2, 0]; + return new Xyz(x, y, z); + } } \ No newline at end of file diff --git a/Unicolour/Hsb.cs b/Unicolour/Hsb.cs index b274f5fc..213587cd 100644 --- a/Unicolour/Hsb.cs +++ b/Unicolour/Hsb.cs @@ -6,12 +6,12 @@ public record Hsb public double H { get; } public double S { get; } public double B { get; } - public ColourTuple Tuple => new(H, S, B); + public ColourTriplet Triplet => new(H, S, B); public double ClampedH => H.Clamp(0.0, 360.0); public double ClampedS => S.Clamp(0.0, 1.0); public double ClampedB => B.Clamp(0.0, 1.0); - public ColourTuple ClampedTuple => new(ClampedH, ClampedS, ClampedB); + public ColourTriplet ClampedTriplet => new(ClampedH, ClampedS, ClampedB); // RGB(0,0,0) is black, but has no explicit hue (and don't want to assume red) // HSB(0,0,0) is black, but want to acknowledge the explicit red hue of 0 diff --git a/Unicolour/Hsl.cs b/Unicolour/Hsl.cs index bf1aa60d..7cddcb2f 100644 --- a/Unicolour/Hsl.cs +++ b/Unicolour/Hsl.cs @@ -6,12 +6,12 @@ public record Hsl public double H { get; } public double S { get; } public double L { get; } - public ColourTuple Tuple => new(H, S, L); + public ColourTriplet Triplet => new(H, S, L); public double ClampedH => H.Clamp(0.0, 360.0); public double ClampedS => S.Clamp(0.0, 1.0); public double ClampedL => L.Clamp(0.0, 1.0); - public ColourTuple ClampedTuple => new(ClampedH, ClampedS, ClampedL); + public ColourTriplet ClampedTriplet => new(ClampedH, ClampedS, ClampedL); public bool HasHue => explicitHue || S > 0.0 && L is > 0.0 and < 1.0; diff --git a/Unicolour/Interpolation.cs b/Unicolour/Interpolation.cs index 557c5ee0..97ba9f10 100644 --- a/Unicolour/Interpolation.cs +++ b/Unicolour/Interpolation.cs @@ -6,7 +6,7 @@ public static Unicolour InterpolateRgb(this Unicolour startColour, Unicolour end { GuardConfiguration(startColour, endColour); - var (r, g, b) = InterpolateTuple(startColour.Rgb.Tuple, endColour.Rgb.Tuple, distance); + var (r, g, b) = InterpolateTriplet(startColour.Rgb.Triplet, endColour.Rgb.Triplet, distance); var alpha = Interpolate(startColour.Alpha.A, endColour.Alpha.A, distance); return Unicolour.FromRgb(startColour.Config, r, g, b, alpha); } @@ -17,8 +17,8 @@ public static Unicolour InterpolateHsb(this Unicolour startColour, Unicolour end var startHsb = startColour.Hsb; var endHsb = endColour.Hsb; - var (start, end) = GetHueBasedTuples((startHsb.HasHue, startHsb.Tuple), (endHsb.HasHue, endHsb.Tuple)); - var (h,s, b) = InterpolateTuple(start, end, distance); + var (start, end) = GetHueBasedTriplets((startHsb.HasHue, startHsb.Triplet), (endHsb.HasHue, endHsb.Triplet)); + var (h,s, b) = InterpolateTriplet(start, end, distance); var alpha = Interpolate(startColour.Alpha.A, endColour.Alpha.A, distance); return Unicolour.FromHsb(startColour.Config, h.Modulo(360), s, b, alpha); } @@ -29,17 +29,17 @@ public static Unicolour InterpolateHsl(this Unicolour startColour, Unicolour end var startHsl = startColour.Hsl; var endHsl = endColour.Hsl; - var (start, end) = GetHueBasedTuples((startHsl.HasHue, startHsl.Tuple), (endHsl.HasHue, endHsl.Tuple)); - var (h,s, l) = InterpolateTuple(start, end, distance); + var (start, end) = GetHueBasedTriplets((startHsl.HasHue, startHsl.Triplet), (endHsl.HasHue, endHsl.Triplet)); + var (h,s, l) = InterpolateTriplet(start, end, distance); var alpha = Interpolate(startColour.Alpha.A, endColour.Alpha.A, distance); return Unicolour.FromHsl(startColour.Config, h.Modulo(360), s, l, alpha); } - + public static Unicolour InterpolateXyz(this Unicolour startColour, Unicolour endColour, double distance) { GuardConfiguration(startColour, endColour); - var (x, y, z) = InterpolateTuple(startColour.Xyz.Tuple, endColour.Xyz.Tuple, distance); + var (x, y, z) = InterpolateTriplet(startColour.Xyz.Triplet, endColour.Xyz.Triplet, distance); var alpha = Interpolate(startColour.Alpha.A, endColour.Alpha.A, distance); return Unicolour.FromXyz(startColour.Config, x, y, z, alpha); } @@ -48,23 +48,32 @@ public static Unicolour InterpolateLab(this Unicolour startColour, Unicolour end { GuardConfiguration(startColour, endColour); - var (l, a, b) = InterpolateTuple(startColour.Lab.Tuple, endColour.Lab.Tuple, distance); + var (l, a, b) = InterpolateTriplet(startColour.Lab.Triplet, endColour.Lab.Triplet, distance); var alpha = Interpolate(startColour.Alpha.A, endColour.Alpha.A, distance); return Unicolour.FromLab(startColour.Config, l, a, b, alpha); } + + public static Unicolour InterpolateOklab(this Unicolour startColour, Unicolour endColour, double distance) + { + GuardConfiguration(startColour, endColour); + + var (l, a, b) = InterpolateTriplet(startColour.Oklab.Triplet, endColour.Oklab.Triplet, distance); + var alpha = Interpolate(startColour.Alpha.A, endColour.Alpha.A, distance); + return Unicolour.FromOklab(startColour.Config, l, a, b, alpha); + } - private static (ColourTuple startHue, ColourTuple endHue) GetHueBasedTuples((bool hasHue, ColourTuple tuple) start, (bool hasHue, ColourTuple tuple) end) + private static (ColourTriplet startHue, ColourTriplet endHue) GetHueBasedTriplets((bool hasHue, ColourTriplet triplet) start, (bool hasHue, ColourTriplet triplet) end) { - double Hue(ColourTuple tuple) => tuple.First; + double Hue(ColourTriplet triplet) => triplet.First; - (ColourTuple, ColourTuple) Result(double startHue, double endHue) => ( - new(startHue, start.tuple.Second, start.tuple.Third), - new(endHue, end.tuple.Second, end.tuple.Third)); + (ColourTriplet, ColourTriplet) Result(double startHue, double endHue) => ( + new(startHue, start.triplet.Second, start.triplet.Third), + new(endHue, end.triplet.Second, end.triplet.Third)); // don't use hue if one colour is monochrome (e.g. black n/a° to green 120° should always stay at hue 120°) var noHue = !start.hasHue && !end.hasHue; - var startHue = noHue || start.hasHue ? Hue(start.tuple) : Hue(end.tuple); - var endHue = noHue || end.hasHue ? Hue(end.tuple) : Hue(start.tuple); + var startHue = noHue || start.hasHue ? Hue(start.triplet) : Hue(end.triplet); + var endHue = noHue || end.hasHue ? Hue(end.triplet) : Hue(start.triplet); if (startHue > endHue) { @@ -83,7 +92,7 @@ private static (ColourTuple startHue, ColourTuple endHue) GetHueBasedTuples((boo return Result(startHue, endHue); } - private static ColourTuple InterpolateTuple(ColourTuple start, ColourTuple end, double distance) + private static ColourTriplet InterpolateTriplet(ColourTriplet start, ColourTriplet end, double distance) { var first = Interpolate(start.First, end.First, distance); var second = Interpolate(start.Second, end.Second, distance); diff --git a/Unicolour/Lab.cs b/Unicolour/Lab.cs index 6bf717f8..134be477 100644 --- a/Unicolour/Lab.cs +++ b/Unicolour/Lab.cs @@ -5,7 +5,7 @@ public record Lab(double L, double A, double B) public double L { get; } = L; public double A { get; } = A; public double B { get; } = B; - public ColourTuple Tuple => new(L, A, B); + public ColourTriplet Triplet => new(L, A, B); public override string ToString() { diff --git a/Unicolour/Matrices.cs b/Unicolour/Matrices.cs index 6b43010f..5c3c0fcd 100644 --- a/Unicolour/Matrices.cs +++ b/Unicolour/Matrices.cs @@ -4,9 +4,23 @@ internal static class Matrices { private static readonly Matrix Bradford = new(new[,] { - {0.8951, 0.2664, -0.1614}, - {-0.7502, 1.7135, 0.0367}, - {0.0389, -0.0685, 1.0296} + {+0.8951, +0.2664, -0.1614}, + {-0.7502, +1.7135, +0.0367}, + {+0.0389, -0.0685, +1.0296} + }); + + public static readonly Matrix OklabM1 = new(new[,] + { + {+0.8189330101, +0.3618667424, -0.1288597137}, + {+0.0329845436, +0.9293118715, +0.0361456387}, + {+0.0482003018, +0.2643662691, +0.6338517070} + }); + + public static readonly Matrix OklabM2 = new(new[,] + { + {+0.2104542553, +0.7936177850, -0.0040720468}, + {+1.9779984951, -2.4285922050, +0.4505937099}, + {+0.0259040371, +0.7827717662, -0.8086757660} }); // based on http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html @@ -47,12 +61,18 @@ public static Matrix RgbToXyzMatrix(Configuration config) {sr * zr, sg * zg, sb * zb} }); - if (rgbWhitePoint == xyzWhitePoint) + var adaptedMatrix = AdaptForWhitePoint(matrix, rgbWhitePoint, xyzWhitePoint); + return adaptedMatrix; + } + + public static Matrix AdaptForWhitePoint(Matrix matrix, WhitePoint sourceWhitePoint, WhitePoint destinationWhitePoint) + { + if (sourceWhitePoint == destinationWhitePoint) { return matrix; } - - var adaptedBradford = AdaptedBradfordMatrix(rgbWhitePoint, xyzWhitePoint); + + var adaptedBradford = AdaptedBradfordMatrix(sourceWhitePoint, destinationWhitePoint); return adaptedBradford.Multiply(matrix); } diff --git a/Unicolour/Oklab.cs b/Unicolour/Oklab.cs new file mode 100644 index 00000000..bbcf0521 --- /dev/null +++ b/Unicolour/Oklab.cs @@ -0,0 +1,11 @@ +namespace Wacton.Unicolour; + +public record Oklab(double L, double A, double B) +{ + public double L { get; } = L; + public double A { get; } = A; + public double B { get; } = B; + public ColourTriplet Triplet => new(L, A, B); + + public override string ToString() => $"{Math.Round(L, 2)} {Math.Round(A, 2)} {Math.Round(B, 2)}"; +} \ No newline at end of file diff --git a/Unicolour/Rgb.cs b/Unicolour/Rgb.cs index 562340d5..91ba2bb3 100644 --- a/Unicolour/Rgb.cs +++ b/Unicolour/Rgb.cs @@ -5,23 +5,23 @@ public record Rgb public double R { get; } public double G { get; } public double B { get; } - public ColourTuple Tuple => new(R, G, B); + public ColourTriplet Triplet => new(R, G, B); public double ClampedR => R.Clamp(0.0, 1.0); public double ClampedG => G.Clamp(0.0, 1.0); public double ClampedB => B.Clamp(0.0, 1.0); - public ColourTuple ClampedTuple => new(ClampedR, ClampedG, ClampedB); + public ColourTriplet ClampedTriplet => new(ClampedR, ClampedG, ClampedB); public int R255 => (int) Math.Round(ClampedR * 255); public int G255 => (int) Math.Round(ClampedG * 255); public int B255 => (int) Math.Round(ClampedB * 255); - public ColourTuple Tuple255 => new(R255, G255, B255); + public ColourTriplet Triplet255 => new(R255, G255, B255); private readonly Func inverseCompand; public double RLinear => inverseCompand(R); public double GLinear => inverseCompand(G); public double BLinear => inverseCompand(B); - public ColourTuple TupleLinear => new(RLinear, GLinear, BLinear); + public ColourTriplet TripletLinear => new(RLinear, GLinear, BLinear); public string Hex => $"#{R255:X2}{G255:X2}{B255:X2}"; diff --git a/Unicolour/Unicolour.cs b/Unicolour/Unicolour.cs index 505e07fc..2121f9a6 100644 --- a/Unicolour/Unicolour.cs +++ b/Unicolour/Unicolour.cs @@ -8,12 +8,14 @@ public partial class Unicolour : IEquatable private Hsl? hsl; private Xyz? xyz; private Lab? lab; + private Oklab? oklab; public Rgb Rgb => Get(() => rgb, ColourSpace.Rgb)!; public Hsb Hsb => Get(() => hsb, ColourSpace.Hsb)!; public Hsl Hsl => Get(() => hsl, ColourSpace.Hsl)!; public Xyz Xyz => Get(() => xyz, ColourSpace.Xyz)!; public Lab Lab => Get(() => lab, ColourSpace.Lab)!; + public Oklab Oklab => Get(() => oklab, ColourSpace.Oklab)!; public Alpha Alpha { get; } public Configuration Config { get; } @@ -55,6 +57,7 @@ private bool ColourSpaceEquals(Unicolour other) ColourSpace.Hsl => Hsl.Equals(other.Hsl), ColourSpace.Xyz => Xyz.Equals(other.Xyz), ColourSpace.Lab => Lab.Equals(other.Lab), + ColourSpace.Oklab => Oklab.Equals(other.Oklab), _ => throw new ArgumentOutOfRangeException() }; } @@ -70,6 +73,7 @@ public override int GetHashCode() ColourSpace.Hsl => Hsl.GetHashCode() * 397, ColourSpace.Xyz => Xyz.GetHashCode() * 397, ColourSpace.Lab => Lab.GetHashCode() * 397, + ColourSpace.Oklab => Oklab.GetHashCode() * 397, _ => throw new ArgumentOutOfRangeException() }; diff --git a/Unicolour/Unicolour.csproj b/Unicolour/Unicolour.csproj index 5385c6a1..58e3bb39 100644 --- a/Unicolour/Unicolour.csproj +++ b/Unicolour/Unicolour.csproj @@ -15,9 +15,9 @@ netstandard2.0 True Resources\Unicolour.png - 1.2.0 - colour color converter RGB HSB HSV HSL LAB XYZ color-spaces colour-spaces interpolation comparison contrast luminance deltaE - Enable initialisation from XYZ & LAB values + 1.3.0 + colour color converter RGB HSB HSV HSL XYZ LAB Oklab color-spaces colour-spaces interpolation comparison contrast luminance deltaE + Add Oklab support Resources\Unicolour.ico LICENSE diff --git a/Unicolour/UnicolourConstructors.cs b/Unicolour/UnicolourConstructors.cs index cc21f285..47693f0b 100644 --- a/Unicolour/UnicolourConstructors.cs +++ b/Unicolour/UnicolourConstructors.cs @@ -7,6 +7,7 @@ public partial class Unicolour private Unicolour(Configuration config, Hsl hsl, Alpha alpha) : this(config, alpha, ColourSpace.Hsl) => this.hsl = hsl; private Unicolour(Configuration config, Xyz xyz, Alpha alpha) : this(config, alpha, ColourSpace.Xyz) => this.xyz = xyz; private Unicolour(Configuration config, Lab lab, Alpha alpha) : this(config, alpha, ColourSpace.Lab) => this.lab = lab; + private Unicolour(Configuration config, Oklab oklab, Alpha alpha) : this(config, alpha, ColourSpace.Oklab) => this.oklab = oklab; public static Unicolour FromHex(string hex) => FromHex(Configuration.Default, hex); public static Unicolour FromHex(Configuration config, string hex) @@ -44,4 +45,9 @@ public static Unicolour FromHex(Configuration config, string hex) public static Unicolour FromLab(Configuration config, (double l, double a, double b) tuple, double alpha = 1.0) => FromLab(config, tuple.l, tuple.a, tuple.b, alpha); public static Unicolour FromLab(double l, double a, double b, double alpha = 1.0) => FromLab(Configuration.Default, l, a, b, alpha); public static Unicolour FromLab(Configuration config, double l, double a, double b, double alpha = 1.0) => new(config, new Lab(l, a, b), new Alpha(alpha)); + + public static Unicolour FromOklab((double l, double a, double b) tuple, double alpha = 1.0) => FromOklab(Configuration.Default, tuple.l, tuple.a, tuple.b, alpha); + public static Unicolour FromOklab(Configuration config, (double l, double a, double b) tuple, double alpha = 1.0) => FromOklab(config, tuple.l, tuple.a, tuple.b, alpha); + public static Unicolour FromOklab(double l, double a, double b, double alpha = 1.0) => FromOklab(Configuration.Default, l, a, b, alpha); + public static Unicolour FromOklab(Configuration config, double l, double a, double b, double alpha = 1.0) => new(config, new Oklab(l, a, b), new Alpha(alpha)); } \ No newline at end of file diff --git a/Unicolour/UnicolourSpaces.cs b/Unicolour/UnicolourSpaces.cs index cc47dd3e..b8a300dd 100644 --- a/Unicolour/UnicolourSpaces.cs +++ b/Unicolour/UnicolourSpaces.cs @@ -2,7 +2,7 @@ public partial class Unicolour { - private enum ColourSpace { Rgb, Hsb, Hsl, Xyz, Lab } + private enum ColourSpace { Rgb, Hsb, Hsl, Xyz, Lab, Oklab } private readonly Dictionary> conversions = new(); @@ -13,7 +13,8 @@ private void SetupConversions() {ColourSpace.Hsb, () => hsb = Conversion.RgbToHsb(Rgb)}, {ColourSpace.Hsl, () => hsl = Conversion.HsbToHsl(Hsb)}, {ColourSpace.Xyz, () => xyz = Conversion.RgbToXyz(Rgb, Config)}, - {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)} + {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)}, + {ColourSpace.Oklab, () => oklab = Conversion.XyzToOklab(Xyz, Config)} }); conversions.Add(ColourSpace.Hsb, new Dictionary @@ -21,7 +22,8 @@ private void SetupConversions() {ColourSpace.Rgb, () => rgb = Conversion.HsbToRgb(Hsb, Config)}, {ColourSpace.Hsl, () => hsl = Conversion.HsbToHsl(Hsb)}, {ColourSpace.Xyz, () => xyz = Conversion.RgbToXyz(Rgb, Config)}, - {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)} + {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)}, + {ColourSpace.Oklab, () => oklab = Conversion.XyzToOklab(Xyz, Config)} }); conversions.Add(ColourSpace.Hsl, new Dictionary @@ -29,7 +31,8 @@ private void SetupConversions() {ColourSpace.Rgb, () => rgb = Conversion.HsbToRgb(Hsb, Config)}, {ColourSpace.Hsb, () => hsb = Conversion.HslToHsb(Hsl)}, {ColourSpace.Xyz, () => xyz = Conversion.RgbToXyz(Rgb, Config)}, - {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)} + {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)}, + {ColourSpace.Oklab, () => oklab = Conversion.XyzToOklab(Xyz, Config)} }); conversions.Add(ColourSpace.Xyz, new Dictionary @@ -37,7 +40,8 @@ private void SetupConversions() {ColourSpace.Rgb, () => rgb = Conversion.XyzToRgb(Xyz, Config)}, {ColourSpace.Hsb, () => hsb = Conversion.RgbToHsb(Rgb)}, {ColourSpace.Hsl, () => hsl = Conversion.HsbToHsl(Hsb)}, - {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)} + {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)}, + {ColourSpace.Oklab, () => oklab = Conversion.XyzToOklab(Xyz, Config)} }); conversions.Add(ColourSpace.Lab, new Dictionary @@ -45,7 +49,17 @@ private void SetupConversions() {ColourSpace.Rgb, () => rgb = Conversion.XyzToRgb(Xyz, Config)}, {ColourSpace.Hsb, () => hsb = Conversion.RgbToHsb(Rgb)}, {ColourSpace.Hsl, () => hsl = Conversion.HsbToHsl(Hsb)}, - {ColourSpace.Xyz, () => xyz = Conversion.LabToXyz(Lab, Config)} + {ColourSpace.Xyz, () => xyz = Conversion.LabToXyz(Lab, Config)}, + {ColourSpace.Oklab, () => oklab = Conversion.XyzToOklab(Xyz, Config)} + }); + + conversions.Add(ColourSpace.Oklab, new Dictionary + { + {ColourSpace.Rgb, () => rgb = Conversion.XyzToRgb(Xyz, Config)}, + {ColourSpace.Hsb, () => hsb = Conversion.RgbToHsb(Rgb)}, + {ColourSpace.Hsl, () => hsl = Conversion.HsbToHsl(Hsb)}, + {ColourSpace.Xyz, () => xyz = Conversion.OklabToXyz(Oklab, Config)}, + {ColourSpace.Lab, () => lab = Conversion.XyzToLab(Xyz, Config)} }); } diff --git a/Unicolour/Utils.cs b/Unicolour/Utils.cs index 4d604b3e..38c29be5 100644 --- a/Unicolour/Utils.cs +++ b/Unicolour/Utils.cs @@ -5,6 +5,8 @@ internal static class Utils { public static double Clamp(this double value, double min, double max) => value < min ? min : value > max ? max : value; + + public static double CubeRoot(double x) => x < 0 ? -Math.Pow(-x, 1 / 3.0) : Math.Pow(x, 1 / 3.0); public static double Modulo(this double value, double modulus) { diff --git a/Unicolour/Xyz.cs b/Unicolour/Xyz.cs index e47d92c2..820560b3 100644 --- a/Unicolour/Xyz.cs +++ b/Unicolour/Xyz.cs @@ -5,7 +5,7 @@ public record Xyz(double X, double Y, double Z) public double X { get; } = X; public double Y { get; } = Y; public double Z { get; } = Z; - public ColourTuple Tuple => new(X, Y, Z); + public ColourTriplet Triplet => new(X, Y, Z); public override string ToString() => $"{Math.Round(X, 2)} {Math.Round(Y, 2)} {Math.Round(Z, 2)}"; } \ No newline at end of file