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;
}
}