Skip to content

Commit

Permalink
Merge branch 'premultiply-alpha' into 'main'
Browse files Browse the repository at this point in the history
Add interpolation with premultiplied alpha

See merge request Wacton/Unicolour!37
  • Loading branch information
waacton committed Nov 4, 2023
2 parents 43b4397 + df590ed commit 63fb354
Show file tree
Hide file tree
Showing 69 changed files with 2,553 additions and 685 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,18 @@ var achromatopsia = unicolour.SimulateAchromatopsia();

## Examples ✨

This repo contains an [example project](Unicolour.Example/Program.cs) that uses `Unicolour` to generate gradients through different colour spaces
and for different colour vision deficiencies...
This repo contains an [example project](Unicolour.Example/Program.cs) that uses `Unicolour` to:
1. Generate gradients through different colour spaces
2. Render the colour spectrum with different colour vision deficiencies
3. Demonstrate interpolation with and without premultiplied alpha

![Gradients through different colour spaces generated from Unicolour](Unicolour.Example/gradients.png)

![Gradients for different colour vision deficiencies generated from Unicolour](Unicolour.Example/vision-deficiency.png)

... and a [console application](Unicolour.Console/Program.cs) that uses `Unicolour` to show colour information for a given hex value.
![Interpolation from red to transparent to blue, with and without premultiplied alpha](Unicolour.Example/alpha-interpolation.png)

There is also a [console application](Unicolour.Console/Program.cs) that uses `Unicolour` to show colour information for a given hex value:

![Colour information from hex value](Unicolour.Console/colour-info.png)

Expand Down
5 changes: 3 additions & 2 deletions Unicolour.Example/Gradient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal static class Gradient
{
private const bool RenderOutOfGamutAsTransparent = false;

internal delegate Unicolour Mix(Unicolour start, Unicolour end, double distance);
internal delegate Unicolour Mix(Unicolour start, Unicolour end, double amount);

internal static Image<Rgba32> Draw((string text, Unicolour colour) label, int width, int height,
Unicolour[] colourPoints, Mix mix)
Expand Down Expand Up @@ -61,7 +61,8 @@ private static void SetLabel(Image<Rgba32> image, string text, Unicolour colour)
private static Rgba32 AsRgba32(Unicolour unicolour)
{
var (r, g, b) = unicolour.Rgb.Byte255.ConstrainedTriplet;
var a = unicolour.IsInDisplayGamut || !RenderOutOfGamutAsTransparent ? 255 : 0;
var alpha = unicolour.Alpha.A255;
var a = unicolour.IsInDisplayGamut || !RenderOutOfGamutAsTransparent ? alpha : 0;
return new Rgba32((byte) r, (byte) g, (byte) b, (byte) a);
}
}
81 changes: 55 additions & 26 deletions Unicolour.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

GenerateColourSpaceGradients();
GenerateVisionDeficiencyGradients();
GenerateAlphaInterpolation();
return;

void GenerateColourSpaceGradients()
Expand Down Expand Up @@ -36,27 +37,27 @@ void GenerateColourSpaceGradients()

Image<Rgba32> DrawColumn(Unicolour[] colourPoints)
{
var rgb = Gradient.Draw(("RGB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixRgb(end, distance));
var rgbLinear = Gradient.Draw(("RGB Linear", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixRgbLinear(end, distance));
var hsb = Gradient.Draw(("HSB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHsb(end, distance));
var hsl = Gradient.Draw(("HSL", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHsl(end, distance));
var hwb = Gradient.Draw(("HWB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHwb(end, distance));
var xyz = Gradient.Draw(("XYZ", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixXyz(end, distance));
var xyy = Gradient.Draw(("xyY", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixXyy(end, distance));
var lab = Gradient.Draw(("LAB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixLab(end, distance));
var lchab = Gradient.Draw(("LCHab", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixLchab(end, distance));
var luv = Gradient.Draw(("LUV", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixLuv(end, distance));
var lchuv = Gradient.Draw(("LCHuv", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixLchuv(end, distance));
var hsluv = Gradient.Draw(("HSLuv", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHsluv(end, distance));
var hpluv = Gradient.Draw(("HPLuv", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHpluv(end, distance));
var ictcp = Gradient.Draw(("ICtCp", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixIctcp(end, distance));
var jzazbz = Gradient.Draw(("JzAzBz", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixJzazbz(end, distance));
var jzczhz = Gradient.Draw(("JzCzHz", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixJzczhz(end, distance));
var oklab = Gradient.Draw(("OKLAB", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixOklab(end, distance));
var oklch = Gradient.Draw(("OKLCH", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixOklch(end, distance));
var cam02 = Gradient.Draw(("CAM02", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixCam02(end, distance));
var cam16 = Gradient.Draw(("CAM16", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixCam16(end, distance));
var hct = Gradient.Draw(("HCT", light), columnWidth, rowHeight, colourPoints, (start, end, distance) => start.MixHct(end, distance));
var rgb = Gradient.Draw(("RGB", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixRgb(end, amount));
var rgbLinear = Gradient.Draw(("RGB Linear", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixRgbLinear(end, amount));
var hsb = Gradient.Draw(("HSB", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixHsb(end, amount));
var hsl = Gradient.Draw(("HSL", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixHsl(end, amount));
var hwb = Gradient.Draw(("HWB", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixHwb(end, amount));
var xyz = Gradient.Draw(("XYZ", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixXyz(end, amount));
var xyy = Gradient.Draw(("xyY", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixXyy(end, amount));
var lab = Gradient.Draw(("LAB", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixLab(end, amount));
var lchab = Gradient.Draw(("LCHab", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixLchab(end, amount));
var luv = Gradient.Draw(("LUV", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixLuv(end, amount));
var lchuv = Gradient.Draw(("LCHuv", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixLchuv(end, amount));
var hsluv = Gradient.Draw(("HSLuv", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixHsluv(end, amount));
var hpluv = Gradient.Draw(("HPLuv", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixHpluv(end, amount));
var ictcp = Gradient.Draw(("ICtCp", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixIctcp(end, amount));
var jzazbz = Gradient.Draw(("JzAzBz", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixJzazbz(end, amount));
var jzczhz = Gradient.Draw(("JzCzHz", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixJzczhz(end, amount));
var oklab = Gradient.Draw(("OKLAB", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixOklab(end, amount));
var oklch = Gradient.Draw(("OKLCH", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixOklch(end, amount));
var cam02 = Gradient.Draw(("CAM02", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixCam02(end, amount));
var cam16 = Gradient.Draw(("CAM16", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixCam16(end, amount));
var hct = Gradient.Draw(("HCT", light), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.MixHct(end, amount));

var columnImage = new Image<Rgba32>(columnWidth, rowHeight * rows);
columnImage.Mutate(context => context
Expand Down Expand Up @@ -105,15 +106,15 @@ void GenerateVisionDeficiencyGradients()

var dark = Unicolour.FromHex("#404046");
var none = Gradient.Draw(("No deficiency", dark), width, rowHeight, colourPoints,
(start, end, distance) => start.MixHsb(end, distance));
(start, end, amount) => start.MixHsb(end, amount));
var protanopia = Gradient.Draw(("Protanopia", dark), width, rowHeight, colourPoints,
(start, end, distance) => start.MixHsb(end, distance).SimulateProtanopia());
(start, end, amount) => start.MixHsb(end, amount).SimulateProtanopia());
var deuteranopia = Gradient.Draw(("Deuteranopia", dark), width, rowHeight, colourPoints,
(start, end, distance) => start.MixHsb(end, distance).SimulateDeuteranopia());
(start, end, amount) => start.MixHsb(end, amount).SimulateDeuteranopia());
var tritanopia = Gradient.Draw(("Tritanopia", dark), width, rowHeight, colourPoints,
(start, end, distance) => start.MixHsb(end, distance).SimulateTritanopia());
(start, end, amount) => start.MixHsb(end, amount).SimulateTritanopia());
var achromatopsia = Gradient.Draw(("Achromatopsia", dark), width, rowHeight, colourPoints,
(start, end, distance) => start.MixHsb(end, distance).SimulateAchromatopsia());
(start, end, amount) => start.MixHsb(end, amount).SimulateAchromatopsia());

var image = new Image<Rgba32>(width, rowHeight * rows);
image.Mutate(context => context
Expand All @@ -125,4 +126,32 @@ void GenerateVisionDeficiencyGradients()
);

image.Save("vision-deficiency.png");
}

void GenerateAlphaInterpolation()
{
const int width = 1000;
const int rows = 2;
const int rowHeight = 120;

var colourPoints = new[]
{
Unicolour.FromRgb(1, 0, 0, 1),
Unicolour.FromRgb(0, 0, 0, 0),
Unicolour.FromRgb(0, 0, 1, 1)
};

var black = Unicolour.FromHex("#000000");
var premultiplied = Gradient.Draw(("With premultiplied alpha", black), width, rowHeight, colourPoints,
(start, end, amount) => start.MixRgb(end, amount, true));
var notPremultiplied = Gradient.Draw(("Without premultiplied alpha", black), width, rowHeight, colourPoints,
(start, end, amount) => start.MixRgb(end, amount, false));

var image = new Image<Rgba32>(width, rowHeight * rows);
image.Mutate(context => context
.DrawImage(premultiplied, new Point(0, rowHeight * 0), 1f)
.DrawImage(notPremultiplied, new Point(0, rowHeight * 1), 1f)
);

image.Save("alpha-interpolation.png");
}
Binary file added Unicolour.Example/alpha-interpolation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
153 changes: 153 additions & 0 deletions Unicolour.Tests/ColourTripletTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
namespace Wacton.Unicolour.Tests;

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

public class ColourTripletTests
{
private static readonly List<TestCaseData> GetHueTestData = new()
{
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, null), null),
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, 0), 7.7),
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, 1), null),
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, 2), 9.9)
};

private static readonly List<TestCaseData> OverrideHueTestData = new()
{
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, null), 6.6, null),
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, 0), 6.6, new ColourTriplet(6.6, 8.8, 9.9, 0)),
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, 1), 6.6, null),
new TestCaseData(new ColourTriplet(7.7, 8.8, 9.9, 2), 6.6, new ColourTriplet(7.7, 8.8, 6.6, 0))
};

private static readonly List<TestCaseData> ModuloHueTestData = new()
{
new TestCaseData(new ColourTriplet(-270, 450, 810, null), new ColourTriplet(-270, 450, 810, null)),
new TestCaseData(new ColourTriplet(-270, 450, 810, 0), new ColourTriplet(90, 450, 810, 0)),
new TestCaseData(new ColourTriplet(-270, 450, 810, 1), null),
new TestCaseData(new ColourTriplet(-270, 450, 810, 2), new ColourTriplet(-270, 450, 90, 2))
};

private static readonly List<TestCaseData> PremultipliedAlphaTestData = new()
{
new TestCaseData(new ColourTriplet(2, 10, -8.8, null), 0.5, new ColourTriplet(1, 5, -4.4, null)),
new TestCaseData(new ColourTriplet(2, 10, -8.8, 0), 0.5, new ColourTriplet(2, 5, -4.4, 0)),
new TestCaseData(new ColourTriplet(2, 10, -8.8, 1), 0.5, null),
new TestCaseData(new ColourTriplet(2, 10, -8.8, 2), 0.5, new ColourTriplet(1, 5, -8.8, 2))
};

private static readonly List<TestCaseData> UnpremultipliedAlphaTestData = new()
{
new TestCaseData(new ColourTriplet(1, 5, -4.4, null), 0.5, new ColourTriplet(2, 10, -8.8, null)),
new TestCaseData(new ColourTriplet(1, 5, -4.4, 0), 0.5, new ColourTriplet(1, 10, -8.8, 0)),
new TestCaseData(new ColourTriplet(1, 5, -4.4, 1), 0.5, null),
new TestCaseData(new ColourTriplet(1, 5, -4.4, 2), 0.5, new ColourTriplet(2, 10, -4.4, 2))
};

[TestCase(0, 0, 0)]
[TestCase(0, 0.5, 1)]
[TestCase(1, 1, 1)]
[TestCase(double.MinValue, 0.5, 0.5)]
[TestCase(0.5, double.MinValue, 0.5)]
[TestCase(0.5, 0.5, double.MinValue)]
[TestCase(double.MaxValue, 0.5, 0.5)]
[TestCase(0.5, double.MaxValue, 0.5)]
[TestCase(0.5, 0.5, double.MaxValue)]
[TestCase(double.Epsilon, 0.5, 0.5)]
[TestCase(0.5, double.Epsilon, 0.5)]
[TestCase(0.5, 0.5, double.Epsilon)]
[TestCase(double.NegativeInfinity, 0.5, 0.5)]
[TestCase(0.5, double.NegativeInfinity, 0.5)]
[TestCase(0.5, 0.5, double.NegativeInfinity)]
[TestCase(double.PositiveInfinity, 0.5, 0.5)]
[TestCase(0.5, double.PositiveInfinity, 0.5)]
[TestCase(0.5, 0.5, double.PositiveInfinity)]
[TestCase(double.NaN, 0.5, 0.5)]
[TestCase(0.5, double.NaN, 0.5)]
[TestCase(0.5, 0.5, double.NaN)]
public void AsArray(double first, double second, double third)
{
var triplet = new ColourTriplet(first, second, third);
var array = triplet.AsArray();
Assert.That(array[0], Is.EqualTo(first));
Assert.That(array[1], Is.EqualTo(second));
Assert.That(array[2], Is.EqualTo(third));
Assert.That(array[0], Is.EqualTo(triplet.First));
Assert.That(array[1], Is.EqualTo(triplet.Second));
Assert.That(array[2], Is.EqualTo(triplet.Third));
}

[TestCaseSource(nameof(GetHueTestData))]
public void GetHue(ColourTriplet triplet, double? expectedHue)
{
if (triplet.HueIndex is null or 1)
{
Assert.Throws<ArgumentOutOfRangeException>(() => triplet.HueValue());
return;
}

var hue = double.NaN;
Assert.DoesNotThrow(() => hue = triplet.HueValue());
Assert.That(hue, Is.EqualTo(expectedHue));
}

[TestCaseSource(nameof(OverrideHueTestData))]
public void OverrideHue(ColourTriplet triplet, double hueOverride, ColourTriplet expectedTriplet)
{
if (triplet.HueIndex is null or 1)
{
Assert.Throws<ArgumentOutOfRangeException>(() => triplet.WithHueOverride(hueOverride));
return;
}

ColourTriplet hueOverrideTriplet = null!;
Assert.DoesNotThrow(() => hueOverrideTriplet = triplet.WithHueOverride(hueOverride));
Assert.That(hueOverrideTriplet.HueValue(), Is.EqualTo(hueOverride));
AssertUtils.AssertTriplet(hueOverrideTriplet, expectedTriplet, 0.00000000001);
}

[TestCaseSource(nameof(ModuloHueTestData))]
public void ModuloHue(ColourTriplet triplet, ColourTriplet expectedTriplet)
{
if (triplet.HueIndex is 1)
{
Assert.Throws<ArgumentOutOfRangeException>(() => triplet.WithHueModulo());
return;
}

ColourTriplet hueModuloTriplet = null!;
Assert.DoesNotThrow(() => hueModuloTriplet = triplet.WithHueModulo());
AssertUtils.AssertTriplet(hueModuloTriplet, expectedTriplet, 0.00000000001);
}

[TestCaseSource(nameof(PremultipliedAlphaTestData))]
public void PremultipliedAlpha(ColourTriplet triplet, double alpha, ColourTriplet expectedTriplet)
{
if (triplet.HueIndex is 1)
{
Assert.Throws<ArgumentOutOfRangeException>(() => triplet.WithPremultipliedAlpha(alpha));
return;
}

ColourTriplet premultipliedAlphaTriplet = null!;
Assert.DoesNotThrow(() => premultipliedAlphaTriplet = triplet.WithPremultipliedAlpha(alpha));
AssertUtils.AssertTriplet(premultipliedAlphaTriplet, expectedTriplet, 0.00000000001);
}

[TestCaseSource(nameof(UnpremultipliedAlphaTestData))]
public void UnpremultipliedAlpha(ColourTriplet triplet, double alpha, ColourTriplet expectedTriplet)
{
if (triplet.HueIndex is 1)
{
Assert.Throws<ArgumentOutOfRangeException>(() => triplet.WithUnpremultipliedAlpha(alpha));
return;
}

ColourTriplet unpremultipliedAlphaTriplet = null!;
Assert.DoesNotThrow(() => unpremultipliedAlphaTriplet = triplet.WithUnpremultipliedAlpha(alpha));
AssertUtils.AssertTriplet(unpremultipliedAlphaTriplet, expectedTriplet, 0.00000000001);
}
}
12 changes: 6 additions & 6 deletions Unicolour.Tests/CoordinateSpaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ public class CoordinateSpaceTests
private readonly ColourTriplet rgbLowerInRange = new(0, 0, 0);

[Test]
public void CartesianRgbToCylindricalHsb()
public void RectangularRgbToCylindricalHsb()
{
AssertRgbToHsb(rgbUpperInRange, rgbUpperOutRange);
AssertRgbToHsb(rgbLowerInRange, rgbLowerOutRange);
}

[Test]
public void CylindricalHsbToCartesianRgb()
public void CylindricalHsbToRectangularRgb()
{
AssertHsbToRgb(hsxUpperInRange, hsxUpperOutRange);
AssertHsbToRgb(hsxLowerInRange, hsxLowerOutRange);
Expand Down Expand Up @@ -69,28 +69,28 @@ public void CylindricalHwbToCylindricalHsb()
}

[Test]
public void CylindricalLchabCartesianLab()
public void CylindricalLchabRectangularLab()
{
AssertLchabToLab(HueUpperInRange, HueUpperOutRange);
AssertLchabToLab(HueLowerInRange, HueLowerOutRange);
}

[Test]
public void CylindricalLchuvCartesianLuv()
public void CylindricalLchuvRectangularLuv()
{
AssertLchuvToLuv(HueUpperInRange, HueUpperOutRange);
AssertLchuvToLuv(HueLowerInRange, HueLowerOutRange);
}

[Test]
public void CylindricalJzczhzCartesianJzazbz()
public void CylindricalJzczhzRectangularJzazbz()
{
AssertJzczhzToJzazbz(HueUpperInRange, HueUpperOutRange);
AssertJzczhzToJzazbz(HueLowerInRange, HueLowerOutRange);
}

[Test]
public void CylindricalOklchCartesianOklab()
public void CylindricalOklchRectangularOklab()
{
AssertOklchToOklab(HueUpperInRange, HueUpperOutRange);
AssertOklchToOklab(HueLowerInRange, HueLowerOutRange);
Expand Down
Loading

0 comments on commit 63fb354

Please sign in to comment.