From 1c019eb21136c6e134b1cf948cd497ea1a52ba76 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 8 Apr 2022 23:51:49 -0400 Subject: [PATCH] Add new HslColor struct --- .../Avalonia.Build.Tasks.csproj | 3 + src/Avalonia.Visuals/Media/Color.cs | 12 + src/Avalonia.Visuals/Media/HslColor.cs | 322 ++++++++++++++++++ src/Avalonia.Visuals/Media/HsvColor.cs | 16 +- 4 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/HslColor.cs diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 5a7daa6d12..9629324c8d 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -83,6 +83,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 9f8588d400..84fde9bfc2 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -308,11 +308,23 @@ namespace Avalonia.Media } } + /// + /// Indicates whether the values of two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are equal; otherwise, false. public static bool operator ==(Color left, Color right) { return left.Equals(right); } + /// + /// Indicates whether the values of two specified objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are not equal; otherwise, false. public static bool operator !=(Color left, Color right) { return !left.Equals(right); diff --git a/src/Avalonia.Visuals/Media/HslColor.cs b/src/Avalonia.Visuals/Media/HslColor.cs new file mode 100644 index 0000000000..21ab669a05 --- /dev/null +++ b/src/Avalonia.Visuals/Media/HslColor.cs @@ -0,0 +1,322 @@ +// Color conversion portions of this source file are adapted from the Windows Community Toolkit project. +// (https://github.com/CommunityToolkit/WindowsCommunityToolkit) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Globalization; +using System.Text; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// Defines a color using the hue/saturation/lightness (HSL) model. + /// +#if !BUILDTASK + public +#endif + readonly struct HslColor : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + public HslColor( + double alpha, + double hue, + double saturation, + double lightness) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + L = MathUtilities.Clamp(lightness, 0.0, 1.0); + + // The maximum value of Hue is technically 360 minus epsilon (just below 360). + // This is because, in a color circle, 360 degrees is equivalent to 0 degrees. + // However, that is too tricky to work with in code and isn't as intuitive. + // Therefore, since 360 == 0, just wrap 360 if needed back to 0. + H = (H == 360.0 ? 0 : H); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// This constructor exists only for internal use where performance is critical. + /// Whether or not the component values are in the correct ranges must be known. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + /// Whether to clamp component values to their required ranges. + internal HslColor( + double alpha, + double hue, + double saturation, + double lightness, + bool clampValues) + { + if (clampValues) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + L = MathUtilities.Clamp(lightness, 0.0, 1.0); + + // See comments in constructor above + H = (H == 360.0 ? 0 : H); + } + else + { + A = alpha; + H = hue; + S = saturation; + L = lightness; + } + } + + /// + /// Gets the Alpha (transparency) component in the range from 0..1. + /// + public double A { get; } + + /// + /// Gets the Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// + public double H { get; } + + /// + /// Gets the Saturation component in the range from 0..1. + /// + public double S { get; } + + /// + /// Gets the Lightness component in the range from 0..1. + /// + public double L { get; } + + /// + public bool Equals(HslColor other) + { + return other.A == A && + other.H == H && + other.S == S && + other.L == L; + } + + /// + public override bool Equals(object? obj) + { + if (obj is HslColor hslColor) + { + return Equals(hslColor); + } + else + { + return false; + } + } + + /// + /// Gets a hashcode for this object. + /// Hashcode is not guaranteed to be unique. + /// + /// The hashcode for this object. + public override int GetHashCode() + { + // Same algorithm as Color + // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ H.GetHashCode(); + hashCode = (hashCode * 397) ^ S.GetHashCode(); + hashCode = (hashCode * 397) ^ L.GetHashCode(); + return hashCode; + } + } + + /// + /// Returns the RGB color model equivalent of this HSL color. + /// + /// The RGB equivalent color. + public Color ToRgb() + { + // Use the by-component conversion method directly for performance + return HslColor.ToRgb(H, S, L, A); + } + + /// + /// Creates a new from individual color component values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + /// A new built from the individual color component values. + public static HslColor FromAhsl(double a, double h, double s, double l) + { + return new HslColor(a, h, s, l); + } + + /// + /// Creates a new from individual color component values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + /// A new built from the individual color component values. + public static HslColor FromHsl(double h, double s, double l) + { + return new HslColor(1.0, h, s, l); + } + + /// + /// Converts the given HSL color to it's RGB color equivalent. + /// + /// The color in the HSL color model. + /// A new RGB equivalent to the given HSLA values. + public static Color ToRgb(HslColor hslColor) + { + return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A); + } + + /// + /// Converts the given HSLA color component values to it's RGB 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 RGB equivalent to the given HSLA values. + public static Color ToRgb( + double hue, + double saturation, + double lightness, + double alpha = 1.0) + { + // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT) + // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs + // It has been modified to ensure input ranges and not throw exceptions. + + // 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; + + double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation; + double h1 = hue / 60; + double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); + double m = lightness - (0.5 * chroma); + double r1, g1, b1; + + if (h1 < 1) + { + r1 = chroma; + g1 = x; + b1 = 0; + } + else if (h1 < 2) + { + r1 = x; + g1 = chroma; + b1 = 0; + } + else if (h1 < 3) + { + r1 = 0; + g1 = chroma; + b1 = x; + } + else if (h1 < 4) + { + r1 = 0; + g1 = x; + b1 = chroma; + } + else if (h1 < 5) + { + r1 = x; + g1 = 0; + b1 = chroma; + } + else + { + r1 = chroma; + g1 = 0; + b1 = x; + } + + return Color.FromArgb( + (byte)(255 * alpha), + (byte)(255 * (r1 + m)), + (byte)(255 * (g1 + m)), + (byte)(255 * (b1 + m))); + } + + /// + /// Indicates whether the values of two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are equal; otherwise, false. + public static bool operator ==(HslColor left, HslColor right) + { + return left.Equals(right); + } + + /// + /// Indicates whether the values of two specified objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are not equal; otherwise, false. + public static bool operator !=(HslColor left, HslColor right) + { + return !(left == right); + } + + /// + /// Explicit conversion from an to a . + /// + /// The to convert. + public static explicit operator Color(HslColor hslColor) + { + return hslColor.ToRgb(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index c58db9df00..6a27c39c9b 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -338,10 +338,10 @@ namespace Avalonia.Media /// /// Converts the given HSVA color component values to it's RGB 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. + /// 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 RGB equivalent to the given HSVA values. public static Color ToRgb( double hue, @@ -467,10 +467,10 @@ namespace Avalonia.Media } return Color.FromArgb( - (byte)Math.Round(alpha * 255), - (byte)Math.Round(r * 255), - (byte)Math.Round(g * 255), - (byte)Math.Round(b * 255)); + (byte)(alpha * 255), + (byte)(r * 255), + (byte)(g * 255), + (byte)(b * 255)); } ///