Browse Source

Merge pull request #7993 from robloo/advanced-colors-2

Minor Color Updates
pull/7994/head
Max Katz 4 years ago
committed by GitHub
parent
commit
578de7ebbc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 83
      src/Avalonia.Base/Media/Color.cs
  2. 55
      src/Avalonia.Base/Media/HslColor.cs
  3. 55
      src/Avalonia.Base/Media/HsvColor.cs
  4. 24
      tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

83
src/Avalonia.Base/Media/Color.cs

@ -166,7 +166,10 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
// Note: The length checks are also an important optimization.
// The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@ -175,7 +178,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@ -185,7 +188,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings
var str = s.ToString();
if (s.Length > 5 &&
// Note: The length checks are also an important optimization.
// The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@ -238,7 +244,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@ -248,7 +254,7 @@ namespace Avalonia.Media
return true;
}
if (s.Length > 5 &&
if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@ -271,6 +277,9 @@ namespace Avalonia.Media
return false;
}
/// <summary>
/// Parses the given span of characters representing a hex color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
/// <summary>
/// Parses the given string representing a CSS color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseCssFormat(string s, out Color color)
{
bool prefixMatched = false;
color = default;
if (s is null)
@ -342,27 +356,35 @@ namespace Avalonia.Media
return false;
}
if (workingString.Length > 6 &&
if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
}
if (workingString.Length > 5 &&
if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
}
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))
if (InternalTryParseByte(components[0], out byte red) &&
InternalTryParseByte(components[1], out byte green) &&
InternalTryParseByte(components[2], out byte blue))
{
color = new Color(0xFF, red, green, blue);
return true;
@ -370,18 +392,45 @@ namespace Avalonia.Media
}
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))
if (InternalTryParseByte(components[0], out byte red) &&
InternalTryParseByte(components[1], out byte green) &&
InternalTryParseByte(components[2], out byte blue) &&
InternalTryParseDouble(components[3], out double alpha))
{
color = new Color((byte)(alpha * 255), red, green, blue);
color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue);
return true;
}
}
// Local function to specially parse a byte value with an optional percentage sign
bool InternalTryParseByte(string inString, out byte outByte)
{
// 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);
outByte = (byte)Math.Round((percentage / 100.0) * 255.0);
return result;
}
else
{
return byte.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outByte);
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
bool InternalTryParseDouble(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);

55
src/Avalonia.Base/Media/HslColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{
/// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary>
#if !BUILDTASK
public
@ -98,24 +99,53 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1.
/// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; }
/// <summary>
/// Gets the Hue component in the range from 0..360.
/// Gets the Hue component in the range from 0..360 (degrees).
/// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; }
/// <summary>
/// Gets the Saturation component in the range from 0..1.
/// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; }
/// <summary>
/// Gets the Lightness component in the range from 0..1.
/// Gets the Lightness component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black.</item>
/// <item>1 is fully white.</item>
/// </list>
/// </remarks>
public double L { get; }
/// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor)
{
bool prefixMatched = false;
hslColor = default;
if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
if (workingString.Length > 6 &&
// Note: The length checks are also an important optimization.
// The shortest possible format is "hsl(0,0,0)", Length = 10.
if (workingString.Length >= 11 &&
workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
}
if (workingString.Length > 5 &&
if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
}
string[] components = workingString.Split(',');

55
src/Avalonia.Base/Media/HsvColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{
/// <summary>
/// Defines a color using the hue/saturation/value (HSV) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary>
#if !BUILDTASK
public
@ -98,24 +99,53 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1.
/// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; }
/// <summary>
/// Gets the Hue component in the range from 0..360.
/// Gets the Hue component in the range from 0..360 (degrees).
/// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; }
/// <summary>
/// Gets the Saturation component in the range from 0..1.
/// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; }
/// <summary>
/// Gets the Value component in the range from 0..1.
/// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage).
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black and shows no color.</item>
/// <item>1 is the brightest and shows full color.</item>
/// </list>
/// </remarks>
public double V { get; }
/// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HsvColor hsvColor)
{
bool prefixMatched = false;
hsvColor = default;
if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
if (workingString.Length > 6 &&
// Note: The length checks are also an important optimization.
// The shortest possible format is "hsv(0,0,0)", Length = 10.
if (workingString.Length >= 11 &&
workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
}
if (workingString.Length > 5 &&
if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
}
string[] components = workingString.Split(',');

24
tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

@ -216,8 +216,8 @@ namespace Avalonia.Base.UnitTests.Media
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(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
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)),
@ -262,8 +262,8 @@ namespace Avalonia.Base.UnitTests.Media
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(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
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)),
@ -303,8 +303,20 @@ namespace Avalonia.Base.UnitTests.Media
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)),
Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(230, 100, 30, 45)),
Tuple.Create("rgba(100, 30, 45, 90%)", new Color(230, 100, 30, 45)),
Tuple.Create("rgb(255,0,0)", new Color(255, 255, 0, 0)),
Tuple.Create("rgb(0,255,0)", new Color(255, 0, 255, 0)),
Tuple.Create("rgb(0,0,255)", new Color(255, 0, 0, 255)),
Tuple.Create("rgb(100%, 0, 0)", new Color(255, 255, 0, 0)),
Tuple.Create("rgb(0, 100%, 0)", new Color(255, 0, 255, 0)),
Tuple.Create("rgb(0, 0, 100%)", new Color(255, 0, 0, 255)),
Tuple.Create("rgba(0, 0, 100%, 50%)", new Color(128, 0, 0, 255)),
Tuple.Create("rgba(50%, 10%, 80%, 50%)", new Color(128, 128, 26, 204)),
Tuple.Create("rgba(50%, 10%, 80%, 0.5)", new Color(128, 128, 26, 204)),
// HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)),

Loading…
Cancel
Save