diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 84fde9bfc2..107bfc7a8e 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -1,3 +1,10 @@ +// Color conversion portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// and 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; #if !BUILDTASK @@ -280,7 +287,7 @@ namespace Avalonia.Media { // Use the by-component conversion method directly for performance // Don't use the HsvColor(Color) constructor to avoid an extra HsvColor - return HsvColor.FromRgb(R, G, B, A); + return Color.ToHsv(R, G, B, A); } /// @@ -308,6 +315,130 @@ namespace Avalonia.Media } } + /// + /// 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 ToHsv(Color color) + { + return Color.ToHsv(color.R, color.G, color.B, color.A); + } + + /// + /// Converts the given RGBA color component values to its HSV color equivalent. + /// + /// The red component in the RGB color model. + /// The green component in the RGB color model. + /// The blue component in the RGB color model. + /// The alpha component. + /// A new equivalent to the given RGBA values. + public static HsvColor ToHsv( + 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 support was added + + // Normalize RGBA components 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. + 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 + // 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(a, hue, saturation, value, false); + } + /// /// Indicates whether the values of two specified objects are equal. /// diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 6a27c39c9b..fcfd6a7040 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -1,6 +1,6 @@ -// Color conversion portions of this source file are adapted from the WinUI project. -// (https://github.com/microsoft/microsoft-ui-xaml) -// +// 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; @@ -89,7 +89,7 @@ namespace Avalonia.Media /// The RGB color to convert to HSV. public HsvColor(Color color) { - var hsv = HsvColor.FromRgb(color); + var hsv = Color.ToHsv(color); A = hsv.A; H = hsv.H; @@ -473,130 +473,6 @@ namespace Avalonia.Media (byte)(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 component values to its HSV color equivalent. - /// - /// The red component in the RGB color model. - /// The green component in the RGB color model. - /// The blue component in the RGB color model. - /// The alpha component. - /// 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 support was added - - // Normalize RGBA components 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. - 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 - // 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(a, hue, saturation, value, false); - } - /// /// Indicates whether the values of two specified objects are equal. ///