Skip to content

Commit

Permalink
Merge branch 'luv' into 'main'
Browse files Browse the repository at this point in the history
Add LUV support

See merge request Wacton/Unicolour!9
  • Loading branch information
waacton committed Apr 16, 2022
2 parents fe2b388 + df8bfd5 commit 15f8027
Show file tree
Hide file tree
Showing 32 changed files with 941 additions and 411 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ A `Unicolour` encapsulates a single colour and its representation across differe
- HSL
- CIE XYZ
- CIE LAB
- CIE LUV
- ~~CIE LCHab~~ _(coming soon)_
- ~~CIE LCHuv~~ _(coming soon)_
- Oklab

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

Expand All @@ -52,6 +56,7 @@ var hsb = unicolour.Hsb;
var hsl = unicolour.Hsl;
var xyz = unicolour.Xyz;
var lab = unicolour.Lab;
var luv = unicolour.Luv;
var oklab = unicolour.Oklab;
```

Expand All @@ -62,6 +67,7 @@ var interpolated = unicolour1.InterpolateHsb(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateHsl(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateXyz(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateLab(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateLuv(unicolour2, 0.5);
var interpolated = unicolour1.InterpolateOklab(unicolour2, 0.5);
```

Expand Down
15 changes: 9 additions & 6 deletions Unicolour.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
var font = fontFamily.CreateFont(24);
var textRgba32 = AsRgba32(Unicolour.FromHex("#E8E8FF"));

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

var image = new Image<Rgba32>(gradientWidth * 2, gradientHeight * labels.Count);
var image = new Image<Rgba32>(gradientWidth * 3, gradientHeight * labels.Count);
Draw(purple, orange, 0);
Draw(black, cyan, 1);
Draw(pink, cyan, 1);
Draw(black, green, 2);
image.Save("gradients.png");

void Draw(Unicolour start, Unicolour end, int column)
{
Expand All @@ -35,6 +39,7 @@ void Draw(Unicolour start, Unicolour end, int column)
start.InterpolateHsl(end, distance),
start.InterpolateXyz(end, distance),
start.InterpolateLab(end, distance),
start.InterpolateLuv(end, distance),
start.InterpolateOklab(end, distance)
};

Expand All @@ -49,8 +54,6 @@ void Draw(Unicolour start, Unicolour end, int column)
}
}

image.Save("gradients.png");

void SetPixels(int column, int pixelIndex, List<Unicolour> unicolours)
{
for (var y = 0; y < gradientHeight; y++)
Expand Down
Binary file modified Unicolour.Example/gradients.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 7 additions & 6 deletions Unicolour.Tests/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static void StandardRgbD65ToXyzD65()
{
Xyz = new(0.200757, 0.119618, 0.506757),
Lab = new(41.1553, 51.4108, -56.4485),
// Luv = new(41.1553, 16.3709, -86.7190)
Luv = new(41.1553, 16.3709, -86.7190)
};

Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrixA).Within(0.0005));
Expand Down Expand Up @@ -113,7 +113,7 @@ public static void StandardRgbD65ToXyzD50()
{
Xyz = new(0.187691, 0.115771, 0.381093),
Lab = new(40.5359, 46.0847, -57.1158),
// Luv = new(40.5359, 18.7523, -78.2057)
Luv = new(40.5359, 18.7523, -78.2057)
};

Assert.That(rgbToXyzMatrix.Data, Is.EqualTo(expectedMatrix).Within(0.0000001));
Expand Down Expand Up @@ -167,7 +167,7 @@ public static void AdobeRgbD65ToXyzD65()
{
Xyz = new(0.234243, 0.134410, 0.535559),
Lab = new(43.4203, 57.3600, -55.4259),
// Luv = new(43.4203, 25.4480, -87.3268)
Luv = new(43.4203, 25.4480, -87.3268)
};

AssertColour(unicolour, expectedColour);
Expand Down Expand Up @@ -210,7 +210,7 @@ public static void AdobeRgbD65ToXyzD50()
{
Xyz = new(0.221673, 0.130920, 0.402670),
Lab = new(42.9015, 52.4152, -55.9013),
// Luv = new(42.9015, 29.0751, -78.5576)
Luv = new(42.9015, 29.0751, -78.5576)
};

AssertColour(unicolour, expectedColour);
Expand Down Expand Up @@ -253,7 +253,7 @@ public static void WideGamutRgbD50ToXyzD65()
{
Xyz = new(0.251993, 0.102404, 0.550393),
Lab = new(38.2704, 87.2838, -65.7493),
// Luv = new(38.2704, 47.3837, -99.6819)
Luv = new(38.2704, 47.3837, -99.6819)
};

AssertColour(unicolour, expectedColour);
Expand Down Expand Up @@ -296,7 +296,7 @@ public static void WideGamutRgbD50ToXyzD50()
{
Xyz = new(0.238795, 0.099490, 0.413181),
Lab = new(37.7508, 82.3084, -66.1402),
// Luv = new(37.7508, 55.1488, -91.6044)
Luv = new(37.7508, 55.1488, -91.6044)
};

AssertColour(unicolour, expectedColour);
Expand Down Expand Up @@ -327,5 +327,6 @@ private static void AssertColour(Unicolour unicolour, TestColour expected)
if (expected.Rgb != null) AssertUtils.AssertColourTriplet(unicolour.Rgb.Triplet, expected.Rgb!, 0.01);
if (expected.Xyz != null) AssertUtils.AssertColourTriplet(unicolour.Xyz.Triplet, expected.Xyz!, 0.001);
if (expected.Lab != null) AssertUtils.AssertColourTriplet(unicolour.Lab.Triplet, expected.Lab!, 0.05);
if (expected.Luv != null) AssertUtils.AssertColourTriplet(unicolour.Luv.Triplet, expected.Luv!, 0.1);
}
}
26 changes: 22 additions & 4 deletions Unicolour.Tests/ConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class ConversionTests
private const double HslTolerance = 0.00000001;
private const double XyzTolerance = 0.00000001;
private const double LabTolerance = 0.00000001;
private const double LuvTolerance = 0.00000001;
private const double OklabTolerance = 0.000001;

[Test]
Expand Down Expand Up @@ -46,6 +47,9 @@ public void HslSameAfterDeconversion()
[Test]
public void LabSameAfterDeconversion() => AssertUtils.AssertRandomLabColours(AssertLabDeconversion);

[Test]
public void LuvSameAfterDeconversion() => AssertUtils.AssertRandomLuvColours(AssertLuvDeconversion);

[Test]
public void OklabSameAfterDeconversion() => AssertUtils.AssertRandomOklabColours(AssertOklabDeconversion);

Expand All @@ -54,7 +58,7 @@ 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, Configuration.Default);
var hsb = Conversion.RgbToHsb(rgb);
var hsl = Conversion.RgbToHsl(rgb);
var hsl = Conversion.HsbToHsl(hsb);

var expectedRoundedHsb = namedColour.Hsb;
var expectedRoundedHsl = namedColour.Hsl;
Expand Down Expand Up @@ -100,16 +104,22 @@ private static void AssertHsbDeconversion(Hsb original)
private static void AssertHslDeconversion(ColourTriplet triplet) => AssertHslDeconversion(new Hsl(triplet.First, triplet.Second, triplet.Third));
private static void AssertHslDeconversion(Hsl original)
{
var deconverted = Conversion.RgbToHsl(Conversion.HsbToRgb(Conversion.HslToHsb(original), Configuration.Default));
var deconverted = Conversion.HsbToHsl(Conversion.HslToHsb(original));
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, HslTolerance, true);
}

private static void AssertXyzDeconversion(ColourTriplet triplet) => AssertXyzDeconversion(new Xyz(triplet.First, triplet.Second, triplet.Third));
private static void AssertXyzDeconversion(Xyz original)
{
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
var deconverted = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, XyzTolerance);
var deconvertedViaLab = Conversion.LabToXyz(Conversion.XyzToLab(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTriplet(deconvertedViaLab.Triplet, original.Triplet, XyzTolerance);

var deconvertedViaLuv = Conversion.LuvToXyz(Conversion.XyzToLuv(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTriplet(deconvertedViaLuv.Triplet, original.Triplet, XyzTolerance);

var deconvertedViaOklab = Conversion.OklabToXyz(Conversion.XyzToOklab(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTriplet(deconvertedViaOklab.Triplet, original.Triplet, XyzTolerance);
}

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

private static void AssertLuvDeconversion(ColourTriplet triplet) => AssertLuvDeconversion(new Luv(triplet.First, triplet.Second, triplet.Third));
private static void AssertLuvDeconversion(Luv original)
{
// note: cannot test deconversion via RGB space as XYZ <-> RGB is not 1:1
var deconverted = Conversion.XyzToLuv(Conversion.LuvToXyz(original, Configuration.Default), Configuration.Default);
AssertUtils.AssertColourTriplet(deconverted.Triplet, original.Triplet, LuvTolerance);
}

private static void AssertOklabDeconversion(ColourTriplet triplet) => AssertOklabDeconversion(new Oklab(triplet.First, triplet.Second, triplet.Third));
private static void AssertOklabDeconversion(Oklab original)
{
Expand Down
131 changes: 131 additions & 0 deletions Unicolour.Tests/CoordinateSpaceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
namespace Wacton.Unicolour.Tests;

using System;
using NUnit.Framework;
using Wacton.Unicolour.Tests.Utils;

/*
* Conversions that are just translations to/from cylindrical coordinate spaces
* should have input values clamped to the range of the source coordinate space
* otherwise they will be mapped to incorrect values
* e.g. RGB with negative -> HSB should produce the same value as RGB with zero -> HSB
*/
public class CoordinateSpaceTests
{
[Test]
public void CartesianRgbToCylindricalHsb()
{
ColourTriplet upperOutRange = new(1.00001, 100, double.PositiveInfinity);
ColourTriplet upperInRange = new(1, 1, 1);
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
ColourTriplet lowerInRange = new(0, 0, 0);
AssertRgbToHsb(upperInRange, upperOutRange);
AssertRgbToHsb(lowerInRange, lowerOutRange);
}

[Test]
public void CylindricalHsbToCartesianRgb()
{
ColourTriplet upperOutRange = new(360.00001, 100, double.PositiveInfinity);
ColourTriplet upperInRange = new(0.00001, 1, 1);
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
ColourTriplet lowerInRange = new(359.99999, 0, 0);
AssertHsbToRgb(upperInRange, upperOutRange);
AssertHsbToRgb(lowerInRange, lowerOutRange);
}

[Test]
public void CylindricalHsbToCylindricalHsl()
{
ColourTriplet upperOutRange = new(360.00001, 100, double.PositiveInfinity);
ColourTriplet upperInRange = new(0.00001, 1, 1);
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
ColourTriplet lowerInRange = new(359.99999, 0, 0);
AssertHsbToHsl(upperInRange, upperOutRange);
AssertHsbToHsl(lowerInRange, lowerOutRange);
}

[Test]
public void CylindricalHslToCylindricalHsb()
{
ColourTriplet upperOutRange = new(360.00001, 100, double.PositiveInfinity);
ColourTriplet upperInRange = new(0.00001, 1, 1);
ColourTriplet lowerOutRange = new(-0.00001, -100, double.NegativeInfinity);
ColourTriplet lowerInRange = new(359.99999, 0, 0);
AssertHslToHsb(upperInRange, upperOutRange);
AssertHslToHsb(lowerInRange, lowerOutRange);
}

private static void AssertRgbToHsb(ColourTriplet inRange, ColourTriplet outRange)
{
Rgb GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third, Configuration.Default);
var inRangeInput = GetInput(inRange);
var outRangeInput = GetInput(outRange);

Hsb GetOutput(Rgb rgb) => Conversion.RgbToHsb(rgb);
var inRangeOutput = GetOutput(inRangeInput);
var outRangeOutput = GetOutput(outRangeInput);

AssertTriplets(
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
}

private static void AssertHsbToRgb(ColourTriplet inRange, ColourTriplet outRange)
{
Hsb GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third);
var inRangeInput = GetInput(inRange);
var outRangeInput = GetInput(outRange);

Rgb GetOutput(Hsb hsb) => Conversion.HsbToRgb(hsb, Configuration.Default);
var inRangeOutput = GetOutput(inRangeInput);
var outRangeOutput = GetOutput(outRangeInput);

AssertTriplets(
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
}

private static void AssertHsbToHsl(ColourTriplet inRange, ColourTriplet outRange)
{
Hsb GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third);
var inRangeInput = GetInput(inRange);
var outRangeInput = GetInput(outRange);

Hsl GetOutput(Hsb hsb) => Conversion.HsbToHsl(hsb);
var inRangeOutput = GetOutput(inRangeInput);
var outRangeOutput = GetOutput(outRangeInput);

AssertTriplets(
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
}

private static void AssertHslToHsb(ColourTriplet inRange, ColourTriplet outRange)
{
Hsl GetInput(ColourTriplet triplet) => new(triplet.First, triplet.Second, triplet.Third);
var inRangeInput = GetInput(inRange);
var outRangeInput = GetInput(outRange);

Hsb GetOutput(Hsl hsl) => Conversion.HslToHsb(hsl);
var inRangeOutput = GetOutput(inRangeInput);
var outRangeOutput = GetOutput(outRangeInput);

AssertTriplets(
AsTriplets(inRangeInput), AsTriplets(outRangeInput),
AsTriplets(inRangeOutput), AsTriplets(outRangeOutput));
}

private static void AssertTriplets(Triplets inRangeInput, Triplets outRangeInput, Triplets inRangeOutput, Triplets outRangeOutput)
{
AssertUtils.AssertColourTriplet(outRangeInput.Constrained, inRangeInput.Unconstrained, 0.00001);
AssertUtils.AssertColourTriplet(outRangeOutput.Unconstrained, inRangeOutput.Unconstrained, 0.00001);
AssertUtils.AssertColourTriplet(outRangeOutput.Constrained, inRangeOutput.Unconstrained, 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 record Triplets(ColourTriplet Unconstrained, ColourTriplet Constrained);
}
19 changes: 19 additions & 0 deletions Unicolour.Tests/EqualityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public void EqualLabGivesEqualObjects()
AssertUnicoloursEqual(unicolour1, unicolour2);
}

[Test]
public void EqualLuvGivesEqualObjects()
{
var unicolour1 = GetRandomLuvUnicolour();
var unicolour2 = Unicolour.FromLuv(unicolour1.Luv.L, unicolour1.Luv.U, unicolour1.Luv.V, unicolour1.Alpha.A);
AssertUnicoloursEqual(unicolour1, unicolour2);
}

[Test]
public void EqualOklabGivesEqualObjects()
{
Expand Down Expand Up @@ -99,6 +107,15 @@ public void NotEqualLabGivesNotEqualObjects()
AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Lab.Triplet);
}

[Test]
public void NotEqualLuvGivesNotEqualObjects()
{
var unicolour1 = GetRandomLuvUnicolour();
var differentTuple = GetDifferent(unicolour1.Luv.Triplet, 1.0).Tuple;
var unicolour2 = Unicolour.FromLuv(differentTuple, unicolour1.Alpha.A + 0.1);
AssertUnicoloursNotEqual(unicolour1, unicolour2, unicolour => unicolour.Luv.Triplet);
}

[Test]
public void NotEqualOklabGivesNotEqualObjects()
{
Expand Down Expand Up @@ -144,6 +161,7 @@ public void DifferentConfigurationObjects()
private static Unicolour GetRandomHslUnicolour() => Unicolour.FromHsl(TestColours.GetRandomHsl().Tuple, TestColours.GetRandomAlpha());
private static Unicolour GetRandomXyzUnicolour() => Unicolour.FromXyz(TestColours.GetRandomXyz().Tuple, TestColours.GetRandomAlpha());
private static Unicolour GetRandomLabUnicolour() => Unicolour.FromLab(TestColours.GetRandomLab().Tuple, TestColours.GetRandomAlpha());
private static Unicolour GetRandomLuvUnicolour() => Unicolour.FromLuv(TestColours.GetRandomLuv().Tuple, TestColours.GetRandomAlpha());
private static Unicolour GetRandomOklabUnicolour() => Unicolour.FromOklab(TestColours.GetRandomOklab().Tuple, TestColours.GetRandomAlpha());
private static ColourTriplet GetDifferent(ColourTriplet triplet, double diff = 0.1) => new(triplet.First + diff, triplet.Second + diff, triplet.Third + diff);

Expand All @@ -154,6 +172,7 @@ private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicol
AssertEqual(unicolour1.Hsl, unicolour2.Hsl);
AssertEqual(unicolour1.Xyz, unicolour2.Xyz);
AssertEqual(unicolour1.Lab, unicolour2.Lab);
AssertEqual(unicolour1.Luv, unicolour2.Luv);
AssertEqual(unicolour1.Oklab, unicolour2.Oklab);
AssertEqual(unicolour1.Alpha, unicolour2.Alpha);
AssertEqual(unicolour1.Luminance, unicolour2.Luminance);
Expand Down
Loading

0 comments on commit 15f8027

Please sign in to comment.