From 425276aa902eb0ed992bfd8c15377be8f2766809 Mon Sep 17 00:00:00 2001 From: Wacton Date: Sun, 22 Oct 2023 15:05:55 +0000 Subject: [PATCH] Add HCT support --- README.md | 9 +- Unicolour.Console/Program.cs | 1 + Unicolour.Console/colour-info.png | Bin 11601 -> 26054 bytes Unicolour.Example/Program.cs | 4 +- Unicolour.Example/gradients.png | Bin 118685 -> 123032 bytes ...igurationTests.cs => ConfigureCamTests.cs} | 6 +- ...urationTests.cs => ConfigureIctcpTests.cs} | 14 +- ...rationTests.cs => ConfigureJzazbzTests.cs} | 14 +- ...igurationTests.cs => ConfigureRgbTests.cs} | 24 +- ...igurationTests.cs => ConfigureXyzTests.cs} | 26 +- Unicolour.Tests/ContrastTests.cs | 10 +- Unicolour.Tests/ConversionTests.cs | 362 ------------------ Unicolour.Tests/CoordinateSpaceTests.cs | 9 +- Unicolour.Tests/DescriptionTests.cs | 58 +-- Unicolour.Tests/DifferenceTests.cs | 58 +-- Unicolour.Tests/DisplayableColourTests.cs | 6 +- Unicolour.Tests/EqualityTests.cs | 24 ++ Unicolour.Tests/ExtremeValuesTests.cs | 61 +-- Unicolour.Tests/GreyscaleTests.cs | 60 ++- Unicolour.Tests/HueOverrideTests.cs | 11 +- Unicolour.Tests/HuedTests.cs | 3 + .../InterpolateGreyscaleHctTests.cs | 124 ++++++ .../InterpolateGreyscaleHpluvTests.cs | 5 - .../InterpolateGreyscaleHsbTests.cs | 2 - .../InterpolateGreyscaleHslTests.cs | 2 - .../InterpolateGreyscaleHsluvTests.cs | 5 - .../InterpolateGreyscaleHwbTests.cs | 2 - .../InterpolateGreyscaleJzczhzTests.cs | 2 - .../InterpolateGreyscaleLchabTests.cs | 2 - .../InterpolateGreyscaleLchuvTests.cs | 2 - .../InterpolateGreyscaleOklchTests.cs | 2 - Unicolour.Tests/InterpolateHctTests.cs | 124 ++++++ Unicolour.Tests/InterpolateHeritageTests.cs | 2 +- .../{Cam02Tests.cs => KnownCam02Tests.cs} | 12 +- .../{Cam16Tests.cs => KnownCam16Tests.cs} | 10 +- Unicolour.Tests/KnownHctTests.cs | 91 +++++ .../{HsluvTests.cs => KnownHsluvTests.cs} | 4 +- .../{OklabTests.cs => KnownOklabTests.cs} | 6 +- .../{XyyTests.cs => KnownXyyTests.cs} | 12 +- Unicolour.Tests/LazyEvaluationTests.cs | 39 +- Unicolour.Tests/LibraryColorMineTests.cs | 30 ++ Unicolour.Tests/LibraryColourfulTests.cs | 40 ++ Unicolour.Tests/LibraryOpenCvTests.cs | 61 +++ Unicolour.Tests/LibrarySixLaborsTests.cs | 38 ++ Unicolour.Tests/MatrixTests.cs | 18 +- Unicolour.Tests/NamedColoursTests.cs | 33 ++ Unicolour.Tests/NotNumberTests.cs | 43 ++- .../ColorMineFactory.cs | 4 +- .../ColourfulFactory.cs | 4 +- .../ITestColourFactory.cs | 2 +- .../OtherLibraries/LibraryTestBase.cs | 112 ++++++ .../OpenCvCsvFactory.cs | 2 +- .../OpenCvFactory.cs | 4 +- .../SixLaborsFactory.cs | 4 +- Unicolour.Tests/OtherLibraryTests.cs | 249 ------------ Unicolour.Tests/README.md | 3 +- Unicolour.Tests/RangeClampTests.cs | 85 ++-- Unicolour.Tests/RoundtripCam02Tests.cs | 29 ++ Unicolour.Tests/RoundtripCam16Tests.cs | 29 ++ Unicolour.Tests/RoundtripHctTests.cs | 41 ++ Unicolour.Tests/RoundtripHpluvTests.cs | 17 + Unicolour.Tests/RoundtripHsbTests.cs | 49 +++ Unicolour.Tests/RoundtripHslTests.cs | 22 ++ Unicolour.Tests/RoundtripHsluvTests.cs | 17 + Unicolour.Tests/RoundtripHwbTests.cs | 33 ++ Unicolour.Tests/RoundtripIctcpTests.cs | 20 + Unicolour.Tests/RoundtripJzazbzTests.cs | 21 + Unicolour.Tests/RoundtripJzczhzTests.cs | 17 + Unicolour.Tests/RoundtripLabTests.cs | 26 ++ Unicolour.Tests/RoundtripLchabTests.cs | 17 + Unicolour.Tests/RoundtripLchuvTests.cs | 33 ++ Unicolour.Tests/RoundtripLuvTests.cs | 26 ++ Unicolour.Tests/RoundtripOklabTests.cs | 26 ++ Unicolour.Tests/RoundtripOklchTests.cs | 17 + Unicolour.Tests/RoundtripRgbTests.cs | 65 ++++ Unicolour.Tests/RoundtripXyyTests.cs | 18 + Unicolour.Tests/RoundtripXyzTests.cs | 121 ++++++ Unicolour.Tests/SmokeTests.cs | 49 +-- Unicolour.Tests/Utils/AssertUtils.cs | 1 + Unicolour.Tests/Utils/RandomColours.cs | 6 +- Unicolour/Cam.cs | 6 +- Unicolour/Cam02.cs | 4 +- Unicolour/Cam16.cs | 4 +- Unicolour/CamConfiguration.cs | 9 + Unicolour/Hct.cs | 146 +++++++ Unicolour/Ictcp.cs | 4 +- Unicolour/Interpolation.cs | 2 + Unicolour/Jzazbz.cs | 3 +- Unicolour/Jzczhz.cs | 4 +- Unicolour/Lab.cs | 2 +- Unicolour/Lchab.cs | 2 +- Unicolour/Lchuv.cs | 2 +- Unicolour/Luv.cs | 2 +- Unicolour/Oklab.cs | 2 +- Unicolour/Oklch.cs | 2 +- Unicolour/Resources/diagram.png | Bin 27421 -> 46948 bytes Unicolour/Unicolour.cs | 2 + Unicolour/Unicolour.csproj | 8 +- Unicolour/UnicolourConstructors.cs | 6 + Unicolour/UnicolourLookups.cs | 18 +- Unicolour/Xyz.cs | 10 +- 101 files changed, 1916 insertions(+), 962 deletions(-) rename Unicolour.Tests/{CamConfigurationTests.cs => ConfigureCamTests.cs} (90%) rename Unicolour.Tests/{IctcpConfigurationTests.cs => ConfigureIctcpTests.cs} (95%) rename Unicolour.Tests/{JzazbzConfigurationTests.cs => ConfigureJzazbzTests.cs} (95%) rename Unicolour.Tests/{RgbConfigurationTests.cs => ConfigureRgbTests.cs} (95%) rename Unicolour.Tests/{XyzConfigurationTests.cs => ConfigureXyzTests.cs} (95%) delete mode 100644 Unicolour.Tests/ConversionTests.cs create mode 100644 Unicolour.Tests/InterpolateGreyscaleHctTests.cs create mode 100644 Unicolour.Tests/InterpolateHctTests.cs rename Unicolour.Tests/{Cam02Tests.cs => KnownCam02Tests.cs} (97%) rename Unicolour.Tests/{Cam16Tests.cs => KnownCam16Tests.cs} (97%) create mode 100644 Unicolour.Tests/KnownHctTests.cs rename Unicolour.Tests/{HsluvTests.cs => KnownHsluvTests.cs} (95%) rename Unicolour.Tests/{OklabTests.cs => KnownOklabTests.cs} (95%) rename Unicolour.Tests/{XyyTests.cs => KnownXyyTests.cs} (82%) create mode 100644 Unicolour.Tests/LibraryColorMineTests.cs create mode 100644 Unicolour.Tests/LibraryColourfulTests.cs create mode 100644 Unicolour.Tests/LibraryOpenCvTests.cs create mode 100644 Unicolour.Tests/LibrarySixLaborsTests.cs create mode 100644 Unicolour.Tests/NamedColoursTests.cs rename Unicolour.Tests/{Factories => OtherLibraries}/ColorMineFactory.cs (98%) rename Unicolour.Tests/{Factories => OtherLibraries}/ColourfulFactory.cs (99%) rename Unicolour.Tests/{Factories => OtherLibraries}/ITestColourFactory.cs (97%) create mode 100644 Unicolour.Tests/OtherLibraries/LibraryTestBase.cs rename Unicolour.Tests/{Factories => OtherLibraries}/OpenCvCsvFactory.cs (98%) rename Unicolour.Tests/{Factories => OtherLibraries}/OpenCvFactory.cs (98%) rename Unicolour.Tests/{Factories => OtherLibraries}/SixLaborsFactory.cs (99%) delete mode 100644 Unicolour.Tests/OtherLibraryTests.cs create mode 100644 Unicolour.Tests/RoundtripCam02Tests.cs create mode 100644 Unicolour.Tests/RoundtripCam16Tests.cs create mode 100644 Unicolour.Tests/RoundtripHctTests.cs create mode 100644 Unicolour.Tests/RoundtripHpluvTests.cs create mode 100644 Unicolour.Tests/RoundtripHsbTests.cs create mode 100644 Unicolour.Tests/RoundtripHslTests.cs create mode 100644 Unicolour.Tests/RoundtripHsluvTests.cs create mode 100644 Unicolour.Tests/RoundtripHwbTests.cs create mode 100644 Unicolour.Tests/RoundtripIctcpTests.cs create mode 100644 Unicolour.Tests/RoundtripJzazbzTests.cs create mode 100644 Unicolour.Tests/RoundtripJzczhzTests.cs create mode 100644 Unicolour.Tests/RoundtripLabTests.cs create mode 100644 Unicolour.Tests/RoundtripLchabTests.cs create mode 100644 Unicolour.Tests/RoundtripLchuvTests.cs create mode 100644 Unicolour.Tests/RoundtripLuvTests.cs create mode 100644 Unicolour.Tests/RoundtripOklabTests.cs create mode 100644 Unicolour.Tests/RoundtripOklchTests.cs create mode 100644 Unicolour.Tests/RoundtripRgbTests.cs create mode 100644 Unicolour.Tests/RoundtripXyyTests.cs create mode 100644 Unicolour.Tests/RoundtripXyzTests.cs create mode 100644 Unicolour/Hct.cs diff --git a/README.md b/README.md index 0777ddcc..901d9704 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ A `Unicolour` encapsulates a single colour and its representation across differe - Oklch - CIECAM02 - CAM16 +- HCT
Diagram of colour space relationships @@ -58,7 +59,7 @@ Unicolour can be used to calculate colour difference via: - ΔECAM02 - ΔECAM16 -Simulations of colour vision deficiency (CVD) / colour blindness is supported for: +Simulation of colour vision deficiency (CVD) / colour blindness is supported for: - Protanopia (no red perception) - Deuteranopia (no green perception) - Tritanopia (no blue perception) @@ -70,7 +71,7 @@ These [can be overridden](#advanced-configuration-) using the `Configuration` pa This library was initially written for personal projects since existing libraries had complex APIs or missing features. The goal of this library is to be accurate, intuitive, and easy to use. Although performance is not a priority, conversions are only calculated once — when first evaluated (either on access or as part of an intermediate conversion step) the result is stored for future use. -It is also [extensively tested](Unicolour.Tests), including comparisons against known colour values and other .NET libraries. +It is also [extensively tested](Unicolour.Tests), including verification of roundtrip conversions, validation using known colour values, and comparisons against other .NET libraries. Targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) for use in .NET 5.0+, .NET Core 2.0+ and .NET Framework 4.6.1+ applications. @@ -98,6 +99,7 @@ Targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net | Oklch | `Unicolour.FromOklch()` | `.Oklch` | `.InterpolateOklch()` | | CIECAM02 | `Unicolour.FromCam02()` | `.Cam02` | `.InterpolateCam02()` | | CAM16 | `Unicolour.FromCam16()` | `.Cam16` | `.InterpolateCam16()` | +| HCT | `Unicolour.FromHct()` | `.Hct` | `.InterpolateHct()` | ## How to use 🌈 1. Install the package from [NuGet](https://www.nuget.org/packages/Wacton.Unicolour/) @@ -133,6 +135,7 @@ var unicolour = Unicolour.FromOklab(0.65, 0.26, -0.01); var unicolour = Unicolour.FromOklch(0.65, 0.26, 356.9); var unicolour = Unicolour.FromCam02(62.86, 40.81, -1.18); var unicolour = Unicolour.FromCam16(62.47, 42.60, -1.36); +var unicolour = Unicolour.FromHct(358.2, 100.38, 55.96); ``` 4. Get representations of a colour in different colour spaces: @@ -156,6 +159,7 @@ var oklab = unicolour.Oklab; var oklch = unicolour.Oklch; var cam02 = unicolour.Cam02; var cam16 = unicolour.Cam16; +var hct = unicolour.Hct; ``` 5. Get properties of a colour @@ -186,6 +190,7 @@ var interpolated = unicolour1.InterpolateOklab(unicolour2, 0.5); var interpolated = unicolour1.InterpolateOklch(unicolour2, 0.5); var interpolated = unicolour1.InterpolateCam02(unicolour2, 0.5); var interpolated = unicolour1.InterpolateCam16(unicolour2, 0.5); +var interpolated = unicolour1.InterpolateHct(unicolour2, 0.5); ``` 7. Compare colours: diff --git a/Unicolour.Console/Program.cs b/Unicolour.Console/Program.cs index 2b0da641..6066ba73 100644 --- a/Unicolour.Console/Program.cs +++ b/Unicolour.Console/Program.cs @@ -72,6 +72,7 @@ static Table GetTable(Unicolour unicolour) table.AddRow("Oklch", $"{unicolour.Oklch}"); table.AddRow("Cam02", $"{unicolour.Cam02}"); table.AddRow("Cam16", $"{unicolour.Cam16}"); + table.AddRow("Hct", $"{unicolour.Hct}"); return table; } diff --git a/Unicolour.Console/colour-info.png b/Unicolour.Console/colour-info.png index 5b863dbb3ba4ee0ba29a87250db262d57ad64933..820ca6baa070d8180712c7046c4a696dedcc6c4c 100644 GIT binary patch literal 26054 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 DrawColumn(Unicolour[] colourPoints) var oklch = Gradient.Draw(("OKLCH", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.InterpolateOklch(end, distance)); var cam02 = Gradient.Draw(("CAM02", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.InterpolateCam02(end, distance)); var cam16 = Gradient.Draw(("CAM16", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.InterpolateCam16(end, distance)); + var hct = Gradient.Draw(("HCT", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.InterpolateHct(end, distance)); var columnImage = new Image(columnWidth, rowHeight * rows); columnImage.Mutate(context => context @@ -77,6 +78,7 @@ Image DrawColumn(Unicolour[] colourPoints) .DrawImage(oklch, new Point(0, rowHeight * 16), 1f) .DrawImage(cam02, new Point(0, rowHeight * 17), 1f) .DrawImage(cam16, new Point(0, rowHeight * 18), 1f) + .DrawImage(hct, new Point(0, rowHeight * 19), 1f) ); return columnImage; diff --git a/Unicolour.Example/gradients.png b/Unicolour.Example/gradients.png index aafb60605799c89cd5fb32d4d2f180889815df6c..476e21014c1a2b2f2d83a21cb87e3b25a66b98ab 100644 GIT binary patch delta 10821 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 diff --git a/Unicolour.Tests/CamConfigurationTests.cs b/Unicolour.Tests/ConfigureCamTests.cs similarity index 90% rename from Unicolour.Tests/CamConfigurationTests.cs rename to Unicolour.Tests/ConfigureCamTests.cs index b2a5e02a..8c747509 100644 --- a/Unicolour.Tests/CamConfigurationTests.cs +++ b/Unicolour.Tests/ConfigureCamTests.cs @@ -3,7 +3,7 @@ namespace Wacton.Unicolour.Tests; using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class CamConfigurationTests +public class ConfigureCamTests { [TestCase(Illuminant.A)] [TestCase(Illuminant.C)] @@ -15,7 +15,7 @@ public static class CamConfigurationTests [TestCase(Illuminant.F2)] [TestCase(Illuminant.F7)] [TestCase(Illuminant.F11)] - public static void XyzWhitePointRoundTripViaCam02(Illuminant xyzIlluminant) + public void XyzWhitePointRoundTripViaCam02(Illuminant xyzIlluminant) { var initialXyzConfig = new XyzConfiguration(CamConfiguration.StandardRgb.WhitePoint); var initialXyz = new Xyz(0.4676, 0.2387, 0.2974); @@ -37,7 +37,7 @@ public static void XyzWhitePointRoundTripViaCam02(Illuminant xyzIlluminant) [TestCase(Illuminant.F2)] [TestCase(Illuminant.F7)] [TestCase(Illuminant.F11)] - public static void XyzWhitePointRoundTripViaCam16(Illuminant xyzIlluminant) + public void XyzWhitePointRoundTripViaCam16(Illuminant xyzIlluminant) { var initialXyzConfig = new XyzConfiguration(CamConfiguration.StandardRgb.WhitePoint); var initialXyz = new Xyz(0.4676, 0.2387, 0.2974); diff --git a/Unicolour.Tests/IctcpConfigurationTests.cs b/Unicolour.Tests/ConfigureIctcpTests.cs similarity index 95% rename from Unicolour.Tests/IctcpConfigurationTests.cs rename to Unicolour.Tests/ConfigureIctcpTests.cs index d7c3a90f..c9b170c5 100644 --- a/Unicolour.Tests/IctcpConfigurationTests.cs +++ b/Unicolour.Tests/ConfigureIctcpTests.cs @@ -4,7 +4,7 @@ using Wacton.Unicolour.Datasets; using Wacton.Unicolour.Tests.Utils; -public static class IctcpConfigurationTests +public class ConfigureIctcpTests { private static readonly WhitePoint D65WhitePoint = WhitePoint.From(Illuminant.D65); private static readonly ColourTriplet XyzWhite = new(D65WhitePoint.X / 100.0, D65WhitePoint.Y / 100.0, D65WhitePoint.Z / 100.0); @@ -20,7 +20,7 @@ public static class IctcpConfigurationTests private static readonly Configuration Config203 = new(RgbConfiguration.Rec2020, XyzConfiguration.D65, ictcpScalar: 203); [Test] // matches the behaviour of papers on Hung & Berns dataset (https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf, figure 6) - public static void Rec2020RgbToIctcp100() + public void Rec2020RgbToIctcp100() { var red = Unicolour.FromXyz(Config100, HungBerns.RedRef.Xyz.Triplet.Tuple); var blue = Unicolour.FromXyz(Config100, HungBerns.BlueRef.Xyz.Triplet.Tuple); @@ -42,7 +42,7 @@ public static void Rec2020RgbToIctcp100() } [Test] // matches the behaviour of python-based "colour-science/colour" (https://github.com/colour-science/colour#31224ictcp-colour-encoding) - public static void Rec2020RgbToIctcp1() + public void Rec2020RgbToIctcp1() { var unicolour = Unicolour.FromRgb(Config1, TestRgb.Tuple); AssertUtils.AssertTriplet(unicolour.Rgb.Linear.Triplet, TestLinearRgb, 0.00000000001); @@ -55,7 +55,7 @@ public static void Rec2020RgbToIctcp1() } [Test] // matches the behaviour of javascript-based "color.js" (https://github.com/LeaVerou/color.js / https://colorjs.io/apps/picker) - public static void Rec2020RgbToIctcp203() + public void Rec2020RgbToIctcp203() { var unicolour = Unicolour.FromRgb(Config203, TestRgb.Tuple); AssertUtils.AssertTriplet(unicolour.Rgb.Linear.Triplet, TestLinearRgb, 0.00000000001); @@ -68,7 +68,7 @@ public static void Rec2020RgbToIctcp203() } [Test] - public static void ConvertTestColour() + public void ConvertTestColour() { var initial100 = Unicolour.FromRgb(Config100, TestRgb.Tuple); var convertedTo1 = initial100.ConvertToConfiguration(Config1); @@ -81,7 +81,7 @@ public static void ConvertTestColour() } [Test] - public static void ConvertWhite() + public void ConvertWhite() { var initial100 = Unicolour.FromXyz(Config100, XyzWhite.Tuple); var convertedTo1 = initial100.ConvertToConfiguration(Config1); @@ -95,7 +95,7 @@ public static void ConvertWhite() } [Test] - public static void ConvertBlack() + public void ConvertBlack() { var initial100 = Unicolour.FromXyz(Config100, 0, 0, 0); var convertedTo1 = initial100.ConvertToConfiguration(Config1); diff --git a/Unicolour.Tests/JzazbzConfigurationTests.cs b/Unicolour.Tests/ConfigureJzazbzTests.cs similarity index 95% rename from Unicolour.Tests/JzazbzConfigurationTests.cs rename to Unicolour.Tests/ConfigureJzazbzTests.cs index b60b1dba..4eddaccf 100644 --- a/Unicolour.Tests/JzazbzConfigurationTests.cs +++ b/Unicolour.Tests/ConfigureJzazbzTests.cs @@ -4,7 +4,7 @@ using Wacton.Unicolour.Datasets; using Wacton.Unicolour.Tests.Utils; -public static class JzazbzConfigurationTests +public class ConfigureJzazbzTests { private static readonly WhitePoint D65WhitePoint = WhitePoint.From(Illuminant.D65); private static readonly ColourTriplet XyzWhite = new(D65WhitePoint.X / 100.0, D65WhitePoint.Y / 100.0, D65WhitePoint.Z / 100.0); @@ -17,7 +17,7 @@ public static class JzazbzConfigurationTests private static readonly Configuration Config203 = new(RgbConfiguration.StandardRgb, XyzConfiguration.D65, jzazbzScalar: 203); [Test] // matches the behaviour of papers on Hung & Berns dataset (https://www.researchgate.net/figure/The-Hung-Berns-data-plotted-in-six-different-color-spaces-a-CIELAB-b-CIELUV-c_fig2_317811721) - public static void XyzD65ToJzazbz100() + public void XyzD65ToJzazbz100() { var red = Unicolour.FromXyz(Config100, HungBerns.RedRef.Xyz.Triplet.Tuple); var blue = Unicolour.FromXyz(Config100, HungBerns.BlueRef.Xyz.Triplet.Tuple); @@ -39,7 +39,7 @@ public static void XyzD65ToJzazbz100() } [Test] // matches the behaviour of python-based "colour-science/colour" (https://github.com/colour-science/colour#31212jzazbz-colourspace) - public static void XyzD65ToJzazbz1() + public void XyzD65ToJzazbz1() { var unicolour = Unicolour.FromXyz(Config1, TestXyz.Tuple); AssertUtils.AssertTriplet(unicolour, new(0.00535048, 0.00924302, 0.00526007), 0.00001); @@ -51,7 +51,7 @@ public static void XyzD65ToJzazbz1() } [Test] // matches the behaviour of javascript-based "color.js" (https://github.com/LeaVerou/color.js / https://colorjs.io/apps/picker) - public static void XyzD65ToJzazbz203() + public void XyzD65ToJzazbz203() { var unicolour = Unicolour.FromXyz(Config203, TestXyz.Tuple); AssertUtils.AssertTriplet(unicolour, new(0.10287841, 0.08613415, 0.05873694), 0.0001); @@ -63,7 +63,7 @@ public static void XyzD65ToJzazbz203() } [Test] - public static void ConvertTestColour() + public void ConvertTestColour() { var initial100 = Unicolour.FromXyz(Config100, TestXyz.Tuple); var convertedTo1 = initial100.ConvertToConfiguration(Config1); @@ -76,7 +76,7 @@ public static void ConvertTestColour() } [Test] - public static void ConvertWhite() + public void ConvertWhite() { var initial100 = Unicolour.FromXyz(Config100, XyzWhite.Tuple); var convertedTo1 = initial100.ConvertToConfiguration(Config1); @@ -90,7 +90,7 @@ public static void ConvertWhite() } [Test] - public static void ConvertBlack() + public void ConvertBlack() { var initial100 = Unicolour.FromXyz(Config100, 0, 0, 0); var convertedTo1 = initial100.ConvertToConfiguration(Config1); diff --git a/Unicolour.Tests/RgbConfigurationTests.cs b/Unicolour.Tests/ConfigureRgbTests.cs similarity index 95% rename from Unicolour.Tests/RgbConfigurationTests.cs rename to Unicolour.Tests/ConfigureRgbTests.cs index 56a0c9a2..0070c4de 100644 --- a/Unicolour.Tests/RgbConfigurationTests.cs +++ b/Unicolour.Tests/ConfigureRgbTests.cs @@ -8,7 +8,7 @@ * expected colour values for these tests based on calculations from * http://www.brucelindbloom.com/index.html?ColorCalculator.html */ -public static class RgbConfigurationTests +public class ConfigureRgbTests { private const double Tolerance = 0.01; @@ -45,7 +45,7 @@ public static class RgbConfigurationTests }; [Test] - public static void XyzD65ToStandardRgbD65() + public void XyzD65ToStandardRgbD65() { // https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB var expectedMatrixA = new[,] @@ -80,7 +80,7 @@ public static void XyzD65ToStandardRgbD65() } [Test] - public static void XyzD50ToStandardRgbD65() + public void XyzD50ToStandardRgbD65() { var standardRgbConfig = new RgbConfiguration( Chromaticity.StandardRgb.R, @@ -111,7 +111,7 @@ public static void XyzD50ToStandardRgbD65() } [Test] - public static void XyzD65ToAdobeRgbD65() + public void XyzD65ToAdobeRgbD65() { var adobeRgbConfig = new RgbConfiguration( AdobeChromaticityR, @@ -131,7 +131,7 @@ public static void XyzD65ToAdobeRgbD65() } [Test] - public static void XyzD50ToAdobeRgbD65() + public void XyzD50ToAdobeRgbD65() { var adobeRgbConfig = new RgbConfiguration( AdobeChromaticityR, @@ -151,7 +151,7 @@ public static void XyzD50ToAdobeRgbD65() } [Test] - public static void XyzD65ToWideGamutRgbD50() + public void XyzD65ToWideGamutRgbD50() { var wideGamutRgbConfig = new RgbConfiguration( WideGamutChromaticityR, @@ -171,7 +171,7 @@ public static void XyzD65ToWideGamutRgbD50() } [Test] - public static void XyzD50ToWideGamutRgbD50() + public void XyzD50ToWideGamutRgbD50() { var wideGamutRgbConfig = new RgbConfiguration( WideGamutChromaticityR, @@ -191,7 +191,7 @@ public static void XyzD50ToWideGamutRgbD50() } [Test] - public static void ConvertStandardRgbToDisplayP3() + public void ConvertStandardRgbToDisplayP3() { var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65); var displayP3Config = new Configuration(RgbConfiguration.DisplayP3, XyzConfiguration.D65); @@ -205,7 +205,7 @@ public static void ConvertStandardRgbToDisplayP3() } [Test] - public static void ConvertDisplayP3ToStandardRgb() + public void ConvertDisplayP3ToStandardRgb() { var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65); var displayP3Config = new Configuration(RgbConfiguration.DisplayP3, XyzConfiguration.D65); @@ -219,7 +219,7 @@ public static void ConvertDisplayP3ToStandardRgb() } [Test] - public static void ConvertStandardRgbToRec2020() + public void ConvertStandardRgbToRec2020() { var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65); var rec2020Config = new Configuration(RgbConfiguration.Rec2020, XyzConfiguration.D65); @@ -233,7 +233,7 @@ public static void ConvertStandardRgbToRec2020() } [Test] - public static void ConvertRec2020ToStandardRgb() + public void ConvertRec2020ToStandardRgb() { var standardRgbConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65); var rec2020Config = new Configuration(RgbConfiguration.Rec2020, XyzConfiguration.D65); @@ -256,7 +256,7 @@ public static void ConvertRec2020ToStandardRgb() [TestCase(Illuminant.F2)] [TestCase(Illuminant.F7)] [TestCase(Illuminant.F11)] - public static void XyzWhitePointRoundTrip(Illuminant xyzIlluminant) + public void XyzWhitePointRoundTrip(Illuminant xyzIlluminant) { var initialXyzConfig = new XyzConfiguration(RgbConfiguration.StandardRgb.WhitePoint); var initialXyz = new Xyz(0.4676, 0.2387, 0.2974); diff --git a/Unicolour.Tests/XyzConfigurationTests.cs b/Unicolour.Tests/ConfigureXyzTests.cs similarity index 95% rename from Unicolour.Tests/XyzConfigurationTests.cs rename to Unicolour.Tests/ConfigureXyzTests.cs index dc396d12..43947fee 100644 --- a/Unicolour.Tests/XyzConfigurationTests.cs +++ b/Unicolour.Tests/ConfigureXyzTests.cs @@ -8,7 +8,7 @@ * expected colour values for these tests based on calculations from * http://www.brucelindbloom.com/index.html?ColorCalculator.html */ -public static class XyzConfigurationTests +public class ConfigureXyzTests { private const double XyzTolerance = 0.001; private const double LabTolerance = 0.05; @@ -27,7 +27,7 @@ public static class XyzConfigurationTests private const double Gamma = 2.19921875; [Test] - public static void StandardRgbD65ToXyzD65() + public void StandardRgbD65ToXyzD65() { // https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ var expectedMatrixA = new[,] @@ -64,7 +64,7 @@ public static void StandardRgbD65ToXyzD65() } [Test] - public static void StandardRgbD65ToXyzD50() + public void StandardRgbD65ToXyzD50() { var standardRgbConfig = new RgbConfiguration( Chromaticity.StandardRgb.R, @@ -97,7 +97,7 @@ public static void StandardRgbD65ToXyzD50() } [Test] - public static void AdobeRgbD65ToXyzD65() + public void AdobeRgbD65ToXyzD65() { var adobeRgbConfig = new RgbConfiguration( AdobeChromaticityR, @@ -119,7 +119,7 @@ public static void AdobeRgbD65ToXyzD65() } [Test] - public static void AdobeRgbD65ToXyzD50() + public void AdobeRgbD65ToXyzD50() { var adobeRgbConfig = new RgbConfiguration( AdobeChromaticityR, @@ -141,7 +141,7 @@ public static void AdobeRgbD65ToXyzD50() } [Test] - public static void WideGamutRgbD50ToXyzD65() + public void WideGamutRgbD50ToXyzD65() { var wideGamutRgbConfig = new RgbConfiguration( WideGamutChromaticityR, @@ -163,7 +163,7 @@ public static void WideGamutRgbD50ToXyzD65() } [Test] - public static void WideGamutRgbD50ToXyzD50() + public void WideGamutRgbD50ToXyzD50() { var wideGamutRgbConfig = new RgbConfiguration( WideGamutChromaticityR, @@ -187,7 +187,7 @@ public static void WideGamutRgbD50ToXyzD50() [TestCase(Illuminant.D65, 0.312727, 0.329023)] [TestCase(Illuminant.D50, 0.345669, 0.358496)] [TestCase(Illuminant.E, 0.333333, 0.333333)] - public static void WhiteChromaticity(Illuminant illuminant, double expectedX, double expectedY) + public void WhiteChromaticity(Illuminant illuminant, double expectedX, double expectedY) { var xyzConfig = new XyzConfiguration(WhitePoint.From(illuminant, Observer.Standard2)); var chromaticity = xyzConfig.ChromaticityWhite; @@ -196,7 +196,7 @@ public static void WhiteChromaticity(Illuminant illuminant, double expectedX, do } [Test] - public static void ConvertWhite() + public void ConvertWhite() { Configuration Config(Illuminant illuminant) => new(RgbConfiguration.StandardRgb, new XyzConfiguration(WhitePoint.From(illuminant))); @@ -227,7 +227,7 @@ public static void ConvertWhite() } [Test] - public static void ConvertBlack() + public void ConvertBlack() { Configuration Config(Illuminant illuminant) => new(RgbConfiguration.StandardRgb, new XyzConfiguration(WhitePoint.From(illuminant))); @@ -267,7 +267,7 @@ public static void ConvertBlack() [TestCase(Illuminant.F2)] [TestCase(Illuminant.F7)] [TestCase(Illuminant.F11)] - public static void RgbWhitePointRoundTrip(Illuminant rgbIlluminant) + public void RgbWhitePointRoundTrip(Illuminant rgbIlluminant) { RgbConfiguration RgbConfig(WhitePoint whitePoint, RgbConfiguration baseConfig) { @@ -295,7 +295,7 @@ RgbConfiguration RgbConfig(WhitePoint whitePoint, RgbConfiguration baseConfig) [TestCase(Illuminant.F2)] [TestCase(Illuminant.F7)] [TestCase(Illuminant.F11)] - public static void Cam02WhitePointRoundTrip(Illuminant camIlluminant) + public void Cam02WhitePointRoundTrip(Illuminant camIlluminant) { CamConfiguration CamConfig(WhitePoint whitePoint, CamConfiguration baseConfig) { @@ -322,7 +322,7 @@ CamConfiguration CamConfig(WhitePoint whitePoint, CamConfiguration baseConfig) [TestCase(Illuminant.F2)] [TestCase(Illuminant.F7)] [TestCase(Illuminant.F11)] - public static void Cam16WhitePointRoundTrip(Illuminant camIlluminant) + public void Cam16WhitePointRoundTrip(Illuminant camIlluminant) { CamConfiguration CamConfig(WhitePoint whitePoint, CamConfiguration baseConfig) { diff --git a/Unicolour.Tests/ContrastTests.cs b/Unicolour.Tests/ContrastTests.cs index ae2f72b8..d659a517 100644 --- a/Unicolour.Tests/ContrastTests.cs +++ b/Unicolour.Tests/ContrastTests.cs @@ -3,10 +3,10 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class ContrastTests +public class ContrastTests { [Test] - public static void KnownContrasts() + public void KnownContrasts() { var black = ColourLimits.Rgb[ColourLimit.Black]; var white = ColourLimits.Rgb[ColourLimit.White]; @@ -23,7 +23,7 @@ public static void KnownContrasts() } [Test] - public static void BeyondMinRgbLuminance() + public void BeyondMinRgbLuminance() { var black = ColourLimits.Rgb[ColourLimit.Black]; var beyondMinRgb = Unicolour.FromRgb(-0.25, -0.5, -0.75); @@ -32,7 +32,7 @@ public static void BeyondMinRgbLuminance() } [Test] - public static void BeyondMaxRgbLuminance() + public void BeyondMaxRgbLuminance() { var white = ColourLimits.Rgb[ColourLimit.White]; var beyondMaxRgb = Unicolour.FromRgb(1.25, 1.5, 1.75); @@ -41,7 +41,7 @@ public static void BeyondMaxRgbLuminance() } [Test] - public static void NaNContrast() + public void NaNContrast() { var notNumber = Unicolour.FromRgb(double.NaN, double.NaN, double.NaN); var grey = Unicolour.FromRgb(0.5, 0.5, 0.5); diff --git a/Unicolour.Tests/ConversionTests.cs b/Unicolour.Tests/ConversionTests.cs deleted file mode 100644 index ededfa8d..00000000 --- a/Unicolour.Tests/ConversionTests.cs +++ /dev/null @@ -1,362 +0,0 @@ -namespace Wacton.Unicolour.Tests; - -using System; -using System.Drawing; -using NUnit.Framework; -using Wacton.Unicolour.Tests.Utils; - -public class ConversionTests -{ - private static readonly RgbConfiguration RgbConfig = RgbConfiguration.StandardRgb; - private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; - private static readonly CamConfiguration CamConfig = CamConfiguration.StandardRgb; - private const double IctcpScalar = 100; - private const double JzazbzScalar = 100; - - private const double DefaultTolerance = 0.00000000001; - private const double RbgTolerance = 0.00000005; - private const double HsbTolerance = 0.000000001; - private const double HslTolerance = 0.0000000001; - private const double XyzTolerance = 0.0000000005; - private const double LuvTolerance = 0.00000001; - private const double JzazbzTolerance = 0.00000005; - private const double OklabTolerance = 0.000005; - private const double Cam02Tolerance = 0.00005; - private const double Cam16Tolerance = 0.00005; - - // no point doing this test starting with Wikipedia's HSB / HSL values since they're rounded - [TestCaseSource(typeof(NamedColours), nameof(Utils.NamedColours.All))] - public void NamedColours(TestColour namedColour) => AssertRgbConversion(namedColour); - - [TestCaseSource(typeof(NamedColours), nameof(Utils.NamedColours.All))] - public void RgbNamedRoundTrip(TestColour namedColour) => AssertRgbRoundTrip(namedColour); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] - public void Rgb255RoundTrip(ColourTriplet triplet) => AssertRgb255RoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] - public void RgbRoundTrip(ColourTriplet triplet) => AssertRgbRoundTrip(triplet); - - [TestCaseSource(typeof(NamedColours), nameof(Utils.NamedColours.All))] - public void HsbNamedRoundTrip(TestColour namedColour) => AssertHsbRoundTrip(namedColour); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] - public void HsbRoundTrip(ColourTriplet triplet) => AssertHsbRoundTrip(triplet); - - [TestCaseSource(typeof(NamedColours), nameof(Utils.NamedColours.All))] - public void HslNamedRoundTrip(TestColour namedColour) => AssertHslRoundTrip(namedColour); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] - public void HslRoundTrip(ColourTriplet triplet) => AssertHslRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HwbTriplets))] - public void HwbRoundTrip(ColourTriplet triplet) => AssertHwbRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] - public void XyzRoundTrip(ColourTriplet triplet) => AssertXyzRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))] - public void XyyRoundTrip(ColourTriplet triplet) => AssertXyyRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] - public void LabRoundTrip(ColourTriplet triplet) => AssertLabRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchabTriplets))] - public void LchabRoundTrip(ColourTriplet triplet) => AssertLchabRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LuvTriplets))] - public void LuvRoundTrip(ColourTriplet triplet) => AssertLuvRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchuvTriplets))] - public void LchuvRoundTrip(ColourTriplet triplet) => AssertLchuvRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsluvTriplets))] - public void HsluvRoundTrip(ColourTriplet triplet) => AssertHsluvRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HpluvTriplets))] - public void HpluvRoundTrip(ColourTriplet triplet) => AssertHpluvRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.IctcpTriplets))] - public void IctcpRoundTrip(ColourTriplet triplet) => AssertIctcpRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.JzazbzTriplets))] - public void JzazbzRoundTrip(ColourTriplet triplet) => AssertJzazbzRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.JzczhzTriplets))] - public void JzczhzRoundTrip(ColourTriplet triplet) => AssertJzczhzRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.OklabTriplets))] - public void OklabRoundTrip(ColourTriplet triplet) => AssertOklabRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.OklchTriplets))] - public void OklchRoundTrip(ColourTriplet triplet) => AssertOklchRoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Cam02Triplets))] - public void Cam02RoundTrip(ColourTriplet triplet) => AssertCam02RoundTrip(triplet); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Cam16Triplets))] - public void Cam16RoundTrip(ColourTriplet triplet) => AssertCam16RoundTrip(triplet); - - private static void AssertRgbConversion(TestColour namedColour) - { - var systemColour = ColorTranslator.FromHtml(namedColour.Hex!); - var rgb = new Rgb(systemColour.R / 255.0, systemColour.G / 255.0, systemColour.B / 255.0, RgbConfig); - var hsb = Hsb.FromRgb(rgb); - var hsl = Hsl.FromHsb(hsb); - - var expectedRoundedHsb = namedColour.Hsb; - var expectedRoundedHsl = namedColour.Hsl; - - Assert.That(Math.Round(hsb.H), Is.EqualTo(expectedRoundedHsb!.First), namedColour.Name!); - Assert.That(Math.Round(hsb.S, 2), Is.EqualTo(expectedRoundedHsb.Second), namedColour.Name!); - Assert.That(Math.Round(hsb.B, 2), Is.EqualTo(expectedRoundedHsb.Third), namedColour.Name!); - - // within 0.02 because it seems like some of wikipedia's HSL values have questionable rounding... - Assert.That(Math.Round(hsl.H), Is.EqualTo(expectedRoundedHsl!.First), namedColour.Name!); - Assert.That(Math.Round(hsl.S, 2), Is.EqualTo(expectedRoundedHsl.Second).Within(0.02), namedColour.Name!); - Assert.That(Math.Round(hsl.L, 2), Is.EqualTo(expectedRoundedHsl.Third).Within(0.02), namedColour.Name!); - } - - private static void AssertRgbRoundTrip(TestColour namedColour) => AssertRgbRoundTrip(GetRgbTripletFromHex(namedColour.Hex!)); - private static void AssertRgb255RoundTrip(ColourTriplet triplet) => AssertRgbRoundTrip(GetNormalisedRgb255Triplet(triplet)); - private static void AssertRgbRoundTrip(ColourTriplet triplet) => AssertRgbRoundTrip(new Rgb(triplet.First, triplet.Second, triplet.Third, RgbConfig)); - private static void AssertRgbRoundTrip(Rgb original) - { - var viaHsb = Hsb.ToRgb(Hsb.FromRgb(original), RgbConfig); - AssertUtils.AssertTriplet(viaHsb.Triplet, original.Triplet, RbgTolerance); - AssertUtils.AssertTriplet(viaHsb.ConstrainedTriplet, original.ConstrainedTriplet, RbgTolerance); - AssertUtils.AssertTriplet(viaHsb.Linear.Triplet, original.Linear.Triplet, RbgTolerance); - AssertUtils.AssertTriplet(viaHsb.Linear.ConstrainedTriplet, original.Linear.ConstrainedTriplet, RbgTolerance); - AssertUtils.AssertTriplet(viaHsb.Byte255.Triplet, original.Byte255.Triplet, RbgTolerance); - AssertUtils.AssertTriplet(viaHsb.Byte255.ConstrainedTriplet, original.Byte255.ConstrainedTriplet, RbgTolerance); - - var viaXyz = Rgb.FromXyz(Rgb.ToXyz(original, RgbConfig, XyzConfig), RgbConfig, XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, original.Triplet, RbgTolerance); - AssertUtils.AssertTriplet(viaXyz.ConstrainedTriplet, original.ConstrainedTriplet, RbgTolerance); - AssertUtils.AssertTriplet(viaXyz.Linear.Triplet, original.Linear.Triplet, RbgTolerance); - AssertUtils.AssertTriplet(viaXyz.Linear.ConstrainedTriplet, original.Linear.ConstrainedTriplet, RbgTolerance); - AssertUtils.AssertTriplet(viaXyz.Byte255.Triplet, original.Byte255.Triplet, RbgTolerance); - AssertUtils.AssertTriplet(viaXyz.Byte255.ConstrainedTriplet, original.Byte255.ConstrainedTriplet, RbgTolerance); - } - - private static void AssertHsbRoundTrip(TestColour namedColour) => AssertHsbRoundTrip(namedColour.Hsb!); - private static void AssertHsbRoundTrip(ColourTriplet triplet) => AssertHsbRoundTrip(new Hsb(triplet.First, triplet.Second, triplet.Third)); - private static void AssertHsbRoundTrip(Hsb original) - { - var viaRgb = Hsb.FromRgb(Hsb.ToRgb(original, RgbConfig)); - AssertUtils.AssertTriplet(viaRgb.Triplet, original.Triplet, HsbTolerance); - - var viaHsl = Hsl.ToHsb(Hsl.FromHsb(original)); - AssertUtils.AssertTriplet(viaHsl.Triplet, original.Triplet, HsbTolerance); - - var viaHwb = Hwb.ToHsb(Hwb.FromHsb(original)); - AssertUtils.AssertTriplet(viaHwb.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertHslRoundTrip(TestColour namedColour) => AssertHslRoundTrip(namedColour.Hsl!); - private static void AssertHslRoundTrip(ColourTriplet triplet) => AssertHslRoundTrip(new Hsl(triplet.First, triplet.Second, triplet.Third)); - private static void AssertHslRoundTrip(Hsl original) - { - var viaHsb = Hsl.FromHsb(Hsl.ToHsb(original)); - AssertUtils.AssertTriplet(viaHsb.Triplet, original.Triplet, HslTolerance); - } - - private static void AssertHwbRoundTrip(ColourTriplet triplet) => AssertHwbRoundTrip(new Hwb(triplet.First, triplet.Second, triplet.Third)); - private static void AssertHwbRoundTrip(Hwb original) - { - // note: cannot test round trip of all HWB values as HWB <-> HSB is not 1:1 - // since when HWB W + B > 100%, it is the same as another HWB where W + B = 100% - // (e.g. W 100 B 50 == W 66.666 B 33.333) - // and HSB -> HWB will always produce HWB that results in W + B <= 100% - var scale = original.ConstrainedW + original.ConstrainedB; - var scaledHwb = new Hwb(original.H, original.ConstrainedW / scale, original.ConstrainedB / scale); - - var needsScaling = scale > 1.0; - if (needsScaling) - { - var hsbFromOriginal = Hwb.ToHsb(original); - var hsbFromScaled = Hwb.ToHsb(scaledHwb); - AssertUtils.AssertTriplet(hsbFromOriginal.Triplet, hsbFromScaled.Triplet, DefaultTolerance); - } - - var viaHsb = Hwb.FromHsb(Hwb.ToHsb(original)); - var expectedHwb = needsScaling ? scaledHwb.Triplet : original.Triplet; - AssertUtils.AssertTriplet(viaHsb.Triplet, expectedHwb, DefaultTolerance); - } - - private static void AssertXyzRoundTrip(ColourTriplet triplet) => AssertXyzRoundTrip(new Xyz(triplet.First, triplet.Second, triplet.Third)); - private static void AssertXyzRoundTrip(Xyz original) - { - var viaRgb = Rgb.ToXyz(Rgb.FromXyz(original, RgbConfig, XyzConfig), RgbConfig, XyzConfig); - AssertUtils.AssertTriplet(viaRgb.Triplet, original.Triplet, XyzTolerance); - - var viaXyy = Xyy.ToXyz(Xyy.FromXyz(original, XyzConfig)); - AssertUtils.AssertTriplet(viaXyy.Triplet, original.Triplet, XyzTolerance); - - var viaLab = Lab.ToXyz(Lab.FromXyz(original, XyzConfig), XyzConfig); - AssertUtils.AssertTriplet(viaLab.Triplet, original.Triplet, XyzTolerance); - - var viaLuv = Luv.ToXyz(Luv.FromXyz(original, XyzConfig), XyzConfig); - AssertUtils.AssertTriplet(viaLuv.Triplet, original.Triplet, XyzTolerance); - - var viaIctcp = Ictcp.ToXyz(Ictcp.FromXyz(original, IctcpScalar, XyzConfig), IctcpScalar, XyzConfig); - AssertUtils.AssertTriplet(viaIctcp.Triplet, viaIctcp.Triplet, XyzTolerance); - - var viaJzazbz = Jzazbz.ToXyz(Jzazbz.FromXyz(original, JzazbzScalar, XyzConfig), JzazbzScalar, XyzConfig); - AssertUtils.AssertTriplet(viaJzazbz.Triplet, viaJzazbz.Triplet, XyzTolerance); - - var viaOklab = Oklab.ToXyz(Oklab.FromXyz(original, XyzConfig), XyzConfig); - AssertUtils.AssertTriplet(viaOklab.Triplet, original.Triplet, XyzTolerance); - - // CAM02 -> XYZ often produces NaNs due to a negative number to a fractional power in the conversion process - var viaCam02 = Cam02.ToXyz(Cam02.FromXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); - AssertUtils.AssertTriplet(viaCam02.Triplet, viaCam02.IsNaN ? ViaCamWithNaN(viaCam02.Triplet) : original.Triplet, XyzTolerance); - - // CAM16 -> XYZ often produces NaNs due to a negative number to a fractional power in the conversion process - var viaCam16 = Cam16.ToXyz(Cam16.FromXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); - AssertUtils.AssertTriplet(viaCam16.Triplet, viaCam16.IsNaN ? ViaCamWithNaN(viaCam16.Triplet) : original.Triplet, XyzTolerance); - } - - private static void AssertXyyRoundTrip(ColourTriplet triplet) => AssertXyyRoundTrip(new Xyy(triplet.First, triplet.Second, triplet.Third)); - private static void AssertXyyRoundTrip(Xyy original) - { - var viaXyz = Xyy.FromXyz(Xyy.ToXyz(original), XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, original.Triplet, XyzTolerance); - } - - private static void AssertLabRoundTrip(ColourTriplet triplet) => AssertLabRoundTrip(new Lab(triplet.First, triplet.Second, triplet.Third)); - private static void AssertLabRoundTrip(Lab original) - { - var viaXyz = Lab.FromXyz(Lab.ToXyz(original, XyzConfig), XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, original.Triplet, DefaultTolerance); - - var viaLchab = Lchab.ToLab(Lchab.FromLab(original)); - AssertUtils.AssertTriplet(viaLchab.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertLchabRoundTrip(ColourTriplet triplet) => AssertLchabRoundTrip(new Lchab(triplet.First, triplet.Second, triplet.Third)); - private static void AssertLchabRoundTrip(Lchab original) - { - var viaLab = Lchab.FromLab(Lchab.ToLab(original)); - AssertUtils.AssertTriplet(viaLab.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertLuvRoundTrip(ColourTriplet triplet) => AssertLuvRoundTrip(new Luv(triplet.First, triplet.Second, triplet.Third)); - private static void AssertLuvRoundTrip(Luv original) - { - var viaXyz = Luv.FromXyz(Luv.ToXyz(original, XyzConfig), XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, original.Triplet, LuvTolerance); - - var viaLchuv = Lchuv.ToLuv(Lchuv.FromLuv(original)); - AssertUtils.AssertTriplet(viaLchuv.Triplet, original.Triplet, LuvTolerance); - } - - private static void AssertLchuvRoundTrip(ColourTriplet triplet) => AssertLchuvRoundTrip(new Lchuv(triplet.First, triplet.Second, triplet.Third)); - private static void AssertLchuvRoundTrip(Lchuv original) - { - var viaLuv = Lchuv.FromLuv(Lchuv.ToLuv(original)); - AssertUtils.AssertTriplet(viaLuv.Triplet, original.Triplet, DefaultTolerance); - - var viaHsluv = Hsluv.ToLchuv(Hsluv.FromLchuv(original)); - AssertUtils.AssertTriplet(viaHsluv.Triplet, original.Triplet, DefaultTolerance); - - var viaHpluv = Hpluv.ToLchuv(Hpluv.FromLchuv(original)); - AssertUtils.AssertTriplet(viaHpluv.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertHsluvRoundTrip(ColourTriplet triplet) => AssertHsluvRoundTrip(new Hsluv(triplet.First, triplet.Second, triplet.Third)); - private static void AssertHsluvRoundTrip(Hsluv original) - { - var viaLch = Hsluv.FromLchuv(Hsluv.ToLchuv(original)); - AssertUtils.AssertTriplet(viaLch.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertHpluvRoundTrip(ColourTriplet triplet) => AssertHpluvRoundTrip(new Hpluv(triplet.First, triplet.Second, triplet.Third)); - private static void AssertHpluvRoundTrip(Hpluv original) - { - var viaLch = Hpluv.FromLchuv(Hpluv.ToLchuv(original)); - AssertUtils.AssertTriplet(viaLch.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertIctcpRoundTrip(ColourTriplet triplet) => AssertIctcpRoundTrip(new Ictcp(triplet.First, triplet.Second, triplet.Third)); - private static void AssertIctcpRoundTrip(Ictcp original) - { - // Ictcp -> XYZ often produces NaNs due to a negative number to a fractional power in the conversion process - var viaXyz = Ictcp.FromXyz(Ictcp.ToXyz(original, IctcpScalar, XyzConfig), IctcpScalar, XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, viaXyz.IsNaN ? new(double.NaN, double.NaN, double.NaN) : original.Triplet, DefaultTolerance); - } - - private static void AssertJzazbzRoundTrip(ColourTriplet triplet) => AssertJzazbzRoundTrip(new Jzazbz(triplet.First, triplet.Second, triplet.Third)); - private static void AssertJzazbzRoundTrip(Jzazbz original) - { - // cannot test round trip of XYZ space as Jzazbz <-> XYZ is not 1:1, e.g. - // - when Jzazbz inputs produces negative XYZ values, which are clamped during XYZ -> Jzazbz - // - when Jzazbz negative inputs trigger a negative number to a fractional power, producing NaNs - // var viaXyz = Jzazbz.FromXyz(Jzazbz.ToXyz(original, xyzConfig), xyzConfig); - // AssertUtils.AssertTriplet(viaXyz.Triplet, viaXyz.IsNaN ? new(double.NaN, double.NaN, double.NaN) : original.Triplet, JzazbzTolerance); - - var viaJzczhz = Jzczhz.ToJzazbz(Jzczhz.FromJzazbz(original)); - AssertUtils.AssertTriplet(viaJzczhz.Triplet, original.Triplet, JzazbzTolerance); - } - - private static void AssertJzczhzRoundTrip(ColourTriplet triplet) => AssertJzczhzRoundTrip(new Jzczhz(triplet.First, triplet.Second, triplet.Third)); - private static void AssertJzczhzRoundTrip(Jzczhz original) - { - var viaJzazbz = Jzczhz.FromJzazbz(Jzczhz.ToJzazbz(original)); - AssertUtils.AssertTriplet(viaJzazbz.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertOklabRoundTrip(ColourTriplet triplet) => AssertOklabRoundTrip(new Oklab(triplet.First, triplet.Second, triplet.Third)); - private static void AssertOklabRoundTrip(Oklab original) - { - var viaXyz = Oklab.FromXyz(Oklab.ToXyz(original, XyzConfig), XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, original.Triplet, OklabTolerance); - - var viaOklch = Oklch.ToOklab(Oklch.FromOklab(original)); - AssertUtils.AssertTriplet(viaOklch.Triplet, original.Triplet, OklabTolerance); - } - - private static void AssertOklchRoundTrip(ColourTriplet triplet) => AssertOklchRoundTrip(new Oklch(triplet.First, triplet.Second, triplet.Third)); - private static void AssertOklchRoundTrip(Oklch original) - { - var viaOklab = Oklch.FromOklab(Oklch.ToOklab(original)); - AssertUtils.AssertTriplet(viaOklab.Triplet, original.Triplet, DefaultTolerance); - } - - private static void AssertCam02RoundTrip(ColourTriplet triplet) => AssertCam02RoundTrip(new Cam02(triplet.First, triplet.Second, triplet.Third, CamConfig)); - private static void AssertCam02RoundTrip(Cam02 original) - { - // CAM <-> XYZ often produces NaNs due to a negative number to a fractional power in the conversion process - var viaXyz = Cam02.FromXyz(Cam02.ToXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, viaXyz.IsNaN ? ViaCamWithNaN(viaXyz.Triplet) : original.Triplet, Cam02Tolerance); - } - - private static void AssertCam16RoundTrip(ColourTriplet triplet) => AssertCam16RoundTrip(new Cam16(triplet.First, triplet.Second, triplet.Third, CamConfig)); - private static void AssertCam16RoundTrip(Cam16 original) - { - // CAM <-> XYZ often produces NaNs due to a negative number to a fractional power in the conversion process - var viaXyz = Cam16.FromXyz(Cam16.ToXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); - AssertUtils.AssertTriplet(viaXyz.Triplet, viaXyz.IsNaN ? ViaCamWithNaN(viaXyz.Triplet) : original.Triplet, Cam16Tolerance); - } - - 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 ColourTriplet GetNormalisedRgb255Triplet(ColourTriplet triplet) - { - var (r255, g255, b255) = triplet; - return new(r255 / 255.0, g255 / 255.0, b255 / 255.0); - } - - // when NaNs occur during CAM <-> XYZ conversion - // if the NaN occurs during CAM -> XYZ: all value are NaN - // if the NaN occurs during XYZ -> CAM: J, H, Q have values and C, M, S are NaN - J is the first item of the triplet - private static ColourTriplet ViaCamWithNaN(ColourTriplet triplet) - { - var first = double.IsNaN(triplet.First) ? double.NaN : triplet.First; - return new(first, double.NaN, double.NaN); - } -} \ No newline at end of file diff --git a/Unicolour.Tests/CoordinateSpaceTests.cs b/Unicolour.Tests/CoordinateSpaceTests.cs index 093794e2..73120d24 100644 --- a/Unicolour.Tests/CoordinateSpaceTests.cs +++ b/Unicolour.Tests/CoordinateSpaceTests.cs @@ -212,14 +212,7 @@ private static void AssertTriplet(ColourTriplet actual, ColourTriplet expected) AssertUtils.AssertTriplet(actual, expected, 0.00001); } - private static Triplets AsTriplets(Rgb rgb) => new(rgb.Triplet, rgb.ConstrainedTriplet); - private static Triplets AsTriplets(Hsb hsb) => new(hsb.Triplet, hsb.ConstrainedTriplet); - private static Triplets AsTriplets(Hsl hsl) => new(hsl.Triplet, hsl.ConstrainedTriplet); - private static Triplets AsTriplets(Hwb hwb) => new(hwb.Triplet, hwb.ConstrainedTriplet); - private static Triplets AsTriplets(Lchab lchab) => new(lchab.Triplet, lchab.ConstrainedTriplet); - private static Triplets AsTriplets(Lchuv lchuv) => new(lchuv.Triplet, lchuv.ConstrainedTriplet); - private static Triplets AsTriplets(Jzczhz jzczhz) => new(jzczhz.Triplet, jzczhz.ConstrainedTriplet); - private static Triplets AsTriplets(Oklch oklch) => new(oklch.Triplet, oklch.ConstrainedTriplet); + private static Triplets AsTriplets(ColourRepresentation representation) => new(representation.Triplet, representation.ConstrainedTriplet); private record Triplets(ColourTriplet Unconstrained, ColourTriplet Constrained); } \ No newline at end of file diff --git a/Unicolour.Tests/DescriptionTests.cs b/Unicolour.Tests/DescriptionTests.cs index 6f36b276..89723d46 100644 --- a/Unicolour.Tests/DescriptionTests.cs +++ b/Unicolour.Tests/DescriptionTests.cs @@ -4,10 +4,10 @@ using System.Linq; using NUnit.Framework; -public static class DescriptionTests +public class DescriptionTests { [Test, Combinatorial] - public static void LightnessNotApplicable( + public void LightnessNotApplicable( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(double.NaN)] double l) @@ -22,7 +22,7 @@ public static void LightnessNotApplicable( } [Test, Combinatorial] - public static void LightnessBlack( + public void LightnessBlack( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(-0.000000000000001, 0)] double l) @@ -37,7 +37,7 @@ public static void LightnessBlack( } [Test, Combinatorial] - public static void LightnessShadow( + public void LightnessShadow( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(0.000000000000001, 0.199999999999999)] double l) @@ -50,7 +50,7 @@ public static void LightnessShadow( } [Test, Combinatorial] - public static void LightnessDark( + public void LightnessDark( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(0.2, 0.399999999999999)] double l) @@ -63,7 +63,7 @@ public static void LightnessDark( } [Test, Combinatorial] - public static void LightnessPure( + public void LightnessPure( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(0.4, 0.599999999999999)] double l) @@ -76,7 +76,7 @@ public static void LightnessPure( } [Test, Combinatorial] - public static void LightnessLight( + public void LightnessLight( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(0.6, 0.799999999999999)] double l) @@ -89,7 +89,7 @@ public static void LightnessLight( } [Test, Combinatorial] - public static void LightnessPale( + public void LightnessPale( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(0.8, 0.999999999999999)] double l) @@ -102,7 +102,7 @@ public static void LightnessPale( } [Test, Combinatorial] - public static void LightnessWhite( + public void LightnessWhite( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(1, 1.000000000000001)] double l) @@ -117,7 +117,7 @@ public static void LightnessWhite( } [Test, Combinatorial] - public static void SaturationNotApplicable( + public void SaturationNotApplicable( [Values(-1, 0, 180, 360, 361)] double h, [Values(double.NaN)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -132,7 +132,7 @@ public static void SaturationNotApplicable( } [Test, Combinatorial] - public static void SaturationGrey( + public void SaturationGrey( [Values(-1, 0, 180, 360, 361)] double h, [Values(-0.000000000000001, 0)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -145,7 +145,7 @@ public static void SaturationGrey( } [Test, Combinatorial] - public static void SaturationFaint( + public void SaturationFaint( [Values(-1, 0, 180, 360, 361)] double h, [Values(0.000000000000001, 0.199999999999999)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -158,7 +158,7 @@ public static void SaturationFaint( } [Test, Combinatorial] - public static void SaturationWeak( + public void SaturationWeak( [Values(-1, 0, 180, 360, 361)] double h, [Values(0.2, 0.399999999999999)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -171,7 +171,7 @@ public static void SaturationWeak( } [Test, Combinatorial] - public static void SaturationMild( + public void SaturationMild( [Values(-1, 0, 180, 360, 361)] double h, [Values(0.4, 0.599999999999999)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -184,7 +184,7 @@ public static void SaturationMild( } [Test, Combinatorial] - public static void SaturationStrong( + public void SaturationStrong( [Values(-1, 0, 180, 360, 361)] double h, [Values(0.6, 0.799999999999999)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -197,7 +197,7 @@ public static void SaturationStrong( } [Test, Combinatorial] - public static void SaturationVibrant( + public void SaturationVibrant( [Values(-1, 0, 180, 360, 361)] double h, [Values(0.8, 1, 1.000000000000001)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -210,7 +210,7 @@ public static void SaturationVibrant( } [Test, Combinatorial] - public static void HueNotApplicable( + public void HueNotApplicable( [Values(double.NaN)] double h, [Values(-0.000000000000001, 0, 0.5, 1, 1.000000000000001)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -225,7 +225,7 @@ public static void HueNotApplicable( } [Test, Combinatorial] - public static void HueRed( + public void HueRed( [Values(345, 0, 14.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -238,7 +238,7 @@ public static void HueRed( } [Test, Combinatorial] - public static void HueOrange( + public void HueOrange( [Values(15, 44.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -251,7 +251,7 @@ public static void HueOrange( } [Test, Combinatorial] - public static void HueYellow( + public void HueYellow( [Values(45, 74.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -264,7 +264,7 @@ public static void HueYellow( } [Test, Combinatorial] - public static void HueChartreuse( + public void HueChartreuse( [Values(75, 104.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -277,7 +277,7 @@ public static void HueChartreuse( } [Test, Combinatorial] - public static void HueGreen( + public void HueGreen( [Values(105, 134.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -290,7 +290,7 @@ public static void HueGreen( } [Test, Combinatorial] - public static void HueMint( + public void HueMint( [Values(135, 164.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -303,7 +303,7 @@ public static void HueMint( } [Test, Combinatorial] - public static void HueCyan( + public void HueCyan( [Values(165, 194.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -316,7 +316,7 @@ public static void HueCyan( } [Test, Combinatorial] - public static void HueAzure( + public void HueAzure( [Values(195, 224.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -329,7 +329,7 @@ public static void HueAzure( } [Test, Combinatorial] - public static void HueBlue( + public void HueBlue( [Values(225, 254.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -342,7 +342,7 @@ public static void HueBlue( } [Test, Combinatorial] - public static void HueViolet( + public void HueViolet( [Values(255, 284.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -355,7 +355,7 @@ public static void HueViolet( } [Test, Combinatorial] - public static void HueMagenta( + public void HueMagenta( [Values(285, 314.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) @@ -368,7 +368,7 @@ public static void HueMagenta( } [Test, Combinatorial] - public static void HueRose( + public void HueRose( [Values(315, 344.9999999999999)] double h, [Values(0.000000000000001, 0.5, 1)] double s, [Values(0.000000000000001, 0.5, 0.999999999999999)] double l) diff --git a/Unicolour.Tests/DifferenceTests.cs b/Unicolour.Tests/DifferenceTests.cs index 4a64f506..3bec9858 100644 --- a/Unicolour.Tests/DifferenceTests.cs +++ b/Unicolour.Tests/DifferenceTests.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class DifferenceTests +public class DifferenceTests { private const double Tolerance = 0.00005; private static Unicolour RandomColour() => RandomColours.UnicolourFromRgb(); @@ -17,7 +17,7 @@ public static class DifferenceTests [TestCase(ColourLimit.Green, ColourLimit.Red, 170.565257)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 258.682686)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 176.314083)] - public static void DeltaE76(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaE76(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -34,7 +34,7 @@ public static void DeltaE76(ColourLimit referenceColour, ColourLimit sampleColou [TestCase(ColourLimit.Green, ColourLimit.Red, 68.800069)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 100.577051)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 70.580743)] - public static void DeltaE94Graphics(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaE94Graphics(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -51,7 +51,7 @@ public static void DeltaE94Graphics(ColourLimit referenceColour, ColourLimit sam [TestCase(ColourLimit.Green, ColourLimit.Red, 64.530477)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 92.093048)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 71.003011)] - public static void DeltaE94Textiles(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaE94Textiles(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -69,7 +69,7 @@ public static void DeltaE94Textiles(ColourLimit referenceColour, ColourLimit sam [TestCase(ColourLimit.Green, ColourLimit.Red, 86.608245)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 83.185881)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 52.881375)] - public static void DeltaE00(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaE00(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -86,7 +86,7 @@ public static void DeltaE00(ColourLimit referenceColour, ColourLimit sampleColou [TestCase(ColourLimit.Green, ColourLimit.Red, 239.982435)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 234.838743)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 322.659678)] - public static void DeltaEItp(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaEItp(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -103,7 +103,7 @@ public static void DeltaEItp(ColourLimit referenceColour, ColourLimit sampleColo [TestCase(ColourLimit.Green, ColourLimit.Red, 0.195524)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 0.271571)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 0.281457)] - public static void DeltaEz(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaEz(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -119,7 +119,7 @@ public static void DeltaEz(ColourLimit referenceColour, ColourLimit sampleColour [TestCase(ColourLimit.Green, ColourLimit.Red, 201.534889)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 308.110214)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 196.009473)] - public static void DeltaEHyab(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaEHyab(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -135,7 +135,7 @@ public static void DeltaEHyab(ColourLimit referenceColour, ColourLimit sampleCol [TestCase(ColourLimit.Green, ColourLimit.Red, 0.519797)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 0.673409)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 0.537117)] - public static void DeltaEOk(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaEOk(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -151,7 +151,7 @@ public static void DeltaEOk(ColourLimit referenceColour, ColourLimit sampleColou [TestCase(ColourLimit.Green, ColourLimit.Red, 76.105436)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 92.321874)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 84.119853)] - public static void DeltaCam02(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaCam02(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -167,7 +167,7 @@ public static void DeltaCam02(ColourLimit referenceColour, ColourLimit sampleCol [TestCase(ColourLimit.Green, ColourLimit.Red, 22.523082)] [TestCase(ColourLimit.Blue, ColourLimit.Green, 24.596320)] [TestCase(ColourLimit.Red, ColourLimit.Blue, 20.689226)] - public static void DeltaCam16(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) + public void DeltaCam16(ColourLimit referenceColour, ColourLimit sampleColour, double expectedDelta) { var reference = ColourLimits.Rgb[referenceColour]; var sample = ColourLimits.Rgb[sampleColour]; @@ -176,58 +176,58 @@ public static void DeltaCam16(ColourLimit referenceColour, ColourLimit sampleCol } [Test] - public static void RandomSymmetricDeltaE76() => AssertRandomSymmetricDeltas(Comparison.DeltaE76); + public void RandomSymmetricDeltaE76() => AssertRandomSymmetricDeltas(Comparison.DeltaE76); [Test] - public static void RandomSymmetricDeltaE00() => AssertRandomSymmetricDeltas(Comparison.DeltaE00); + public void RandomSymmetricDeltaE00() => AssertRandomSymmetricDeltas(Comparison.DeltaE00); [Test] - public static void RandomSymmetricDeltaEItp() => AssertRandomSymmetricDeltas(Comparison.DeltaEItp); + public void RandomSymmetricDeltaEItp() => AssertRandomSymmetricDeltas(Comparison.DeltaEItp); [Test] - public static void RandomSymmetricDeltaEz() => AssertRandomSymmetricDeltas(Comparison.DeltaEz); + public void RandomSymmetricDeltaEz() => AssertRandomSymmetricDeltas(Comparison.DeltaEz); [Test] - public static void RandomSymmetricDeltaEHyab() => AssertRandomSymmetricDeltas(Comparison.DeltaEHyab); + public void RandomSymmetricDeltaEHyab() => AssertRandomSymmetricDeltas(Comparison.DeltaEHyab); [Test] - public static void RandomSymmetricDeltaEOk() => AssertRandomSymmetricDeltas(Comparison.DeltaEOk); + public void RandomSymmetricDeltaEOk() => AssertRandomSymmetricDeltas(Comparison.DeltaEOk); [Test] - public static void RandomSymmetricDeltaECam02() => AssertRandomSymmetricDeltas(Comparison.DeltaECam02); + public void RandomSymmetricDeltaECam02() => AssertRandomSymmetricDeltas(Comparison.DeltaECam02); [Test] - public static void RandomSymmetricDeltaECam16() => AssertRandomSymmetricDeltas(Comparison.DeltaECam16); + public void RandomSymmetricDeltaECam16() => AssertRandomSymmetricDeltas(Comparison.DeltaECam16); [Test] - public static void NotNumberDeltaE76() => AssertNotNumberDeltas(Comparison.DeltaE76, ColourSpace.Lab); + public void NotNumberDeltaE76() => AssertNotNumberDeltas(Comparison.DeltaE76, ColourSpace.Lab); [Test] - public static void NotNumberDeltaE94ForGraphics() => AssertNotNumberDeltas((reference, sample) => reference.DeltaE94(sample), ColourSpace.Lab); + public void NotNumberDeltaE94ForGraphics() => AssertNotNumberDeltas((reference, sample) => reference.DeltaE94(sample), ColourSpace.Lab); [Test] - public static void NotNumberDeltaE94ForTextiles() => AssertNotNumberDeltas((reference, sample) => reference.DeltaE94(sample, true), ColourSpace.Lab); + public void NotNumberDeltaE94ForTextiles() => AssertNotNumberDeltas((reference, sample) => reference.DeltaE94(sample, true), ColourSpace.Lab); [Test] - public static void NotNumberDeltaE00() => AssertNotNumberDeltas(Comparison.DeltaE00, ColourSpace.Lab); + public void NotNumberDeltaE00() => AssertNotNumberDeltas(Comparison.DeltaE00, ColourSpace.Lab); [Test] - public static void NotNumberDeltaEItp() => AssertNotNumberDeltas(Comparison.DeltaEItp, ColourSpace.Ictcp); + public void NotNumberDeltaEItp() => AssertNotNumberDeltas(Comparison.DeltaEItp, ColourSpace.Ictcp); [Test] - public static void NotNumberDeltaEz() => AssertNotNumberDeltas(Comparison.DeltaEz, ColourSpace.Jzczhz); + public void NotNumberDeltaEz() => AssertNotNumberDeltas(Comparison.DeltaEz, ColourSpace.Jzczhz); [Test] - public static void NotNumberDeltaEHyab() => AssertNotNumberDeltas(Comparison.DeltaEHyab, ColourSpace.Lab); + public void NotNumberDeltaEHyab() => AssertNotNumberDeltas(Comparison.DeltaEHyab, ColourSpace.Lab); [Test] - public static void NotNumberDeltaEOk() => AssertNotNumberDeltas(Comparison.DeltaEOk, ColourSpace.Oklab); + public void NotNumberDeltaEOk() => AssertNotNumberDeltas(Comparison.DeltaEOk, ColourSpace.Oklab); [Test] - public static void NotNumberDeltaECam02() => AssertNotNumberDeltas(Comparison.DeltaECam02, ColourSpace.Cam02); + public void NotNumberDeltaECam02() => AssertNotNumberDeltas(Comparison.DeltaECam02, ColourSpace.Cam02); [Test] - public static void NotNumberDeltaECam16() => AssertNotNumberDeltas(Comparison.DeltaECam16, ColourSpace.Cam16); + public void NotNumberDeltaECam16() => AssertNotNumberDeltas(Comparison.DeltaECam16, ColourSpace.Cam16); private static void AssertRandomSymmetricDeltas(Func getDelta) { diff --git a/Unicolour.Tests/DisplayableColourTests.cs b/Unicolour.Tests/DisplayableColourTests.cs index 0358715c..0bcd0948 100644 --- a/Unicolour.Tests/DisplayableColourTests.cs +++ b/Unicolour.Tests/DisplayableColourTests.cs @@ -2,13 +2,13 @@ using NUnit.Framework; -public static class DisplayableColourTests +public class DisplayableColourTests { [TestCase(0.0, 0.0, 0.0)] [TestCase(0.5, 0.5, 0.5)] [TestCase(1.0, 1.0, 1.0)] [TestCase(double.Epsilon, double.Epsilon, double.Epsilon)] - public static void DisplayableRgb(double r, double g, double b) + public void DisplayableRgb(double r, double g, double b) { var unicolour = Unicolour.FromRgb(r, g, b); Assert.That(unicolour.IsDisplayable, Is.True); @@ -35,7 +35,7 @@ public static void DisplayableRgb(double r, double g, double b) [TestCase(double.NaN, 0.5, 0.5)] [TestCase(0.5, double.NaN, 0.5)] [TestCase(0.5, 0.5, double.NaN)] - public static void UndisplayableRgb(double r, double g, double b) + public void UndisplayableRgb(double r, double g, double b) { var unicolour = Unicolour.FromRgb(r, g, b); Assert.That(unicolour.IsDisplayable, Is.False); diff --git a/Unicolour.Tests/EqualityTests.cs b/Unicolour.Tests/EqualityTests.cs index c799e7e9..cc056fb0 100644 --- a/Unicolour.Tests/EqualityTests.cs +++ b/Unicolour.Tests/EqualityTests.cs @@ -159,6 +159,14 @@ public void EqualCam16GivesEqualObjects() var unicolour2 = Unicolour.FromCam16(unicolour1.Cam16.Triplet.Tuple, unicolour1.Alpha.A); AssertUnicoloursEqual(unicolour1, unicolour2); } + + [Test] + public void EqualHctGivesEqualObjects() + { + var unicolour1 = RandomColours.UnicolourFromHct(); + var unicolour2 = Unicolour.FromHct(unicolour1.Hct.Triplet.Tuple, unicolour1.Alpha.A); + AssertUnicoloursEqual(unicolour1, unicolour2); + } [Test] public void NotEqualRgbGivesNotEqualObjects() @@ -331,6 +339,15 @@ public void NotEqualCam16GivesNotEqualObjects() AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Cam16.Triplet); } + [Test] + public void NotEqualHctGivesNotEqualObjects() + { + var unicolour1 = RandomColours.UnicolourFromHct(); + var differentTuple = GetDifferent(unicolour1.Hct.Triplet, 1.0).Tuple; + var unicolour2 = Unicolour.FromHct(differentTuple, unicolour1.Alpha.A + 0.1); + AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Hct.Triplet); + } + [Test] public void DifferentConfigurationObjects() { @@ -407,12 +424,19 @@ private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicol AssertEqual(unicolour1.Oklch, unicolour2.Oklch); AssertEqual(unicolour1.Cam02, unicolour2.Cam02); AssertEqual(unicolour1.Cam16, unicolour2.Cam16); + AssertEqual(unicolour1.Hct, unicolour2.Hct); AssertEqual(unicolour1.Alpha, unicolour2.Alpha); AssertEqual(unicolour1.Hex, unicolour2.Hex); AssertEqual(unicolour1.IsDisplayable, unicolour2.IsDisplayable); AssertEqual(unicolour1.RelativeLuminance, unicolour2.RelativeLuminance); AssertEqual(unicolour1.Description, unicolour2.Description); + AssertEqual(unicolour1.Temperature, unicolour2.Temperature); AssertEqual(unicolour1, unicolour2); + + if (unicolour1.Xyz.HctToXyzSearchResult != null) + { + AssertEqual(unicolour1.Xyz.HctToXyzSearchResult, unicolour2.Xyz.HctToXyzSearchResult); + } } private static void AssertUnicoloursNotEqual(Unicolour unicolour1, Unicolour unicolour2, Func getTriplet) diff --git a/Unicolour.Tests/ExtremeValuesTests.cs b/Unicolour.Tests/ExtremeValuesTests.cs index 3dc743f3..556ce989 100644 --- a/Unicolour.Tests/ExtremeValuesTests.cs +++ b/Unicolour.Tests/ExtremeValuesTests.cs @@ -3,10 +3,10 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class ExtremeValuesTests +public class ExtremeValuesTests { [Test, Combinatorial] - public static void Rgb( + public void Rgb( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -15,7 +15,7 @@ public static void Rgb( } [Test, Combinatorial] - public static void RgbConfiguration( + public void RgbConfiguration( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double chromaticity, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double whitePoint, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double linear) @@ -29,7 +29,7 @@ public static void RgbConfiguration( } [Test, Combinatorial] - public static void Hsb( + public void Hsb( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -38,7 +38,7 @@ public static void Hsb( } [Test, Combinatorial] - public static void Hsl( + public void Hsl( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -47,7 +47,7 @@ public static void Hsl( } [Test, Combinatorial] - public static void Hwb( + public void Hwb( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -56,7 +56,7 @@ public static void Hwb( } [Test, Combinatorial] - public static void Xyz( + public void Xyz( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -65,7 +65,7 @@ public static void Xyz( } [Test, Combinatorial] - public static void XyzConfiguration( + public void XyzConfiguration( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double whitePoint) { var xyzConfig = new XyzConfiguration(new WhitePoint(whitePoint, whitePoint, whitePoint)); @@ -74,7 +74,7 @@ public static void XyzConfiguration( } [Test, Combinatorial] - public static void Xyy( + public void Xyy( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -83,7 +83,7 @@ public static void Xyy( } [Test, Combinatorial] - public static void Lab( + public void Lab( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -92,7 +92,7 @@ public static void Lab( } [Test, Combinatorial] - public static void Lchab( + public void Lchab( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -101,7 +101,7 @@ public static void Lchab( } [Test, Combinatorial] - public static void Luv( + public void Luv( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -110,7 +110,7 @@ public static void Luv( } [Test, Combinatorial] - public static void Lchuv( + public void Lchuv( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -119,7 +119,7 @@ public static void Lchuv( } [Test, Combinatorial] - public static void Hsluv( + public void Hsluv( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -128,7 +128,7 @@ public static void Hsluv( } [Test, Combinatorial] - public static void Hpluv( + public void Hpluv( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -137,7 +137,7 @@ public static void Hpluv( } [Test, Combinatorial] - public static void Ictcp( + public void Ictcp( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -146,7 +146,7 @@ public static void Ictcp( } [Test, Combinatorial] - public static void IctcpConfiguration( + public void IctcpConfiguration( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double scalar) { var config = new Configuration(ictcpScalar: scalar); @@ -154,7 +154,7 @@ public static void IctcpConfiguration( } [Test, Combinatorial] - public static void Jzazbz( + public void Jzazbz( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -163,7 +163,7 @@ public static void Jzazbz( } [Test, Combinatorial] - public static void JzazbzConfiguration( + public void JzazbzConfiguration( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double scalar) { var config = new Configuration(jzazbzScalar: scalar); @@ -171,7 +171,7 @@ public static void JzazbzConfiguration( } [Test, Combinatorial] - public static void Jzczhz( + public void Jzczhz( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -180,7 +180,7 @@ public static void Jzczhz( } [Test, Combinatorial] - public static void Oklab( + public void Oklab( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -189,7 +189,7 @@ public static void Oklab( } [Test, Combinatorial] - public static void Oklch( + public void Oklch( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -198,7 +198,7 @@ public static void Oklch( } [Test, Combinatorial] - public static void Cam02( + public void Cam02( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -207,7 +207,7 @@ public static void Cam02( } [Test, Combinatorial] - public static void Cam16( + public void Cam16( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) @@ -216,7 +216,7 @@ public static void Cam16( } [Test, Combinatorial] - public static void Cam02Configuration( + public void Cam02Configuration( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double whitePoint, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double adaptingLuminance, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double backgroundLuminance) @@ -227,7 +227,7 @@ public static void Cam02Configuration( } [Test, Combinatorial] - public static void Cam16Configuration( + public void Cam16Configuration( [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double whitePoint, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double adaptingLuminance, [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double backgroundLuminance) @@ -236,4 +236,13 @@ public static void Cam16Configuration( var config = new Configuration(camConfiguration: camConfig); AssertUtils.AssertNoPropertyError(Unicolour.FromCam16(config, 62.47, 42.60, -1.36)); } + + [Test, Combinatorial] + public void Hct( + [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double first, + [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double second, + [Values(double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN)] double third) + { + AssertUtils.AssertNoPropertyError(Unicolour.FromHct(first, second, third)); + } } \ No newline at end of file diff --git a/Unicolour.Tests/GreyscaleTests.cs b/Unicolour.Tests/GreyscaleTests.cs index 089764b8..77519251 100644 --- a/Unicolour.Tests/GreyscaleTests.cs +++ b/Unicolour.Tests/GreyscaleTests.cs @@ -8,7 +8,7 @@ public class GreyscaleTests { private static readonly List NaNProducingSpaces = new() - { ColourSpace.Ictcp, ColourSpace.Jzazbz, ColourSpace.Jzczhz, ColourSpace.Cam02, ColourSpace.Cam16 }; + { ColourSpace.Ictcp, ColourSpace.Jzazbz, ColourSpace.Jzczhz, ColourSpace.Cam02, ColourSpace.Cam16, ColourSpace.Hct }; [TestCase(0.0, 0.0, 0.0, true)] [TestCase(-0.00000000001, 0.0, -0.0, true)] @@ -66,13 +66,14 @@ public class GreyscaleTests [TestCase(180.0, 0.5, 0.49999999999, false)] public void Hwb(double h, double w, double b, bool expected) => AssertUnicolour(Unicolour.FromHwb(h, w, b), expected); - // XYZ does not currently attempt to determine greyscale status from XYZ triplet, too much room for error - // subsequent colour spaces may later report to be greyscale based on their own triplet values - [TestCase(0.0, 0.0, 0.0)] - [TestCase(0.5, 0.5, 0.5)] - [TestCase(0.25, 0.5, 0.75)] - [TestCase(0.95047, 1.0, 1.08883)] - public void Xyz(double x, double y, double z) => AssertUnicolour(Unicolour.FromXyz(x, y, z), false); + [TestCase(0.0, 0.0, 0.0, true)] + [TestCase(0.0, -0.00000000001, 0.0, true)] + [TestCase(0.00000000001, 0.0, 0.00000000001, true)] + [TestCase(0.0, 0.00000000001, 0.0, false)] + [TestCase(0.5, 0.5, 0.5, false)] + [TestCase(0.25, 0.5, 0.75, false)] + [TestCase(0.95047, 1.0, 1.08883, false)] + public void Xyz(double x, double y, double z, bool expected) => AssertUnicolour(Unicolour.FromXyz(x, y, z), expected); [TestCase(0.0, 0.0, 0.0, true)] [TestCase(0.0, 0.0, -0.00000000001, true)] @@ -87,6 +88,12 @@ public class GreyscaleTests [TestCase(50.0, -0.00000000001, 0.0, false)] [TestCase(50.0, 0.0, 0.00000000001, false)] [TestCase(50.0, 0.0, -0.00000000001, false)] + [TestCase(0.0, 0.00000000001, -0.00000000001, true)] + [TestCase(-0.00000000001, 50, -50, true)] + [TestCase(0.00000000001, 50, -50, false)] + [TestCase(100.0, 50, -50, true)] + [TestCase(100.00000000001, 50, -50, true)] + [TestCase(99.99999999999, 50, -50, false)] public void Lab(double l, double a, double b, bool expected) => AssertUnicolour(Unicolour.FromLab(l, a, b), expected); [TestCase(50.0, 0.0, 180.0, true)] @@ -105,6 +112,12 @@ public class GreyscaleTests [TestCase(50.0, -0.00000000001, 0.0, false)] [TestCase(50.0, 0.0, 0.00000000001, false)] [TestCase(50.0, 0.0, -0.00000000001, false)] + [TestCase(0.0, 0.00000000001, -0.00000000001, true)] + [TestCase(-0.00000000001, 50, -50, true)] + [TestCase(0.00000000001, 50, -50, false)] + [TestCase(100.0, 50, -50, true)] + [TestCase(100.00000000001, 50, -50, true)] + [TestCase(99.99999999999, 50, -50, false)] public void Luv(double l, double u, double v, bool expected) => AssertUnicolour(Unicolour.FromLuv(l, u, v), expected); [TestCase(50.0, 0.0, 180.0, true)] @@ -145,6 +158,10 @@ public class GreyscaleTests [TestCase(0.5, -0.00000000001, 0.0, false)] [TestCase(0.5, 0.0, 0.00000000001, false)] [TestCase(0.5, 0.0, -0.00000000001, false)] + [TestCase(0.0, 0.1, -0.1, true)] + [TestCase(-0.00000000001, 0.1, -0.1, true)] + [TestCase(0.00000000001, 0.1, -0.1, false)] + [TestCase(1.0, 0.1, -0.1, false)] public void Ictcp(double i, double ct, double cp, bool expected) => AssertUnicolour(Unicolour.FromIctcp(i, ct, cp), expected); [TestCase(0.5, 0.0, 0.0, true)] @@ -152,6 +169,10 @@ public class GreyscaleTests [TestCase(0.5, -0.00000000001, 0.0, false)] [TestCase(0.5, 0.0, 0.00000000001, false)] [TestCase(0.5, 0.0, -0.00000000001, false)] + [TestCase(0.0, 0.1, -0.1, true)] + [TestCase(-0.00000000001, 0.1, -0.1, true)] + [TestCase(0.00000000001, 0.1, -0.1, false)] + [TestCase(1.0, 0.1, -0.1, false)] public void Jzazbz(double jz, double az, double bz, bool expected) => AssertUnicolour(Unicolour.FromJzazbz(jz, az, bz), expected); [TestCase(0.5, 0.0, 180.0, true)] @@ -159,10 +180,8 @@ public class GreyscaleTests [TestCase(0.5, 0.00000000001, 180.0, false)] [TestCase(0.0, 0.1, 180.0, true)] [TestCase(-0.00000000001, 0.1, 180.0, true)] - [TestCase(0.00000000001, 0.05, 180.0, false)] - [TestCase(1.0, 0.1, 180.0, true)] - [TestCase(1.00000000001, 0.1, 180.0, true)] - [TestCase(0.99999999999, 0.1, 180.0, false)] + [TestCase(0.00000000001, 0.1, 180.0, false)] + [TestCase(1.0, 0.1, 180.0, false)] public void Jzczhz(double jz, double cz, double hz, bool expected) => AssertUnicolour(Unicolour.FromJzczhz(jz, cz, hz), expected); [TestCase(0.5, 0.0, 0.0, true)] @@ -170,6 +189,12 @@ public class GreyscaleTests [TestCase(0.5, -0.00000000001, 0.0, false)] [TestCase(0.5, 0.0, 0.00000000001, false)] [TestCase(0.5, 0.0, -0.00000000001, false)] + [TestCase(0.0, 0.00000000001, -0.00000000001, true)] + [TestCase(-0.00000000001, 0.1, -0.1, true)] + [TestCase(0.00000000001, 0.1, -0.1, false)] + [TestCase(1.0, 0.1, -0.1, true)] + [TestCase(1.00000000001, 0.1, -0.1, true)] + [TestCase(0.99999999999, 0.1, -0.1, false)] public void Oklab(double l, double a, double b, bool expected) => AssertUnicolour(Unicolour.FromOklab(l, a, b), expected); [TestCase(0.5, 0.0, 180.0, true)] @@ -196,6 +221,17 @@ public class GreyscaleTests [TestCase(50.0, 0.0, 0.00000000001, false)] [TestCase(50.0, 0.0, -0.00000000001, false)] public void Cam16(double j, double a, double b, bool expected) => AssertUnicolour(Unicolour.FromCam16(j, a, b), expected); + + [TestCase(180.0, 0.0, 50, true)] + [TestCase(180.0, -0.00000000001, 50, true)] + [TestCase(180.0, 0.00000000001, 50, false)] + [TestCase(180.0, 50, 0.0, true)] + [TestCase(180.0, 50, -0.00000000001, true)] + [TestCase(180.0, 50, 0.00000000001, false)] + [TestCase(180.0, 50, 100.0, true)] + [TestCase(180.0, 50, 100.00000000001, true)] + [TestCase(180.0, 50, 99.99999999999, false)] + public void Hct(double h, double c, double t, bool expected) => AssertUnicolour(Unicolour.FromHct(h, c, t), expected); private static void AssertUnicolour(Unicolour unicolour, bool shouldBeGreyscale) { diff --git a/Unicolour.Tests/HueOverrideTests.cs b/Unicolour.Tests/HueOverrideTests.cs index 4168ed0f..0f96a0dd 100644 --- a/Unicolour.Tests/HueOverrideTests.cs +++ b/Unicolour.Tests/HueOverrideTests.cs @@ -3,11 +3,10 @@ using System; using NUnit.Framework; -public static class HueOverrideTests +public class HueOverrideTests { - [Test] - public static void NoHue() + public void NoHue() { var triplet = new ColourTriplet(7.7, 8.8, 9.9, null); Assert.Throws(() => triplet.HueValue()); @@ -15,7 +14,7 @@ public static void NoHue() } [Test] - public static void FirstHue() + public void FirstHue() { var triplet = new ColourTriplet(7.7, 8.8, 9.9, 0); Assert.That(triplet.HueValue(), Is.EqualTo(7.7)); @@ -25,7 +24,7 @@ public static void FirstHue() } [Test] - public static void SecondHue() + public void SecondHue() { var triplet = new ColourTriplet(7.7, 8.8, 9.9, 1); Assert.Throws(() => triplet.HueValue()); @@ -33,7 +32,7 @@ public static void SecondHue() } [Test] - public static void ThirdHue() + public void ThirdHue() { var triplet = new ColourTriplet(7.7, 8.8, 9.9, 2); Assert.That(triplet.HueValue(), Is.EqualTo(9.9)); diff --git a/Unicolour.Tests/HuedTests.cs b/Unicolour.Tests/HuedTests.cs index ae14deaa..3590c35b 100644 --- a/Unicolour.Tests/HuedTests.cs +++ b/Unicolour.Tests/HuedTests.cs @@ -33,6 +33,9 @@ public class HuedTests [Test] public void Oklch() => AssertUnicolour(Unicolour.FromOklch(0, 0, 180), new List()); + + [Test] + public void Hct() => AssertUnicolour(Unicolour.FromHct(180, 0, 0), new List()); private static void AssertUnicolour(Unicolour unicolour, List adjacentHuedSpaces) { diff --git a/Unicolour.Tests/InterpolateGreyscaleHctTests.cs b/Unicolour.Tests/InterpolateGreyscaleHctTests.cs new file mode 100644 index 00000000..2c50d760 --- /dev/null +++ b/Unicolour.Tests/InterpolateGreyscaleHctTests.cs @@ -0,0 +1,124 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +// note: HCT is a composite of LAB & CAM16, therefore there is no obvious cartesian/hueless space to compare against +// so using RGB to generate non-HCT greyscales +// ---------- +// greyscale RGB has no hue - shouldn't assume to start at red (0 degrees) when interpolating +// greyscale HCT has a hue so it should be used (it just can't be seen until there is some chroma & tone) +public class InterpolateGreyscaleHctTests +{ + private const double RgbWhiteChroma = 2.8690369750685738; + + [Test] + public void GreyscaleStartColour() + { + var rgbBlack = Unicolour.FromRgb255(0, 0, 0); + var rgbWhite = Unicolour.FromRgb255(255, 255, 255); + var hctBlack = Unicolour.FromHct(180, 100, 0); // no tone = black + var hctWhite = Unicolour.FromHct(180, 100, 100); // full tone = white + + var green = Unicolour.FromHct(120, 100, 50); + var fromRgbBlack = rgbBlack.InterpolateHct(green, 0.5); + var fromRgbWhite = rgbWhite.InterpolateHct(green, 0.5); + var fromHctBlack = hctBlack.InterpolateHct(green, 0.5); + var fromHctWhite = hctWhite.InterpolateHct(green, 0.5); + + // no obvious way to create known HCT value when starting from non-HCT space + // so need to calculate what the expected Chroma will be for RGB-white + // however, not really a problem as this test focuses on hue + const double expectedFromRgbWhiteChroma = (RgbWhiteChroma + 100) * 0.5; + + // greyscale interpolates differently depending on the initial colour space + AssertTriplet(fromRgbBlack.Hct.Triplet, new(120, 50, 25)); + AssertTriplet(fromRgbWhite.Hct.Triplet, new(120, expectedFromRgbWhiteChroma, 75)); + AssertTriplet(fromHctBlack.Hct.Triplet, new(150, 100, 25)); + AssertTriplet(fromHctWhite.Hct.Triplet, new(150, 100, 75)); + } + + [Test] + public void GreyscaleEndColour() + { + var rgbBlack = Unicolour.FromRgb255(0, 0, 0); + var rgbWhite = Unicolour.FromRgb255(255, 255, 255); + var hctBlack = Unicolour.FromHct(180, 100, 0); // no tone = black + var hctWhite = Unicolour.FromHct(180, 100, 100); // full tone = white + + var blue = Unicolour.FromHct(240, 100, 50); + var toRgbBlack = blue.InterpolateHct(rgbBlack, 0.5); + var toRgbWhite = blue.InterpolateHct(rgbWhite, 0.5); + var toHctBlack = blue.InterpolateHct(hctBlack, 0.5); + var toHctWhite = blue.InterpolateHct(hctWhite, 0.5); + + // no obvious way to create known HCT value when starting from non-HCT space + // so need to calculate what the expected Chroma will be for RGB-white + // however, not really a problem as this test focuses on hue + const double expectedFromRgbWhiteChroma = (RgbWhiteChroma + 100) * 0.5; + + // greyscale interpolates differently depending on the initial colour space + AssertTriplet(toRgbBlack.Hct.Triplet, new(240, 50, 25)); + AssertTriplet(toRgbWhite.Hct.Triplet, new(240, expectedFromRgbWhiteChroma, 75)); + AssertTriplet(toHctBlack.Hct.Triplet, new(210, 100, 25)); + AssertTriplet(toHctWhite.Hct.Triplet, new(210, 100, 75)); + } + + // note: due to how HCT is derived + // there is no obvious way of mapping non-HCT greyscales to specific HCT hues + // so this just tests what it can + [Test] + public void GreyscaleBothRgbColours() + { + var black = Unicolour.FromRgb(0, 0, 0); + var white = Unicolour.FromRgb(1, 1, 1); + var grey = Unicolour.FromRgb(0.5, 0.5, 0.5); + + var blackToWhite = black.InterpolateHct(white, 0.5); + var blackToGrey = black.InterpolateHct(grey, 0.5); + var whiteToGrey = white.InterpolateHct(grey, 0.5); + + // colours created from RGB therefore hue does not change + // (except for HCT for RGB-black, which converts to a different hue than other greyscales) + Assert.That(black.Hct.H, Is.EqualTo(0)); + Assert.That(blackToWhite.Hct.H, Is.EqualTo(blackToGrey.Hct.H).Within(0.001)); + Assert.That(whiteToGrey.Hct.H, Is.EqualTo(white.Hct.H).Within(0.001)); + Assert.That(whiteToGrey.Hct.H, Is.EqualTo(grey.Hct.H).Within(0.001)); + } + + // note: due to how HCT is derived + // there is no obvious way of mapping non-HCT greyscales to specific HCT hues + // so this just tests what it can + [Test] + public void GreyscaleBothHctColours() + { + var black = Unicolour.FromHct(0, 0, 0); + var white = Unicolour.FromHct(300, 0, 100); + var grey = Unicolour.FromHct(100, 0, 50); + + var blackToWhite = black.InterpolateHct(white, 0.5); + var blackToGrey = black.InterpolateHct(grey, 0.5); + var whiteToGrey = white.InterpolateHct(grey, 0.5); + + AssertGrey(blackToWhite.Rgb); + AssertGrey(blackToGrey.Rgb); + AssertGrey(whiteToGrey.Rgb); + + // colours created from HCT therefore hue changes + AssertTriplet(blackToWhite.Hct.Triplet, new(330, 0, 50)); + AssertTriplet(blackToGrey.Hct.Triplet, new(50, 0, 25)); + AssertTriplet(whiteToGrey.Hct.Triplet, new(20, 0, 75)); + } + + private static void AssertTriplet(ColourTriplet actual, ColourTriplet expected) + { + AssertUtils.AssertTriplet(actual, expected, AssertUtils.InterpolationTolerance); + } + + private static void AssertGrey(Rgb rgb) + { + Assert.That(rgb.R, Is.EqualTo(rgb.G).Within(0.05)); + Assert.That(rgb.G, Is.EqualTo(rgb.B).Within(0.05)); + Assert.That(rgb.B, Is.EqualTo(rgb.R).Within(0.05)); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/InterpolateGreyscaleHpluvTests.cs b/Unicolour.Tests/InterpolateGreyscaleHpluvTests.cs index ad5fdf84..c007ad3c 100644 --- a/Unicolour.Tests/InterpolateGreyscaleHpluvTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleHpluvTests.cs @@ -25,7 +25,6 @@ public void GreyscaleStartColour() var fromHpluvWhite = hpluvWhite.InterpolateHpluv(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(fromRgbBlack.Hpluv.Triplet, new(120, 50, 25)); AssertTriplet(fromRgbWhite.Hpluv.Triplet, new(120, 50, 75)); AssertTriplet(fromHpluvBlack.Hpluv.Triplet, new(150, 100, 25)); @@ -47,7 +46,6 @@ public void GreyscaleEndColour() var toHpluvWhite = blue.InterpolateHpluv(hpluvWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(toRgbBlack.Hpluv.Triplet, new(240, 50, 25)); AssertTriplet(toRgbWhite.Hpluv.Triplet, new(240, 50, 75)); AssertTriplet(toHpluvBlack.Hpluv.Triplet, new(210, 100, 25)); @@ -71,11 +69,8 @@ public void GreyscaleBothLuvColours() // colours created from LUV therefore hue does not change AssertTriplet(blackToWhite.Hpluv.Triplet, new(0, 0, 50)); - Assert.That(blackToWhite.Hpluv.UseAsHued, Is.False); AssertTriplet(blackToGrey.Hpluv.Triplet, new(0, 0, 25)); - Assert.That(blackToGrey.Hpluv.UseAsHued, Is.False); AssertTriplet(whiteToGrey.Hpluv.Triplet, new(0, 0, 75)); - Assert.That(whiteToGrey.Hpluv.UseAsHued, Is.False); } [Test] diff --git a/Unicolour.Tests/InterpolateGreyscaleHsbTests.cs b/Unicolour.Tests/InterpolateGreyscaleHsbTests.cs index 631e9c99..1a1ac2e8 100644 --- a/Unicolour.Tests/InterpolateGreyscaleHsbTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleHsbTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromHsbWhite = hsbWhite.InterpolateHsb(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(fromRgbBlack.Hsb.Triplet, new(120, 0.5, 0.5)); AssertTriplet(fromRgbWhite.Hsb.Triplet, new(120, 0.5, 1)); AssertTriplet(fromHsbBlack.Hsb.Triplet, new(150, 1, 0.5)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toHsbWhite = blue.InterpolateHsb(hsbWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(toRgbBlack.Hsb.Triplet, new(240, 0.5, 0.5)); AssertTriplet(toRgbWhite.Hsb.Triplet, new(240, 0.5, 1)); AssertTriplet(toHsbBlack.Hsb.Triplet, new(210, 1, 0.5)); diff --git a/Unicolour.Tests/InterpolateGreyscaleHslTests.cs b/Unicolour.Tests/InterpolateGreyscaleHslTests.cs index c9d3b4e7..19754b2c 100644 --- a/Unicolour.Tests/InterpolateGreyscaleHslTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleHslTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromHslWhite = hslWhite.InterpolateHsl(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(fromRgbBlack.Hsl.Triplet, new(120, 0.5, 0.25)); AssertTriplet(fromRgbWhite.Hsl.Triplet, new(120, 0.5, 0.75)); AssertTriplet(fromHslBlack.Hsl.Triplet, new(150, 1, 0.25)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toHslWhite = blue.InterpolateHsl(hslWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(toRgbBlack.Hsl.Triplet, new(240, 0.5, 0.25)); AssertTriplet(toRgbWhite.Hsl.Triplet, new(240, 0.5, 0.75)); AssertTriplet(toHslBlack.Hsl.Triplet, new(210, 1, 0.25)); diff --git a/Unicolour.Tests/InterpolateGreyscaleHsluvTests.cs b/Unicolour.Tests/InterpolateGreyscaleHsluvTests.cs index 2d34ce95..c177e398 100644 --- a/Unicolour.Tests/InterpolateGreyscaleHsluvTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleHsluvTests.cs @@ -25,7 +25,6 @@ public void GreyscaleStartColour() var fromHsluvWhite = hsluvWhite.InterpolateHsluv(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(fromRgbBlack.Hsluv.Triplet, new(120, 50, 25)); AssertTriplet(fromRgbWhite.Hsluv.Triplet, new(120, 50, 75)); AssertTriplet(fromHsluvBlack.Hsluv.Triplet, new(150, 100, 25)); @@ -47,7 +46,6 @@ public void GreyscaleEndColour() var toHsluvWhite = blue.InterpolateHsluv(hsluvWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(toRgbBlack.Hsluv.Triplet, new(240, 50, 25)); AssertTriplet(toRgbWhite.Hsluv.Triplet, new(240, 50, 75)); AssertTriplet(toHsluvBlack.Hsluv.Triplet, new(210, 100, 25)); @@ -71,11 +69,8 @@ public void GreyscaleBothLuvColours() // colours created from LUV therefore hue does not change AssertTriplet(blackToWhite.Hsluv.Triplet, new(0, 0, 50)); - Assert.That(blackToWhite.Hsluv.UseAsHued, Is.False); AssertTriplet(blackToGrey.Hsluv.Triplet, new(0, 0, 25)); - Assert.That(blackToGrey.Hsluv.UseAsHued, Is.False); AssertTriplet(whiteToGrey.Hsluv.Triplet, new(0, 0, 75)); - Assert.That(whiteToGrey.Hsluv.UseAsHued, Is.False); } [Test] diff --git a/Unicolour.Tests/InterpolateGreyscaleHwbTests.cs b/Unicolour.Tests/InterpolateGreyscaleHwbTests.cs index 7824b795..d6f4d05f 100644 --- a/Unicolour.Tests/InterpolateGreyscaleHwbTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleHwbTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromHwbWhite = hwbWhite.InterpolateHwb(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(fromRgbBlack.Hwb.Triplet, new(120, 0, 0.5)); AssertTriplet(fromRgbWhite.Hwb.Triplet, new(120, 0.5, 0)); AssertTriplet(fromHwbBlack.Hwb.Triplet, new(150, 0, 0.5)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toHwbWhite = blue.InterpolateHwb(hwbWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since greyscale RGB assumes saturation of 0 (but saturation can be any value) AssertTriplet(toRgbBlack.Hwb.Triplet, new(240, 0, 0.5)); AssertTriplet(toRgbWhite.Hwb.Triplet, new(240, 0.5, 0)); AssertTriplet(toHwbBlack.Hwb.Triplet, new(210, 0, 0.5)); diff --git a/Unicolour.Tests/InterpolateGreyscaleJzczhzTests.cs b/Unicolour.Tests/InterpolateGreyscaleJzczhzTests.cs index 5a11b32c..6106865e 100644 --- a/Unicolour.Tests/InterpolateGreyscaleJzczhzTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleJzczhzTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromJchWhite = jchWhite.InterpolateJzczhz(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since Jzazbz black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(fromJabBlack.Jzczhz.Triplet, new(0.25, 0.25, 120)); AssertTriplet(fromJabWhite.Jzczhz.Triplet, new(0.75, 0.25, 120)); AssertTriplet(fromJchBlack.Jzczhz.Triplet, new(0.25, 0.5, 150)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toJzczhzWhite = blue.InterpolateJzczhz(jcWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since Jzazbz black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(toJzazbzBlack.Jzczhz.Triplet, new(0.25, 0.25, 240)); AssertTriplet(toJzazbzWhite.Jzczhz.Triplet, new(0.75, 0.25, 240)); AssertTriplet(toJzczhzBlack.Jzczhz.Triplet, new(0.25, 0.5, 210)); diff --git a/Unicolour.Tests/InterpolateGreyscaleLchabTests.cs b/Unicolour.Tests/InterpolateGreyscaleLchabTests.cs index 52eb4b4e..4ca38fe3 100644 --- a/Unicolour.Tests/InterpolateGreyscaleLchabTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleLchabTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromLchabWhite = lchabWhite.InterpolateLchab(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since LAB black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(fromLabBlack.Lchab.Triplet, new(25, 50, 120)); AssertTriplet(fromLabWhite.Lchab.Triplet, new(75, 50, 120)); AssertTriplet(fromLchabBlack.Lchab.Triplet, new(25, 100, 150)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toLchabWhite = blue.InterpolateLchab(lchabWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since LAB black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(toLabBlack.Lchab.Triplet, new(25, 50, 240)); AssertTriplet(toLabWhite.Lchab.Triplet, new(75, 50, 240)); AssertTriplet(toLchabBlack.Lchab.Triplet, new(25, 100, 210)); diff --git a/Unicolour.Tests/InterpolateGreyscaleLchuvTests.cs b/Unicolour.Tests/InterpolateGreyscaleLchuvTests.cs index 8bb1ffdf..43d9d85e 100644 --- a/Unicolour.Tests/InterpolateGreyscaleLchuvTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleLchuvTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromLchuvWhite = lchuvWhite.InterpolateLchuv(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since LUV black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(fromLuvBlack.Lchuv.Triplet, new(25, 50, 120)); AssertTriplet(fromLuvWhite.Lchuv.Triplet, new(75, 50, 120)); AssertTriplet(fromLchuvBlack.Lchuv.Triplet, new(25, 100, 150)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toLchuvWhite = blue.InterpolateLchuv(lchuvWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since LUV black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(toLuvBlack.Lchuv.Triplet, new(25, 50, 240)); AssertTriplet(toLuvWhite.Lchuv.Triplet, new(75, 50, 240)); AssertTriplet(toLchuvBlack.Lchuv.Triplet, new(25, 100, 210)); diff --git a/Unicolour.Tests/InterpolateGreyscaleOklchTests.cs b/Unicolour.Tests/InterpolateGreyscaleOklchTests.cs index ffa951e3..c0e8581d 100644 --- a/Unicolour.Tests/InterpolateGreyscaleOklchTests.cs +++ b/Unicolour.Tests/InterpolateGreyscaleOklchTests.cs @@ -22,7 +22,6 @@ public void GreyscaleStartColour() var fromOklchWhite = oklchWhite.InterpolateOklch(green, 0.5); // greyscale interpolates differently depending on the initial colour space - // since Oklab black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(fromOklabBlack.Oklch.Triplet, new(0.25, 0.25, 120)); AssertTriplet(fromOklabWhite.Oklch.Triplet, new(0.75, 0.25, 120)); AssertTriplet(fromOklchBlack.Oklch.Triplet, new(0.25, 0.5, 150)); @@ -44,7 +43,6 @@ public void GreyscaleEndColour() var toOklchWhite = blue.InterpolateOklch(oklchWhite, 0.5); // greyscale interpolates differently depending on the initial colour space - // since Oklab black/white assumes chroma of 0 (but chroma can be any value) AssertTriplet(toOklabBlack.Oklch.Triplet, new(0.25, 0.25, 240)); AssertTriplet(toOklabWhite.Oklch.Triplet, new(0.75, 0.25, 240)); AssertTriplet(toOklchBlack.Oklch.Triplet, new(0.25, 0.5, 210)); diff --git a/Unicolour.Tests/InterpolateHctTests.cs b/Unicolour.Tests/InterpolateHctTests.cs new file mode 100644 index 00000000..5f2f03c7 --- /dev/null +++ b/Unicolour.Tests/InterpolateHctTests.cs @@ -0,0 +1,124 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class InterpolateHctTests +{ + [Test] + public void SameColour() + { + var unicolour1 = Unicolour.FromHct(180, 30, 75, 0.5); + var unicolour2 = Unicolour.FromHct(180, 30, 75, 0.5); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.25); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.75); + var interpolated3 = unicolour1.InterpolateHct(unicolour2, 0.75); + var interpolated4 = unicolour2.InterpolateHct(unicolour1, 0.25); + + AssertInterpolated(interpolated1, (180, 30, 75, 0.5)); + AssertInterpolated(interpolated2, (180, 30, 75, 0.5)); + AssertInterpolated(interpolated3, (180, 30, 75, 0.5)); + AssertInterpolated(interpolated4, (180, 30, 75, 0.5)); + } + + [Test] + public void Equidistant() + { + var unicolour1 = Unicolour.FromHct(0, 0, 0, 0); + var unicolour2 = Unicolour.FromHct(180, 120, 100); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.5); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.5); + + AssertInterpolated(interpolated1, (90, 60, 50, 0.5)); + AssertInterpolated(interpolated2, (90, 60, 50, 0.5)); + } + + [Test] + public void EquidistantViaZero() + { + var unicolour1 = Unicolour.FromHct(0, 0, 0, 0); + var unicolour2 = Unicolour.FromHct(340, 60, 80, 0.2); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.5); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.5); + + AssertInterpolated(interpolated1, (350, 30, 40, 0.1)); + AssertInterpolated(interpolated2, (350, 30, 40, 0.1)); + } + + [Test] + public void CloserToEndColour() + { + var unicolour1 = Unicolour.FromHct(0, 120, 0); + var unicolour2 = Unicolour.FromHct(180, 0, 100, 0.5); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.75); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.75); + + AssertInterpolated(interpolated1, (135, 30, 75, 0.625)); + AssertInterpolated(interpolated2, (45, 90, 25, 0.875)); + } + + [Test] + public void CloserToEndColourViaZero() + { + var unicolour1 = Unicolour.FromHct(300, 120, 0); + var unicolour2 = Unicolour.FromHct(60, 0, 100, 0.5); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.75); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.75); + + AssertInterpolated(interpolated1, (30, 30, 75, 0.625)); + AssertInterpolated(interpolated2, (330, 90, 25, 0.875)); + } + + [Test] + public void CloserToStartColour() + { + var unicolour1 = Unicolour.FromHct(0, 120, 0); + var unicolour2 = Unicolour.FromHct(180, 0, 100, 0.5); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.25); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.25); + + AssertInterpolated(interpolated1, (45, 90, 25, 0.875)); + AssertInterpolated(interpolated2, (135, 30, 75, 0.625)); + } + + [Test] + public void CloserToStartColourViaZero() + { + var unicolour1 = Unicolour.FromHct(300, 120, 0); + var unicolour2 = Unicolour.FromHct(60, 0, 100, 0.5); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 0.25); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 0.25); + + AssertInterpolated(interpolated1, (330, 90, 25, 0.875)); + AssertInterpolated(interpolated2, (30, 30, 75, 0.625)); + } + + [Test] + public void BeyondEndColour() + { + var unicolour1 = Unicolour.FromHct(0, 48, 60, 0.8); + var unicolour2 = Unicolour.FromHct(90, 72, 40, 0.9); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, 1.5); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, 1.5); + + AssertInterpolated(interpolated1, (135, 84, 30, 0.95)); + AssertInterpolated(interpolated2, (315, 36, 70, 0.75)); + } + + [Test] + public void BeyondStartColour() + { + var unicolour1 = Unicolour.FromHct(0, 48, 60, 0.8); + var unicolour2 = Unicolour.FromHct(90, 72, 40, 0.9); + var interpolated1 = unicolour1.InterpolateHct(unicolour2, -0.5); + var interpolated2 = unicolour2.InterpolateHct(unicolour1, -0.5); + + AssertInterpolated(interpolated1, (315, 36, 70, 0.75)); + AssertInterpolated(interpolated2, (135, 84, 30, 0.95)); + } + + private static void AssertInterpolated(Unicolour unicolour, (double first, double second, double third, double alpha) expected) + { + AssertUtils.AssertInterpolated(unicolour.Hct.Triplet, unicolour.Alpha.A, expected); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/InterpolateHeritageTests.cs b/Unicolour.Tests/InterpolateHeritageTests.cs index 76c6c2c8..85b3b4e2 100644 --- a/Unicolour.Tests/InterpolateHeritageTests.cs +++ b/Unicolour.Tests/InterpolateHeritageTests.cs @@ -10,7 +10,7 @@ public class InterpolateHeritageTests private static readonly List HuedSpaces = new() { ColourSpace.Hsb, ColourSpace.Hsl, ColourSpace.Hwb, ColourSpace.Lchab, ColourSpace.Lchuv, - ColourSpace.Hsluv, ColourSpace.Hpluv, ColourSpace.Jzczhz, ColourSpace.Oklch + ColourSpace.Hsluv, ColourSpace.Hpluv, ColourSpace.Jzczhz, ColourSpace.Oklch, ColourSpace.Hct }; private static readonly List NonHuedSpaces = AssertUtils.AllColourSpaces.Except(HuedSpaces).ToList(); diff --git a/Unicolour.Tests/Cam02Tests.cs b/Unicolour.Tests/KnownCam02Tests.cs similarity index 97% rename from Unicolour.Tests/Cam02Tests.cs rename to Unicolour.Tests/KnownCam02Tests.cs index 43953dbe..30df6622 100644 --- a/Unicolour.Tests/Cam02Tests.cs +++ b/Unicolour.Tests/KnownCam02Tests.cs @@ -3,14 +3,14 @@ using NUnit.Framework; using static Cam; -public static class Cam02Tests +public class KnownCam02Tests { // generally high tolerances because the CYAN test data is rounded private const double ModelTolerance = 0.00005; private const double UcsTolerance = 0.0001; [Test] // matching values from https://github.com/igd-geo/pcolor/blob/master/de.fhg.igd.pcolor.test/src/de/fhg/igd/pcolor/test/CAMWorkedExample.java#L54 - public static void Cyan200La() + public void Cyan200La() { var xyz = new Xyz(0.1931, 0.2393, 0.1014); var camConfig = new CamConfiguration(new WhitePoint(98.88, 90, 32.03), 200, 18, Surround.Average); @@ -32,7 +32,7 @@ public static void Cyan200La() } [Test] // matching values from https://github.com/igd-geo/pcolor/blob/master/de.fhg.igd.pcolor.test/src/de/fhg/igd/pcolor/test/CAMWorkedExample.java#L76 - public static void Cyan20La() + public void Cyan20La() { var xyz = new Xyz(0.1931, 0.2393, 0.1014); var camConfig = new CamConfiguration(new WhitePoint(98.88, 90, 32.03), 20, 18, Surround.Average); @@ -54,7 +54,7 @@ public static void Cyan20La() } [Test] // matching values from https://github.com/colour-science/colour#34colour-appearance-models---colourappearance & https://github.com/colour-science/colour#3126cam02-lcd-cam02-scd-and-cam02-ucs-colourspaces---luo-cui-and-li-2006 - public static void Red() + public void Red() { var xyz = new Xyz(0.20654008, 0.12197225, 0.05136952); var camConfig = new CamConfiguration(new WhitePoint(95.05, 100.00, 108.88), 318.31, 20, Surround.Average); @@ -79,7 +79,7 @@ public static void Red() // J = 100 (full lightness) // regardless of the viewing conditions [Test, Combinatorial] - public static void White( + public void White( [Values(1, 10, 100, 1000, 10000)] double lux, [Values(1, 5, 20, 50, 100)] double background, [Values(Surround.Dark, Surround.Dim, Surround.Average)] Surround surround) @@ -99,7 +99,7 @@ public static void White( // all CAM properties = 0 // regardless of the viewing conditions [Test] - public static void Black( + public void Black( [Values(1, 10, 100, 1000, 10000)] double lux, [Values(1, 5, 20, 50, 100)] double background, [Values(Surround.Dark, Surround.Dim, Surround.Average)] Surround surround) diff --git a/Unicolour.Tests/Cam16Tests.cs b/Unicolour.Tests/KnownCam16Tests.cs similarity index 97% rename from Unicolour.Tests/Cam16Tests.cs rename to Unicolour.Tests/KnownCam16Tests.cs index 25a5f50b..6830ade0 100644 --- a/Unicolour.Tests/Cam16Tests.cs +++ b/Unicolour.Tests/KnownCam16Tests.cs @@ -3,13 +3,13 @@ using NUnit.Framework; using static Cam; -public static class Cam16Tests +public class KnownCam16Tests { private const double ModelTolerance = 0.00000000001; private const double UcsTolerance = 0.000000005; // different to model because RED test UCS values are rounded [Test] // matching values from https://github.com/colour-science/colour#34colour-appearance-models---colourappearance & https://github.com/colour-science/colour#3127cam16-lcd-cam16-scd-and-cam16-ucs-colourspaces---li-et-al-2017 - public static void Red() + public void Red() { var xyz = new Xyz(0.20654008, 0.12197225, 0.05136952); var camConfig = new CamConfiguration(new WhitePoint(95.05, 100.00, 108.88), 318.31, 20, Surround.Average); @@ -33,7 +33,7 @@ public static void Red() } [Test] // matching values from https://observablehq.com/@jrus/cam16 - public static void Blue() + public void Blue() { var xyz = new Xyz(0.23446234045762356, 0.23897966766938545, 0.6049634765734733); var camConfig = new CamConfiguration(WhitePoint.From(Illuminant.D65), 40, 20, Surround.Average); @@ -60,7 +60,7 @@ public static void Blue() // J = 100 (full lightness), H ~= 209.5 & Hc = "34G66B" (D65 'white' in CAM16 has a slight blueness) // regardless of the viewing conditions [Test, Combinatorial] - public static void White( + public void White( [Values(1, 10, 100, 1000, 10000)] double lux, [Values(1, 5, 20, 50, 100)] double background, [Values(Surround.Dark, Surround.Dim, Surround.Average)] Surround surround) @@ -82,7 +82,7 @@ public static void White( // all CAM properties = 0 // regardless of the viewing conditions [Test] - public static void Black( + public void Black( [Values(1, 10, 100, 1000, 10000)] double lux, [Values(1, 5, 20, 50, 100)] double background, [Values(Surround.Dark, Surround.Dim, Surround.Average)] Surround surround) diff --git a/Unicolour.Tests/KnownHctTests.cs b/Unicolour.Tests/KnownHctTests.cs new file mode 100644 index 00000000..1b807587 --- /dev/null +++ b/Unicolour.Tests/KnownHctTests.cs @@ -0,0 +1,91 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class KnownHctTests +{ + private static readonly ColourTriplet HctWhiteTriplet = new(209.492, 2.869, 100.000); + + [Test] + public void Red() + { + var red = ColourLimits.Rgb[ColourLimit.Red]; + AssertUtils.AssertTriplet(red, new(27.408, 113.358, 53.241), 0.005); + } + + [Test] + public void Green() + { + var green = ColourLimits.Rgb[ColourLimit.Green]; + AssertUtils.AssertTriplet(green, new(142.139, 108.410, 87.737), 0.005); + } + + [Test] + public void Blue() + { + var blue = ColourLimits.Rgb[ColourLimit.Blue]; + AssertUtils.AssertTriplet(blue, new(282.788, 87.230, 32.302), 0.005); + } + + [Test] + public void Black() + { + var black = ColourLimits.Rgb[ColourLimit.Black]; + AssertUtils.AssertTriplet(black, new(0.000, 0.000, 0.000), 0.005); + } + + [Test] + public void White() + { + var white = ColourLimits.Rgb[ColourLimit.White]; + AssertUtils.AssertTriplet(white, HctWhiteTriplet, 0.005); + } + + [Test] + public void WhiteD65ToHct() + { + var whiteD65 = Unicolour.FromRgb(new Configuration(xyzConfiguration: XyzConfiguration.D65), 1.0, 1.0, 1.0); + AssertUtils.AssertTriplet(whiteD65, new(0.9505, 1.0000, 1.0888), 0.0001); // XYZ values for D65 white + AssertUtils.AssertTriplet(whiteD65, HctWhiteTriplet, 0.005); // HCT values for D65 white (no adaptation needed) + } + + [Test] + public void WhiteD65FromHct() + { + var whiteD65 = Unicolour.FromHct(new Configuration(xyzConfiguration: XyzConfiguration.D65), 209.492, 2.869, 100.000); + AssertUtils.AssertTriplet(whiteD65, new(0.9505, 1.0000, 1.0888), 0.0001); // XYZ values for D65 white (no adaptation needed) + } + + [Test] + public void WhiteD50ToHct() + { + var whiteD50 = Unicolour.FromRgb(new Configuration(xyzConfiguration: XyzConfiguration.D50), 1.0, 1.0, 1.0); + AssertUtils.AssertTriplet(whiteD50, new(0.9642, 1.0000, 0.8252), 0.0001); // XYZ values for D50 white + AssertUtils.AssertTriplet(whiteD50, HctWhiteTriplet, 0.005); // HCT values same as D65 white due to adaptation + } + + [Test] + public void WhiteD50FromHct() + { + var whiteD50 = Unicolour.FromHct(new Configuration(xyzConfiguration: XyzConfiguration.D50), 209.492, 2.869, 100.000); + AssertUtils.AssertTriplet(whiteD50, new(0.9642, 1.0000, 0.8252), 0.0001); // XYZ values for D50 white due to adaptation + } + + [Test] + public void WhiteEqualEnergyToHct() + { + var equalEnergyConfig = new XyzConfiguration(WhitePoint.From(Illuminant.E)); + var whiteE = Unicolour.FromRgb(new Configuration(xyzConfiguration: equalEnergyConfig), 1.0, 1.0, 1.0); + AssertUtils.AssertTriplet(whiteE, new(1.0000, 1.0000, 1.0000), 0.0001); // XYZ values for E white + AssertUtils.AssertTriplet(whiteE, HctWhiteTriplet, 0.005); // HCT values same as D65 white due to adaptation + } + + [Test] + public void WhiteEqualEnergyFromHct() + { + var equalEnergyConfig = new XyzConfiguration(WhitePoint.From(Illuminant.E)); + var whiteE = Unicolour.FromHct(new Configuration(xyzConfiguration: equalEnergyConfig), 209.492, 2.869, 100.000); + AssertUtils.AssertTriplet(whiteE, new(1.0000, 1.0000, 1.0000), 0.0001); // XYZ values for E white due to adaptation + } +} \ No newline at end of file diff --git a/Unicolour.Tests/HsluvTests.cs b/Unicolour.Tests/KnownHsluvTests.cs similarity index 95% rename from Unicolour.Tests/HsluvTests.cs rename to Unicolour.Tests/KnownHsluvTests.cs index f201e566..36946f78 100644 --- a/Unicolour.Tests/HsluvTests.cs +++ b/Unicolour.Tests/KnownHsluvTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class HsluvTests +public class KnownHsluvTests { /* * note that at each step of the conversion process, more tolerance is required to match the HSLuv test data set @@ -12,7 +12,7 @@ public static class HsluvTests * Unicolour calculates matrix from chromaticities and matches Bruce Lindbloom's sRGB D65 matrix (first matrix value: 0.41245643908969187) */ [TestCaseSource(typeof(HsluvTestColour), nameof(HsluvTestColour.All))] - public static void SnapshotTestColour(TestColour testColour) + public void SnapshotTestColour(TestColour testColour) { var hex = testColour.Hex!; var unicolour = Unicolour.FromHex(hex); diff --git a/Unicolour.Tests/OklabTests.cs b/Unicolour.Tests/KnownOklabTests.cs similarity index 95% rename from Unicolour.Tests/OklabTests.cs rename to Unicolour.Tests/KnownOklabTests.cs index 2893fa20..4f40d44c 100644 --- a/Unicolour.Tests/OklabTests.cs +++ b/Unicolour.Tests/KnownOklabTests.cs @@ -6,7 +6,7 @@ // Oklab actually 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 +public class KnownOklabTests { private static readonly List<(ColourTriplet xyz, ColourTriplet expectedOklab)> TestData = new() { @@ -17,7 +17,7 @@ public static class OklabTests }; [Test] - public static void FromXyzD65() + public void FromXyzD65() { foreach (var (xyz, expectedOklab) in TestData) { @@ -26,7 +26,7 @@ public static void FromXyzD65() } [Test] - public static void FromXyzD50() + public void FromXyzD50() { foreach (var (xyz, expectedOklab) in TestData) { diff --git a/Unicolour.Tests/XyyTests.cs b/Unicolour.Tests/KnownXyyTests.cs similarity index 82% rename from Unicolour.Tests/XyyTests.cs rename to Unicolour.Tests/KnownXyyTests.cs index 69618d73..48c01c88 100644 --- a/Unicolour.Tests/XyyTests.cs +++ b/Unicolour.Tests/KnownXyyTests.cs @@ -6,14 +6,14 @@ // XYY has special handling for black, since otherwise it would result in a divide-by-zero // some implementations would set all values to zero // but I think it's more intuitive to set chromaticity to the same as white, and set only luminance to 0 -public static class XyyTests +public class KnownXyyTests { private const double Tolerance = 0.000001; [TestCase(Illuminant.D65, 0.312727, 0.329023, 1.000000)] [TestCase(Illuminant.D50, 0.345669, 0.358496, 1.000000)] [TestCase(Illuminant.E, 0.333333, 0.333333, 1.000000)] - public static void White(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) + public void White(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) { var unicolour = Unicolour.FromRgb(GetConfig(illuminant), 1, 1, 1); AssertUtils.AssertTriplet(unicolour.Xyy.Triplet, new(expectedX, expectedY, expectedLuminance), Tolerance); @@ -22,7 +22,7 @@ public static void White(Illuminant illuminant, double expectedX, double expecte [TestCase(Illuminant.D65, 0.312727, 0.329023, 0.214041)] [TestCase(Illuminant.D50, 0.345669, 0.358496, 0.214041)] [TestCase(Illuminant.E, 0.333333, 0.333333, 0.214041)] - public static void Grey(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) + public void Grey(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) { var unicolour = Unicolour.FromRgb(GetConfig(illuminant), 0.5, 0.5, 0.5); AssertUtils.AssertTriplet(unicolour.Xyy.Triplet, new(expectedX, expectedY, expectedLuminance), Tolerance); @@ -31,7 +31,7 @@ public static void Grey(Illuminant illuminant, double expectedX, double expected [TestCase(Illuminant.D65, 0.312727, 0.329023, 0.000001)] [TestCase(Illuminant.D50, 0.345669, 0.358496, 0.000001)] [TestCase(Illuminant.E, 0.333333, 0.333333, 0.000001)] - public static void NearBlack(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) + public void NearBlack(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) { var unicolour = Unicolour.FromRgb(GetConfig(illuminant), 0.00001, 0.00001, 0.00001); AssertUtils.AssertTriplet(unicolour.Xyy.Triplet, new(expectedX, expectedY, expectedLuminance), Tolerance); @@ -40,7 +40,7 @@ public static void NearBlack(Illuminant illuminant, double expectedX, double exp [TestCase(Illuminant.D65, 0.312727, 0.329023, 0.000000)] [TestCase(Illuminant.D50, 0.345669, 0.358496, 0.000000)] [TestCase(Illuminant.E, 0.333333, 0.333333, 0.000000)] - public static void Black(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) + public void Black(Illuminant illuminant, double expectedX, double expectedY, double expectedLuminance) { var unicolour = Unicolour.FromRgb(GetConfig(illuminant), 0, 0, 0); AssertUtils.AssertTriplet(unicolour.Xyy.Triplet, new(expectedX, expectedY, expectedLuminance), Tolerance); @@ -49,7 +49,7 @@ public static void Black(Illuminant illuminant, double expectedX, double expecte [TestCase(-0.00000000001)] [TestCase(0)] [TestCase(0.00000000001)] - public static void ChromaticityY(double chromaticityY) + public void ChromaticityY(double chromaticityY) { var unicolour = Unicolour.FromXyy(0.5, chromaticityY, 1); var xyz = unicolour.Xyz; diff --git a/Unicolour.Tests/LazyEvaluationTests.cs b/Unicolour.Tests/LazyEvaluationTests.cs index 92c69f7c..1452a44f 100644 --- a/Unicolour.Tests/LazyEvaluationTests.cs +++ b/Unicolour.Tests/LazyEvaluationTests.cs @@ -7,7 +7,7 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class LazyEvaluationTests +public class LazyEvaluationTests { // RgbLinear & Rgb255 are currently calculated during Rgb construction // instead of separately like the other colour spaces @@ -38,80 +38,81 @@ public static class LazyEvaluationTests new TestCaseData(RandomColours.UnicolourFromOklab).SetName("Oklab"), new TestCaseData(RandomColours.UnicolourFromOklch).SetName("Oklch"), new TestCaseData(RandomColours.UnicolourFromCam02).SetName("Cam02"), - new TestCaseData(RandomColours.UnicolourFromCam16).SetName("Cam16") + new TestCaseData(RandomColours.UnicolourFromCam16).SetName("Cam16"), + new TestCaseData(RandomColours.UnicolourFromHct).SetName("Hct") }; [TestCaseSource(nameof(TestCases))] - public static void InitialUnicolour(Func unicolourFunction) + public void InitialUnicolour(Func unicolourFunction) { var unicolour = unicolourFunction(); AssertBackingFields(unicolour); } [TestCaseSource(nameof(TestCases))] - public static void AfterEquality(Func unicolourFunction) + public void AfterEquality(Func unicolourFunction) { var unicolour = unicolourFunction(); var other = unicolourFunction(); - var _ = unicolour.Equals(other); + _ = unicolour.Equals(other); AssertBackingFields(unicolour); } [TestCaseSource(nameof(TestCases))] - public static void AfterInterpolation(Func unicolourFunction) + public void AfterInterpolation(Func unicolourFunction) { var unicolour = unicolourFunction(); var other = unicolourFunction(); var initialColourSpace = unicolour.InitialColourSpace; - var _ = Interpolation.Interpolate(initialColourSpace, unicolour, other, 0.5); + _ = Interpolation.Interpolate(initialColourSpace, unicolour, other, 0.5); AssertBackingFields(unicolour); } [TestCaseSource(nameof(TestCases))] - public static void AfterHex(Func unicolourFunction) + public void AfterHex(Func unicolourFunction) { var unicolour = unicolourFunction(); - var _ = unicolour.Hex; + _ = unicolour.Hex; AssertBackingFieldEvaluated(unicolour, ColourSpace.Rgb); } [TestCaseSource(nameof(TestCases))] - public static void AfterIsDisplayable(Func unicolourFunction) + public void AfterIsDisplayable(Func unicolourFunction) { var unicolour = unicolourFunction(); - var _ = unicolour.IsDisplayable; + _ = unicolour.IsDisplayable; AssertBackingFieldEvaluated(unicolour, ColourSpace.Rgb); } [TestCaseSource(nameof(TestCases))] - public static void AfterRelativeLuminance(Func unicolourFunction) + public void AfterRelativeLuminance(Func unicolourFunction) { var unicolour = unicolourFunction(); - var _ = unicolour.RelativeLuminance; + _ = unicolour.RelativeLuminance; AssertBackingFieldEvaluated(unicolour, ColourSpace.Rgb); } [TestCaseSource(nameof(TestCases))] - public static void AfterDescription(Func unicolourFunction) + public void AfterDescription(Func unicolourFunction) { var unicolour = unicolourFunction(); - var _ = unicolour.Description; + _ = unicolour.Description; AssertBackingFieldEvaluated(unicolour, ColourSpace.Hsl); } [TestCaseSource(nameof(TestCases))] - public static void AfterTemperature(Func unicolourFunction) + public void AfterTemperature(Func unicolourFunction) { var unicolour = unicolourFunction(); - var _ = unicolour.Temperature; + _ = unicolour.Temperature; AssertBackingFieldEvaluated(unicolour, ColourSpace.Xyz); } [TestCaseSource(nameof(TestCases))] - public static void AfterConfigurationConversion(Func unicolourFunction) + public void AfterConfigurationConversion(Func unicolourFunction) { var unicolour = unicolourFunction(); - var _ = unicolour.ConvertToConfiguration(Configuration.Default); + _ = unicolour.ConvertToConfiguration(Configuration.Default); AssertBackingFieldEvaluated(unicolour, ColourSpace.Xyz); } diff --git a/Unicolour.Tests/LibraryColorMineTests.cs b/Unicolour.Tests/LibraryColorMineTests.cs new file mode 100644 index 00000000..b590a6f3 --- /dev/null +++ b/Unicolour.Tests/LibraryColorMineTests.cs @@ -0,0 +1,30 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.OtherLibraries; +using Wacton.Unicolour.Tests.Utils; + +/* + * not testing from HWB / LCHuv / HSLuv / HPLuv / ICtCp / JzAzBz / JzCzHz / Oklab / Oklch / CAM02 / CAM16 / HCT --- does not support them + * not testing from RGB [0-1] --- RGB only accepts 0-255 + * not testing from XYZ / xyY / LAB / LCHab / LUV --- does a terrible job + */ +public class LibraryColorMineTests : LibraryTestBase +{ + private static readonly ITestColourFactory ColorMineFactory = new ColorMineFactory(); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void Named(TestColour namedColour) => AssertFromHex(namedColour.Hex!, ColorMineFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] + public void Hex(string hex) => AssertFromHex(hex, ColorMineFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] + public void Rgb255(ColourTriplet triplet) => AssertFromRgb255(triplet, ColorMineFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] + public void Hsb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsb, ColorMineFactory.FromHsb); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] + public void Hsl(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsl, ColorMineFactory.FromHsl); +} \ No newline at end of file diff --git a/Unicolour.Tests/LibraryColourfulTests.cs b/Unicolour.Tests/LibraryColourfulTests.cs new file mode 100644 index 00000000..b943d620 --- /dev/null +++ b/Unicolour.Tests/LibraryColourfulTests.cs @@ -0,0 +1,40 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.OtherLibraries; +using Wacton.Unicolour.Tests.Utils; + +/* + * not testing from HSB / HSL / HWB / HSLuv / HPLuv / ICtCp / Oklab / Oklch / CAM02 / CAM16 / HCT --- does not support them + * not testing from LUV / LCHuv --- appears to give wrong values (XYZ clamping?) + * not testing JzAzBz / JzCzHz --- generates different values, due to multiplying XYZ by different values + * (Jzazbz paper is ambiguous about XYZ input, more details here https://github.com/nschloe/colorio/issues/41 - Unicolour aims to match plots of colour datasets like Colorio) + */ +public class LibraryColourfulTests : LibraryTestBase +{ + private static readonly ITestColourFactory ColourfulFactory = new ColourfulFactory(); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void Named(TestColour namedColour) => AssertFromHex(namedColour.Hex!, ColourfulFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] + public void Hex(string hex) => AssertFromHex(hex, ColourfulFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] + public void Rgb255(ColourTriplet triplet) => AssertFromRgb255(triplet, ColourfulFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] + public void Rgb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromRgb, ColourfulFactory.FromRgb); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void Xyz(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyz, ColourfulFactory.FromXyz); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))] + public void Xyy(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyy, ColourfulFactory.FromXyy); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] + public void Lab(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromLab, ColourfulFactory.FromLab); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchabTriplets))] + public void Lchab(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromLchab, ColourfulFactory.FromLchab); +} \ No newline at end of file diff --git a/Unicolour.Tests/LibraryOpenCvTests.cs b/Unicolour.Tests/LibraryOpenCvTests.cs new file mode 100644 index 00000000..a8a0a691 --- /dev/null +++ b/Unicolour.Tests/LibraryOpenCvTests.cs @@ -0,0 +1,61 @@ +namespace Wacton.Unicolour.Tests; + +using System; +using NUnit.Framework; +using Wacton.Unicolour.Tests.OtherLibraries; +using Wacton.Unicolour.Tests.Utils; + +/* + * not testing from HWB / xyY / LCHab / LCHuv / HSLuv / HPLuv / ICtCp / JzAzBz / JzCzHz / Oklab / Oklch / CAM02 / CAM16 / HCT --- does not support them + * (also I've given up trying to make OpenCvSharp work in a dockerised unix environment...) + */ +public class LibraryOpenCvTests : LibraryTestBase +{ + private static readonly ITestColourFactory OpenCvFactory = new OpenCvFactory(); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void Named(TestColour namedColour) => RunIfWindows(() => AssertFromHex(namedColour.Hex!, OpenCvFactory)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] + public void Hex(string hex) => RunIfWindows(() => AssertFromHex(hex, OpenCvFactory)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] + public void Rgb255(ColourTriplet triplet) => RunIfWindows(() => AssertFromRgb255(triplet, OpenCvFactory)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] + public void Rgb(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromRgb, OpenCvFactory.FromRgb)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] + public void Hsb(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromHsb, OpenCvFactory.FromHsb)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] + public void Hsl(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromHsl, OpenCvFactory.FromHsl)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void Xyz(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromXyz, OpenCvFactory.FromXyz)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] + public void Lab(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromLab, OpenCvFactory.FromLab)); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LuvTriplets))] + public void Luv(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromLuv, OpenCvFactory.FromLuv)); + + // in order to test OpenCV in a non-windows environment, this looks up a stored precomputed value + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void CrossPlatform(TestColour namedColour) => AssertFromCsvData(namedColour.Hex!, namedColour.Name!); + + private static void RunIfWindows(Action action) + { + bool IsWindows() => Environment.OSVersion.Platform == PlatformID.Win32NT; + Assume.That(IsWindows()); + action(); + } + + private static void AssertFromCsvData(string hex, string name) + { + var unicolour = Unicolour.FromHex(hex); + var otherLibColour = OpenCvCsvFactory.FromName(name); + AssertHex(unicolour, hex); + AssertTestColour(unicolour, otherLibColour); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/LibrarySixLaborsTests.cs b/Unicolour.Tests/LibrarySixLaborsTests.cs new file mode 100644 index 00000000..aad8df2e --- /dev/null +++ b/Unicolour.Tests/LibrarySixLaborsTests.cs @@ -0,0 +1,38 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.OtherLibraries; +using Wacton.Unicolour.Tests.Utils; + +/* + * not testing from HWB / LCHab / HSLuv / HPLuv / ICtCp / JzAzBz / JzCzHz / Oklab / Oklch / CAM02 / CAM16 / HCT --- does not support them + * not testing from LAB / LUV / LCHuv --- appears to give wrong values (XYZ clamping?) + */ +public class LibrarySixLaborsTests : LibraryTestBase +{ + private static readonly ITestColourFactory SixLaborsFactory = new SixLaborsFactory(); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void Named(TestColour namedColour) => AssertFromHex(namedColour.Hex!, SixLaborsFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] + public void Hex(string hex) => AssertFromHex(hex, SixLaborsFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] + public void Rgb255(ColourTriplet triplet) => AssertFromRgb255(triplet, SixLaborsFactory); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] + public void Rgb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromRgb, SixLaborsFactory.FromRgb); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] + public void Hsb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsb, SixLaborsFactory.FromHsb); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] + public void Hsl(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsl, SixLaborsFactory.FromHsl); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void Xyz(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyz, SixLaborsFactory.FromXyz); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))] + public void Xyy(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyy, SixLaborsFactory.FromXyy); +} \ No newline at end of file diff --git a/Unicolour.Tests/MatrixTests.cs b/Unicolour.Tests/MatrixTests.cs index 1fd2745a..0158f2dc 100644 --- a/Unicolour.Tests/MatrixTests.cs +++ b/Unicolour.Tests/MatrixTests.cs @@ -5,7 +5,7 @@ using MathNet.Numerics.LinearAlgebra; using NUnit.Framework; -public static class MatrixTests +public class MatrixTests { private static readonly double[,] DataA = { @@ -49,7 +49,7 @@ public static class MatrixTests } [Test] - public static void MultiplyCompatibleDimensions() + public void MultiplyCompatibleDimensions() { // 3x3 * 3x3 AssertMatrixMultiply(DataA, DataB, ExpectedMultiplied); @@ -61,7 +61,7 @@ public static void MultiplyCompatibleDimensions() } [Test] - public static void MultiplyIncompatibleDimensions() + public void MultiplyIncompatibleDimensions() { // 3x3 * 1x3 var matrixA = new Matrix(DataA); @@ -70,14 +70,14 @@ public static void MultiplyIncompatibleDimensions() } [Test] - public static void InverseThreeByThree() + public void InverseThreeByThree() { AssertMatrixInverse(DataA, ExpectedInverseA); AssertMatrixInverse(DataB, ExpectedInverseB); } [Test] - public static void InverseUnsupported() + public void InverseUnsupported() { // invert only supports 3x3 invert, as that is all that is required for colours var twoByTwo = new Matrix(new[,] @@ -94,7 +94,7 @@ public static void InverseUnsupported() } [Test] - public static void Scale() + public void Scale() { var identity = new[,] { @@ -123,7 +123,7 @@ public static void Scale() } [Test] - public static void Select() + public void Select() { var identity = new[,] { @@ -176,7 +176,7 @@ public static void Select() } [Test] - public static void ToTripletCompatibleDimensions() + public void ToTripletCompatibleDimensions() { var triplet = new ColourTriplet(1.1, 2.2, 3.3); var matrixFromData = new Matrix(new[,] @@ -194,7 +194,7 @@ public static void ToTripletCompatibleDimensions() } [Test] - public static void ToTripletIncompatibleDimensions() + public void ToTripletIncompatibleDimensions() { var matrix = new Matrix(DataA); Assert.Throws(() => matrix.ToTriplet()); diff --git a/Unicolour.Tests/NamedColoursTests.cs b/Unicolour.Tests/NamedColoursTests.cs new file mode 100644 index 00000000..36876b28 --- /dev/null +++ b/Unicolour.Tests/NamedColoursTests.cs @@ -0,0 +1,33 @@ +namespace Wacton.Unicolour.Tests; + +using System; +using System.Drawing; +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class NamedColoursTests +{ + private static readonly RgbConfiguration RgbConfig = RgbConfiguration.StandardRgb; + + // no point doing this test starting with Wikipedia's HSB / HSL values since they're rounded + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void RgbToHsb(TestColour namedColour) + { + var systemColour = ColorTranslator.FromHtml(namedColour.Hex!); + var rgb = new Rgb(systemColour.R / 255.0, systemColour.G / 255.0, systemColour.B / 255.0, RgbConfig); + var hsb = Hsb.FromRgb(rgb); + var hsl = Hsl.FromHsb(hsb); + + var expectedRoundedHsb = namedColour.Hsb; + var expectedRoundedHsl = namedColour.Hsl; + + Assert.That(Math.Round(hsb.H), Is.EqualTo(expectedRoundedHsb!.First), namedColour.Name!); + Assert.That(Math.Round(hsb.S, 2), Is.EqualTo(expectedRoundedHsb.Second), namedColour.Name!); + Assert.That(Math.Round(hsb.B, 2), Is.EqualTo(expectedRoundedHsb.Third), namedColour.Name!); + + // within 0.02 because it seems like some of wikipedia's HSL values have questionable rounding... + Assert.That(Math.Round(hsl.H), Is.EqualTo(expectedRoundedHsl!.First), namedColour.Name!); + Assert.That(Math.Round(hsl.S, 2), Is.EqualTo(expectedRoundedHsl.Second).Within(0.02), namedColour.Name!); + Assert.That(Math.Round(hsl.L, 2), Is.EqualTo(expectedRoundedHsl.Third).Within(0.02), namedColour.Name!); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/NotNumberTests.cs b/Unicolour.Tests/NotNumberTests.cs index 4ce1f5a2..954f8168 100644 --- a/Unicolour.Tests/NotNumberTests.cs +++ b/Unicolour.Tests/NotNumberTests.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class NotNumberTests +public class NotNumberTests { private static double[][] testCases = { @@ -18,61 +18,64 @@ public static class NotNumberTests }; [TestCaseSource(nameof(testCases))] - public static void Rgb(double r, double g, double b) => AssertUnicolour(Unicolour.FromRgb(r, g, b)); + public void Rgb(double r, double g, double b) => AssertUnicolour(Unicolour.FromRgb(r, g, b)); [TestCaseSource(nameof(testCases))] - public static void Hsb(double h, double s, double b) => AssertUnicolour(Unicolour.FromHsb(h, s, b)); + public void Hsb(double h, double s, double b) => AssertUnicolour(Unicolour.FromHsb(h, s, b)); [TestCaseSource(nameof(testCases))] - public static void Hsl(double h, double s, double l) => AssertUnicolour(Unicolour.FromHsl(h, s, l)); + public void Hsl(double h, double s, double l) => AssertUnicolour(Unicolour.FromHsl(h, s, l)); [TestCaseSource(nameof(testCases))] - public static void Hwb(double h, double w, double b) => AssertUnicolour(Unicolour.FromHwb(h, w, b)); + public void Hwb(double h, double w, double b) => AssertUnicolour(Unicolour.FromHwb(h, w, b)); [TestCaseSource(nameof(testCases))] - public static void Xyz(double x, double y, double z) => AssertUnicolour(Unicolour.FromXyz(x, y, z)); + public void Xyz(double x, double y, double z) => AssertUnicolour(Unicolour.FromXyz(x, y, z)); [TestCaseSource(nameof(testCases))] - public static void Xyy(double x, double y, double upperY) => AssertUnicolour(Unicolour.FromXyy(x, y, upperY)); + public void Xyy(double x, double y, double upperY) => AssertUnicolour(Unicolour.FromXyy(x, y, upperY)); [TestCaseSource(nameof(testCases))] - public static void Lab(double l, double a, double b) => AssertUnicolour(Unicolour.FromLab(l, a, b)); + public void Lab(double l, double a, double b) => AssertUnicolour(Unicolour.FromLab(l, a, b)); [TestCaseSource(nameof(testCases))] - public static void Lchab(double l, double c, double h) => AssertUnicolour(Unicolour.FromLchab(l, c, h)); + public void Lchab(double l, double c, double h) => AssertUnicolour(Unicolour.FromLchab(l, c, h)); [TestCaseSource(nameof(testCases))] - public static void Luv(double l, double u, double v) => AssertUnicolour(Unicolour.FromLuv(l, u, v)); + public void Luv(double l, double u, double v) => AssertUnicolour(Unicolour.FromLuv(l, u, v)); [TestCaseSource(nameof(testCases))] - public static void Lchuv(double l, double c, double h) => AssertUnicolour(Unicolour.FromLchuv(l, c, h)); + public void Lchuv(double l, double c, double h) => AssertUnicolour(Unicolour.FromLchuv(l, c, h)); [TestCaseSource(nameof(testCases))] - public static void Hsluv(double h, double s, double l) => AssertUnicolour(Unicolour.FromHsluv(h, s, l)); + public void Hsluv(double h, double s, double l) => AssertUnicolour(Unicolour.FromHsluv(h, s, l)); [TestCaseSource(nameof(testCases))] - public static void Hpluv(double h, double s, double l) => AssertUnicolour(Unicolour.FromHpluv(h, s, l)); + public void Hpluv(double h, double s, double l) => AssertUnicolour(Unicolour.FromHpluv(h, s, l)); [TestCaseSource(nameof(testCases))] - public static void Ictcp(double i, double ct, double cp) => AssertUnicolour(Unicolour.FromIctcp(i, ct, cp)); + public void Ictcp(double i, double ct, double cp) => AssertUnicolour(Unicolour.FromIctcp(i, ct, cp)); [TestCaseSource(nameof(testCases))] - public static void Jzazbz(double jz, double az, double bz) => AssertUnicolour(Unicolour.FromJzazbz(jz, az, bz)); + public void Jzazbz(double jz, double az, double bz) => AssertUnicolour(Unicolour.FromJzazbz(jz, az, bz)); [TestCaseSource(nameof(testCases))] - public static void Jzczhz(double jz, double cz, double hz) => AssertUnicolour(Unicolour.FromJzczhz(jz, cz, hz)); + public void Jzczhz(double jz, double cz, double hz) => AssertUnicolour(Unicolour.FromJzczhz(jz, cz, hz)); [TestCaseSource(nameof(testCases))] - public static void Oklab(double l, double a, double b) => AssertUnicolour(Unicolour.FromOklab(l, a, b)); + public void Oklab(double l, double a, double b) => AssertUnicolour(Unicolour.FromOklab(l, a, b)); [TestCaseSource(nameof(testCases))] - public static void Oklch(double l, double c, double h) => AssertUnicolour(Unicolour.FromOklch(l, c, h)); + public void Oklch(double l, double c, double h) => AssertUnicolour(Unicolour.FromOklch(l, c, h)); [TestCaseSource(nameof(testCases))] - public static void Cam02(double j, double a, double b) => AssertUnicolour(Unicolour.FromCam02(j, a, b)); + public void Cam02(double j, double a, double b) => AssertUnicolour(Unicolour.FromCam02(j, a, b)); [TestCaseSource(nameof(testCases))] - public static void Cam16(double j, double a, double b) => AssertUnicolour(Unicolour.FromCam16(j, a, b)); + public void Cam16(double j, double a, double b) => AssertUnicolour(Unicolour.FromCam16(j, a, b)); + + [TestCaseSource(nameof(testCases))] + public void Hct(double h, double c, double t) => AssertUnicolour(Unicolour.FromHct(h, c, t)); // LUV -> XYZ converts NaNs to 0s // which results in downstream RGB / HSB / HSL containing real values but are used as NaN diff --git a/Unicolour.Tests/Factories/ColorMineFactory.cs b/Unicolour.Tests/OtherLibraries/ColorMineFactory.cs similarity index 98% rename from Unicolour.Tests/Factories/ColorMineFactory.cs rename to Unicolour.Tests/OtherLibraries/ColorMineFactory.cs index 09abe2da..b7f0b312 100644 --- a/Unicolour.Tests/Factories/ColorMineFactory.cs +++ b/Unicolour.Tests/OtherLibraries/ColorMineFactory.cs @@ -1,4 +1,4 @@ -namespace Wacton.Unicolour.Tests.Factories; +namespace Wacton.Unicolour.Tests.OtherLibraries; using System; using System.Collections.Generic; @@ -13,7 +13,7 @@ namespace Wacton.Unicolour.Tests.Factories; using ColorMineLuv = ColorMine.ColorSpaces.Luv; /* - * ColorMine does not support HWB / LCHuv / HSLuv / HPLuv / ICtCp / Jzazbz / Jzczhz / Oklab / Oklch / CAM02 / CAM16 + * ColorMine does not support HWB / LCHuv / HSLuv / HPLuv / ICtCp / Jzazbz / Jzczhz / Oklab / Oklch / CAM02 / CAM16 / HCT * ColorMine does not expose linear RGB * ColorMine does a bad job of converting to HSL * ColorMine does a terrible job of converting from XYZ / LAB / LCHab / LUV diff --git a/Unicolour.Tests/Factories/ColourfulFactory.cs b/Unicolour.Tests/OtherLibraries/ColourfulFactory.cs similarity index 99% rename from Unicolour.Tests/Factories/ColourfulFactory.cs rename to Unicolour.Tests/OtherLibraries/ColourfulFactory.cs index e61596d8..afe8c20a 100644 --- a/Unicolour.Tests/Factories/ColourfulFactory.cs +++ b/Unicolour.Tests/OtherLibraries/ColourfulFactory.cs @@ -1,4 +1,4 @@ -namespace Wacton.Unicolour.Tests.Factories; +namespace Wacton.Unicolour.Tests.OtherLibraries; using System; using System.Collections.Generic; @@ -15,7 +15,7 @@ namespace Wacton.Unicolour.Tests.Factories; using ColourfulIlluminants = Colourful.Illuminants; /* - * Colourful does not support HSB / HSL / HWB / HSLuv / HPLuv / ICtCp / Oklab / Oklch CAM02 / CAM16 + * Colourful does not support HSB / HSL / HWB / HSLuv / HPLuv / ICtCp / Oklab / Oklch CAM02 / CAM16 / HCT * Colourful appears to behave oddly converting from LUV / LCHuv * Colourful handles XYZ -> JzAzBz / JzCzHz differently to Unicolour (related to https://github.com/nschloe/colorio/issues/41 and inconsistent ranges in the plots from the paper) * --- which results in wildly different Jz* values and makes comparing them pointless diff --git a/Unicolour.Tests/Factories/ITestColourFactory.cs b/Unicolour.Tests/OtherLibraries/ITestColourFactory.cs similarity index 97% rename from Unicolour.Tests/Factories/ITestColourFactory.cs rename to Unicolour.Tests/OtherLibraries/ITestColourFactory.cs index c38b7821..b91e88ba 100644 --- a/Unicolour.Tests/Factories/ITestColourFactory.cs +++ b/Unicolour.Tests/OtherLibraries/ITestColourFactory.cs @@ -1,4 +1,4 @@ -namespace Wacton.Unicolour.Tests.Factories; +namespace Wacton.Unicolour.Tests.OtherLibraries; using Wacton.Unicolour.Tests.Utils; diff --git a/Unicolour.Tests/OtherLibraries/LibraryTestBase.cs b/Unicolour.Tests/OtherLibraries/LibraryTestBase.cs new file mode 100644 index 00000000..e339da28 --- /dev/null +++ b/Unicolour.Tests/OtherLibraries/LibraryTestBase.cs @@ -0,0 +1,112 @@ +namespace Wacton.Unicolour.Tests.OtherLibraries; + +using System; +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class LibraryTestBase +{ + private const bool PrintExclusions = false; + + internal delegate Unicolour UnicolourFromTuple((double first, double second, double third) tuple, double alpha = 1.0); + internal delegate TestColour TestColourFromTuple(ColourTriplet triplet); + + internal static void AssertTriplet(ColourTriplet triplet, UnicolourFromTuple getUnicolour, TestColourFromTuple getTestColour) + { + var unicolour = getUnicolour(triplet.Tuple); + var testColour = getTestColour(triplet); + AssertTestColour(unicolour, testColour); + } + + internal static void AssertFromHex(string hex, ITestColourFactory testColourFactory) + { + var unicolour = Unicolour.FromHex(hex); + var (r255, g255, b255, _) = SystemColorUtils.HexToRgb255(hex); + var testColour = testColourFactory.FromRgb255(r255, g255, b255, $"HEX [{hex}]"); + AssertHex(unicolour, hex); + AssertTestColour(unicolour, testColour); + } + + internal static void AssertFromRgb255(ColourTriplet triplet, ITestColourFactory testColourFactory) + { + var (first, second, third) = triplet; + var r255 = (int)first; + var g255 = (int)second; + var b255 = (int)third; + var unicolour = Unicolour.FromRgb255(r255, g255, b255); + var testColour = testColourFactory.FromRgb255(r255, g255, b255); + AssertTestColour(unicolour, testColour); + } + + internal static void AssertHex(Unicolour unicolour, string hex) + { + var hasAlpha = hex.Length is 8 or 9; + var expectedHex = hasAlpha ? hex[..^2] : hex; + var expectedA = hasAlpha ? hex.Substring(hex.Length - 2, 2) : "FF"; + Assert.That(unicolour.Hex.Contains(expectedHex.ToUpper())); + Assert.That(unicolour.Alpha.Hex, Is.EqualTo(expectedA.ToUpper())); + } + + internal static void AssertTestColour(Unicolour unicolour, TestColour testColour) + { + var colourName = testColour.Name; + var tolerances = testColour.Tolerances; + if (colourName == null) throw new ArgumentException("Malformed test colour: no name"); + if (tolerances == null) throw new ArgumentException("Malformed test colour: no tolerances"); + + if (testColour.ExcludeFromAllTests) + { + PrintExclusion(colourName, "all colour spaces", string.Join(", ", testColour.ExcludeFromAllTestReasons)); + return; + } + + var unicolourRgb = testColour.IsRgbConstrained ? unicolour.Rgb.ConstrainedTriplet : unicolour.Rgb.Triplet; + var unicolourRgbLinear = testColour.IsRgbLinearConstrained ? unicolour.Rgb.Linear.ConstrainedTriplet : unicolour.Rgb.Linear.Triplet; + AssertTriplet(unicolourRgb, testColour.Rgb, tolerances.Rgb, $"{colourName} -> RGB"); + AssertTriplet(unicolourRgbLinear, testColour.RgbLinear, tolerances.RgbLinear, $"{colourName} -> RGB Linear"); + AssertTriplet(unicolour.Xyz.Triplet, testColour.Xyz, tolerances.Xyz, $"{colourName} -> XYZ"); + AssertTriplet(unicolour.Lab.Triplet, testColour.Lab, tolerances.Lab, $"{colourName} -> LAB"); + AssertTriplet(unicolour.Luv.Triplet, testColour.Luv, tolerances.Luv, $"{colourName} -> LUV"); + + if (testColour.ExcludeFromXyyTests) + { + PrintExclusion(colourName, "xyY", string.Join(", ", testColour.ExcludeFromXyyTestReasons)); + } + else + { + AssertTriplet(unicolour.Xyy.Triplet, testColour.Xyy, tolerances.Xyy, $"{colourName} -> xyY"); + } + + if (testColour.ExcludeFromHsxTests) + { + PrintExclusion(colourName, "HSB/HSL", string.Join(", ", testColour.ExcludeFromHsxTestReasons)); + } + else + { + AssertTriplet(unicolour.Hsb.ConstrainedTriplet, testColour.Hsb, tolerances.Hsb, $"{colourName} -> HSB"); + AssertTriplet(unicolour.Hsl.ConstrainedTriplet, testColour.Hsl, tolerances.Hsl, $"{colourName} -> HSL"); + } + + if (testColour.ExcludeFromLchTests) + { + PrintExclusion(colourName, "LCH", string.Join(", ", testColour.ExcludeFromLchTestReasons)); + } + else + { + AssertTriplet(unicolour.Lchab.ConstrainedTriplet, testColour.Lchab, tolerances.Lchab, $"{colourName} -> LCHab"); + AssertTriplet(unicolour.Lchuv.ConstrainedTriplet, testColour.Lchuv, tolerances.Lchuv, $"{colourName} -> LCHuv"); + } + } + + private static void AssertTriplet(ColourTriplet actual, ColourTriplet? expected, double tolerance, string info) + { + if (expected == null) return; + AssertUtils.AssertTriplet(actual, expected, tolerance, info); + } + + private static void PrintExclusion(string colourName, string excludedTestName, string reasons) + { + if (!PrintExclusions) return; + Console.WriteLine($"Excluded test colour {colourName} -> {excludedTestName}, because: {reasons}"); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/Factories/OpenCvCsvFactory.cs b/Unicolour.Tests/OtherLibraries/OpenCvCsvFactory.cs similarity index 98% rename from Unicolour.Tests/Factories/OpenCvCsvFactory.cs rename to Unicolour.Tests/OtherLibraries/OpenCvCsvFactory.cs index 3bcc22dc..93d93eb2 100644 --- a/Unicolour.Tests/Factories/OpenCvCsvFactory.cs +++ b/Unicolour.Tests/OtherLibraries/OpenCvCsvFactory.cs @@ -1,4 +1,4 @@ -namespace Wacton.Unicolour.Tests.Factories; +namespace Wacton.Unicolour.Tests.OtherLibraries; using System; using System.Collections.Generic; diff --git a/Unicolour.Tests/Factories/OpenCvFactory.cs b/Unicolour.Tests/OtherLibraries/OpenCvFactory.cs similarity index 98% rename from Unicolour.Tests/Factories/OpenCvFactory.cs rename to Unicolour.Tests/OtherLibraries/OpenCvFactory.cs index c57f39be..ca95f0f4 100644 --- a/Unicolour.Tests/Factories/OpenCvFactory.cs +++ b/Unicolour.Tests/OtherLibraries/OpenCvFactory.cs @@ -1,4 +1,4 @@ -namespace Wacton.Unicolour.Tests.Factories; +namespace Wacton.Unicolour.Tests.OtherLibraries; using System; using System.Collections.Generic; @@ -7,7 +7,7 @@ namespace Wacton.Unicolour.Tests.Factories; using Wacton.Unicolour.Tests.Utils; /* - * OpenCV does not support HWB / xyY / LCHab / LCHuv / HSLuv / HPLuv / ICtCp / Jzazbz / Jzczhz / Oklab / Oklch / CAM02 / CAM16 + * OpenCV does not support HWB / xyY / LCHab / LCHuv / HSLuv / HPLuv / ICtCp / Jzazbz / Jzczhz / Oklab / Oklch / CAM02 / CAM16 / HCT * OpenCV does not expose linear RGB * OpenCV RGB -> XYZ expects to linear RGB values (that have not been companded) * OpenCV XYZ -> RGB actually converts to linear RGB (not companded) diff --git a/Unicolour.Tests/Factories/SixLaborsFactory.cs b/Unicolour.Tests/OtherLibraries/SixLaborsFactory.cs similarity index 99% rename from Unicolour.Tests/Factories/SixLaborsFactory.cs rename to Unicolour.Tests/OtherLibraries/SixLaborsFactory.cs index 4a37a85c..91dd82ec 100644 --- a/Unicolour.Tests/Factories/SixLaborsFactory.cs +++ b/Unicolour.Tests/OtherLibraries/SixLaborsFactory.cs @@ -1,4 +1,4 @@ -namespace Wacton.Unicolour.Tests.Factories; +namespace Wacton.Unicolour.Tests.OtherLibraries; using System; using System.Collections.Generic; @@ -18,7 +18,7 @@ namespace Wacton.Unicolour.Tests.Factories; using SixLaborsIlluminants = SixLabors.ImageSharp.ColorSpaces.Illuminants; /* - * SixLabors does not support HWB / HSLuv / HPLuv / ICtCp / Jzazbz / Jzczhz / Oklab / Oklch / CAM02 / CAM16 + * SixLabors does not support HWB / HSLuv / HPLuv / ICtCp / Jzazbz / Jzczhz / Oklab / Oklch / CAM02 / CAM16 / HCT * SixLabors does not do a great job of converting to or from LAB / LUV * SixLabors does not handle very small RGB -> HSB / HSL * SixLabors produces unexpected results for XYZ -> HSB / HSL due to clamping RGB during conversion diff --git a/Unicolour.Tests/OtherLibraryTests.cs b/Unicolour.Tests/OtherLibraryTests.cs deleted file mode 100644 index 45750e59..00000000 --- a/Unicolour.Tests/OtherLibraryTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -namespace Wacton.Unicolour.Tests; - -using System; -using NUnit.Framework; -using Wacton.Unicolour.Tests.Factories; -using Wacton.Unicolour.Tests.Utils; - -public class OtherLibraryTests -{ - private const bool PrintExclusions = true; - - private static readonly ITestColourFactory OpenCvFactory = new OpenCvFactory(); - private static readonly ITestColourFactory ColourfulFactory = new ColourfulFactory(); - private static readonly ITestColourFactory ColorMineFactory = new ColorMineFactory(); - private static readonly ITestColourFactory SixLaborsFactory = new SixLaborsFactory(); - - private delegate Unicolour UnicolourFromTuple((double first, double second, double third) tuple, double alpha = 1.0); - private delegate TestColour TestColourFromTuple(ColourTriplet triplet); - - /* - * OPENCV: - * not testing from HWB / xyY / LCHab / LCHuv / HSLuv / HPLuv / ICtCp / JzAzBz / JzCzHz / Oklab / Oklch / CAM02 / CAM16 --- does not support them - * (also I've given up trying to make OpenCvSharp work in a dockerised unix environment...) - */ - [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] - public void OpenCvWindowsNamed(TestColour namedColour) => RunIfWindows(() => AssertFromHex(namedColour.Hex!, OpenCvFactory)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] - public void OpenCvWindowsHex(string hex) => RunIfWindows(() => AssertFromHex(hex, OpenCvFactory)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] - public void OpenCvWindowsRgb255(ColourTriplet triplet) => RunIfWindows(() => AssertFromRgb255(triplet, OpenCvFactory)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] - public void OpenCvWindowsRgb(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromRgb, OpenCvFactory.FromRgb)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] - public void OpenCvWindowsHsb(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromHsb, OpenCvFactory.FromHsb)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] - public void OpenCvWindowsHsl(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromHsl, OpenCvFactory.FromHsl)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] - public void OpenCvWindowsXyz(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromXyz, OpenCvFactory.FromXyz)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] - public void OpenCvWindowsLab(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromLab, OpenCvFactory.FromLab)); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LuvTriplets))] - public void OpenCvWindowsLuv(ColourTriplet triplet) => RunIfWindows(() => AssertTriplet(triplet, Unicolour.FromLuv, OpenCvFactory.FromLuv)); - - // in order to test OpenCV in a non-windows environment, this looks up a stored precomputed value - [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] - public void OpenCvCrossPlatform(TestColour namedColour) => AssertFromCsvData(namedColour.Hex!, namedColour.Name!); - - /* - * COLOURFUL: - * not testing from HSB / HSL / HWB / HSLuv / HPLuv / ICtCp / Oklab / Oklch / CAM02 / CAM16 --- does not support them - * not testing from LUV / LCHuv --- appears to give wrong values (XYZ clamping?) - * not testing JzAzBz / JzCzHz --- generates different values, due to multiplying XYZ by different values - * (Jzazbz paper is ambiguous about XYZ input, more details here https://github.com/nschloe/colorio/issues/41 - Unicolour aims to match plots of colour datasets like Colorio) - */ - [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] - public void ColourfulNamed(TestColour namedColour) => AssertFromHex(namedColour.Hex!, ColourfulFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] - public void ColourfulHex(string hex) => AssertFromHex(hex, ColourfulFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] - public void ColourfulRgb255(ColourTriplet triplet) => AssertFromRgb255(triplet, ColourfulFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] - public void ColourfulRgb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromRgb, ColourfulFactory.FromRgb); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] - public void ColourfulXyz(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyz, ColourfulFactory.FromXyz); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))] - public void ColourfulXyy(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyy, ColourfulFactory.FromXyy); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] - public void ColourfulLab(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromLab, ColourfulFactory.FromLab); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchabTriplets))] - public void ColourfulLchab(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromLchab, ColourfulFactory.FromLchab); - - /* COLORMINE: - * not testing from HWB / LCHuv / HSLuv / HPLuv / ICtCp / JzAzBz / JzCzHz / Oklab / Oklch / CAM02 / CAM16 --- does not support them - * not testing from RGB [0-1] --- RGB only accepts 0-255 - * not testing from XYZ / xyY / LAB / LCHab / LUV --- does a terrible job - */ - [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] - public void ColorMineNamed(TestColour namedColour) => AssertFromHex(namedColour.Hex!, ColorMineFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] - public void ColorMineHex(string hex) => AssertFromHex(hex, ColorMineFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] - public void ColorMineRgb255(ColourTriplet triplet) => AssertFromRgb255(triplet, ColorMineFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] - public void ColorMineHsb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsb, ColorMineFactory.FromHsb); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] - public void ColorMineHsl(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsl, ColorMineFactory.FromHsl); - - /* - * SIXLABORS: - * not testing from HWB / LCHab / HSLuv / HPLuv / ICtCp / JzAzBz / JzCzHz / Oklab / Oklch / CAM02 / CAM16 --- does not support them - * not testing from LAB / LUV / LCHuv --- appears to give wrong values (XYZ clamping?) - */ - [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] - public void SixLaborsNamed(TestColour namedColour) => AssertFromHex(namedColour.Hex!, SixLaborsFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HexStrings))] - public void SixLaborsHex(string hex) => AssertFromHex(hex, SixLaborsFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] - public void SixLaborsRgb255(ColourTriplet triplet) => AssertFromRgb255(triplet, SixLaborsFactory); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] - public void SixLaborsRgb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromRgb, SixLaborsFactory.FromRgb); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] - public void SixLaborsHsb(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsb, SixLaborsFactory.FromHsb); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] - public void SixLaborsHsl(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromHsl, SixLaborsFactory.FromHsl); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] - public void SixLaborsXyz(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyz, SixLaborsFactory.FromXyz); - - [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))] - public void SixLaborsXyy(ColourTriplet triplet) => AssertTriplet(triplet, Unicolour.FromXyy, SixLaborsFactory.FromXyy); - - private static void RunIfWindows(Action action) - { - bool IsWindows() => Environment.OSVersion.Platform == PlatformID.Win32NT; - Assume.That(IsWindows()); - action(); - } - - private static void AssertTriplet(ColourTriplet triplet, UnicolourFromTuple getUnicolour, TestColourFromTuple getTestColour) - { - var unicolour = getUnicolour(triplet.Tuple); - var testColour = getTestColour(triplet); - AssertTestColour(unicolour, testColour); - } - - private static void AssertFromHex(string hex, ITestColourFactory testColourFactory) - { - var unicolour = Unicolour.FromHex(hex); - var (r255, g255, b255, _) = SystemColorUtils.HexToRgb255(hex); - var testColour = testColourFactory.FromRgb255(r255, g255, b255, $"HEX [{hex}]"); - AssertHex(unicolour, hex); - AssertTestColour(unicolour, testColour); - } - - private static void AssertFromRgb255(ColourTriplet triplet, ITestColourFactory testColourFactory) - { - var (first, second, third) = triplet; - var r255 = (int)first; - var g255 = (int)second; - var b255 = (int)third; - var unicolour = Unicolour.FromRgb255(r255, g255, b255); - var testColour = testColourFactory.FromRgb255(r255, g255, b255); - AssertTestColour(unicolour, testColour); - } - - private static void AssertFromCsvData(string hex, string name) - { - var unicolour = Unicolour.FromHex(hex); - var otherLibColour = OpenCvCsvFactory.FromName(name); - AssertHex(unicolour, hex); - AssertTestColour(unicolour, otherLibColour); - } - - private static void AssertHex(Unicolour unicolour, string hex) - { - var hasAlpha = hex.Length is 8 or 9; - var expectedHex = hasAlpha ? hex[..^2] : hex; - var expectedA = hasAlpha ? hex.Substring(hex.Length - 2, 2) : "FF"; - Assert.That(unicolour.Hex.Contains(expectedHex.ToUpper())); - Assert.That(unicolour.Alpha.Hex, Is.EqualTo(expectedA.ToUpper())); - } - - private static void AssertTestColour(Unicolour unicolour, TestColour testColour) - { - var colourName = testColour.Name; - var tolerances = testColour.Tolerances; - if (colourName == null) throw new ArgumentException("Malformed test colour: no name"); - if (tolerances == null) throw new ArgumentException("Malformed test colour: no tolerances"); - - if (testColour.ExcludeFromAllTests) - { - PrintExclusion(colourName, "all colour spaces", string.Join(", ", testColour.ExcludeFromAllTestReasons)); - return; - } - - var unicolourRgb = testColour.IsRgbConstrained ? unicolour.Rgb.ConstrainedTriplet : unicolour.Rgb.Triplet; - var unicolourRgbLinear = testColour.IsRgbLinearConstrained ? unicolour.Rgb.Linear.ConstrainedTriplet : unicolour.Rgb.Linear.Triplet; - AssertTriplet(unicolourRgb, testColour.Rgb, tolerances.Rgb, $"{colourName} -> RGB"); - AssertTriplet(unicolourRgbLinear, testColour.RgbLinear, tolerances.RgbLinear, $"{colourName} -> RGB Linear"); - AssertTriplet(unicolour.Xyz.Triplet, testColour.Xyz, tolerances.Xyz, $"{colourName} -> XYZ"); - AssertTriplet(unicolour.Lab.Triplet, testColour.Lab, tolerances.Lab, $"{colourName} -> LAB"); - AssertTriplet(unicolour.Luv.Triplet, testColour.Luv, tolerances.Luv, $"{colourName} -> LUV"); - - if (testColour.ExcludeFromXyyTests) - { - PrintExclusion(colourName, "xyY", string.Join(", ", testColour.ExcludeFromXyyTestReasons)); - } - else - { - AssertTriplet(unicolour.Xyy.Triplet, testColour.Xyy, tolerances.Xyy, $"{colourName} -> xyY"); - } - - if (testColour.ExcludeFromHsxTests) - { - PrintExclusion(colourName, "HSB/HSL", string.Join(", ", testColour.ExcludeFromHsxTestReasons)); - } - else - { - AssertTriplet(unicolour.Hsb.ConstrainedTriplet, testColour.Hsb, tolerances.Hsb, $"{colourName} -> HSB"); - AssertTriplet(unicolour.Hsl.ConstrainedTriplet, testColour.Hsl, tolerances.Hsl, $"{colourName} -> HSL"); - } - - if (testColour.ExcludeFromLchTests) - { - PrintExclusion(colourName, "LCH", string.Join(", ", testColour.ExcludeFromLchTestReasons)); - } - else - { - AssertTriplet(unicolour.Lchab.ConstrainedTriplet, testColour.Lchab, tolerances.Lchab, $"{colourName} -> LCHab"); - AssertTriplet(unicolour.Lchuv.ConstrainedTriplet, testColour.Lchuv, tolerances.Lchuv, $"{colourName} -> LCHuv"); - } - } - - private static void AssertTriplet(ColourTriplet actual, ColourTriplet? expected, double tolerance, string info) - { - if (expected == null) return; - AssertUtils.AssertTriplet(actual, expected, tolerance, info); - } - - private static void PrintExclusion(string colourName, string excludedTestName, string reasons) - { - if (!PrintExclusions) return; - Console.WriteLine($"Excluded test colour {colourName} -> {excludedTestName}, because: {reasons}"); - } -} \ No newline at end of file diff --git a/Unicolour.Tests/README.md b/Unicolour.Tests/README.md index 4cc392b8..9a8a7165 100644 --- a/Unicolour.Tests/README.md +++ b/Unicolour.Tests/README.md @@ -1,9 +1,10 @@ Tests to add / update for new colour spaces: - Smoke tests -- Known value conversion tests (e.g. Oklab, Hsluv) +- Known value conversion tests (if data available, e.g. Oklab, Hsluv) - Roundtrip conversion tests - Interpolation tests - Greyscale interpolation tests (if colour space has hue component) +- Hued tests (if colour space has hue component) - Equality tests - Extreme values tests - Greyscale tests diff --git a/Unicolour.Tests/RangeClampTests.cs b/Unicolour.Tests/RangeClampTests.cs index 2df3051e..7f223a07 100644 --- a/Unicolour.Tests/RangeClampTests.cs +++ b/Unicolour.Tests/RangeClampTests.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using NUnit.Framework; -public static class RangeClampTests +public class RangeClampTests { [Test] - public static void RgbRange() + public void RgbRange() { Range rRange = new(0.0, 1.0); Range gRange = new(0.0, 1.0); @@ -17,14 +17,14 @@ public static void RgbRange() AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - var representations = new[] {beyondMax, beyondMin}; + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedR, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedG, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedB, x => x.ConstrainedTriplet.Third); } [Test] - public static void RgbLinearRange() + public void RgbLinearRange() { Range rRange = new(0.0, 1.0); Range gRange = new(0.0, 1.0); @@ -33,15 +33,15 @@ public static void RgbLinearRange() var beyondMin = new RgbLinear(rRange.BeyondMin, gRange.BeyondMin, bRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedR, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedG, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedB, x => x.ConstrainedTriplet.Third); } [Test] - public static void Rgb255Range() + public void Rgb255Range() { Range rRange = new(0, 255); Range gRange = new(0, 255); @@ -50,15 +50,15 @@ public static void Rgb255Range() var beyondMin = new Rgb255(rRange.BeyondMin, gRange.BeyondMin, bRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedR, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedG, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedB, x => x.ConstrainedTriplet.Third); } [Test] - public static void HsbRange() + public void HsbRange() { Range hRange = new(0.0, 360.0); Range sRange = new(0.0, 1.0); @@ -67,15 +67,15 @@ public static void HsbRange() var beyondMin = new Hsb(hRange.BeyondMin, sRange.BeyondMin, bRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedS, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedB, x => x.ConstrainedTriplet.Third); } [Test] - public static void HslRange() + public void HslRange() { Range hRange = new(0.0, 360.0); Range sRange = new(0.0, 1.0); @@ -84,15 +84,15 @@ public static void HslRange() var beyondMin = new Hsl(hRange.BeyondMin, sRange.BeyondMin, lRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedS, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedL, x => x.ConstrainedTriplet.Third); } [Test] - public static void HwbRange() + public void HwbRange() { Range hRange = new(0.0, 360.0); Range wRange = new(0.0, 1.0); @@ -101,41 +101,41 @@ public static void HwbRange() var beyondMin = new Hwb(hRange.BeyondMin, wRange.BeyondMin, bRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedW, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedB, x => x.ConstrainedTriplet.Third); } [Test] - public static void LchabRange() // only the hue is constrained + public void LchabRange() // only the hue is constrained { Range hRange = new(0.0, 360.0); var beyondMax = new Lchab(100, 230, hRange.BeyondMax); var beyondMin = new Lchab(0, 0, hRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet.Third, beyondMax.Triplet.Third); AssertConstrained(beyondMin.Triplet.Third, beyondMin.ConstrainedTriplet.Third); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.Third); } [Test] - public static void LchuvRange() // only the hue is constrained + public void LchuvRange() // only the hue is constrained { Range hRange = new(0.0, 360.0); var beyondMax = new Lchuv(100, 230, hRange.BeyondMax); var beyondMin = new Lchuv(0, 0, hRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet.Third, beyondMax.Triplet.Third); AssertConstrained(beyondMin.Triplet.Third, beyondMin.ConstrainedTriplet.Third); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.Third); } [Test] - public static void HsluvRange() + public void HsluvRange() { Range hRange = new(0.0, 360.0); Range sRange = new(0.0, 100.0); @@ -144,15 +144,15 @@ public static void HsluvRange() var beyondMin = new Hsluv(hRange.BeyondMin, sRange.BeyondMin, lRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedS, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedL, x => x.ConstrainedTriplet.Third); } [Test] - public static void HpluvRange() + public void HpluvRange() { Range hRange = new(0.0, 360.0); Range sRange = new(0.0, 100.0); @@ -161,38 +161,51 @@ public static void HpluvRange() var beyondMin = new Hpluv(hRange.BeyondMin, sRange.BeyondMin, lRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet, beyondMax.Triplet); AssertConstrained(beyondMin.Triplet, beyondMin.ConstrainedTriplet); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.First); AssertConstrainedValue(representations, x => x.ConstrainedS, x => x.ConstrainedTriplet.Second); AssertConstrainedValue(representations, x => x.ConstrainedL, x => x.ConstrainedTriplet.Third); } [Test] - public static void JzczhzRange() // only the hue is constrained + public void JzczhzRange() // only the hue is constrained { Range hRange = new(0.0, 360.0); var beyondMax = new Jzczhz(1, 0.1, hRange.BeyondMax); var beyondMin = new Jzczhz(0, 0, hRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet.Third, beyondMax.Triplet.Third); AssertConstrained(beyondMin.Triplet.Third, beyondMin.ConstrainedTriplet.Third); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.Third); } [Test] - public static void OklchRange() // only the hue is constrained + public void OklchRange() // only the hue is constrained { Range hRange = new(0.0, 360.0); var beyondMax = new Oklch(100, 230, hRange.BeyondMax); var beyondMin = new Oklch(0, 0, hRange.BeyondMin); AssertConstrained(beyondMax.ConstrainedTriplet.Third, beyondMax.Triplet.Third); AssertConstrained(beyondMin.Triplet.Third, beyondMin.ConstrainedTriplet.Third); - - var representations = new[] {beyondMax, beyondMin}; + + var representations = new[] { beyondMax, beyondMin }; AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.Third); } + + [Test] + public void HctRange() // only the hue is constrained + { + Range hRange = new(0.0, 360.0); + var beyondMax = new Hct(hRange.BeyondMax, 100, 100); + var beyondMin = new Hct(hRange.BeyondMin, 0, 0); + AssertConstrained(beyondMax.ConstrainedTriplet.First, beyondMax.Triplet.First); + AssertConstrained(beyondMin.Triplet.First, beyondMin.ConstrainedTriplet.First); + + var representations = new[] { beyondMax, beyondMin }; + AssertConstrainedValue(representations, x => x.ConstrainedH, x => x.ConstrainedTriplet.First); + } private static void AssertConstrained(ColourTriplet lesser, ColourTriplet greater) { diff --git a/Unicolour.Tests/RoundtripCam02Tests.cs b/Unicolour.Tests/RoundtripCam02Tests.cs new file mode 100644 index 00000000..b61815d1 --- /dev/null +++ b/Unicolour.Tests/RoundtripCam02Tests.cs @@ -0,0 +1,29 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripCam02Tests +{ + private const double Tolerance = 0.00005; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + private static readonly CamConfiguration CamConfig = CamConfiguration.StandardRgb; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Cam02Triplets))] + public void ViaXyz(ColourTriplet triplet) + { + // CAM <-> XYZ can produce NaNs due to a negative number to a fractional power in the conversion process + var original = new Cam02(triplet.First, triplet.Second, triplet.Third, CamConfig); + var roundtrip = Cam02.FromXyz(Cam02.ToXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.IsNaN ? ViaCamWithNaN(roundtrip.Triplet) : original.Triplet, Tolerance); + } + + // when NaNs occur during CAM <-> XYZ conversion + // if the NaN occurs during CAM -> XYZ: all value are NaN + // if the NaN occurs during XYZ -> CAM: J, H, Q have values and C, M, S are NaN - J is the first item of the triplet + private static ColourTriplet ViaCamWithNaN(ColourTriplet triplet) + { + var first = double.IsNaN(triplet.First) ? double.NaN : triplet.First; + return new(first, double.NaN, double.NaN); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripCam16Tests.cs b/Unicolour.Tests/RoundtripCam16Tests.cs new file mode 100644 index 00000000..0dc550d7 --- /dev/null +++ b/Unicolour.Tests/RoundtripCam16Tests.cs @@ -0,0 +1,29 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripCam16Tests +{ + private const double Tolerance = 0.00005; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + private static readonly CamConfiguration CamConfig = CamConfiguration.StandardRgb; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Cam16Triplets))] + public void ViaXyz(ColourTriplet triplet) + { + // CAM <-> XYZ can produce NaNs due to a negative number to a fractional power in the conversion process + var original = new Cam16(triplet.First, triplet.Second, triplet.Third, CamConfig); + var roundtrip = Cam16.FromXyz(Cam16.ToXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.IsNaN ? ViaCamWithNaN(roundtrip.Triplet) : original.Triplet, Tolerance); + } + + // when NaNs occur during CAM <-> XYZ conversion + // if the NaN occurs during CAM -> XYZ: all value are NaN + // if the NaN occurs during XYZ -> CAM: J, H, Q have values and C, M, S are NaN - J is the first item of the triplet + private static ColourTriplet ViaCamWithNaN(ColourTriplet triplet) + { + var first = double.IsNaN(triplet.First) ? double.NaN : triplet.First; + return new(first, double.NaN, double.NaN); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripHctTests.cs b/Unicolour.Tests/RoundtripHctTests.cs new file mode 100644 index 00000000..ef145bf2 --- /dev/null +++ b/Unicolour.Tests/RoundtripHctTests.cs @@ -0,0 +1,41 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripHctTests +{ + private const double Tolerance = 0.00005; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HctTriplets))] + public void ViaXyz(ColourTriplet triplet) + { + var original = new Hct(triplet.First, triplet.Second, triplet.Third); + var xyz = Hct.ToXyz(original, XyzConfig); + var roundtrip = Hct.FromXyz(xyz, XyzConfig); + + // very rarely, HCT -> XYZ fails to converge, in which case it's assumed there is no suitable XYZ + // so far, after 100,000s of randomly generated tests, only seen where HCT has high C value or low T value + if (!xyz.HctToXyzSearchResult!.Converged) + { + Assert.That(original.C > 88 || original.T < 12, Is.True); + AssertUtils.AssertTriplet(roundtrip.Triplet, new(double.NaN, double.NaN, double.NaN), Tolerance); + return; + } + + // slightly less rarely, sometimes HCT -> XYZ converges + // but results in an XYZ that produces a NaN during XYZ -> CAM16 due to negative number to a fractional power + // which presents as at least a NaN CAM16.Model.C, while LAB.L is calculated correctly + if (roundtrip.IsNaN) + { + var cam16 = Hct.Cam16Component(xyz); + var lab = Hct.LabComponent(xyz); + Assert.That(cam16.Model.C, Is.NaN); + Assert.That(lab.L, Is.NaN.Or.EqualTo(original.T).Within(Tolerance)); + return; + } + + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripHpluvTests.cs b/Unicolour.Tests/RoundtripHpluvTests.cs new file mode 100644 index 00000000..b685f51e --- /dev/null +++ b/Unicolour.Tests/RoundtripHpluvTests.cs @@ -0,0 +1,17 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripHpluvTests +{ + private const double Tolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HpluvTriplets))] + public void ViaLchuv(ColourTriplet triplet) + { + var original = new Hpluv(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hpluv.FromLchuv(Hpluv.ToLchuv(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripHsbTests.cs b/Unicolour.Tests/RoundtripHsbTests.cs new file mode 100644 index 00000000..87f7fbc1 --- /dev/null +++ b/Unicolour.Tests/RoundtripHsbTests.cs @@ -0,0 +1,49 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripHsbTests +{ + private const double Tolerance = 0.000000001; + private static readonly RgbConfiguration RgbConfig = RgbConfiguration.StandardRgb; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] + public void ViaRgb(ColourTriplet triplet) => AssertViaRgb(triplet); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void ViaRgbNamed(TestColour namedColour) => AssertViaRgb(namedColour.Hsb!); + + private static void AssertViaRgb(ColourTriplet triplet) + { + var original = new Hsb(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hsb.FromRgb(Hsb.ToRgb(original, RgbConfig)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] + public void ViaHsl(ColourTriplet triplet) => AssertViaHsl(triplet); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void ViaHslNamed(TestColour namedColour) => AssertViaHsl(namedColour.Hsb!); + + private static void AssertViaHsl(ColourTriplet triplet) + { + var original = new Hsb(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hsl.ToHsb(Hsl.FromHsb(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsbTriplets))] + public void ViaHwb(ColourTriplet triplet) => AssertViaHwb(triplet); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void ViaHwbNamed(TestColour namedColour) => AssertViaHwb(namedColour.Hsb!); + + private static void AssertViaHwb(ColourTriplet triplet) + { + var original = new Hsb(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hwb.ToHsb(Hwb.FromHsb(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripHslTests.cs b/Unicolour.Tests/RoundtripHslTests.cs new file mode 100644 index 00000000..b9891ea7 --- /dev/null +++ b/Unicolour.Tests/RoundtripHslTests.cs @@ -0,0 +1,22 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripHslTests +{ + private const double Tolerance = 0.0000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HslTriplets))] + public void ViaHsb(ColourTriplet triplet) => AssertViaHsb(triplet); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void ViaHsbNamed(TestColour namedColour) => AssertViaHsb(namedColour.Hsl!); + + private static void AssertViaHsb(ColourTriplet triplet) + { + var original = new Hsl(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hsl.FromHsb(Hsl.ToHsb(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripHsluvTests.cs b/Unicolour.Tests/RoundtripHsluvTests.cs new file mode 100644 index 00000000..74922703 --- /dev/null +++ b/Unicolour.Tests/RoundtripHsluvTests.cs @@ -0,0 +1,17 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripHsluvTests +{ + private const double Tolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HsluvTriplets))] + public void ViaLchuv(ColourTriplet triplet) + { + var original = new Hsluv(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hsluv.FromLchuv(Hsluv.ToLchuv(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripHwbTests.cs b/Unicolour.Tests/RoundtripHwbTests.cs new file mode 100644 index 00000000..f59e8a87 --- /dev/null +++ b/Unicolour.Tests/RoundtripHwbTests.cs @@ -0,0 +1,33 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripHwbTests +{ + private const double Tolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.HwbTriplets))] + public void ViaHsb(ColourTriplet triplet) + { + // note: cannot test round trip of all HWB values as HWB <-> HSB is not 1:1 + // since when HWB W + B > 100%, it is the same as another HWB where W + B = 100% + // (e.g. W 100 B 50 == W 66.666 B 33.333) + // and HSB -> HWB will always produce HWB that results in W + B <= 100% + var original = new Hwb(triplet.First, triplet.Second, triplet.Third); + var scale = original.ConstrainedW + original.ConstrainedB; + var scaled = new Hwb(original.H, original.ConstrainedW / scale, original.ConstrainedB / scale); + + var needsScaling = scale > 1.0; + if (needsScaling) + { + var roundtripFromOriginal = Hwb.ToHsb(original); + var roundtripFromScaled = Hwb.ToHsb(scaled); + AssertUtils.AssertTriplet(roundtripFromOriginal.Triplet, roundtripFromScaled.Triplet, Tolerance); + } + + var roundtrip = Hwb.FromHsb(Hwb.ToHsb(original)); + var expected = needsScaling ? scaled.Triplet : original.Triplet; + AssertUtils.AssertTriplet(roundtrip.Triplet, expected, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripIctcpTests.cs b/Unicolour.Tests/RoundtripIctcpTests.cs new file mode 100644 index 00000000..5c4a6e09 --- /dev/null +++ b/Unicolour.Tests/RoundtripIctcpTests.cs @@ -0,0 +1,20 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripIctcpTests +{ + private const double DefaultTolerance = 0.00000000001; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + private const double IctcpScalar = 100; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.IctcpTriplets))] + public void ViaXyz(ColourTriplet triplet) + { + // Ictcp -> XYZ can produce NaNs due to a negative number to a fractional power in the conversion process + var original = new Ictcp(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Ictcp.FromXyz(Ictcp.ToXyz(original, IctcpScalar, XyzConfig), IctcpScalar, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.IsNaN ? new(double.NaN, double.NaN, double.NaN) : original.Triplet, DefaultTolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripJzazbzTests.cs b/Unicolour.Tests/RoundtripJzazbzTests.cs new file mode 100644 index 00000000..bd0cfee4 --- /dev/null +++ b/Unicolour.Tests/RoundtripJzazbzTests.cs @@ -0,0 +1,21 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripJzazbzTests +{ + private const double Tolerance = 0.00000005; + + // cannot test roundtrip via XYZ as Jzazbz <-> XYZ is not 1:1, e.g. + // - when Jzazbz inputs produces negative XYZ values, which are clamped during XYZ -> Jzazbz + // - when Jzazbz negative inputs trigger a negative number to a fractional power, producing NaNs + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.JzazbzTriplets))] + public void ViaJzczhz(ColourTriplet triplet) + { + var original = new Jzazbz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Jzczhz.ToJzazbz(Jzczhz.FromJzazbz(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripJzczhzTests.cs b/Unicolour.Tests/RoundtripJzczhzTests.cs new file mode 100644 index 00000000..74817c9a --- /dev/null +++ b/Unicolour.Tests/RoundtripJzczhzTests.cs @@ -0,0 +1,17 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripJzczhzTests +{ + private const double Tolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.JzczhzTriplets))] + public void ViaJzazbz(ColourTriplet triplet) + { + var original = new Jzczhz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Jzczhz.FromJzazbz(Jzczhz.ToJzazbz(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripLabTests.cs b/Unicolour.Tests/RoundtripLabTests.cs new file mode 100644 index 00000000..094a1212 --- /dev/null +++ b/Unicolour.Tests/RoundtripLabTests.cs @@ -0,0 +1,26 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripLabTests +{ + private const double Tolerance = 0.00000000001; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] + public void ViaXyz(ColourTriplet triplet) + { + var original = new Lab(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Lab.FromXyz(Lab.ToXyz(original, XyzConfig), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LabTriplets))] + public void ViaLchab(ColourTriplet triplet) + { + var original = new Lab(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Lchab.ToLab(Lchab.FromLab(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripLchabTests.cs b/Unicolour.Tests/RoundtripLchabTests.cs new file mode 100644 index 00000000..3ed7cff9 --- /dev/null +++ b/Unicolour.Tests/RoundtripLchabTests.cs @@ -0,0 +1,17 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripLchabTests +{ + private const double Tolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchabTriplets))] + public void ViaLab(ColourTriplet triplet) + { + var original = new Lchab(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Lchab.FromLab(Lchab.ToLab(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripLchuvTests.cs b/Unicolour.Tests/RoundtripLchuvTests.cs new file mode 100644 index 00000000..67bbee51 --- /dev/null +++ b/Unicolour.Tests/RoundtripLchuvTests.cs @@ -0,0 +1,33 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripLchuvTests +{ + private const double Tolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchuvTriplets))] + public void ViaLuv(ColourTriplet triplet) + { + var original = new Lchuv(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Lchuv.FromLuv(Lchuv.ToLuv(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchuvTriplets))] + public void ViaHsluv(ColourTriplet triplet) + { + var original = new Lchuv(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hsluv.ToLchuv(Hsluv.FromLchuv(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LchuvTriplets))] + public void ViaHpluv(ColourTriplet triplet) + { + var original = new Lchuv(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hpluv.ToLchuv(Hpluv.FromLchuv(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripLuvTests.cs b/Unicolour.Tests/RoundtripLuvTests.cs new file mode 100644 index 00000000..2b82c0b8 --- /dev/null +++ b/Unicolour.Tests/RoundtripLuvTests.cs @@ -0,0 +1,26 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripLuvTests +{ + private const double Tolerance = 0.00000001; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LuvTriplets))] + public void ViaXyz(ColourTriplet triplet) + { + var original = new Luv(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Luv.FromXyz(Luv.ToXyz(original, XyzConfig), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.LuvTriplets))] + public void ViaLchuv(ColourTriplet triplet) + { + var original = new Luv(triplet.First, triplet.Second, triplet.Third); + var viaLchuv = Lchuv.ToLuv(Lchuv.FromLuv(original)); + AssertUtils.AssertTriplet(viaLchuv.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripOklabTests.cs b/Unicolour.Tests/RoundtripOklabTests.cs new file mode 100644 index 00000000..e51cd299 --- /dev/null +++ b/Unicolour.Tests/RoundtripOklabTests.cs @@ -0,0 +1,26 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripOklabTests +{ + private const double Tolerance = 0.000005; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.OklabTriplets))] + public void ViaXyz(ColourTriplet triplet) + { + var original = new Oklab(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Oklab.FromXyz(Oklab.ToXyz(original, XyzConfig), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.OklabTriplets))] + public void ViaOklch(ColourTriplet triplet) + { + var original = new Oklab(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Oklch.ToOklab(Oklch.FromOklab(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripOklchTests.cs b/Unicolour.Tests/RoundtripOklchTests.cs new file mode 100644 index 00000000..0b218e6d --- /dev/null +++ b/Unicolour.Tests/RoundtripOklchTests.cs @@ -0,0 +1,17 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripOklchTests +{ + private const double DefaultTolerance = 0.00000000001; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.OklchTriplets))] + public void ViaOklab(ColourTriplet triplet) + { + var original = new Oklch(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Oklch.FromOklab(Oklch.ToOklab(original)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, DefaultTolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripRgbTests.cs b/Unicolour.Tests/RoundtripRgbTests.cs new file mode 100644 index 00000000..91735227 --- /dev/null +++ b/Unicolour.Tests/RoundtripRgbTests.cs @@ -0,0 +1,65 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripRgbTests +{ + private const double Tolerance = 0.00000005; + private static readonly RgbConfiguration RgbConfig = RgbConfiguration.StandardRgb; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] + public void ViaHsb(ColourTriplet triplet) => AssertViaHsb(triplet); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] + public void ViaHsb255(ColourTriplet triplet) => AssertViaHsb(GetNormalisedRgb255Triplet(triplet)); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void ViaHsbNamed(TestColour namedColour) => AssertViaHsb(GetRgbTripletFromHex(namedColour.Hex!)); + + private static void AssertViaHsb(ColourTriplet triplet) + { + var original = new Rgb(triplet.First, triplet.Second, triplet.Third, RgbConfig); + var roundtrip = Hsb.ToRgb(Hsb.FromRgb(original), RgbConfig); + AssertRoundtrip(original, roundtrip); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.RgbTriplets))] + public void ViaXyz(ColourTriplet triplet) => AssertViaXyz(triplet); + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.Rgb255Triplets))] + public void ViaXyz255(ColourTriplet triplet) => AssertViaXyz(GetNormalisedRgb255Triplet(triplet)); + + [TestCaseSource(typeof(NamedColours), nameof(NamedColours.All))] + public void ViaXyzNamed(TestColour namedColour) => AssertViaXyz(GetRgbTripletFromHex(namedColour.Hex!)); + + private static void AssertViaXyz(ColourTriplet triplet) + { + var original = new Rgb(triplet.First, triplet.Second, triplet.Third, RgbConfig); + var roundtrip = Rgb.FromXyz(Rgb.ToXyz(original, RgbConfig, XyzConfig), RgbConfig, XyzConfig); + AssertRoundtrip(original, roundtrip); + } + + private static void AssertRoundtrip(Rgb original, Rgb roundtrip) + { + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + AssertUtils.AssertTriplet(roundtrip.ConstrainedTriplet, original.ConstrainedTriplet, Tolerance); + AssertUtils.AssertTriplet(roundtrip.Linear.Triplet, original.Linear.Triplet, Tolerance); + AssertUtils.AssertTriplet(roundtrip.Linear.ConstrainedTriplet, original.Linear.ConstrainedTriplet, Tolerance); + AssertUtils.AssertTriplet(roundtrip.Byte255.Triplet, original.Byte255.Triplet, Tolerance); + AssertUtils.AssertTriplet(roundtrip.Byte255.ConstrainedTriplet, original.Byte255.ConstrainedTriplet, Tolerance); + } + + 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 ColourTriplet GetNormalisedRgb255Triplet(ColourTriplet triplet) + { + 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/RoundtripXyyTests.cs b/Unicolour.Tests/RoundtripXyyTests.cs new file mode 100644 index 00000000..171e33ee --- /dev/null +++ b/Unicolour.Tests/RoundtripXyyTests.cs @@ -0,0 +1,18 @@ +namespace Wacton.Unicolour.Tests; + +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripXyyTests +{ + private const double Tolerance = 0.0000000005; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))] + public void XyyRoundTrip(ColourTriplet triplet) + { + var original = new Xyy(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Xyy.FromXyz(Xyy.ToXyz(original), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/RoundtripXyzTests.cs b/Unicolour.Tests/RoundtripXyzTests.cs new file mode 100644 index 00000000..f495dd6b --- /dev/null +++ b/Unicolour.Tests/RoundtripXyzTests.cs @@ -0,0 +1,121 @@ +namespace Wacton.Unicolour.Tests; + +using System; +using NUnit.Framework; +using Wacton.Unicolour.Tests.Utils; + +public class RoundtripXyzTests +{ + private const double Tolerance = 0.0000000005; + private static readonly RgbConfiguration RgbConfig = RgbConfiguration.StandardRgb; + private static readonly XyzConfiguration XyzConfig = XyzConfiguration.D65; + private static readonly CamConfiguration CamConfig = CamConfiguration.StandardRgb; + private const double IctcpScalar = 100; + private const double JzazbzScalar = 100; + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaRgb(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Rgb.ToXyz(Rgb.FromXyz(original, RgbConfig, XyzConfig), RgbConfig, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaXyy(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Xyy.ToXyz(Xyy.FromXyz(original, XyzConfig)); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaLab(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Lab.ToXyz(Lab.FromXyz(original, XyzConfig), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaLuv(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Luv.ToXyz(Luv.FromXyz(original, XyzConfig), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaIctcp(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Ictcp.ToXyz(Ictcp.FromXyz(original, IctcpScalar, XyzConfig), IctcpScalar, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaJzazbz(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Jzazbz.ToXyz(Jzazbz.FromXyz(original, JzazbzScalar, XyzConfig), JzazbzScalar, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaOklab(ColourTriplet triplet) + { + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Oklab.ToXyz(Oklab.FromXyz(original, XyzConfig), XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaCam02(ColourTriplet triplet) + { + // CAM02 -> XYZ can produce NaNs due to a negative number to a fractional power in the conversion process + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Cam02.ToXyz(Cam02.FromXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.IsNaN ? ViaCamWithNaN(roundtrip.Triplet) : original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaCam16(ColourTriplet triplet) + { + // CAM16 -> XYZ can produce NaNs due to a negative number to a fractional power in the conversion process + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Cam16.ToXyz(Cam16.FromXyz(original, CamConfig, XyzConfig), CamConfig, XyzConfig); + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.IsNaN ? ViaCamWithNaN(roundtrip.Triplet) : original.Triplet, Tolerance); + } + + [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyzTriplets))] + public void ViaHct(ColourTriplet triplet) + { + // HCT -> XYZ can produce NaNs due to a negative number to a fractional power in the CAM16 conversion process + var original = new Xyz(triplet.First, triplet.Second, triplet.Third); + var roundtrip = Hct.ToXyz(Hct.FromXyz(original, XyzConfig), XyzConfig); + + // not quite as accurate as other XYZ roundtrips, but still accurate + const double viaHctTolerance = Tolerance * 100; + if (Math.Abs(roundtrip.X - original.X) > viaHctTolerance || + Math.Abs(roundtrip.Z - original.Z) > viaHctTolerance) + { + // very rarely, XYZ roundtrip via HCT fails + // due to HCT -> XYZ finding a different CAM.Model.J that produces the target LAB.L / HCT.T + // effectively: multiple XYZ where Y is the same and results in the same CAM16.Model.H, CAM16.Model.C, LAB.L + Assert.That(roundtrip.Y, Is.EqualTo(original.Y).Within(viaHctTolerance)); + } + else + { + AssertUtils.AssertTriplet(roundtrip.Triplet, roundtrip.IsNaN ? new(double.NaN, double.NaN, double.NaN) : original.Triplet, viaHctTolerance); + } + } + + // when NaNs occur during CAM <-> XYZ conversion + // if the NaN occurs during CAM -> XYZ: all value are NaN + // if the NaN occurs during XYZ -> CAM: J, H, Q have values and C, M, S are NaN - J is the first item of the triplet + private static ColourTriplet ViaCamWithNaN(ColourTriplet triplet) + { + var first = double.IsNaN(triplet.First) ? double.NaN : triplet.First; + return new(first, double.NaN, double.NaN); + } +} \ No newline at end of file diff --git a/Unicolour.Tests/SmokeTests.cs b/Unicolour.Tests/SmokeTests.cs index 69057326..27538d6b 100644 --- a/Unicolour.Tests/SmokeTests.cs +++ b/Unicolour.Tests/SmokeTests.cs @@ -3,113 +3,118 @@ using NUnit.Framework; using Wacton.Unicolour.Tests.Utils; -public static class SmokeTests +public class SmokeTests { [TestCase("000000")] [TestCase("FFFFFF")] [TestCase("667788")] - public static void UnicolourHex(string hex) => AssertHex(hex); + public void UnicolourHex(string hex) => AssertHex(hex); [TestCase(0, 0, 0)] [TestCase(255, 255, 255)] [TestCase(124, 125, 126)] - public static void UnicolourRgb255(int r, int g, int b) => AssertRgb255(r, g, b); + public void UnicolourRgb255(int r, int g, int b) => AssertRgb255(r, g, b); [TestCase(0, 0, 0)] [TestCase(1, 1, 1)] [TestCase(0.4, 0.5, 0.6)] - public static void UnicolourRgb(double r, double g, double b) => AssertInit(r, g, b, Unicolour.FromRgb, Unicolour.FromRgb, Unicolour.FromRgb, Unicolour.FromRgb); + public void UnicolourRgb(double r, double g, double b) => AssertInit(r, g, b, Unicolour.FromRgb, Unicolour.FromRgb, Unicolour.FromRgb, Unicolour.FromRgb); [TestCase(0, 0, 0)] [TestCase(360, 1, 1)] [TestCase(180, 0.4, 0.6)] - public static void UnicolourHsb(double h, double s, double b) => AssertInit(h, s, b, Unicolour.FromHsb, Unicolour.FromHsb, Unicolour.FromHsb, Unicolour.FromHsb); + public void UnicolourHsb(double h, double s, double b) => AssertInit(h, s, b, Unicolour.FromHsb, Unicolour.FromHsb, Unicolour.FromHsb, Unicolour.FromHsb); [TestCase(0, 0, 0)] [TestCase(360, 1, 1)] [TestCase(180, 0.4, 0.6)] - public static void UnicolourHsl(double h, double s, double l) => AssertInit(h, s, l, Unicolour.FromHsl, Unicolour.FromHsl, Unicolour.FromHsl, Unicolour.FromHsl); + public void UnicolourHsl(double h, double s, double l) => AssertInit(h, s, l, Unicolour.FromHsl, Unicolour.FromHsl, Unicolour.FromHsl, Unicolour.FromHsl); [TestCase(0, 0, 0)] [TestCase(360, 1, 1)] [TestCase(180, 0.4, 0.6)] - public static void UnicolourHwb(double h, double w, double b) => AssertInit(h, w, b, Unicolour.FromHwb, Unicolour.FromHwb, Unicolour.FromHwb, Unicolour.FromHwb); + public void UnicolourHwb(double h, double w, double b) => AssertInit(h, w, b, Unicolour.FromHwb, Unicolour.FromHwb, Unicolour.FromHwb, Unicolour.FromHwb); [TestCase(0, 0, 0)] [TestCase(1, 1, 1)] [TestCase(0.4, 0.5, 0.6)] - public static void UnicolourXyz(double x, double y, double z) => AssertInit(x, y, z, Unicolour.FromXyz, Unicolour.FromXyz, Unicolour.FromXyz, Unicolour.FromXyz); + public void UnicolourXyz(double x, double y, double z) => AssertInit(x, y, z, Unicolour.FromXyz, Unicolour.FromXyz, Unicolour.FromXyz, Unicolour.FromXyz); [TestCase(0, 0, 0)] [TestCase(1, 1, 1)] [TestCase(0.4, 0.5, 0.6)] - public static void UnicolourXyy(double x, double y, double upperY) => AssertInit(x, y, upperY, Unicolour.FromXyy, Unicolour.FromXyy, Unicolour.FromXyy, Unicolour.FromXyy); + public void UnicolourXyy(double x, double y, double upperY) => AssertInit(x, y, upperY, Unicolour.FromXyy, Unicolour.FromXyy, Unicolour.FromXyy, Unicolour.FromXyy); [TestCase(0, -128, -128)] [TestCase(100, 128, 128)] [TestCase(50, -1, 1)] - public static void UnicolourLab(double l, double a, double b) => AssertInit(l, a, b, Unicolour.FromLab, Unicolour.FromLab, Unicolour.FromLab, Unicolour.FromLab); + public void UnicolourLab(double l, double a, double b) => AssertInit(l, a, b, Unicolour.FromLab, Unicolour.FromLab, Unicolour.FromLab, Unicolour.FromLab); [TestCase(0, 0, 0)] [TestCase(100, 230, 360)] [TestCase(50, 115, 180)] - public static void UnicolourLchab(double l, double c, double h) => AssertInit(l, c, h, Unicolour.FromLchab, Unicolour.FromLchab, Unicolour.FromLchab, Unicolour.FromLchab); + public void UnicolourLchab(double l, double c, double h) => AssertInit(l, c, h, Unicolour.FromLchab, Unicolour.FromLchab, Unicolour.FromLchab, Unicolour.FromLchab); [TestCase(0, -100, -100)] [TestCase(100, 100, 100)] [TestCase(50, -1, 1)] - public static void UnicolourLuv(double l, double u, double v) => AssertInit(l, u, v, Unicolour.FromLuv, Unicolour.FromLuv, Unicolour.FromLuv, Unicolour.FromLuv); + public void UnicolourLuv(double l, double u, double v) => AssertInit(l, u, v, Unicolour.FromLuv, Unicolour.FromLuv, Unicolour.FromLuv, Unicolour.FromLuv); [TestCase(0, 0, 0)] [TestCase(100, 230, 360)] [TestCase(50, 115, 180)] - public static void UnicolourLchuv(double l, double c, double h) => AssertInit(l, c, h, Unicolour.FromLchuv, Unicolour.FromLchuv, Unicolour.FromLchuv, Unicolour.FromLchuv); + public void UnicolourLchuv(double l, double c, double h) => AssertInit(l, c, h, Unicolour.FromLchuv, Unicolour.FromLchuv, Unicolour.FromLchuv, Unicolour.FromLchuv); [TestCase(0, 0, 0)] [TestCase(360, 100, 100)] [TestCase(180, 50, 50)] - public static void UnicolourHsluv(double h, double s, double l) => AssertInit(h, s, l, Unicolour.FromHsluv, Unicolour.FromHsluv, Unicolour.FromHsluv, Unicolour.FromHsluv); + public void UnicolourHsluv(double h, double s, double l) => AssertInit(h, s, l, Unicolour.FromHsluv, Unicolour.FromHsluv, Unicolour.FromHsluv, Unicolour.FromHsluv); [TestCase(0, 0, 0)] [TestCase(360, 100, 100)] [TestCase(180, 50, 50)] - public static void UnicolourHpluv(double h, double s, double l) => AssertInit(h, s, l, Unicolour.FromHpluv, Unicolour.FromHpluv, Unicolour.FromHpluv, Unicolour.FromHpluv); + public void UnicolourHpluv(double h, double s, double l) => AssertInit(h, s, l, Unicolour.FromHpluv, Unicolour.FromHpluv, Unicolour.FromHpluv, Unicolour.FromHpluv); [TestCase(0, -0.5, -0.5)] [TestCase(1, 0.5, 0.5)] [TestCase(0.5, -0.01, 0.01)] - public static void UnicolourIctcp(double i, double ct, double cp) => AssertInit(i, ct, cp, Unicolour.FromIctcp, Unicolour.FromIctcp, Unicolour.FromIctcp, Unicolour.FromIctcp); + public void UnicolourIctcp(double i, double ct, double cp) => AssertInit(i, ct, cp, Unicolour.FromIctcp, Unicolour.FromIctcp, Unicolour.FromIctcp, Unicolour.FromIctcp); [TestCase(0, -0.10, -0.16)] [TestCase(0.17, 0.11, 0.12)] [TestCase(0.085, -0.0001, 0.0001)] - public static void UnicolourJzazbz(double j, double a, double b) => AssertInit(j, a, b, Unicolour.FromJzazbz, Unicolour.FromJzazbz, Unicolour.FromJzazbz, Unicolour.FromJzazbz); + public void UnicolourJzazbz(double j, double a, double b) => AssertInit(j, a, b, Unicolour.FromJzazbz, Unicolour.FromJzazbz, Unicolour.FromJzazbz, Unicolour.FromJzazbz); [TestCase(0, 0, 0)] [TestCase(0.17, 0.16, 360)] [TestCase(0.085, 0.08, 180)] - public static void UnicolourJzczhz(double j, double c, double h) => AssertInit(j, c, h, Unicolour.FromJzczhz, Unicolour.FromJzczhz, Unicolour.FromJzczhz, Unicolour.FromJzczhz); + public void UnicolourJzczhz(double j, double c, double h) => AssertInit(j, c, h, Unicolour.FromJzczhz, Unicolour.FromJzczhz, Unicolour.FromJzczhz, Unicolour.FromJzczhz); [TestCase(0, -0.5, -0.5)] [TestCase(1, 0.5, 0.5)] [TestCase(0.5, -0.001, 0.001)] - public static void UnicolourOklab(double l, double a, double b) => AssertInit(l, a, b, Unicolour.FromOklab, Unicolour.FromOklab, Unicolour.FromOklab, Unicolour.FromOklab); + public void UnicolourOklab(double l, double a, double b) => AssertInit(l, a, b, Unicolour.FromOklab, Unicolour.FromOklab, Unicolour.FromOklab, Unicolour.FromOklab); [TestCase(0, 0, 0)] [TestCase(1, 0.5, 360)] [TestCase(0.5, 0.25, 180)] - public static void UnicolourOklch(double l, double c, double h) => AssertInit(l, c, h, Unicolour.FromOklch, Unicolour.FromOklch, Unicolour.FromOklch, Unicolour.FromOklch); + public void UnicolourOklch(double l, double c, double h) => AssertInit(l, c, h, Unicolour.FromOklch, Unicolour.FromOklch, Unicolour.FromOklch, Unicolour.FromOklch); [TestCase(0, -50, -50)] [TestCase(100, 50, 50)] [TestCase(50, -1, 1)] - public static void UnicolourCam02(double j, double a, double b) => AssertInit(j, a, b, Unicolour.FromCam02, Unicolour.FromCam02, Unicolour.FromCam02, Unicolour.FromCam02); + public void UnicolourCam02(double j, double a, double b) => AssertInit(j, a, b, Unicolour.FromCam02, Unicolour.FromCam02, Unicolour.FromCam02, Unicolour.FromCam02); [TestCase(0, -50, -50)] [TestCase(100, 50, 50)] [TestCase(50, -1, 1)] - public static void UnicolourCam16(double j, double a, double b) => AssertInit(j, a, b, Unicolour.FromCam16, Unicolour.FromCam16, Unicolour.FromCam16, Unicolour.FromCam16); + public void UnicolourCam16(double j, double a, double b) => AssertInit(j, a, b, Unicolour.FromCam16, Unicolour.FromCam16, Unicolour.FromCam16, Unicolour.FromCam16); + [TestCase(0, 0, 0)] + [TestCase(360, 120, 100)] + [TestCase(180, 60, 50)] + public void UnicolourHct(double h, double c, double t) => AssertInit(h, c, t, Unicolour.FromHct, Unicolour.FromHct, Unicolour.FromHct, Unicolour.FromHct); + 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); private delegate Unicolour FromTuple((double first, double second, double third) tuple, double alpha = 1.0); diff --git a/Unicolour.Tests/Utils/AssertUtils.cs b/Unicolour.Tests/Utils/AssertUtils.cs index b1d8ca83..30c3408e 100644 --- a/Unicolour.Tests/Utils/AssertUtils.cs +++ b/Unicolour.Tests/Utils/AssertUtils.cs @@ -68,6 +68,7 @@ void AccessProperties() AccessProperty(() => unicolour.Cam16); AccessProperty(() => unicolour.Config); AccessProperty(() => unicolour.Description); + AccessProperty(() => unicolour.Hct); AccessProperty(() => unicolour.Hex); AccessProperty(() => unicolour.Hpluv); AccessProperty(() => unicolour.Hsb); diff --git a/Unicolour.Tests/Utils/RandomColours.cs b/Unicolour.Tests/Utils/RandomColours.cs index 5d089c85..5dc26aef 100644 --- a/Unicolour.Tests/Utils/RandomColours.cs +++ b/Unicolour.Tests/Utils/RandomColours.cs @@ -29,6 +29,7 @@ internal static class RandomColours public static readonly List OklchTriplets = new(); public static readonly List Cam02Triplets = new(); public static readonly List Cam16Triplets = new(); + public static readonly List HctTriplets = new(); // ReSharper restore CollectionNeverQueried.Global static RandomColours() @@ -56,6 +57,7 @@ static RandomColours() OklchTriplets.Add(Oklch()); Cam02Triplets.Add(Cam02()); Cam16Triplets.Add(Cam16()); + HctTriplets.Add(Hct()); } } @@ -79,7 +81,8 @@ static RandomColours() public static ColourTriplet Oklab() => new(Rng(), Rng(-0.5, 0.5), Rng(-0.5, 0.5)); public static ColourTriplet Oklch() => new(Rng(), Rng(0, 0.5), Rng(0, 360)); public static ColourTriplet Cam02() => new(Rng(0, 100), Rng(-50, 50), Rng(-50, 50)); // from own test values - public static ColourTriplet Cam16() => new(Rng(0, 100), Rng(-50, 50), Rng(-50, 50)); // from own test values + public static ColourTriplet Cam16() => new(Rng(0, 100), Rng(-50, 50), Rng(-50, 50)); // from own test values + public static ColourTriplet Hct() => new(Rng(0, 360), Rng(0, 120), Rng(0, 100)); // from own test values public static double Alpha() => Random.NextDouble(); private static double Rng() => Random.NextDouble(); @@ -104,6 +107,7 @@ static RandomColours() public static Unicolour UnicolourFromOklch() => Unicolour.FromOklch(Oklch().Tuple, Alpha()); public static Unicolour UnicolourFromCam02() => Unicolour.FromCam02(Cam02().Tuple, Alpha()); public static Unicolour UnicolourFromCam16() => Unicolour.FromCam16(Cam16().Tuple, Alpha()); + public static Unicolour UnicolourFromHct() => Unicolour.FromHct(Hct().Tuple, Alpha()); private static string Hex() { diff --git a/Unicolour/Cam.cs b/Unicolour/Cam.cs index 96d807dc..ac88e3d3 100644 --- a/Unicolour/Cam.cs +++ b/Unicolour/Cam.cs @@ -98,15 +98,17 @@ internal static string GetHueComposition(double h) { if (double.IsNaN(h)) return "-"; var hPrime = GetHPrime(h); - var i = hPrime switch + int? index = hPrime switch { >= Angle1 and < Angle2 => 1, >= Angle2 and < Angle3 => 2, >= Angle3 and < Angle4 => 3, >= Angle4 and < Angle5 => 4, - _ => throw new Exception("hPrime out of range") + _ => null }; + if (index == null) return "-"; + var i = index.Value; var hQuad = Quad(i) + 100 * E(i + 1) * (hPrime - Angle(i)) / (E(i + 1) * (hPrime - Angle(i)) + E(i) * (Angle(i + 1) - hPrime)); diff --git a/Unicolour/Cam02.cs b/Unicolour/Cam02.cs index b77ae026..d87e56ee 100644 --- a/Unicolour/Cam02.cs +++ b/Unicolour/Cam02.cs @@ -12,7 +12,9 @@ public record Cam02 : ColourRepresentation public Ucs Ucs { get; } public Model Model { get; } - internal override bool IsGreyscale => Model.Chroma <= 0; // presumably also A.Equals(0.0) && B.Equals(0.0) + // J lightness bounds not clear (and is different between Model and UCS) + // presumably also greyscale when A.Equals(0.0) && B.Equals(0.0) + internal override bool IsGreyscale => Model.Chroma <= 0; public Cam02(double j, double a, double b, CamConfiguration camConfig) : this(new Ucs(j, a, b), camConfig, ColourHeritage.None) {} diff --git a/Unicolour/Cam16.cs b/Unicolour/Cam16.cs index 8aa872c8..efa6f164 100644 --- a/Unicolour/Cam16.cs +++ b/Unicolour/Cam16.cs @@ -12,7 +12,9 @@ public record Cam16 : ColourRepresentation public Ucs Ucs { get; } public Model Model { get; } - internal override bool IsGreyscale => Model.Chroma <= 0; // presumably also A.Equals(0.0) && B.Equals(0.0) + // J lightness bounds not clear (and is different between Model and UCS) + // presumably also greyscale when A.Equals(0.0) && B.Equals(0.0) + internal override bool IsGreyscale => Model.Chroma <= 0; public Cam16(double j, double a, double b, CamConfiguration camConfig) : this(new Ucs(j, a, b), camConfig, ColourHeritage.None) {} diff --git a/Unicolour/CamConfiguration.cs b/Unicolour/CamConfiguration.cs index b279c835..3ce34c22 100644 --- a/Unicolour/CamConfiguration.cs +++ b/Unicolour/CamConfiguration.cs @@ -35,9 +35,18 @@ public class CamConfiguration * hard to find guidance for default CAM settings; this is based on data in "Usage guidelines for CIECAM97s" (Moroney, 2000) * - sRGB standard ambient illumination level of 64 lux ~= 4 * - La = E * R / PI / 5 where E = lux & R = 1 --> 64 / PI / 5 + * ---------- + * I don't know why Google's HCT luminance calculations don't match the above + * they suggest 200 lux -> ~11.72 luminance, but the formula above gives ~12.73 luminance + * and they appear to ignore the division by 5 and incorporate XYZ luminance (Y) */ public static readonly CamConfiguration StandardRgb = new(WhitePoint.From(Illuminant.D65), LuxToLuminance(64), 20, Surround.Average); + public static readonly CamConfiguration Hct = new(WhitePoint.From(Illuminant.D65), LuxToLuminance(200) * 5 * DefaultHctY(), DefaultHctY() * 100, Surround.Average); internal static double LuxToLuminance(double lux) => lux / Math.PI / 5.0; + + // just for HCT, use specific XYZ configuration + private const double DefaultHctLightness = 50; + private static double DefaultHctY() => Lab.ToXyz(new Lab(DefaultHctLightness, 0, 0), XyzConfiguration.D65).Y; public CamConfiguration(WhitePoint whitePoint, double adaptingLuminance, double backgroundLuminance, Surround surround) { diff --git a/Unicolour/Hct.cs b/Unicolour/Hct.cs new file mode 100644 index 00000000..4475e0d4 --- /dev/null +++ b/Unicolour/Hct.cs @@ -0,0 +1,146 @@ +namespace Wacton.Unicolour; + +public record Hct : ColourRepresentation +{ + protected override int? HueIndex => 0; + public double H => First; + public double C => Second; + public double T => Third; + public double ConstrainedH => ConstrainedFirst; + protected override double ConstrainedFirst => H.Modulo(360.0); + internal override bool IsGreyscale => C <= 0.0 || T is <= 0.0 or >= 100.0; + + public Hct(double h, double c, double t) : this(h, c, t, ColourHeritage.None) {} + internal Hct(double h, double c, double t, ColourHeritage heritage) : base(h, c, t, heritage) {} + + protected override string FirstString => UseAsHued ? $"{H:F1}°" : "—°"; + protected override string SecondString => $"{C:F2}"; + protected override string ThirdString => $"{T:F2}"; + public override string ToString() => base.ToString(); + + /* + * HCT is a transform of XYZ + * (just a combination of LAB & CAM16, but with specific XYZ & CAM configuration, so can't reuse existing colour space calculations) + * Forward: https://material.io/blog/science-of-color-design + * Reverse: n/a - no published reverse transform and I don't want to port Google code, so using my own naive search + */ + + internal static Cam16 Cam16Component(Xyz xyz) => Cam16.FromXyz(xyz, CamConfiguration.Hct, XyzConfiguration.D65); + internal static Lab LabComponent(Xyz xyz) => Lab.FromXyz(xyz, XyzConfiguration.D65); + + internal static Hct FromXyz(Xyz xyz, XyzConfiguration xyzConfig) + { + var xyzMatrix = Matrix.FromTriplet(xyz.Triplet); + var d65Matrix = Adaptation.WhitePoint(xyzMatrix, xyzConfig.WhitePoint, WhitePoint.From(Illuminant.D65)); + var d65Xyz = new Xyz(d65Matrix.ToTriplet(), ColourHeritage.From(xyz)); + + var cam16 = Cam16Component(d65Xyz); + var lab = LabComponent(d65Xyz); + + var h = cam16.Model.H; + var c = cam16.Model.C; + var t = lab.L; + return new Hct(h, c, t, ColourHeritage.From(xyz)); + } + + internal static Xyz ToXyz(Hct hct, XyzConfiguration xyzConfig) + { + var targetY = Lab.ToXyz(new Lab(hct.T, 0, 0), XyzConfiguration.D65).Y; + var result = FindBestJ(targetY, hct); + var d65Xyz = result.Converged ? result.Data.Xyz : new Xyz(double.NaN, double.NaN, double.NaN); + var d65Matrix = Matrix.FromTriplet(d65Xyz.Triplet); + var xyzMatrix = Adaptation.WhitePoint(d65Matrix, WhitePoint.From(Illuminant.D65), xyzConfig.WhitePoint); + var xyz = new Xyz(xyzMatrix.ToTriplet(), ColourHeritage.From(hct)) + { + HctToXyzSearchResult = result + }; + + return xyz; + } + + // i'm sure some smart people have some fancy-pants algorithms to do this efficiently + // but until there's some kind of published reverse transformation algorithm, this gets the job done + // (albeit rather slowly...) + private static HctToXyzSearchResult FindBestJ(double targetY, Hct hct) + { + HctToXyzSearchData latest = GetStartingData(targetY, hct); + HctToXyzSearchData best = latest; + + var step = latest.J; + var iterations = 0; + while (!double.IsNaN(latest.DeltaY) && Math.Abs(latest.DeltaY) > 0.000000001 && iterations < 100) + { + var j = latest.J + (latest.DeltaY > 0 ? -step : step); + var data = ProcessJ(targetY, j, hct); + var deltaY = data.DeltaY; + if (Math.Abs(deltaY) < Math.Abs(best.DeltaY)) + { + best = data; + } + + // change in sign of delta means target is now in the other direction + var overshot = double.IsNaN(deltaY) || Math.Sign(latest.DeltaY) != Math.Sign(deltaY); + if (overshot) + { + step /= 2.0; + } + + latest = data; + iterations++; + } + + var converged = !double.IsNaN(latest.DeltaY) && iterations < 100; + return new HctToXyzSearchResult(best, iterations, converged); + } + + private static HctToXyzSearchData GetStartingData(double targetY, Hct hct) + { + var xzPairs = new List<(double, double)> { (0, 0), (0, 1), (1, 0), (1, 1) }; + HctToXyzSearchData best = InitialData; + foreach (var (x, z) in xzPairs) + { + var j = Cam16.FromXyz(new Xyz(x, targetY, z), CamConfiguration.Hct, XyzConfiguration.D65).Model.J; + var data = ProcessJ(targetY, j, hct); + if (Math.Abs(data.DeltaY) < best.DeltaY) + { + best = data; + } + } + + return best; + } + + private static HctToXyzSearchData ProcessJ(double targetY, double j, Hct hct) + { + var camModel = new Cam.Model(j, hct.C, hct.H, 0, 0, 0); + var cam16 = new Cam16(camModel, CamConfiguration.Hct, ColourHeritage.None); + var xyz = Cam16.ToXyz(cam16, CamConfiguration.Hct, XyzConfiguration.D65); + var deltaY = xyz.Y - targetY; + return new HctToXyzSearchData(hct, j, cam16, xyz, targetY, deltaY); + } + + private static readonly HctToXyzSearchData InitialData = new( + J: double.PositiveInfinity, DeltaY: double.PositiveInfinity, + Hct: null!, Cam16: null!, Xyz: null!, TargetY: double.NaN); +} + +// only for potential debugging or diagnostics +// until there is an "official" HCT -> XYZ reverse transform +internal record HctToXyzSearchResult(HctToXyzSearchData Data, int Iterations, bool Converged) +{ + internal HctToXyzSearchData Data { get; } = Data; + internal int Iterations { get; } = Iterations; + internal bool Converged { get; } = Converged; + public override string ToString() => $"{Data} · Iterations:{Iterations} · Converged:{Converged}"; +} + +internal record HctToXyzSearchData(Hct Hct, double J, Cam16 Cam16, Xyz Xyz, double TargetY, double DeltaY) +{ + internal Hct Hct { get; } = Hct; + internal double J { get; } = J; + internal Cam16 Cam16 { get; } = Cam16; + internal Xyz Xyz { get; } = Xyz; + internal double TargetY { get; } = TargetY; + internal double DeltaY { get; } = DeltaY; + public override string ToString() => $"J:{J:F4} · ΔY:{DeltaY:F4}"; +} \ No newline at end of file diff --git a/Unicolour/Ictcp.cs b/Unicolour/Ictcp.cs index 7e60021b..2206dc51 100644 --- a/Unicolour/Ictcp.cs +++ b/Unicolour/Ictcp.cs @@ -6,7 +6,9 @@ public record Ictcp : ColourRepresentation public double I => First; public double Ct => Second; public double Cp => Third; - internal override bool IsGreyscale => Ct.Equals(0.0) && Cp.Equals(0.0); + + // no clear lightness upper-bound + internal override bool IsGreyscale => I <= 0.0 || (Ct.Equals(0.0) && Cp.Equals(0.0)); public Ictcp(double i, double ct, double cp) : this(i, ct, cp, ColourHeritage.None) {} internal Ictcp(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second, triplet.Third, heritage) {} diff --git a/Unicolour/Interpolation.cs b/Unicolour/Interpolation.cs index 0c0af895..e502d92a 100644 --- a/Unicolour/Interpolation.cs +++ b/Unicolour/Interpolation.cs @@ -21,6 +21,7 @@ public static class Interpolation public static Unicolour InterpolateOklch(this Unicolour start, Unicolour end, double distance) => Interpolate(ColourSpace.Oklch, start, end, distance); public static Unicolour InterpolateCam02(this Unicolour start, Unicolour end, double distance) => Interpolate(ColourSpace.Cam02, start, end, distance); public static Unicolour InterpolateCam16(this Unicolour start, Unicolour end, double distance) => Interpolate(ColourSpace.Cam16, start, end, distance); + public static Unicolour InterpolateHct(this Unicolour start, Unicolour end, double distance) => Interpolate(ColourSpace.Hct, start, end, distance); internal static Unicolour Interpolate(ColourSpace colourSpace, Unicolour startColour, Unicolour endColour, double distance) { @@ -125,6 +126,7 @@ private static UnicolourConstructor GetConstructor(ColourSpace colourSpace) ColourSpace.Oklch => Unicolour.FromOklch, ColourSpace.Cam02 => Unicolour.FromCam02, ColourSpace.Cam16 => Unicolour.FromCam16, + ColourSpace.Hct => Unicolour.FromHct, _ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null) }; } diff --git a/Unicolour/Jzazbz.cs b/Unicolour/Jzazbz.cs index afb6854a..e29b0143 100644 --- a/Unicolour/Jzazbz.cs +++ b/Unicolour/Jzazbz.cs @@ -9,7 +9,8 @@ public record Jzazbz : ColourRepresentation // based on the figures from the paper, greyscale behaviour is the same as LAB // i.e. non-lightness axes are zero - internal override bool IsGreyscale => A.Equals(0.0) && B.Equals(0.0); + // but no clear lightness upper-bound + internal override bool IsGreyscale => J <= 0.0 || (A.Equals(0.0) && B.Equals(0.0)); public Jzazbz(double j, double a, double b) : this(j, a, b, ColourHeritage.None) {} internal Jzazbz(double j, double a, double b, ColourHeritage heritage) : base(j, a, b, heritage) {} diff --git a/Unicolour/Jzczhz.cs b/Unicolour/Jzczhz.cs index 897d4c99..59e10516 100644 --- a/Unicolour/Jzczhz.cs +++ b/Unicolour/Jzczhz.cs @@ -11,9 +11,9 @@ public record Jzczhz : ColourRepresentation public double ConstrainedH => ConstrainedThird; protected override double ConstrainedThird => H.Modulo(360.0); - // I'm assuming JCH has the same greyscale behaviour as LCH, i.e. greyscale = no chroma, no lightness, or full lightness + // no clear lightness upper-bound // (paper says lightness J is 0 - 1 but seems like it's a scaling of their plot of Rec.2020 gamut - in my tests maxes out after ~0.17) - internal override bool IsGreyscale => C <= 0.0 || J is <= 0.0 or >= 1.0; + internal override bool IsGreyscale => J <= 0.0 || C <= 0.0; public Jzczhz(double j, double c, double h) : this(j, c, h, ColourHeritage.None) {} internal Jzczhz(double j, double c, double h, ColourHeritage heritage) : base(j, c, h, heritage) {} diff --git a/Unicolour/Lab.cs b/Unicolour/Lab.cs index 13682c78..26bb085e 100644 --- a/Unicolour/Lab.cs +++ b/Unicolour/Lab.cs @@ -8,7 +8,7 @@ public record Lab : ColourRepresentation public double L => First; public double A => Second; public double B => Third; - internal override bool IsGreyscale => A.Equals(0.0) && B.Equals(0.0); + internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || (A.Equals(0.0) && B.Equals(0.0)); public Lab(double l, double a, double b) : this(l, a, b, ColourHeritage.None) {} internal Lab(double l, double a, double b, ColourHeritage heritage) : base(l, a, b, heritage) {} diff --git a/Unicolour/Lchab.cs b/Unicolour/Lchab.cs index 0a5783d2..d4913fd0 100644 --- a/Unicolour/Lchab.cs +++ b/Unicolour/Lchab.cs @@ -10,7 +10,7 @@ public record Lchab : ColourRepresentation public double H => Third; public double ConstrainedH => ConstrainedThird; protected override double ConstrainedThird => H.Modulo(360.0); - internal override bool IsGreyscale => C <= 0.0 || L is <= 0.0 or >= 100.0; + internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || C <= 0.0; public Lchab(double l, double c, double h) : this(l, c, h, ColourHeritage.None) {} internal Lchab(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage) {} diff --git a/Unicolour/Lchuv.cs b/Unicolour/Lchuv.cs index 6a2aeff8..062607a9 100644 --- a/Unicolour/Lchuv.cs +++ b/Unicolour/Lchuv.cs @@ -10,7 +10,7 @@ public record Lchuv : ColourRepresentation public double H => Third; public double ConstrainedH => ConstrainedThird; protected override double ConstrainedThird => H.Modulo(360.0); - internal override bool IsGreyscale => C <= 0.0 || L is <= 0.0 or >= 100.0; + internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || C <= 0.0; public Lchuv(double l, double c, double h) : this(l, c, h, ColourHeritage.None) {} internal Lchuv(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage) {} diff --git a/Unicolour/Luv.cs b/Unicolour/Luv.cs index 258ddf26..992df452 100644 --- a/Unicolour/Luv.cs +++ b/Unicolour/Luv.cs @@ -8,7 +8,7 @@ public record Luv : ColourRepresentation public double L => First; public double U => Second; public double V => Third; - internal override bool IsGreyscale => U.Equals(0.0) && V.Equals(0.0); + internal override bool IsGreyscale => L is <= 0.0 or >= 100.0 || (U.Equals(0.0) && V.Equals(0.0)); public Luv(double l, double u, double v) : this(l, u, v, ColourHeritage.None) {} internal Luv(double l, double u, double v, ColourHeritage heritage) : base(l, u, v, heritage) {} diff --git a/Unicolour/Oklab.cs b/Unicolour/Oklab.cs index 7d1bb17f..d07c882c 100644 --- a/Unicolour/Oklab.cs +++ b/Unicolour/Oklab.cs @@ -8,7 +8,7 @@ public record Oklab : ColourRepresentation public double L => First; public double A => Second; public double B => Third; - internal override bool IsGreyscale => A.Equals(0.0) && B.Equals(0.0); + internal override bool IsGreyscale => L is <= 0.0 or >= 1.0 || (A.Equals(0.0) && B.Equals(0.0)); public Oklab(double l, double a, double b) : this(l, a, b, ColourHeritage.None) {} internal Oklab(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second, triplet.Third, heritage) {} diff --git a/Unicolour/Oklch.cs b/Unicolour/Oklch.cs index b9fce48e..c4a94e6c 100644 --- a/Unicolour/Oklch.cs +++ b/Unicolour/Oklch.cs @@ -10,7 +10,7 @@ public record Oklch : ColourRepresentation public double H => Third; public double ConstrainedH => ConstrainedThird; protected override double ConstrainedThird => H.Modulo(360.0); - internal override bool IsGreyscale => C <= 0.0 || L is <= 0.0 or >= 1.0; + internal override bool IsGreyscale => L is <= 0.0 or >= 1.0 || C <= 0.0; public Oklch(double l, double c, double h) : this(l, c, h, ColourHeritage.None) {} internal Oklch(double l, double c, double h, ColourHeritage heritage) : base(l, c, h, heritage) {} diff --git a/Unicolour/Resources/diagram.png b/Unicolour/Resources/diagram.png index 8d28042b8945c8fb9df7707b9d571c329a41751f..a733409518a74c6ec29401b1e0c0a1f1b8968c97 100644 GIT binary patch literal 46948 zcmYg&1z6MH7dMR}r6^s32#Rz!2na}b3mYxD>a5b5ra?uOAJN~d%VBnD$Ja>R&t z`1`-_^M0Ra&z^1Hd(Y>7&bjBDd%yQATtiKPh=7^^0|SFdNl{J<0|RFn0|V3g0Ur7r z(4`J0`VG@fOW`d>)fnwA`U8%&jH(OyNnT&kvmy4c#y>h! zmr8OnI$j_4SMW1wW@ZEr>pS$%4}ge1Tzh+!{?iE+j45BGkQW6VN&^{OQ9{#S)l?n{ z;7vtfn%hS{n(w1LU+qBp_*O&2_1!$JEkCpDzc(7)xrBn__}ORwET;j+GJRQ#1{vl4 z@AdR)?Kbn{`wK|^0gIK1>3E<@^#!)$zu&Vs`utunG3DvO7v*@@-u=Hea)iy7-Ee=V z|6K$)*p8CiZ8_h*eH)N!*HA*=xA`-W=Dt&0TrQm_==T=x|GznpD&C36{m&wDE=r|u zOl!4>={RE*VF$l||5m!|1X)})Y`hRmp7Re!_n1cM%#0S~ni?Q}QPO_+H-LqmPx;dZ z*+^v^ro?#Y74>Hun^-Z&7PYF*W33Y*xlJ}oOV@0X$MQUQ%V7Cm6H{vFl z_qMGT+sH)E)jNajj>(YaV)d&)>uUX-ZD+DT`N7*w5hqZbXaw z{zOm}pLOYDcho%z_ZQo@e@y1>{GwvH2lkYSuo>Nlru*N>C-(pe;!BczJlX0#m~6TN%qS=-8Yp*zB*`mx@&+*TI-WjgSzk1KLP5qZhJ+{Z zFozBGjV^7i;%DjRO5wmW7^Lr-->wrHP~G#54&8NoftJfywvhu-x3<- z$bLq@$V{^A8$;^}m-~;54xZk7&}{E%MYP_Sa+1 zG;w5jafb1b2oCEkm%PQ$+{J;%vYTr^dXIO17P@kGNhentaMbefyyl*(SI|F`w5U_f z@{63Fof-bi!d}qY`p(~N9OSxE=?T8N{Iol#1({&9n{|wJ5c4R#OTNdQ{-3cwl_z}x zX!dHEo)H*0>Q{@!yg>~#lqTpn?+;s+TigZtopq{H_`d>x_v2SPT89Vuipm^{v;5lK z_tV)k!1gJ)Ib7OH%g>#Kd^R;+y?*`C<9lwr?3q4Ko_BK`4re~`~m z5r^1j2ZP=_Ecb;xi$}Vrr}+(AuEeas_sXCC z^v^)p2tZ9+hqn#OVf|#=fPYnY|Iu z2EoriZKy=-W1l{KI{0=2L3WRg{eRddLZbkdkXu>UHk| zSZz<^**8#9Q%c@qpDO{b{nO2}|LNv|(0fWj_gsDffx%dro2}(~HJ*SeYAiQVucpt# z$ZdhuWz{tC521wgZ1^P42L5c7k5t~<@?`iw!_Q+2Zj1hVkl(ENGmpho$pjRvk|CK;AR&Oqk2Y<3`>$!e^c@l>Pd)@!fx5W`9>FJl|bjZuYSi2gguT_`&qUe7dh<GSeXO+HUC_qPg=sH3p4ivn%!Gn`N2% zL1Yp1@SPdxI^a!}&FrOLiN30?iu+skdZX5;3n~-6# zdH5X0na9Ot4l-BFbnQlA`xnT!y)xXMqPw9?M!xQm|W$J#rhZt)*Ht38J44GOJ$~&k&K;R4tJ>cD!r#98PLEk&KO@Xiy=|*z`m)3*ZHVYOg$^rqoya@>=F|S?`j@?`BFEg~&4w#sk8#8^!TCE#5!ksQN zY6>GLd+9_c6oBOzSH>8SlIayf+}x=9(6izT@@b|U?uHST>wnF;|C^wiza_kivfC_k zTC(w0X2l4~`D(^T$?M5 zaW}+q3*lMovvr{~oo+EBDuZfD0{&AL@M=;0mMhIrgOke1oyC{Gek+;C245YiAAWIz z)rwHgJ;@C26cK^NeJjalYBYJ7NB*C~|7G(q7MBk-VuA{LHQW82xJ>%3am89d*zoW) z!g)?3Kq_CLr0DL0qiq=)SJ~YMNXRtVHTdG3bU-F_iKe>CX_!#Ru6vZOlNm`gYZ(y} zm6UC0IEpfN(ySGI*}l(E-tbEu zEs8w)H|L)|nO#fs;s1aycn8=3-&d4eXD=fwAImZjRD0Wvc_y?( zK);TNgslK1z_;gz@JH1He0Sa}$(Z1uvc8)^zKFQT<_hVk-1&=Fr_D~aB<>i*mgXyA zSIvKdgad}~14s;AI_l~Qs(xtxNq912hhuqjLvuhn(F>6dB#pVXUZhiH5Sv}gWJfa> zYwjG4cIC(aS&F~Rp^QYj43Qlv(^JfbXsahJ_^+3PZPp}zk3TbesH&mb+x6qN>IjXm zrgZ$XRkin(*g+<#v)I9W_%=C3NA=>6MX&$y)4-`ym(sf>>AJt}RBH$iCH17p0*lg; zQr$FHwytfrA9VeG@85nQ=zG3FY?yN7Qmds8VblcztArYYCGjM1q0??B;uBqHi^`io z$8|O~WBs4u-{5cYpObQH#5|x*#SGTK89!Y#s^zI_{gab{<+VYt<4jU;Nx;^O^H4{MXRw)V&%57pRG?$@ zO#D!$i&az%KTXo0tzz&{4g+Esq%QS-&M8NgBS;^ljzPw~Hg_m!N~ zR}|?z+4x&fHufy>1X}MKj40Se;2C?OwKpL4FCH;P0;Q|yM`c=k=)bH{;lX!^3#elR z&Vcbxb8>nA93sq&in~0gPvuCa&lp!EVrSzpTiK-_-{z)2lav*%o4bA-g(irNMBflc z2*bKCMr;m(@Y2gH4qyl%JB9W`4iq)i?v^fAAM_QnvMTvS+P$`O{q%+QRkcpn+?}Js z9{FBZaSRX-ingZ3f7bN$6GyOta?J?sNGMZaXb)%IIB%p35bXtqye8AE=jkhUZC1b7 zklK*;(&z{N0BVe0?L^#WUY)qV$RNzYz2h8u5VI;7dT{^L}vTr52k{U29EyG`6M1B6n~l5aiF&hhV|3_Sejk_a@0rKd)O zdGIs5D(RO**3N#3QsQQ1X=%?fqHPF+dhOslC9UTs1jumFivQ=Wzz%S~IvG@&gCZ-z zIJ+DPe2Bw75He2sYQe5^?PS#Z*7EZ7X5602S{hK<=gp4lMos)&LcF(kjF3_*z+t5$xqZ&b0n< zrdjE2W<&Qh9s8#qc0Vc31UK8X42R0ruW7Z4FpuVTmCC5l-1OV?C4!5Kzxxn5Zp@!N z(vAKk5)b+X1G-Y^cL2e*J=e=u^=tL(lS|M#|7bH@b5YxavLx6^l}Uq6(;E|JLlKD) z$bA3*LeBf@=h&98T$zuLjPsUV-Y^CK0krJ#_CtH11XgKnIglS?-%sQa!O7!KT5~gR z{jMOky8Y>AU-rCFw+`%n#Ah~_Z>j>iEEz)b515(qc7ETd3?kpbL76@cj*nb0R7qg3 z?d+!QRQiukMXsp~eE;r2LY(n~DwkfF=0nZAN#@mamJ!WY-8lNS7~_8Gtxq~R9T>hB^!a?RI#a+-D@ zw(_F?rG`xDD6?VF65CbsFZXU5Pd^*W@Ibrxw2Llsd_(fk?e3iU;{^07x^hl8;#Y3E z)lIttNN>+NA`vd2et4O(Kfem|9Z45flDxAC5*L9!8yu-!PyBP8bvzIpkdR)%Z`c(O zaB>;Un<`?fQ%rYqODlGjNd&}D>$S$3gMNPK=Y3^=aA>$BCeg@o#kVU;>K;3Bw%m0k zGliNqA}KqmzifeS^h`m!YroD_LFvkKdT#221Zqm@nRByWGUbi^%hf`XP=N!~6Ul!HyG@b^}3XWds7cx)#Cocth(1X6yBP zZ~?o+K03*iu@4cO7#Yua7h@NSqF^%s+yb-R#OY$I4797$cFL`&N zIyg3_wH1c=O!Q|>EYD9AMO&LqQ5C2U#b2v9h4$tXdtH*j>$2-bNh#o>4|C)tj9~gV zwAYuJdb2lhA8{X;EnrzH#>dhB<(wN_`3@%TbZTvZj!0~x{}zgLe(oyJ=OCD(W;J_@&?>|-6@(&3DmH^= zo~7ApOA_=nC%|?!H@~yL)cOMx&(W*+^lVGVa6kEwOI{zZZ1JP&J3er(9d-VY>+KbA z6j}sEET^4{Y_ek%?}d$tA}gRi*6QjGk0euk8m5)$N{i1rol0Yt&gqt=Qqb51=T49y zN%oHFC4w>eKeg&|a-ILsS2mNpImO7Ek!Jpd$uFSDi%;oba&wL}Jj(^=2Q@TFrkLPK z`Y?RVF*Dr(ZN1`Nd)Qe|Tqa|UUw&%c@7a?NMtrS^arUFwv-Xg{bCM9z_noTs`4G^l=TOw*WHOF@hgQsZ1wBVj+u7cL z)LrogT{< zxFv@jSRE~xb-9@sdkuzN^}s6|!0in@;`oqRU0KgVKWR5PHkD2FiTSM{7NyB}Yr1V| z+3SS5__vp+J5<()_=d$vO7^;ehbg$KvfI3%COc(yOlj|u;xjb6Dp*WZLSPv(c(w4H za&OQuID#}%P&VM@p0)haEDRX?#qG+iRnl!3+T_-zAoq0l-uKs0ofEene8VpfcHCMu z^d!+Jg<`tTc7Qx2Fl-S}|PvWZrW&H@yp`vj50UgM@Nk;je#>=c$0i^py!b zvtyg66AXOK(&?1z2{%5PdF{P@sc+G8G(Rz6<`_j=TKdWrHpsVGv{#+j9&|HW+4<3y z<4dd$#4LyL&?@DGrXRdFkEHK@>%I3X^WsCChu@yhz}Y2sMt0er&CQBvIq^?xy_n>t zl1uNvebUYpAO0?y9R$>eKJ(b^Zp3W#Ks$!c2Gs!~nW)bCllmmsHF>!wEqVQo%@P)xV~Pa*=i9>b2? z(io;NQ4YiDIi?e2aLC9B=)pKPbOkbwq04Fgj@a=b{E(cl zt(F_{N&5X;-qfO&T=bmZucDHFNSQ<4p?KYiKmCypQA>93Ux?+zW!AHtJv+a z#=A@*+y;(|v7l9n=~q*-pd)u}95G8rAYLXH-6&2Zzp#QaFem`CSuscJ(FJ{Omofk{ zN&q1{^~B92iPP%5@-ZW@wYB2coL(}NA>g-k=$z8GIoq>6OFw4&+*QKqW+m@O z2kU`5B3&Q3d?8$(P2QuJ=85<#3Qs%W3lMYkaxgF6w_nqfGSd*(6OcRsN@D}y{#$MHD zSRsgA(XQ_R)zX=YGybCa#TL%V{bVw17wUi3SS}Davl^J`+x`0K4WVf}h^w)Z4l+7) zp0`b~`Ygic#{WvJ z5sbZ8nn3I^QbHgA_k=r1cuiD;H2XuTEU@59&Ln|fROy2IBoD1`1SBM;qwVZNu#ib- z(HZj!Besu-+aNm6HCl=P1UY)nw)Q{7bTmOUcY@upUGy%ZbGwQKl*i^gJ&R-TL6SS5 zNyGiPTgO@d^hS_wwHRe)y}#+)x&398hf`QM+wr}Bemva4TqI`P1N-*5mG6MwnZUYO z1ti4<;nNA?_u!Y9vivxzcLFA?83#6{OZsO%k*htRfRj04OZXvd00A7N+2kmwKLICg z`EV1$rZMNSdvD9mrs#N%o6el3$0O0pye={Nd=}E;^|msF6>ILrvTZ0Pl0m0!>p3IT zXSD_Od+A(fY4OOU^0apCb_KI~!IUWvd7q2?9MTROyrT94#)DiC!n@LBC1VzpV(P1% z)wj|6H&wH0wYUE3w~ijgL%nP%(#1hAP&Gb-+JN2(4hkQI-ER*-;i2+x_NwJc;8J^7 zck}uW8eDQhqKiTFszP}aP-g%pTV|Ii2DD)poV({ecTOTs3N2eQIcv)r*WL5S=-^r; zi^aB!eZutlZU?b*3;MCuh-g|RySbrCWo8Pvr;hH^{(Omo``XA@#sS=Yvn+pmbgqUtUZt=%H3PYPL6ToL1Ctmt_($1@Bs{8%10k zY8AJQmMXVGu_t5qx5Fj-AN$$Z-GFU`yG-^Q97(W9e(jEM72_CGps-IRT_nz>gbUB( z4$9>}Nvlmk#;-amZ*a~;AAm$aeg@~bH8(H56QkGulbXw7&u7O%54?c<->mi{OBC|4 z|I*9tjYL;mtvW(XyNC-;J{NYTEL?ssK)`UWQsozN+o$sx`b#8+h;&0 z0dUgT`aclJBG>e|$r9*XNZbX&V79^vr+Ko#fL=Um{?xYaxmrZ80qF2XJ2uwnb+pFC zT29s+q#E3rZVDocvU&$)3LoHl{Iz+gAH7@VE)Lby=4k zK$kIM8$meYaWY~A_9@rblwvHgfVSl{rL0O-`ApwQXC6=pOW1_p{GjJRDRv;ur3^pj zlqwASYgaqeA(qdvVN~M!ONZ$}B6%gffi!VZ-nR3V)h6RhbIo=KQSHTyMg{I)#P?&i zp>*)g9YVGVNETh@giBJI8{&ZZY;GKbl1kh@j*1cj>CWCKj85T{R_s z+(~{rs2u*s^m9#o(U+@8nFeC|pQ;RdgcZO{peb+ntVr_yJm+welaMJ-ImaLGArEtJ z@6an;V7=6odOnB&cKf|t;)JW_>kYmS!#jYK9D;A9dQVY$Ok%zO<>ix+)r_E+*7d4t zS_j5rxmDdv)x}VzG853Fqtnk{+W8AG;8VOwesxq4GkR`zN(ahF@D7lARMpv=nyFe- zz|7OOpr2NXSCPew)@U8jA|<{!v=Y|jr14miu<=>7;o<>f%Eyi}Cx83&^TF9|>>w>u zs6R7Ec~eHU`U9~!^L}jCS^tm~Y!#n7FPK7G6aH!n-rg;i)$n6nbe5Gzd^p{0Xe!G{ zhv{jvGo4@Pmir-t4yQ)8UwNMf4ElQW(=7dDXM^65G|&|ZrIl{st=XnQR*!KXHJR2z zrb+NyKL+i33?%+3BBp+!eS()(J-}Jv881)A?>23dE#?2V}Ciy#o5PuK3vj?Y0G3D)cl%%=hBKGS3nBQR&T;f>$_#;217L_OL#;qX873Ro+w(jA3M57<}m z%%sP5+g3k9g31p(abx_>By=-ukvZ<&!Dn$1lAJU7wIoYZIp70R*7J|2wZ-R;4fqIx zU218aBi|0+T1Y65{R*U!e`d%0x09ICi)P}gw}WT0hiQdzsg~!nT|gta$C7*0-*{_Z z6wmz$DqB zr;t-XBfx@F#rSpclKGs!gQ+0xxtra57;Li6bjFSWH9ORrV-hnqF76I-vLQKhJM@GY zdN0OIv~wjyf~cwhZDhFUM_QW?X5ULa4dQo_;jeM)|J@pJ+O3Hwmp)HxjB|&m!gPS>$Pi(T!9M>g}>rc=zW`& zmpm7)6w<<$d>#yc$tr;bs5DV5$^Fy4lH2U*mX*-8p2dN*ekOuCOsc^if)L?CMN zI8TePuhlg0y{5!FB}UbHLC$$6j;N*pqDCtR9b?qc0*b?EcjGstarV^ZhZs3r1)l`j zLG0>d6hVZ_o|4d76<{ZPet5{pp{BVvS+az4?yx?*Qh!MQ?G;ccE=BX2$Nont&AB@o8)_L51aL z^>$w#b`QTDcZoOPsg6xHve;)e`dv>eNxtQsJl~wi+!Nyyet}7XejKrZA0fO)f)WH8 zcq5W8o2*1#O~4bf9k4QOiE32n$>ScOWZ6T*^A7)mr~~tzZj!yh z=t+?HoIlAMK4MEuHj+8!VYyyOoft=%mSpHHu9ytMI=X9s0&lq_t?>!LJFE2K?+>`= zbz(HNj@pnpEeuhh`9FbIWz#OUt+TP(&zi#svZAcrC;BxQdjzT)$?4f>oV4XNiA4tn z-w2{yOsk2~9OQ(d&?@5vzfLzFmy4YyjhPmDUWj{lSPyuj`xbvXqVtP08;Kby=f$b_ z+TvXh{>G`-m#x2Pl4BD4<}c+me7V=i<&B@oE3e^eh|9P= z1g+-yI9Fv`Lu=kfYYAtcy30A)`F|GnpfrBOWg@xy#U9O(lwC~}=Ck8ptyN3YJ;4?_ z>Z>ZxGu^u)dcb2Lt{BrPw>ayRfsg1+$Y-VF_Qa1Q#$*?`NudnqH<6&FYwFh>27w+%wk=`Z4FlvOkL-s-U*E?Kv4$s&Tu);nk{lzvL`HFpJ$4bJ< zgia69PZmvuHz$feG^igN<;m3`mB@$n>Pmdm$6=a$FVTFpr{QF$lM;(C`-@XWPOtL0 zoqOAErK$NoMZG$i(Jb#|kg!ep_`}vntE}8s0uEnY~bDFxUSgB%W0FP!W??&z`h5WkTneC zjAtnAq?M0cJVWt`qJZ5)l6@Q7Z-^Me(d zZR-tiT*b;{_0rur2(x*>f{yi)hO{o30$Y0DdgZ)I;jS1jxRKU4=1s&{UmzgWP@O50 ze}7P`H+=I|ZXemM%>jxg<`!;eXIaP>X~7KfgPw+K~Gq6mjIXU<)L+#?;>P*Zkm4fWOa$!UU)V2*|Qdn&{aHRHlZ%O z!N;+xN_1h;G+2(^7!PrS1p*`LD_{r=y`{pD*l(#4zYsi1QL=z^WsW57I zEbnF-A_E=>Yx=l=q#F&=n%EazIvptJ|MY;~U|4l(tKUW1LZ&5%Q_Lhf+o0e5P5(q~ za!@tzr29fMmNVh6%b$T6xWwFa%b5C>6G%*a;T_V;Mzy<5nK?-|>;S&G3RKoJ9^4E` z@|F5r{uxYe|K=Un%_+;zL=8V~*UVnG?&dxiGgo0@At0X?anrBT>8Idh-N)YAes$Pa zS`KaAhv_dHV+vn7#`R4$i^`AU<2bR>=5kZbKa?<`chL18`H(-B z7o+Z!#euf~A5EVr8cw9MDVEkR+yIBj)8*DL)i}~DF%k%abXoQueVN*=Xq|kpdi#lZt#S%qg|l$n02u z5ui-*MHd1!t&cn+|6W*xE!=8!&VH6EacX`b>75tMAbg&-@@_rPX(C8$y{@BHs6j~7 zPBTWp>tlbX_!$YX#dZ0UX{)hGPu8pqT{bul8$I8P^0Hss&mpJCF~Lo0)Y3BCz8vl1 z0vhR60|w+zAGTL+{fCf`jqT!VB_mgC3W-d-x$`?3bcP4ED>A&CG6uNpZ4q1hN&55O<{u=2{m>go`l*Y#VPlNEFSYh z2B-d>0shUSss$+F0kksoBqazu5PbX=wfnh@drY|vM}?VlZF^sCBG?jdUNp=%nUp(F zCT{4)Yiq_DT1#v;`I6i2tKZ}qRtyD<(8uW260>zJQplibY+aImE4AOyoe|$MQh_K4 zjNT}xG>(#HF8}iKXzFnBs8YdU@>)W zcNTpGe7VOn@VRj86VXmRVqN?|M)$)2@MR=}v!)!rK2<~werC?Bz0S9W)R2@w8bRPS z5_Pyxq*}K%@%%*B?xMIHW;?2XL1F%aNAg4b-;LB-QoOqlbq3VbqZE#1d$5Aq!L#A{ zRD*n#zbr3bz6(9udCp2gQ`Ds0kArvzJLz%Te-6_n*e&p=ZQd$Rxc zVZY+sDdEYpg&u%iXWaE4UbM8NMgbVlD@mEZpUh|6q$Pm+NBtSqY}6c(TG?6nie0Cz zCi)OrBhgt|PF#GdjU!M-!tW%Ks)|BRCe~E%r4#5R>ar}Ps!i+36WyGlmCKDjrRs_| zC(q=0&m|kG%ozoElq6bdW;@z4#r>KuzcZ~Y>y{VPG=I}h*Ox!`{ROEX<51cma$VLD{RDVp(E$>j0B$vC#_VEpL~|(Nl$Q{^Dw!t2k@PQ zF=Rfs2@K5sX_>&Pr1!q;@^ynagN%&$F5c$$d!R`r%%b_uAlPBT;+h0|TXaLZctVzO zRJ&zO0e=Z&TvLy&e_g^S6;PT_Y5&OB(Wq>)wDR0*1ZGB)Ti1BYDW>x5HLiL}gL}0@ zSST;pQ9&Uk50C?g=SM%f-U;HqqB`huLqw1QbsTMBn*CJ%{IqV_{ty9)6+e9W*5=e^ zJ(_BCwxsg&_=NP=RD*;5V7kxY;k-qj3~0k) z$9Y34eFA0)&uRJ;(Ojc@7pukKT+p+ z)XhHz0wdaY6t1u_X-?JMQolZV9IL|v8X#3VtcQe|?Ka2+kLs^KPM)B5tlo3V|B4s> z6Fcemh5yZU>)SAyhX*R}rx#)c_MDPwcnO5Iden7eMf&F>It)|%l1|=!FY=y!Fqd)EwZ9&=E98|>_nONgSGz&S4aU^B_ztnvZ@@@PaGdE|Kipu@&d!>8GEMzbW{-Y=mL)if z-n=sp`A}7zV&4W(`|=d!Y+gBLoJxSW?g9}1!&^;_T#1Wm@qwm@|r-M9Pa9C65 z=I}}hTO4vV_sGD1-pO0Mj&bG@60N_@ruC!eB>BFf^=EWLJ8+!PYTYvT11k@jkVlJ5 z7Pa0#;esf0w1yh~b#Hw%TTN)|0Z8q;n8e#JEjA##0%aww;Q8Y$fAREw5ET!8BlLml zkkPAcl~w(}F;GP-JN+YzoIf_ZZkdU>EvlnPF+}vI_B|(T%k7r6JZ+;?^I;y6SzFuD z3l3u&uZB5x792j(ZYYpVgVAk7`PzuCA2j2p6yGZYC`p@p@L%6_)RO1-Ux_3XzX1;n zIGR_#if^%Q`A9kf7bFmBP&)W0m>BsZAG>aO87(Gl8I@w(HYzz$Wn`|pa}Gq0^Q**K z=(|CQd)}FX&j{$MN8;$#F5JiOx#Iirqrs}hWnb_ZANIiR9&br~vvobXBg1>V|H)1f zxF>Gm5zNFc8DMP@W_Poou)zTSGl9|ct-i*k0n^!g*qYWd zO9l1IsVm#!a9IWXe*!X9&-v0;tfXvAffc3o^-GL>!r!or0*jiO)MouqSx=v(qyiSL z=i->_x|-+BeHZ|DZw+=a+rDZ~ z>gdvnPq3V?>G_lIE9CYlZg`tx$#Q(P|sal1WTbegMezbC*jDdpn z26dkFJU9)P?9HB#eew-HFWFr=%?g^ zp-~xyb#+6_Pw02t;2f_L%}?+ey*)R~dU<9$eD$fMJQ`X;m}FEPDk}_6vYw_SrUQQF zR}>lP9W1jX;CZxH66iEd7SjkhCcXghy5}hw8n(c=Qj+vj0bjVDf^5`3nVT1SR%i3t zZgQiCyU=161D|gJz+kWuj`eyg1pTwGB1NVFZps(AF`{PDQUb^*J>itON(ftFpLt28 z;26|v!;fZ(^_0Tf&CSgsZ<68nC0*3_@82yuCZ#G$O8CLf`$MA#qpG1n4rqnAKC09daQ4~i zavCC2m*Pt53re#{d?<>HEBR3?GxT2Ba&g*x%UY@YmpH9+ zQZLS_wnXm|#-~N-C}>)?zGDOcfk`8s*PkvI)E&=#uVMP#m3ehqRb7{?t~<_gFA6Q* zfBLH0ZzCsNT%hs(JMl2&7IHVRV4ZDs+t+e?I)~coz06T^wQVY(PEsqd;}-QPWKoDD zew!wKyk=?%b4ft+*5G}-3*8zNxr>X0SH)*PZqc?KdLL16q1d^6xd9MXbPNj+KP(vm zotWhN!hQ&Jm``-)>dBk^pDMaxi+t)5d--T!t(wh_ZH1Q$ii)|Ogls;w(Ew2R1$3Qos!aM z{8u?^E7+`#e%b9#U}wixS~0!rb;x6!F{jI(@!va))El_I)02%eRUQq#U|QXGpG!-( zo!TSX`pqQIVF#vn)%Hs*`n;|_ipDq}sJ@4XCv7J3hYJzxcAQJb){l$p7LJL{L{ztP za&;h!EIfBvlm)N0)mYBuaDiH<269 zdzY!x;^Va(tvYY5j=Aa$ycm7D0!QDR!`@M=`P0X)EWRd$z%nAh2 z0}UH70s*o;&ikrT@mEE{gJZi@i(hOKV`EC^iWPSEE4tjT-Y` z*(k&eT%FNuRiqWCcO8vE3xfpg<_76dON5JgKw$y2 zOgh;l7m=Nr%)!-uYC0(8KEM2)?(Ep^q@b>BaYx04NZrExMWNMj!kxcNS-IUvxVutJ^$%Ts{pny*M zh1Qkjzv9iprp88AOSjKInSYCWuD2aA9=JqdTBV`?qj?j~)h@EZ5tOJ$*v_Jt9JvJXM>+C~}hoI`+Y~dODI89l~fDuM@OE z$jc|;%HOK|aq3E3U0Vm@3&$$+A1g*h)gz|O8uGuzT~0+`5jH>1d&7g=%{5c=R8k5z zucqHn3kVm?0Y~$SdT~SUN7QaIjE#THBET`~VdrQDdwc-%2<)#gMIxlw21}6xW~T*% z!dx}oQn>ve%uQd)87eHKG+)Rqv>|dYzyHMaB|ERcKDNJqO_mIwU7IAJ5XvpdE}dbGn{?Cb>D?T^V#|wBLcAIUGiv)gt7|qMJaq)X?nj zNsXf)N?*#!3I38~LKh|6*TdZ(+F|>Zs{gMjNA9V~f7Ki4I*z=2=OR2!)W1WF_hm40 z3hOFFdC2?INB_InzI{qJ29ut^th=s){W^RSEUY|vz+d0Fy2f80R{;u5bFzR7s;(% zw8iErH`q(i^4O~d_WcnHv&85!la{_d*L^8e|D(=WdD9ARaFg)S*Nx57gqW0UiPlnp z%Em-VNkzO<*Akl>6vP-J&jJGlT-}(IW%9RIH#8V0gU+Hxe62fm^ygB$jw-5}jK0Uf zQ!FDSxxpIz1}#2O*E?f(ahwuY831t_kx$>2U2iO5mkHzTuW1Awb;p-nR}G7YPD>Lu zccC8=&^2&r0F$b=<4+==`x5m$&nrtyaK~NZ$nuA6@^Q|qRTYu*kp#<=*(_HCwt6}MV7Di6&_6i%?gp5tiEt1YIvEcL z&~X5_E|!p}OYC|Xwr{eho%}R*+nan(aAG+dVm%lW;GEDBBKr5#jWa3rf?rAP^XG^~ z*CSl(q424gq>)JX@Te$(#Y)k8L-*spb2F?x2Ar61EQ+45KiNR2qRYjyQRCv59xLzM z%7>i-yu+q-=Kd?E`U{IN_8&y=A=dUQ+P<&j=jR_f=s<1Fv>@)Q9%p=uRMXg?nZUZ! zg9Q^%-{PXtLY+fO`_Zgl7cAjVo~&!HK>g-oxp6;N$f}9wos3@9Q5To*a)FEM>O!$% zb+*Kd=g&W^s8-gL7<3wUF0w&mMm!r+eU@=rF0T1DqdX;(E!p)l0D!gBzS!sIX6Uvc`q?8Ijbaprx5 zarg*S0NRaS-s*w};_}`rsgCo4y+W_n)7d$-won=kSoVys2ssLb~;_HW>f$3@|+>6;w%eCmuT6(`K?Gdt|WhNX299j^-+qi z|I3v&;E{0CX;`&j5l3(6=AeVnX!3Gqvbn|_OAdz-CjxYl*3xKxeD08NCw}sckVbTV z`R^NjW4DY=ibQ3jcj#mK9tVcS1Fu-sc#ytL-m5}vKO24eE{DywGnf;Zx#VMe8K;rJi%`Kun)2+w8)b2g^RoE`tyqt;i-{b^%XZy9- zC$-GTr;PiU-x>8k?1Y&#q+KBVye}r97BlIiwO~hrL^!T{n}-!S6RW@TXR@NFb!%knsOS5j6_(uGu&`CN4o`iG#PZRggC`0*!yl6+8tDJcKMJ zFo3>iS#9TO0Jiuw@%EFjIhx=M?H6L~+F9V(*jPqyY}6pS9(2ae(;*8LYfk817uhA` zXOP|U+j4p5@t=IVIX&#>(q`mY+&D{&oSb^cc&ABw)#-&-EDuh{T6f;$tHKwqsV1$W z=|yy^I$)o67xK#&-ywlL-o?X2bH-&?vO7YUL#=ix#`yuFtL9YdltO=miQOzOCW5w; zXIrVdl$BKl{2^DuzaZ2T)!@mw_nQ65ZEj=YhE?s?Sc8LO{9sz5o0f9UE{#^;a8gzq za&YwqADwmk;%YHu*~0?}A5^D-;#vxEVUcYCKJ^5P>sZYP5xFG1pXSZbi67fpI#bHM zol!=#0Hrea;vYr%-~~M=;Ec59x?N4PWdLV_tBw#e?@Yckii!YU%(guya}FM-;7S<< zfXmf}4Xrb69G^WKpb{_Xq8*TWlf!8bvW#79x|8lS_S{=>T~haF9q&2~r5)jlou0N3 zndN2d*!)?q^Um1bJ`&E0mmhP7vOI$%v#TlF5sKW#bkbHnZtvhQFH>v)YQ}#>v@)C4!)KLZCZwg944*X1E-~A zERdZrT465(I)yvGYmqZ5scWyeA-`S6Qxm+c#;hLYIHLMjYEqxmCwbHpSA3B|=V2PN zUZBIn$1^c?)R{uvJNd*K!EJH9p!Y-zb>(xdDObnGi zBynmp;%r%aqg(79Y>vEkQ{%g}%B#4t>JUGJmygyzF1f)fjlm<@DaZ3Fv0h$B9j9v}eLF#Knb|$kx|aW!bFI^m@FG!#2(lmsH;JhO;R+E&0f%P{_;h#2N zp)M>NQ!2WhKXtAr&qOd`!YX^gKZ5ZasHdZ8c?^h*NN7dhEP22M!CD2B^Ib9TQ3_&R z*LwoTI|+Aj=szVW;RT^?)X@u3u5!28tZFxgeSlu8UD?~}>=h$R(lf-8a-;97SA%HR zAk^q^-TXK{F0M#)ySn{0+jfam1?6*Q6dn{oo4z3&}&ueGl4s(-CF$(|Iv|u%@n20pwuLa55Pq;CenOA*|Fy_O#JRVV zhwRkcz`?-IYX(b*k{FHjISl8>#Bbj}5 z!RpglJ9Wr^RlEscjC7F{7PY&w)xh-ZaZ|Mo^*}A`RRzJjeMwO$k1;sq;eNcmpVim1=3#ZZWFw{bseIz1Y^?HeowYp{3|l$Y zlnhoVPAF-Q11R@rKpncfW91U=ezaUc9XG|^U}KwR6cS0r_gxw#44R62xLdl0vcDm_ z4s|w9QK@wv%)q2VD~wT9Q8Tb{@tcv8v#B1BuJ^%v6PU`WoRO8k^JS@RaNU#NT-mm5 z>79iI-b9|fopUc?;6$g#qvrG*7BrO&qR$ubl}s=BBpP0vUaB;Dlnz1G(mRE$##LK|W*8NMWssmDf>OlqT+>tg<43)w@~Z@$T&4TDrp5dw z!J<(|%=?2-q72ieVCJ*coKaa_$)LtMu2NCXcNG~ZNZxE<>_Ctx$LwqYY+70eUakWE zTd!3{gEl#MN7?2vKP&KDL*&Viz3;kKYo_T^KedLo3IiLqa1Xn)y`6cyEISx+@LKzN)*&yt<9DYwqzbIOe8{Znp?3ZFI~Dme(>a6-4x575%0DCu(fnH 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�?H;xvPR*)OFh<1tl`eqG4yTyI1l060fYQR@ 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 diff --git a/Unicolour/Unicolour.cs b/Unicolour/Unicolour.cs index c8e1ab81..e7f3af68 100644 --- a/Unicolour/Unicolour.cs +++ b/Unicolour/Unicolour.cs @@ -21,6 +21,7 @@ public partial class Unicolour : IEquatable private Oklch? oklch; private Cam02? cam02; private Cam16? cam16; + private Hct? hct; internal readonly ColourRepresentation InitialRepresentation; internal readonly ColourSpace InitialColourSpace; @@ -43,6 +44,7 @@ public partial class Unicolour : IEquatable public Oklch Oklch => Get(ColourSpace.Oklch); public Cam02 Cam02 => Get(ColourSpace.Cam02); public Cam16 Cam16 => Get(ColourSpace.Cam16); + public Hct Hct => Get(ColourSpace.Hct); public Alpha Alpha { get; } public Configuration Config { get; } diff --git a/Unicolour/Unicolour.csproj b/Unicolour/Unicolour.csproj index ca3e41f1..74a24f6b 100644 --- a/Unicolour/Unicolour.csproj +++ b/Unicolour/Unicolour.csproj @@ -7,7 +7,7 @@ Wacton.Unicolour $(AssemblyName) William Acton - Colour conversion, interpolation, and comparison + Colour conversion, interpolation, and comparison for .NET William Acton https://gitlab.com/Wacton/Unicolour https://gitlab.com/Wacton/Unicolour @@ -15,9 +15,9 @@ netstandard2.0 True Resources\Unicolour.png - 2.4.0 - colour color RGB HSB HSV HSL HWB XYZ xyY LAB LUV LCH LCHab LCHuv HSLuv HPLuv ICtCp JzAzBz JzCzHz Oklab Oklch CAM02 CAM16 converter colour-converter colour-conversion color-converter color-conversion colour-space colour-spaces color-space color-spaces interpolation colour-interpolation color-interpolation comparison colour-comparison color-comparison contrast luminance deltaE chromaticity display-p3 rec-2020 temperature cct duv cvd colour-vision-deficiency color-vision-deficiency colour-blindness color-blindness protanopia deuteranopia tritanopia achromatopsia - Add colour vision deficiency (CVD) / colour blindness simulation + 2.5.0 + colour color RGB HSB HSV HSL HWB XYZ xyY LAB LUV LCH LCHab LCHuv HSLuv HPLuv ICtCp JzAzBz JzCzHz Oklab Oklch CAM02 CAM16 HCT converter colour-converter colour-conversion color-converter color-conversion colour-space colour-spaces color-space color-spaces interpolation colour-interpolation color-interpolation comparison colour-comparison color-comparison contrast luminance deltaE chromaticity display-p3 rec-2020 temperature cct duv cvd colour-vision-deficiency color-vision-deficiency colour-blindness color-blindness protanopia deuteranopia tritanopia achromatopsia + Add HCT support Resources\Unicolour.ico LICENSE diff --git a/Unicolour/UnicolourConstructors.cs b/Unicolour/UnicolourConstructors.cs index 1bc220ae..09a75be4 100644 --- a/Unicolour/UnicolourConstructors.cs +++ b/Unicolour/UnicolourConstructors.cs @@ -127,4 +127,10 @@ public static Unicolour FromHex(Configuration config, string hex) public static Unicolour FromCam16(Configuration config, (double j, double a, double b) tuple, double alpha = 1.0) => FromCam16(config, tuple.j, tuple.a, tuple.b, alpha); public static Unicolour FromCam16(Configuration config, double j, double a, double b, double alpha = 1.0) => new(config, new Cam16(j, a, b, config.Cam), new Alpha(alpha)); internal static Unicolour FromCam16(Configuration config, ColourHeritage heritage, double j, double a, double b, double alpha = 1.0) => new(config, new Cam16(new Cam.Ucs(j, a, b), config.Cam, heritage), new Alpha(alpha)); + + public static Unicolour FromHct(double h, double c, double t, double alpha = 1.0) => FromHct(Configuration.Default, h, c, t, alpha); + public static Unicolour FromHct((double h, double c, double t) tuple, double alpha = 1.0) => FromHct(Configuration.Default, tuple.h, tuple.c, tuple.t, alpha); + public static Unicolour FromHct(Configuration config, (double h, double c, double t) tuple, double alpha = 1.0) => FromHct(config, tuple.h, tuple.c, tuple.t, alpha); + public static Unicolour FromHct(Configuration config, double h, double c, double t, double alpha = 1.0) => new(config, new Hct(h, c, t), new Alpha(alpha)); + internal static Unicolour FromHct(Configuration config, ColourHeritage heritage, double h, double c, double t, double alpha = 1.0) => new(config, new Hct(h, c, t, heritage), new Alpha(alpha)); } \ No newline at end of file diff --git a/Unicolour/UnicolourLookups.cs b/Unicolour/UnicolourLookups.cs index f8405a5b..97defe95 100644 --- a/Unicolour/UnicolourLookups.cs +++ b/Unicolour/UnicolourLookups.cs @@ -1,6 +1,6 @@ namespace Wacton.Unicolour; -internal enum ColourSpace { Rgb, RgbLinear, Rgb255, Hsb, Hsl, Hwb, Xyz, Xyy, Lab, Lchab, Luv, Lchuv, Hsluv, Hpluv, Ictcp, Jzazbz, Jzczhz, Oklab, Oklch, Cam02, Cam16 } +internal enum ColourSpace { Rgb, RgbLinear, Rgb255, Hsb, Hsl, Hwb, Xyz, Xyy, Lab, Lchab, Luv, Lchuv, Hsluv, Hpluv, Ictcp, Jzazbz, Jzczhz, Oklab, Oklch, Cam02, Cam16, Hct } public partial class Unicolour { @@ -31,7 +31,8 @@ public partial class Unicolour { typeof(Oklab), ColourSpace.Oklab }, { typeof(Oklch), ColourSpace.Oklch }, { typeof(Cam02), ColourSpace.Cam02 }, - { typeof(Cam16), ColourSpace.Cam16 } + { typeof(Cam16), ColourSpace.Cam16 }, + { typeof(Hct), ColourSpace.Hct } }; internal List GetRepresentations(List colourSpaces) => colourSpaces.Select(GetRepresentation).ToList(); @@ -60,6 +61,7 @@ internal ColourRepresentation GetRepresentation(ColourSpace colourSpace) ColourSpace.Oklch => Oklch, ColourSpace.Cam02 => Cam02, ColourSpace.Cam16 => Cam16, + ColourSpace.Hct => Hct, _ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null) }; } @@ -108,6 +110,7 @@ private T Get(ColourSpace targetSpace) where T : ColourRepresentation ColourSpace.Oklch => oklch, ColourSpace.Cam02 => cam02, ColourSpace.Cam16 => cam16, + ColourSpace.Hct => hct, _ => throw new ArgumentOutOfRangeException(nameof(colourSpace), colourSpace, null) }; } @@ -135,6 +138,7 @@ private void SetBackingField(ColourSpace targetSpace) ColourSpace.Oklch => () => oklch = EvaluateOklch(), ColourSpace.Cam02 => () => cam02 = EvaluateCam02(), ColourSpace.Cam16 => () => cam16 = EvaluateCam16(), + ColourSpace.Hct => () => hct = EvaluateHct(), _ => throw new ArgumentOutOfRangeException(nameof(targetSpace), targetSpace, null) }; @@ -217,6 +221,7 @@ private Xyz EvaluateXyz() ColourSpace.Oklch => Oklab.ToXyz(Oklab, Config.Xyz), ColourSpace.Cam02 => Cam02.ToXyz(Cam02, Config.Cam, Config.Xyz), ColourSpace.Cam16 => Cam16.ToXyz(Cam16, Config.Cam, Config.Xyz), + ColourSpace.Hct => Hct.ToXyz(Hct, Config.Xyz), _ => throw new ArgumentOutOfRangeException() }; } @@ -354,4 +359,13 @@ private Cam16 EvaluateCam16() _ => Cam16.FromXyz(Xyz, Config.Cam, Config.Xyz) }; } + + private Hct EvaluateHct() + { + return InitialColourSpace switch + { + ColourSpace.Hct => (Hct)InitialRepresentation, + _ => Hct.FromXyz(Xyz, Config.Xyz) + }; + } } \ No newline at end of file diff --git a/Unicolour/Xyz.cs b/Unicolour/Xyz.cs index 6868b318..4208e1aa 100644 --- a/Unicolour/Xyz.cs +++ b/Unicolour/Xyz.cs @@ -7,9 +7,9 @@ public record Xyz : ColourRepresentation public double Y => Second; public double Z => Third; - // could compare whitepoint against config.XyzWhitePoint - // but requires making assumptions about floating-point comparison, which I don't want to do - internal override bool IsGreyscale => false; + // no clear luminance upper-bound; usually Y >= 1 is max luminance + // but since custom white points can be provided, don't want to make the assumption + internal override bool IsGreyscale => Y <= 0; public Xyz(double x, double y, double z) : this(x, y, z, ColourHeritage.None) {} internal Xyz(ColourTriplet triplet, ColourHeritage heritage) : this(triplet.First, triplet.Second, triplet.Third, heritage) {} @@ -24,4 +24,8 @@ internal Xyz(double x, double y, double z, ColourHeritage heritage) : base(x, y, * XYZ is considered the root colour representation (in terms of Unicolour implementation) * so does not contain any forward (from another space) or reverse (back to original space) functions */ + + // only for potential debugging or diagnostics + // until there is an "official" HCT -> XYZ reverse transform + internal HctToXyzSearchResult? HctToXyzSearchResult; } \ No newline at end of file