diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs index 276ae7be..dafec310 100644 --- a/components/ColorPicker/src/ColorPicker.cs +++ b/components/ColorPicker/src/ColorPicker.cs @@ -510,12 +510,12 @@ private void UpdateColorControlValues() if (this.IsAlphaEnabled) { // Remove only the "#" sign - this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty); + this.HexInputTextBox.Text = rgbColor.ToString().TrimStart('#'); } else { // Remove the "#" sign and alpha hex - this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty).Substring(2); + this.HexInputTextBox.Text = rgbColor.ToString().TrimStart('#')[2..]; } } @@ -532,10 +532,10 @@ private void UpdateColorControlValues() int decimals = 0; hsvColor = new HsvColor() { - H = Math.Round(hsvColor.H, decimals), - S = Math.Round(hsvColor.S, 2 + decimals), - V = Math.Round(hsvColor.V, 2 + decimals), - A = Math.Round(hsvColor.A, 2 + decimals) + Hue = Math.Round(hsvColor.Hue, decimals), + Saturation = Math.Round(hsvColor.Saturation, 2 + decimals), + Value = Math.Round(hsvColor.Value, 2 + decimals), + Alpha = Math.Round(hsvColor.Alpha, 2 + decimals) }; // Must update HSV color @@ -554,10 +554,10 @@ private void UpdateColorControlValues() { this.ColorSpectrumControl.HsvColor = new System.Numerics.Vector4() { - X = Convert.ToSingle(hsvColor.H), - Y = Convert.ToSingle(hsvColor.S), - Z = Convert.ToSingle(hsvColor.V), - W = Convert.ToSingle(hsvColor.A) + X = Convert.ToSingle(hsvColor.Hue), + Y = Convert.ToSingle(hsvColor.Saturation), + Z = Convert.ToSingle(hsvColor.Value), + W = Convert.ToSingle(hsvColor.Alpha) }; } @@ -565,9 +565,9 @@ private void UpdateColorControlValues() if (this.ColorSpectrumThirdDimensionSlider != null) { // Convert the channels into a usable range for the user - double hue = hsvColor.H; - double staturation = hsvColor.S * 100; - double value = hsvColor.V * 100; + double hue = hsvColor.Hue; + double staturation = hsvColor.Saturation * 100; + double value = hsvColor.Value * 100; switch (this.GetActiveColorSpectrumThirdDimension()) { @@ -610,10 +610,10 @@ private void UpdateColorControlValues() if (this.GetActiveColorRepresentation() == ColorRepresentation.Hsva) { // Convert the channels into a usable range for the user - double hue = hsvColor.H; - double staturation = hsvColor.S * 100; - double value = hsvColor.V * 100; - double alpha = hsvColor.A * 100; + double hue = hsvColor.Hue; + double staturation = hsvColor.Saturation * 100; + double value = hsvColor.Value * 100; + double alpha = hsvColor.Alpha * 100; // Hue if (this.Channel1NumberBox != null) @@ -792,10 +792,10 @@ private void SetColorChannel( oldHsvColor = this.savedHsvColor.Value; } - double hue = oldHsvColor.H; - double saturation = oldHsvColor.S; - double value = oldHsvColor.V; - double alpha = oldHsvColor.A; + double hue = oldHsvColor.Hue; + double saturation = oldHsvColor.Saturation; + double value = oldHsvColor.Value; + double alpha = oldHsvColor.Alpha; switch (channel) { @@ -825,20 +825,11 @@ private void SetColorChannel( } } - newRgbColor = Helpers.ColorHelper.FromHsv( - hue, - saturation, - value, - alpha); + var hsv = HsvColor.Create(hue, saturation, value, alpha); + newRgbColor = hsv; // Must update HSV color - this.savedHsvColor = new HsvColor() - { - H = hue, - S = saturation, - V = value, - A = alpha - }; + this.savedHsvColor = hsv; this.savedHsvColorRgbEquivalent = newRgbColor; } else @@ -1351,16 +1342,14 @@ private void ColorModeComboBox_SelectionChanged(object sender, SelectionChangedE /// private void ColorPreviewer_ColorChangeRequested(object? sender, HsvColor hsvColor) { - Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); - // Regardless of the active color model, the previewer always uses HSV // Therefore, always calculate HSV color here // Warning: Always maintain/use HSV information in the saved HSV color // This avoids loss of precision and drift caused by continuously converting to/from RGB this.savedHsvColor = hsvColor; - this.savedHsvColorRgbEquivalent = rgbColor; + this.savedHsvColorRgbEquivalent = hsvColor; - this.ScheduleColorUpdate(rgbColor); + this.ScheduleColorUpdate(hsvColor); return; } diff --git a/components/ColorPicker/src/ColorPickerRenderingHelpers.cs b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs index 7ac31a24..59a91dfc 100644 --- a/components/ColorPicker/src/ColorPickerRenderingHelpers.cs +++ b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs @@ -79,23 +79,13 @@ public static async Task CreateChannelBitmapAsync( if (isAlphaMaxForced && channel != ColorChannel.Alpha) { - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = baseHsvColor.S, - V = baseHsvColor.V, - A = 1.0 - }; + baseHsvColor.Alpha = 1.0; } // Convert HSV to RGB once if (colorRepresentation == ColorRepresentation.Rgba) { - baseRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - baseHsvColor.S, - baseHsvColor.V, - baseHsvColor.A); + baseRgbColor = baseHsvColor; } // Maximize Saturation and Value channels when in HSVA mode @@ -106,31 +96,15 @@ public static async Task CreateChannelBitmapAsync( switch (channel) { case ColorChannel.Channel1: - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = 1.0, - V = 1.0, - A = baseHsvColor.A - }; + baseHsvColor.Saturation = 1.0; + baseHsvColor.Value = 1.0; break; case ColorChannel.Channel2: - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = baseHsvColor.S, - V = 1.0, - A = baseHsvColor.A - }; + + baseHsvColor.Value = 1; break; case ColorChannel.Channel3: - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = 1.0, - V = baseHsvColor.V, - A = baseHsvColor.A - }; + baseHsvColor.Saturation = 1; break; } } @@ -300,11 +274,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep hue - newRgbColor = Helpers.ColorHelper.FromHsv( - Math.Clamp(channelValue, 0.0, 360.0), - baseHsvColor.S, - baseHsvColor.V, - baseHsvColor.A); + var hsv = baseHsvColor; + hsv.Hue = channelValue; + newRgbColor = hsv; } else { @@ -326,11 +298,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep saturation - newRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - Math.Clamp(channelValue, 0.0, 1.0), - baseHsvColor.V, - baseHsvColor.A); + var hsv = baseHsvColor; + hsv.Saturation = channelValue; + newRgbColor = hsv; } else { @@ -352,11 +322,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep value - newRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - baseHsvColor.S, - Math.Clamp(channelValue, 0.0, 1.0), - baseHsvColor.A); + var hsv = baseHsvColor; + hsv.Value = channelValue; + newRgbColor = hsv; } else { @@ -378,11 +346,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep alpha - newRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - baseHsvColor.S, - baseHsvColor.V, - Math.Clamp(channelValue, 0.0, 1.0)); + var hsv = baseHsvColor; + hsv.Alpha = channelValue; + newRgbColor = hsv; } else { diff --git a/components/ColorPicker/src/ColorPickerSlider.cs b/components/ColorPicker/src/ColorPickerSlider.cs index 1bff1d26..98e97e82 100644 --- a/components/ColorPicker/src/ColorPickerSlider.cs +++ b/components/ColorPicker/src/ColorPickerSlider.cs @@ -55,7 +55,7 @@ public void UpdateColors() this.UpdateBackground(hsvColor); // Calculate and set the foreground ensuring contrast with the background - Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + Color rgbColor = hsvColor; Color selectedRgbColor; double sliderPercent = this.Value / (this.Maximum - this.Minimum); @@ -64,13 +64,7 @@ public void UpdateColors() if (this.IsAlphaMaxForced && this.ColorChannel != ColorChannel.Alpha) { - hsvColor = new HsvColor() - { - H = hsvColor.H, - S = hsvColor.S, - V = hsvColor.V, - A = 1.0 - }; + hsvColor.Alpha = 1.0; } switch (this.ColorChannel) @@ -78,51 +72,39 @@ public void UpdateColors() case ColorChannel.Channel1: { var channelValue = Math.Clamp(sliderPercent * 360.0, 0.0, 360.0); - - hsvColor = new HsvColor() + hsvColor.Hue = channelValue; + if (this.IsSaturationValueMaxForced) { - H = channelValue, - S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V, - A = hsvColor.A - }; + hsvColor.Saturation = 1.0; + hsvColor.Value = 1.0; + } break; } case ColorChannel.Channel2: { var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor() + hsvColor.Saturation = channelValue; + if (this.IsSaturationValueMaxForced) { - H = hsvColor.H, - S = channelValue, - V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V, - A = hsvColor.A - }; + hsvColor.Value = 1.0; + } break; } case ColorChannel.Channel3: { var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor() + hsvColor.Value = channelValue; + if (this.IsSaturationValueMaxForced) { - H = hsvColor.H, - S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - V = channelValue, - A = hsvColor.A - }; + hsvColor.Saturation = 1.0; + } break; } } - selectedRgbColor = Helpers.ColorHelper.FromHsv( - hsvColor.H, - hsvColor.S, - hsvColor.V, - hsvColor.A); + selectedRgbColor = hsvColor; } else { diff --git a/components/ColorPicker/src/Converters/AccentColorConverter.cs b/components/ColorPicker/src/Converters/AccentColorConverter.cs index 2c3204fd..785ff53b 100644 --- a/components/ColorPicker/src/Converters/AccentColorConverter.cs +++ b/components/ColorPicker/src/Converters/AccentColorConverter.cs @@ -33,22 +33,13 @@ public static HsvColor GetAccent(HsvColor hsvColor, int accentStep) { if (accentStep != 0) { - double colorValue = hsvColor.V; + double colorValue = hsvColor.Value; colorValue += accentStep * AccentColorConverter.ValueDelta; colorValue = Math.Round(colorValue, 2); - - return new HsvColor() - { - A = Math.Clamp(hsvColor.A, 0.0, 1.0), - H = Math.Clamp(hsvColor.H, 0.0, 360.0), - S = Math.Clamp(hsvColor.S, 0.0, 1.0), - V = Math.Clamp(colorValue, 0.0, 1.0), - }; - } - else - { - return hsvColor; + hsvColor.Value = colorValue; } + + return hsvColor; } /// @@ -63,28 +54,26 @@ public object Convert( HsvColor? hsvColor = null; // Get the current color in HSV - if (value is Color valueColor) - { - rgbColor = valueColor; - } - else if (value is HsvColor valueHsvColor) + switch (value) { - hsvColor = valueHsvColor; - } - else if (value is SolidColorBrush valueBrush) - { - rgbColor = valueBrush.Color; - } - else - { - // Invalid color value provided - return DependencyProperty.UnsetValue; + case Color valueColor: + rgbColor = valueColor; + break; + case HsvColor valueHsvColor: + hsvColor = valueHsvColor; + break; + case SolidColorBrush valueBrush: + rgbColor = valueBrush.Color; + break; + default: + // Invalid color value provided + return DependencyProperty.UnsetValue; } // Get the value component delta try { - accentStep = int.Parse(parameter?.ToString()!, CultureInfo.InvariantCulture); + accentStep = int.Parse(parameter?.ToString()!, CultureInfo.InvariantCulture); } catch { @@ -100,9 +89,7 @@ public object Convert( if (hsvColor != null) { - var hsv = AccentColorConverter.GetAccent(hsvColor.Value, accentStep); - - return Helpers.ColorHelper.FromHsv(hsv.H, hsv.S, hsv.V, hsv.A); + return (Color)GetAccent(hsvColor.Value, accentStep); } else { diff --git a/components/ColorPicker/src/Converters/ColorToHexConverter.cs b/components/ColorPicker/src/Converters/ColorToHexConverter.cs index 5eadf247..bfa5467d 100644 --- a/components/ColorPicker/src/Converters/ColorToHexConverter.cs +++ b/components/ColorPicker/src/Converters/ColorToHexConverter.cs @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.WinUI.Helpers; using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; + namespace CommunityToolkit.WinUI.Controls; /// @@ -21,22 +22,20 @@ public object Convert( { Color color; - if (value is Color valueColor) - { - color = valueColor; - } - else if (value is SolidColorBrush valueBrush) + switch (value) { - color = valueBrush.Color; - } - else - { - // Invalid color value provided - return DependencyProperty.UnsetValue; + case Color valueColor: + color = valueColor; + break; + case SolidColorBrush valueBrush: + color = valueBrush.Color; + break; + default: + // Invalid color value provided + return DependencyProperty.UnsetValue; } - string hexColor = color.ToHex().Replace("#", string.Empty); - return hexColor; + return color.ToString().TrimStart('#'); } /// @@ -48,29 +47,13 @@ public object ConvertBack( { string hexValue = value.ToString()!; - if (hexValue.StartsWith("#")) - { - try - { - return hexValue.ToColor(); - } - catch - { - // Invalid hex color value provided - return DependencyProperty.UnsetValue; - } - } - else - { - try - { - return ("#" + hexValue).ToColor(); - } - catch - { - // Invalid hex color value provided - return DependencyProperty.UnsetValue; - } - } + if (!hexValue.StartsWith('#')) + hexValue = $"#{hexValue}"; + + if (ColorHelper.TryParseHexColor(hexValue, out var color)) + return color; + + // Invalid hex color value provided + return DependencyProperty.UnsetValue; } } diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs new file mode 100644 index 00000000..fec8bcb0 --- /dev/null +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI; +using System.Globalization; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A class containing extensions methods for +/// +public static class ColorExtensions +{ + /// + /// Converts a to a premultiplied Int32 - 4 byte ARGB structure. + /// + /// The color to convert. + /// The int representation of the color. + public static int ToInt(this Color color) + { + var a = color.A + 1; + var col = (color.A << 24) | ((byte)((color.R * a) >> 8) << 16) | ((byte)((color.G * a) >> 8) << 8) | (byte)((color.B * a) >> 8); + return col; + } + + /// + /// Converts a to an . + /// + /// The to convert. + /// The converted . + public static HslColor ToHsl(this Color color) => (HslColor)color; + + /// + /// Converts a to an . + /// + /// The to convert. + /// The converted . + public static HsvColor ToHsv(this Color color) => (HsvColor)color; + + /// + public static Color AlphaOver(this Color @base, Color overlay) + => AlphaOver(@base, overlay, (double)overlay.A / 255); + + /// + /// Performs an AlphaOverlay between two colors. + /// + /// The base color. + /// The overlay color. + /// The alpha factor. + /// The resulting color from alpha overlaying over . + public static Color AlphaOver(this Color @base, Color overlay, double alpha) + { + // Find the color's mix, using the overlay's opacity as a factor + var mix = MixColors(@base, overlay, alpha); + + // Restore the alpha to match the base + mix.A = @base.A; + return mix; + } + + /// + /// Gets a color with the same saturation and value, but with an adjusted hue. + /// + /// The original color. + /// The new hue. + /// A with a new hue and the same saturation and value. + public static HsvColor WithHue(this Color original, double hue) + { + var hsv = (HsvColor)original; + hsv.Hue = hue; + return hsv; + } + + /// + /// Gets a color with the same hue and value, but with an adjusted saturation. + /// + /// The original color. + /// The new saturation. + /// A with a new saturation and the same hue and value. + public static HsvColor WithSaturation(this Color original, double saturation) + { + var hsv = (HsvColor)original; + hsv.Saturation = saturation; + return hsv; + } + + /// + /// Gets a color with the same hue and saturation, but with an adjusted saturation. + /// + /// The original color. + /// The new value. + /// A with a new value and the same hue and saturation. + public static HsvColor WithValue(this Color original, double value) + { + var hsv = (HsvColor)original; + hsv.Value = value; + return hsv; + } + + /// + /// Gets a color with the same hue and saturation, but with an adjusted lightness. + /// + /// The original color. + /// The new lightness. + /// A with a new lightness and the same hue and saturation. + public static HslColor WithLightness(this Color original, double lightness) + { + var hsl = (HslColor)original; + hsl.Lightness = lightness; + return hsl; + } + + private static Color MixColors(Color color1, Color color2, double factor) + { + factor = Math.Clamp(factor, 0, 1); + + // Formula for linearly blending a channel + var invFactor = 1 - factor; + byte Blend(byte c1, byte c2) => (byte)Math.Round((c1 * factor) + (c2 * invFactor)); + + if (factor is 0) + return color1; + + if (factor is 1) + return color2; + + // Blend each channel + var a = Blend(color1.A, color2.A); + var r = Blend(color1.R, color2.R); + var g = Blend(color1.G, color2.G); + var b = Blend(color1.B, color2.B); + + return Color.FromArgb(a, r, g, b); + } + +#if NET10_0_OR_GREATER + + extension(Color _) + { + /// + public static bool TryParseColor(string colorString, out Color color) => ColorHelper.TryParseColor(colorString, out color); + + /// + public static bool TryParseHexColor(string hexString, out Color color) => ColorHelper.TryParseHexColor(hexString, out color); + + /// + public static bool TryParseHslColor(string hslColor, out HslColor color) => ColorHelper.TryParseHslColor(hslColor, out color); + + /// + public static bool TryParseHsvColor(string hsvColor, out HsvColor color) => ColorHelper.TryParseHsvColor(hsvColor, out color); + + /// + public static bool TryParseScreenColor(string screenColor, out Color color) => ColorHelper.TryParseScreenColor(screenColor, out color); + + /// + public static bool TryParseColorName(string colorName, out Color color) => ColorHelper.TryParseColorName(colorName, out color); + + /// + public static Color ParseColor(string colorString) => ColorHelper.ParseColor(colorString); + + /// + public static Color ParseHexColor(string hexColor) => ColorHelper.ParseHexColor(hexColor); + + /// + public static HslColor ParseHslColor(string hslColor) => ColorHelper.ParseHslColor(hslColor); + + /// + public static HsvColor ParseHsvColor(string hsvColor) => ColorHelper.ParseHsvColor(hsvColor); + + /// + public static Color ParseScreenColor(string screenColor) => ColorHelper.ParseScreenColor(screenColor); + + /// + public static Color ParseColorName(string colorName) => ColorHelper.ParseColorName(colorName); + + /// + /// Gets a from alpha, hue, saturation, and lightness channel info. + /// + /// + /// This returns a in order to avoid unneccesary conversions. + /// However, the will be implicitly cast to a if needed. + /// + /// The color's hue. + /// The color's saturation. + /// The color's lightness. + /// The color's alpha value. + /// The color as a . + public static HslColor FromAhsl(double a, double h, double s, double l) => HslColor.Create(h, s, l, a); + + /// + /// Gets a from alpha, hue, saturation, and value channel info. + /// + /// + /// This returns a in order to avoid unneccesary conversions. + /// However, the will be implicitly cast to a if needed. + /// + /// The color's hue. + /// The color's saturation. + /// The color's value. + /// The color's alpha value. + /// The color as a . + public static HsvColor FromAhsv(double a, double h, double s, double v) => HsvColor.Create(h, s, v, a); + + /// + /// Mixes two colors using a factor for deciding how much influence each color has. + /// + /// The first color. + /// The second color. + /// The influence of each color, where 0 is entirely and 1 is entirely . + /// The mix of the two colors. + public static Color Mix(Color color1, Color color2, double factor) + => MixColors(color1, color2, factor); + + /// + /// Adds two colors. + /// + /// + /// Simple RGB summation, with each channel clamped seperately. Alpha is NOT included, and will always be opaque. + /// + /// The first color. + /// The second color. + /// The sume of the two colors. + public static Color Add(Color color1, Color color2) + { + static byte ClampedAdd(byte b1, byte b2) => (byte)int.Min(b1 + b2, 255); + + var r = ClampedAdd(color1.R, color2.R); + var g = ClampedAdd(color1.G, color2.G); + var b = ClampedAdd(color1.B, color2.B); + + return Color.FromArgb(255, r, g, b); + } + + /// + /// Subtracts a color from another. + /// + /// + /// Simple RGB subtraction, with each channel clamped seperately. Alpha is NOT included, and will always be opaque. + /// + /// The first color. + /// The second color. + /// The sume of the two colors. + public static Color Subtract(Color color1, Color color2) + { + static byte ClampedSubtract(byte b1, byte b2) => (byte)int.Max(b1 - b2, 0); + + var r = ClampedSubtract(color1.R, color2.R); + var g = ClampedSubtract(color1.G, color2.G); + var b = ClampedSubtract(color1.B, color2.B); + + return Color.FromArgb(255, r, g, b); + } + + /// + public static Color operator +(Color color1, Color color2) => Add(color1, color2); + + /// + public static Color operator -(Color color1, Color color2) => Subtract(color1, color2); + } + +#endif +} diff --git a/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs b/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs new file mode 100644 index 00000000..77b96dd4 --- /dev/null +++ b/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +public static partial class ColorHelper +{ + /// + /// Creates a from a XAML color string. + /// Any format used in XAML should work. + /// + /// The XAML color string. + /// The created . + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with ColorHelper.ParseColor(string colorString).")] + public static Color ToColor(this string colorString) => ParseColor(colorString); + + /// + /// Creates a from the specified hue, saturation, lightness, and alpha values. + /// + /// 0..360 range hue + /// 0..1 range saturation + /// 0..1 range lightness + /// 0..1 alpha + /// The created . + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with (Color)HslColor.Create().")] + public static Color FromHsl(double hue, double saturation, double lightness, double alpha = 1.0) + => HslColor.Create(hue, saturation, lightness, alpha); + + /// + /// Creates a from the specified hue, saturation, value, and alpha values. + /// + /// 0..360 range hue + /// 0..1 range saturation + /// 0..1 range value + /// 0..1 alpha + /// The created . + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with (Color)HsvColor.Create().")] + public static Color FromHsv(double hue, double saturation, double value, double alpha = 1.0) + => HsvColor.Create(hue, saturation, value, alpha); + + /// + /// Converts a to a hexadecimal string representation. + /// + /// The color to convert. + /// The hexadecimal string representation of the color. + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with .ToString().")] + public static string ToHex(this Color color) => color.ToString(); +} diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index 284d7d88..d03855e7 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Globalization; + #if WINAPPSDK using Microsoft.UI; #else @@ -15,287 +16,343 @@ namespace CommunityToolkit.WinUI.Helpers; /// /// This class provides static helper methods for colors. /// -public static class ColorHelper +public static partial class ColorHelper { /// - /// Creates a from a XAML color string. - /// Any format used in XAML should work. + /// Attempts to parse a string as a color. /// - /// The XAML color string. - /// The created . - public static Color ToColor(this string colorString) + /// + /// Any format used in XAML should work. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseColor(string colorString, out Color color) { + color = default; + if (string.IsNullOrEmpty(colorString)) - { - ThrowArgumentException(); - } + return false; - if (colorString[0] == '#') + // Try as hex + if (TryParseHexColor(colorString, out color)) + return true; + + // Try as screen color + if (TryParseScreenColor(colorString, out color)) + return true; + + // TODO: Should hsl and hsv be added? + + if (TryParseColorName(colorString, out color)) + return true; + + return false; + } + + /// + /// Attempts to parse a string as a hex color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseHexColor(string hexString, out Color color) + { + color = default; + + // Ensure string begins with '#' + // and is long enough to not break later in the parser. + if (string.IsNullOrEmpty(hexString) || + hexString.Length < 2 || + hexString[0] != '#') + return false; + + // Convert base 16 string to uint + if (!uint.TryParse(hexString[1..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cuint)) + return false; + + // Extract 4 bytes + byte b3 = (byte)(cuint >> 24); + byte b2 = (byte)((cuint >> 16) & 0xff); + byte b1 = (byte)((cuint >> 8) & 0xff); + byte b0 = (byte)(cuint & 0xff); + + // Extract 4 half-bytes + byte h3 = (byte)(cuint >> 12); + byte h2 = (byte)((cuint >> 8) & 0xf); + byte h1 = (byte)((cuint >> 4) & 0xf); + byte h0 = (byte)(cuint & 0xf); + h3 = (byte)(h3 << 4 | h3); + h2 = (byte)(h2 << 4 | h2); + h1 = (byte)(h1 << 4 | h1); + h0 = (byte)(h0 << 4 | h0); + + // Select bytes based on string length + switch (hexString.Length) { - switch (colorString.Length) - { - case 9: - { - var cuint = Convert.ToUInt32(colorString.Substring(1), 16); - var a = (byte)(cuint >> 24); - var r = (byte)((cuint >> 16) & 0xff); - var g = (byte)((cuint >> 8) & 0xff); - var b = (byte)(cuint & 0xff); - - return Color.FromArgb(a, r, g, b); - } - - case 7: - { - var cuint = Convert.ToUInt32(colorString.Substring(1), 16); - var r = (byte)((cuint >> 16) & 0xff); - var g = (byte)((cuint >> 8) & 0xff); - var b = (byte)(cuint & 0xff); - - return Color.FromArgb(255, r, g, b); - } - - case 5: - { - var cuint = Convert.ToUInt16(colorString.Substring(1), 16); - var a = (byte)(cuint >> 12); - var r = (byte)((cuint >> 8) & 0xf); - var g = (byte)((cuint >> 4) & 0xf); - var b = (byte)(cuint & 0xf); - a = (byte)(a << 4 | a); - r = (byte)(r << 4 | r); - g = (byte)(g << 4 | g); - b = (byte)(b << 4 | b); - - return Color.FromArgb(a, r, g, b); - } - - case 4: - { - var cuint = Convert.ToUInt16(colorString.Substring(1), 16); - var r = (byte)((cuint >> 8) & 0xf); - var g = (byte)((cuint >> 4) & 0xf); - var b = (byte)(cuint & 0xf); - r = (byte)(r << 4 | r); - g = (byte)(g << 4 | g); - b = (byte)(b << 4 | b); - - return Color.FromArgb(255, r, g, b); - } - - default: return ThrowFormatException(); - } + // #AaRrGgBb + case 9: + color = Color.FromArgb(b3, b2, b1, b0); + return true; + // #RrGgBb + case 7: + color = Color.FromArgb(255, b2, b1, b0); + return true; + // #ARGB + case 5: + color = Color.FromArgb(h3, h2, h1, h0); + return true; + // #RGB + case 4: + color = Color.FromArgb(255, h2, h1, h0); + return true; + // Invalid format + default: + return false; } + } - if (colorString.Length > 3 && colorString[0] == 's' && colorString[1] == 'c' && colorString[2] == '#') - { - var values = colorString.Split(','); + /// + /// Attempts to parse a string as a hsl color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseHslColor(string hslColor, out HslColor color) + { + color = default; + + if (!MatchArgPattern(hslColor, "hsl", out var args)) + return false; + + if (args.Length != 3) + return false; + + color = HslColor.Create(args[0], args[1], args[2]); + return true; + } + + /// + /// Attempts to parse a string as a hsv color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseHsvColor(string hsvColor, out HsvColor color) + { + color = default; + + if (!MatchArgPattern(hsvColor, "hsv", out var args)) + return false; + + if (args.Length != 3) + return false; + + color = HsvColor.Create(args[0], args[1], args[2]); + return true; + } - if (values.Length == 4) - { - var scA = double.Parse(values[0].Substring(3), CultureInfo.InvariantCulture); - var scR = double.Parse(values[1], CultureInfo.InvariantCulture); - var scG = double.Parse(values[2], CultureInfo.InvariantCulture); - var scB = double.Parse(values[3], CultureInfo.InvariantCulture); + /// + /// Attempts to parse a string as a screen color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseScreenColor(string screenColor, out Color color) + { + color = default; - return Color.FromArgb((byte)(scA * 255), (byte)(scR * 255), (byte)(scG * 255), (byte)(scB * 255)); - } + // Ensure the string begins with "sc#" + if (screenColor.Length < 3 || screenColor[0..3] != "sc#") + return false; - if (values.Length == 3) - { - var scR = double.Parse(values[0].Substring(3), CultureInfo.InvariantCulture); - var scG = double.Parse(values[1], CultureInfo.InvariantCulture); - var scB = double.Parse(values[2], CultureInfo.InvariantCulture); + // Get arguments + screenColor = screenColor[3..]; + var values = screenColor.Split(','); - return Color.FromArgb(255, (byte)(scR * 255), (byte)(scG * 255), (byte)(scB * 255)); - } + // Parse the arguments from string doubles into bytes + var args = new byte[values.Length]; + for (int i = 0; i < values.Length; i++) + { + if (!double.TryParse(values[i], out var arg)) + return false; - return ThrowFormatException(); + args[i] = (byte)(arg * 255); } - var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorString); + // Assign channel data based on arguments + switch (args.Length) + { + // sc#Alpha,Red,Green,Blue + case 4: + color = Color.FromArgb(args[0], args[1], args[2], args[3]); + return true; + // sc#Red,Green,Blue + case 3: + color = Color.FromArgb(255, args[0], args[1], args[2]); + return true; + // Invalid format + default: + return false; + } + } + /// + /// Attempts to parse a string color name. + /// + /// The color name. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseColorName(string colorName, out Color color) + { + color = default; + +#pragma warning disable CS8605 // Unboxing a possibly null value. + var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorName); if (prop != null) { -#pragma warning disable CS8605 // Unboxing a possibly null value. - return (Color)prop.GetValue(null); -#pragma warning restore CS8605 // Unboxing a possibly null value. + color = (Color)prop.GetValue(null); + return true; } +#pragma warning restore CS8605 // Unboxing a possibly null value. - return ThrowFormatException(); - - static void ThrowArgumentException() => throw new ArgumentException("The parameter \"colorString\" must not be null or empty."); - static Color ThrowFormatException() => throw new FormatException("The parameter \"colorString\" is not a recognized Color format."); + return false; } /// - /// Converts a to a hexadecimal string representation. + /// Creates a from a XAML color string. + /// Any format used in XAML should work. /// - /// The color to convert. - /// The hexadecimal string representation of the color. - public static string ToHex(this Color color) + /// The XAML color string. + /// The created . + public static Color ParseColor(string colorString) { - return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; + if (TryParseColor(colorString, out var color)) + return color; + + throw new FormatException($"The string '{colorString}' could not be parsed as a string."); } /// - /// Converts a to a premultiplied Int32 - 4 byte ARGB structure. + /// Parses a hexadecimal color string into a . /// - /// The color to convert. - /// The int representation of the color. - public static int ToInt(this Color color) + /// The hex color string. + /// The resulting . + public static Color ParseHexColor(string hexColor) { - var a = color.A + 1; - var col = (color.A << 24) | ((byte)((color.R * a) >> 8) << 16) | ((byte)((color.G * a) >> 8) << 8) | (byte)((color.B * a) >> 8); - return col; + if (TryParseHexColor(hexColor, out var color)) + return color; + + throw new FormatException($"The string '{hexColor}' could not be parsed as a hex color."); } + + /// + /// Parses a hsl color string into a . + /// + /// The hsl color string. + /// The resulting . + public static HslColor ParseHslColor(string hslColor) + { + if (TryParseHslColor(hslColor, out var color)) + return color; + throw new FormatException($"The string '{hslColor}' could not be parsed as a hsl color."); + } + /// - /// Converts a to an . + /// Parses a hsv color string into a . /// - /// The to convert. - /// The converted . - public static HslColor ToHsl(this Color color) + /// The hsv color string. + /// The resulting . + public static HsvColor ParseHsvColor(string hsvColor) { - const double toDouble = 1.0 / 255; - var r = toDouble * color.R; - var g = toDouble * color.G; - var b = toDouble * color.B; - var max = Math.Max(Math.Max(r, g), b); - var min = Math.Min(Math.Min(r, g), b); - var chroma = max - min; - double h1; + if (TryParseHsvColor(hsvColor, out var color)) + return color; - if (chroma == 0) - { - h1 = 0; - } - else if (max == r) - { - // The % operator doesn't do proper modulo on negative - // numbers, so we'll add 6 before using it - h1 = (((g - b) / chroma) + 6) % 6; - } - else if (max == g) - { - h1 = 2 + ((b - r) / chroma); - } - else - { - h1 = 4 + ((r - g) / chroma); - } + throw new FormatException($"The string '{hsvColor}' could not be parsed as a hsv color."); + } + + /// + /// Parses a screen color string into a . + /// + /// The screen color string. + /// The resulting . + public static Color ParseScreenColor(string screenColor) + { + if (TryParseScreenColor(screenColor, out var color)) + return color; - double lightness = 0.5 * (max + min); - double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); - HslColor ret; - ret.H = 60 * h1; - ret.S = saturation; - ret.L = lightness; - ret.A = toDouble * color.A; - return ret; + throw new FormatException($"The string '{screenColor}' is not a valid ScreenColor string"); } /// - /// Converts a to an . + /// Parses a color by name. /// - /// The to convert. - /// The converted . - public static HsvColor ToHsv(this Color color) + /// The color's name. + /// The resulting . + /// Throws if the color name is not recognized. + public static Color ParseColorName(string colorName) { - const double toDouble = 1.0 / 255; - var r = toDouble * color.R; - var g = toDouble * color.G; - var b = toDouble * color.B; - var max = Math.Max(Math.Max(r, g), b); - var min = Math.Min(Math.Min(r, g), b); + if (TryParseColorName(colorName, out var color)) + return color; + + throw new FormatException($"The name '{colorName}' not a valid color"); + } + + internal static (double h1, double chroma) CalculateHueAndChroma(Color color, out double min, out double max, out double alpha) + { + // This code is shared between both the conversion + // to both HSL and HSV from RGB. + + var r = (double)color.R / 255; + var g = (double)color.G / 255; + var b = (double)color.B / 255; + alpha = (double)color.A / 255; + + max = Math.Max(Math.Max(r, g), b); + min = Math.Min(Math.Min(r, g), b); var chroma = max - min; - double h1; + double h1; if (chroma == 0) { + // No max h1 = 0; } else if (max == r) { - // The % operator doesn't do proper modulo on negative - // numbers, so we'll add 6 before using it - h1 = (((g - b) / chroma) + 6) % 6; + // Red is max + h1 = ((g - b) / chroma + 6) % 6; } else if (max == g) { + // Green is max h1 = 2 + ((b - r) / chroma); } else { + // Blue is max h1 = 4 + ((r - g) / chroma); } - double saturation = chroma == 0 ? 0 : chroma / max; - HsvColor ret; - ret.H = 60 * h1; - ret.S = saturation; - ret.V = max; - ret.A = toDouble * color.A; - return ret; + return (h1, chroma); } - /// - /// Creates a from the specified hue, saturation, lightness, and alpha values. - /// - /// 0..360 range hue - /// 0..1 range saturation - /// 0..1 range lightness - /// 0..1 alpha - /// The created . - public static Color FromHsl(double hue, double saturation, double lightness, double alpha = 1.0) + internal static Color FromHueChroma(double h1, double chroma, double x, double m, double alpha) { - if (hue < 0 || hue > 360) - { - throw new ArgumentOutOfRangeException(nameof(hue)); - } + // This code is shared between both the conversion + // from both HSL and HSV to RGB. - 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, g1, b1) = h1 switch { - r1 = x; - g1 = 0; - b1 = chroma; - } - else - { - r1 = chroma; - g1 = 0; - b1 = x; - } + < 1 => (chroma, x, 0d), + < 2 => (x, chroma, 0d), + < 3 => (0d, chroma, x), + < 4 => (0d, x, chroma), + < 5 => (x, 0d, chroma), + _ => (chroma, 0d, x), + }; byte r = (byte)(255 * (r1 + m)); byte g = (byte)(255 * (g1 + m)); @@ -306,68 +363,39 @@ public static Color FromHsl(double hue, double saturation, double lightness, dou } /// - /// Creates a from the specified hue, saturation, value, and alpha values. + /// Parses a string to match the argument pattern "(args[0], args[1], ...)" /// - /// 0..360 range hue - /// 0..1 range saturation - /// 0..1 range value - /// 0..1 alpha - /// The created . - public static Color FromHsv(double hue, double saturation, double value, double alpha = 1.0) + private static bool MatchArgPattern(string value, string funcName, out T?[] args) + where T : IParsable { - if (hue < 0 || hue > 360) - { - throw new ArgumentOutOfRangeException(nameof(hue)); - } + args = []; - double chroma = value * saturation; - double h1 = hue / 60; - double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); - double m = value - chroma; - double r1, g1, b1; + // Find opening and closing parenthesis + var argsStart = value.IndexOf('('); + var argsEnd = value.Length - 1; + if (argsStart is -1) + return false; - 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 + // Ensure the string begins with the function name + if (value[0..argsStart] != funcName) + return false; + + // Ensure the string ends with ')' + if (value[argsEnd] != ')') + return false; + + // Split to substring between the parenthesis + value = value[(argsStart + 1)..argsEnd]; + var argStrings = value.Split(','); + + // Parse the args into an array of doubles + args = new T[argStrings.Length]; + for (var i = 0; i < argStrings.Length; i++) { - r1 = chroma; - g1 = 0; - b1 = x; + if (!T.TryParse(argStrings[i], null, out args[i])) + return false; } - byte r = (byte)(255 * (r1 + m)); - byte g = (byte)(255 * (g1 + m)); - byte b = (byte)(255 * (b1 + m)); - byte a = (byte)(255 * alpha); - - return Color.FromArgb(a, r, g, b); + return true; } } diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index c69dbdeb..6638fe3d 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -2,30 +2,167 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; + namespace CommunityToolkit.WinUI; /// -/// Defines a color in Hue/Saturation/Lightness (HSL) space. +/// Defines a color in Hue/Saturation/Lightness (HSL) space with alpha. /// public struct HslColor { /// - /// The Hue in 0..360 range. + /// The hue value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Hue property")] public double H; /// - /// The Saturation in 0..1 range. + /// The saturation value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Saturation property")] public double S; /// - /// The Lightness in 0..1 range. + /// The lightness value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Lightness property")] public double L; /// - /// The Alpha/opacity in 0..1 range. + /// The alpha/opacity value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Alpha property")] public double A; + + /// + /// Initializes a new instance of the struct. + /// + /// The to convert to a . + private HslColor(Color color) + { + // Calculate hue, chroma, and supplementary values + (double h1, double chroma) = ColorHelper.CalculateHueAndChroma(color, out var min, out var max, out var alpha); + + // Calculate saturation and lightness + double lightness = 0.5 * (max + min); + double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); + + // Set hsl properties + Hue = 60 * h1; + Saturation = saturation; + Lightness = lightness; + Alpha = alpha; + } + + /// + /// Creates a new . + /// + /// The color's hue. + /// The color's saturation. + /// The color's lightness. + /// The alpha/opacity. + /// The new . + public static HslColor Create(double hue, double saturation, double lightness, double alpha = 1) + { + HslColor color = default; +#pragma warning disable 0618 + color.H = hue; + color.S = saturation; + color.L = lightness; + color.A = alpha; +#pragma warning restore 0618 + return color; + } + + // This class contains deprecated public backing fields to be removed in a future version. + // Suppress the warnings from using them in their new wrapping properties. +#pragma warning disable 0618 + + /// + /// Gets or sets the hue. + /// + /// + /// This value is clamped between 0 and 360. + /// + public double Hue + { + readonly get => Math.Clamp(H, 0, 360); + set => H = Math.Clamp(value, 0, 360); + } + + /// + /// Gets or sets the saturation. + /// + /// + /// This value is clamped between 0 and 1. + /// + public double Saturation + { + readonly get => Math.Clamp(S, 0, 1); + set => S = Math.Clamp(value, 0, 1); + } + + /// + /// Gets or sets the lightness. + /// + /// + /// This value is clamped between 0 and 1. + /// + public double Lightness + { + readonly get => Math.Clamp(L, 0, 1); + set => L = Math.Clamp(value, 0, 1); + } + + /// + /// Gets or sets the alpha/opacity. + /// + /// + /// This value is clamped between 0 and 1. + /// + public double Alpha + { + readonly get => Math.Clamp(A, 0, 1); + set => A = Math.Clamp(value, 0, 1); + } + +#pragma warning restore 0618 + + /// + /// Converts the to a . + /// + /// The as a . + public readonly Color ToColor() + { + 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); + + return ColorHelper.FromHueChroma(h1, chroma, x, m, Alpha); + } + + /// + public override readonly string ToString() => $"hsl({Hue:N0}, {Saturation}, {Lightness})"; + + /// + /// Cast a to a . + /// + public static implicit operator Color(HslColor hsl) => hsl.ToColor(); + + /// + /// Cast a to + /// + public static explicit operator HslColor(Color color) => new(color); + + /// + /// Cast a to + /// + public static explicit operator HslColor(HsvColor color) => new(color); } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index f30f72a6..0e185cf2 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -2,30 +2,169 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; + namespace CommunityToolkit.WinUI; /// -/// Defines a color in Hue/Saturation/Value (HSV) space. +/// Defines a color in Hue/Saturation/Value (HSV) space with alpha. /// public struct HsvColor { /// - /// The Hue in 0..360 range. + /// The hue value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Hue property")] public double H; /// - /// The Saturation in 0..1 range. + /// The saturation value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Saturation property")] public double S; /// - /// The Value in 0..1 range. + /// The "value" value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Value property")] public double V; /// - /// The Alpha/opacity in 0..1 range. + /// The alpha/opacity value. /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Alpha property")] public double A; + + /// + /// Initializes a new instance of the struct. + /// + /// The to convert to a . + private HsvColor(Color color) + { + // Calculate hue, chroma, and supplementary values + (double h1, double chroma) = ColorHelper.CalculateHueAndChroma(color, out var _, out var max, out var alpha); + + // Calculate saturation and value + double saturation = chroma == 0 ? 0 : chroma / max; + double value = max; + + // Set hsv properties + Hue = 60 * h1; + Saturation = saturation; + Value = value; + Alpha = alpha; + } + + /// + /// Creates a new . + /// + /// The color's hue. + /// The color's saturation. + /// The color's value. + /// The alpha/opacity. + /// The new . + public static HsvColor Create(double hue, double saturation, double value, double alpha = 1) + { + HsvColor color = default; +#pragma warning disable 0618 + color.H = hue; + color.S = saturation; + color.V = value; + color.A = alpha; +#pragma warning restore 0618 + return color; + } + + + // This class contains deprecated public backing fields to be removed in a future version. + // Suppress the warnings from using them in their new wrapping properties. +#pragma warning disable 0618 + + /// + /// Gets or sets the hue. + /// + /// + /// This value is clamped between 0 and 360. + /// + public double Hue + { + readonly get => Math.Clamp(H, 0, 360); + set => H = Math.Clamp(value, 0, 360); + } + + /// + /// Gets or sets the saturation. + /// + /// + /// This value is clamped between 0 and 1. + /// + public double Saturation + { + readonly get => Math.Clamp(S, 0, 1); + set => S = Math.Clamp(value, 0, 1); + } + + /// + /// Gets or sets the color value. + /// + /// + /// This value is clamped between 0 and 1. + /// + public double Value + { + readonly get => Math.Clamp(V, 0, 1); + set => V = Math.Clamp(value, 0, 1); + } + + /// + /// Gets or sets the alpha/opacity. + /// + /// + /// This value is clamped between 0 and 1. + /// + public double Alpha + { + readonly get => Math.Clamp(A, 0, 1); + set => A = Math.Clamp(value, 0, 1); + } + +#pragma warning restore 0618 + + + /// + /// Converts the to a . + /// + /// The as a . + public readonly Color ToColor() + { + double chroma = Value * Saturation; + double h1 = Hue / 60; + double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); + double m = Value - chroma; + + return ColorHelper.FromHueChroma(h1, chroma, x, m, Alpha); + } + + /// + public override readonly string ToString() => $"hsv({Hue:N0}, {Saturation}, {Value})"; + + /// + /// Cast a to a . + /// + public static implicit operator Color(HsvColor hsv) => hsv.ToColor(); + + /// + /// Cast a to + /// + public static explicit operator HsvColor(Color color) => new(color); + + /// + /// Cast a to + /// + public static explicit operator HsvColor(HslColor color) => new(color); } diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index 94821501..e6cd1c21 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -3,59 +3,93 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.WinUI.Helpers; +using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; namespace HelpersTests; [TestClass] public class Test_ColorHelper { + // Keep testing the old APIs until they are removed + // In the meantime suppress the warnings from using them +#pragma warning disable 0618 + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_ParseColor_Predifined() + { + Assert.AreEqual(ColorHelper.ParseColor("Red"), Colors.Red); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_ParseColor_Hex8Digits() + { + Assert.AreEqual(ColorHelper.ParseColor("#FFFF0000"), Colors.Red); + } + [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Predifined() + public void Test_ColorHelper_ParseColor_Hex6Digits() { - Assert.AreEqual("Red".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#FF0000"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex8Digits() + public void Test_ColorHelper_ParseColor_Hex4Digits() { - Assert.AreEqual("#FFFF0000".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#FF00"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex6Digits() + public void Test_ColorHelper_ParseColor_Hex3Digits() { - Assert.AreEqual("#FF0000".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#F00"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex4Digits() + public void Test_ColorHelper_ParseColor_ScreenColor() { - Assert.AreEqual("#FF00".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("sc#1.0,1.0,0,0"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex3Digits() + public void Test_ColorHelper_ParseHslColor() { - Assert.AreEqual("#F00".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseHslColor("hsl(0,1,0.5)"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_ScreenColor() + public void Test_ColorHelper_ParseHsvColor() { - Assert.AreEqual("sc#1.0,1.0,0,0".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseHsvColor("hsv(0,1,1)"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToHex() { - Assert.AreEqual(Colors.Red.ToHex(), "#FFFF0000"); + Assert.AreEqual(Colors.Red.ToString(), "#FFFF0000"); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_HslToString() + { + Assert.AreEqual(((HslColor)Colors.Red).ToString(), "hsl(0, 1, 0.5)"); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_HsvToString() + { + Assert.AreEqual(((HsvColor)Colors.Red).ToString(), "hsv(0, 1, 1)"); } [TestCategory("Helpers")] @@ -69,7 +103,7 @@ public void Test_ColorHelper_ToInt() [TestMethod] public void Test_ColorHelper_ToHsl() { - HslColor hslRed; + HslColor hslRed = default; hslRed.A = 1.0; // Alpha hslRed.H = 0.0; // Hue hslRed.S = 1.0; // Saturation @@ -82,7 +116,7 @@ public void Test_ColorHelper_ToHsl() [TestMethod] public void Test_ColorHelper_ToHsl_White() { - HslColor hslWhite; + HslColor hslWhite = default; hslWhite.A = 1.0; // Alpha hslWhite.H = 0.0; // Hue hslWhite.S = 0.0; // Saturation @@ -96,7 +130,7 @@ public void Test_ColorHelper_ToHsl_White() public void Test_ColorHelper_ToHsl_MaxR() { // Test when given an RGB value where R is the max value. - HslColor hslColor; + HslColor hslColor = default; hslColor.A = 1.0; // Alpha hslColor.H = 330.0; // Hue hslColor.S = 1.0; // Saturation @@ -114,11 +148,13 @@ public void Test_ColorHelper_ToHsl_MaxR() [TestMethod] public void Test_ColorHelper_ToHsv() { - HsvColor hsvColor; - hsvColor.A = 1.0; // Alpha - hsvColor.H = 100; // Hue - hsvColor.S = 0.25; // Saturation - hsvColor.V = 0.80; // Value + HsvColor hsvColor = new HsvColor + { + A = 1.0, // Alpha + H = 100, // Hue + S = 0.25, // Saturation + V = 0.80, // Value + }; // Use a test color with non-zero/non-max values for both RGB and HSV var color = Windows.UI.Color.FromArgb(255, 170, 204, 153).ToHsv(); @@ -136,11 +172,13 @@ public void Test_ColorHelper_ToHsv() [TestMethod] public void Test_ColorHelper_ToHsv_MaxR() { - HsvColor hsvColor; - hsvColor.A = 1.0; // Alpha - hsvColor.H = 330; // Hue - hsvColor.S = 0.58823529; // Saturation - hsvColor.V = 1; // Value + HsvColor hsvColor = new HsvColor + { + A = 1.0, // Alpha + H = 330, // Hue + S = 0.58823529, // Saturation + V = 1, // Value + }; // Use a test color with non-zero/non-max values for both RGB and HSV var color = Windows.UI.Color.FromArgb(255, 255, 105, 180).ToHsv(); @@ -156,15 +194,78 @@ public void Test_ColorHelper_ToHsv_MaxR() [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_FromHsl() + public void Test_ColorHelper_CreateHsl() + { + Assert.AreEqual(HslColor.Create(0.0, 1.0, 0.5), Colors.Red); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_CreateHsv() + { + Assert.AreEqual(HsvColor.Create(0.0, 1.0, 1.0), Colors.Red); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_AlphaOver() { - Assert.AreEqual(CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(0.0, 1.0, 0.5), Colors.Red); + Assert.AreEqual(Colors.Red.AlphaOver(Colors.Blue, 0.5), Color.FromArgb(255, 128, 0, 128)); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_FromHsv() + public void Test_ColorHelper_WithHue() { - Assert.AreEqual(CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsv(0.0, 1.0, 1.0), Colors.Red); + Assert.AreEqual(Colors.Blue.WithHue(0), Colors.Red); } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithSaturation() + { + Assert.AreEqual(Colors.Red.WithSaturation(0), Colors.White); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithValue() + { + Assert.AreEqual(Colors.Red.WithValue(0), Colors.Black); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithLightness() + { + Assert.AreEqual(Colors.Red.WithLightness(0), Colors.Black); + } + +#if NET10_0_OR_GREATER + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Mix() + { + Assert.AreEqual(Color.Mix(Colors.White, Colors.Black, 0.6625), Colors.DarkGray); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Add() + { + Assert.AreEqual(Color.Add(Colors.Red, Colors.Blue), Colors.Magenta); + Assert.AreEqual(Colors.Red + Colors.Blue, Colors.Magenta); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Subtract() + { + Assert.AreEqual(Color.Subtract(Colors.Magenta, Colors.Red), Colors.Blue); + Assert.AreEqual(Colors.Magenta - Colors.Red, Colors.Blue); + } + +#endif +#pragma warning restore 0618 }