From b7b9b8a35c52abf70fda96780dff0acfc5e652ed Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 22:16:59 -0500 Subject: [PATCH 01/23] Add new HsvColor struct --- src/Avalonia.Visuals/Media/HsvColor.cs | 493 +++++++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 src/Avalonia.Visuals/Media/HsvColor.cs diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs new file mode 100644 index 0000000000..605d89346e --- /dev/null +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -0,0 +1,493 @@ +using System; + +namespace Avalonia.Media +{ + /// + /// Defines a color using the hue/saturation/value (HSV) model. + /// + public struct HsvColor : IEquatable + { + /// + /// Represents a null with zero set for all channels. + /// + public static readonly HsvColor Empty = new HsvColor(); + + //////////////////////////////////////////////////////////////////////// + // Constructors + //////////////////////////////////////////////////////////////////////// + + /// + /// Initializes a new instance of the struct. + /// + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + public HsvColor( + double hue, + double saturation, + double value) + { + A = 1.0; + H = Math.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); + S = Math.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); + V = Math.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + /// The Alpha (transparency) channel value in the range from 0..1. + public HsvColor( + double hue, + double saturation, + double value, + double alpha) + { + A = Math.Clamp((double.IsNaN(alpha) ? 1 : alpha), 0, 1); // Default to no transparency + H = Math.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); + S = Math.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); + V = Math.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The RGB color to convert to HSV. + public HsvColor(Color color) + { + var hsv = HsvColor.FromRgb(color); + + A = hsv.A; + H = hsv.H; + S = hsv.S; + V = hsv.V; + } + + //////////////////////////////////////////////////////////////////////// + // Properties + //////////////////////////////////////////////////////////////////////// + + /// + /// Gets the Alpha (transparency) channel value in the range from 0..1. + /// + public double A { get; } + + /// + /// Gets the Hue channel value in the range from 0..360. + /// + public double H { get; } + + /// + /// Gets the Saturation channel value in the range from 0..1. + /// + public double S { get; } + + /// + /// Gets the Value channel value in the range from 0..1. + /// + public double V { get; } + + /// + /// Gets a value indicating whether this is uninitialized and + /// considered null. + /// + public bool IsEmpty + { + get => A == 0 && + H == 0 && + S == 0 && + V == 0; + } + + //////////////////////////////////////////////////////////////////////// + // Methods + //////////////////////////////////////////////////////////////////////// + + /// + public bool Equals(HsvColor other) + { + if (object.Equals(other.A, A) == false) { return false; } + if (object.Equals(other.H, H) == false) { return false; } + if (object.Equals(other.S, S) == false) { return false; } + if (object.Equals(other.V, V) == false) { return false; } + + return true; + } + + /// + public override bool Equals(object? obj) + { + if (obj is HsvColor hsvColor) + { + return Equals(hsvColor); + } + 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() + { + return HashCode.Combine(A, H, S, V); + } + + /// + /// Gets the RGB color model equivalent of this HSV color. + /// + /// The RGB equivalent color. + public Color ToRgb() + { + return HsvColor.ToRgb(this); + } + + //////////////////////////////////////////////////////////////////////// + // Static Methods + //////////////////////////////////////////////////////////////////////// + + /// + /// Creates a new from individual color channel values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Alpha (transparency) channel value in the range from 0..1. + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + /// A new built from the individual color channel values. + public static HsvColor FromAhsv(double a, double h, double s, double v) + { + return new HsvColor(h, s, v, a); + } + + /// + /// Creates a new from individual color channel values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + /// A new built from the individual color channel values. + public static HsvColor FromHsv(double h, double s, double v) + { + return new HsvColor(h, s, v, 1.0); + } + + /// + /// Converts the given HSV color to it's RGB color equivalent. + /// + /// The color in the HSV color model. + /// A new RGB equivalent to the given HSVA values. + public static Color ToRgb(HsvColor hsvColor) + { + return HsvColor.ToRgb(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + } + + /// + /// Converts the given HSVA color channel values to it's RGB color equivalent. + /// + /// The hue channel value in the HSV color model in the range from 0..360. + /// The saturation channel value in the HSV color model in the range from 0..1. + /// The value channel value in the HSV color model in the range from 0..1. + /// The alpha channel value in the range from 0..1. + /// A new RGB equivalent to the given HSVA values. + public static Color ToRgb( + double hue, + double saturation, + double value, + double alpha = 1.0) + { + // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) + // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp + // This was used because it is the best documented and likely most optimized for performance + // Alpha channel support was added + + // 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 and value 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; + + // The first thing that we need to do is to determine the chroma (see above for its definition). + // Remember from above that: + // + // 1. The chroma is the difference between the maximum and the minimum of the RGB channels, + // 2. The value is the maximum of the RGB channels, and + // 3. The saturation comes from dividing the chroma by the maximum of the RGB channels (i.e., the value). + // + // From these facts, you can see that we can retrieve the chroma by simply multiplying the saturation and the value, + // and we can retrieve the minimum of the RGB channels by subtracting the chroma from the value. + double chroma = saturation * value; + double min = value - chroma; + + // If the chroma is zero, then we have a greyscale color. In that case, the maximum and the minimum RGB channels + // have the same value (and, indeed, all of the RGB channels are the same), so we can just immediately return + // the minimum value as the value of all the channels. + if (chroma == 0) + { + return Color.FromArgb( + (byte)Math.Round(alpha * 255), + (byte)Math.Round(min * 255), + (byte)Math.Round(min * 255), + (byte)Math.Round(min * 255)); + } + + // If the chroma is not zero, then we need to continue. The first step is to figure out + // what section of the color wheel we're located in. In order to do that, we'll divide the hue by 60. + // The resulting value means we're in one of the following locations: + // + // 0 - Between red and yellow. + // 1 - Between yellow and green. + // 2 - Between green and cyan. + // 3 - Between cyan and blue. + // 4 - Between blue and purple. + // 5 - Between purple and red. + // + // In each of these sextants, one of the RGB channels is completely present, one is partially present, and one is not present. + // For example, as we transition between red and yellow, red is completely present, green is becoming increasingly present, and blue is not present. + // Then, as we transition from yellow and green, green is now completely present, red is becoming decreasingly present, and blue is still not present. + // As we transition from green to cyan, green is still completely present, blue is becoming increasingly present, and red is no longer present. And so on. + // + // To convert from hue to RGB value, we first need to figure out which of the three channels is in which configuration + // in the sextant that we're located in. Next, we figure out what value the completely-present color should have. + // We know that chroma = (max - min), and we know that this color is the max color, so to find its value we simply add + // min to chroma to retrieve max. Finally, we consider how far we've transitioned from the pure form of that color + // to the next color (e.g., how far we are from pure red towards yellow), and give a value to the partially present channel + // equal to the minimum plus the chroma (i.e., the max minus the min), multiplied by the percentage towards the new color. + // This gets us a value between the maximum and the minimum representing the partially present channel. + // Finally, the not-present color must be equal to the minimum value, since it is the one least participating in the overall color. + int sextant = (int)(hue / 60); + double intermediateColorPercentage = (hue / 60) - sextant; + double max = chroma + min; + + double r = 0; + double g = 0; + double b = 0; + + switch (sextant) + { + case 0: + r = max; + g = min + (chroma * intermediateColorPercentage); + b = min; + break; + case 1: + r = min + (chroma * (1 - intermediateColorPercentage)); + g = max; + b = min; + break; + case 2: + r = min; + g = max; + b = min + (chroma * intermediateColorPercentage); + break; + case 3: + r = min; + g = min + (chroma * (1 - intermediateColorPercentage)); + b = max; + break; + case 4: + r = min + (chroma * intermediateColorPercentage); + g = min; + b = max; + break; + case 5: + r = max; + g = min; + b = min + (chroma * (1 - intermediateColorPercentage)); + break; + } + + return Color.FromArgb( + (byte)Math.Round(alpha * 255), + (byte)Math.Round(r * 255), + (byte)Math.Round(g * 255), + (byte)Math.Round(b * 255)); + } + + /// + /// Converts the given RGB color to it's HSV color equivalent. + /// + /// The color in the RGB color model. + /// A new equivalent to the given RGBA values. + public static HsvColor FromRgb(Color color) + { + return HsvColor.FromRgb(color.R, color.G, color.B, color.A); + } + + /// + /// Converts the given RGBA color channel values to it's HSV color equivalent. + /// + /// The red channel value in the RGB color model. + /// The green channel value in the RGB color model. + /// The blue channel value in the RGB color model. + /// The alpha channel value. + /// A new equivalent to the given RGBA values. + public static HsvColor FromRgb( + byte red, + byte green, + byte blue, + byte alpha = 0xFF) + { + // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) + // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp + // This was used because it is the best documented and likely most optimized for performance + // Alpha channel support was added + + // Normalize RGBA channel values into the 0..1 range used by this algorithm + double r = red / 255.0; + double g = green / 255.0; + double b = blue / 255.0; + double a = alpha / 255.0; + + double hue; + double saturation; + double value; + + double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b); + double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b); + + // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255). + // Conceptually speaking, it represents how much color is present. + // If at least one of R, G, B is 255, then there exists as much color as there can be. + // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds + // to black (i.e., the absence of any color). + value = max; + + // The "chroma" of the color is a value directly proportional to the extent to which + // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0), + // then the chroma is maximized - this is a pure yellow, no gray of any kind. + // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero + // implies that this color is pure greyscale, with no actual hue to be found. + double chroma = max - min; + + // If the chrome is zero, then hue is technically undefined - a greyscale color + // has no hue. For the sake of convenience, we'll just set hue to zero, since + // it will be unused in this circumstance. Since the color is purely gray, + // saturation is also equal to zero - you can think of saturation as basically + // a measure of hue intensity, such that no hue at all corresponds to a + // nonexistent intensity. + if (chroma == 0) + { + hue = 0.0; + saturation = 0.0; + } + else + { + // In this block, hue is properly defined, so we'll extract both hue + // and saturation information from the RGB color. + + // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees. + // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red. + // Every other hue is somewhere between either red and green, green and blue, and blue and red, + // so every other hue can be thought of as an angle on this color wheel. + // These if/else statements determines where on this color wheel our color lies. + if (r == max) + { + // If the red channel is the most pronounced channel, then we exist + // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees + // where red dominates. We figure out where in that section we are exactly + // by considering whether the green or the blue channel is greater - by subtracting green from blue, + // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then + // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger, + // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue + // even if we're very close to greyscale. + hue = 60 * (g - b) / chroma; + } + else if (g == max) + { + // We do the exact same for the case where the green channel is the most pronounced channel, + // only this time we want to see if we should tilt towards the blue direction or the red direction. + // We add 120 to center our value in the green third of the color wheel. + hue = 120 + (60 * (b - r) / chroma); + } + else // blue == max + { + // And we also do the exact same for the case where the blue channel is the most pronounced channel, + // only this time we want to see if we should tilt towards the red direction or the green direction. + // We add 240 to center our value in the blue third of the color wheel. + hue = 240 + (60 * (r - g) / chroma); + } + + // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero - + // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values. + if (hue < 0.0) + { + hue += 360.0; + } + + // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is. + // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value). + // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1, + // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be. + // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5, + // meaning that this color is partially washed out. + // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue. + saturation = chroma / value; + } + + return new HsvColor(hue, saturation, value, a); + } + + //////////////////////////////////////////////////////////////////////// + // Operators + //////////////////////////////////////////////////////////////////////// + + /// + /// 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 ==(HsvColor left, HsvColor 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 !=(HsvColor left, HsvColor right) + { + return !(left == right); + } + + /// + /// Explicit conversion from an to a . + /// + /// The to convert. + public static explicit operator Color(HsvColor hsvColor) + { + return hsvColor.ToRgb(); + } + } +} From 6dd502b0bd2c65e8e6b9e298ec881f79a0225931 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 22:17:14 -0500 Subject: [PATCH 02/23] Add Empty support in Color --- src/Avalonia.Visuals/Media/Color.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 083c15cd13..074d98122a 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -14,6 +14,11 @@ namespace Avalonia.Media #endif readonly struct Color : IEquatable { + /// + /// Represents a null with zero set for all channels. + /// + public static readonly Color Empty = new Color(); + static Color() { #if !BUILDTASK @@ -41,6 +46,18 @@ namespace Avalonia.Media /// public byte B { get; } + /// + /// Gets a value indicating whether this is uninitialized and + /// considered null. + /// + public bool IsEmpty + { + get => A == 0 && + R == 0 && + G == 0 && + B == 0; + } + public Color(byte a, byte r, byte g, byte b) { A = a; @@ -272,9 +289,7 @@ namespace Avalonia.Media return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } - /// - /// Check if two colors are equal. - /// + /// public bool Equals(Color other) { return A == other.A && R == other.R && G == other.G && B == other.B; From c3d7577ef7ec6954817ef7108c9c78509761c66a Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 22:54:11 -0500 Subject: [PATCH 03/23] Add additional license comments for color conversion code --- src/Avalonia.Visuals/Media/HsvColor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 605d89346e..284dbc8d68 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -1,4 +1,9 @@ -using System; +// Color conversion portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; namespace Avalonia.Media { From 50e0547622515c197d10a953328ebf3215166f9f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 23:35:03 -0500 Subject: [PATCH 04/23] Use var per maintainer request Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com> --- src/Avalonia.Visuals/Media/HsvColor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 284dbc8d68..3e4e694426 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -248,8 +248,8 @@ namespace Avalonia.Media // // From these facts, you can see that we can retrieve the chroma by simply multiplying the saturation and the value, // and we can retrieve the minimum of the RGB channels by subtracting the chroma from the value. - double chroma = saturation * value; - double min = value - chroma; + var chroma = saturation * value; + var min = value - chroma; // If the chroma is zero, then we have a greyscale color. In that case, the maximum and the minimum RGB channels // have the same value (and, indeed, all of the RGB channels are the same), so we can just immediately return From f221afda13b648376d386bdb954872068d36ae71 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 23:35:16 -0500 Subject: [PATCH 05/23] Use var per maintainer request Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com> --- src/Avalonia.Visuals/Media/HsvColor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 3e4e694426..fadd1b20c7 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -390,7 +390,7 @@ namespace Avalonia.Media // then the chroma is maximized - this is a pure yellow, no gray of any kind. // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero // implies that this color is pure greyscale, with no actual hue to be found. - double chroma = max - min; + var chroma = max - min; // If the chrome is zero, then hue is technically undefined - a greyscale color // has no hue. For the sake of convenience, we'll just set hue to zero, since From 6f779152d3303656c214b8b06f0281666e6c44fd Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 23:55:05 -0500 Subject: [PATCH 06/23] Use MathUtilities.Clamp() for .NET Standard 2.0 support --- src/Avalonia.Visuals/Media/HsvColor.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index fadd1b20c7..39ddcf7de6 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -33,9 +34,9 @@ namespace Avalonia.Media double value) { A = 1.0; - H = Math.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); - S = Math.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); - V = Math.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); + H = MathUtilities.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); + S = MathUtilities.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); + V = MathUtilities.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); } /// @@ -51,10 +52,10 @@ namespace Avalonia.Media double value, double alpha) { - A = Math.Clamp((double.IsNaN(alpha) ? 1 : alpha), 0, 1); // Default to no transparency - H = Math.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); - S = Math.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); - V = Math.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); + A = MathUtilities.Clamp((double.IsNaN(alpha) ? 1 : alpha), 0, 1); // Default to no transparency + H = MathUtilities.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); + S = MathUtilities.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); + V = MathUtilities.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); } /// From 53ca7f89225f4b28c1b66ce41b8eb8536f33b33c Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 23:56:03 -0500 Subject: [PATCH 07/23] Use custom hash algo for .NET Standard 2.0 support --- src/Avalonia.Visuals/Media/HsvColor.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 39ddcf7de6..c46d3bf4b8 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -143,7 +143,16 @@ namespace Avalonia.Media /// The hashcode for this object. public override int GetHashCode() { - return HashCode.Combine(A, H, S, V); + // 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) ^ V.GetHashCode(); + return hashCode; + } } /// From fd2635aae43500ca898ba6fe562192091ef68f78 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Feb 2022 23:56:21 -0500 Subject: [PATCH 08/23] Readonly struct --- src/Avalonia.Visuals/Media/HsvColor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index c46d3bf4b8..fb431ae7d2 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// /// Defines a color using the hue/saturation/value (HSV) model. /// - public struct HsvColor : IEquatable + public readonly struct HsvColor : IEquatable { /// /// Represents a null with zero set for all channels. From 4f11cffa4a24716425e14cb3f6ef9a3c98feb570 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Feb 2022 13:04:18 -0500 Subject: [PATCH 09/23] Add Color.ToHsv() helper method --- src/Avalonia.Visuals/Media/Color.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 074d98122a..8421b952ac 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -289,6 +289,15 @@ namespace Avalonia.Media return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } + /// + /// Gets the HSV color model equivalent of this RGB color. + /// + /// The HSV equivalent color. + public HsvColor ToHsv() + { + return new HsvColor(this); + } + /// public bool Equals(Color other) { From dbd75d65719592a917bcaf6f3a411b37f4010fc3 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Feb 2022 13:05:31 -0500 Subject: [PATCH 10/23] Standardize comments --- src/Avalonia.Visuals/Media/Color.cs | 2 +- src/Avalonia.Visuals/Media/HsvColor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 8421b952ac..d59e2bf645 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -290,7 +290,7 @@ namespace Avalonia.Media } /// - /// Gets the HSV color model equivalent of this RGB color. + /// Returns the HSV color model equivalent of this RGB color. /// /// The HSV equivalent color. public HsvColor ToHsv() diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index fb431ae7d2..8d1a6e6d59 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -156,7 +156,7 @@ namespace Avalonia.Media } /// - /// Gets the RGB color model equivalent of this HSV color. + /// Returns the RGB color model equivalent of this HSV color. /// /// The RGB equivalent color. public Color ToRgb() From 2102e83a835cbf2c49523ea5fba2b5b60c35315a Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Feb 2022 16:17:57 -0500 Subject: [PATCH 11/23] Switch public/internal conditionally to match Color --- src/Avalonia.Visuals/Media/HsvColor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 8d1a6e6d59..36f8483230 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -11,7 +11,10 @@ namespace Avalonia.Media /// /// Defines a color using the hue/saturation/value (HSV) model. /// - public readonly struct HsvColor : IEquatable +#if !BUILDTASK + public +#endif + readonly struct HsvColor : IEquatable { /// /// Represents a null with zero set for all channels. From 2ba39784f1983bc5dd9d1144442fe2705c3d2876 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Feb 2022 16:37:29 -0500 Subject: [PATCH 12/23] Add HsvColor to Avalonia.Build.Tasks.csproj --- src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e864ea2007..5a7daa6d12 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) From 0f0b8aaf865bd6d74adc39a7c087a4fe879ace35 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Feb 2022 20:03:17 -0500 Subject: [PATCH 13/23] Add missing alpha clamping caught in review --- src/Avalonia.Visuals/Media/HsvColor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 36f8483230..708c77ec41 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -245,13 +245,16 @@ namespace Avalonia.Media hue += 360.0; } - // We similarly clamp saturation and value between 0 and 1. + // 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 first thing that we need to do is to determine the chroma (see above for its definition). // Remember from above that: // From 086d0c2b4cec7bedf85537331fac02ff6b45ddfd Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 1 Mar 2022 15:24:39 -0500 Subject: [PATCH 14/23] Optimizations --- src/Avalonia.Visuals/Media/Color.cs | 4 +++- src/Avalonia.Visuals/Media/HsvColor.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index d59e2bf645..16bcf127d5 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -295,7 +295,9 @@ namespace Avalonia.Media /// The HSV equivalent color. public HsvColor ToHsv() { - return new HsvColor(this); + // Use the by-channel conversion method directly for performance + // Don't use the HsvColor(Color) constructor to avoid an extra HsvColor + return HsvColor.FromRgb(R, G, B, A); } /// diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 708c77ec41..8e93f950fc 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -164,7 +164,8 @@ namespace Avalonia.Media /// The RGB equivalent color. public Color ToRgb() { - return HsvColor.ToRgb(this); + // Use the by-channel conversion method directly for performance + return HsvColor.ToRgb(H, S, V, A); } //////////////////////////////////////////////////////////////////////// From 836483d53c1b409d2154f765222022707a763a44 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 11:18:07 -0500 Subject: [PATCH 15/23] Optimization from review --- src/Avalonia.Visuals/Media/HsvColor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 8e93f950fc..7c06a08369 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -118,10 +118,10 @@ namespace Avalonia.Media /// public bool Equals(HsvColor other) { - if (object.Equals(other.A, A) == false) { return false; } - if (object.Equals(other.H, H) == false) { return false; } - if (object.Equals(other.S, S) == false) { return false; } - if (object.Equals(other.V, V) == false) { return false; } + if (other.A != A) { return false; } + if (other.H != H) { return false; } + if (other.S != S) { return false; } + if (other.V != V) { return false; } return true; } From 8cab8af039b46c387bfb59a5b592450bda379fe9 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 16:19:11 -0500 Subject: [PATCH 16/23] Remove Empty field and IsEmpty property from Color/HsvColor --- src/Avalonia.Visuals/Media/Color.cs | 17 ----------------- src/Avalonia.Visuals/Media/HsvColor.cs | 17 ----------------- 2 files changed, 34 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 16bcf127d5..e974bbb100 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -14,11 +14,6 @@ namespace Avalonia.Media #endif readonly struct Color : IEquatable { - /// - /// Represents a null with zero set for all channels. - /// - public static readonly Color Empty = new Color(); - static Color() { #if !BUILDTASK @@ -46,18 +41,6 @@ namespace Avalonia.Media /// public byte B { get; } - /// - /// Gets a value indicating whether this is uninitialized and - /// considered null. - /// - public bool IsEmpty - { - get => A == 0 && - R == 0 && - G == 0 && - B == 0; - } - public Color(byte a, byte r, byte g, byte b) { A = a; diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 7c06a08369..dc92639ee9 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -16,11 +16,6 @@ namespace Avalonia.Media #endif readonly struct HsvColor : IEquatable { - /// - /// Represents a null with zero set for all channels. - /// - public static readonly HsvColor Empty = new HsvColor(); - //////////////////////////////////////////////////////////////////////// // Constructors //////////////////////////////////////////////////////////////////////// @@ -99,18 +94,6 @@ namespace Avalonia.Media /// public double V { get; } - /// - /// Gets a value indicating whether this is uninitialized and - /// considered null. - /// - public bool IsEmpty - { - get => A == 0 && - H == 0 && - S == 0 && - V == 0; - } - //////////////////////////////////////////////////////////////////////// // Methods //////////////////////////////////////////////////////////////////////// From 3e5acdc2c66d2f60e140f8029d1d01d37cc7d08f Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 16:21:31 -0500 Subject: [PATCH 17/23] Remove section headings --- src/Avalonia.Visuals/Media/HsvColor.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index dc92639ee9..1e0af9f7dd 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -16,10 +16,6 @@ namespace Avalonia.Media #endif readonly struct HsvColor : IEquatable { - //////////////////////////////////////////////////////////////////////// - // Constructors - //////////////////////////////////////////////////////////////////////// - /// /// Initializes a new instance of the struct. /// @@ -70,10 +66,6 @@ namespace Avalonia.Media V = hsv.V; } - //////////////////////////////////////////////////////////////////////// - // Properties - //////////////////////////////////////////////////////////////////////// - /// /// Gets the Alpha (transparency) channel value in the range from 0..1. /// @@ -94,10 +86,6 @@ namespace Avalonia.Media /// public double V { get; } - //////////////////////////////////////////////////////////////////////// - // Methods - //////////////////////////////////////////////////////////////////////// - /// public bool Equals(HsvColor other) { @@ -151,10 +139,6 @@ namespace Avalonia.Media return HsvColor.ToRgb(H, S, V, A); } - //////////////////////////////////////////////////////////////////////// - // Static Methods - //////////////////////////////////////////////////////////////////////// - /// /// Creates a new from individual color channel values. /// @@ -460,10 +444,6 @@ namespace Avalonia.Media return new HsvColor(hue, saturation, value, a); } - //////////////////////////////////////////////////////////////////////// - // Operators - //////////////////////////////////////////////////////////////////////// - /// /// Indicates whether the values of two specified objects are equal. /// From b67c086f71bceded485b8051f852764110f3ab4a Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 16:25:37 -0500 Subject: [PATCH 18/23] Make alpha the first parameter in the constructor matching Color --- src/Avalonia.Visuals/Media/HsvColor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 1e0af9f7dd..f99abdc78d 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -36,15 +36,15 @@ namespace Avalonia.Media /// /// Initializes a new instance of the struct. /// + /// The Alpha (transparency) channel value in the range from 0..1. /// The Hue channel value in the range from 0..360. /// The Saturation channel value in the range from 0..1. /// The Value channel value in the range from 0..1. - /// The Alpha (transparency) channel value in the range from 0..1. public HsvColor( + double alpha, double hue, double saturation, - double value, - double alpha) + double value) { A = MathUtilities.Clamp((double.IsNaN(alpha) ? 1 : alpha), 0, 1); // Default to no transparency H = MathUtilities.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); From bdac3d08521f6a1a07ad25177b707e8d767f9d62 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 16:32:44 -0500 Subject: [PATCH 19/23] Fix parameter ordering --- src/Avalonia.Visuals/Media/HsvColor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index f99abdc78d..b7b98e4227 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -153,7 +153,7 @@ namespace Avalonia.Media /// A new built from the individual color channel values. public static HsvColor FromAhsv(double a, double h, double s, double v) { - return new HsvColor(h, s, v, a); + return new HsvColor(a, h, s, v); } /// @@ -169,7 +169,7 @@ namespace Avalonia.Media /// A new built from the individual color channel values. public static HsvColor FromHsv(double h, double s, double v) { - return new HsvColor(h, s, v, 1.0); + return new HsvColor(1.0, h, s, v); } /// @@ -441,7 +441,7 @@ namespace Avalonia.Media saturation = chroma / value; } - return new HsvColor(hue, saturation, value, a); + return new HsvColor(a, hue, saturation, value); } /// From 53237d2ef2f69c57bc4f32943066412c218c4e06 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 16:39:03 -0500 Subject: [PATCH 20/23] Remove IsNaN checks from constructor In XAML NaN is used in some cases so I wanted to cover this case. However, it isn't worth the performance cost in a constructor like this. NaN, Infinity, etc. will cause exceptions. --- src/Avalonia.Visuals/Media/HsvColor.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index b7b98e4227..0a2eb3a45c 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -27,10 +27,10 @@ namespace Avalonia.Media double saturation, double value) { - A = 1.0; - H = MathUtilities.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); - S = MathUtilities.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); - V = MathUtilities.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); + A = 1.0; // Default to no transparency + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + V = MathUtilities.Clamp(value, 0.0, 1.0); } /// @@ -46,10 +46,10 @@ namespace Avalonia.Media double saturation, double value) { - A = MathUtilities.Clamp((double.IsNaN(alpha) ? 1 : alpha), 0, 1); // Default to no transparency - H = MathUtilities.Clamp((double.IsNaN(hue) ? 0 : hue), 0, 360); - S = MathUtilities.Clamp((double.IsNaN(saturation) ? 0 : saturation), 0, 1); - V = MathUtilities.Clamp((double.IsNaN(value) ? 0 : value), 0, 1); + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + V = MathUtilities.Clamp(value, 0.0, 1.0); } /// From 2c59e1583982325ae82d37b83651c0ad5fd10aa7 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 2 Mar 2022 16:47:30 -0500 Subject: [PATCH 21/23] Add internal constructor that can skip parameter clamping --- src/Avalonia.Visuals/Media/HsvColor.cs | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 0a2eb3a45c..6c80fbd1c1 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -52,6 +52,41 @@ namespace Avalonia.Media V = MathUtilities.Clamp(value, 0.0, 1.0); } + /// + /// Initializes a new instance of the struct. + /// + /// + /// This constructor exists only for internal use where performance is critical. + /// Whether or not the channel values are in the correct ranges must be known. + /// + /// The Alpha (transparency) channel value in the range from 0..1. + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + /// Whether to clamp channel values to their required ranges. + internal HsvColor( + double alpha, + double hue, + double saturation, + double value, + 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); + V = MathUtilities.Clamp(value, 0.0, 1.0); + } + else + { + A = alpha; + H = hue; + S = saturation; + V = value; + } + } + /// /// Initializes a new instance of the struct. /// @@ -441,7 +476,7 @@ namespace Avalonia.Media saturation = chroma / value; } - return new HsvColor(a, hue, saturation, value); + return new HsvColor(a, hue, saturation, value, false); } /// From 58afdf7666983a544e819a25ddd143c6293396fe Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 7 Mar 2022 22:02:16 -0500 Subject: [PATCH 22/23] Remove constructor and method with only HSV channels Only variants with an alpha channel (AHSV) are supported. This is to avoid concerns about accidental parameter ordering and the API matches with Color. --- src/Avalonia.Visuals/Media/HsvColor.cs | 33 -------------------------- 1 file changed, 33 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 6c80fbd1c1..7ba4c54080 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -16,23 +16,6 @@ namespace Avalonia.Media #endif readonly struct HsvColor : IEquatable { - /// - /// Initializes a new instance of the struct. - /// - /// The Hue channel value in the range from 0..360. - /// The Saturation channel value in the range from 0..1. - /// The Value channel value in the range from 0..1. - public HsvColor( - double hue, - double saturation, - double value) - { - A = 1.0; // Default to no transparency - H = MathUtilities.Clamp(hue, 0.0, 360.0); - S = MathUtilities.Clamp(saturation, 0.0, 1.0); - V = MathUtilities.Clamp(value, 0.0, 1.0); - } - /// /// Initializes a new instance of the struct. /// @@ -191,22 +174,6 @@ namespace Avalonia.Media return new HsvColor(a, h, s, v); } - /// - /// Creates a new from individual color channel values. - /// - /// - /// This exists for symmetry with the struct; however, the - /// appropriate constructor should commonly be used instead. - /// - /// The Hue channel value in the range from 0..360. - /// The Saturation channel value in the range from 0..1. - /// The Value channel value in the range from 0..1. - /// A new built from the individual color channel values. - public static HsvColor FromHsv(double h, double s, double v) - { - return new HsvColor(1.0, h, s, v); - } - /// /// Converts the given HSV color to it's RGB color equivalent. /// From 5436786ad5c29372511fa3ad89dbd071cf5608cb Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 7 Mar 2022 22:03:24 -0500 Subject: [PATCH 23/23] Simplify --- src/Avalonia.Visuals/Media/HsvColor.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 7ba4c54080..8b2f10d088 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -107,12 +107,10 @@ namespace Avalonia.Media /// public bool Equals(HsvColor other) { - if (other.A != A) { return false; } - if (other.H != H) { return false; } - if (other.S != S) { return false; } - if (other.V != V) { return false; } - - return true; + return other.A == A && + other.H == H && + other.S == S && + other.V == V; } ///