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 getProperty) } } - public static void AssertEqual(T object1, T object2) + internal static void AssertEqual(T object1, T object2) { if (object1 == null || object2 == null) { @@ -177,7 +180,7 @@ public static void AssertEqual(T object1, T object2) Assert.That(object1.ToString(), Is.EqualTo(object2.ToString())); } - public static void AssertNotEqual(T object1, T object2) + internal static void AssertNotEqual(T object1, T object2) { if (object1 == null || object2 == null) { diff --git a/Unicolour.Tests/UtilsTests.cs b/Unicolour.Tests/UtilsTests.cs index 2111c4f6..8f07836a 100644 --- a/Unicolour.Tests/UtilsTests.cs +++ b/Unicolour.Tests/UtilsTests.cs @@ -5,6 +5,15 @@ namespace Wacton.Unicolour.Tests; public class UtilsTests { + [TestCase(0.0, true)] + [TestCase(0.00000000000004, true)] + [TestCase(0.00000000000005, false)] + [TestCase(0.00000000000006, false)] + [TestCase(-0.00000000000004, true)] + [TestCase(-0.00000000000005, false)] + [TestCase(-0.00000000000006, false)] + public void EffectivelyZero(double value, bool expected) => Assert.That(value.IsEffectivelyZero(), Is.EqualTo(expected)); + [TestCase(0.5)] [TestCase(0.0)] [TestCase(1.0)] diff --git a/Unicolour.sln b/Unicolour.sln index 581408f3..6ece3f03 100644 --- a/Unicolour.sln +++ b/Unicolour.sln @@ -4,14 +4,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unicolour", "Unicolour\Unic EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unicolour.Tests", "Unicolour.Tests\Unicolour.Tests.csproj", "{43CC17D1-2A95-4554-9900-D97FA9CEA2D0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unicolour.Example", "Unicolour.Example\Unicolour.Example.csproj", "{DA851ED7-FCE5-4A17-818A-ED80BE673EBB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Gradients", "Example.Gradients\Example.Gradients.csproj", "{DA851ED7-FCE5-4A17-818A-ED80BE673EBB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unicolour.Datasets", "Unicolour.Datasets\Unicolour.Datasets.csproj", "{4CE26106-3B57-4C0F-9AB4-0C7A3C34F91F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unicolour.Console", "Unicolour.Console\Unicolour.Console.csproj", "{D59F060C-178E-4FFE-8908-5C39F7D7DFDF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Console", "Example.Console\Example.Console.csproj", "{D59F060C-178E-4FFE-8908-5C39F7D7DFDF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unicolour.Readme", "Unicolour.Readme\Unicolour.Readme.csproj", "{2E6EE4C6-4E37-469B-B6DA-0C016EC79ABB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Diagrams", "Example.Diagrams\Example.Diagrams.csproj", "{D3002594-7C64-48CE-BD72-16AB4B0B8C61}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,5 +44,9 @@ Global {2E6EE4C6-4E37-469B-B6DA-0C016EC79ABB}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E6EE4C6-4E37-469B-B6DA-0C016EC79ABB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E6EE4C6-4E37-469B-B6DA-0C016EC79ABB}.Release|Any CPU.Build.0 = Release|Any CPU + {D3002594-7C64-48CE-BD72-16AB4B0B8C61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3002594-7C64-48CE-BD72-16AB4B0B8C61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3002594-7C64-48CE-BD72-16AB4B0B8C61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3002594-7C64-48CE-BD72-16AB4B0B8C61}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Unicolour/BoundingLines.cs b/Unicolour/BoundingLines.cs deleted file mode 100644 index eaafabba..00000000 --- a/Unicolour/BoundingLines.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace Wacton.Unicolour; - -internal static class BoundingLines -{ - internal static double CalculateMaxChroma(double lightness, double hue) - { - var hueRad = hue / 360 * Math.PI * 2; - return GetBoundingLines(lightness).Select(x => DistanceFromOriginAngle(hueRad, x)).Min(); - } - - internal static double CalculateMaxChroma(double lightness) - { - return GetBoundingLines(lightness).Select(DistanceFromOrigin).Min(); - } - - // https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L249 - private static IEnumerable GetBoundingLines(double l) - { - const double kappa = 903.2962962; - const double epsilon = 0.0088564516; - var matrixR = Matrix.FromTriplet(3.240969941904521, -1.537383177570093, -0.498610760293); - var matrixG = Matrix.FromTriplet(-0.96924363628087, 1.87596750150772, 0.041555057407175); - var matrixB = Matrix.FromTriplet(0.055630079696993, -0.20397695888897, 1.056971514242878); - - var sub1 = Math.Pow(l + 16, 3) / 1560896; - var sub2 = sub1 > epsilon ? sub1 : l / kappa; - - IEnumerable CalculateLines(Matrix matrix) - { - var s1 = sub2 * (284517 * matrix[0, 0] - 94839 * matrix[2, 0]); - var s2 = sub2 * (838422 * matrix[2, 0] + 769860 * matrix[1, 0] + 731718 * matrix[0, 0]); - var s3 = sub2 * (632260 * matrix[2, 0] - 126452 * matrix[1, 0]); - - var slope0 = s1 / s3; - var intercept0 = s2 * l / s3; - var slope1 = s1 / (s3 + 126452); - var intercept1 = (s2 - 769860) * l / (s3 + 126452); - return new[] {new Line(slope0, intercept0), new Line(slope1, intercept1)}; - } - - var lines = new List(); - lines.AddRange(CalculateLines(matrixR)); - lines.AddRange(CalculateLines(matrixG)); - lines.AddRange(CalculateLines(matrixB)); - return lines; - } - - private static double DistanceFromOriginAngle(double theta, Line line) - { - var distance = line.Intercept / (Math.Sin(theta) - line.Slope * Math.Cos(theta)); - return distance < 0 ? double.PositiveInfinity : distance; - } - - private static double DistanceFromOrigin(Line line) => Math.Abs(line.Intercept) / Math.Sqrt(Math.Pow(line.Slope, 2) + 1); - - private record Line(double Slope, double Intercept) - { - public double Slope { get; } = Slope; - public double Intercept { get; } = Intercept; - } -} \ No newline at end of file diff --git a/Unicolour/CamConfiguration.cs b/Unicolour/CamConfiguration.cs index 12b30a23..b39f9d77 100644 --- a/Unicolour/CamConfiguration.cs +++ b/Unicolour/CamConfiguration.cs @@ -5,10 +5,27 @@ // therefore D is always calculated public class CamConfiguration { + /* + * hard to find guidance for default CAM settings; this is based on data in "Usage guidelines for CIECAM97s" (Moroney, 2000) + * - sRGB standard ambient illumination level of 64 lux ~= 4 + * - La = E * R / PI / 5 where E = lux & R = 1 --> 64 / PI / 5 + * ---------- + * I don't know why Google's HCT luminance calculations don't match the above + * they suggest 200 lux -> ~11.72 luminance, but the formula above gives ~12.73 luminance + * and they appear to ignore the division by 5 and incorporate XYZ luminance (Y) + */ + public static readonly CamConfiguration StandardRgb = new(Illuminant.D65.GetWhitePoint(Observer.Degree2), LuxToLuminance(64), 20, Surround.Average, "sRGB"); + public static readonly CamConfiguration Hct = new(Illuminant.D65.GetWhitePoint(Observer.Degree2), LuxToLuminance(200) * 5 * DefaultHctY(), DefaultHctY() * 100, Surround.Average, "HCT"); + internal static double LuxToLuminance(double lux) => lux / Math.PI / 5.0; + + private const double DefaultHctLightness = 50; // just for HCT, use specific XYZ configuration + private static double DefaultHctY() => Lab.ToXyz(new Lab(DefaultHctLightness, 0, 0), XyzConfiguration.D65).Y; + public WhitePoint WhitePoint { get; } public double AdaptingLuminance { get; } // [L_A] Luminance of adapting field (brightness of the room where the colour is being viewed) public double BackgroundLuminance { get; } // [Y_b] Luminance of background (brightness of the area surrounding the colour) public Surround Surround { get; } // 0 = dark (movie theatre), 1 = dim (bright TV in dim room), 2 = average (surface colours) + public string Name { get; } internal double F => Surround switch { @@ -34,32 +51,18 @@ public class CamConfiguration _ => throw new ArgumentOutOfRangeException() }; - /* - * hard to find guidance for default CAM settings; this is based on data in "Usage guidelines for CIECAM97s" (Moroney, 2000) - * - sRGB standard ambient illumination level of 64 lux ~= 4 - * - La = E * R / PI / 5 where E = lux & R = 1 --> 64 / PI / 5 - * ---------- - * I don't know why Google's HCT luminance calculations don't match the above - * they suggest 200 lux -> ~11.72 luminance, but the formula above gives ~12.73 luminance - * and they appear to ignore the division by 5 and incorporate XYZ luminance (Y) - */ - public static readonly CamConfiguration StandardRgb = new(Illuminant.D65.GetWhitePoint(Observer.Degree2), LuxToLuminance(64), 20, Surround.Average); - public static readonly CamConfiguration Hct = new(Illuminant.D65.GetWhitePoint(Observer.Degree2), LuxToLuminance(200) * 5 * DefaultHctY(), DefaultHctY() * 100, Surround.Average); - internal static double LuxToLuminance(double lux) => lux / Math.PI / 5.0; - - // just for HCT, use specific XYZ configuration - private const double DefaultHctLightness = 50; - private static double DefaultHctY() => Lab.ToXyz(new Lab(DefaultHctLightness, 0, 0), XyzConfiguration.D65).Y; - - public CamConfiguration(WhitePoint whitePoint, double adaptingLuminance, double backgroundLuminance, Surround surround) + public CamConfiguration(WhitePoint whitePoint, + double adaptingLuminance, double backgroundLuminance, + Surround surround, string name = Utils.Unnamed) { WhitePoint = whitePoint; AdaptingLuminance = adaptingLuminance; BackgroundLuminance = backgroundLuminance; Surround = surround; + Name = name; } - - public override string ToString() => $"CAM {AdaptingLuminance:f0} {BackgroundLuminance:f0} {Surround}"; + + public override string ToString() => $"{Name} · {AdaptingLuminance:f0}, {BackgroundLuminance:f0}, {Surround}"; } public enum Surround diff --git a/Unicolour/Configuration.cs b/Unicolour/Configuration.cs index ccb3cee8..3468c951 100644 --- a/Unicolour/Configuration.cs +++ b/Unicolour/Configuration.cs @@ -26,5 +26,5 @@ public Configuration( JzazbzScalar = jzazbzScalar; } - public override string ToString() => $"{Id}"; + public override string ToString() => $"RGB:[{Rgb.Name}] · XYZ:[{Xyz.Name}] · CAM:[{Cam.Name}] · Id:[{Id}]"; } \ No newline at end of file diff --git a/Unicolour/Hpluv.cs b/Unicolour/Hpluv.cs index a69baea4..50601b81 100644 --- a/Unicolour/Hpluv.cs +++ b/Unicolour/Hpluv.cs @@ -46,7 +46,7 @@ internal static Hpluv FromLchuv(Lchuv lchuv) break; default: { - var maxChroma = BoundingLines.CalculateMaxChroma(lchLightness); + var maxChroma = CalculateMaxChroma(lchLightness); saturation = chroma / maxChroma * 100; lightness = lchLightness; break; @@ -76,7 +76,7 @@ internal static Lchuv ToLchuv(Hpluv hpluv) break; default: { - var maxChroma = BoundingLines.CalculateMaxChroma(hslLightness); + var maxChroma = CalculateMaxChroma(hslLightness); chroma = maxChroma / 100 * saturation; lightness = hslLightness; break; @@ -85,4 +85,11 @@ internal static Lchuv ToLchuv(Hpluv hpluv) return new Lchuv(lightness, chroma, hue, ColourHeritage.From(hpluv)); } + + private static double CalculateMaxChroma(double lightness) + { + return Hsluv.GetBoundingLines(lightness).Select(DistanceFromOrigin).Min(); + } + + private static double DistanceFromOrigin(Line line) => Math.Abs(line.Intercept) / Math.Sqrt(Math.Pow(line.Slope, 2) + 1); } \ No newline at end of file diff --git a/Unicolour/Hsb.cs b/Unicolour/Hsb.cs index 8c9f1e0e..6a5e4043 100644 --- a/Unicolour/Hsb.cs +++ b/Unicolour/Hsb.cs @@ -31,7 +31,7 @@ internal Hsb(double h, double s, double b, ColourHeritage heritage) : base(h, s, internal static Hsb FromRgb(Rgb rgb) { var (r, g, b) = rgb.ConstrainedTriplet; - var components = new[] {r, g, b}; + var components = new[] { r, g, b }; var xMax = components.Max(); var xMin = components.Min(); var chroma = xMax - xMin; diff --git a/Unicolour/Hsluv.cs b/Unicolour/Hsluv.cs index 3c019812..22f14a9c 100644 --- a/Unicolour/Hsluv.cs +++ b/Unicolour/Hsluv.cs @@ -46,7 +46,7 @@ internal static Hsluv FromLchuv(Lchuv lchuv) break; default: { - var maxChroma = BoundingLines.CalculateMaxChroma(lchLightness, hue); + var maxChroma = CalculateMaxChroma(lchLightness, hue); saturation = chroma / maxChroma * 100; lightness = lchLightness; break; @@ -76,7 +76,7 @@ internal static Lchuv ToLchuv(Hsluv hsluv) break; default: { - var maxChroma = BoundingLines.CalculateMaxChroma(hslLightness, hue); + var maxChroma = CalculateMaxChroma(hslLightness, hue); chroma = maxChroma / 100 * saturation; lightness = hslLightness; break; @@ -85,4 +85,48 @@ internal static Lchuv ToLchuv(Hsluv hsluv) return new Lchuv(lightness, chroma, hue, ColourHeritage.From(hsluv)); } + + private static double CalculateMaxChroma(double lightness, double hue) + { + var hueRad = hue / 360 * Math.PI * 2; + return GetBoundingLines(lightness).Select(x => DistanceFromOriginAngle(hueRad, x)).Min(); + } + + private static double DistanceFromOriginAngle(double theta, Line line) + { + var distance = line.Intercept / (Math.Sin(theta) - line.Slope * Math.Cos(theta)); + return distance < 0 ? double.PositiveInfinity : distance; + } + + // https://github.com/hsluv/hsluv-haxe/blob/master/src/hsluv/Hsluv.hx#L249 + internal static IEnumerable GetBoundingLines(double l) + { + const double kappa = 903.2962962; + const double epsilon = 0.0088564516; + var matrixR = Matrix.FromTriplet(3.240969941904521, -1.537383177570093, -0.498610760293); + var matrixG = Matrix.FromTriplet(-0.96924363628087, 1.87596750150772, 0.041555057407175); + var matrixB = Matrix.FromTriplet(0.055630079696993, -0.20397695888897, 1.056971514242878); + + var sub1 = Math.Pow(l + 16, 3) / 1560896; + var sub2 = sub1 > epsilon ? sub1 : l / kappa; + + IEnumerable CalculateLines(Matrix matrix) + { + var s1 = sub2 * (284517 * matrix[0, 0] - 94839 * matrix[2, 0]); + var s2 = sub2 * (838422 * matrix[2, 0] + 769860 * matrix[1, 0] + 731718 * matrix[0, 0]); + var s3 = sub2 * (632260 * matrix[2, 0] - 126452 * matrix[1, 0]); + + var slope0 = s1 / s3; + var intercept0 = s2 * l / s3; + var slope1 = s1 / (s3 + 126452); + var intercept1 = (s2 - 769860) * l / (s3 + 126452); + return new[] { new Line(slope0, intercept0), new Line(slope1, intercept1) }; + } + + var lines = new List(); + lines.AddRange(CalculateLines(matrixR)); + lines.AddRange(CalculateLines(matrixG)); + lines.AddRange(CalculateLines(matrixB)); + return lines; + } } \ No newline at end of file diff --git a/Unicolour/Illuminant.cs b/Unicolour/Illuminant.cs index bd346478..b1caf189 100644 --- a/Unicolour/Illuminant.cs +++ b/Unicolour/Illuminant.cs @@ -2,16 +2,16 @@ namespace Wacton.Unicolour; public class Illuminant { - public static readonly Illuminant A = new(Spd.A, nameof(A)); - public static readonly Illuminant C = new(Spd.C, nameof(C)); - public static readonly Illuminant D50 = new(Spd.D50, nameof(D50)); - public static readonly Illuminant D55 = new(Spd.D55, nameof(D55)); - public static readonly Illuminant D65 = new(Spd.D65, nameof(D65)); - public static readonly Illuminant D75 = new(Spd.D75, nameof(D75)); - public static readonly Illuminant E = new(Spd.E, nameof(E)); - public static readonly Illuminant F2 = new(Spd.F2, nameof(F2)); - public static readonly Illuminant F7 = new(Spd.F7, nameof(F7)); - public static readonly Illuminant F11 = new(Spd.F11, nameof(F11)); + public static readonly Illuminant A = new(Spd.A, $"Illuminant {nameof(A)}"); + public static readonly Illuminant C = new(Spd.C, $"Illuminant {nameof(C)}"); + public static readonly Illuminant D50 = new(Spd.D50, $"Illuminant {nameof(D50)}"); + public static readonly Illuminant D55 = new(Spd.D55, $"Illuminant {nameof(D55)}"); + public static readonly Illuminant D65 = new(Spd.D65, $"Illuminant {nameof(D65)}"); + public static readonly Illuminant D75 = new(Spd.D75, $"Illuminant {nameof(D75)}"); + public static readonly Illuminant E = new(Spd.E, $"Illuminant {nameof(E)}"); + public static readonly Illuminant F2 = new(Spd.F2, $"Illuminant {nameof(F2)}"); + public static readonly Illuminant F7 = new(Spd.F7, $"Illuminant {nameof(F7)}"); + public static readonly Illuminant F11 = new(Spd.F11, $"Illuminant {nameof(F11)}"); // as far as I'm aware, these are the latest ASTM standards // and the 2 degree observers are an exact match with calculations on the calculator at http://www.brucelindbloom.com/ @@ -43,7 +43,7 @@ public class Illuminant public string Name { get; } private readonly Spd spd = new(); - public Illuminant(Spd spd, string name = "(unnamed)") + public Illuminant(Spd spd, string name = Utils.Unnamed) { this.spd = spd; Name = name; @@ -51,7 +51,7 @@ public Illuminant(Spd spd, string name = "(unnamed)") // allows white point to be defined explicitly, not relative to an observer private readonly WhitePoint? whitePoint; - public Illuminant(WhitePoint whitePoint, string name = "(unnamed)") + public Illuminant(WhitePoint whitePoint, string name = Utils.Unnamed) { this.whitePoint = whitePoint; Name = name; @@ -74,5 +74,5 @@ public WhitePoint GetWhitePoint(Observer observer) return WhitePoint.FromXyz(xyz); } - public override string ToString() => $"Illuminant {Name}"; + public override string ToString() => Name; } \ No newline at end of file diff --git a/Unicolour/Line.cs b/Unicolour/Line.cs new file mode 100644 index 00000000..4313aa26 --- /dev/null +++ b/Unicolour/Line.cs @@ -0,0 +1,51 @@ +namespace Wacton.Unicolour; + +internal record Line(double Slope, double Intercept) +{ + internal double Slope { get; } = Slope; + internal double Intercept { get; } = Intercept; + + internal static Line FromPoints((double x, double y) point1, (double x, double y) point2) + { + var (x1, y1) = point1; + var (x2, y2) = point2; + var sameX = x1 == x2; + var sameY = y1 == y2; + + if (sameX && sameY) return new Line(double.NaN, double.NaN); // no line between points in the same location + if (sameX) return new Line(double.PositiveInfinity, x1); // vertical line between points + if (sameY) return new Line(0, y1); // horizontal line between points + + var slope = (y2 - y1) / (x2 - x1); + var intercept = y1 - slope * x1; + return new Line(slope, intercept); + } + + internal bool IsVertical => double.IsInfinity(Slope); + + internal double GetY(double x) => Slope * x + Intercept; + + internal (double x, double y) GetIntersect(Line other) + { + double x, y; + if (IsVertical) + { + x = Intercept; + y = other.GetY(x); + } + else if (other.IsVertical) + { + x = other.Intercept; + y = GetY(x); + } + else + { + x = (other.Intercept - Intercept) / (Slope - other.Slope); + y = GetY(x); + } + + return new(x, y); + } + + public override string ToString() => $"y = {Slope}x + {Intercept}"; +} \ No newline at end of file diff --git a/Unicolour/Observer.cs b/Unicolour/Observer.cs index 6744c4d8..ae26b374 100644 --- a/Unicolour/Observer.cs +++ b/Unicolour/Observer.cs @@ -2,13 +2,13 @@ namespace Wacton.Unicolour; public class Observer { - public static readonly Observer Degree2 = new(Cmf.Degree2, "1931 2\u00b0"); - public static readonly Observer Degree10 = new(Cmf.Degree10, "1964 10\u00b0"); + public static readonly Observer Degree2 = new(Cmf.Degree2, "1931 2\u00b0 Observer"); + public static readonly Observer Degree10 = new(Cmf.Degree10, "1964 10\u00b0 Observer"); public string Name { get; } private readonly Cmf cmf; - public Observer(Cmf cmf, string name = "(unnamed)") + public Observer(Cmf cmf, string name = Utils.Unnamed) { this.cmf = cmf; Name = name; @@ -18,5 +18,5 @@ public Observer(Cmf cmf, string name = "(unnamed)") internal double ColourMatchY(int wavelength) => cmf[wavelength].y; internal double ColourMatchZ(int wavelength) => cmf[wavelength].z; - public override string ToString() => $"Observer {Name}"; + public override string ToString() => Name; } \ No newline at end of file diff --git a/Unicolour/RgbConfiguration.cs b/Unicolour/RgbConfiguration.cs index c71e0f0e..2d42efc1 100644 --- a/Unicolour/RgbConfiguration.cs +++ b/Unicolour/RgbConfiguration.cs @@ -14,14 +14,16 @@ public class RgbConfiguration public WhitePoint WhitePoint { get; } public Func CompandFromLinear { get; } public Func InverseCompandToLinear { get; } - + public string Name { get; } + public RgbConfiguration( Chromaticity chromaticityR, Chromaticity chromaticityG, Chromaticity chromaticityB, WhitePoint whitePoint, Func fromLinear, - Func toLinear) + Func toLinear, + string name = Utils.Unnamed) { ChromaticityR = chromaticityR; ChromaticityG = chromaticityG; @@ -29,7 +31,8 @@ public RgbConfiguration( WhitePoint = whitePoint; CompandFromLinear = fromLinear; InverseCompandToLinear = toLinear; + Name = name; } - public override string ToString() => $"RGB {WhitePoint} {ChromaticityR} {ChromaticityG} {ChromaticityB}"; + public override string ToString() => Name; } \ No newline at end of file diff --git a/Unicolour/RgbModels.cs b/Unicolour/RgbModels.cs index 0a5f9b96..bf221684 100644 --- a/Unicolour/RgbModels.cs +++ b/Unicolour/RgbModels.cs @@ -25,7 +25,7 @@ public static double ToLinear(double nonlinear) : Companding.InverseGamma((value + 0.055) / 1.055, 2.4)); } - public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear); + public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear, "sRGB"); } public static class DisplayP3 @@ -38,7 +38,7 @@ public static class DisplayP3 public static double FromLinear(double value) => StandardRgb.FromLinear(value); public static double ToLinear(double value) => StandardRgb.ToLinear(value); - public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear); + public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear, "Display P3"); } public static class Rec2020 @@ -69,7 +69,7 @@ public static double ToLinear(double nonlinear) }); } - public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear); + public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear, "Rec. 2020"); } public static class A98 @@ -89,7 +89,7 @@ public static double ToLinear(double nonlinear) return Companding.ReflectWhenNegative(nonlinear, value => Companding.InverseGamma(value, 563 / 256.0)); } - public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear); + public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear, "A98 RGB"); } public static class ProPhoto @@ -117,6 +117,6 @@ public static double ToLinear(double nonlinear) : Companding.InverseGamma(value, 1.8)); } - public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear); + public static RgbConfiguration RgbConfiguration => new(R, G, B, WhitePoint, FromLinear, ToLinear, "ProPhoto RGB"); } } \ No newline at end of file diff --git a/Unicolour/Spectral.cs b/Unicolour/Spectral.cs new file mode 100644 index 00000000..5bd05b2e --- /dev/null +++ b/Unicolour/Spectral.cs @@ -0,0 +1,180 @@ +namespace Wacton.Unicolour; + +internal class Spectral +{ + private readonly Observer observer; + private readonly Chromaticity white; + private readonly Lazy> boundarySegments; + + internal Spectral(Observer observer, Chromaticity white) + { + this.observer = observer; + this.white = white; + boundarySegments = new Lazy>(GetBoundarySegments); + } + + internal Intersects? FindBoundaryIntersects(Chromaticity sample) + { + if (sample == white) return null; + var whiteToSampleLine = Line.FromPoints(white.Xy, sample.Xy); + + var intersects = boundarySegments.Value + .Select(segment => GetIntersect(segment, whiteToSampleLine, sample)) + .Where(intersect => intersect.IsOnSegment && !double.IsInfinity(intersect.DistanceToSample)) + .OrderByDescending(intersect => intersect.Segment.StartWavelength) + .ToList(); + + if (!intersects.Any()) return null; + + var minDistanceToSample = intersects.Min(x => x.DistanceToSample); + var nearIntersects = intersects.Where(x => Utils.IsEffectivelyZero(x.DistanceToSample - minDistanceToSample)).ToList(); + var farIntersects = intersects.Except(nearIntersects).ToList(); + + // filter line of purple if there are other candidates that are just as close + FilterLineOfPurple(nearIntersects); + FilterLineOfPurple(farIntersects); + void FilterLineOfPurple(List closestIntersects) + { + if (closestIntersects.Count > 1) + { + closestIntersects.RemoveAll(x => x.Segment.IsLineOfPurples); + } + } + + var near = nearIntersects.First(); + var far = farIntersects.First(); + return new Intersects(near, far, sample, white); + } + + private List GetBoundarySegments() + { + var result = new List(); + for (var startNm = 360; startNm < 700; startNm++) + { + result.Add(GetSegment(startNm, startNm + 1)); + } + + result.Add(GetSegment(700, 360)); + return result; + } + + private Segment GetSegment(int startWavelength, int endWavelength) + { + var startChromaticity = GetChromaticity(startWavelength); + var endChromaticity = GetChromaticity(endWavelength); + var line = Line.FromPoints(startChromaticity.Xy, endChromaticity.Xy); + return new Segment(startWavelength, endWavelength, startChromaticity, endChromaticity, line); + } + + private readonly Dictionary wavelengthToChromaticity = new(); + private Chromaticity GetChromaticity(int wavelength) + { + if (wavelengthToChromaticity.ContainsKey(wavelength)) + { + return wavelengthToChromaticity[wavelength]; + } + + var xyz = Xyz.FromSpd(new Spd { { wavelength, 1.0 } }, observer); + var xyy = Xyy.FromXyz(xyz, white); + var chromaticity = xyy.Chromaticity; + wavelengthToChromaticity.Add(wavelength, chromaticity); + return chromaticity; + } + + private Intersect GetIntersect(Segment segment, Line whiteToSampleLine, Chromaticity sample) + { + var (x, y) = whiteToSampleLine.GetIntersect(segment.Line); + var intersectChromaticity = new Chromaticity(x, y); + var distanceToSample = Distance(sample, intersectChromaticity); + var distanceToWhite = Distance(white, intersectChromaticity); + return new Intersect(segment, intersectChromaticity, distanceToSample, distanceToWhite); + } + + private static double Distance(Chromaticity chromaticity1, Chromaticity chromaticity2) + { + return Math.Sqrt(Math.Pow(chromaticity2.X - chromaticity1.X, 2) + Math.Pow(chromaticity2.Y - chromaticity1.Y, 2)); + } + + internal record Intersects(Intersect Near, Intersect Far, Chromaticity Sample, Chromaticity White) + { + internal Intersect Near { get; } = Near; + internal Intersect Far { get; } = Far; + internal Chromaticity Sample { get; } = Sample; + internal Chromaticity White { get; } = White; + + internal double DominantWavelength() + { + var useNegativeComplement = Near.Segment.IsLineOfPurples; + return useNegativeComplement ? -Far.Wavelength : Near.Wavelength; + } + + internal double ExcitationPurity() + { + var sampleToWhiteDistance = Distance(Sample, White); + return sampleToWhiteDistance / Near.DistanceToWhite; + } + + internal bool IsImaginary() + { + (double x, double y) nearDelta = (Sample.X - Near.Chromaticity.X, Sample.Y - Near.Chromaticity.Y); + (double x, double y) farDelta = (Sample.X - Far.Chromaticity.X, Sample.Y - Far.Chromaticity.Y); + + // if sample is effectively the same chromaticity as the intersect point + // it is on the boundary and should be treated as monochromatic light + var isMonochromatic = Math.Abs(nearDelta.x).IsEffectivelyZero() && Math.Abs(nearDelta.y).IsEffectivelyZero(); + if (isMonochromatic) return false; + + (int near, int far) signX = (Math.Sign(nearDelta.x), Math.Sign(farDelta.x)); + (int near, int far) signY = (Math.Sign(nearDelta.y), Math.Sign(farDelta.y)); + + // both intersects are in the same direction; outside of spectral locus + bool IsSameDirection((int near, int far) signs) + { + if (signs is { near: 0, far: 0 }) + { + return false; + } + + return signs.near == signs.far; + } + + var isImaginary = IsSameDirection(signX) || IsSameDirection(signY); + return isImaginary; + } + + public override string ToString() => $"{Near} & {Far}"; + } + + internal record Segment(int StartWavelength, int EndWavelength, Chromaticity StartChromaticity, Chromaticity EndChromaticity, Line Line) + { + internal int StartWavelength { get; } = StartWavelength; + internal int EndWavelength { get; } = EndWavelength; + internal bool IsLineOfPurples => EndWavelength < StartWavelength; + internal Chromaticity StartChromaticity { get; } = StartChromaticity; + internal Chromaticity EndChromaticity { get; } = EndChromaticity; + internal Line Line { get; } = Line; + internal double Length => Distance(StartChromaticity, EndChromaticity); + + public override string ToString() => $"{StartWavelength} \u27f6 {EndWavelength}"; + } + + internal record Intersect(Segment Segment, Chromaticity Chromaticity, double DistanceToSample, double DistanceToWhite) + { + internal Segment Segment { get; } = Segment; + internal Chromaticity Chromaticity { get; } = Chromaticity; + internal double DistanceToSample { get; } = DistanceToSample; + internal double DistanceToWhite { get; } = DistanceToWhite; + + internal double DistanceFromStart => Distance(Chromaticity, Segment.StartChromaticity); + internal double DistanceFromEnd => Distance(Chromaticity, Segment.EndChromaticity); + internal double SegmentLengthViaIntersect => DistanceFromStart + DistanceFromEnd; + internal double SegmentLengthDifference => Math.Abs(SegmentLengthViaIntersect - Segment.Length); // the closer to zero, the closer it lies on the segment + internal bool IsOnSegment => SegmentLengthDifference.IsEffectivelyZero(); + internal double SegmentInterpolationAmount => DistanceFromStart / SegmentLengthViaIntersect; + internal double Wavelength => Interpolation.Interpolate(Segment.StartWavelength, Segment.EndWavelength, SegmentInterpolationAmount); + + public override string ToString() => $"{Segment} · {Wavelength:F2} nm · {DistanceToSample:F4} from sample"; + } + + public override string ToString() => $"Spectral locus for {observer}"; +} \ No newline at end of file diff --git a/Unicolour/Temperature.cs b/Unicolour/Temperature.cs index 5dfe381c..58f1b1cc 100644 --- a/Unicolour/Temperature.cs +++ b/Unicolour/Temperature.cs @@ -2,7 +2,7 @@ namespace Wacton.Unicolour; using static Planckian; -public record Temperature(double Cct, double Duv) +public record Temperature(double Cct, double Duv = 0.0) { public double Cct { get; } = Cct; public double Duv { get; } = Duv; @@ -23,7 +23,7 @@ internal static Temperature FromCct(double cct, Locus locus, Planckian planckian { return locus switch { - Locus.Blackbody => new Temperature(cct, 0), + Locus.Blackbody => new Temperature(cct, Duv: 0), Locus.Daylight => FromChromaticity(Daylight.GetChromaticity(cct), planckian), _ => throw new ArgumentOutOfRangeException(nameof(locus), locus, null) }; diff --git a/Unicolour/Unicolour.Constructors.cs b/Unicolour/Unicolour.Constructors.cs index d2764766..1c2a3acd 100644 --- a/Unicolour/Unicolour.Constructors.cs +++ b/Unicolour/Unicolour.Constructors.cs @@ -53,29 +53,35 @@ public Unicolour(Configuration config, string hex, double alphaOverride) : this(config, ColourSpace.Rgb, Parse(hex) with { a = alphaOverride }) { } - - /* construction from temperature */ - public Unicolour(double cct, double duv, double luminance = 1.0) : - this(Configuration.Default, cct, duv, luminance) + + /* construction from chromaticity */ + public Unicolour(Chromaticity chromaticity, double luminance = 1.0) : + this(Configuration.Default, chromaticity, luminance) { } + public Unicolour(Configuration config, Chromaticity chromaticity, double luminance = 1.0) : + this(config, ColourSpace.Xyy, chromaticity.X, chromaticity.Y, luminance) + { + } + + /* construction from temperature */ public Unicolour(double cct, Locus locus = Locus.Blackbody, double luminance = 1.0) : this(Configuration.Default, cct, locus, luminance) { } - public Unicolour(Configuration config, double cct, double duv, double luminance = 1.0) : - this(config, new Temperature(cct, duv), luminance) + public Unicolour(Configuration config, double cct, Locus locus = Locus.Blackbody, double luminance = 1.0) : + this(config, Temperature.FromCct(cct, locus, config.Xyz.Planckian), luminance) { } - public Unicolour(Configuration config, double cct, Locus locus = Locus.Blackbody, double luminance = 1.0) : - this(config, Temperature.FromCct(cct, locus, config.Xyz.Planckian), luminance) + public Unicolour(Temperature temperature, double luminance = 1.0) : + this(Configuration.Default, temperature, luminance) { } - internal Unicolour(Configuration config, Temperature temperature, double luminance) : + public Unicolour(Configuration config, Temperature temperature, double luminance = 1.0) : this(config, ColourSpace.Xyy, TemperatureToXyyTuple(temperature, config.Xyz.Observer, luminance)) { this.temperature = new Lazy(() => temperature); diff --git a/Unicolour/Unicolour.Lookups.cs b/Unicolour/Unicolour.Lookups.cs index aa54f7dc..8a530e79 100644 --- a/Unicolour/Unicolour.Lookups.cs +++ b/Unicolour/Unicolour.Lookups.cs @@ -169,7 +169,7 @@ private Xyy EvaluateXyy() return InitialColourSpace switch { ColourSpace.Xyy => (Xyy)InitialRepresentation, - _ => Xyy.FromXyz(Xyz, Config.Xyz) + _ => Xyy.FromXyz(Xyz, Config.Xyz.WhiteChromaticity) }; } diff --git a/Unicolour/Unicolour.cs b/Unicolour/Unicolour.cs index c314628c..a6068791 100644 --- a/Unicolour/Unicolour.cs +++ b/Unicolour/Unicolour.cs @@ -24,6 +24,7 @@ public partial class Unicolour : IEquatable private readonly Lazy cam16 = null!; private readonly Lazy hct = null!; private readonly Lazy temperature = null!; + private readonly Lazy spectralIntersects = null!; internal readonly ColourRepresentation InitialRepresentation; internal readonly ColourSpace InitialColourSpace; @@ -57,8 +58,11 @@ public partial class Unicolour : IEquatable public bool IsInDisplayGamut => Rgb.IsInGamut; public double RelativeLuminance => RgbLinear.RelativeLuminance; public string Description => isUnseen ? UnseenDescription : string.Join(" ", ColourDescription.Get(Hsl)); + public double DominantWavelength => spectralIntersects.Value?.DominantWavelength() ?? double.NaN; + public double ExcitationPurity => spectralIntersects.Value?.ExcitationPurity() ?? double.NaN; + public bool IsImaginary => spectralIntersects.Value?.IsImaginary() ?? !Xyy.UseAsGreyscale; public Temperature Temperature => temperature.Value; - + internal Unicolour(Configuration config, ColourHeritage heritage, ColourSpace colourSpace, double first, double second, double third, double alpha = 1.0) { @@ -97,6 +101,11 @@ internal Unicolour(Configuration config, ColourHeritage heritage, cam16 = new Lazy(EvaluateCam16); hct = new Lazy(EvaluateHct); + spectralIntersects = new Lazy(() => + Xyy.UseAsNaN || Xyy.UseAsGreyscale + ? null + : Config.Xyz.Spectral.FindBoundaryIntersects(Chromaticity)); + // this will get overridden when called by the derived constructor that takes temperature as a parameter temperature = new Lazy(() => Temperature.FromChromaticity(Chromaticity, Config.Xyz.Planckian)); } diff --git a/Unicolour/Unicolour.csproj b/Unicolour/Unicolour.csproj index 79a027b2..036c2ffc 100644 --- a/Unicolour/Unicolour.csproj +++ b/Unicolour/Unicolour.csproj @@ -15,9 +15,9 @@ netstandard2.0 True Resources\Unicolour.png - 4.0.0 - colour color RGB HSB HSV HSL HWB XYZ xyY LAB LUV LCH LCHab LCHuv HSLuv HPLuv ICtCp JzAzBz JzCzHz Oklab Oklch CAM02 CAM16 HCT converter colour-converter colour-conversion color-converter color-conversion colour-space colour-spaces color-space color-spaces interpolation colour-interpolation color-interpolation colour-mixing color-mixing comparison colour-comparison color-comparison contrast luminance deltaE chromaticity display-p3 rec-2020 gamut-mapping temperature cct duv cvd colour-vision-deficiency color-vision-deficiency colour-blindness color-blindness protanopia deuteranopia tritanopia achromatopsia spd - Create colour from temperature or SPD, and add hue interpolation options + 4.1.0 + colour color RGB HSB HSV HSL HWB XYZ xyY LAB LUV LCH LCHab LCHuv HSLuv HPLuv ICtCp JzAzBz JzCzHz Oklab Oklch CAM02 CAM16 HCT converter colour-converter colour-conversion color-converter color-conversion colour-space colour-spaces color-space color-spaces interpolation colour-interpolation color-interpolation colour-mixing color-mixing comparison colour-comparison color-comparison contrast luminance deltaE chromaticity display-p3 rec-2020 gamut-mapping temperature cct duv cvd colour-vision-deficiency color-vision-deficiency colour-blindness color-blindness protanopia deuteranopia tritanopia achromatopsia spd dominant-wavelength excitation-purity imaginary-color imaginary-colour + Add dominant wavelength, excitation purity, and imaginary colours Resources\Unicolour.ico LICENSE diff --git a/Unicolour/Utils.cs b/Unicolour/Utils.cs index c96f9f01..fa59b89e 100644 --- a/Unicolour/Utils.cs +++ b/Unicolour/Utils.cs @@ -4,6 +4,11 @@ internal static class Utils { + internal const string Unnamed = "(unnamed)"; + + // based on smallest value required for monochromatic light to be treated as intersecting the spectral locus (see: Spectral.Intersect.IsOnSegment) + // only intended for when a little bit of rounding is necessary + internal static bool IsEffectivelyZero(this double x) => Math.Abs(x) < 5e-14; internal static double Clamp(this double x, double min, double max) => x < min ? min : x > max ? max : x; internal static double Clamp(this int x, int min, int max) => x < min ? min : x > max ? max : x; internal static double CubeRoot(double x) => x < 0 ? -Math.Pow(-x, 1 / 3.0) : Math.Pow(x, 1 / 3.0); diff --git a/Unicolour/WhitePoint.cs b/Unicolour/WhitePoint.cs index 796ef82c..e107fb7e 100644 --- a/Unicolour/WhitePoint.cs +++ b/Unicolour/WhitePoint.cs @@ -5,9 +5,19 @@ public record WhitePoint(double X, double Y, double Z) public double X { get; } = X; public double Y { get; } = Y; public double Z { get; } = Z; - internal Matrix AsXyzMatrix() => Matrix.FromTriplet(X, Y, Z).Select(x => x / 100.0); public static WhitePoint FromXyz(Xyz xyz) => new(xyz.X * 100, xyz.Y * 100, xyz.Z * 100); + internal Matrix AsXyzMatrix() => Matrix.FromTriplet(X, Y, Z).Select(x => x / 100.0); + + public Chromaticity ToChromaticity() + { + var x = X / 100.0; + var y = Y / 100.0; + var z = Z / 100.0; + var normalisation = x + y + z; + return new(x / normalisation, y / normalisation); + } + public override string ToString() => $"({X}, {Y}, {Z})"; } \ No newline at end of file diff --git a/Unicolour/Xyy.cs b/Unicolour/Xyy.cs index 729b160b..fc38dd50 100644 --- a/Unicolour/Xyy.cs +++ b/Unicolour/Xyy.cs @@ -29,14 +29,14 @@ internal Xyy(double x, double y, double upperY, ColourHeritage heritage) : base( * Reverse: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space */ - internal static Xyy FromXyz(Xyz xyz, XyzConfiguration xyzConfig) + internal static Xyy FromXyz(Xyz xyz, Chromaticity whiteChromaticity) { var (x, y, z) = xyz.Triplet; var normalisation = x + y + z; var isBlack = normalisation == 0.0; - - var chromaticityX = isBlack ? xyzConfig.ChromaticityWhite.X : x / normalisation; - var chromaticityY = isBlack ? xyzConfig.ChromaticityWhite.Y : y / normalisation; + + var chromaticityX = isBlack ? whiteChromaticity.X : x / normalisation; + var chromaticityY = isBlack ? whiteChromaticity.Y : y / normalisation; var luminance = isBlack ? 0 : y; return new Xyy(chromaticityX, chromaticityY, luminance, ColourHeritage.From(xyz)); } diff --git a/Unicolour/Xyz.cs b/Unicolour/Xyz.cs index 050645da..9bb7639b 100644 --- a/Unicolour/Xyz.cs +++ b/Unicolour/Xyz.cs @@ -43,12 +43,12 @@ internal static Xyz FromSpd(Spd spd, Observer observer) var ySum = wavelengths.Sum(wavelength => spd.SpectralPower(wavelength) * observer.ColourMatchY(wavelength) * delta); var zSum = wavelengths.Sum(wavelength => spd.SpectralPower(wavelength) * observer.ColourMatchZ(wavelength) * delta); - var k = 100 / ySum; - var x = xSum * k; - var y = ySum * k; - var z = zSum * k; - - return new Xyz(x / 100.0, y / 100.0, z / 100.0); + // note: could support a notion of "absolute" XYZ by simply returning without dividing by ySum + // though unclear if actually useful + var x = xSum / ySum; + var y = ySum / ySum; + var z = zSum / ySum; + return new Xyz(x, y, z); } // only for potential debugging or diagnostics diff --git a/Unicolour/XyzConfiguration.cs b/Unicolour/XyzConfiguration.cs index f760d40d..9239caef 100644 --- a/Unicolour/XyzConfiguration.cs +++ b/Unicolour/XyzConfiguration.cs @@ -2,40 +2,35 @@ public class XyzConfiguration { + public static readonly XyzConfiguration D65 = new(Illuminant.D65, Observer.Degree2, nameof(D65)); + public static readonly XyzConfiguration D50 = new(Illuminant.D50, Observer.Degree2, nameof(D50)); + public WhitePoint WhitePoint { get; } - public Chromaticity ChromaticityWhite => GetChromaticity(WhitePoint); + public Chromaticity WhiteChromaticity => WhitePoint.ToChromaticity(); public Observer Observer { get; } + internal Spectral Spectral { get; } internal Planckian Planckian { get; } - - public static readonly XyzConfiguration D65 = new(Illuminant.D65, Observer.Degree2); - public static readonly XyzConfiguration D50 = new(Illuminant.D50, Observer.Degree2); + public string Name { get; } // even if white point has been hardcoded, still need observer to calculate CCT - public XyzConfiguration(WhitePoint whitePoint) : - this(whitePoint, Observer.Degree2) + public XyzConfiguration(WhitePoint whitePoint, string name = Utils.Unnamed) : + this(whitePoint, Observer.Degree2, name) { } - public XyzConfiguration(Illuminant illuminant, Observer observer) : - this(illuminant.GetWhitePoint(observer), observer) + public XyzConfiguration(Illuminant illuminant, Observer observer, string name = Utils.Unnamed) : + this(illuminant.GetWhitePoint(observer), observer, name) { } - public XyzConfiguration(WhitePoint whitePoint, Observer observer) + public XyzConfiguration(WhitePoint whitePoint, Observer observer, string name = Utils.Unnamed) { WhitePoint = whitePoint; Observer = observer; + Spectral = new Spectral(observer, WhiteChromaticity); Planckian = new Planckian(observer); + Name = name; } - - private static Chromaticity GetChromaticity(WhitePoint whitePoint) - { - var x = whitePoint.X / 100.0; - var y = whitePoint.Y / 100.0; - var z = whitePoint.Z / 100.0; - var normalisation = x + y + z; - return new(x / normalisation, y / normalisation); - } - - public override string ToString() => $"XYZ {WhitePoint}"; + + public override string ToString() => $"{Name} · white point {WhitePoint}"; } \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index b2e62612..61f668b9 100644 --- a/docs/README.md +++ b/docs/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). @@ -146,7 +149,7 @@ var (l, c, h) = colour.Oklch.Triplet; ### 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); @@ -187,25 +190,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. @@ -220,6 +225,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# @@ -254,7 +278,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# @@ -269,8 +293,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); @@ -352,19 +376,59 @@ Console.WriteLine(rec2020Colour.Rgb); // 0.57 0.96 0.27 ``` ## ✨ Examples -This repo contains an [example project](https://github.com/waacton/Unicolour/tree/main/Unicolour.Example/Program.cs) that uses Unicolour to: -1. Generate gradients through each colour space - ![Gradients through different colour spaces, generated from Unicolour](gradients.png) -2. Render the colour spectrum with different colour vision deficiencies - ![Spectrum rendered with different colour vision deficiencies, generated from Unicolour](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](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](temperature.png) - -There is also a [console application](https://github.com/waacton/Unicolour/tree/main/Unicolour.Console/Program.cs) that uses Unicolour to show colour information for a given hex value. - -![Colour information from hex value](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](https://github.com/waacton/Unicolour/tree/main/Example.Gradients/Program.cs) project. + +| ![Gradients generated through different colour spaces, created with Unicolour](gradient-colour-spaces.png) | +|-----------------------------------------------------------------------------------------------------------------| +| _Gradients generated through each colour space_ | + +| ![Visualisation of temperature from 1,000 K to 13,000 K, created with Unicolour](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](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](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](https://github.com/waacton/Unicolour/tree/main/Example.Diagrams/Program.cs) project. + +| ![CIE xy chromaticity diagram with sRGB gamut, created with Unicolour](diagram-xy-chromaticity-rgb.png) | +|--------------------------------------------------------------------------------------------------------------| +| _CIE xy chromaticity diagram with sRGB gamut_ | + +| ![CIE xy chromaticity diagram with Planckian or blackbody locus, created with Unicolour](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](diagram-spectral-locus.png) | +|---------------------------------------------------------------------------------------------------------------------------------------| +| _CIE xy chromaticity diagram with spectral locus plotted at 1 nm intervals_ | + +| ![CIE 1960 colour space, created with Unicolour](diagram-uv-chromaticity.png) | +|------------------------------------------------------------------------------------| +| _CIE 1960 colour space_ | + +| ![CIE 1960 colour space with Planckian or blackbody locus, created with Unicolour](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](https://github.com/waacton/Unicolour/tree/main/Example.Console/Program.cs) project. + +| ![Console application showing colour information from hex value, created with Unicolour](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/docs/README_us.md b/docs/README_us.md index 367f967b..dead73a8 100644 --- a/docs/README_us.md +++ b/docs/README_us.md @@ -10,9 +10,10 @@ Unicolour is a .NET library written in C# for working with color: - Color space conversion - Color mixing / color interpolation - Color difference / color distance +- Color gamut mapping - Color chromaticity - Color temperature -- Color 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 color information is available, such as chromaticity coordinates and [temperature](#convert-between-color-and-temperature). +Other useful color information is available, such as chromaticity coordinates, +[temperature](#convert-between-color-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). @@ -146,7 +149,7 @@ var (l, c, h) = color.Oklch.Triplet; ### Mix colors -Two colors can be mixed by [interpolating between them in any color space](#-examples), +Two colors can be mixed by [interpolating between them in any color 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); @@ -187,25 +190,27 @@ var difference = red.Difference(blue, DeltaE.Cie76); | ΔECAM02 | `DeltaE.Cam02` | | ΔECAM16 | `DeltaE.Cam16` | +### Map color into display gamut +Colours that cannot be displayed with the [configured RGB model](#rgbconfiguration) can be mapped to the closest in-gamut color. +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 color and temperature Correlated color temperature (CCT) and delta UV (∆uv) can be obtained from a color, and can be used to create a color. 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 color into display gamut -Colours that cannot be displayed with the [configured RGB model](#rgbconfiguration) can be mapped to the closest in-gamut color. -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 color from spectral power distribution A spectral power distribution (SPD) can be used to create a color. Wavelengths should be provided in either 1 nm or 5 nm intervals, and omitted wavelengths are assumed to have zero spectral power. @@ -220,6 +225,25 @@ var spd = new Spd var intenseYellow = new Unicolour(spd); ``` +### Get wavelength attributes +The dominant wavelength and excitation purity of a color 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 colors +Whether or not a color is imaginary — one that cannot be produced by the eye — can be determined using the spectral locus. +They are the colors 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 color vision deficiency A new `Unicolour` can be generated that simulates how a color appears to someone with a particular color vision deficiency (CVD) or color blindness. ```c# @@ -254,7 +278,7 @@ var color = new Unicolour(defaultConfig, ColourSpace.Rgb255, 192, 255, 238); ``` ## 💡 Configuration -The `Configuration` parameter can be used to customize how color is processed. +The `Configuration` parameter can be used to define the context of the color. Example configuration with predefined Rec. 2020 RGB & illuminant D50 (2° observer) XYZ: ```c# @@ -269,8 +293,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); @@ -352,19 +376,59 @@ Console.WriteLine(rec2020Colour.Rgb); // 0.57 0.96 0.27 ``` ## ✨ Examples -This repo contains an [example project](https://github.com/waacton/Unicolour/tree/main/Unicolour.Example/Program.cs) that uses Unicolour to: -1. Generate gradients through each color space - ![Gradients through different color spaces, generated from Unicolour](gradients.png) -2. Render the color spectrum with different color vision deficiencies - ![Spectrum rendered with different color vision deficiencies, generated from Unicolour](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](alpha-interpolation.png) -4. Visualize correlated color temperature (CCT) from 1,000 K to 13,000 K - ![Visualization of temperature from 1,000 K to 13,000 K, generated from Unicolour](temperature.png) - -There is also a [console application](https://github.com/waacton/Unicolour/tree/main/Unicolour.Console/Program.cs) that uses Unicolour to show color information for a given hex value. - -![Color information from hex value](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 color data](#diagrams) +3. [A colorful 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](https://github.com/waacton/Unicolour/tree/main/Example.Gradients/Program.cs) project. + +| ![Gradients generated through different color spaces, created with Unicolour](gradient-color-spaces.png) | +|-----------------------------------------------------------------------------------------------------------------| +| _Gradients generated through each color space_ | + +| ![Visualization of temperature from 1,000 K to 13,000 K, created with Unicolour](gradient-temperature.png) | +|-----------------------------------------------------------------------------------------------------------------| +| _Visualization of temperature from 1,000 K to 13,000 K_ | + +| ![Color spectrum rendered with different color vision deficiencies, created with Unicolour](gradient-vision-deficiency.png) | +|------------------------------------------------------------------------------------------------------------------------------------| +| _Color spectrum rendered with different color vision deficiencies_ | + +| ![Demonstration of interpolating from red to transparent to blue, with and without premultiplied alpha, created with Unicolour](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 color data using 📈 [ScottPlot](https://github.com/scottplot/scottplot) can be seen in the [Example.Diagrams](https://github.com/waacton/Unicolour/tree/main/Example.Diagrams/Program.cs) project. + +| ![CIE xy chromaticity diagram with sRGB gamut, created with Unicolour](diagram-xy-chromaticity-rgb.png) | +|--------------------------------------------------------------------------------------------------------------| +| _CIE xy chromaticity diagram with sRGB gamut_ | + +| ![CIE xy chromaticity diagram with Planckian or blackbody locus, created with Unicolour](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](diagram-spectral-locus.png) | +|---------------------------------------------------------------------------------------------------------------------------------------| +| _CIE xy chromaticity diagram with spectral locus plotted at 1 nm intervals_ | + +| ![CIE 1960 color space, created with Unicolour](diagram-uv-chromaticity.png) | +|------------------------------------------------------------------------------------| +| _CIE 1960 color space_ | + +| ![CIE 1960 color space with Planckian or blackbody locus, created with Unicolour](diagram-uv-chromaticity-blackbody.png) | +|--------------------------------------------------------------------------------------------------------------------------------| +| _CIE 1960 color space with Planckian or blackbody locus_ | + +### Console +Example code to create a colorful console application using ⌨️ [Spectre.Console](https://github.com/spectreconsole/spectre.console) can be seen in the [Example.Console](https://github.com/waacton/Unicolour/tree/main/Example.Console/Program.cs) project. + +| ![Console application showing color information from hex value, created with Unicolour](console-color-info.png) | +|------------------------------------------------------------------------------------------------------------------------| +| Console application showing color information from hex value | ## 🔮 Datasets Some color datasets have been compiled for convenience and are available as a [NuGet package](https://www.nuget.org/packages/Wacton.Unicolour.Datasets/). diff --git a/docs/console-colour-info.png b/docs/console-colour-info.png new file mode 100644 index 00000000..bda3646e Binary files /dev/null and b/docs/console-colour-info.png differ diff --git a/docs/diagram-spectral-locus.png b/docs/diagram-spectral-locus.png new file mode 100644 index 00000000..78cd20e9 Binary files /dev/null and b/docs/diagram-spectral-locus.png differ diff --git a/docs/diagram-uv-chromaticity-blackbody.png b/docs/diagram-uv-chromaticity-blackbody.png new file mode 100644 index 00000000..8cbe1a4d Binary files /dev/null and b/docs/diagram-uv-chromaticity-blackbody.png differ diff --git a/docs/diagram-uv-chromaticity.png b/docs/diagram-uv-chromaticity.png new file mode 100644 index 00000000..e8155117 Binary files /dev/null and b/docs/diagram-uv-chromaticity.png differ diff --git a/docs/diagram-xy-chromaticity-blackbody.png b/docs/diagram-xy-chromaticity-blackbody.png new file mode 100644 index 00000000..b7c301ff Binary files /dev/null and b/docs/diagram-xy-chromaticity-blackbody.png differ diff --git a/docs/diagram-xy-chromaticity-rgb.png b/docs/diagram-xy-chromaticity-rgb.png new file mode 100644 index 00000000..503176b3 Binary files /dev/null and b/docs/diagram-xy-chromaticity-rgb.png differ diff --git a/docs/alpha-interpolation.png b/docs/gradient-alpha-interpolation.png similarity index 100% rename from docs/alpha-interpolation.png rename to docs/gradient-alpha-interpolation.png diff --git a/docs/gradients.png b/docs/gradient-colour-spaces.png similarity index 100% rename from docs/gradients.png rename to docs/gradient-colour-spaces.png diff --git a/docs/temperature.png b/docs/gradient-temperature.png similarity index 100% rename from docs/temperature.png rename to docs/gradient-temperature.png diff --git a/docs/vision-deficiency.png b/docs/gradient-vision-deficiency.png similarity index 100% rename from docs/vision-deficiency.png rename to docs/gradient-vision-deficiency.png