diff --git a/Unicolour.Console/Unicolour.Console.csproj b/Example.Console/Example.Console.csproj
similarity index 71%
rename from Unicolour.Console/Unicolour.Console.csproj
rename to Example.Console/Example.Console.csproj
index 6119eab4..5edfb68e 100644
--- a/Unicolour.Console/Unicolour.Console.csproj
+++ b/Example.Console/Example.Console.csproj
@@ -8,8 +8,10 @@
true
true
win-x64
- Wacton.Unicolour.Console
- Wacton.Unicolour.Console
+ Wacton.Unicolour.Example.Console
+ Wacton.Unicolour.Example.Console
+ William Acton
+ Wacton.Unicolour.Example.Console
diff --git a/Unicolour.Console/Program.cs b/Example.Console/Program.cs
similarity index 100%
rename from Unicolour.Console/Program.cs
rename to Example.Console/Program.cs
diff --git a/Example.Diagrams/Example.Diagrams.csproj b/Example.Diagrams/Example.Diagrams.csproj
new file mode 100644
index 00000000..a408592e
--- /dev/null
+++ b/Example.Diagrams/Example.Diagrams.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+ false
+ Wacton.Unicolour.Example.Diagrams
+ Wacton.Unicolour.Example.Diagrams
+ William Acton
+ Wacton.Unicolour.Example.Diagrams
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example.Diagrams/Program.cs b/Example.Diagrams/Program.cs
new file mode 100644
index 00000000..8890f18e
--- /dev/null
+++ b/Example.Diagrams/Program.cs
@@ -0,0 +1,190 @@
+using ScottPlot;
+using ScottPlot.Plottables;
+using Wacton.Unicolour.Example.Diagrams;
+
+const string outputDirectory = "../../../../Unicolour.Readme/docs/";
+const int height = 640;
+
+/*
+ * note: chromaticity diagrams that are filled with colour take a couple of minutes to generate each
+ * because they process 100,000s of colours, checking if imaginary and converting to RGB
+ * could easily be much faster if not trying to make a solid block of colour
+ */
+
+var spectralLocus = Utils.GetSpectralLocus();
+var rgbGamut = Utils.GetRgbGamut();
+var blackbodyLocus = Utils.GetBlackbodyLocus();
+var isotherms = Utils.GetIsotherms();
+
+SpectralLocus();
+XyChromaticityWithRgb();
+XyChromaticityWithBlackbody();
+UvChromaticity();
+UvChromaticityWithBlackbody();
+return;
+
+void SpectralLocus()
+{
+ var rangeX = (0.0, 0.8);
+ var rangeY = (0.0, 0.9);
+ var plot = Utils.GetEmptyPlot(rangeX, rangeY, majorTickInterval: 0.1);
+ plot.DataBackground = Color.Gray(96);
+ plot.GetDefaultGrid().MajorLineStyle = new LineStyle { Color = Color.Gray(128) };
+
+ var spectralCoordinates = spectralLocus.Select(colour => new Coordinates(colour.Chromaticity.X, colour.Chromaticity.Y)).ToList();
+ var spectralLocusScatter = plot.Add.Scatter(spectralCoordinates);
+ spectralLocusScatter.Color = Color.Gray(64);
+ spectralLocusScatter.LineWidth = 1;
+ spectralLocusScatter.MarkerStyle = MarkerStyle.None;
+
+ var firstCoordinate = new Coordinates(spectralLocus.First().Chromaticity.X, spectralLocus.First().Chromaticity.Y);
+ var lastCoordinate = new Coordinates(spectralLocus.Last().Chromaticity.X, spectralLocus.Last().Chromaticity.Y);
+ var lineOfPurples = plot.Add.Line(firstCoordinate, lastCoordinate);
+ lineOfPurples.Color = new Color(255, 0, 255);
+ lineOfPurples.LineWidth = 1;
+ lineOfPurples.MarkerStyle = MarkerStyle.None;
+
+ foreach (var colour in spectralLocus)
+ {
+ var chromaticity = colour.Chromaticity;
+ var marker = new Marker
+ {
+ X = colour.Chromaticity.X,
+ Y = colour.Chromaticity.Y,
+ Color = Utils.GetPlotColour(chromaticity)!.Value,
+ Size = 5f
+ };
+
+ plot.Add.Plottable(marker);
+ }
+
+ var width = Utils.GetWidth(rangeX, rangeY, height);
+ plot.SavePng(Path.Combine(outputDirectory, "diagram-spectral-locus.png"), width, height);
+}
+
+void XyChromaticityWithRgb()
+{
+ var rangeX = (0.0, 0.8);
+ var rangeY = (0.0, 0.9);
+ var plot = Utils.GetEmptyPlot(rangeX, rangeY, majorTickInterval: 0.1);
+
+ var fillMarkers = Utils.GetXyFillMarkers(rangeX, rangeY, increment: 0.001);
+ foreach (var marker in fillMarkers)
+ {
+ plot.Add.Plottable(marker);
+ }
+
+ var spectralCoordinates = spectralLocus.Select(colour => new Coordinates(colour.Chromaticity.X, colour.Chromaticity.Y)).ToList();
+ spectralCoordinates.Add(spectralCoordinates.First());
+ var spectralLocusScatter = plot.Add.Scatter(spectralCoordinates);
+ spectralLocusScatter.Color = Colors.Black;
+ spectralLocusScatter.LineWidth = 2.5f;
+ spectralLocusScatter.MarkerStyle = MarkerStyle.None;
+
+ var rgbCoordinates = rgbGamut.Select(colour => new Coordinates(colour.Chromaticity.X, colour.Chromaticity.Y));
+ var rgbPolygon = plot.Add.Polygon(rgbCoordinates.ToArray());
+ rgbPolygon.LineStyle = new LineStyle { Color = new Color(0f, 0f, 0f, 0.25f), Width = 2.5f };
+ rgbPolygon.FillStyle = new FillStyle { Color = Colors.Transparent };
+
+ var width = Utils.GetWidth(rangeX, rangeY, height);
+ plot.SavePng(Path.Combine(outputDirectory, "diagram-xy-chromaticity-rgb.png"), width, height);
+}
+
+void XyChromaticityWithBlackbody()
+{
+ var rangeX = (0.0, 0.8);
+ var rangeY = (0.0, 0.9);
+ var plot = Utils.GetEmptyPlot(rangeX, rangeY, majorTickInterval: 0.1);
+
+ var fillMarkers = Utils.GetXyFillMarkers(rangeX, rangeY, increment: 0.001);
+ foreach (var marker in fillMarkers)
+ {
+ plot.Add.Plottable(marker);
+ }
+
+ var spectralCoordinates = spectralLocus.Select(colour => new Coordinates(colour.Chromaticity.X, colour.Chromaticity.Y)).ToList();
+ spectralCoordinates.Add(spectralCoordinates.First());
+ var spectralLocusScatter = plot.Add.Scatter(spectralCoordinates);
+ spectralLocusScatter.Color = Colors.Black;
+ spectralLocusScatter.LineWidth = 2.5f;
+ spectralLocusScatter.MarkerStyle = MarkerStyle.None;
+
+ var blackbodyCoordinates = blackbodyLocus.Select(colour => new Coordinates(colour.Chromaticity.X, colour.Chromaticity.Y)).ToList();
+ var blackbodyScatter = plot.Add.Scatter(blackbodyCoordinates);
+ blackbodyScatter.Color = Colors.Black;
+ blackbodyScatter.LineWidth = 2.5f;
+ blackbodyScatter.MarkerStyle = MarkerStyle.None;
+
+ foreach (var (startColour, endColour) in isotherms)
+ {
+ var startCoordinates = new Coordinates(startColour.Chromaticity.X, startColour.Chromaticity.Y);
+ var endCoordinates = new Coordinates(endColour.Chromaticity.X, endColour.Chromaticity.Y);
+ var duvLine = plot.Add.Line(startCoordinates, endCoordinates);
+ duvLine.Color = Colors.Black;
+ duvLine.LineWidth = 2.5f;
+ }
+
+ var width = Utils.GetWidth(rangeX, rangeY, height);
+ plot.SavePng(Path.Combine(outputDirectory, "diagram-xy-chromaticity-blackbody.png"), width, height);
+}
+
+void UvChromaticity()
+{
+ var rangeU = (0.0, 0.7);
+ var rangeV = (0.0, 0.4);
+ var plot = Utils.GetEmptyPlot(rangeU, rangeV, majorTickInterval: 0.05);
+
+ var fillMarkers = Utils.GetUvFillMarkers(rangeU, rangeV, increment: 0.001);
+ foreach (var marker in fillMarkers)
+ {
+ plot.Add.Plottable(marker);
+ }
+
+ var spectralCoordinates = spectralLocus.Select(colour => new Coordinates(colour.Chromaticity.U, colour.Chromaticity.V)).ToList();
+ spectralCoordinates.Add(spectralCoordinates.First());
+ var spectralLocusScatter = plot.Add.Scatter(spectralCoordinates);
+ spectralLocusScatter.Color = Colors.Black;
+ spectralLocusScatter.LineWidth = 2.5f;
+ spectralLocusScatter.MarkerStyle = MarkerStyle.None;
+
+ var width = Utils.GetWidth(rangeU, rangeV, height);
+ plot.SavePng(Path.Combine(outputDirectory, "diagram-uv-chromaticity.png"), width, height);
+}
+
+void UvChromaticityWithBlackbody()
+{
+ var rangeU = (0.1, 0.45);
+ var rangeV = (0.25, 0.4);
+ var plot = Utils.GetEmptyPlot(rangeU, rangeV, majorTickInterval: 0.05);
+
+ var fillMarkers = Utils.GetUvFillMarkers(rangeU, rangeV, increment: 0.00025);
+ foreach (var marker in fillMarkers)
+ {
+ plot.Add.Plottable(marker);
+ }
+
+ var spectralCoordinates = spectralLocus.Select(colour => new Coordinates(colour.Chromaticity.U, colour.Chromaticity.V)).ToList();
+ spectralCoordinates.Add(spectralCoordinates.First());
+ var spectralLocusScatter = plot.Add.Scatter(spectralCoordinates);
+ spectralLocusScatter.Color = Colors.Black;
+ spectralLocusScatter.LineWidth = 2.5f;
+ spectralLocusScatter.MarkerStyle = MarkerStyle.None;
+
+ var blackbodyCoordinates = blackbodyLocus.Select(colour => new Coordinates(colour.Chromaticity.U, colour.Chromaticity.V)).ToList();
+ var blackbodyScatter = plot.Add.Scatter(blackbodyCoordinates);
+ blackbodyScatter.Color = Colors.Black;
+ blackbodyScatter.LineWidth = 2.5f;
+ blackbodyScatter.MarkerStyle = MarkerStyle.None;
+
+ foreach (var (startColour, endColour) in isotherms)
+ {
+ var startCoordinates = new Coordinates(startColour.Chromaticity.U, startColour.Chromaticity.V);
+ var endCoordinates = new Coordinates(endColour.Chromaticity.U, endColour.Chromaticity.V);
+ var duvLine = plot.Add.Line(startCoordinates, endCoordinates);
+ duvLine.Color = Colors.Black;
+ duvLine.LineWidth = 2.5f;
+ }
+
+ var width = Utils.GetWidth(rangeU, rangeV, height);
+ plot.SavePng(Path.Combine(outputDirectory, "diagram-uv-chromaticity-blackbody.png"), width, height);
+}
\ No newline at end of file
diff --git a/Example.Diagrams/Utils.cs b/Example.Diagrams/Utils.cs
new file mode 100644
index 00000000..8077ab39
--- /dev/null
+++ b/Example.Diagrams/Utils.cs
@@ -0,0 +1,152 @@
+namespace Wacton.Unicolour.Example.Diagrams;
+
+using ScottPlot;
+using ScottPlot.Plottables;
+using ScottPlot.TickGenerators;
+
+internal static class Utils
+{
+ internal static Plot GetEmptyPlot((double min, double max) rangeX, (double min, double max) rangeY, double majorTickInterval)
+ {
+ var plot = new Plot();
+ plot.Axes.SetLimits(rangeX.min, rangeX.max, rangeY.min, rangeY.max);
+ plot.Axes.Bottom.TickGenerator = GetTickGenerator(rangeX.min, rangeX.max, majorTickInterval);
+ plot.Axes.Left.TickGenerator = GetTickGenerator(rangeY.min, rangeY.max, majorTickInterval);
+ return plot;
+ }
+
+ internal static List GetSpectralLocus()
+ {
+ var data = new List();
+ for (var nm = 360; nm < 700; nm++)
+ {
+ data.Add(new Unicolour(new Spd { { nm, 1.0 } }));
+ }
+
+ return data;
+ }
+
+ internal static List GetRgbGamut()
+ {
+ var r = new Unicolour(ColourSpace.Rgb, 1, 0, 0);
+ var g = new Unicolour(ColourSpace.Rgb, 0, 1, 0);
+ var b = new Unicolour(ColourSpace.Rgb, 0, 0, 1);
+ return new List { r, g, b };
+ }
+
+ internal static List GetBlackbodyLocus()
+ {
+ var data = new List();
+ for (var cct = 500; cct < 20000; cct += 100)
+ {
+ data.Add(new Unicolour(new Temperature(cct)));
+ }
+
+ return data;
+ }
+
+ internal static List<(Unicolour start, Unicolour end)> GetIsotherms()
+ {
+ var data = new List<(Unicolour start, Unicolour end)>();
+ for (var cct = 2000; cct < 10000; cct += 1000)
+ {
+ var upper = new Unicolour(new Temperature(cct, 0.05));
+ var lower = new Unicolour(new Temperature(cct, -0.05));
+ data.Add((upper, lower));
+ }
+
+ return data;
+ }
+
+ internal static List GetXyFillMarkers((double min, double max) rangeX, (double min, double max) rangeY, double increment)
+ {
+ var data = new List();
+ for (var x = rangeX.min; x < rangeX.max; x += increment)
+ {
+ for (var y = rangeY.min; y < rangeY.max; y += increment)
+ {
+ var chromaticity = new Chromaticity(x, y);
+ var color = GetPlotColour(chromaticity);
+ if (color == null) continue;
+
+ var marker = new Marker { X = x, Y = y, Color = color.Value, Size = 2.5f };
+ data.Add(marker);
+ }
+ }
+
+ return data;
+ }
+
+ internal static List GetUvFillMarkers((double min, double max) rangeU, (double min, double max) rangeV, double increment)
+ {
+ var data = new List();
+ for (var u = rangeU.min; u < rangeU.max; u += increment)
+ {
+ for (var v = rangeV.min; v < rangeV.max; v += increment)
+ {
+ var chromaticity = Chromaticity.FromUv(u, v);
+ var color = GetPlotColour(chromaticity);
+ if (color == null) continue;
+
+ var marker = new Marker { X = u, Y = v, Color = color.Value, Size = 2.5f };
+ data.Add(marker);
+ }
+ }
+
+ return data;
+ }
+
+ private static readonly Dictionary ChromaticityCache = new();
+ internal static Color? GetPlotColour(Chromaticity chromaticity)
+ {
+ Color? color;
+ if (ChromaticityCache.ContainsKey(chromaticity))
+ {
+ color = ChromaticityCache[chromaticity];
+ }
+ else
+ {
+ var unicolour = new Unicolour(chromaticity);
+ color = unicolour.IsImaginary ? null : GetScaledColour(unicolour.Rgb);
+ ChromaticityCache.Add(chromaticity, color);
+ }
+
+ return color;
+ }
+
+ private static Color GetScaledColour(Rgb rgb)
+ {
+ var components = new[] { rgb.R, rgb.G, rgb.B };
+ var max = components.Max();
+ var scaled = components.Select(component => Clamp(component / max, 0, 1)).ToList();
+ return new Color((float)scaled[0], (float)scaled[1], (float)scaled[2]);
+ }
+
+ internal static int GetWidth((double min, double max) rangeX, (double min, double max) rangeY, int height)
+ {
+ return (int)(height * (rangeX.max - rangeX.min) / (rangeY.max - rangeY.min));
+ }
+
+ private static NumericManual GetTickGenerator(double min, double max, double majorTickInterval)
+ {
+ const double increment = 0.05;
+ var ticks = new List();
+ var nextMajorTick = min;
+ for (var i = min; i < max + increment; i += increment)
+ {
+ var isMajor = IsEffectivelyZero(i - nextMajorTick);
+ if (isMajor)
+ {
+ nextMajorTick += majorTickInterval;
+ }
+
+ ticks.Add(new Tick(i, isMajor ? $"{i:F2}" : string.Empty, isMajor));
+ }
+
+ return new NumericManual(ticks.ToArray());
+ }
+
+ private static bool IsEffectivelyZero(double x) => Math.Abs(x) < 5e-14;
+
+ private static double Clamp(double value, double min, double max) => value < min ? min : value > max ? max : value;
+}
\ No newline at end of file
diff --git a/Unicolour.Example/Unicolour.Example.csproj b/Example.Gradients/Example.Gradients.csproj
similarity index 80%
rename from Unicolour.Example/Unicolour.Example.csproj
rename to Example.Gradients/Example.Gradients.csproj
index 41b7286f..53fe2ceb 100644
--- a/Unicolour.Example/Unicolour.Example.csproj
+++ b/Example.Gradients/Example.Gradients.csproj
@@ -6,9 +6,10 @@
enable
enable
false
- Wacton.Unicolour.Example
- Wacton.Unicolour.Example
+ Wacton.Unicolour.Example.Gradients
+ Wacton.Unicolour.Example.Gradients
William Acton
+ Wacton.Unicolour.Example.Gradients
diff --git a/Unicolour.Example/Inconsolata-Regular.ttf b/Example.Gradients/Inconsolata-Regular.ttf
similarity index 100%
rename from Unicolour.Example/Inconsolata-Regular.ttf
rename to Example.Gradients/Inconsolata-Regular.ttf
diff --git a/Unicolour.Example/OFL.txt b/Example.Gradients/OFL.txt
similarity index 100%
rename from Unicolour.Example/OFL.txt
rename to Example.Gradients/OFL.txt
diff --git a/Example.Gradients/Program.cs b/Example.Gradients/Program.cs
new file mode 100644
index 00000000..a99f1b3b
--- /dev/null
+++ b/Example.Gradients/Program.cs
@@ -0,0 +1,187 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using Wacton.Unicolour;
+using Wacton.Unicolour.Datasets;
+using Wacton.Unicolour.Example.Gradients;
+
+const string outputDirectory = "../../../../Unicolour.Readme/docs/";
+
+ColourSpaces();
+Temperature();
+VisionDeficiency();
+AlphaInterpolation();
+return;
+
+void ColourSpaces()
+{
+ const int columns = 3;
+ const int columnWidth = 800;
+ const int rows = 21;
+ const int rowHeight = 100;
+
+ var purple = new Unicolour(ColourSpace.Hsb, 260, 1.0, 0.33);
+ var orange = new Unicolour(ColourSpace.Hsb, 30, 0.66, 1.0);
+ var pink = new Unicolour("#FF1493");
+ var cyan = new Unicolour(ColourSpace.Rgb255, 0, 255, 255);
+ var black = new Unicolour(ColourSpace.Rgb, 0, 0, 0);
+ var green = new Unicolour(ColourSpace.Rgb, 0, 1, 0);
+
+ var lightText = new Unicolour("#E8E8FF");
+ var column1 = DrawColumn(new[] { purple, orange });
+ var column2 = DrawColumn(new[] { pink, cyan });
+ var column3 = DrawColumn(new[] { black, green });
+
+ var image = new Image(columnWidth * columns, rowHeight * rows);
+ image.Mutate(context => context
+ .DrawImage(column1, new Point(columnWidth * 0, 0), 1f)
+ .DrawImage(column2, new Point(columnWidth * 1, 0), 1f)
+ .DrawImage(column3, new Point(columnWidth * 2, 0), 1f)
+ );
+
+ image.Save(Path.Combine(outputDirectory, "gradient-colour-spaces.png"));
+ return;
+
+ Image DrawColumn(Unicolour[] colourPoints)
+ {
+ var rgb = Utils.Draw(("RGB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount));
+ var rgbLinear = Utils.Draw(("RGB Linear", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.RgbLinear, amount));
+ var hsb = Utils.Draw(("HSB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount));
+ var hsl = Utils.Draw(("HSL", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hsl, amount));
+ var hwb = Utils.Draw(("HWB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hwb, amount));
+ var xyz = Utils.Draw(("XYZ", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Xyz, amount));
+ var xyy = Utils.Draw(("xyY", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Xyy, amount));
+ var lab = Utils.Draw(("LAB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Lab, amount));
+ var lchab = Utils.Draw(("LCHab", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Lchab, amount));
+ var luv = Utils.Draw(("LUV", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Luv, amount));
+ var lchuv = Utils.Draw(("LCHuv", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Lchuv, amount));
+ var hsluv = Utils.Draw(("HSLuv", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hsluv, amount));
+ var hpluv = Utils.Draw(("HPLuv", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hpluv, amount));
+ var ictcp = Utils.Draw(("ICtCp", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Ictcp, amount));
+ var jzazbz = Utils.Draw(("JzAzBz", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Jzazbz, amount));
+ var jzczhz = Utils.Draw(("JzCzHz", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Jzczhz, amount));
+ var oklab = Utils.Draw(("OKLAB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Oklab, amount));
+ var oklch = Utils.Draw(("OKLCH", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Oklch, amount));
+ var cam02 = Utils.Draw(("CAM02", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Cam02, amount));
+ var cam16 = Utils.Draw(("CAM16", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Cam16, amount));
+ var hct = Utils.Draw(("HCT", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hct, amount));
+
+ var columnImage = new Image(columnWidth, rowHeight * rows);
+ columnImage.Mutate(context => context
+ .DrawImage(rgb, new Point(0, rowHeight * 0), 1f)
+ .DrawImage(rgbLinear, new Point(0, rowHeight * 1), 1f)
+ .DrawImage(hsb, new Point(0, rowHeight * 2), 1f)
+ .DrawImage(hsl, new Point(0, rowHeight * 3), 1f)
+ .DrawImage(hwb, new Point(0, rowHeight * 4), 1f)
+ .DrawImage(xyz, new Point(0, rowHeight * 5), 1f)
+ .DrawImage(xyy, new Point(0, rowHeight * 6), 1f)
+ .DrawImage(lab, new Point(0, rowHeight * 7), 1f)
+ .DrawImage(lchab, new Point(0, rowHeight * 8), 1f)
+ .DrawImage(luv, new Point(0, rowHeight * 9), 1f)
+ .DrawImage(lchuv, new Point(0, rowHeight * 10), 1f)
+ .DrawImage(hsluv, new Point(0, rowHeight * 11), 1f)
+ .DrawImage(hpluv, new Point(0, rowHeight * 12), 1f)
+ .DrawImage(ictcp, new Point(0, rowHeight * 13), 1f)
+ .DrawImage(jzazbz, new Point(0, rowHeight * 14), 1f)
+ .DrawImage(jzczhz, new Point(0, rowHeight * 15), 1f)
+ .DrawImage(oklab, new Point(0, rowHeight * 16), 1f)
+ .DrawImage(oklch, new Point(0, rowHeight * 17), 1f)
+ .DrawImage(cam02, new Point(0, rowHeight * 18), 1f)
+ .DrawImage(cam16, new Point(0, rowHeight * 19), 1f)
+ .DrawImage(hct, new Point(0, rowHeight * 20), 1f)
+ );
+
+ return columnImage;
+ }
+}
+
+void Temperature()
+{
+ const int width = 1200;
+ const int rows = 1;
+ const int rowHeight = 120;
+
+ var scaledPoints = new List();
+ for (var i = 1000; i <= 13000; i += 100)
+ {
+ var rgb = new Unicolour(i).Rgb;
+ var rgbComponents = new[] { rgb.R, rgb.G, rgb.B };
+ var max = rgbComponents.Max();
+ var scaledRgb = rgbComponents.Select(x => x / max).ToArray();
+ var scaledUnicolour = new Unicolour(ColourSpace.Rgb, scaledRgb[0], scaledRgb[1], scaledRgb[2]);
+ scaledPoints.Add(scaledUnicolour);
+ }
+
+ var text = Css.Black;
+
+ var scaled = Utils.Draw(("CCT (1,000 K - 13,000 K)", text), width, rowHeight, scaledPoints.ToArray(),
+ (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount));
+
+ var image = new Image(width, rowHeight * rows);
+ image.Mutate(context => context
+ .DrawImage(scaled, new Point(0, rowHeight * 0), 1f)
+ );
+
+ image.Save(Path.Combine(outputDirectory, "gradient-temperature.png"));
+}
+
+void VisionDeficiency()
+{
+ const int width = 1200;
+ const int rows = 5;
+ const int rowHeight = 100;
+
+ // not using OKLCH for the spectrum because the uniform luminance results in flat gradient for Achromatopsia
+ var colourPoints = new Unicolour[]
+ {
+ new(ColourSpace.Hsb, 0, 0.666, 1),
+ new(ColourSpace.Hsb, 360, 0.666, 1)
+ };
+
+ var darkText = new Unicolour("#404046");
+
+ var none = Utils.Draw(("No deficiency", darkText), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing));
+ var protanopia = Utils.Draw(("Protanopia", darkText), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateProtanopia());
+ var deuteranopia = Utils.Draw(("Deuteranopia", darkText), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateDeuteranopia());
+ var tritanopia = Utils.Draw(("Tritanopia", darkText), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateTritanopia());
+ var achromatopsia = Utils.Draw(("Achromatopsia", darkText), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateAchromatopsia());
+
+ var image = new Image(width, rowHeight * rows);
+ image.Mutate(context => context
+ .DrawImage(none, new Point(0, rowHeight * 0), 1f)
+ .DrawImage(protanopia, new Point(0, rowHeight * 1), 1f)
+ .DrawImage(deuteranopia, new Point(0, rowHeight * 2), 1f)
+ .DrawImage(tritanopia, new Point(0, rowHeight * 3), 1f)
+ .DrawImage(achromatopsia, new Point(0, rowHeight * 4), 1f)
+ );
+
+ image.Save(Path.Combine(outputDirectory, "gradient-vision-deficiency.png"));
+}
+
+void AlphaInterpolation()
+{
+ const int width = 1000;
+ const int rows = 2;
+ const int rowHeight = 120;
+
+ var colourPoints = new[] { Css.Red, Css.Transparent, Css.Blue };
+ var text = Css.Black;
+
+ var premultiplied = Utils.Draw(("With premultiplied alpha", text), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount, premultiplyAlpha: true));
+ var notPremultiplied = Utils.Draw(("Without premultiplied alpha", text), width, rowHeight, colourPoints,
+ (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount, premultiplyAlpha: false));
+
+ var image = new Image(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(Path.Combine(outputDirectory, "gradient-alpha-interpolation.png"));
+}
\ No newline at end of file
diff --git a/Unicolour.Example/Gradient.cs b/Example.Gradients/Utils.cs
similarity index 97%
rename from Unicolour.Example/Gradient.cs
rename to Example.Gradients/Utils.cs
index d7f9de48..cb89ce34 100644
--- a/Unicolour.Example/Gradient.cs
+++ b/Example.Gradients/Utils.cs
@@ -1,4 +1,4 @@
-namespace Wacton.Unicolour.Example;
+namespace Wacton.Unicolour.Example.Gradients;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
@@ -6,7 +6,7 @@ namespace Wacton.Unicolour.Example;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
-internal static class Gradient
+internal static class Utils
{
private const bool RenderOutOfGamutAsTransparent = false;
diff --git a/README.md b/README.md
index f44ac115..1da13688 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,10 @@ Unicolour is a .NET library written in C# for working with colour:
- Colour space conversion
- Colour mixing / colour interpolation
- Colour difference / colour distance
+- Colour gamut mapping
- Colour chromaticity
- Colour temperature
-- Colour gamut mapping
+- Wavelength attributes
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,12 +99,14 @@ var difference = white.Difference(black, DeltaE.Ciede2000);
Console.WriteLine(difference); // 100.0000
```
-Other useful colour information is available, such as chromaticity coordinates and [temperature](#convert-between-colour-and-temperature).
+Other useful colour information is available, such as chromaticity coordinates,
+[temperature](#convert-between-colour-and-temperature), and [dominant wavelength](#get-wavelength-attributes).
```c#
var equalTristimulus = new Unicolour(ColourSpace.Xyz, 0.5, 0.5, 0.5);
Console.WriteLine(equalTristimulus.Chromaticity.Xy); // (0.3333, 0.3333)
Console.WriteLine(equalTristimulus.Chromaticity.Uv); // (0.2105, 0.3158)
Console.WriteLine(equalTristimulus.Temperature); // 5455.5 K (Δuv -0.00442)
+Console.WriteLine(equalTristimulus.DominantWavelength); // 596.1
```
Reference white points (e.g. D65) and the RGB model (e.g. sRGB) [can be configured](#-configuration).
@@ -217,7 +220,7 @@ XYZ is considered the root colour space.
### Mix colours
-Two colours can be mixed by [interpolating between them in any colour space](#-examples),
+Two colours can be mixed by [interpolating between them in any colour space](#gradients),
taking into account cyclic hue, interpolation distance, and alpha premultiplication.
```c#
var red = new Unicolour(ColourSpace.Rgb, 1.0, 0.0, 0.0);
@@ -258,25 +261,27 @@ var difference = red.Difference(blue, DeltaE.Cie76);
| ΔECAM02 | `DeltaE.Cam02` |
| ΔECAM16 | `DeltaE.Cam16` |
+### Map colour into display gamut
+Colours that cannot be displayed with the [configured RGB model](#rgbconfiguration) can be mapped to the closest in-gamut colour.
+The gamut mapping algorithm conforms to CSS specifications.
+```c#
+var outOfGamut = new Unicolour(ColourSpace.Rgb, -0.51, 1.02, -0.31);
+var inGamut = outOfGamut.MapToGamut();
+```
+
### Convert between colour and temperature
Correlated colour temperature (CCT) and delta UV (∆uv) can be obtained from a colour, and can be used to create a colour.
CCT from 500 K to 1,000,000,000 K is supported but only CCT from 1,000 K to 20,000 K is guaranteed to have high accuracy.
```c#
-var d50 = new Unicolour(ColourSpace.Xyy, 0.3457, 0.3585, 1.0);
+var chromaticity = new Chromaticity(0.3457, 0.3585);
+var d50 = new Unicolour(chromaticity);
var (cct, duv) = d50.Temperature;
-var d65 = new Unicolour(6504, 0.0032);
+var temperature = new Temperature(6504, 0.0032);
+var d65 = new Unicolour(temperature);
var (x, y) = d65.Chromaticity;
```
-### Map colour into display gamut
-Colours that cannot be displayed with the [configured RGB model](#rgbconfiguration) can be mapped to the closest in-gamut colour.
-The gamut mapping algorithm conforms to CSS specifications.
-```c#
-var outOfGamut = new Unicolour(ColourSpace.Rgb, -0.51, 1.02, -0.31);
-var inGamut = outOfGamut.MapToGamut();
-```
-
### Create colour from spectral power distribution
A spectral power distribution (SPD) can be used to create a colour.
Wavelengths should be provided in either 1 nm or 5 nm intervals, and omitted wavelengths are assumed to have zero spectral power.
@@ -291,6 +296,25 @@ var spd = new Spd
var intenseYellow = new Unicolour(spd);
```
+### Get wavelength attributes
+The dominant wavelength and excitation purity of a colour can be derived using the spectral locus.
+Wavelengths from 360 nm to 700 nm are supported.
+```c#
+var chromaticity = new Chromaticity(0.1, 0.8);
+var hyperGreen = new Unicolour(chromaticity);
+var dominantWavelength = hyperGreen.DominantWavelength;
+var excitationPurity = hyperGreen.ExcitationPurity;
+```
+
+### Detect imaginary colours
+Whether or not a colour is imaginary — one that cannot be produced by the eye — can be determined using the spectral locus.
+They are the colours that lie outside of the horseshoe-shaped curve of the [CIE xy chromaticity diagram](#diagrams).
+```c#
+var chromaticity = new Chromaticity(0.05, 0.05);
+var impossibleBlue = new Unicolour(chromaticity);
+var isImaginary = impossibleBlue.IsImaginary;
+```
+
### Simulate colour vision deficiency
A new `Unicolour` can be generated that simulates how a colour appears to someone with a particular colour vision deficiency (CVD) or colour blindness.
```c#
@@ -325,7 +349,7 @@ var colour = new Unicolour(defaultConfig, ColourSpace.Rgb255, 192, 255, 238);
```
## 💡 Configuration
-The `Configuration` parameter can be used to customise how colour is processed.
+The `Configuration` parameter can be used to define the context of the colour.
Example configuration with predefined Rec. 2020 RGB & illuminant D50 (2° observer) XYZ:
```c#
@@ -340,8 +364,8 @@ var rgbConfig = new RgbConfiguration(
chromaticityG: new(0.1152, 0.8264),
chromaticityB: new(0.1566, 0.0177),
whitePoint: Illuminant.D50.GetWhitePoint(Observer.Degree2),
- fromLinear: value => Companding.Gamma(value, 2.19921875),
- toLinear: value => Companding.InverseGamma(value, 2.19921875)
+ fromLinear: value => Math.Pow(value, 1 / 2.19921875),
+ toLinear: value => Math.Pow(value, 2.19921875)
);
var xyzConfig = new XyzConfiguration(Illuminant.C, Observer.Degree10);
@@ -423,19 +447,59 @@ Console.WriteLine(rec2020Colour.Rgb); // 0.57 0.96 0.27
```
## ✨ Examples
-This repo contains an [example project](Unicolour.Example/Program.cs) that uses Unicolour to:
-1. Generate gradients through each colour space
- ![Gradients through different colour spaces, generated from Unicolour](docs/gradients.png)
-2. Render the colour spectrum with different colour vision deficiencies
- ![Spectrum rendered with different colour vision deficiencies, generated from Unicolour](docs/vision-deficiency.png)
-3. Demonstrate interpolation with and without premultiplied alpha
- ![Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha, generated from Unicolour](docs/alpha-interpolation.png)
-4. Visualise correlated colour temperature (CCT) from 1,000 K to 13,000 K
- ![Visualisation of temperature from 1,000 K to 13,000 K, generated from Unicolour](docs/temperature.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](docs/colour-info.png)
+This repository contains multiple projects to show examples of Unicolour being used to create:
+1. [Images of gradients](#gradients)
+2. [Diagrams of colour data](#diagrams)
+3. [A colourful console application](#console)
+
+### Gradients
+Example code to create images of gradients using 📷 [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) can be seen in the [Example.Gradients](Example.Gradients/Program.cs) project.
+
+| ![Gradients generated through different colour spaces, created with Unicolour](docs/gradient-colour-spaces.png) |
+|-----------------------------------------------------------------------------------------------------------------|
+| _Gradients generated through each colour space_ |
+
+| ![Visualisation of temperature from 1,000 K to 13,000 K, created with Unicolour](docs/gradient-temperature.png) |
+|-----------------------------------------------------------------------------------------------------------------|
+| _Visualisation of temperature from 1,000 K to 13,000 K_ |
+
+| ![Colour spectrum rendered with different colour vision deficiencies, created with Unicolour](docs/gradient-vision-deficiency.png) |
+|------------------------------------------------------------------------------------------------------------------------------------|
+| _Colour spectrum rendered with different colour vision deficiencies_ |
+
+| ![Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha, created with Unicolour](docs/gradient-alpha-interpolation.png) |
+|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| _Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha_ |
+
+### Diagrams
+Example code to create diagrams of colour data using 📈 [ScottPlot](https://github.com/scottplot/scottplot) can be seen in the [Example.Diagrams](Example.Diagrams/Program.cs) project.
+
+| ![CIE xy chromaticity diagram with sRGB gamut, created with Unicolour](docs/diagram-xy-chromaticity-rgb.png) |
+|--------------------------------------------------------------------------------------------------------------|
+| _CIE xy chromaticity diagram with sRGB gamut_ |
+
+| ![CIE xy chromaticity diagram with Planckian or blackbody locus, created with Unicolour](docs/diagram-xy-chromaticity-blackbody.png) |
+|--------------------------------------------------------------------------------------------------------------------------------------|
+| _CIE xy chromaticity diagram with Planckian or blackbody locus_ |
+
+| ![CIE xy chromaticity diagram with spectral locus plotted at 1 nm intervals, created with Unicolour](docs/diagram-spectral-locus.png) |
+|---------------------------------------------------------------------------------------------------------------------------------------|
+| _CIE xy chromaticity diagram with spectral locus plotted at 1 nm intervals_ |
+
+| ![CIE 1960 colour space, created with Unicolour](docs/diagram-uv-chromaticity.png) |
+|------------------------------------------------------------------------------------|
+| _CIE 1960 colour space_ |
+
+| ![CIE 1960 colour space with Planckian or blackbody locus, created with Unicolour](docs/diagram-uv-chromaticity-blackbody.png) |
+|--------------------------------------------------------------------------------------------------------------------------------|
+| _CIE 1960 colour space with Planckian or blackbody locus_ |
+
+### Console
+Example code to create a colourful console application using ⌨️ [Spectre.Console](https://github.com/spectreconsole/spectre.console) can be seen in the [Example.Console](Example.Console/Program.cs) project.
+
+| ![Console application showing colour information from hex value, created with Unicolour](docs/console-colour-info.png) |
+|------------------------------------------------------------------------------------------------------------------------|
+| Console application showing colour information from hex value |
## 🔮 Datasets
Some colour datasets have been compiled for convenience and are available as a [NuGet package](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/).
diff --git a/Unicolour.Datasets/Unicolour.Datasets.csproj b/Unicolour.Datasets/Unicolour.Datasets.csproj
index f2f358cb..0c467982 100644
--- a/Unicolour.Datasets/Unicolour.Datasets.csproj
+++ b/Unicolour.Datasets/Unicolour.Datasets.csproj
@@ -19,10 +19,6 @@
Predefine datasets for use with Wacton.Unicolour v4
-
-
-
-
True
@@ -30,4 +26,8 @@
+
+
+
+
diff --git a/Unicolour.Example/Program.cs b/Unicolour.Example/Program.cs
deleted file mode 100644
index e191ceed..00000000
--- a/Unicolour.Example/Program.cs
+++ /dev/null
@@ -1,223 +0,0 @@
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
-using Wacton.Unicolour;
-using Wacton.Unicolour.Datasets;
-using Wacton.Unicolour.Example;
-
-QuickstartExamples();
-GenerateColourSpaceGradients();
-GenerateVisionDeficiencyGradients();
-GenerateAlphaInterpolation();
-GenerateTemperature();
-return;
-
-void QuickstartExamples()
-{
- var cyan = new Unicolour("#00FFFF");
- Console.WriteLine(cyan.Hsl); // 180.0° 100.0% 50.0%
-
- var yellow = new Unicolour(ColourSpace.Rgb255, 255, 255, 0);
- Console.WriteLine(yellow.Hex); // #FFFF00
-
- var red = new Unicolour(ColourSpace.Rgb, 1.0, 0.0, 0.0);
- var blue = new Unicolour(ColourSpace.Hsb, 240, 1.0, 1.0);
-
- /* RGB: [1, 0, 0] ⟶ [0, 0, 1] = [0.5, 0, 0.5] */
- var purple = red.Mix(blue, ColourSpace.Rgb);
- Console.WriteLine(purple.Rgb); // 0.50 0.00 0.50
- Console.WriteLine(purple.Hex); // #800080
-
- /* HSL: [0, 1, 0.5] ⟶ [240, 1, 0.5] = [300, 1, 0.5] */
- var magenta = red.Mix(blue, ColourSpace.Hsl);
- Console.WriteLine(magenta.Rgb); // 1.00 0.00 1.00
- Console.WriteLine(magenta.Hex); // #FF00FF
-
- var white = new Unicolour(ColourSpace.Oklab, 1.0, 0.0, 0.0);
- var black = new Unicolour(ColourSpace.Oklab, 0.0, 0.0, 0.0);
- var difference = white.Difference(black, DeltaE.Ciede2000);
- Console.WriteLine(difference); // 100.0000
-
- var equalTristimulus = new Unicolour(ColourSpace.Xyz, 0.5, 0.5, 0.5);
- Console.WriteLine(equalTristimulus.Chromaticity.Xy); // (0.3333, 0.3333)
- Console.WriteLine(equalTristimulus.Chromaticity.Uv); // (0.2105, 0.3158)
- Console.WriteLine(equalTristimulus.Temperature); // 5455.5 K (Δuv -0.00442)
-}
-
-void GenerateColourSpaceGradients()
-{
- const int columns = 3;
- const int columnWidth = 800;
- const int rows = 21;
- const int rowHeight = 100;
-
- var purple = new Unicolour(ColourSpace.Hsb, 260, 1.0, 0.33);
- var orange = new Unicolour(ColourSpace.Hsb, 30, 0.66, 1.0);
- var pink = new Unicolour("#FF1493");
- var cyan = new Unicolour(ColourSpace.Rgb255, 0, 255, 255);
- Console.WriteLine(cyan.Hsl); // 180.0° 100.0% 50.0%
-
- var yellow = new Unicolour(ColourSpace.Rgb255, 255, 255, 0);
- Console.WriteLine(yellow.Hex); // #FFFF00
-
- var black = new Unicolour(ColourSpace.Rgb, 0, 0, 0);
- var green = new Unicolour(ColourSpace.Rgb, 0, 1, 0);
-
- var lightText = new Unicolour("#E8E8FF");
- var column1 = DrawColumn(new[] { purple, orange });
- var column2 = DrawColumn(new[] { pink, cyan });
- var column3 = DrawColumn(new[] { black, green });
-
- var image = new Image(columnWidth * columns, rowHeight * rows);
- image.Mutate(context => context
- .DrawImage(column1, new Point(columnWidth * 0, 0), 1f)
- .DrawImage(column2, new Point(columnWidth * 1, 0), 1f)
- .DrawImage(column3, new Point(columnWidth * 2, 0), 1f)
- );
-
- image.Save("gradients.png");
- return;
-
- Image DrawColumn(Unicolour[] colourPoints)
- {
- var rgb = Gradient.Draw(("RGB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount));
- var rgbLinear = Gradient.Draw(("RGB Linear", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.RgbLinear, amount));
- var hsb = Gradient.Draw(("HSB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount));
- var hsl = Gradient.Draw(("HSL", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hsl, amount));
- var hwb = Gradient.Draw(("HWB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hwb, amount));
- var xyz = Gradient.Draw(("XYZ", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Xyz, amount));
- var xyy = Gradient.Draw(("xyY", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Xyy, amount));
- var lab = Gradient.Draw(("LAB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Lab, amount));
- var lchab = Gradient.Draw(("LCHab", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Lchab, amount));
- var luv = Gradient.Draw(("LUV", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Luv, amount));
- var lchuv = Gradient.Draw(("LCHuv", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Lchuv, amount));
- var hsluv = Gradient.Draw(("HSLuv", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hsluv, amount));
- var hpluv = Gradient.Draw(("HPLuv", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hpluv, amount));
- var ictcp = Gradient.Draw(("ICtCp", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Ictcp, amount));
- var jzazbz = Gradient.Draw(("JzAzBz", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Jzazbz, amount));
- var jzczhz = Gradient.Draw(("JzCzHz", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Jzczhz, amount));
- var oklab = Gradient.Draw(("OKLAB", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Oklab, amount));
- var oklch = Gradient.Draw(("OKLCH", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Oklch, amount));
- var cam02 = Gradient.Draw(("CAM02", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Cam02, amount));
- var cam16 = Gradient.Draw(("CAM16", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Cam16, amount));
- var hct = Gradient.Draw(("HCT", lightText), columnWidth, rowHeight, colourPoints, (start, end, amount) => start.Mix(end, ColourSpace.Hct, amount));
-
- var columnImage = new Image(columnWidth, rowHeight * rows);
- columnImage.Mutate(context => context
- .DrawImage(rgb, new Point(0, rowHeight * 0), 1f)
- .DrawImage(rgbLinear, new Point(0, rowHeight * 1), 1f)
- .DrawImage(hsb, new Point(0, rowHeight * 2), 1f)
- .DrawImage(hsl, new Point(0, rowHeight * 3), 1f)
- .DrawImage(hwb, new Point(0, rowHeight * 4), 1f)
- .DrawImage(xyz, new Point(0, rowHeight * 5), 1f)
- .DrawImage(xyy, new Point(0, rowHeight * 6), 1f)
- .DrawImage(lab, new Point(0, rowHeight * 7), 1f)
- .DrawImage(lchab, new Point(0, rowHeight * 8), 1f)
- .DrawImage(luv, new Point(0, rowHeight * 9), 1f)
- .DrawImage(lchuv, new Point(0, rowHeight * 10), 1f)
- .DrawImage(hsluv, new Point(0, rowHeight * 11), 1f)
- .DrawImage(hpluv, new Point(0, rowHeight * 12), 1f)
- .DrawImage(ictcp, new Point(0, rowHeight * 13), 1f)
- .DrawImage(jzazbz, new Point(0, rowHeight * 14), 1f)
- .DrawImage(jzczhz, new Point(0, rowHeight * 15), 1f)
- .DrawImage(oklab, new Point(0, rowHeight * 16), 1f)
- .DrawImage(oklch, new Point(0, rowHeight * 17), 1f)
- .DrawImage(cam02, new Point(0, rowHeight * 18), 1f)
- .DrawImage(cam16, new Point(0, rowHeight * 19), 1f)
- .DrawImage(hct, new Point(0, rowHeight * 20), 1f)
- );
-
- return columnImage;
- }
-}
-
-void GenerateVisionDeficiencyGradients()
-{
- const int width = 1200;
- const int rows = 5;
- const int rowHeight = 100;
-
- // not using OKLCH for the spectrum because the uniform luminance results in flat gradient for Achromatopsia
- var colourPoints = new Unicolour[]
- {
- new(ColourSpace.Hsb, 0, 0.666, 1),
- new(ColourSpace.Hsb, 360, 0.666, 1)
- };
-
- var darkText = new Unicolour("#404046");
-
- var none = Gradient.Draw(("No deficiency", darkText), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing));
- var protanopia = Gradient.Draw(("Protanopia", darkText), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateProtanopia());
- var deuteranopia = Gradient.Draw(("Deuteranopia", darkText), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateDeuteranopia());
- var tritanopia = Gradient.Draw(("Tritanopia", darkText), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateTritanopia());
- var achromatopsia = Gradient.Draw(("Achromatopsia", darkText), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Hsb, amount, HueSpan.Increasing).SimulateAchromatopsia());
-
- var image = new Image(width, rowHeight * rows);
- image.Mutate(context => context
- .DrawImage(none, new Point(0, rowHeight * 0), 1f)
- .DrawImage(protanopia, new Point(0, rowHeight * 1), 1f)
- .DrawImage(deuteranopia, new Point(0, rowHeight * 2), 1f)
- .DrawImage(tritanopia, new Point(0, rowHeight * 3), 1f)
- .DrawImage(achromatopsia, new Point(0, rowHeight * 4), 1f)
- );
-
- image.Save("vision-deficiency.png");
-}
-
-void GenerateAlphaInterpolation()
-{
- const int width = 1000;
- const int rows = 2;
- const int rowHeight = 120;
-
- var colourPoints = new[] { Css.Red, Css.Transparent, Css.Blue };
- var text = Css.Black;
-
- var premultiplied = Gradient.Draw(("With premultiplied alpha", text), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount, premultiplyAlpha: true));
- var notPremultiplied = Gradient.Draw(("Without premultiplied alpha", text), width, rowHeight, colourPoints,
- (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount, premultiplyAlpha: false));
-
- var image = new Image(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");
-}
-
-void GenerateTemperature()
-{
- const int width = 1200;
- const int rows = 1;
- const int rowHeight = 120;
-
- var scaledPoints = new List();
- for (var i = 1000; i <= 13000; i += 100)
- {
- var rgb = new Unicolour(i).Rgb;
- var rgbComponents = new[] { rgb.R, rgb.G, rgb.B };
- var max = rgbComponents.Max();
- var scaledRgb = rgbComponents.Select(x => x / max).ToArray();
- var scaledUnicolour = new Unicolour(ColourSpace.Rgb, scaledRgb[0], scaledRgb[1], scaledRgb[2]);
- scaledPoints.Add(scaledUnicolour);
- }
-
- var text = Css.Black;
-
- var scaled = Gradient.Draw(("CCT (1,000 K - 13,000 K)", text), width, rowHeight, scaledPoints.ToArray(),
- (start, end, amount) => start.Mix(end, ColourSpace.Rgb, amount));
-
- var image = new Image(width, rowHeight * rows);
- image.Mutate(context => context
- .DrawImage(scaled, new Point(0, rowHeight * 0), 1f)
- );
-
- image.Save("temperature.png");
-}
\ No newline at end of file
diff --git a/Unicolour.Readme/Program.cs b/Unicolour.Readme/Program.cs
index b28920ea..723b9edc 100644
--- a/Unicolour.Readme/Program.cs
+++ b/Unicolour.Readme/Program.cs
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
+using Wacton.Unicolour;
var sourceRoot = Path.GetFullPath("./docs");
var solutionRoot = AppDomain.CurrentDomain.BaseDirectory.Split("Unicolour.Readme")[0];
@@ -66,4 +67,183 @@ void CopyDirectory(string sourcePath, string targetPath)
var targetSubDirectory = Path.Combine(targetPath, directoryName);
CopyDirectory(sourceDirectory, targetSubDirectory);
}
+}
+
+/*
+ --------------------------------------------------
+ The following are examples used in the readme
+ --------------------------------------------------
+ */
+
+#pragma warning disable CS8321 // Local function is declared but never used
+// ReSharper disable UnusedVariable
+
+void Overview()
+{
+ Unicolour pink = new("#FF1493");
+ Console.WriteLine(pink.Oklab); // 0.65 +0.26 -0.01
+}
+
+void Installation()
+{
+ Unicolour colour = new(ColourSpace.Rgb255, 192, 255, 238);
+}
+
+void Quickstart()
+{
+ var cyan = new Unicolour("#00FFFF");
+ Console.WriteLine(cyan.Hsl); // 180.0° 100.0% 50.0%
+
+ var yellow = new Unicolour(ColourSpace.Rgb255, 255, 255, 0);
+ Console.WriteLine(yellow.Hex); // #FFFF00
+
+ var red = new Unicolour(ColourSpace.Rgb, 1.0, 0.0, 0.0);
+ var blue = new Unicolour(ColourSpace.Hsb, 240, 1.0, 1.0);
+
+ /* RGB: [1, 0, 0] ⟶ [0, 0, 1] = [0.5, 0, 0.5] */
+ var purple = red.Mix(blue, ColourSpace.Rgb);
+ Console.WriteLine(purple.Rgb); // 0.50 0.00 0.50
+ Console.WriteLine(purple.Hex); // #800080
+
+ /* HSL: [0, 1, 0.5] ⟶ [240, 1, 0.5] = [300, 1, 0.5] */
+ var magenta = red.Mix(blue, ColourSpace.Hsl);
+ Console.WriteLine(magenta.Rgb); // 1.00 0.00 1.00
+ Console.WriteLine(magenta.Hex); // #FF00FF
+
+ var white = new Unicolour(ColourSpace.Oklab, 1.0, 0.0, 0.0);
+ var black = new Unicolour(ColourSpace.Oklab, 0.0, 0.0, 0.0);
+ var difference = white.Difference(black, DeltaE.Ciede2000);
+ Console.WriteLine(difference); // 100.0000
+
+ var equalTristimulus = new Unicolour(ColourSpace.Xyz, 0.5, 0.5, 0.5);
+ Console.WriteLine(equalTristimulus.Chromaticity.Xy); // (0.3333, 0.3333)
+ Console.WriteLine(equalTristimulus.Chromaticity.Uv); // (0.2105, 0.3158)
+ Console.WriteLine(equalTristimulus.Temperature); // 5455.5 K (Δuv -0.00442)
+ Console.WriteLine(equalTristimulus.DominantWavelength); // 596.1
+}
+
+void FeatureConvert()
+{
+ Unicolour colour = new(ColourSpace.Rgb255, 192, 255, 238);
+ var (l, c, h) = colour.Oklch.Triplet;
+}
+
+void FeatureMix()
+{
+ var red = new Unicolour(ColourSpace.Rgb, 1.0, 0.0, 0.0);
+ var blue = new Unicolour(ColourSpace.Hsb, 240, 1.0, 1.0);
+ var magenta = red.Mix(blue, ColourSpace.Hsl, 0.5, HueSpan.Decreasing);
+ var green = red.Mix(blue, ColourSpace.Hsl, 0.5, HueSpan.Increasing);
+}
+
+void FeatureCompare()
+{
+ var red = new Unicolour(ColourSpace.Rgb, 1.0, 0.0, 0.0);
+ var blue = new Unicolour(ColourSpace.Hsb, 240, 1.0, 1.0);
+ var contrast = red.Contrast(blue);
+ var difference = red.Difference(blue, DeltaE.Cie76);
+}
+
+void FeatureGamutMap()
+{
+ var outOfGamut = new Unicolour(ColourSpace.Rgb, -0.51, 1.02, -0.31);
+ var inGamut = outOfGamut.MapToGamut();
+}
+
+void FeatureTemperature()
+{
+ var chromaticity = new Chromaticity(0.3457, 0.3585);
+ var d50 = new Unicolour(chromaticity);
+ var (cct, duv) = d50.Temperature;
+
+ var temperature = new Temperature(6504, 0.0032);
+ var d65 = new Unicolour(temperature);
+ var (x, y) = d65.Chromaticity;
+}
+
+void FeatureSpd()
+{
+ var spd = new Spd
+ {
+ { 575, 0.5 },
+ { 580, 1.0 },
+ { 585, 0.5 }
+ };
+
+ var intenseYellow = new Unicolour(spd);
+}
+
+void FeatureWavelength()
+{
+ var chromaticity = new Chromaticity(0.1, 0.8);
+ var hyperGreen = new Unicolour(chromaticity);
+ var dominantWavelength = hyperGreen.DominantWavelength;
+ var excitationPurity = hyperGreen.ExcitationPurity;
+}
+
+void FeatureImaginary()
+{
+ var chromaticity = new Chromaticity(0.05, 0.05);
+ var impossibleBlue = new Unicolour(chromaticity);
+ var isImaginary = impossibleBlue.IsImaginary;
+}
+
+void FeatureCvd()
+{
+ var colour = new Unicolour(ColourSpace.Rgb255, 192, 255, 238);
+ var noRed = colour.SimulateProtanopia();
+}
+
+void FeatureInvalid()
+{
+ var bad1 = new Unicolour(ColourSpace.Oklab, double.NegativeInfinity, double.NaN, double.Epsilon);
+ var bad2 = new Unicolour(ColourSpace.Cam16, double.NaN, double.MaxValue, double.MinValue);
+ var bad3 = bad1.Mix(bad2, ColourSpace.Hct, amount: double.PositiveInfinity);
+}
+
+void FeatureDefaults()
+{
+ var defaultConfig = new Configuration(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
+ var colour = new Unicolour(defaultConfig, ColourSpace.Rgb255, 192, 255, 238);
+}
+
+void ConfigPredefined()
+{
+ Configuration config = new(RgbConfiguration.Rec2020, XyzConfiguration.D50);
+ Unicolour colour = new(config, ColourSpace.Rgb255, 204, 64, 132);
+}
+
+void ConfigManual()
+{
+ var rgbConfig = new RgbConfiguration(
+ chromaticityR: new(0.7347, 0.2653),
+ chromaticityG: new(0.1152, 0.8264),
+ chromaticityB: new(0.1566, 0.0177),
+ whitePoint: Illuminant.D50.GetWhitePoint(Observer.Degree2),
+ fromLinear: value => Math.Pow(value, 1 / 2.19921875),
+ toLinear: value => Math.Pow(value, 2.19921875)
+ );
+
+ var xyzConfig = new XyzConfiguration(Illuminant.C, Observer.Degree10);
+
+ var config = new Configuration(rgbConfig, xyzConfig);
+ var colour = new Unicolour(config, ColourSpace.Rgb255, 202, 97, 143);
+}
+
+void ConfigConvert()
+{
+ /* pure sRGB green */
+ var srgbConfig = new Configuration(RgbConfiguration.StandardRgb);
+ var srgbColour = new Unicolour(srgbConfig, ColourSpace.Rgb, 0, 1, 0);
+ Console.WriteLine(srgbColour.Rgb); // 0.00 1.00 0.00
+
+ /* ⟶ Display P3 */
+ var displayP3Config = new Configuration(RgbConfiguration.DisplayP3);
+ var displayP3Colour = srgbColour.ConvertToConfiguration(displayP3Config);
+ Console.WriteLine(displayP3Colour.Rgb); // 0.46 0.99 0.30
+
+ /* ⟶ Rec. 2020 */
+ var rec2020Config = new Configuration(RgbConfiguration.Rec2020);
+ var rec2020Colour = displayP3Colour.ConvertToConfiguration(rec2020Config);
+ Console.WriteLine(rec2020Colour.Rgb); // 0.57 0.96 0.27
}
\ No newline at end of file
diff --git a/Unicolour.Readme/README.md b/Unicolour.Readme/README.md
index 557579ed..c8a87542 100644
--- a/Unicolour.Readme/README.md
+++ b/Unicolour.Readme/README.md
@@ -10,9 +10,10 @@ Unicolour is a .NET library written in C# for working with colour:
- Colour space conversion
- Colour mixing / colour interpolation
- Colour difference / colour distance
+- Colour gamut mapping
- Colour chromaticity
- Colour temperature
-- Colour gamut mapping
+- Wavelength attributes
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,12 +99,14 @@ var difference = white.Difference(black, DeltaE.Ciede2000);
Console.WriteLine(difference); // 100.0000
```
-Other useful colour information is available, such as chromaticity coordinates and [temperature](#convert-between-colour-and-temperature).
+Other useful colour information is available, such as chromaticity coordinates,
+[temperature](#convert-between-colour-and-temperature), and [dominant wavelength](#get-wavelength-attributes).
```c#
var equalTristimulus = new Unicolour(ColourSpace.Xyz, 0.5, 0.5, 0.5);
Console.WriteLine(equalTristimulus.Chromaticity.Xy); // (0.3333, 0.3333)
Console.WriteLine(equalTristimulus.Chromaticity.Uv); // (0.2105, 0.3158)
Console.WriteLine(equalTristimulus.Temperature); // 5455.5 K (Δuv -0.00442)
+Console.WriteLine(equalTristimulus.DominantWavelength); // 596.1
```
Reference white points (e.g. D65) and the RGB model (e.g. sRGB) [can be configured](#-configuration).
@@ -217,7 +220,7 @@ XYZ is considered the root colour space.
### Mix colours
-Two colours can be mixed by [interpolating between them in any colour space](#-examples),
+Two colours can be mixed by [interpolating between them in any colour space](#gradients),
taking into account cyclic hue, interpolation distance, and alpha premultiplication.
```c#
var red = new Unicolour(ColourSpace.Rgb, 1.0, 0.0, 0.0);
@@ -258,25 +261,27 @@ var difference = red.Difference(blue, DeltaE.Cie76);
| ΔECAM02 | `DeltaE.Cam02` |
| ΔECAM16 | `DeltaE.Cam16` |
+### Map colour into display gamut
+Colours that cannot be displayed with the [configured RGB model](#rgbconfiguration) can be mapped to the closest in-gamut colour.
+The gamut mapping algorithm conforms to CSS specifications.
+```c#
+var outOfGamut = new Unicolour(ColourSpace.Rgb, -0.51, 1.02, -0.31);
+var inGamut = outOfGamut.MapToGamut();
+```
+
### Convert between colour and temperature
Correlated colour temperature (CCT) and delta UV (∆uv) can be obtained from a colour, and can be used to create a colour.
CCT from 500 K to 1,000,000,000 K is supported but only CCT from 1,000 K to 20,000 K is guaranteed to have high accuracy.
```c#
-var d50 = new Unicolour(ColourSpace.Xyy, 0.3457, 0.3585, 1.0);
+var chromaticity = new Chromaticity(0.3457, 0.3585);
+var d50 = new Unicolour(chromaticity);
var (cct, duv) = d50.Temperature;
-var d65 = new Unicolour(6504, 0.0032);
+var temperature = new Temperature(6504, 0.0032);
+var d65 = new Unicolour(temperature);
var (x, y) = d65.Chromaticity;
```
-### Map colour into display gamut
-Colours that cannot be displayed with the [configured RGB model](#rgbconfiguration) can be mapped to the closest in-gamut colour.
-The gamut mapping algorithm conforms to CSS specifications.
-```c#
-var outOfGamut = new Unicolour(ColourSpace.Rgb, -0.51, 1.02, -0.31);
-var inGamut = outOfGamut.MapToGamut();
-```
-
### Create colour from spectral power distribution
A spectral power distribution (SPD) can be used to create a colour.
Wavelengths should be provided in either 1 nm or 5 nm intervals, and omitted wavelengths are assumed to have zero spectral power.
@@ -291,6 +296,25 @@ var spd = new Spd
var intenseYellow = new Unicolour(spd);
```
+### Get wavelength attributes
+The dominant wavelength and excitation purity of a colour can be derived using the spectral locus.
+Wavelengths from 360 nm to 700 nm are supported.
+```c#
+var chromaticity = new Chromaticity(0.1, 0.8);
+var hyperGreen = new Unicolour(chromaticity);
+var dominantWavelength = hyperGreen.DominantWavelength;
+var excitationPurity = hyperGreen.ExcitationPurity;
+```
+
+### Detect imaginary colours
+Whether or not a colour is imaginary — one that cannot be produced by the eye — can be determined using the spectral locus.
+They are the colours that lie outside of the horseshoe-shaped curve of the [CIE xy chromaticity diagram](#diagrams).
+```c#
+var chromaticity = new Chromaticity(0.05, 0.05);
+var impossibleBlue = new Unicolour(chromaticity);
+var isImaginary = impossibleBlue.IsImaginary;
+```
+
### Simulate colour vision deficiency
A new `Unicolour` can be generated that simulates how a colour appears to someone with a particular colour vision deficiency (CVD) or colour blindness.
```c#
@@ -325,7 +349,7 @@ var colour = new Unicolour(defaultConfig, ColourSpace.Rgb255, 192, 255, 238);
```
## 💡 Configuration
-The `Configuration` parameter can be used to customise how colour is processed.
+The `Configuration` parameter can be used to define the context of the colour.
Example configuration with predefined Rec. 2020 RGB & illuminant D50 (2° observer) XYZ:
```c#
@@ -340,8 +364,8 @@ var rgbConfig = new RgbConfiguration(
chromaticityG: new(0.1152, 0.8264),
chromaticityB: new(0.1566, 0.0177),
whitePoint: Illuminant.D50.GetWhitePoint(Observer.Degree2),
- fromLinear: value => Companding.Gamma(value, 2.19921875),
- toLinear: value => Companding.InverseGamma(value, 2.19921875)
+ fromLinear: value => Math.Pow(value, 1 / 2.19921875),
+ toLinear: value => Math.Pow(value, 2.19921875)
);
var xyzConfig = new XyzConfiguration(Illuminant.C, Observer.Degree10);
@@ -423,19 +447,59 @@ Console.WriteLine(rec2020Colour.Rgb); // 0.57 0.96 0.27
```
## ✨ Examples
-This repo contains an [example project](../Unicolour.Example/Program.cs) that uses Unicolour to:
-1. Generate gradients through each colour space
- ![Gradients through different colour spaces, generated from Unicolour](docs/gradients.png)
-2. Render the colour spectrum with different colour vision deficiencies
- ![Spectrum rendered with different colour vision deficiencies, generated from Unicolour](docs/vision-deficiency.png)
-3. Demonstrate interpolation with and without premultiplied alpha
- ![Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha, generated from Unicolour](docs/alpha-interpolation.png)
-4. Visualise correlated colour temperature (CCT) from 1,000 K to 13,000 K
- ![Visualisation of temperature from 1,000 K to 13,000 K, generated from Unicolour](docs/temperature.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](docs/colour-info.png)
+This repository contains multiple projects to show examples of Unicolour being used to create:
+1. [Images of gradients](#gradients)
+2. [Diagrams of colour data](#diagrams)
+3. [A colourful console application](#console)
+
+### Gradients
+Example code to create images of gradients using 📷 [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) can be seen in the [Example.Gradients](../Example.Gradients/Program.cs) project.
+
+| ![Gradients generated through different colour spaces, created with Unicolour](docs/gradient-colour-spaces.png) |
+|-----------------------------------------------------------------------------------------------------------------|
+| _Gradients generated through each colour space_ |
+
+| ![Visualisation of temperature from 1,000 K to 13,000 K, created with Unicolour](docs/gradient-temperature.png) |
+|-----------------------------------------------------------------------------------------------------------------|
+| _Visualisation of temperature from 1,000 K to 13,000 K_ |
+
+| ![Colour spectrum rendered with different colour vision deficiencies, created with Unicolour](docs/gradient-vision-deficiency.png) |
+|------------------------------------------------------------------------------------------------------------------------------------|
+| _Colour spectrum rendered with different colour vision deficiencies_ |
+
+| ![Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha, created with Unicolour](docs/gradient-alpha-interpolation.png) |
+|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| _Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha_ |
+
+### Diagrams
+Example code to create diagrams of colour data using 📈 [ScottPlot](https://github.com/scottplot/scottplot) can be seen in the [Example.Diagrams](../Example.Diagrams/Program.cs) project.
+
+| ![CIE xy chromaticity diagram with sRGB gamut, created with Unicolour](docs/diagram-xy-chromaticity-rgb.png) |
+|--------------------------------------------------------------------------------------------------------------|
+| _CIE xy chromaticity diagram with sRGB gamut_ |
+
+| ![CIE xy chromaticity diagram with Planckian or blackbody locus, created with Unicolour](docs/diagram-xy-chromaticity-blackbody.png) |
+|--------------------------------------------------------------------------------------------------------------------------------------|
+| _CIE xy chromaticity diagram with Planckian or blackbody locus_ |
+
+| ![CIE xy chromaticity diagram with spectral locus plotted at 1 nm intervals, created with Unicolour](docs/diagram-spectral-locus.png) |
+|---------------------------------------------------------------------------------------------------------------------------------------|
+| _CIE xy chromaticity diagram with spectral locus plotted at 1 nm intervals_ |
+
+| ![CIE 1960 colour space, created with Unicolour](docs/diagram-uv-chromaticity.png) |
+|------------------------------------------------------------------------------------|
+| _CIE 1960 colour space_ |
+
+| ![CIE 1960 colour space with Planckian or blackbody locus, created with Unicolour](docs/diagram-uv-chromaticity-blackbody.png) |
+|--------------------------------------------------------------------------------------------------------------------------------|
+| _CIE 1960 colour space with Planckian or blackbody locus_ |
+
+### Console
+Example code to create a colourful console application using ⌨️ [Spectre.Console](https://github.com/spectreconsole/spectre.console) can be seen in the [Example.Console](../Example.Console/Program.cs) project.
+
+| ![Console application showing colour information from hex value, created with Unicolour](docs/console-colour-info.png) |
+|------------------------------------------------------------------------------------------------------------------------|
+| Console application showing colour information from hex value |
## 🔮 Datasets
Some colour datasets have been compiled for convenience and are available as a [NuGet package](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/).
diff --git a/Unicolour.Readme/Unicolour.Readme.csproj b/Unicolour.Readme/Unicolour.Readme.csproj
index 60201025..e4b426d0 100644
--- a/Unicolour.Readme/Unicolour.Readme.csproj
+++ b/Unicolour.Readme/Unicolour.Readme.csproj
@@ -10,33 +10,52 @@
-
+
Always
-
+
Always
-
+
Always
-
+
Always
-
+
Always
-
+
Always
-
+
Always
-
+
Always
-
+
Always
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
diff --git a/Unicolour.Readme/docs/colour-info.png b/Unicolour.Readme/docs/colour-info.png
deleted file mode 100644
index 820ca6ba..00000000
Binary files a/Unicolour.Readme/docs/colour-info.png and /dev/null differ
diff --git a/Unicolour.Readme/docs/console-colour-info.png b/Unicolour.Readme/docs/console-colour-info.png
new file mode 100644
index 00000000..bda3646e
Binary files /dev/null and b/Unicolour.Readme/docs/console-colour-info.png differ
diff --git a/Unicolour.Readme/docs/diagram-spectral-locus.png b/Unicolour.Readme/docs/diagram-spectral-locus.png
new file mode 100644
index 00000000..78cd20e9
Binary files /dev/null and b/Unicolour.Readme/docs/diagram-spectral-locus.png differ
diff --git a/Unicolour.Readme/docs/diagram-uv-chromaticity-blackbody.png b/Unicolour.Readme/docs/diagram-uv-chromaticity-blackbody.png
new file mode 100644
index 00000000..8cbe1a4d
Binary files /dev/null and b/Unicolour.Readme/docs/diagram-uv-chromaticity-blackbody.png differ
diff --git a/Unicolour.Readme/docs/diagram-uv-chromaticity.png b/Unicolour.Readme/docs/diagram-uv-chromaticity.png
new file mode 100644
index 00000000..e8155117
Binary files /dev/null and b/Unicolour.Readme/docs/diagram-uv-chromaticity.png differ
diff --git a/Unicolour.Readme/docs/diagram-xy-chromaticity-blackbody.png b/Unicolour.Readme/docs/diagram-xy-chromaticity-blackbody.png
new file mode 100644
index 00000000..b7c301ff
Binary files /dev/null and b/Unicolour.Readme/docs/diagram-xy-chromaticity-blackbody.png differ
diff --git a/Unicolour.Readme/docs/diagram-xy-chromaticity-rgb.png b/Unicolour.Readme/docs/diagram-xy-chromaticity-rgb.png
new file mode 100644
index 00000000..503176b3
Binary files /dev/null and b/Unicolour.Readme/docs/diagram-xy-chromaticity-rgb.png differ
diff --git a/Unicolour.Readme/docs/alpha-interpolation.png b/Unicolour.Readme/docs/gradient-alpha-interpolation.png
similarity index 100%
rename from Unicolour.Readme/docs/alpha-interpolation.png
rename to Unicolour.Readme/docs/gradient-alpha-interpolation.png
diff --git a/Unicolour.Readme/docs/gradients.png b/Unicolour.Readme/docs/gradient-colour-spaces.png
similarity index 100%
rename from Unicolour.Readme/docs/gradients.png
rename to Unicolour.Readme/docs/gradient-colour-spaces.png
diff --git a/Unicolour.Readme/docs/temperature.png b/Unicolour.Readme/docs/gradient-temperature.png
similarity index 100%
rename from Unicolour.Readme/docs/temperature.png
rename to Unicolour.Readme/docs/gradient-temperature.png
diff --git a/Unicolour.Readme/docs/vision-deficiency.png b/Unicolour.Readme/docs/gradient-vision-deficiency.png
similarity index 100%
rename from Unicolour.Readme/docs/vision-deficiency.png
rename to Unicolour.Readme/docs/gradient-vision-deficiency.png
diff --git a/Unicolour.Tests/ChromaticityTests.cs b/Unicolour.Tests/ChromaticityTests.cs
index add64b76..e1f02673 100644
--- a/Unicolour.Tests/ChromaticityTests.cs
+++ b/Unicolour.Tests/ChromaticityTests.cs
@@ -5,6 +5,17 @@ namespace Wacton.Unicolour.Tests;
public class ChromaticityTests
{
+ [TestCaseSource(typeof(RandomColours), nameof(RandomColours.XyyTriplets))]
+ public void SameAsXyy(ColourTriplet triplet)
+ {
+ var chromaticity = new Chromaticity(triplet.First, triplet.Second);
+ var luminance = triplet.Third;
+ var fromChromaticity = new Unicolour(chromaticity, luminance);
+ var fromXyy = new Unicolour(ColourSpace.Xyy, chromaticity.X, chromaticity.Y, luminance);
+ TestUtils.AssertTriplet(fromChromaticity.Xyy.Triplet, fromXyy.Xyy.Triplet, 0.0);
+ Assert.That(fromChromaticity.Chromaticity, Is.EqualTo(fromXyy.Chromaticity));
+ }
+
// https://en.wikipedia.org/wiki/Standard_illuminant#White_points_of_standard_illuminants
[TestCase(0.44757, 0.40745, nameof(Illuminant.A), nameof(Observer.Degree2))]
[TestCase(0.45117, 0.40594, nameof(Illuminant.A), nameof(Observer.Degree10))]
diff --git a/Unicolour.Tests/ConfigureXyzTests.cs b/Unicolour.Tests/ConfigureXyzTests.cs
index b058f7d9..9df37a45 100644
--- a/Unicolour.Tests/ConfigureXyzTests.cs
+++ b/Unicolour.Tests/ConfigureXyzTests.cs
@@ -191,7 +191,7 @@ public void WhiteChromaticity(string illuminantName, double expectedX, double ex
{
var illuminant = TestUtils.Illuminants[illuminantName];
var xyzConfig = new XyzConfiguration(illuminant, Observer.Degree2);
- var chromaticity = xyzConfig.ChromaticityWhite;
+ var chromaticity = xyzConfig.WhiteChromaticity;
Assert.That(Math.Round(chromaticity.X, 6), Is.EqualTo(Math.Round(expectedX, 6)));
Assert.That(Math.Round(chromaticity.Y, 6), Is.EqualTo(Math.Round(expectedY, 6)));
}
diff --git a/Unicolour.Tests/DescriptionTests.cs b/Unicolour.Tests/DescriptionTests.cs
index 3549c72b..ea53c678 100644
--- a/Unicolour.Tests/DescriptionTests.cs
+++ b/Unicolour.Tests/DescriptionTests.cs
@@ -382,7 +382,7 @@ public void HueRose(
private static void AssertDescription(double h, double s, double l, ColourDescription included, List excluded)
{
- var hues = new List {h, h + 360, h - 360};
+ var hues = new List { h, h + 360, h - 360 };
foreach (var hue in hues)
{
var unicolour = new Unicolour(ColourSpace.Hsl, hue, s, l);
diff --git a/Unicolour.Tests/DominantWavelengthTests.cs b/Unicolour.Tests/DominantWavelengthTests.cs
new file mode 100644
index 00000000..7e647b97
--- /dev/null
+++ b/Unicolour.Tests/DominantWavelengthTests.cs
@@ -0,0 +1,165 @@
+namespace Wacton.Unicolour.Tests;
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using Wacton.Unicolour.Tests.Utils;
+
+public class DominantWavelengthTests
+{
+ private static readonly Configuration RgbStandardXyzD65 = new(RgbConfiguration.StandardRgb, XyzConfiguration.D65);
+ private static readonly Configuration RgbStandardXyzE = new(RgbConfiguration.StandardRgb, new XyzConfiguration(Illuminant.E, Observer.Degree2));
+ private static readonly Configuration RgbA98XyzD65 = new(RgbConfiguration.A98, XyzConfiguration.D65);
+ private static readonly Configuration RgbA98XyzD50 = new(RgbConfiguration.A98, XyzConfiguration.D50);
+ private static readonly Configuration RgbProPhotoXyzE = new(RgbConfiguration.ProPhoto, new XyzConfiguration(Illuminant.E, Observer.Degree2));
+ private static readonly Configuration RgbProPhotoXyzA = new(RgbConfiguration.ProPhoto, new XyzConfiguration(Illuminant.A, Observer.Degree2));
+ private static readonly Configuration RgbProPhotoXyzD75 = new(RgbConfiguration.ProPhoto, new XyzConfiguration(Illuminant.D75, Observer.Degree2));
+
+ /*
+ * expected colour values for these tests based on calculations from
+ * http://www.brucelindbloom.com/index.html?ColorCalculator.html
+ */
+ private static readonly List RgbTestData = new()
+ {
+ new TestCaseData(RgbStandardXyzD65, 1, 0, 0, 611.4).SetName("sRGB, D65, Red"),
+ new TestCaseData(RgbStandardXyzD65, 0, 1, 0, 549.1).SetName("sRGB, D65, Green"),
+ new TestCaseData(RgbStandardXyzD65, 0, 0, 1, 464.2).SetName("sRGB, D65, Blue"),
+ new TestCaseData(RgbStandardXyzD65, 0, 1, 1, 491.4).SetName("sRGB, D65, Cyan"),
+ new TestCaseData(RgbStandardXyzD65, 1, 0, 1, -549.1).SetName("sRGB, D65, Magenta"),
+ new TestCaseData(RgbStandardXyzD65, 1, 1, 0, 570.5).SetName("sRGB, D65, Yellow"),
+ new TestCaseData(RgbStandardXyzD65, 1, 1, 1, double.NaN).SetName("sRGB, D65, White"),
+ new TestCaseData(RgbStandardXyzD65, 0, 0, 0, double.NaN).SetName("sRGB, D65, Black"),
+
+ new TestCaseData(RgbStandardXyzE, 1, 0, 0, 612.1).SetName("sRGB, E, Red"),
+ new TestCaseData(RgbStandardXyzE, 0, 1, 0, 552.6).SetName("sRGB, E, Green"),
+ new TestCaseData(RgbStandardXyzE, 0, 0, 1, 464.2).SetName("sRGB, E, Blue"),
+ new TestCaseData(RgbStandardXyzE, 0, 1, 1, 491.8).SetName("sRGB, E, Cyan"),
+ new TestCaseData(RgbStandardXyzE, 1, 0, 1, -552.6).SetName("sRGB, E, Magenta"),
+ new TestCaseData(RgbStandardXyzE, 1, 1, 0, 573.2).SetName("sRGB, E, Yellow"),
+ new TestCaseData(RgbStandardXyzE, 1, 1, 1, double.NaN).SetName("sRGB, E, White"),
+ new TestCaseData(RgbStandardXyzE, 0, 0, 0, double.NaN).SetName("sRGB, E, Black"),
+
+ new TestCaseData(RgbA98XyzD65, 1, 0, 0, 611.4).SetName("A98, D65, Red"),
+ new TestCaseData(RgbA98XyzD65, 0, 1, 0, 534.7).SetName("A98, D65, Green"),
+ new TestCaseData(RgbA98XyzD65, 0, 0, 1, 464.2).SetName("A98, D65, Blue"),
+ new TestCaseData(RgbA98XyzD65, 0, 1, 1, 491.4).SetName("A98, D65, Cyan"),
+ new TestCaseData(RgbA98XyzD65, 1, 0, 1, -534.7).SetName("A98, D65, Magenta"),
+ new TestCaseData(RgbA98XyzD65, 1, 1, 0, 570.5).SetName("A98, D65, Yellow"),
+ new TestCaseData(RgbA98XyzD65, 1, 1, 1, double.NaN).SetName("A98, D65, White"),
+ new TestCaseData(RgbA98XyzD65, 0, 0, 0, double.NaN).SetName("A98, D65, Black"),
+
+ new TestCaseData(RgbA98XyzD50, 1, 0, 0, 611.8).SetName("A98, D50, Red"),
+ new TestCaseData(RgbA98XyzD50, 0, 1, 0, 536.9).SetName("A98, D50, Green"),
+ new TestCaseData(RgbA98XyzD50, 0, 0, 1, 463.9).SetName("A98, D50, Blue"),
+ new TestCaseData(RgbA98XyzD50, 0, 1, 1, 493.9).SetName("A98, D50, Cyan"),
+ new TestCaseData(RgbA98XyzD50, 1, 0, 1, -536.9).SetName("A98, D50, Magenta"),
+ new TestCaseData(RgbA98XyzD50, 1, 1, 0, 572.5).SetName("A98, D50, Yellow"),
+ new TestCaseData(RgbA98XyzD50, 1, 1, 1, double.NaN).SetName("A98, D50, White"),
+ new TestCaseData(RgbA98XyzD50, 0, 0, 0, double.NaN).SetName("A98, D50, Black"),
+
+ new TestCaseData(RgbProPhotoXyzE, 1, 0, 0, -493.8).SetName("ProPhoto, E, Red"),
+ new TestCaseData(RgbProPhotoXyzE, 0, 1, 0, 534.1).SetName("ProPhoto, E, Green"),
+ new TestCaseData(RgbProPhotoXyzE, 0, 0, 1, 473.2).SetName("ProPhoto, E, Blue"),
+ new TestCaseData(RgbProPhotoXyzE, 0, 1, 1, 493.8).SetName("ProPhoto, E, Cyan"),
+ new TestCaseData(RgbProPhotoXyzE, 1, 0, 1, -534.1).SetName("ProPhoto, E, Magenta"),
+ new TestCaseData(RgbProPhotoXyzE, 1, 1, 0, 576.1).SetName("ProPhoto, E, Yellow"),
+ new TestCaseData(RgbProPhotoXyzE, 1, 1, 1, double.NaN).SetName("ProPhoto, E, White"),
+ new TestCaseData(RgbProPhotoXyzE, 0, 0, 0, double.NaN).SetName("ProPhoto, E, Black"),
+
+ new TestCaseData(RgbProPhotoXyzA, 1, 0, 0, 638.7).SetName("ProPhoto, A, Red"),
+ new TestCaseData(RgbProPhotoXyzA, 0, 1, 0, 540.3).SetName("ProPhoto, A, Green"),
+ new TestCaseData(RgbProPhotoXyzA, 0, 0, 1, 480.5).SetName("ProPhoto, A, Blue"),
+ new TestCaseData(RgbProPhotoXyzA, 0, 1, 1, 503.2).SetName("ProPhoto, A, Cyan"),
+ new TestCaseData(RgbProPhotoXyzA, 1, 0, 1, -540.3).SetName("ProPhoto, A, Magenta"),
+ new TestCaseData(RgbProPhotoXyzA, 1, 1, 0, 582.7).SetName("ProPhoto, A, Yellow"),
+ new TestCaseData(RgbProPhotoXyzA, 1, 1, 1, double.NaN).SetName("ProPhoto, A, White"),
+ new TestCaseData(RgbProPhotoXyzA, 0, 0, 0, double.NaN).SetName("ProPhoto, A, Black"),
+
+ new TestCaseData(RgbProPhotoXyzD75, 1, 0, 0, -492.3).SetName("ProPhoto, D75, Red"),
+ new TestCaseData(RgbProPhotoXyzD75, 0, 1, 0, 530.2).SetName("ProPhoto, D75, Green"),
+ new TestCaseData(RgbProPhotoXyzD75, 0, 0, 1, 472.2).SetName("ProPhoto, D75, Blue"),
+ new TestCaseData(RgbProPhotoXyzD75, 0, 1, 1, 492.3).SetName("ProPhoto, D75, Cyan"),
+ new TestCaseData(RgbProPhotoXyzD75, 1, 0, 1, -530.2).SetName("ProPhoto, D75, Magenta"),
+ new TestCaseData(RgbProPhotoXyzD75, 1, 1, 0, 572.7).SetName("ProPhoto, D75, Yellow"),
+ new TestCaseData(RgbProPhotoXyzD75, 1, 1, 1, double.NaN).SetName("ProPhoto, D75, White"),
+ new TestCaseData(RgbProPhotoXyzD75, 0, 0, 0, double.NaN).SetName("ProPhoto, D75, Black")
+ };
+
+ [TestCaseSource(nameof(RgbTestData))]
+ public void RgbGamut(Configuration configuration, double r, double g, double b, double expectedWavelength)
+ {
+ var unicolour = new Unicolour(configuration, ColourSpace.Rgb, r, g, b);
+ var hasLuminance = unicolour.Xyy.Luminance > 0;
+ Assert.That(unicolour.DominantWavelength, Is.EqualTo(hasLuminance ? expectedWavelength : double.NaN).Within(0.25));
+ }
+
+ private static readonly Dictionary<(Illuminant illuminant, Observer observer), Configuration> Configurations = new()
+ {
+ { (Illuminant.D65, Observer.Degree2), new(xyzConfiguration: new(Illuminant.D65, Observer.Degree2)) },
+ { (Illuminant.D65, Observer.Degree10), new(xyzConfiguration: new(Illuminant.D65, Observer.Degree10)) },
+ { (Illuminant.E, Observer.Degree2), new(xyzConfiguration: new(Illuminant.E, Observer.Degree2)) },
+ { (Illuminant.E, Observer.Degree10), new(xyzConfiguration: new(Illuminant.E, Observer.Degree10)) }
+ };
+
+ [Test]
+ public void Monochromatic(
+ [Range(360, 700)] int wavelength,
+ [Values(nameof(Observer.Degree2), nameof(Observer.Degree10))] string observerName,
+ [Values(nameof(Illuminant.D65), nameof(Illuminant.E))] string illuminantName)
+ {
+ var illuminant = TestUtils.Illuminants[illuminantName];
+ var observer = TestUtils.Observers[observerName];
+ var config = Configurations[(illuminant, observer)];
+
+ var unicolour = new Unicolour(config, new Spd { { wavelength, 1.0 } });
+ Assert.That(unicolour.DominantWavelength, Is.EqualTo(wavelength).Within(0.000000005));
+ }
+
+ /*
+ * expected colour values for these tests based on calculations from
+ * http://www.brucelindbloom.com/index.html?ColorCalculator.html
+ */
+ private static readonly List ImaginaryTestData = new()
+ {
+ new TestCaseData(RgbStandardXyzD65, 0, 0, 477.2).SetName("sRGB, D65, (0,0)"),
+ new TestCaseData(RgbStandardXyzD65, 0, 1, 520.4).SetName("sRGB, D65, (0,1)"),
+ new TestCaseData(RgbStandardXyzD65, 1, 0, -497.3).SetName("sRGB, D65, (1,0)"),
+ new TestCaseData(RgbStandardXyzD65, 1, 1, 577.2).SetName("sRGB, D65, (1,1)"),
+
+ new TestCaseData(RgbStandardXyzE, 0, 0, 476.8).SetName("sRGB, E, (0,0)"),
+ new TestCaseData(RgbStandardXyzE, 0, 1, 521.2).SetName("sRGB, E, (0,1)"),
+ new TestCaseData(RgbStandardXyzE, 1, 0, -498.2).SetName("sRGB, E, (1,0)"),
+ new TestCaseData(RgbStandardXyzE, 1, 1, 578.1).SetName("sRGB, E, (1,1)"),
+
+ new TestCaseData(RgbA98XyzD65, 0, 0, 477.2).SetName("A98, D65, (0,0)"),
+ new TestCaseData(RgbA98XyzD65, 0, 1, 520.4).SetName("A98, D65, (0,1)"),
+ new TestCaseData(RgbA98XyzD65, 1, 0, -497.3).SetName("A98, D65, (1,0)"),
+ new TestCaseData(RgbA98XyzD65, 1, 1, 577.2).SetName("A98, D65, (1,1)"),
+
+ new TestCaseData(RgbA98XyzD50, 0, 0, 477.1).SetName("A98, D50, (0,0)"),
+ new TestCaseData(RgbA98XyzD50, 0, 1, 522.1).SetName("A98, D50, (0,1)"),
+ new TestCaseData(RgbA98XyzD50, 1, 0, -500.2).SetName("A98, D50, (1,0)"),
+ new TestCaseData(RgbA98XyzD50, 1, 1, 577.3).SetName("A98, D50, (1,1)"),
+
+ new TestCaseData(RgbProPhotoXyzE, 0, 0, 476.8).SetName("ProPhoto, E, (0,0)"),
+ new TestCaseData(RgbProPhotoXyzE, 0, 1, 521.2).SetName("ProPhoto, E, (0,1)"),
+ new TestCaseData(RgbProPhotoXyzE, 1, 0, -498.2).SetName("ProPhoto, E, (1,0)"),
+ new TestCaseData(RgbProPhotoXyzE, 1, 1, 578.1).SetName("ProPhoto, E, (1,1)"),
+
+ new TestCaseData(RgbProPhotoXyzA, 0, 0, 476.0).SetName("ProPhoto, A, (0,0)"),
+ new TestCaseData(RgbProPhotoXyzA, 0, 1, 528.4).SetName("ProPhoto, A, (0,1)"),
+ new TestCaseData(RgbProPhotoXyzA, 1, 0, -508.9).SetName("ProPhoto, A, (1,0)"),
+ new TestCaseData(RgbProPhotoXyzA, 1, 1, 580.7).SetName("ProPhoto, A, (1,1)"),
+
+ new TestCaseData(RgbProPhotoXyzD75, 0, 0, 477.2).SetName("ProPhoto, D75, (0,0)"),
+ new TestCaseData(RgbProPhotoXyzD75, 0, 1, 519.8).SetName("ProPhoto, D75, (0,1)"),
+ new TestCaseData(RgbProPhotoXyzD75, 1, 0, -496.1).SetName("ProPhoto, D75, (1,0)"),
+ new TestCaseData(RgbProPhotoXyzD75, 1, 1, 577.2).SetName("ProPhoto, D75, (1,1)")
+ };
+
+ [TestCaseSource(nameof(ImaginaryTestData))]
+ public void Imaginary(Configuration config, double x, double y, double expectedWavelength)
+ {
+ var unicolour = new Unicolour(config, new Chromaticity(x, y));
+ Assert.That(unicolour.DominantWavelength, Is.EqualTo(expectedWavelength).Within(0.25));
+ }
+}
\ No newline at end of file
diff --git a/Unicolour.Tests/EqualityTests.cs b/Unicolour.Tests/EqualityTests.cs
index 8ccd50ec..1896babe 100644
--- a/Unicolour.Tests/EqualityTests.cs
+++ b/Unicolour.Tests/EqualityTests.cs
@@ -72,9 +72,10 @@ public void DifferentConfigurationObjects()
new Chromaticity(0.5, 0.5),
new WhitePoint(0.9, 1.0, 1.1),
RgbModels.StandardRgb.FromLinear,
- RgbModels.StandardRgb.ToLinear);
- var xyzConfig1 = new XyzConfiguration(new WhitePoint(0.95, 1.0, 1.05));
- var camConfig1 = new CamConfiguration(new WhitePoint(0.9, 1.0, 1.1), 4, 20, Surround.Dark);
+ RgbModels.StandardRgb.ToLinear,
+ "RGB 1");
+ var xyzConfig1 = new XyzConfiguration(new WhitePoint(0.95, 1.0, 1.05), "XYZ 1");
+ var camConfig1 = new CamConfiguration(new WhitePoint(0.9, 1.0, 1.1), 4, 20, Surround.Dark, "CAM 1");
var config1 = new Configuration(rgbConfig1, xyzConfig1, camConfig1);
var rgbConfig2 = new RgbConfiguration(
@@ -83,9 +84,10 @@ public void DifferentConfigurationObjects()
new Chromaticity(0.5, 0.5),
new WhitePoint(0.9, 1.0, 1.1),
RgbModels.StandardRgb.FromLinear,
- RgbModels.StandardRgb.ToLinear);
- var xyzConfig2 = new XyzConfiguration(new WhitePoint(0.95001, 1.0001, 1.05001));
- var camConfig2 = new CamConfiguration(new WhitePoint(0.9, 1.0, 1.1), 4, 20, Surround.Dim);
+ RgbModels.StandardRgb.ToLinear,
+ "RGB 2");
+ var xyzConfig2 = new XyzConfiguration(new WhitePoint(0.95001, 1.0001, 1.05001), "XYZ 2");
+ var camConfig2 = new CamConfiguration(new WhitePoint(0.9, 1.0, 1.1), 4, 20, Surround.Dim, "CAM 2");
var config2 = new Configuration(rgbConfig2, xyzConfig2, camConfig2);
AssertEqual(config1.Rgb.ChromaticityR, config2.Rgb.ChromaticityR);
@@ -118,35 +120,38 @@ public void DifferentColourHeritageObjects()
private static void AssertUnicoloursEqual(Unicolour unicolour1, Unicolour unicolour2)
{
- AssertEqual(unicolour1.Rgb, unicolour2.Rgb);
- AssertEqual(unicolour1.Rgb.Byte255, unicolour2.Rgb.Byte255);
- AssertEqual(unicolour1.RgbLinear, unicolour2.RgbLinear);
+ AssertEqual(unicolour1.Alpha, unicolour2.Alpha);
+ AssertEqual(unicolour1.Cam02, unicolour2.Cam02);
+ AssertEqual(unicolour1.Cam16, unicolour2.Cam16);
+ AssertEqual(unicolour1.Chromaticity, unicolour2.Chromaticity);
+ AssertEqual(unicolour1.Description, unicolour2.Description);
+ AssertEqual(unicolour1.DominantWavelength, unicolour2.DominantWavelength);
+ AssertEqual(unicolour1.ExcitationPurity, unicolour2.ExcitationPurity);
+ AssertEqual(unicolour1.Hct, unicolour2.Hct);
+ AssertEqual(unicolour1.Hex, unicolour2.Hex);
+ AssertEqual(unicolour1.Hpluv, unicolour2.Hpluv);
AssertEqual(unicolour1.Hsb, unicolour2.Hsb);
AssertEqual(unicolour1.Hsl, unicolour2.Hsl);
+ AssertEqual(unicolour1.Hsluv, unicolour2.Hsluv);
AssertEqual(unicolour1.Hwb, unicolour2.Hwb);
- AssertEqual(unicolour1.Xyz, unicolour2.Xyz);
- AssertEqual(unicolour1.Xyy, unicolour2.Xyy);
+ AssertEqual(unicolour1.Ictcp, unicolour2.Ictcp);
+ AssertEqual(unicolour1.IsImaginary, unicolour2.IsImaginary);
+ AssertEqual(unicolour1.IsInDisplayGamut, unicolour2.IsInDisplayGamut);
+ AssertEqual(unicolour1.Jzazbz, unicolour2.Jzazbz);
+ AssertEqual(unicolour1.Jzczhz, unicolour2.Jzczhz);
AssertEqual(unicolour1.Lab, unicolour2.Lab);
AssertEqual(unicolour1.Lchab, unicolour2.Lchab);
AssertEqual(unicolour1.Luv, unicolour2.Luv);
AssertEqual(unicolour1.Lchuv, unicolour2.Lchuv);
- AssertEqual(unicolour1.Hsluv, unicolour2.Hsluv);
- AssertEqual(unicolour1.Hpluv, unicolour2.Hpluv);
- AssertEqual(unicolour1.Ictcp, unicolour2.Ictcp);
- AssertEqual(unicolour1.Jzazbz, unicolour2.Jzazbz);
- AssertEqual(unicolour1.Jzczhz, unicolour2.Jzczhz);
AssertEqual(unicolour1.Oklab, unicolour2.Oklab);
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.Chromaticity, unicolour2.Chromaticity);
- AssertEqual(unicolour1.IsInDisplayGamut, unicolour2.IsInDisplayGamut);
AssertEqual(unicolour1.RelativeLuminance, unicolour2.RelativeLuminance);
- AssertEqual(unicolour1.Description, unicolour2.Description);
+ AssertEqual(unicolour1.Rgb, unicolour2.Rgb);
+ AssertEqual(unicolour1.Rgb.Byte255, unicolour2.Rgb.Byte255);
+ AssertEqual(unicolour1.RgbLinear, unicolour2.RgbLinear);
AssertEqual(unicolour1.Temperature, unicolour2.Temperature);
+ AssertEqual(unicolour1.Xyz, unicolour2.Xyz);
+ AssertEqual(unicolour1.Xyy, unicolour2.Xyy);
if (unicolour1.Xyz.HctToXyzSearchResult != null)
{
@@ -169,6 +174,7 @@ private static void AssertConfigurationEqual(Configuration config1, Configuratio
AssertEqual(config1.Rgb.InverseCompandToLinear, config2.Rgb.InverseCompandToLinear);
AssertEqual(config1.Xyz.WhitePoint, config2.Xyz.WhitePoint);
AssertEqual(config1.Xyz.Observer, config2.Xyz.Observer);
+ AssertEqual(config1.Xyz.Spectral, config2.Xyz.Spectral);
AssertEqual(config1.Xyz.Planckian, config2.Xyz.Planckian);
AssertEqual(config1.Cam.WhitePoint, config2.Cam.WhitePoint);
AssertEqual(config1.Cam.AdaptingLuminance, config2.Cam.AdaptingLuminance);
diff --git a/Unicolour.Tests/ExcitationPurityTests.cs b/Unicolour.Tests/ExcitationPurityTests.cs
new file mode 100644
index 00000000..03aac108
--- /dev/null
+++ b/Unicolour.Tests/ExcitationPurityTests.cs
@@ -0,0 +1,55 @@
+namespace Wacton.Unicolour.Tests;
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using Wacton.Unicolour.Tests.Utils;
+
+public class ExcitationPurityTests
+{
+ [Test]
+ public void RgbGamut([Values(0, 255)] int r, [Values(0, 255)] int g, [Values(0, 255)] int b)
+ {
+ var unicolour = new Unicolour(ColourSpace.Rgb255, r, g, b);
+ var greyscale = r == g && g == b;
+ Assert.That(unicolour.ExcitationPurity, greyscale ? Is.NaN : Is.LessThan(1.0));
+ }
+
+ [Test]
+ public void Greyscale([Range(0, 1, 0.1)] double value)
+ {
+ var unicolour = new Unicolour(ColourSpace.Rgb, value, value, value);
+ Assert.That(unicolour.ExcitationPurity, Is.NaN);
+ }
+
+ private static readonly Dictionary<(Illuminant illuminant, Observer observer), Configuration> Configurations = new()
+ {
+ { (Illuminant.D65, Observer.Degree2), new(xyzConfiguration: new(Illuminant.D65, Observer.Degree2)) },
+ { (Illuminant.D65, Observer.Degree10), new(xyzConfiguration: new(Illuminant.D65, Observer.Degree10)) },
+ { (Illuminant.E, Observer.Degree2), new(xyzConfiguration: new(Illuminant.E, Observer.Degree2)) },
+ { (Illuminant.E, Observer.Degree10), new(xyzConfiguration: new(Illuminant.E, Observer.Degree10)) }
+ };
+
+ [Test]
+ public void Monochromatic(
+ [Range(360, 700)] int wavelength,
+ [Values(nameof(Observer.Degree2), nameof(Observer.Degree10))] string observerName,
+ [Values(nameof(Illuminant.D65), nameof(Illuminant.E))] string illuminantName)
+ {
+ var illuminant = TestUtils.Illuminants[illuminantName];
+ var observer = TestUtils.Observers[observerName];
+ var config = Configurations[(illuminant, observer)];
+
+ var unicolour = new Unicolour(config, new Spd { { wavelength, 1.0 } });
+ Assert.That(unicolour.ExcitationPurity, Is.EqualTo(1.0).Within(0.00000000000005));
+ }
+
+ [TestCase(0, 0)]
+ [TestCase(0, 1)]
+ [TestCase(1, 0)]
+ [TestCase(1, 1)]
+ public void Imaginary(double x, double y)
+ {
+ var unicolour = new Unicolour(new Chromaticity(x, y));
+ Assert.That(unicolour.ExcitationPurity, Is.GreaterThan(1.0));
+ }
+}
\ No newline at end of file
diff --git a/Unicolour.Tests/ExtremeValuesTests.cs b/Unicolour.Tests/ExtremeValuesTests.cs
index 4200b5c5..2573e045 100644
--- a/Unicolour.Tests/ExtremeValuesTests.cs
+++ b/Unicolour.Tests/ExtremeValuesTests.cs
@@ -31,13 +31,22 @@ public void CreateFromHexWithAlpha(
TestUtils.AssertNoPropertyError(new Unicolour(hex, alpha));
}
+ [Test, Combinatorial]
+ public void CreateFromChromaticity(
+ [ValueSource(typeof(TestUtils), nameof(TestUtils.ExtremeDoubles))] double x,
+ [ValueSource(typeof(TestUtils), nameof(TestUtils.ExtremeDoubles))] double y,
+ [ValueSource(typeof(TestUtils), nameof(TestUtils.ExtremeDoubles))] double luminance)
+ {
+ TestUtils.AssertNoPropertyError(new Unicolour(new Chromaticity(x, y), luminance));
+ }
+
[Test, Combinatorial]
public void CreateFromTemperature(
[ValueSource(typeof(TestUtils), nameof(TestUtils.ExtremeDoubles))] double cct,
[ValueSource(typeof(TestUtils), nameof(TestUtils.ExtremeDoubles))] double duv,
[ValueSource(typeof(TestUtils), nameof(TestUtils.ExtremeDoubles))] double luminance)
{
- TestUtils.AssertNoPropertyError(new Unicolour(cct, duv, luminance));
+ TestUtils.AssertNoPropertyError(new Unicolour(new Temperature(cct, duv), luminance));
}
[Test, Combinatorial]
diff --git a/Unicolour.Tests/ImaginaryTests.cs b/Unicolour.Tests/ImaginaryTests.cs
new file mode 100644
index 00000000..724cee6c
--- /dev/null
+++ b/Unicolour.Tests/ImaginaryTests.cs
@@ -0,0 +1,83 @@
+namespace Wacton.Unicolour.Tests;
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using Wacton.Unicolour.Tests.Utils;
+
+public class ImaginaryTests
+{
+ [Test]
+ public void RgbGamut([Values(0, 255)] int r, [Values(0, 255)] int g, [Values(0, 255)] int b)
+ {
+ var unicolour = new Unicolour(ColourSpace.Rgb255, r, g, b);
+ Assert.That(unicolour.IsImaginary, Is.False);
+ }
+
+ [Test]
+ public void Greyscale([Range(0, 1, 0.1)] double value)
+ {
+ var unicolour = new Unicolour(ColourSpace.Rgb, value, value, value);
+ Assert.That(unicolour.IsImaginary, Is.False);
+ }
+
+ private static readonly Dictionary<(Illuminant illuminant, Observer observer), Configuration> Configurations = new()
+ {
+ { (Illuminant.D65, Observer.Degree2), new(xyzConfiguration: new(Illuminant.D65, Observer.Degree2)) },
+ { (Illuminant.D65, Observer.Degree10), new(xyzConfiguration: new(Illuminant.D65, Observer.Degree10)) },
+ { (Illuminant.E, Observer.Degree2), new(xyzConfiguration: new(Illuminant.E, Observer.Degree2)) },
+ { (Illuminant.E, Observer.Degree10), new(xyzConfiguration: new(Illuminant.E, Observer.Degree10)) }
+ };
+
+ [Test]
+ public void Monochromatic(
+ [Range(360, 700)] int wavelength,
+ [Values(nameof(Observer.Degree2), nameof(Observer.Degree10))] string observerName,
+ [Values(nameof(Illuminant.D65), nameof(Illuminant.E))] string illuminantName)
+ {
+ var illuminant = TestUtils.Illuminants[illuminantName];
+ var observer = TestUtils.Observers[observerName];
+ var config = Configurations[(illuminant, observer)];
+
+ var unicolour = new Unicolour(config, new Spd { { wavelength, 1.0 } });
+ Assert.That(unicolour.IsImaginary, Is.False);
+ }
+
+ private const double Offset = 0.0000001;
+ [TestCase(Edge.Bottom, 0, Offset, false)] // coordinates directly above are inside the boundary
+ [TestCase(Edge.Bottom, 0, -Offset, true)]
+ [TestCase(Edge.Bottom, Offset, 0, true)]
+ [TestCase(Edge.Bottom, -Offset, 0, true)]
+ [TestCase(Edge.Left, 0, Offset, true)]
+ [TestCase(Edge.Left, 0, -Offset, true)]
+ [TestCase(Edge.Left, Offset, 0, false)] // coordinates directly to the right are inside the boundary
+ [TestCase(Edge.Left, -Offset, 0, true)]
+ [TestCase(Edge.Right, 0, Offset, true)]
+ [TestCase(Edge.Right, 0, -Offset, true)]
+ [TestCase(Edge.Right, Offset, 0, true)]
+ [TestCase(Edge.Right, -Offset, 0, false)] // coordinates directly to the left are inside the boundary
+ [TestCase(Edge.Top, 0, Offset, true)]
+ [TestCase(Edge.Top, 0, -Offset, false)] // coordinates directly below are inside the boundary
+ [TestCase(Edge.Top, Offset, 0, true)]
+ [TestCase(Edge.Top, -Offset, 0, true)]
+ public void BoundaryEdge(Edge edge, double xOffset, double yOffset, bool expectedImaginary)
+ {
+ // effectively the bounding box, based on 2 degree observer
+ var wavelength = edge switch
+ {
+ Edge.Bottom => 404,
+ Edge.Left => 504,
+ Edge.Top => 521,
+ Edge.Right => 699,
+ _ => throw new ArgumentOutOfRangeException(nameof(edge), edge, null)
+ };
+
+ var monochromatic = new Unicolour(new Spd { { wavelength, 1.0 } });
+ var chromaticity = monochromatic.Chromaticity;
+ var offsetChromaticity = new Chromaticity(chromaticity.X + xOffset, chromaticity.Y + yOffset);
+ var unicolour = new Unicolour(offsetChromaticity);
+ Assert.That(unicolour.IsImaginary, Is.EqualTo(expectedImaginary));
+ }
+
+ public enum Edge { Bottom, Left, Top, Right }
+}
\ No newline at end of file
diff --git a/Unicolour.Tests/KnownTemperatureTests.cs b/Unicolour.Tests/KnownTemperatureTests.cs
index d67eb8f2..6ba77094 100644
--- a/Unicolour.Tests/KnownTemperatureTests.cs
+++ b/Unicolour.Tests/KnownTemperatureTests.cs
@@ -93,7 +93,7 @@ public void DaylightCct(string illuminantName, double cct, double x, double y)
// however, the blackbody CCT should be similar no matter how they are constructed
var blackbodyTemperature = Temperature.FromChromaticity(fromChromaticity.Chromaticity, TestUtils.PlanckianObserverDegree2);
- var fromBlackbodyCct = new Unicolour(config, blackbodyTemperature.Cct, blackbodyTemperature.Duv);
+ var fromBlackbodyCct = new Unicolour(config, blackbodyTemperature);
Assert.That(fromBlackbodyCct.Temperature.Cct, Is.EqualTo(fromChromaticity.Temperature.Cct).Within(0.75));
Assert.That(fromBlackbodyCct.Temperature.Duv, Is.EqualTo(fromChromaticity.Temperature.Duv).Within(0.0005));
Assert.That(fromChromaticity.Temperature.Cct, Is.EqualTo(fromColour.Temperature.Cct).Within(0.75));
diff --git a/Unicolour.Tests/LineTests.cs b/Unicolour.Tests/LineTests.cs
new file mode 100644
index 00000000..17874883
--- /dev/null
+++ b/Unicolour.Tests/LineTests.cs
@@ -0,0 +1,153 @@
+namespace Wacton.Unicolour.Tests;
+
+using NUnit.Framework;
+using Wacton.Unicolour.Tests.Utils;
+
+public class LineTests
+{
+ [Test]
+ public void None()
+ {
+ (double x, double y) point1 = (1, 1);
+ (double x, double y) point2 = (1, 1);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.NaN);
+ Assert.That(line.Intercept, Is.NaN);
+ }
+
+ [Test]
+ public void Vertical()
+ {
+ (double x, double y) point1 = (1, 2);
+ (double x, double y) point2 = (1, 3);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(double.PositiveInfinity));
+ Assert.That(line.Intercept, Is.EqualTo(point1.x));
+ }
+
+ [Test]
+ public void Horizontal()
+ {
+ (double x, double y) point1 = (2, 1);
+ (double x, double y) point2 = (3, 1);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(0));
+ Assert.That(line.Intercept, Is.EqualTo(point1.y));
+ }
+
+ [Test]
+ public void DiagonalPositive1()
+ {
+ (double x, double y) point1 = (1, 1);
+ (double x, double y) point2 = (2, 2);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(1));
+ Assert.That(line.Intercept, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void DiagonalPositive2()
+ {
+ (double x, double y) point1 = (1, 2);
+ (double x, double y) point2 = (2, 4);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(2));
+ Assert.That(line.Intercept, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void DiagonalNegative1()
+ {
+ (double x, double y) point1 = (1, -1);
+ (double x, double y) point2 = (2, -2);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(-1));
+ Assert.That(line.Intercept, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void DiagonalNegative2()
+ {
+ (double x, double y) point1 = (1, -2);
+ (double x, double y) point2 = (2, -4);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(-2));
+ Assert.That(line.Intercept, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void DiagonalPositive1Offset()
+ {
+ (double x, double y) point1 = (1, 11);
+ (double x, double y) point2 = (2, 12);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(1));
+ Assert.That(line.Intercept, Is.EqualTo(10));
+ }
+
+ [Test]
+ public void DiagonalPositive2Offset()
+ {
+ (double x, double y) point1 = (1, 12);
+ (double x, double y) point2 = (2, 14);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(2));
+ Assert.That(line.Intercept, Is.EqualTo(10));
+ }
+
+ [Test]
+ public void DiagonalNegative1Offset()
+ {
+ (double x, double y) point1 = (1, -11);
+ (double x, double y) point2 = (2, -12);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(-1));
+ Assert.That(line.Intercept, Is.EqualTo(-10));
+ }
+
+ [Test]
+ public void DiagonalNegative2Offset()
+ {
+ (double x, double y) point1 = (1, -12);
+ (double x, double y) point2 = (2, -14);
+ var line = Line.FromPoints(point1, point2);
+ Assert.That(line.Slope, Is.EqualTo(-2));
+ Assert.That(line.Intercept, Is.EqualTo(-10));
+ }
+
+ [Test]
+ public void IntersectVertical()
+ {
+ var horizontalLine = Line.FromPoints((2, 1), (3, 1));
+ var verticalLine = Line.FromPoints((5, 10), (5, 20));
+ AssertIntersect(horizontalLine, verticalLine, 5, 1);
+ }
+
+ [Test]
+ public void IntersectDiagonal()
+ {
+ var diagonalPositiveLine = Line.FromPoints((-5, -5), (5, 5));
+ var diagonalNegativeLine = Line.FromPoints((5, -5), (-5, 5));
+ AssertIntersect(diagonalPositiveLine, diagonalNegativeLine, 0, 0);
+ }
+
+ [Test]
+ public void DifferentPointsSameLine()
+ {
+ var line1 = Line.FromPoints((0, 0), (10, 10));
+ var line2 = Line.FromPoints((20, 20), (50, 50));
+ AssertIntersect(line1, line2, double.NaN, double.NaN);
+ TestUtils.AssertEqual(line1, line2);
+ }
+
+ private static void AssertIntersect(Line line1, Line line2, double expectedX, double expectedY)
+ {
+ var intersect1 = line1.GetIntersect(line2);
+ Assert.That(intersect1.x, Is.EqualTo(expectedX));
+ Assert.That(intersect1.y, Is.EqualTo(expectedY));
+
+ var intersect2 = line2.GetIntersect(line1);
+ Assert.That(intersect2.x, Is.EqualTo(expectedX));
+ Assert.That(intersect2.y, Is.EqualTo(expectedY));
+ }
+}
\ No newline at end of file
diff --git a/Unicolour.Tests/RoundtripXyyTests.cs b/Unicolour.Tests/RoundtripXyyTests.cs
index 250b92dd..3b8419e5 100644
--- a/Unicolour.Tests/RoundtripXyyTests.cs
+++ b/Unicolour.Tests/RoundtripXyyTests.cs
@@ -12,7 +12,7 @@ public class RoundtripXyyTests
public void XyyRoundTrip(ColourTriplet triplet)
{
var original = new Xyy(triplet.First, triplet.Second, triplet.Third);
- var roundtrip = Xyy.FromXyz(Xyy.ToXyz(original), XyzConfig);
+ var roundtrip = Xyy.FromXyz(Xyy.ToXyz(original), XyzConfig.WhiteChromaticity);
TestUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance);
}
}
\ No newline at end of file
diff --git a/Unicolour.Tests/RoundtripXyzTests.cs b/Unicolour.Tests/RoundtripXyzTests.cs
index a233b33d..8d078eaf 100644
--- a/Unicolour.Tests/RoundtripXyzTests.cs
+++ b/Unicolour.Tests/RoundtripXyzTests.cs
@@ -25,7 +25,7 @@ public void ViaRgbLinear(ColourTriplet triplet)
public void ViaXyy(ColourTriplet triplet)
{
var original = new Xyz(triplet.First, triplet.Second, triplet.Third);
- var roundtrip = Xyy.ToXyz(Xyy.FromXyz(original, XyzConfig));
+ var roundtrip = Xyy.ToXyz(Xyy.FromXyz(original, XyzConfig.WhiteChromaticity));
TestUtils.AssertTriplet(roundtrip.Triplet, original.Triplet, Tolerance);
}
diff --git a/Unicolour.Tests/SmokeTests.cs b/Unicolour.Tests/SmokeTests.cs
index 1df48719..5be93e1d 100644
--- a/Unicolour.Tests/SmokeTests.cs
+++ b/Unicolour.Tests/SmokeTests.cs
@@ -142,11 +142,37 @@ public void HexDigit8WithAlphaOverride(
AssertNoError(expected, new Unicolour(hex, alphaOverride));
AssertNoError(expected, new Unicolour(Configuration.Default, hex, alphaOverride));
}
+
+ private static readonly List ChromaticityValues = new() { 0.0, 0.25, 0.4, 0.5, 0.6, 0.75, 1.0 };
+ private static readonly List LuminanceValues = new() { 1.0, 0.5, 0.0 };
+
+ [Test]
+ public void Chromaticity(
+ [ValueSource(nameof(ChromaticityValues))] double x,
+ [ValueSource(nameof(ChromaticityValues))] double y)
+ {
+ var chromaticity = new Chromaticity(x, y);
+ var expected = new Unicolour(chromaticity);
+ AssertNoError(expected, new Unicolour(chromaticity));
+ AssertNoError(expected, new Unicolour(Configuration.Default, chromaticity));
+ }
+
+ [Test]
+ public void ChromaticityWithLuminance(
+ [ValueSource(nameof(ChromaticityValues))] double x,
+ [ValueSource(nameof(ChromaticityValues))] double y,
+ [ValueSource(nameof(LuminanceValues))] double luminance)
+ {
+ var chromaticity = new Chromaticity(x, y);
+ var expected = new Unicolour(chromaticity, luminance);
+ AssertNoError(expected, new Unicolour(chromaticity, luminance));
+ AssertNoError(expected, new Unicolour(Configuration.Default, chromaticity, luminance));
+ }
+
private static readonly List CctValues = new() { 400, 500, 1000, 6504, 20000, 25000, 1e9 };
private static readonly List DuvValues = new() { -0.05, 0, 0.05 };
private static readonly List LocusValues = new() { Locus.Blackbody, Locus.Daylight };
- private static readonly List LuminanceValues = new() { 1.0, 0.5, 0.0 };
[Test]
public void TemperatureOnlyCct(
@@ -162,9 +188,10 @@ public void TemperatureWithDuv(
[ValueSource(nameof(CctValues))] double cct,
[ValueSource(nameof(DuvValues))] double duv)
{
- var expected = new Unicolour(cct, duv);
- AssertNoError(expected, new Unicolour(cct, duv));
- AssertNoError(expected, new Unicolour(Configuration.Default, cct, duv));
+ var temperature = new Temperature(cct, duv);
+ var expected = new Unicolour(temperature);
+ AssertNoError(expected, new Unicolour(temperature));
+ AssertNoError(expected, new Unicolour(Configuration.Default, temperature));
}
[Test, Combinatorial]
@@ -196,9 +223,10 @@ public void TemperatureWithDuvAndLuminance(
[ValueSource(nameof(DuvValues))] double duv,
[ValueSource(nameof(LuminanceValues))] double luminance)
{
- var expected = new Unicolour(cct, duv, luminance);
- AssertNoError(expected, new Unicolour(cct, duv, luminance));
- AssertNoError(expected, new Unicolour(Configuration.Default, cct, duv, luminance));
+ var temperature = new Temperature(cct, duv);
+ var expected = new Unicolour(temperature, luminance);
+ AssertNoError(expected, new Unicolour(temperature, luminance));
+ AssertNoError(expected, new Unicolour(Configuration.Default, temperature, luminance));
}
[Test, Combinatorial]
diff --git a/Unicolour.Tests/SpectralLocusTests.cs b/Unicolour.Tests/SpectralLocusTests.cs
new file mode 100644
index 00000000..4829c965
--- /dev/null
+++ b/Unicolour.Tests/SpectralLocusTests.cs
@@ -0,0 +1,129 @@
+namespace Wacton.Unicolour.Tests;
+
+using NUnit.Framework;
+using Wacton.Unicolour.Tests.Utils;
+
+public class SpectralLocusTests
+{
+ private static readonly Observer Observer = Observer.Degree2;
+ private static readonly Chromaticity WhiteChromaticity = new(0.3, 0.3);
+ private static readonly Spectral Spectral = new(Observer, WhiteChromaticity);
+
+ // when the sample is exactly the same as the white point
+ // there is no line that connects them
+ // resulting in no possible intersects
+ [Test]
+ public void SampleIsWhitePoint()
+ {
+ var intersects = Spectral.FindBoundaryIntersects(WhiteChromaticity);
+ Assert.That(intersects, Is.Null);
+ }
+
+ // when the sample coordinate contains infinity
+ // intersect can be calculated but distance to any other coordinate is infinity
+ // resulting in infinity boundary intersect coordinates (no distinction between which is "near" vs "far")
+ [Test]
+ public void InfiniteDistanceToSampleX([Values(0.1, 0, -0.1)] double offset)
+ {
+ var intersects = Spectral.FindBoundaryIntersects(new(double.PositiveInfinity, WhiteChromaticity.Y + offset));
+ Assert.That(intersects, Is.Null);
+ }
+
+ // when the sample coordinate contains infinity
+ // intersect can be calculated but distance to any other coordinate is infinity
+ // resulting in infinity boundary intersect coordinates (no distinction between which is "near" vs "far")
+ [Test]
+ public void InfiniteDistanceToSampleY([Values(0.1, 0, -0.1)] double offset)
+ {
+ var intersects = Spectral.FindBoundaryIntersects(new(WhiteChromaticity.X + offset, double.PositiveInfinity));
+ Assert.That(intersects, Is.Null);
+ }
+
+ [TestCase(0.1, false)]
+ [TestCase(-0.1, false)]
+ [TestCase(1.1, true)]
+ [TestCase(-1.1, true)]
+ public void HorizontalIntersect(double offset, bool expectedImaginary)
+ {
+ var sample = new Chromaticity(WhiteChromaticity.X + offset, WhiteChromaticity.Y);
+ var intersects = Spectral.FindBoundaryIntersects(sample)!;
+ var isImaginary = intersects.IsImaginary();
+ Assert.That(isImaginary, Is.EqualTo(expectedImaginary));
+ }
+
+ [TestCase(0.1, false)]
+ [TestCase(-0.1, false)]
+ [TestCase(1.1, true)]
+ [TestCase(-1.1, true)]
+ public void VerticalIntersect(double offset, bool expectedImaginary)
+ {
+ var sample = new Chromaticity(WhiteChromaticity.X, WhiteChromaticity.Y + offset);
+ var intersects = Spectral.FindBoundaryIntersects(sample)!;
+ var isImaginary = intersects.IsImaginary();
+ Assert.That(isImaginary, Is.EqualTo(expectedImaginary));
+ }
+
+ [Test]
+ public void SameSample()
+ {
+ var sample1 = new Chromaticity(WhiteChromaticity.X + 0.1, WhiteChromaticity.Y + 0.1);
+ var sample2 = new Chromaticity(WhiteChromaticity.X + 0.1, WhiteChromaticity.Y + 0.1);
+ var intersects1 = Spectral.FindBoundaryIntersects(sample1)!;
+ var intersects2 = Spectral.FindBoundaryIntersects(sample2)!;
+ TestUtils.AssertEqual(intersects1.Near, intersects2.Near);
+ TestUtils.AssertEqual(intersects1.Far, intersects2.Far);
+ TestUtils.AssertEqual(intersects1.Sample, intersects2.Sample);
+ TestUtils.AssertEqual(intersects1.White, intersects2.White);
+ TestUtils.AssertEqual(intersects1, intersects2);
+ }
+
+ [Test]
+ public void DifferentSampleSameLine()
+ {
+ var sample1 = new Chromaticity(WhiteChromaticity.X + 0.1, WhiteChromaticity.Y + 0.1);
+ var sample2 = new Chromaticity(WhiteChromaticity.X + 1.1, WhiteChromaticity.Y + 1.1);
+ var intersects1 = Spectral.FindBoundaryIntersects(sample1)!;
+ var intersects2 = Spectral.FindBoundaryIntersects(sample2)!;
+
+ TestUtils.AssertEqual(intersects1.Near.Segment, intersects2.Near.Segment);
+ TestUtils.AssertEqual(intersects1.Near.Wavelength, intersects2.Near.Wavelength);
+ TestUtils.AssertEqual(intersects1.Near.DistanceToWhite, intersects2.Near.DistanceToWhite);
+ TestUtils.AssertNotEqual(intersects1.Near.DistanceToSample, intersects2.Near.DistanceToSample);
+ TestUtils.AssertEqual(intersects1.Far.Segment, intersects2.Far.Segment);
+ TestUtils.AssertEqual(intersects1.Far.Wavelength, intersects2.Far.Wavelength);
+ TestUtils.AssertEqual(intersects1.Far.DistanceToWhite, intersects2.Far.DistanceToWhite);
+ TestUtils.AssertNotEqual(intersects1.Far.DistanceToSample, intersects2.Far.DistanceToSample);
+
+ TestUtils.AssertNotEqual(intersects1.Near, intersects2.Near);
+ TestUtils.AssertNotEqual(intersects1.Far, intersects2.Far);
+ TestUtils.AssertNotEqual(intersects1.Sample, intersects2.Sample);
+ TestUtils.AssertEqual(intersects1.White, intersects2.White);
+
+ TestUtils.AssertNotEqual(intersects1, intersects2);
+ }
+
+ [Test]
+ public void DifferentSampleDifferentLine()
+ {
+ var sample1 = new Chromaticity(WhiteChromaticity.X + 0.1, WhiteChromaticity.Y + 0.1);
+ var sample2 = new Chromaticity(WhiteChromaticity.X - 0.1, WhiteChromaticity.Y + 0.1);
+ var intersects1 = Spectral.FindBoundaryIntersects(sample1)!;
+ var intersects2 = Spectral.FindBoundaryIntersects(sample2)!;
+
+ TestUtils.AssertNotEqual(intersects1.Near.Segment, intersects2.Near.Segment);
+ TestUtils.AssertNotEqual(intersects1.Near.Wavelength, intersects2.Near.Wavelength);
+ TestUtils.AssertNotEqual(intersects1.Near.DistanceToWhite, intersects2.Near.DistanceToWhite);
+ TestUtils.AssertNotEqual(intersects1.Near.DistanceToSample, intersects2.Near.DistanceToSample);
+ TestUtils.AssertNotEqual(intersects1.Far.Segment, intersects2.Far.Segment);
+ TestUtils.AssertNotEqual(intersects1.Far.Wavelength, intersects2.Far.Wavelength);
+ TestUtils.AssertNotEqual(intersects1.Far.DistanceToWhite, intersects2.Far.DistanceToWhite);
+ TestUtils.AssertNotEqual(intersects1.Far.DistanceToSample, intersects2.Far.DistanceToSample);
+
+ TestUtils.AssertNotEqual(intersects1.Near, intersects2.Near);
+ TestUtils.AssertNotEqual(intersects1.Far, intersects2.Far);
+ TestUtils.AssertNotEqual(intersects1.Sample, intersects2.Sample);
+ TestUtils.AssertEqual(intersects1.White, intersects2.White);
+
+ TestUtils.AssertNotEqual(intersects1, intersects2);
+ }
+}
\ No newline at end of file
diff --git a/Unicolour.Tests/Utils/TestUtils.cs b/Unicolour.Tests/Utils/TestUtils.cs
index d1caa30a..c13852b8 100644
--- a/Unicolour.Tests/Utils/TestUtils.cs
+++ b/Unicolour.Tests/Utils/TestUtils.cs
@@ -10,8 +10,8 @@ internal static class TestUtils
// generating planckian tables is expensive, but this is the set of tables needed for most temperature tests
internal static readonly Planckian PlanckianObserverDegree2 = new(Observer.Degree2);
- public static List AllColourSpaces => Enum.GetValues().ToList();
- public static readonly List AllColourSpacesTestCases = new()
+ internal static List AllColourSpaces => Enum.GetValues().ToList();
+ internal static readonly List AllColourSpacesTestCases = new()
{
new TestCaseData(ColourSpace.Rgb),
new TestCaseData(ColourSpace.RgbLinear),
@@ -36,7 +36,7 @@ internal static class TestUtils
new TestCaseData(ColourSpace.Hct)
};
- public static readonly List AllIlluminantsTestCases = new()
+ internal static readonly List AllIlluminantsTestCases = new()
{
new TestCaseData(Illuminant.A),
new TestCaseData(Illuminant.C),
@@ -70,11 +70,11 @@ internal static class TestUtils
{ nameof(Observer.Degree10), Observer.Degree10 }
};
- public static List ExtremeDoubles = new() { double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN };
+ internal static List ExtremeDoubles = new() { double.MinValue, double.MaxValue, double.Epsilon, double.NegativeInfinity, double.PositiveInfinity, double.NaN };
- public const double MixTolerance = 0.00000000005;
+ internal const double MixTolerance = 0.00000000005;
- public static void AssertTriplet(ColourTriplet actual, ColourTriplet expected, double tolerance, string? info = null)
+ internal static void AssertTriplet(ColourTriplet actual, ColourTriplet expected, double tolerance, string? info = null)
{
var details = $"Expected --- {expected}\nActual ----- {actual}";
string FailMessage(string channel) => $"{(info == null ? string.Empty : $"{info} · ")}{channel}\n{details}";
@@ -83,7 +83,7 @@ public static void AssertTriplet(ColourTriplet actual, ColourTriplet expected, d
AssertTripletValue(actual.Third, expected.Third, tolerance, FailMessage("Channel 3"), actual.HueIndex == 2);
}
- public static void AssertTriplet(Unicolour unicolour, ColourTriplet expected, double tolerance) where T : ColourRepresentation
+ internal static void AssertTriplet(Unicolour unicolour, ColourTriplet expected, double tolerance) where T : ColourRepresentation
{
var colourSpace = RepresentationTypeToColourSpace[typeof(T)];
var colourRepresentation = unicolour.GetRepresentation(colourSpace);
@@ -110,7 +110,7 @@ private static void AssertNormalisedForHue(double actualHue, double expectedHue,
failMessage);
}
- public static void AssertMixed(ColourTriplet triplet, double alpha, (double first, double second, double third, double alpha) expected)
+ internal static void AssertMixed(ColourTriplet triplet, double alpha, (double first, double second, double third, double alpha) expected)
{
Assert.That(triplet.First, Is.EqualTo(expected.first).Within(MixTolerance), "First");
Assert.That(triplet.Second, Is.EqualTo(expected.second).Within(MixTolerance), "Second");
@@ -118,7 +118,7 @@ public static void AssertMixed(ColourTriplet triplet, double alpha, (double firs
Assert.That(alpha, Is.EqualTo(expected.alpha).Within(MixTolerance), "Alpha");
}
- public static void AssertNoPropertyError(Unicolour unicolour)
+ internal static void AssertNoPropertyError(Unicolour unicolour)
{
Assert.DoesNotThrow(AccessProperties);
return;
@@ -131,6 +131,8 @@ void AccessProperties()
AccessProperty(() => unicolour.Chromaticity);
AccessProperty(() => unicolour.Config);
AccessProperty(() => unicolour.Description);
+ AccessProperty(() => unicolour.DominantWavelength);
+ AccessProperty(() => unicolour.ExcitationPurity);
AccessProperty(() => unicolour.Hct);
AccessProperty(() => unicolour.Hex);
AccessProperty(() => unicolour.Hpluv);
@@ -139,6 +141,7 @@ void AccessProperties()
AccessProperty(() => unicolour.Hsluv);
AccessProperty(() => unicolour.Hwb);
AccessProperty(() => unicolour.Ictcp);
+ AccessProperty(() => unicolour.IsImaginary);
AccessProperty(() => unicolour.IsInDisplayGamut);
AccessProperty(() => unicolour.Jzazbz);
AccessProperty(() => unicolour.Jzczhz);
@@ -163,7 +166,7 @@ void AccessProperty(Func