using System; using System.Linq; using Chernobyl.Mathematics; using Chernobyl.Mathematics.Utility; namespace Chernobyl.Graphics { /// /// Represents a color with hue, saturation, and lightness. /// public struct ColorHsla : IEquatable { /// The color as a radial value on a color wheel from 0.0 (0 degrees) to /// 1.0 (360 degrees). /// The amount of color (the opposite of grayness) from 0.0 (pure /// gray) to 1.0 (full color). /// The amount of lightness in the color from 0.0 (black) to 1.0 /// (white). When lightness is 0.5, the color is “pure”. /// The amount of opacity in the color between 0.0 (fully transparent) /// and 1.0 (fully opaque). public ColorHsla(double hue, double saturation = 1.0, double luminosity = .5, double alpha = 1.0) { hue.Throw(nameof(hue)).IfNotBetween(0, 1); saturation.Throw(nameof(saturation)).IfNotBetween(0, 1); luminosity.Throw(nameof(luminosity)).IfNotBetween(0, 1); alpha.Throw(nameof(alpha)).IfNotBetween(0, 1); Hsla = new[] {hue, saturation, luminosity, alpha}; } /// /// Constructs an HSLA without error checking for improved performance internally. Should /// not be made public due to lack of error checking. /// /// The hsla values that are gauranteed to be correct. ColorHsla(double[] hsla) { Hsla = hsla; } /// /// The color as a radial value on a color wheel from 0.0 (0 degrees) to 1.0 (360 degrees). /// public double Hue => Hsla[0]; /// /// The amount of color (the opposite of grayness) from 0.0 (pure gray) to 1.0 (full color). /// public double Saturation => Hsla[1]; /// /// The amount of lightness in the color from 0.0 (black) to 1.0 (white). When lightness is 0.5, /// the color is “pure”. /// public double Luminosity => Hsla[LuminosityIndex]; /// /// The amount of opacity in the color between 0.0 (fully transparent) and 1.0 (fully opaque). /// public double Alpha => Hsla[3]; /// /// Gets the color in hue, saturation, luminosity, and alpha order. /// public double[] Hsla { get; } /// /// Returns the HSLA with its values rounded to a specified number of fractional digits. /// public ColorHsla Round(int digits) => new ColorHsla(Hsla.Select(v => Math.Round(v, digits)).ToArray()); /// /// Returns the color with reduced luminosity. /// /// The amount to darken by between 0 and 1. public ColorHsla Darken(double amount) { amount.Throw(nameof(amount)).IfNotBetween(0.0, 1.0); var hsla = new double[4]; Hsla.CopyTo(hsla, 0); hsla[LuminosityIndex] = Math.Max(hsla[LuminosityIndex] - amount, 0.0); return new ColorHsla(hsla); } /// /// Returns the color with reduced luminosity. /// /// The amount to darken by between 0 and 1. public ColorHsla Lighten(double amount) { amount.Throw(nameof(amount)).IfNotBetween(0.0, 1.0); var hsla = new double[4]; Hsla.CopyTo(hsla, 0); hsla[LuminosityIndex] = Math.Min(hsla[LuminosityIndex] + amount, 1.0); return new ColorHsla(hsla); } /// /// Returns a value that is a straight line value from this to /// by . /// /// /// The weight of to apply to /// this, 0 being fully equal to this and 1 being fully equal /// to , and anything above 1 resulting in an addition to /// . public ColorHsla Lerp(ColorHsla second, double amount) { amount.Throw(nameof(amount)).IfNotBetween(0.0, 1.0); return new ColorHsla(Hsla.Zip(second.Hsla, (f, s) => f.Lerp(s, amount)).ToArray()); } /// /// Converts and HSLA color to an ARGB color. /// public static explicit operator ColorRgba(ColorHsla hsla) { // Implementation from https://stackoverflow.com/a/4794649 (with changes) byte r, g, b; if (hsla.Saturation == 0) { r = (byte)Math.Round(hsla.Luminosity * ColorRgba.MaxChannelByteValue); g = (byte)Math.Round(hsla.Luminosity * ColorRgba.MaxChannelByteValue); b = (byte)Math.Round(hsla.Luminosity * ColorRgba.MaxChannelByteValue); } else { var t2 = hsla.Luminosity < 0.5d ? hsla.Luminosity * (1d + hsla.Saturation) : (hsla.Luminosity + hsla.Saturation) - (hsla.Luminosity * hsla.Saturation); var t1 = 2d * hsla.Luminosity - t2; var tr = hsla.Hue + (1.0d / 3.0d); var tg = hsla.Hue; var tb = hsla.Hue - (1.0d / 3.0d); double ColorCalc(double c, double _t1, double _t2) { if (c < 0) c += 1d; if (c > 1) c -= 1d; if (6.0d * c < 1.0d) return _t1 + (_t2 - _t1) * 6.0d * c; if (2.0d * c < 1.0d) return _t2; if (3.0d * c < 2.0d) return _t1 + (_t2 - _t1) * (2.0d / 3.0d - c) * 6.0d; return _t1; } tr = ColorCalc(tr, t1, t2); tg = ColorCalc(tg, t1, t2); tb = ColorCalc(tb, t1, t2); r = (byte)Math.Round(tr * ColorRgba.MaxChannelByteValue); g = (byte)Math.Round(tg * ColorRgba.MaxChannelByteValue); b = (byte)Math.Round(tb * ColorRgba.MaxChannelByteValue); } return new ColorRgba(r, g, b, ColorRgba.ColorDoubleToByte(hsla.Alpha)); } /// /// Converts and RGBA color to an HSLA color. /// public static explicit operator ColorHsla(ColorRgba rgba) { // Implementation from https://stackoverflow.com/a/4794649 (with changes) var r = ColorRgba.ColorByteToDouble(rgba.R); var g = ColorRgba.ColorByteToDouble(rgba.G); var b = ColorRgba.ColorByteToDouble(rgba.B); var min = Math.Min(Math.Min(r, g), b); var max = Math.Max(Math.Max(r, g), b); var delta = max - min; var h = 0.0; var s = 0.0; var l = (max + min) / 2.0; if (delta != 0) { if (l < 0.5f) { s = delta / (max + min); } else { s = delta / (2.0f - max - min); } if (r == max) { h = (g - b) / delta; } else if (g == max) { h = 2f + (b - r) / delta; } else if (b == max) { h = 4f + (r - g) / delta; } } //h = h * 60f; if (h < 0) h += 6; return new ColorHsla(h / 6, s, l, ColorRgba.ColorByteToDouble(rgba.A)); } /// /// Compares the values of one color to another. /// /// The color from the left side of the equality operation. /// The color from the right side of the equality operation. /// True if the values of the colors are the same, false if otherwise. public static bool operator ==(ColorHsla leftSide, ColorHsla rightSide) { return (leftSide.Hue == rightSide.Hue && leftSide.Saturation == rightSide.Saturation && leftSide.Luminosity == rightSide.Luminosity && leftSide.Alpha == rightSide.Alpha); } /// /// Compares the values of one color to another. /// /// The color from the left side of the equality operation. /// The color from the right side of the equality operation. /// True if the values of the colors are the different, false if otherwise. public static bool operator !=(ColorHsla leftSide, ColorHsla rightSide) => !(leftSide == rightSide); /// public bool Equals(ColorHsla other) => (this == other); /// public override bool Equals(object obj) => (obj is ColorHsla color && this == color); /// public override int GetHashCode() { return (Hue.GetHashCode() ^ Hue.GetHashCode() + Saturation.GetHashCode() ^ Saturation.GetHashCode() + Luminosity.GetHashCode() ^ Luminosity.GetHashCode() + Alpha.GetHashCode() ^ Alpha.GetHashCode()); } /// /// Converts this color into a string of this format: "{R:000 G:000 B:000 A:000}" where /// '000' is a number between 0 and 255. /// public override string ToString() => $"{{H:{Hue} S:{Saturation} L:{Luminosity} A:{Alpha}}}"; /// /// The color black as HLSA. /// public static readonly ColorHsla Black = new ColorHsla(0.0, luminosity: 0.0); /// /// Red as HSLA /// public static readonly ColorHsla Red = new ColorHsla(0); /// /// Yellow as HSLA /// public static readonly ColorHsla Yellow = new ColorHsla(0.16666666666); // TODO: add more colors: https://en.wikipedia.org/wiki/Web_colors#X11_color_names /// /// The color white as HLSA. /// public static readonly ColorHsla White = new ColorHsla(0.0, luminosity: 1.0); // The index of the luminosity component. const int LuminosityIndex = 2; } }