From 1d0668a1846266d9cfb75516b7379852789fcb9f Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 9 Apr 2022 00:07:58 -0400 Subject: [PATCH] Move RgbToHsv conversion into Color All color types will ONLY have .ToModel() conversion methods. .FromModel() conversion will not be provided. This unifies the API and allows easily extending with more color models in the future without breaking things. Each type is responsible to convert itself to other supported types -- direction is one way in the API. --- src/Avalonia.Visuals/Media/Color.cs | 133 ++++++++++++++++++++++++- src/Avalonia.Visuals/Media/HsvColor.cs | 132 +----------------------- 2 files changed, 136 insertions(+), 129 deletions(-) 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. ///