Browse Source

Merge pull request #7951 from robloo/css-colors

Adds new HslColor struct and Support for CSS Color Formats
pull/7962/head
Jumar Macato 5 years ago
committed by GitHub
parent
commit
2f534072f0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/ControlCatalog/Pages/CanvasPage.xaml
  2. 3
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  3. 442
      src/Avalonia.Visuals/Media/Color.cs
  4. 476
      src/Avalonia.Visuals/Media/HslColor.cs
  5. 259
      src/Avalonia.Visuals/Media/HsvColor.cs
  6. 124
      tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs

2
samples/ControlCatalog/Pages/CanvasPage.xaml

@ -14,7 +14,7 @@
</LinearGradientBrush>
</Rectangle.OpacityMask>
</Rectangle>
<Rectangle Fill="Green" Stroke="Black" StrokeThickness="2" Width="40" Height="20" Canvas.Left="150" Canvas.Top="10" RadiusX="10" RadiusY="5" />
<Rectangle Fill="hsva(240, 83%, 73%, 90%)" Stroke="hsl(5, 85%, 85%)" StrokeThickness="2" Width="40" Height="20" Canvas.Left="150" Canvas.Top="10" RadiusX="10" RadiusY="5" />
<Ellipse Fill="Green" Width="58" Height="58" Canvas.Left="88" Canvas.Top="100"/>
<Path Fill="Orange" Data="M 0,0 c 0,0 50,0 50,-50 c 0,0 50,0 50,50 h -50 v 50 l -50,-50 Z" Canvas.Left="30" Canvas.Top="250"/>
<Path Fill="OrangeRed" Canvas.Left="180" Canvas.Top="250">

3
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -83,6 +83,9 @@
<Compile Include="../Avalonia.Visuals/Media/Color.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Visuals/Media/HslColor.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Visuals/Media/HsvColor.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>

442
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
@ -14,6 +21,8 @@ namespace Avalonia.Media
#endif
readonly struct Color : IEquatable<Color>
{
private const double byteToDouble = 1.0 / 255;
static Color()
{
#if !BUILDTASK
@ -41,6 +50,13 @@ namespace Avalonia.Media
/// </summary>
public byte B { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="a">The alpha component.</param>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
public Color(byte a, byte r, byte g, byte b)
{
A = a;
@ -144,17 +160,46 @@ namespace Avalonia.Media
return false;
}
if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
if (s[0] == '#' &&
TryParseHexFormat(s.AsSpan(), out color))
{
return true;
}
if (s.Length > 5 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
TryParseCssFormat(s, out color))
{
return true;
}
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
HslColor.TryParse(s, out HslColor hslColor))
{
color = hslColor.ToRgb();
return true;
}
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
HsvColor.TryParse(s, out HsvColor hsvColor))
{
color = hsvColor.ToRgb();
return true;
}
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
@ -172,21 +217,52 @@ namespace Avalonia.Media
if (s.Length == 0)
{
color = default;
return false;
}
if (s[0] == '#')
if (s[0] == '#' &&
TryParseHexFormat(s, out color))
{
return true;
}
// At this point all parsing uses strings
var str = s.ToString();
if (s.Length > 5 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
TryParseCssFormat(str, out color))
{
return true;
}
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
HslColor.TryParse(str, out HslColor hslColor))
{
return TryParseInternal(s, out color);
color = hslColor.ToRgb();
return true;
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
HsvColor.TryParse(str, out HsvColor hsvColor))
{
color = hsvColor.ToRgb();
return true;
}
var knownColor = KnownColors.GetKnownColor(str);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
@ -195,7 +271,7 @@ namespace Avalonia.Media
return false;
}
private static bool TryParseInternal(ReadOnlySpan<char> s, out Color color)
private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
{
@ -249,6 +325,91 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
private static bool TryParseCssFormat(string s, out Color color)
{
color = default;
if (s is null)
{
return false;
}
string workingString = s.Trim();
if (workingString.Length == 0 ||
workingString.IndexOf(",", StringComparison.Ordinal) < 0)
{
return false;
}
if (workingString.Length > 6 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
}
if (workingString.Length > 5 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
}
string[] components = workingString.Split(',');
if (components.Length == 3) // RGB
{
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue))
{
color = new Color(0xFF, red, green, blue);
return true;
}
}
else if (components.Length == 4) // RGBA
{
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) &&
TryInternalParse(components[3], out double alpha))
{
color = new Color((byte)(alpha * 255), red, green, blue);
return true;
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0;
return result;
}
else
{
return double.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble);
}
}
return false;
}
/// <summary>
/// Returns the string representation of the color.
/// </summary>
@ -272,15 +433,24 @@ namespace Avalonia.Media
return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
}
/// <summary>
/// Returns the HSL color model equivalent of this RGB color.
/// </summary>
/// <returns>The HSL equivalent color.</returns>
public HslColor ToHsl()
{
// Don't use the HslColor(Color) constructor to avoid an extra HslColor
return Color.ToHsl(R, G, B, A);
}
/// <summary>
/// Returns the HSV color model equivalent of this RGB color.
/// </summary>
/// <returns>The HSV equivalent color.</returns>
public HsvColor ToHsv()
{
// 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);
return Color.ToHsv(R, G, B, A);
}
/// <inheritdoc/>
@ -289,11 +459,13 @@ namespace Avalonia.Media
return A == other.A && R == other.R && G == other.G && B == other.B;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Color other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
@ -306,11 +478,261 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Converts the given RGB color to its HSL color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
public static HslColor ToHsl(Color color)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsl(
(byteToDouble * color.R),
(byteToDouble * color.G),
(byteToDouble * color.B),
(byteToDouble * color.A));
}
/// <summary>
/// Converts the given RGBA color component values to their HSL color equivalent.
/// </summary>
/// <param name="red">The Red component in the RGB color model.</param>
/// <param name="green">The Green component in the RGB color model.</param>
/// <param name="blue">The Blue component in the RGB color model.</param>
/// <param name="alpha">The Alpha component.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
public static HslColor ToHsl(
byte red,
byte green,
byte blue,
byte alpha = 0xFF)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsl(
(byteToDouble * red),
(byteToDouble * green),
(byteToDouble * blue),
(byteToDouble * alpha));
}
/// <summary>
/// Converts the given RGBA color component values to their HSL color equivalent.
/// </summary>
/// <remarks>
/// Warning: No bounds checks or clamping is done on the input component values.
/// This method is for internal-use only and the caller must ensure bounds.
/// </remarks>
/// <param name="r">The Red component in the RGB color model within the range 0..1.</param>
/// <param name="g">The Green component in the RGB color model within the range 0..1.</param>
/// <param name="b">The Blue component in the RGB color model within the range 0..1.</param>
/// <param name="a">The Alpha component in the RGB color model within the range 0..1.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
internal static HslColor ToHsl(
double r,
double g,
double b,
double a = 1.0)
{
// Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
// It has been modified.
double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
double chroma = max - min;
double h1;
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);
}
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false);
}
/// <summary>
/// Converts the given RGB color to its HSV color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor ToHsv(Color color)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsv(
(byteToDouble * color.R),
(byteToDouble * color.G),
(byteToDouble * color.B),
(byteToDouble * color.A));
}
/// <summary>
/// Converts the given RGBA color component values to their HSV color equivalent.
/// </summary>
/// <param name="red">The Red component in the RGB color model.</param>
/// <param name="green">The Green component in the RGB color model.</param>
/// <param name="blue">The Blue component in the RGB color model.</param>
/// <param name="alpha">The Alpha component.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor ToHsv(
byte red,
byte green,
byte blue,
byte alpha = 0xFF)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsv(
(byteToDouble * red),
(byteToDouble * green),
(byteToDouble * blue),
(byteToDouble * alpha));
}
/// <summary>
/// Converts the given RGBA color component values to their HSV color equivalent.
/// </summary>
/// <remarks>
/// Warning: No bounds checks or clamping is done on the input component values.
/// This method is for internal-use only and the caller must ensure bounds.
/// </remarks>
/// <param name="r">The Red component in the RGB color model within the range 0..1.</param>
/// <param name="g">The Green component in the RGB color model within the range 0..1.</param>
/// <param name="b">The Blue component in the RGB color model within the range 0..1.</param>
/// <param name="a">The Alpha component in the RGB color model within the range 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
internal static HsvColor ToHsv(
double r,
double g,
double b,
double a = 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 support was added
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, clampValues: false);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="Color"/> objects are equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are equal; otherwise, false.</returns>
public static bool operator ==(Color left, Color right)
{
return left.Equals(right);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="Color"/> objects are not equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are not equal; otherwise, false.</returns>
public static bool operator !=(Color left, Color right)
{
return !left.Equals(right);

476
src/Avalonia.Visuals/Media/HslColor.cs

@ -0,0 +1,476 @@
// Color conversion portions of this source file are adapted from the Windows Community Toolkit project.
// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Globalization;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media
{
/// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model.
/// </summary>
#if !BUILDTASK
public
#endif
readonly struct HslColor : IEquatable<HslColor>
{
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
/// </summary>
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the range from 0..1.</param>
public HslColor(
double alpha,
double hue,
double saturation,
double lightness)
{
A = MathUtilities.Clamp(alpha, 0.0, 1.0);
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
L = MathUtilities.Clamp(lightness, 0.0, 1.0);
// The maximum value of Hue is technically 360 minus epsilon (just below 360).
// This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
// However, that is too tricky to work with in code and isn't as intuitive.
// Therefore, since 360 == 0, just wrap 360 if needed back to 0.
H = (H == 360.0 ? 0 : H);
}
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
/// </summary>
/// <remarks>
/// This constructor exists only for internal use where performance is critical.
/// Whether or not the component values are in the correct ranges must be known.
/// </remarks>
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the range from 0..1.</param>
/// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
internal HslColor(
double alpha,
double hue,
double saturation,
double lightness,
bool clampValues)
{
if (clampValues)
{
A = MathUtilities.Clamp(alpha, 0.0, 1.0);
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
L = MathUtilities.Clamp(lightness, 0.0, 1.0);
// See comments in constructor above
H = (H == 360.0 ? 0 : H);
}
else
{
A = alpha;
H = hue;
S = saturation;
L = lightness;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
/// </summary>
/// <param name="color">The RGB color to convert to HSL.</param>
public HslColor(Color color)
{
var hsl = Color.ToHsl(color);
A = hsl.A;
H = hsl.H;
S = hsl.S;
L = hsl.L;
}
/// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1.
/// </summary>
public double A { get; }
/// <summary>
/// Gets the Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
public double H { get; }
/// <summary>
/// Gets the Saturation component in the range from 0..1.
/// </summary>
public double S { get; }
/// <summary>
/// Gets the Lightness component in the range from 0..1.
/// </summary>
public double L { get; }
/// <inheritdoc/>
public bool Equals(HslColor other)
{
return other.A == A &&
other.H == H &&
other.S == S &&
other.L == L;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
if (obj is HslColor hslColor)
{
return Equals(hslColor);
}
else
{
return false;
}
}
/// <summary>
/// Gets a hashcode for this object.
/// Hashcode is not guaranteed to be unique.
/// </summary>
/// <returns>The hashcode for this object.</returns>
public override int GetHashCode()
{
// Same algorithm as Color
// This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements
unchecked
{
int hashCode = A.GetHashCode();
hashCode = (hashCode * 397) ^ H.GetHashCode();
hashCode = (hashCode * 397) ^ S.GetHashCode();
hashCode = (hashCode * 397) ^ L.GetHashCode();
return hashCode;
}
}
/// <summary>
/// Returns the RGB color model equivalent of this HSL color.
/// </summary>
/// <returns>The RGB equivalent color.</returns>
public Color ToRgb()
{
// Use the by-component conversion method directly for performance
return HslColor.ToRgb(H, S, L, A);
}
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
// Use a format similar to CSS. However:
// - To ensure precision is never lost, allow decimal places.
// This is especially important for round-trip serialization.
// - To maintain numerical consistency, do not use percent.
//
// Example:
//
// hsla(hue, saturation, lightness, alpha)
// hsla(230, 1.0, 0.5, 1.0)
//
sb.Append("hsva(");
sb.Append(H.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(S.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(L.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(A.ToString(CultureInfo.InvariantCulture));
sb.Append(')');
return sb.ToString();
}
/// <summary>
/// Parses an HSL color string.
/// </summary>
/// <param name="s">The HSL color string to parse.</param>
/// <returns>The parsed <see cref="HslColor"/>.</returns>
public static HslColor Parse(string s)
{
if (s is null)
{
throw new ArgumentNullException(nameof(s));
}
if (TryParse(s, out HslColor hslColor))
{
return hslColor;
}
throw new FormatException($"Invalid HSL color string: '{s}'.");
}
/// <summary>
/// Parses an HSL color string.
/// </summary>
/// <param name="s">The HSL color string to parse.</param>
/// <param name="hslColor">The parsed <see cref="HslColor"/>.</param>
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor)
{
hslColor = default;
if (s is null)
{
return false;
}
string workingString = s.Trim();
if (workingString.Length == 0 ||
workingString.IndexOf(",", StringComparison.Ordinal) < 0)
{
return false;
}
if (workingString.Length > 6 &&
workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
}
if (workingString.Length > 5 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
}
string[] components = workingString.Split(',');
if (components.Length == 3) // HSL
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double lightness))
{
hslColor = new HslColor(1.0, hue, saturation, lightness);
return true;
}
}
else if (components.Length == 4) // HSLA
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double lightness) &&
TryInternalParse(components[3], out double alpha))
{
hslColor = new HslColor(alpha, hue, saturation, lightness);
return true;
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0;
return result;
}
else
{
return double.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble);
}
}
return false;
}
/// <summary>
/// Creates a new <see cref="HslColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="l">The Lightness component in the range from 0..1.</param>
/// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
public static HslColor FromAhsl(double a, double h, double s, double l)
{
return new HslColor(a, h, s, l);
}
/// <summary>
/// Creates a new <see cref="HslColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="l">The Lightness component in the range from 0..1.</param>
/// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
public static HslColor FromHsl(double h, double s, double l)
{
return new HslColor(1.0, h, s, l);
}
/// <summary>
/// Converts the given HSL color to its RGB color equivalent.
/// </summary>
/// <param name="hslColor">The color in the HSL color model.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
public static Color ToRgb(HslColor hslColor)
{
return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
}
/// <summary>
/// Converts the given HSLA color component values to their RGB color equivalent.
/// </summary>
/// <param name="hue">The Hue component in the HSL color model in the range from 0..360.</param>
/// <param name="saturation">The Saturation component in the HSL color model in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the HSL color model in the range from 0..1.</param>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
public static Color ToRgb(
double hue,
double saturation,
double lightness,
double alpha = 1.0)
{
// Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
// It has been modified to ensure input ranges and not throw exceptions.
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
while (hue >= 360.0)
{
hue -= 360.0;
}
while (hue < 0.0)
{
hue += 360.0;
}
// We similarly clamp saturation, lightness and alpha between 0 and 1.
saturation = saturation < 0.0 ? 0.0 : saturation;
saturation = saturation > 1.0 ? 1.0 : saturation;
lightness = lightness < 0.0 ? 0.0 : lightness;
lightness = lightness > 1.0 ? 1.0 : lightness;
alpha = alpha < 0.0 ? 0.0 : alpha;
alpha = alpha > 1.0 ? 1.0 : alpha;
double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation;
double h1 = hue / 60;
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
double m = lightness - (0.5 * chroma);
double r1, g1, b1;
if (h1 < 1)
{
r1 = chroma;
g1 = x;
b1 = 0;
}
else if (h1 < 2)
{
r1 = x;
g1 = chroma;
b1 = 0;
}
else if (h1 < 3)
{
r1 = 0;
g1 = chroma;
b1 = x;
}
else if (h1 < 4)
{
r1 = 0;
g1 = x;
b1 = chroma;
}
else if (h1 < 5)
{
r1 = x;
g1 = 0;
b1 = chroma;
}
else
{
r1 = chroma;
g1 = 0;
b1 = x;
}
return Color.FromArgb(
(byte)Math.Round(255 * alpha),
(byte)Math.Round(255 * (r1 + m)),
(byte)Math.Round(255 * (g1 + m)),
(byte)Math.Round(255 * (b1 + m)));
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HslColor"/> objects are equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are equal; otherwise, false.</returns>
public static bool operator ==(HslColor left, HslColor right)
{
return left.Equals(right);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HslColor"/> objects are not equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are not equal; otherwise, false.</returns>
public static bool operator !=(HslColor left, HslColor right)
{
return !(left == right);
}
/// <summary>
/// Explicit conversion from an <see cref="HslColor"/> to a <see cref="Color"/>.
/// </summary>
/// <param name="hslColor">The <see cref="HslColor"/> to convert.</param>
public static explicit operator Color(HslColor hslColor)
{
return hslColor.ToRgb();
}
}
}

259
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;
@ -21,11 +21,11 @@ namespace Avalonia.Media
/// <summary>
/// Initializes a new instance of the <see cref="HsvColor"/> struct.
/// </summary>
/// <param name="alpha">The Alpha (transparency) channel value in the range from 0..1.</param>
/// <param name="hue">The Hue channel value in the range from 0..360.
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation channel value in the range from 0..1.</param>
/// <param name="value">The Value channel value in the range from 0..1.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="value">The Value component in the range from 0..1.</param>
public HsvColor(
double alpha,
double hue,
@ -49,14 +49,14 @@ namespace Avalonia.Media
/// </summary>
/// <remarks>
/// 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.
/// Whether or not the component values are in the correct ranges must be known.
/// </remarks>
/// <param name="alpha">The Alpha (transparency) channel value in the range from 0..1.</param>
/// <param name="hue">The Hue channel value in the range from 0..360.
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation channel value in the range from 0..1.</param>
/// <param name="value">The Value channel value in the range from 0..1.</param>
/// <param name="clampValues">Whether to clamp channel values to their required ranges.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="value">The Value component in the range from 0..1.</param>
/// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
internal HsvColor(
double alpha,
double hue,
@ -89,7 +89,7 @@ namespace Avalonia.Media
/// <param name="color">The RGB color to convert to HSV.</param>
public HsvColor(Color color)
{
var hsv = HsvColor.FromRgb(color);
var hsv = Color.ToHsv(color);
A = hsv.A;
H = hsv.H;
@ -98,23 +98,23 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the Alpha (transparency) channel value in the range from 0..1.
/// Gets the Alpha (transparency) component in the range from 0..1.
/// </summary>
public double A { get; }
/// <summary>
/// Gets the Hue channel value in the range from 0..360.
/// Gets the Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
public double H { get; }
/// <summary>
/// Gets the Saturation channel value in the range from 0..1.
/// Gets the Saturation component in the range from 0..1.
/// </summary>
public double S { get; }
/// <summary>
/// Gets the Value channel value in the range from 0..1.
/// Gets the Value component in the range from 0..1.
/// </summary>
public double V { get; }
@ -165,7 +165,7 @@ namespace Avalonia.Media
/// <returns>The RGB equivalent color.</returns>
public Color ToRgb()
{
// Use the by-channel conversion method directly for performance
// Use the by-component conversion method directly for performance
return HsvColor.ToRgb(H, S, V, A);
}
@ -174,26 +174,16 @@ namespace Avalonia.Media
{
var sb = new StringBuilder();
// Use a format similar to HSL in HTML/CSS "hsla(0, 100%, 50%, 0.5)"
//
// However:
// - To ensure precision is never lost, allow decimal places
// - To maintain numerical consistency do not use percent
// Use a format similar to CSS. However:
// - To ensure precision is never lost, allow decimal places.
// This is especially important for round-trip serialization.
// - To maintain numerical consistency, do not use percent.
//
// Example:
//
// hsva(hue, saturation, value, alpha)
// hsva(230, 1.0, 0.5, 1.0)
//
// Where:
//
// hue : double from 0 to 360
// saturation : double from 0 to 1
// (HTML uses a percentage)
// value : double from 0 to 1
// (HTML uses a percentage)
// alpha : double from 0 to 1
// (HTML does not use a percentage for alpha)
sb.Append("hsva(");
sb.Append(H.ToString(CultureInfo.InvariantCulture));
@ -270,8 +260,8 @@ namespace Avalonia.Media
if (components.Length == 3) // HSV
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value))
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double value))
{
hsvColor = new HsvColor(1.0, hue, saturation, value);
return true;
@ -280,37 +270,80 @@ namespace Avalonia.Media
else if (components.Length == 4) // HSVA
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value) &&
double.TryParse(components[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double alpha))
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double value) &&
TryInternalParse(components[3], out double alpha))
{
hsvColor = new HsvColor(alpha, hue, saturation, value);
return true;
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0;
return result;
}
else
{
return double.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble);
}
}
return false;
}
/// <summary>
/// Creates a new <see cref="HsvColor"/> from individual color channel values.
/// Creates a new <see cref="HsvColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="a">The Alpha (transparency) channel value in the range from 0..1.</param>
/// <param name="h">The Hue channel value in the range from 0..360.</param>
/// <param name="s">The Saturation channel value in the range from 0..1.</param>
/// <param name="v">The Value channel value in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> built from the individual color channel values.</returns>
/// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="v">The Value component in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> built from the individual color component values.</returns>
public static HsvColor FromAhsv(double a, double h, double s, double v)
{
return new HsvColor(a, h, s, v);
}
/// <summary>
/// Converts the given HSV color to it's RGB color equivalent.
/// Creates a new <see cref="HsvColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="v">The Value component in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> built from the individual color component values.</returns>
public static HsvColor FromHsv(double h, double s, double v)
{
return new HsvColor(1.0, h, s, v);
}
/// <summary>
/// Converts the given HSV color to its RGB color equivalent.
/// </summary>
/// <param name="hsvColor">The color in the HSV color model.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
@ -320,12 +353,12 @@ namespace Avalonia.Media
}
/// <summary>
/// Converts the given HSVA color channel values to it's RGB color equivalent.
/// Converts the given HSVA color component values to their RGB color equivalent.
/// </summary>
/// <param name="hue">The hue channel value in the HSV color model in the range from 0..360.</param>
/// <param name="saturation">The saturation channel value in the HSV color model in the range from 0..1.</param>
/// <param name="value">The value channel value in the HSV color model in the range from 0..1.</param>
/// <param name="alpha">The alpha channel value in the range from 0..1.</param>
/// <param name="hue">The Hue component in the HSV color model in the range from 0..360.</param>
/// <param name="saturation">The Saturation component in the HSV color model in the range from 0..1.</param>
/// <param name="value">The Value component in the HSV color model in the range from 0..1.</param>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
public static Color ToRgb(
double hue,
@ -336,7 +369,7 @@ namespace Avalonia.Media
// 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
// Alpha support was added
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
@ -457,130 +490,6 @@ namespace Avalonia.Media
(byte)Math.Round(b * 255));
}
/// <summary>
/// Converts the given RGB color to it's HSV color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor FromRgb(Color color)
{
return HsvColor.FromRgb(color.R, color.G, color.B, color.A);
}
/// <summary>
/// Converts the given RGBA color channel values to it's HSV color equivalent.
/// </summary>
/// <param name="red">The red channel value in the RGB color model.</param>
/// <param name="green">The green channel value in the RGB color model.</param>
/// <param name="blue">The blue channel value in the RGB color model.</param>
/// <param name="alpha">The alpha channel value.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
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.
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);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HsvColor"/> objects are equal.
/// </summary>

124
tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs

@ -199,5 +199,129 @@ namespace Avalonia.Visuals.UnitTests.Media
{
Assert.False(Color.TryParse(input, out _));
}
[Fact]
public void Try_Parse_HslColor()
{
// Inline data requires constants, so the data is handled internally here
var data = new Tuple<string, HslColor>[]
{
// HSV
Tuple.Create("hsl(0, 0, 0)", new HslColor(1, 0, 0, 0)),
Tuple.Create("hsl(0, 0%, 0%)", new HslColor(1, 0, 0, 0)),
Tuple.Create("hsl(180, 0.5, 0.5)", new HslColor(1, 180, 0.5, 0.5)),
Tuple.Create("hsl(180, 50%, 50%)", new HslColor(1, 180, 0.5, 0.5)),
Tuple.Create("hsl(360, 1.0, 1.0)", new HslColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsl(360, 100%, 100%)", new HslColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsl(-1000, -1000, -1000)", new HslColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsl(-1000, -1000%, -1000%)", new HslColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsl(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsl(300, 80%, 20%)", new HslColor(1.0, 300, 0.8, 0.2)),
// HSVA
Tuple.Create("hsla(0, 0, 0, 0)", new HslColor(0, 0, 0, 0)),
Tuple.Create("hsla(0, 0%, 0%, 0%)", new HslColor(0, 0, 0, 0)),
Tuple.Create("hsla(180, 0.5, 0.5, 0.5)", new HslColor(0.5, 180, 0.5, 0.5)),
Tuple.Create("hsla(180, 50%, 50%, 50%)", new HslColor(0.5, 180, 0.5, 0.5)),
Tuple.Create("hsla(360, 1.0, 1.0, 1.0)", new HslColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsla(360, 100%, 100%, 100%)", new HslColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsla(-1000, -1000, -1000, -1000)", new HslColor(0, 0, 0, 0)), // Clamps to min
Tuple.Create("hsla(-1000, -1000%, -1000%, -1000%)", new HslColor(0, 0, 0, 0)), // Clamps to min
Tuple.Create("hsla(1000, 1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsla(1000, 1000%, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsla(300, 0.9, 0.2, 0.8)", new HslColor(0.8, 300, 0.9, 0.2)),
Tuple.Create("hsla(300, 90%, 20%, 0.8)", new HslColor(0.8, 300, 0.9, 0.2)),
};
foreach (var dataPoint in data)
{
Assert.True(HslColor.TryParse(dataPoint.Item1, out HslColor parsedHslColor));
Assert.True(dataPoint.Item2 == parsedHslColor);
}
}
[Fact]
public void Try_Parse_HsvColor()
{
// Inline data requires constants, so the data is handled internally here
var data = new Tuple<string, HsvColor>[]
{
// HSV
Tuple.Create("hsv(0, 0, 0)", new HsvColor(1, 0, 0, 0)),
Tuple.Create("hsv(0, 0%, 0%)", new HsvColor(1, 0, 0, 0)),
Tuple.Create("hsv(180, 0.5, 0.5)", new HsvColor(1, 180, 0.5, 0.5)),
Tuple.Create("hsv(180, 50%, 50%)", new HsvColor(1, 180, 0.5, 0.5)),
Tuple.Create("hsv(360, 1.0, 1.0)", new HsvColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsv(360, 100%, 100%)", new HsvColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsv(-1000, -1000, -1000)", new HsvColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsv(-1000, -1000%, -1000%)", new HsvColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max
Tuple.Create("hsv(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsv(300, 80%, 20%)", new HsvColor(1.0, 300, 0.8, 0.2)),
// HSVA
Tuple.Create("hsva(0, 0, 0, 0)", new HsvColor(0, 0, 0, 0)),
Tuple.Create("hsva(0, 0%, 0%, 0%)", new HsvColor(0, 0, 0, 0)),
Tuple.Create("hsva(180, 0.5, 0.5, 0.5)", new HsvColor(0.5, 180, 0.5, 0.5)),
Tuple.Create("hsva(180, 50%, 50%, 50%)", new HsvColor(0.5, 180, 0.5, 0.5)),
Tuple.Create("hsva(360, 1.0, 1.0, 1.0)", new HsvColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsva(360, 100%, 100%, 100%)", new HsvColor(1, 0, 1, 1)), // Wraps Hue to zero
Tuple.Create("hsva(-1000, -1000, -1000, -1000)", new HsvColor(0, 0, 0, 0)), // Clamps to min
Tuple.Create("hsva(-1000, -1000%, -1000%, -1000%)", new HsvColor(0, 0, 0, 0)), // Clamps to min
Tuple.Create("hsva(1000, 1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsva(1000, 1000%, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsva(300, 0.9, 0.2, 0.8)", new HsvColor(0.8, 300, 0.9, 0.2)),
Tuple.Create("hsva(300, 90%, 20%, 0.8)", new HsvColor(0.8, 300, 0.9, 0.2)),
};
foreach (var dataPoint in data)
{
Assert.True(HsvColor.TryParse(dataPoint.Item1, out HsvColor parsedHsvColor));
Assert.True(dataPoint.Item2 == parsedHsvColor);
}
}
[Fact]
public void Try_Parse_All_Formats_With_Conversion()
{
// Inline data requires constants, so the data is handled internally here
var data = new Tuple<string, Color>[]
{
// RGB
Tuple.Create("White", new Color(0xff, 0xff, 0xff, 0xff)),
Tuple.Create("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(229, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 100, 30, 45)),
// HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)),
Tuple.Create("hsla(296, 0.85, 0.12, 0.9)", new Color(230, 53, 5, 57)),
Tuple.Create("hsla(296, 85%, 12%, 90%)", new Color(230, 53, 5, 57)),
// HSV
Tuple.Create("hsv(240, 83%, 78%)", new Color(255, 34, 34, 199)),
Tuple.Create("hsva(240, 0.83, 0.78, 0.9)", new Color(230, 34, 34, 199)),
Tuple.Create("hsva(240, 83%, 78%, 90%)", new Color(230, 34, 34, 199)),
};
foreach (var dataPoint in data)
{
Assert.True(Color.TryParse(dataPoint.Item1, out Color parsedColor));
Assert.True(dataPoint.Item2 == parsedColor);
}
}
}
}

Loading…
Cancel
Save