diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 7d74b4d602..3ee151389a 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -478,7 +478,6 @@ namespace Avalonia.Media /// The HSL equivalent color. public HslColor ToHsl() { - // Don't use the HslColor(Color) constructor to avoid an extra HslColor return Color.ToHsl(R, G, B, A); } @@ -488,7 +487,6 @@ namespace Avalonia.Media /// The HSV equivalent color. public HsvColor ToHsv() { - // Don't use the HsvColor(Color) constructor to avoid an extra HsvColor return Color.ToHsv(R, G, B, A); } diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index 624cc88ad4..84f2149367 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -165,10 +165,18 @@ namespace Avalonia.Media /// The RGB equivalent color. public Color ToRgb() { - // Use the by-component conversion method directly for performance return HslColor.ToRgb(H, S, L, A); } + /// + /// Returns the HSV color model equivalent of this HSL color. + /// + /// The HSV equivalent color. + public HsvColor ToHsv() + { + return HslColor.ToHsv(H, S, L, A); + } + /// public override string ToString() { @@ -432,13 +440,67 @@ namespace Avalonia.Media b1 = x; } - return Color.FromArgb( + return new Color( (byte)Math.Round(255 * alpha), (byte)Math.Round(255 * (r1 + m)), (byte)Math.Round(255 * (g1 + m)), (byte)Math.Round(255 * (b1 + m))); } + /// + /// Converts the given HSLA color component values to their HSV color equivalent. + /// + /// The Hue component in the HSL color model in the range from 0..360. + /// The Saturation component in the HSL color model in the range from 0..1. + /// The Lightness component in the HSL color model in the range from 0..1. + /// The Alpha component in the range from 0..1. + /// A new equivalent to the given HSLA values. + public static HsvColor ToHsv( + double hue, + double saturation, + double lightness, + double alpha = 1.0) + { + // We want the hue to be between 0 and 359, + // so we first ensure that that's the case. + while (hue >= 360.0) + { + hue -= 360.0; + } + + while (hue < 0.0) + { + hue += 360.0; + } + + // We similarly clamp saturation, lightness and alpha between 0 and 1. + saturation = saturation < 0.0 ? 0.0 : saturation; + saturation = saturation > 1.0 ? 1.0 : saturation; + + lightness = lightness < 0.0 ? 0.0 : lightness; + lightness = lightness > 1.0 ? 1.0 : lightness; + + alpha = alpha < 0.0 ? 0.0 : alpha; + alpha = alpha > 1.0 ? 1.0 : alpha; + + // The conversion algorithm is from the below link + // https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion + + double s; + double v = lightness + (saturation * Math.Min(lightness, 1.0 - lightness)); + + if (v <= 0) + { + s = 0; + } + else + { + s = 2.0 * (1.0 - (lightness / v)); + } + + return new HsvColor(alpha, hue, s, v); + } + /// /// Indicates whether the values of two specified objects are equal. /// diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 3c6336c445..03949d32aa 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -195,10 +195,18 @@ namespace Avalonia.Media /// The RGB equivalent color. public Color ToRgb() { - // Use the by-component conversion method directly for performance return HsvColor.ToRgb(H, S, V, A); } + /// + /// Returns the HSL color model equivalent of this HSV color. + /// + /// The HSL equivalent color. + public HslColor ToHsl() + { + return HsvColor.ToHsl(H, S, V, A); + } + /// public override string ToString() { @@ -510,13 +518,67 @@ namespace Avalonia.Media break; } - return Color.FromArgb( + return new Color( (byte)Math.Round(alpha * 255), (byte)Math.Round(r * 255), (byte)Math.Round(g * 255), (byte)Math.Round(b * 255)); } + /// + /// Converts the given HSVA color component values to their HSL color equivalent. + /// + /// The Hue component in the HSV color model in the range from 0..360. + /// The Saturation component in the HSV color model in the range from 0..1. + /// The Value component in the HSV color model in the range from 0..1. + /// The Alpha component in the range from 0..1. + /// A new equivalent to the given HSVA values. + public static HslColor ToHsl( + double hue, + double saturation, + double value, + double alpha = 1.0) + { + // We want the hue to be between 0 and 359, + // so we first ensure that that's the case. + while (hue >= 360.0) + { + hue -= 360.0; + } + + while (hue < 0.0) + { + hue += 360.0; + } + + // We similarly clamp saturation, value and alpha between 0 and 1. + saturation = saturation < 0.0 ? 0.0 : saturation; + saturation = saturation > 1.0 ? 1.0 : saturation; + + value = value < 0.0 ? 0.0 : value; + value = value > 1.0 ? 1.0 : value; + + alpha = alpha < 0.0 ? 0.0 : alpha; + alpha = alpha > 1.0 ? 1.0 : alpha; + + // The conversion algorithm is from the below link + // https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion + + double s; + double l = value * (1.0 - (saturation / 2.0)); + + if (l <= 0 || l >= 1) + { + s = 0.0; + } + else + { + s = (value - l) / Math.Min(l, 1.0 - l); + } + + return new HslColor(alpha, hue, s, l); + } + /// /// Indicates whether the values of two specified objects are equal. /// diff --git a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs index 36929d5e95..1ed3ea50b9 100644 --- a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs @@ -335,5 +335,34 @@ namespace Avalonia.Base.UnitTests.Media Assert.True(dataPoint.Item2 == parsedColor); } } + + [Fact] + public void Hsv_To_From_Hsl_Conversion() + { + // Note that conversion of values more representative of actual colors is not done due to rounding error + // It would be necessary to introduce a different equality comparison that accounts for rounding differences in values + // This is a result of the math in the conversion itself + // RGB doesn't have this problem because it uses whole numbers + var data = new Tuple[] + { + Tuple.Create(new HsvColor(1.0, 0.0, 0.0, 0.0), new HslColor(1.0, 0.0, 0.0, 0.0)), + Tuple.Create(new HsvColor(1.0, 359.0, 1.0, 1.0), new HslColor(1.0, 359.0, 1.0, 0.5)), + + Tuple.Create(new HsvColor(1.0, 128.0, 0.0, 0.0), new HslColor(1.0, 128.0, 0.0, 0.0)), + Tuple.Create(new HsvColor(1.0, 128.0, 0.0, 1.0), new HslColor(1.0, 128.0, 0.0, 1.0)), + Tuple.Create(new HsvColor(1.0, 128.0, 1.0, 1.0), new HslColor(1.0, 128.0, 1.0, 0.5)), + + Tuple.Create(new HsvColor(0.23, 0.5, 1.0, 1.0), new HslColor(0.23, 0.5, 1.0, 0.5)), + }; + + foreach (var dataPoint in data) + { + var convertedHsl = dataPoint.Item1.ToHsl(); + var convertedHsv = dataPoint.Item2.ToHsv(); + + Assert.Equal(convertedHsv, dataPoint.Item1); + Assert.Equal(convertedHsl, dataPoint.Item2); + } + } } }