< Summary

Information
Class: Allyaria.Theming.Types.HexColor
Assembly: Allyaria.Theming
File(s): /home/runner/work/allyaria/allyaria/src/Allyaria.Theming/Types/HexColor.cs
Line coverage
99%
Covered lines: 447
Uncovered lines: 4
Coverable lines: 451
Total lines: 1405
Line coverage: 99.1%
Branch coverage
88%
Covered branches: 127
Total branches: 143
Branch coverage: 88.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
.ctor(...)100%22100%
.ctor(...)100%88100%
get_A()100%11100%
get_B()100%11100%
get_G()100%11100%
get_H()100%11100%
get_R()100%11100%
get_S()100%11100%
get_V()100%11100%
BlendValue(...)100%11100%
ClampToByte(...)100%11100%
CompareTo(...)100%11100%
ContrastRatio(...)50%44100%
Desaturate(...)100%11100%
EnsureContrast(...)91.66%121295.83%
Equals(...)100%66100%
Equals(...)50%22100%
FromHsva(...)100%22100%
GetHashCode()100%11100%
HsvaToRgba(...)100%1212100%
Invert()100%11100%
IsDark()100%11100%
IsLight()100%11100%
IsOpaque()100%11100%
IsTransparent()100%11100%
Parse(...)100%11100%
ParseAlpha(...)50%4487.5%
ParseByte(...)50%22100%
ParseChannel(...)66.66%6690.9%
ParseHex(...)100%99100%
ParseHsva(...)100%66100%
ParseHue(...)50%44100%
ParsePercent(...)60%101094.44%
ParseRgba(...)90%1010100%
RgbToHsv(...)100%1414100%
SearchTowardPole(...)100%1010100%
SearchValueRail(...)100%1212100%
SetAlpha(...)100%11100%
ShiftLightness(...)100%22100%
ToAccent()100%11100%
ToDisabled()100%11100%
ToDragged()100%11100%
ToElevation1()100%11100%
ToElevation2()100%11100%
ToElevation3()100%11100%
ToElevation4()100%11100%
ToElevation5()100%11100%
ToFocused()100%11100%
ToForeground()100%11100%
ToHovered()100%11100%
ToLerpLinear(...)100%11100%
ToLerpLinearPreserveAlpha(...)100%11100%
ToPressed()100%11100%
ToRelativeLuminance()100%11100%
ToString()100%11100%
ToVisited()100%11100%
TryParse(...)100%22100%
TryParseColorName(...)100%22100%
ValueDirection(...)100%22100%
op_Equality(...)100%11100%
op_GreaterThan(...)100%11100%
op_GreaterThanOrEqual(...)100%11100%
op_Implicit(...)100%11100%
op_Implicit(...)100%11100%
op_Inequality(...)100%11100%
op_LessThan(...)100%11100%
op_LessThanOrEqual(...)100%11100%

File(s)

/home/runner/work/allyaria/allyaria/src/Allyaria.Theming/Types/HexColor.cs

#LineLine coverage
 1namespace Allyaria.Theming.Types;
 2
 3/// <summary>Represents an immutable color theme with red, green, blue, and alpha channels.</summary>
 4/// <remarks>
 5///     <para>
 6///     A <see cref="HexColor" /> can be constructed directly from component values (<see cref="HexByte" />) or parsed f
 7///     a wide range of color string formats, including:
 8///     </para>
 9///     <list type="bullet">
 10///         <item>
 11///             <description><b>Hexadecimal</b> — <c>#RGB</c>, <c>#RGBA</c>, <c>#RRGGBB</c>, or <c>#RRGGBBAA</c></descri
 12///         </item>
 13///         <item>
 14///             <description>
 15///             <b>Functional RGB(A)</b> — <c>rgb(...)</c> and <c>rgba(...)</c>, including modern CSS Color Level 4 synt
 16///             (e.g., <c>rgb(255 0 0 / .5)</c>)
 17///             </description>
 18///         </item>
 19///         <item>
 20///             <description><b>Functional HSV(A)</b> — <c>hsv(...)</c> and <c>hsva(...)</c></description>
 21///         </item>
 22///         <item>
 23///             <description>
 24///             <b>Named Colors</b> — case-insensitive lookup against the global <see cref="Colors" /> registry (e.g.,
 25///             <c>"Red500"</c>). The lookup uses the comparer configured in that registry (typically
 26///             <see cref="StringComparer.InvariantCultureIgnoreCase" />).
 27///             </description>
 28///         </item>
 29///     </list>
 30///     <para>
 31///     The type is a <see langword="readonly struct" />, ensuring immutability and thread safety. All channel values ar
 32///     normalized at construction time, and derived HSV components (<see cref="H" />, <see cref="S" />, <see cref="V" /
 33///     are computed automatically.
 34///     </para>
 35///     <para>
 36///     If an input string cannot be parsed as a supported format or a known color name, an
 37///     <see cref="AryArgumentException" /> is thrown.
 38///     </para>
 39/// </remarks>
 40public readonly struct HexColor : IComparable<HexColor>, IEquatable<HexColor>
 41{
 42    /// <summary>
 43    /// Regular-expression sub-pattern used to match an alpha (opacity) component in color functions. Accepts values <c>
 44    /// <c>1</c>, <c>1.0</c>, <c>0.5</c>, or fractional forms such as <c>.5</c>.
 45    /// </summary>
 46    private const string AlphaPattern = @"(?<alpha>(?:0?\.\d+|0|1(?:\.0+)?|(?:100|[1-9]?\d)%))";
 47
 48    /// <summary>Precompiled regex pattern for hexadecimal color strings.</summary>
 149    private static readonly Regex HexColorPattern = new(
 150        pattern: "^#([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
 151        options: RegexOptions.Compiled | RegexOptions.IgnoreCase
 152    );
 53
 54    /// <summary>
 55    /// Compiled regular expression that matches an <c>hsva(h, s%, v%, a)</c> color function containing hue, saturation,
 56    /// and alpha components. Alpha values are validated against <see cref="AlphaPattern" />.
 57    /// </summary>
 158    private static readonly Regex HsvaPattern = new(
 159        pattern:
 160        @"^hsva\s*\(\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))%?\s*,\s*([+-]?(?:\d+(?:\.\d+)
 161        AlphaPattern + @"\s*\)$",
 162        options: RegexOptions.Compiled | RegexOptions.IgnoreCase
 163    );
 64
 65    /// <summary>
 66    /// Compiled regular expression that matches an <c>hsv(h, s%, v%)</c> color function containing hue, saturation, and
 67    /// components. The pattern supports optional signs and decimal fractions, but does <b>not</b> allow an alpha compon
 68    /// (see <see cref="HsvaPattern" /> for that).
 69    /// </summary>
 170    private static readonly Regex HsvPattern = new(
 171        pattern:
 172        @"^hsv\s*\(\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*,\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))%?\s*,\s*([+-]?(?:\d+(?:\.\d+)?
 173        options: RegexOptions.Compiled | RegexOptions.IgnoreCase
 174    );
 75
 76    /// <summary>
 77    /// Compiled regular expression that matches modern CSS Color Level 4 syntax for <c>rgb(...)</c> or <c>rgba(...)</c>
 78    /// channels are space-separated and the alpha component follows a slash—for example, <c>rgb(255 0 0 / .5)</c>. Fall
 79    /// to the same channel validation as <see cref="RgbaPattern" />.
 80    /// </summary>
 181    private static readonly Regex RgbaCss4Pattern = new(
 182        pattern:
 183        @"^rgba?\s*\(\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+)%?)\s+([+-]?(?:\d+(?:\.\d+)?|\.\d+)%?)\s+([+-]?(?:\d+(?:\.\d+)?|\.\
 184        + AlphaPattern + @")?\s*\)$",
 185        options: RegexOptions.Compiled | RegexOptions.IgnoreCase
 186    );
 87
 88    /// <summary>
 89    /// Compiled regular expression that matches an <c>rgba(r, g, b, a)</c> color function with integer RGB channels and
 90    /// explicit alpha component validated by <see cref="AlphaPattern" />. This corresponds to the traditional comma-sep
 91    /// CSS syntax.
 92    /// </summary>
 193    private static readonly Regex RgbaPattern = new(
 194        pattern:
 195        @"^rgba\s*\(\s*([+-]?(?:\d{1,3}(?:\.\d+)?%?))\s*,\s*([+-]?(?:\d{1,3}(?:\.\d+)?%?))\s*,\s*([+-]?(?:\d{1,3}(?:\.\d
 196        + AlphaPattern + @"\s*\)$",
 197        options: RegexOptions.Compiled | RegexOptions.IgnoreCase
 198    );
 99
 100    /// <summary>
 101    /// Compiled regular expression that matches an <c>rgb(r, g, b)</c> color function consisting of three integer chann
 102    /// values in the 0–255 range. This pattern does not support alpha; use <see cref="RgbaPattern" /> or
 103    /// <see cref="RgbaCss4Pattern" /> for variants that do.
 104    /// </summary>
 1105    private static readonly Regex RgbPattern = new(
 1106        pattern:
 1107        @"^rgb\s*\(\s*([+-]?(?:\d{1,3}(?:\.\d+)?%?))\s*,\s*([+-]?(?:\d{1,3}(?:\.\d+)?%?))\s*,\s*([+-]?(?:\d{1,3}(?:\.\d+
 1108        options: RegexOptions.Compiled | RegexOptions.IgnoreCase
 1109    );
 110
 111    /// <summary>
 112    /// Initializes a new instance of the <see cref="HexColor" /> struct with all channels default-initialized to 0.
 113    /// </summary>
 114    public HexColor()
 2115        : this(red: new HexByte(), green: new HexByte(), blue: new HexByte(), alpha: new HexByte()) { }
 116
 117    /// <summary>Initializes a new instance of the <see cref="HexColor" /> struct from individual channels.</summary>
 118    /// <param name="red">The red channel theme.</param>
 119    /// <param name="green">The green channel theme.</param>
 120    /// <param name="blue">The blue channel theme.</param>
 121    /// <param name="alpha">The optional alpha channel theme; if <see langword="null" />, defaults to 255 (opaque).</par
 122    public HexColor(HexByte red, HexByte green, HexByte blue, HexByte? alpha = null)
 123    {
 1373177124        R = red;
 1373177125        G = green;
 1373177126        B = blue;
 1373177127        A = alpha ?? new HexByte(value: 255);
 128
 1373177129        RgbToHsv(hue: out var h, saturation: out var s, value: out var v);
 130
 1373177131        H = h;
 1373177132        S = s;
 1373177133        V = v;
 1373177134    }
 135
 136    /// <summary>
 137    /// Initializes a new instance of the <see cref="HexColor" /> struct from a color string. Accepts the following form
 138    /// <list type="bullet">
 139    ///     <item>
 140    ///         <description>Hex: <c>#RGB</c>, <c>#RGBA</c>, <c>#RRGGBB</c>, <c>#RRGGBBAA</c></description>
 141    ///     </item>
 142    ///     <item>
 143    ///         <description>RGB(A): <c>rgb(...)</c>, <c>rgba(...)</c> (including CSS Color Level 4 slash-alpha syntax)<
 144    ///     </item>
 145    ///     <item>
 146    ///         <description>HSV(A): <c>hsv(...)</c>, <c>hsva(...)</c></description>
 147    ///     </item>
 148    ///     <item>
 149    ///         <description>
 150    ///         <b>Named colors</b>: a case-insensitive lookup against the global <c>Colors</c> registry (e.g., <c>"Red5
 151    ///         ). This is attempted only if the theme does not start with <c>"#"</c>, <c>"rgb"</c>, or <c>"hsv"</c>.
 152    ///         </description>
 153    ///     </item>
 154    /// </list>
 155    /// </summary>
 156    /// <param name="value">The color string to parse. Leading/trailing whitespace is ignored.</param>
 157    /// <exception cref="AryArgumentException">
 158    /// Thrown when <paramref name="value" /> is null/empty or does not match any supported format or known color name.
 159    /// </exception>
 160    /// <remarks>
 161    /// Named-color comparison follows the <see cref="StringComparer" /> used by the <c>Colors</c> registry (e.g.,
 162    /// <c>InvariantCultureIgnoreCase</c>). If layering concerns matter, consider moving name resolution out of
 163    /// <see cref="HexColor" /> to avoid an upward dependency on theming constants.
 164    /// </remarks>
 165    public HexColor(string value)
 166    {
 13027167        AryGuard.NotNullOrWhiteSpace(value: value);
 168
 169        HexByte red;
 170        HexByte green;
 171        HexByte blue;
 172        HexByte alpha;
 13026173        var trimmed = value.Trim();
 174
 13026175        if (trimmed.StartsWith(value: "hsv", comparisonType: StringComparison.OrdinalIgnoreCase))
 176        {
 7177            ParseHsva(value: trimmed, red: out red, green: out green, blue: out blue, alpha: out alpha);
 178        }
 13019179        else if (trimmed.StartsWith(value: "rgb", comparisonType: StringComparison.OrdinalIgnoreCase))
 180        {
 5181            ParseRgba(value: trimmed, red: out red, green: out green, blue: out blue, alpha: out alpha);
 182        }
 13014183        else if (trimmed.StartsWith(value: "#", comparisonType: StringComparison.OrdinalIgnoreCase))
 184        {
 13005185            ParseHex(value: trimmed, red: out red, green: out green, blue: out blue, alpha: out alpha);
 186        }
 9187        else if (!TryParseColorName(value: trimmed, red: out red, green: out green, blue: out blue, alpha: out alpha))
 188        {
 5189            throw new AryArgumentException(message: $"Invalid color string: {value}.", argName: nameof(value));
 190        }
 191
 13013192        R = red;
 13013193        G = green;
 13013194        B = blue;
 13013195        A = alpha;
 196
 13013197        RgbToHsv(hue: out var h, saturation: out var s, value: out var v);
 198
 13013199        H = h;
 13013200        S = s;
 13013201        V = v;
 13013202    }
 203
 204    /// <summary>Gets the alpha (opacity) channel.</summary>
 1354997205    public HexByte A { get; }
 206
 207    /// <summary>Gets the blue channel.</summary>
 4188823208    public HexByte B { get; }
 209
 210    /// <summary>Gets the green channel.</summary>
 4188823211    public HexByte G { get; }
 212
 213    /// <summary>
 214    /// Gets the <b>Hue</b> component of the color, expressed in degrees within the range [0, 360). Represents the color
 215    /// dominant wavelength — <c>0</c> = red, <c>120</c> = green, <c>240</c> = blue.
 216    /// </summary>
 1167221217    public double H { get; }
 218
 219    /// <summary>Gets the red channel.</summary>
 4188830220    public HexByte R { get; }
 221
 222    /// <summary>
 223    /// Gets the <b>Saturation</b> component of the color, normalized to the range <c>[0, 1]</c>. A theme of <c>0</c> in
 224    /// a fully desaturated color (gray), while <c>1</c> indicates full color intensity.
 225    /// </summary>
 1167220226    public double S { get; }
 227
 228    /// <summary>
 229    /// Gets the <b>Value</b> (brightness) component of the color, normalized to the range <c>[0, 1]</c>. A theme of <c>
 230    /// represents black, and <c>1</c> represents the brightest possible version of the color.
 231    /// </summary>
 674267232    public double V { get; }
 233
 234    /// <summary>
 235    /// Linearly interpolates the current <see cref="V" /> (theme/brightness) toward a specified target theme by a given
 236    /// factor within the closed interval <c>[0, 1]</c>.
 237    /// </summary>
 238    /// <param name="target">The target brightness theme to blend toward (0–1).</param>
 239    /// <param name="factor">
 240    /// The interpolation factor clamped to <c>[0, 1]</c>. A theme of <c>0</c> returns the current <see cref="V" />; a t
 241    /// <c>1</c> returns the <paramref name="target" />.
 242    /// </param>
 243    /// <returns>The interpolated scalar theme resulting from the linear blend.</returns>
 16125244    private double BlendValue(double target, double factor) => V + (target - V) * factor;
 245
 246    /// <summary>Clamps a normalized channel (0–1) to a byte (0–255) using banker's rounding (ToEven).</summary>
 247    /// <param name="value">The normalized theme.</param>
 248    /// <returns>The clamped 8-bit channel theme.</returns>
 249    private static byte ClampToByte(double value)
 3501678250        => (byte)Math.Clamp(value: Math.Round(value: value * 255.0, mode: MidpointRounding.ToEven), min: 0, max: 255);
 251
 252    /// <summary>Compares this instance with another <see cref="HexColor" /> to determine relative ordering.</summary>
 253    /// <param name="other">The other color to compare.</param>
 254    /// <returns>
 255    /// A theme less than zero if this instance precedes <paramref name="other" />; zero if equal; greater than zero if 
 256    /// instance follows <paramref name="other" />.
 257    /// </returns>
 258    public int CompareTo(HexColor other)
 5259        => (Red: R, Green: G, Blue: B, Alpha: A).CompareTo(other: (other.R, other.G, other.B, other.A));
 260
 261    /// <summary>
 262    /// Calculates the WCAG 2.2 contrast ratio between the current color (treated as the foreground) and a specified bac
 263    /// color, based on their relative luminance in the sRGB color space.
 264    /// </summary>
 265    /// <param name="background">
 266    /// The background <see cref="HexColor" /> against which to measure contrast. Both the current color and
 267    /// <paramref name="background" /> are assumed to be fully opaque.
 268    /// </param>
 269    /// <exception cref="AryArgumentException">Thrown when the contrast ratio is NaN or Infinity.</exception>
 270    /// <returns>
 271    /// A <see cref="double" /> representing the contrast ratio, defined as <c>(Lighter + 0.05) / (Darker + 0.05)</c>, w
 272    /// <em>L</em> is the WCAG relative luminance. The ratio ranges from <c>1.0</c> (no contrast) to <em>21.0</em> (maxi
 273    /// contrast).
 274    /// </returns>
 275    /// <remarks>
 276    /// The returned ratio can be evaluated against WCAG 2.2 contrast thresholds:
 277    /// <list type="bullet">
 278    ///     <item>
 279    ///         <description>Normal text: ≥ 4.5 : 1</description>
 280    ///     </item>
 281    ///     <item>
 282    ///         <description>Large text (≥ 18 pt or 14 pt bold): ≥ 3 : 1</description>
 283    ///     </item>
 284    ///     <item>
 285    ///         <description>UI components and graphics: ≥ 3 : 1</description>
 286    ///     </item>
 287    /// </list>
 288    /// </remarks>
 289    public double ContrastRatio(HexColor background)
 290    {
 1210814291        var foregroundL = ToRelativeLuminance();
 1210814292        var backgroundL = background.ToRelativeLuminance();
 1210814293        var lighter = Math.Max(val1: foregroundL, val2: backgroundL);
 1210814294        var darker = Math.Min(val1: foregroundL, val2: backgroundL);
 1210814295        var result = (lighter + 0.05) / (darker + 0.05);
 296
 1210814297        return double.IsNaN(d: result) || double.IsInfinity(d: result)
 1210814298            ? throw new AryArgumentException(
 1210814299                message: "The specified colors are not valid colors.", argName: nameof(background)
 1210814300            )
 1210814301            : result;
 302    }
 303
 304    /// <summary>
 305    /// Reduces the color’s saturation by a specified fraction and optionally blends its brightness toward a mid-tone to
 306    /// maintain perceptual balance.
 307    /// </summary>
 308    /// <param name="desaturateBy">
 309    /// The amount to decrease saturation, expressed as a normalized fraction in <c>[0,1]</c> (e.g., <c>0.6</c> = reduce
 310    /// 60%). Values outside this range are clamped automatically.
 311    /// </param>
 312    /// <param name="valueBlendTowardMid">
 313    /// The blend factor toward a mid-tone brightness (V = 0.5), clamped to <c>[0, 1]</c>. Higher values yield a more ne
 314    /// evenly lit result.
 315    /// </param>
 316    /// <returns>A new <see cref="HexColor" /> instance representing the desaturated color.</returns>
 317    public HexColor Desaturate(double desaturateBy = 0.5, double valueBlendTowardMid = 0.15)
 16125318        => FromHsva(
 16125319            hue: H,
 16125320            saturation: Math.Clamp(value: S - Math.Clamp(value: desaturateBy, min: 0.0, max: 1.0), min: 0.0, max: 1.0),
 16125321            value: BlendValue(target: 0.5, factor: Math.Clamp(value: valueBlendTowardMid, min: 0.0, max: 1.0)),
 16125322            alpha: A.ToNormalized()
 16125323        );
 324
 325    /// <summary>
 326    /// Resolves a foreground color that meets (or best-approaches) a minimum contrast ratio over the background by pres
 327    /// the foreground hue and saturation (HSV H/S) and adjusting only theme (V). If that hue rail cannot reach the targ
 328    /// (even at V = 0 or 1), the method mixes toward black and white and returns the closest solution that meets—or
 329    /// best-approaches—the target.
 330    /// </summary>
 331    /// <param name="background">Background color (opaque).</param>
 332    /// <param name="minimumRatio">Required minimum contrast ratio (1–21, e.g., <c>4.5</c> for body text).</param>
 333    /// <exception cref="AryArgumentException">Thrown when the minimum ratio is less than 1 or greater than 21.</excepti
 334    /// <returns>The resolved color.</returns>
 335    public HexColor EnsureContrast(HexColor background, double minimumRatio = 3.0)
 336    {
 178686337        AryGuard.InRange(value: minimumRatio, min: 1.0, max: 21.0);
 338
 178686339        var startRatio = ContrastRatio(background: background);
 340
 178686341        if (startRatio >= minimumRatio)
 342        {
 120498343            return this;
 344        }
 345
 58188346        var direction = ValueDirection(background: background);
 347
 348        // 1) Preferred theme-rail attempt
 58188349        var first = SearchValueRail(direction: direction, background: background, minimumRatio: minimumRatio);
 350
 58188351        if (first.IsMinimumMet)
 352        {
 53393353            return first.ForegroundColor;
 354        }
 355
 356        // 2) Poles
 4795357        var towardWhite = SearchTowardPole(
 4795358            pole: Colors.White.SetAlpha(alpha: A.Value), background: background, minimumRatio: minimumRatio
 4795359        );
 360
 4795361        var towardBlack = SearchTowardPole(
 4795362            pole: Colors.Black.SetAlpha(alpha: A.Value), background: background, minimumRatio: minimumRatio
 4795363        );
 364
 4795365        if (towardWhite.IsMinimumMet)
 366        {
 4244367            return towardWhite.ForegroundColor;
 368        }
 369
 551370        if (towardBlack.IsMinimumMet)
 371        {
 550372            return towardBlack.ForegroundColor;
 373        }
 374
 375        // 3) Best-effort fallback (no one met the target): pick highest contrast among ALL candidates tried
 1376        var best = first;
 377
 1378        if (towardWhite.ContrastRatio > best.ContrastRatio)
 379        {
 380            // Code Coverage: This is unreachable code.
 0381            best = towardWhite;
 382        }
 383
 1384        if (towardBlack.ContrastRatio > best.ContrastRatio)
 385        {
 1386            best = towardBlack;
 387        }
 388
 1389        return best.ForegroundColor;
 390    }
 391
 392    /// <summary>Indicates whether the current color is equal to another <see cref="HexColor" />.</summary>
 393    /// <param name="other">The color to compare with.</param>
 394    /// <returns><see langword="true" /> if the colors are equal; otherwise, <see langword="false" />.</returns>
 395    public bool Equals(HexColor other)
 2246396        => R.Equals(other: other.R) && G.Equals(other: other.G) && B.Equals(other: other.B) && A.Equals(other: other.A);
 397
 398    /// <summary>Determines whether the specified object is equal to the current instance.</summary>
 399    /// <param name="obj">The object to compare with.</param>
 400    /// <returns><see langword="true" /> if equal; otherwise, <see langword="false" />.</returns>
 38401    public override bool Equals(object? obj) => obj is HexColor other && Equals(other: other);
 402
 403    /// <summary>Creates a <see cref="HexColor" /> from HSVA component values.</summary>
 404    /// <param name="hue">The hue component in degrees (nominally 0–360; values are normalized).</param>
 405    /// <param name="saturation">The saturation component in the 0–1 range.</param>
 406    /// <param name="value">The theme (brightness) component in the 0–1 range.</param>
 407    /// <param name="alpha">The alpha component in the 0–1 range. Defaults to 1.0 (opaque).</param>
 408    /// <returns>A <see cref="HexColor" /> representing the specified HSVA.</returns>
 409    public static HexColor FromHsva(double hue, double saturation, double value, double alpha = 1.0)
 410    {
 1167222411        var h = hue % 360.0;
 412
 1167222413        if (h < 0)
 414        {
 1415            h += 360.0;
 416        }
 417
 1167222418        var s = Math.Clamp(value: saturation, min: 0.0, max: 1.0);
 1167222419        var v = Math.Clamp(value: value, min: 0.0, max: 1.0);
 1167222420        var a = HexByte.FromNormalized(value: Math.Clamp(value: alpha, min: 0.0, max: 1.0));
 421
 1167222422        return HsvaToRgba(hue: h, saturation: s, value: v, alpha: a);
 423    }
 424
 425    /// <summary>Returns a hash code for this instance.</summary>
 426    /// <returns>A hash code for the current object.</returns>
 2427    public override int GetHashCode() => HashCode.Combine(value1: R, value2: G, value3: B, value4: A);
 428
 429    /// <summary>Converts HSVA values to an equivalent <see cref="HexColor" />.</summary>
 430    /// <param name="hue">Hue in degrees (can wrap), nominally 0–360.</param>
 431    /// <param name="saturation">Saturation in the 0–1 range.</param>
 432    /// <param name="value">Value (brightness) in the 0–1 range.</param>
 433    /// <param name="alpha">Alpha channel as a <see cref="HexByte" />.</param>
 434    /// <returns>A <see cref="HexColor" /> representing the HSVA input.</returns>
 435    private static HexColor HsvaToRgba(double hue, double saturation, double value, HexByte alpha)
 436    {
 1167226437        hue %= 360.0;
 438
 1167226439        if (hue < 0)
 440        {
 1441            hue += 360.0;
 442        }
 443
 1167226444        hue = (hue % 360.0 + 360.0) % 360.0;
 445
 446        double red, green, blue;
 447
 1167226448        var chroma = value * saturation;
 1167226449        var prime = hue / 60.0;
 1167226450        var x = chroma * (1.0 - Math.Abs(value: prime % 2.0 - 1.0));
 1167226451        var m = value - chroma;
 452
 453        switch (prime)
 454        {
 455            case < 1:
 988576456                red = chroma;
 988576457                green = x;
 988576458                blue = 0;
 459
 988576460                break;
 461            case < 2:
 20339462                red = x;
 20339463                green = chroma;
 20339464                blue = 0;
 465
 20339466                break;
 467            case < 3:
 27659468                red = 0;
 27659469                green = chroma;
 27659470                blue = x;
 471
 27659472                break;
 473            case < 4:
 89314474                red = 0;
 89314475                green = x;
 89314476                blue = chroma;
 477
 89314478                break;
 479            case < 5:
 5901480                red = x;
 5901481                green = 0;
 5901482                blue = chroma;
 483
 5901484                break;
 485            default:
 35437486                red = chroma;
 35437487                green = 0;
 35437488                blue = x;
 489
 490                break;
 491        }
 492
 1167226493        return new HexColor(
 1167226494            red: ClampToByte(value: red + m), green: ClampToByte(value: green + m), blue: ClampToByte(value: blue + m),
 1167226495            alpha: alpha
 1167226496        );
 497    }
 498
 499    /// <summary>
 500    /// Produces the photographic negative of the current color by inverting each RGB channel (i.e., <c>R' = 255 − R</c>
 501    /// <c>G' = 255 − G</c>, <c>B' = 255 − B</c>). The alpha channel is preserved.
 502    /// </summary>
 503    /// <remarks>
 504    /// This matches the classic “negative image” operation performed in RGB space. The resulting hue/saturation/theme m
 505    /// equal a simple 180° hue rotation; it is the exact per-channel inversion of the underlying RGB values.
 506    /// </remarks>
 507    /// <returns>A new <see cref="HexColor" /> representing the RGB-inverted (negative) color.</returns>
 508    public HexColor Invert()
 509    {
 1510        var r = (byte)(255 - R.Value);
 1511        var g = (byte)(255 - G.Value);
 1512        var b = (byte)(255 - B.Value);
 513
 1514        return new HexColor(red: r, green: g, blue: b, alpha: A);
 515    }
 516
 517    /// <summary>Determines whether the color is perceptually dark based on its relative luminance.</summary>
 518    /// <returns>
 519    /// <see langword="true" /> if <see cref="ToRelativeLuminance()" /> is less than <c>0.5</c>; otherwise,
 520    /// <see langword="false" />.
 521    /// </returns>
 522    /// <remarks>
 523    /// This method provides a simple luminance-based classification that can be used for dynamic contrast adjustments, 
 524    /// choosing a light foreground color for dark backgrounds.
 525    /// </remarks>
 2526    public bool IsDark() => ToRelativeLuminance() < 0.5;
 527
 528    /// <summary>Determines whether the color is perceptually light based on its relative luminance.</summary>
 529    /// <returns>
 530    /// <see langword="true" /> if <see cref="ToRelativeLuminance()" /> is greater than or equal to <c>0.5</c>; otherwis
 531    /// <see langword="false" />.
 532    /// </returns>
 533    /// <remarks>
 534    /// Complements <see cref="IsDark()" />. Useful for automatically selecting dark text or icons when the background c
 535    /// light.
 536    /// </remarks>
 2537    public bool IsLight() => ToRelativeLuminance() >= 0.5;
 538
 539    /// <summary>Determines whether the current color is fully opaque.</summary>
 540    /// <returns>
 541    /// <see langword="true" /> if the alpha channel (<see cref="A" />) equals <c>255</c>; otherwise, <see langword="fal
 542    /// </returns>
 543    /// <remarks>
 544    /// This check is useful for quickly identifying colors that require no alpha blending or compositing in rendering
 545    /// pipelines.
 546    /// </remarks>
 1547    public bool IsOpaque() => A.Value is 255;
 548
 549    /// <summary>Determines whether the current color is fully transparent.</summary>
 550    /// <returns>
 551    /// <see langword="true" /> if the alpha channel (<see cref="A" />) equals <c>0</c>; otherwise, <see langword="false
 552    /// </returns>
 553    /// <remarks>A fully transparent color contributes no visible effect when composited over other surfaces.</remarks>
 3139554    public bool IsTransparent() => A.Value is 0;
 555
 556    /// <summary>Parses a color string into a new <see cref="HexColor" />.</summary>
 557    /// <param name="value">The color string to parse.</param>
 558    /// <returns>A new <see cref="HexColor" />.</returns>
 1559    public static HexColor Parse(string value) => new(value: value);
 560
 561    /// <summary>
 562    /// Parses a normalized alpha component from a string value in the range [0, 1] and converts it to a <see cref="HexB
 563    /// representation.
 564    /// </summary>
 565    /// <param name="value">
 566    /// The string containing the alpha value to parse. Leading and trailing whitespace are ignored. The value must repr
 567    /// finite number within the inclusive range [0, 1].
 568    /// </param>
 569    /// <returns>A <see cref="HexByte" /> corresponding to the parsed and clamped alpha value.</returns>
 570    /// <exception cref="AryArgumentException">
 571    /// Thrown when <paramref name="value" /> cannot be parsed as a finite <see cref="double" /> or is outside the [0, 1
 572    /// range.
 573    /// </exception>
 574    private static HexByte ParseAlpha(string value)
 575    {
 3576        var trimmed = value.Trim();
 577
 3578        if (!double.TryParse(
 3579                s: trimmed, style: NumberStyles.Float, provider: CultureInfo.InvariantCulture, result: out var alpha
 3580            ) ||
 3581            !double.IsFinite(d: alpha))
 582        {
 583            // Code Coverage: This is unreachable code.
 0584            throw new AryArgumentException(message: $"Invalid alpha value: {value}", argName: nameof(value));
 585        }
 586
 3587        AryGuard.InRange(value: alpha, min: 0.0, max: 1.0, argName: nameof(value));
 588
 3589        return HexByte.FromNormalized(value: alpha);
 590    }
 591
 592    /// <summary>
 593    /// Parses an integer channel value from a string and returns it as a <see cref="HexByte" />. The value is expected 
 594    /// represent an 8-bit channel in the range [0, 255].
 595    /// </summary>
 596    /// <param name="value">The string containing the channel value to parse.</param>
 597    /// <returns>A <see cref="HexByte" /> corresponding to the parsed channel value.</returns>
 598    /// <exception cref="AryArgumentException">
 599    /// Thrown when <paramref name="value" /> cannot be parsed as a byte or is outside the valid 0–255 range.
 600    /// </exception>
 601    private static HexByte ParseByte(string value)
 9602        => byte.TryParse(
 9603            s: value, style: NumberStyles.Integer, provider: CultureInfo.InvariantCulture, result: out var byteValue
 9604        )
 9605            ? new HexByte(value: byteValue)
 9606            : throw new AryArgumentException(message: $"Byte theme is out of range: {value}", argName: nameof(value));
 607
 608    /// <summary>
 609    /// Parses a color channel from a string that may be expressed either as an absolute value or as a percentage. Perce
 610    /// values are normalized to the range [0, 1] before being converted to a <see cref="HexByte" />.
 611    /// </summary>
 612    /// <param name="value">
 613    /// The channel string to parse. If it ends with <c>'%'</c>, it is interpreted as a percentage in the range [0, 100]
 614    /// otherwise, it is parsed as an integer channel value in the range [0, 255].
 615    /// </param>
 616    /// <returns>A <see cref="HexByte" /> representing the parsed channel value.</returns>
 617    /// <exception cref="AryArgumentException">
 618    /// Thrown when <paramref name="value" /> cannot be parsed as a valid channel value or percentage, or when the numer
 619    /// percentage is outside the 0–100 range.
 620    /// </exception>
 621    private static HexByte ParseChannel(string value)
 622    {
 12623        var trimmed = value.Trim();
 624
 12625        if (!trimmed.EndsWith(value: '%'))
 626        {
 9627            return ParseByte(value: trimmed);
 628        }
 629
 3630        var text = trimmed.TrimEnd(trimChar: '%').Trim();
 631
 3632        if (!double.TryParse(
 3633                s: text, style: NumberStyles.Float, provider: CultureInfo.InvariantCulture, result: out var channel
 3634            ) ||
 3635            !double.IsFinite(d: channel))
 636        {
 637            // Code Coverage: This is unreachable code.
 0638            throw new AryArgumentException(message: $"Invalid channel percentage: {value}", argName: nameof(value));
 639        }
 640
 3641        AryGuard.InRange(value: channel, min: 0.0, max: 100.0, argName: nameof(value));
 642
 3643        return HexByte.FromNormalized(value: channel / 100.0);
 644    }
 645
 646    /// <summary>
 647    /// Parses a hexadecimal color string and outputs channel components. Accepts <c>#RGB</c>, <c>#RGBA</c>, <c>#RRGGBB<
 648    /// and <c>#RRGGBBAA</c>.
 649    /// </summary>
 650    /// <param name="value">The hex color string.</param>
 651    /// <param name="red">The resulting red component.</param>
 652    /// <param name="green">The resulting green component.</param>
 653    /// <param name="blue">The resulting blue component.</param>
 654    /// <param name="alpha">The resulting alpha component.</param>
 655    /// <exception cref="AryArgumentException">Thrown when the format is invalid or contains non-hex digits.</exception>
 656    private static void ParseHex(string value,
 657        out HexByte red,
 658        out HexByte green,
 659        out HexByte blue,
 660        out HexByte alpha)
 661    {
 13005662        var match = HexColorPattern.Match(input: value.Trim());
 663
 13005664        if (!match.Success)
 665        {
 4666            throw new AryArgumentException(message: $"Invalid hex color format: {value}", argName: nameof(value));
 667        }
 668
 13001669        var hexValue = match.Groups[groupnum: 1].Value;
 670
 13001671        switch (hexValue.Length)
 672        {
 673            case 3:
 1674                Span<char> buf3 = stackalloc char[8];
 1675                buf3[index: 0] = hexValue[index: 0];
 1676                buf3[index: 1] = hexValue[index: 0];
 1677                buf3[index: 2] = hexValue[index: 1];
 1678                buf3[index: 3] = hexValue[index: 1];
 1679                buf3[index: 4] = hexValue[index: 2];
 1680                buf3[index: 5] = hexValue[index: 2];
 1681                buf3[index: 6] = 'F';
 1682                buf3[index: 7] = 'F';
 1683                hexValue = new string(value: buf3);
 684
 1685                break;
 686
 687            case 4:
 1688                Span<char> buf4 = stackalloc char[8];
 1689                buf4[index: 0] = hexValue[index: 0];
 1690                buf4[index: 1] = hexValue[index: 0];
 1691                buf4[index: 2] = hexValue[index: 1];
 1692                buf4[index: 3] = hexValue[index: 1];
 1693                buf4[index: 4] = hexValue[index: 2];
 1694                buf4[index: 5] = hexValue[index: 2];
 1695                buf4[index: 6] = hexValue[index: 3];
 1696                buf4[index: 7] = hexValue[index: 3];
 1697                hexValue = new string(value: buf4);
 698
 1699                break;
 700
 701            case 6:
 45702                hexValue += "FF";
 703
 704                break;
 705
 706            case 8:
 707                break;
 708        }
 709
 13001710        var r = byte.Parse(s: hexValue[..2], style: NumberStyles.HexNumber, provider: CultureInfo.InvariantCulture);
 711
 13001712        var g = byte.Parse(
 13001713            s: hexValue.AsSpan(start: 2, length: 2), style: NumberStyles.HexNumber,
 13001714            provider: CultureInfo.InvariantCulture
 13001715        );
 716
 13001717        var b = byte.Parse(
 13001718            s: hexValue.AsSpan(start: 4, length: 2), style: NumberStyles.HexNumber,
 13001719            provider: CultureInfo.InvariantCulture
 13001720        );
 721
 13001722        var a = byte.Parse(
 13001723            s: hexValue.AsSpan(start: 6, length: 2), style: NumberStyles.HexNumber,
 13001724            provider: CultureInfo.InvariantCulture
 13001725        );
 726
 13001727        red = new HexByte(value: r);
 13001728        green = new HexByte(value: g);
 13001729        blue = new HexByte(value: b);
 13001730        alpha = new HexByte(value: a);
 13001731    }
 732
 733    /// <summary>Parses an HSVA string and outputs equivalent RGBA channel components.</summary>
 734    /// <param name="value">The HSVA string to parse.</param>
 735    /// <param name="red">The resulting red component.</param>
 736    /// <param name="green">The resulting green component.</param>
 737    /// <param name="blue">The resulting blue component.</param>
 738    /// <param name="alpha">The resulting alpha component.</param>
 739    /// <exception cref="AryArgumentException">Thrown when the string is invalid or values are out of range.</exception>
 740    private static void ParseHsva(string value,
 741        out HexByte red,
 742        out HexByte green,
 743        out HexByte blue,
 744        out HexByte alpha)
 745    {
 7746        var match = HsvaPattern.Match(input: value);
 747
 7748        if (!match.Success)
 749        {
 6750            match = HsvPattern.Match(input: value);
 751
 6752            if (!match.Success)
 753            {
 2754                throw new AryArgumentException(message: $"Invalid HSV(A) color: {value}", argName: nameof(value));
 755            }
 756        }
 757
 5758        var h = ParseHue(value: match.Groups[groupnum: 1].Value);
 5759        var s = ParsePercent(value: match.Groups[groupnum: 2].Value);
 4760        var v = ParsePercent(value: match.Groups[groupnum: 3].Value);
 761
 4762        var a = match.Groups[groupname: "alpha"].Success
 4763            ? ParseAlpha(value: match.Groups[groupname: "alpha"].Value)
 4764            : new HexByte(value: 255);
 765
 4766        var color = HsvaToRgba(hue: h, saturation: s, value: v, alpha: a);
 767
 4768        red = color.R;
 4769        green = color.G;
 4770        blue = color.B;
 4771        alpha = color.A;
 4772    }
 773
 774    /// <summary>Parses a hue theme from a string.</summary>
 775    /// <param name="value">The string containing the hue theme.</param>
 776    /// <returns>The parsed hue (not yet normalized).</returns>
 777    /// <exception cref="AryArgumentException">Thrown when the hue theme is invalid.</exception>
 778    private static double ParseHue(string value)
 5779        => !double.TryParse(
 5780                s: value, style: NumberStyles.Float, provider: CultureInfo.InvariantCulture, result: out var hue
 5781            ) ||
 5782            !double.IsFinite(d: hue)
 5783                ? throw new AryArgumentException(message: $"Invalid hue theme: {value}", argName: nameof(value))
 5784                : hue;
 785
 786    /// <summary>
 787    /// Parses a percentage-like string into a fractional value in the range [0, 1]. Inputs may be given either as a raw
 788    /// fraction (e.g., <c>0.5</c>) or as a percentage (e.g., <c>50</c> or <c>50%</c>).
 789    /// </summary>
 790    /// <param name="value">
 791    /// The string containing the percentage. If it ends with <c>'%'</c>, it is treated as a percentage between 0 and 10
 792    /// otherwise values ≤ 1 are interpreted as fractions and larger values as percentages.
 793    /// </param>
 794    /// <returns>The parsed percentage expressed as a normalized fraction in the range [0, 1].</returns>
 795    /// <exception cref="AryArgumentException">
 796    /// Thrown when <paramref name="value" /> cannot be parsed as a finite <see cref="double" /> or when the resulting
 797    /// percentage is outside the 0–100 range.
 798    /// </exception>
 799    private static double ParsePercent(string value)
 800    {
 9801        var trimmed = value.Trim();
 9802        var hadPercent = trimmed.EndsWith(value: "%", comparisonType: StringComparison.Ordinal);
 803
 9804        var numericText = hadPercent
 9805            ? trimmed.TrimEnd(trimChar: '%').Trim()
 9806            : trimmed;
 807
 9808        if (!double.TryParse(
 9809                s: numericText, style: NumberStyles.Float, provider: CultureInfo.InvariantCulture,
 9810                result: out var number
 9811            ) ||
 9812            !double.IsFinite(d: number))
 813        {
 814            // Code Coverage: This is unreachable code.
 0815            throw new AryArgumentException(message: $"Invalid percentage value: {value}", argName: nameof(value));
 816        }
 817
 9818        var percent = hadPercent
 9819            ? number
 9820            : number <= 1.0
 9821                ? number * 100.0
 9822                : number;
 823
 9824        AryGuard.InRange(value: percent, min: 0.0, max: 100.0, argName: nameof(value));
 825
 8826        return percent / 100.0;
 827    }
 828
 829    /// <summary>Parses an RGBA functional string and outputs component channels.</summary>
 830    /// <param name="value">The RGBA string to parse.</param>
 831    /// <param name="red">The resulting red component.</param>
 832    /// <param name="green">The resulting green component.</param>
 833    /// <param name="blue">The resulting blue component.</param>
 834    /// <param name="alpha">The resulting alpha component.</param>
 835    /// <exception cref="AryArgumentException">Thrown when the input is invalid.</exception>
 836    private static void ParseRgba(string value,
 837        out HexByte red,
 838        out HexByte green,
 839        out HexByte blue,
 840        out HexByte alpha)
 841    {
 5842        var match = RgbaPattern.Match(input: value);
 843
 5844        if (!match.Success)
 845        {
 4846            match = RgbPattern.Match(input: value);
 847
 4848            if (!match.Success)
 849            {
 2850                match = RgbaCss4Pattern.Match(input: value);
 851
 2852                if (!match.Success)
 853                {
 1854                    throw new AryArgumentException(message: $"Invalid RGB(A) color: {value}", argName: nameof(value));
 855                }
 856            }
 857        }
 858
 4859        red = ParseChannel(value: match.Groups[groupnum: 1].Value);
 4860        green = ParseChannel(value: match.Groups[groupnum: 2].Value);
 4861        blue = ParseChannel(value: match.Groups[groupnum: 3].Value);
 862
 4863        alpha = match.Groups[groupname: "alpha"].Success
 4864            ? match.Groups[groupname: "alpha"].Value.TrimEnd().EndsWith(
 4865                value: "%", comparisonType: StringComparison.Ordinal
 4866            )
 4867                ? HexByte.FromNormalized(
 4868                    value: Math.Clamp(
 4869                        value: double.Parse(
 4870                            s: match.Groups[groupname: "alpha"].Value.TrimEnd(trimChar: '%'),
 4871                            style: NumberStyles.Float, provider: CultureInfo.InvariantCulture
 4872                        ) / 100.0, min: 0.0, max: 1.0
 4873                    )
 4874                )
 4875                : ParseAlpha(value: match.Groups[groupname: "alpha"].Value)
 4876            : new HexByte(value: 255);
 4877    }
 878
 879    /// <summary>
 880    /// Converts the current RGB color components (<see cref="R" />, <see cref="G" />, <see cref="B" />) into their
 881    /// corresponding HSV (Hue, Saturation, Value) representation.
 882    /// </summary>
 883    /// <param name="hue">
 884    /// The resulting hue component, expressed in degrees within the range [0, 360). A theme of <c>0</c> represents red,
 885    /// <c>120</c> represents green, and <c>240</c> represents blue.
 886    /// </param>
 887    /// <param name="saturation">
 888    /// The resulting saturation component, a normalized theme in the range [0, 1], where <c>0</c> indicates a shade of 
 889    /// and <c>1</c> represents full color intensity.
 890    /// </param>
 891    /// <param name="value">
 892    /// The resulting theme (brightness) component, a normalized theme in the range [0, 1], where <c>0</c> represents bl
 893    /// <c>1</c> represents the brightest form of the color.
 894    /// </param>
 895    /// <remarks>
 896    /// This method performs a precise RGB → HSV conversion without floating-point equality comparisons. The dominant co
 897    /// channel is determined from the original byte values to prevent loss of precision. When all RGB components are eq
 898    /// shade of gray), the hue is set to <c>0</c> by convention.
 899    /// </remarks>
 900    private void RgbToHsv(out double hue, out double saturation, out double value)
 901    {
 902        // Determine max/min from bytes for sector & delta, then scale once:
 1386190903        var red = R.Value;
 1386190904        var green = G.Value;
 1386190905        var blue = B.Value;
 906
 1386190907        var maxByte = Math.Max(val1: red, val2: Math.Max(val1: green, val2: blue));
 1386190908        var minByte = Math.Min(val1: red, val2: Math.Min(val1: green, val2: blue));
 909
 1386190910        var max = maxByte / 255.0;
 1386190911        var min = minByte / 255.0;
 1386190912        var delta = max - min;
 913
 1386190914        var rN = red / 255.0;
 1386190915        var gN = green / 255.0;
 1386190916        var bN = blue / 255.0;
 917
 918        // Value (V)
 1386190919        value = max;
 920
 921        // Saturation (S)
 1386190922        saturation = max <= 0.0
 1386190923            ? 0.0
 1386190924            : delta / max;
 925
 926        // Hue (H)
 1386190927        if (delta is 0.0)
 928        {
 929            // Gray — hue undefined; choose 0 by convention
 1101153930            hue = 0.0;
 931
 1101153932            return;
 933        }
 934
 935        // Pick sector by the *byte* that was the maximum (exact compare, stable on ties).
 936        // Tie-breaking falls back to the first true branch in this order: R, then G, then B.
 285037937        if (red >= green && red >= blue)
 938        {
 120836939            hue = 60.0 * ((gN - bN) / delta);
 940        }
 164201941        else if (green >= red && green >= blue)
 942        {
 85721943            hue = 60.0 * ((bN - rN) / delta + 2.0);
 944        }
 945        else
 946        {
 78480947            hue = 60.0 * ((rN - gN) / delta + 4.0);
 948        }
 949
 950        // Normalize hue to [0, 360)
 285037951        if (hue < 0.0)
 952        {
 33460953            hue += 360.0;
 954        }
 285037955    }
 956
 957    /// <summary>
 958    /// Binary-search mixing of a starting foreground toward a pole (black or white) in sRGB, returning the closest solu
 959    /// that meets—or best-approaches—the target contrast ratio.
 960    /// </summary>
 961    /// <param name="pole">Target pole (typically Black or White).</param>
 962    /// <param name="background">Background color (opaque).</param>
 963    /// <param name="minimumRatio">Target contrast ratio.</param>
 964    /// <returns>The resolution result for this pole.</returns>
 965    private ContrastResult SearchTowardPole(HexColor pole, HexColor background, double minimumRatio)
 966    {
 967        const int iterations = 18;
 968        const double eps = 1e-4;
 969
 9590970        var bestRatio = -1.0;
 9590971        var bestColor = this;
 972
 9590973        var met = false;
 9590974        var low = 0.0;
 9590975        var high = 1.0;
 976
 268520977        for (var i = 0; i < iterations; i++)
 978        {
 134260979            var mid = Math.Clamp(value: 0.5 * (low + high), min: 0.0, max: 1.0);
 134260980            var candidate = ToLerpLinearPreserveAlpha(end: pole, factor: mid);
 134260981            var ratio = candidate.ContrastRatio(background: background);
 982
 134260983            if (ratio > bestRatio)
 984            {
 24573985                bestRatio = ratio;
 24573986                bestColor = candidate;
 987            }
 988
 134260989            if (ratio >= minimumRatio)
 990            {
 41437991                met = true;
 41437992                high = mid;
 993            }
 994            else
 995            {
 92823996                low = mid;
 997            }
 998
 134260999            if (high - low < eps)
 1000            {
 1001                break;
 1002            }
 1003        }
 1004
 95901005        var finalColor = met
 95901006            ? ToLerpLinearPreserveAlpha(end: pole, factor: high)
 95901007            : bestColor;
 1008
 95901009        var finalRatio = finalColor.ContrastRatio(background: background);
 1010
 95901011        return new ContrastResult(ForegroundColor: finalColor, ContrastRatio: finalRatio, IsMinimumMet: met);
 1012    }
 1013
 1014    /// <summary>
 1015    /// Binary search along the HSV theme rail (holding H and S constant) to find the minimum-change V that meets a requ
 1016    /// contrast ratio; returns the best-approaching candidate when unreachable.
 1017    /// </summary>
 1018    /// <param name="direction"><c>+1</c> to brighten; <c>-1</c> to darken.</param>
 1019    /// <param name="background">Background color.</param>
 1020    /// <param name="minimumRatio">Target contrast ratio.</param>
 1021    /// <returns>Resolution result for this search branch.</returns>
 1022    private ContrastResult SearchValueRail(int direction, HexColor background, double minimumRatio)
 1023    {
 1024        const int iterations = 18;
 1025        const double eps = 1e-4;
 1026
 581881027        var bestRatio = -1.0;
 581881028        var bestColor = FromHsva(hue: H, saturation: S, value: V, alpha: A.ToNormalized());
 1029
 1030        double low, high;
 581881031        double? found = null;
 1032
 581881033        if (direction > 0)
 1034        {
 326581035            low = V;
 326581036            high = 1.0;
 1037        }
 1038        else
 1039        {
 255301040            low = 0.0;
 255301041            high = V;
 1042        }
 1043
 14370001044        for (var i = 0; i < iterations; i++)
 1045        {
 7185001046            var mid = Math.Clamp(value: 0.5 * (low + high), min: 0.0, max: 1.0);
 7185001047            var candidate = FromHsva(hue: H, saturation: S, value: mid, alpha: A.ToNormalized());
 7185001048            var ratio = candidate.ContrastRatio(background: background);
 1049
 7185001050            if (ratio > bestRatio)
 1051            {
 2234031052                bestRatio = ratio;
 2234031053                bestColor = candidate;
 1054            }
 1055
 7185001056            if (ratio >= minimumRatio)
 1057            {
 5099711058                found = mid;
 5099711059                high = mid;
 1060            }
 1061            else
 1062            {
 2085291063                low = mid;
 1064            }
 1065
 7185001066            if (high - low < eps)
 1067            {
 1068                break;
 1069            }
 1070        }
 1071
 581881072        if (!found.HasValue)
 1073        {
 47951074            return new ContrastResult(ForegroundColor: bestColor, ContrastRatio: bestRatio, IsMinimumMet: false);
 1075        }
 1076
 533931077        var finalColor = FromHsva(hue: H, saturation: S, value: high, alpha: A.ToNormalized());
 533931078        var finalRatio = finalColor.ContrastRatio(background: background);
 1079
 533931080        return new ContrastResult(ForegroundColor: finalColor, ContrastRatio: finalRatio, IsMinimumMet: true);
 1081    }
 1082
 1083    /// <summary>
 1084    /// Creates a new <see cref="HexColor" /> instance using the current red, green, and blue components, but with the
 1085    /// specified alpha (opacity) theme.
 1086    /// </summary>
 1087    /// <param name="alpha">
 1088    /// The new alpha component, expressed as a byte in the range <c>[0, 255]</c>, where <c>0</c> represents full transp
 1089    /// and <c>255</c> represents full opacity.
 1090    /// </param>
 1091    /// <returns>
 1092    /// A new <see cref="HexColor" /> that is identical in color to the current instance but with the provided alpha the
 1093    /// applied.
 1094    /// </returns>
 1095    /// <remarks>
 1096    /// This method does not modify the current instance; it returns a new color structure with the updated transparency
 1097    /// component.
 1098    /// </remarks>
 660221099    public HexColor SetAlpha(byte alpha) => new(red: R, green: G, blue: B, alpha: new HexByte(value: alpha));
 1100
 1101    /// <summary>
 1102    /// Adjusts the perceived lightness of the color by shifting its theme (<see cref="V" />) upward or downward dependi
 1103    /// whether the color is currently light or dark.
 1104    /// </summary>
 1105    /// <param name="delta">
 1106    /// The magnitude of change to apply to <see cref="V" />, expressed as a fraction in <c>[0,1]</c>. Positive values l
 1107    /// dark colors and darken light colors automatically.
 1108    /// </param>
 1109    /// <returns>A new <see cref="HexColor" /> instance with the adjusted brightness level.</returns>
 1110    public HexColor ShiftLightness(double delta = 0.05)
 1111    {
 2046261112        var direction = V >= 0.5
 2046261113            ? -1.0
 2046261114            : 1.0;
 1115
 2046261116        return FromHsva(
 2046261117            hue: H, saturation: S, value: Math.Clamp(value: V + delta * direction, min: 0.0, max: 1.0),
 2046261118            alpha: A.ToNormalized()
 2046261119        );
 1120    }
 1121
 1122    /// <summary>
 1123    /// Converts the current color into an accent variant suitable for emphasizing interactive or highlighted UI element
 1124    /// Internally adjusts perceived lightness using <see cref="ShiftLightness(double)" /> with a stronger delta.
 1125    /// </summary>
 1126    /// <returns>A new <see cref="HexColor" /> representing the accent variant of the current color.</returns>
 1128491127    public HexColor ToAccent() => ShiftLightness(delta: 0.6);
 1128
 1129    /// <summary>
 1130    /// Produces a disabled-state variant of the current color by reducing saturation and slightly flattening perceived
 1131    /// contrast. Intended for controls that are present but not interactive.
 1132    /// </summary>
 1133    /// <returns>A new <see cref="HexColor" /> representing the disabled-state color.</returns>
 80611134    public HexColor ToDisabled() => Desaturate(desaturateBy: 0.6);
 1135
 1136    /// <summary>
 1137    /// Produces a dragged-state variant of the current color by adjusting lightness for improved visual feedback while 
 1138    /// draggable element is being moved.
 1139    /// </summary>
 1140    /// <returns>A new <see cref="HexColor" /> representing the dragged-state color.</returns>
 80611141    public HexColor ToDragged() => ShiftLightness(delta: 0.18);
 1142
 1143    /// <summary>
 1144    /// Produces a slightly elevated variant of the current color corresponding to the first elevation level in layered 
 1145    /// surfaces.
 1146    /// </summary>
 1147    /// <returns>A new <see cref="HexColor" /> representing the elevation 1 color.</returns>
 6191148    public HexColor ToElevation1() => ShiftLightness(delta: 0.02);
 1149
 1150    /// <summary>
 1151    /// Produces an elevated variant of the current color corresponding to the second elevation level in layered UI surf
 1152    /// </summary>
 1153    /// <returns>A new <see cref="HexColor" /> representing the elevation 2 color.</returns>
 6191154    public HexColor ToElevation2() => ShiftLightness(delta: 0.04);
 1155
 1156    /// <summary>
 1157    /// Produces an elevated variant of the current color corresponding to the third elevation level in layered UI surfa
 1158    /// </summary>
 1159    /// <returns>A new <see cref="HexColor" /> representing the elevation 3 color.</returns>
 6191160    public HexColor ToElevation3() => ShiftLightness(delta: 0.06);
 1161
 1162    /// <summary>
 1163    /// Produces an elevated variant of the current color corresponding to the fourth elevation level in layered UI surf
 1164    /// </summary>
 1165    /// <returns>A new <see cref="HexColor" /> representing the elevation 4 color.</returns>
 6191166    public HexColor ToElevation4() => ShiftLightness(delta: 0.08);
 1167
 1168    /// <summary>
 1169    /// Produces an elevated variant of the current color corresponding to the fifth elevation level in layered UI surfa
 1170    /// </summary>
 1171    /// <returns>A new <see cref="HexColor" /> representing the elevation 5 color.</returns>
 6191172    public HexColor ToElevation5() => ShiftLightness(delta: 0.1);
 1173
 1174    /// <summary>
 1175    /// Produces a focused-state variant of the current color, typically used to render focus rings or outlines with enh
 1176    /// prominence relative to the base color.
 1177    /// </summary>
 1178    /// <returns>A new <see cref="HexColor" /> representing the focused-state color.</returns>
 80611179    public HexColor ToFocused() => ShiftLightness(delta: 0.1);
 1180
 1181    /// <summary>
 1182    /// Derives a high-contrast foreground variant from the current color by strongly adjusting lightness. Intended for 
 1183    /// iconography rendered on top of the base color.
 1184    /// </summary>
 1185    /// <returns>A new <see cref="HexColor" /> suitable for use as a foreground color.</returns>
 564251186    public HexColor ToForeground() => ShiftLightness(delta: 0.9);
 1187
 1188    /// <summary>
 1189    /// Produces a hover-state variant of the current color by slightly adjusting lightness to provide visual feedback w
 1190    /// pointer hovers over an interactive element.
 1191    /// </summary>
 1192    /// <returns>A new <see cref="HexColor" /> representing the hovered-state color.</returns>
 80611193    public HexColor ToHovered() => ShiftLightness(delta: 0.06);
 1194
 1195    /// <summary>
 1196    /// Gamma-correct (linear-light) interpolation between two colors, including alpha. Uses linearization via
 1197    /// <see cref="HexByte.ToLerpLinearHexByte" /> for perceptually better blends than plain sRGB lerp.
 1198    /// </summary>
 1199    /// <param name="end">InlineEnd color.</param>
 1200    /// <param name="factor">Interpolation factor clamped to <c>[0, 1]</c>.</param>
 1201    /// <returns>The interpolated color.</returns>
 1202    public HexColor ToLerpLinear(HexColor end, double factor)
 11203        => new(
 11204            red: R.ToLerpLinearHexByte(end: end.R, factor: factor),
 11205            green: G.ToLerpLinearHexByte(end: end.G, factor: factor),
 11206            blue: B.ToLerpLinearHexByte(end: end.B, factor: factor),
 11207            alpha: A.ToLerpHexByte(end: end.A, factor: factor)
 11208        );
 1209
 1210    /// <summary>
 1211    /// Gamma-correct (linear-light) interpolation between two colors, preserving alpha. Uses linearization via
 1212    /// <see cref="HexByte.ToLerpLinearHexByte" /> for perceptually better blends than plain sRGB lerp.
 1213    /// </summary>
 1214    /// <param name="end">InlineEnd color.</param>
 1215    /// <param name="factor">Interpolation factor clamped to <c>[0, 1]</c>.</param>
 1216    /// <returns>The interpolated color.</returns>
 1217    public HexColor ToLerpLinearPreserveAlpha(HexColor end, double factor)
 1399191218        => new(
 1399191219            red: R.ToLerpLinearHexByte(end: end.R, factor: factor),
 1399191220            green: G.ToLerpLinearHexByte(end: end.G, factor: factor),
 1399191221            blue: B.ToLerpLinearHexByte(end: end.B, factor: factor),
 1399191222            alpha: A
 1399191223        );
 1224
 1225    /// <summary>
 1226    /// Produces a pressed-state variant of the current color by adjusting lightness to convey a deeper interaction stat
 1227    /// a control is actively pressed or engaged.
 1228    /// </summary>
 1229    /// <returns>A new <see cref="HexColor" /> representing the pressed-state color.</returns>
 80611230    public HexColor ToPressed() => ShiftLightness(delta: 0.14);
 1231
 1232    /// <summary>Computes WCAG relative luminance from an opaque sRGB color.</summary>
 1233    /// <returns>Relative luminance in <c>[0, 1]</c>.</returns>
 1234    /// <remarks>
 1235    /// Relative luminance is computed using the linearized sRGB formula: <c>0.2126 × R + 0.7152 × G + 0.0722 × B</c>.
 1236    /// </remarks>
 1237    public double ToRelativeLuminance()
 1238    {
 24216341239        var redLuminance = R.ToSrgbLinearValue();
 24216341240        var greenLuminance = G.ToSrgbLinearValue();
 24216341241        var blueLuminance = B.ToSrgbLinearValue();
 1242
 24216341243        return 0.2126 * redLuminance + 0.7152 * greenLuminance + 0.0722 * blueLuminance;
 1244    }
 1245
 1246    /// <summary>Returns a hexadecimal string representation of the color in the form <c>#RRGGBBAA</c>.</summary>
 1247    /// <returns>The string representation.</returns>
 306221248    public override string ToString() => $"#{R}{G}{B}{A}";
 1249
 1250    /// <summary>
 1251    /// Produces a visited-state variant of the current color by modestly reducing saturation while preserving general h
 1252    /// brightness, making it suitable for indicating visited links or previously activated actions.
 1253    /// </summary>
 1254    /// <returns>A new <see cref="HexColor" /> representing the visited-state color.</returns>
 80611255    public HexColor ToVisited() => Desaturate(desaturateBy: 0.3);
 1256
 1257    /// <summary>Attempts to parse the specified color string.</summary>
 1258    /// <param name="value">The color string to parse.</param>
 1259    /// <param name="result">When this method returns, contains the parsed color or default on failure.</param>
 1260    /// <returns><see langword="true" /> if parsing succeeded; otherwise, <see langword="false" />.</returns>
 1261    public static bool TryParse(string? value, out HexColor result)
 1262    {
 31263        if (string.IsNullOrWhiteSpace(value: value))
 1264        {
 11265            result = default(HexColor);
 1266
 11267            return false;
 1268        }
 1269
 1270        try
 1271        {
 21272            result = new HexColor(value: value);
 1273
 11274            return true;
 1275        }
 11276        catch
 1277        {
 11278            result = default(HexColor);
 1279
 11280            return false;
 1281        }
 21282    }
 1283
 1284    /// <summary>
 1285    /// Attempts to resolve a named color (e.g., <c>"Red500"</c>) from the global color registry and returns its RGBA ch
 1286    /// components.
 1287    /// </summary>
 1288    /// <param name="value">
 1289    /// The color name to look up. Comparison is case-insensitive (per the dictionary’s comparer). Leading/trailing whit
 1290    /// is ignored.
 1291    /// </param>
 1292    /// <param name="red">On success, receives the resolved red channel; otherwise <c>0</c>.</param>
 1293    /// <param name="green">On success, receives the resolved green channel; otherwise <c>0</c>.</param>
 1294    /// <param name="blue">On success, receives the resolved blue channel; otherwise <c>0</c>.</param>
 1295    /// <param name="alpha">On success, receives the resolved alpha channel; otherwise <c>0</c> (transparent).</param>
 1296    /// <returns><see langword="true" /> if the name maps to a known color; otherwise <see langword="false" />.</returns
 1297    /// <remarks>
 1298    /// On failure, all channel outputs are set to <c>0</c>. The lookup uses whatever <see cref="StringComparer" /> the
 1299    /// <c>Colors</c> dictionary was constructed with (e.g., <c>InvariantCultureIgnoreCase</c> or <c>OrdinalIgnoreCase</
 1300    /// </remarks>
 1301    private static bool TryParseColorName(string value,
 1302        out HexByte red,
 1303        out HexByte green,
 1304        out HexByte blue,
 1305        out HexByte alpha)
 1306    {
 91307        red = new HexByte();
 91308        green = new HexByte();
 91309        blue = new HexByte();
 91310        alpha = new HexByte();
 1311
 91312        if (!Colors.TryGet(name: value, value: out var color))
 1313        {
 51314            return false;
 1315        }
 1316
 41317        red = color.R;
 41318        green = color.G;
 41319        blue = color.B;
 41320        alpha = color.A;
 1321
 41322        return true;
 1323    }
 1324
 1325    /// <summary>
 1326    /// Chooses the initial direction to adjust HSV Value (V) for the foreground in order to locally increase contrast a
 1327    /// the background. Returns <c>+1</c> to brighten or <c>-1</c> to darken.
 1328    /// </summary>
 1329    /// <param name="background">Background color (opaque).</param>
 1330    /// <returns><c>+1</c> if brightening increases contrast more; otherwise <c>-1</c>.</returns>
 1331    private int ValueDirection(HexColor background)
 1332    {
 1333        const double step = 0.02; // 2% of the 0..1 range
 1334
 581881335        var up = FromHsva(
 581881336            hue: H, saturation: S, value: Math.Clamp(value: V + step, min: 0.0, max: 1.0), alpha: A.ToNormalized()
 581881337        );
 1338
 581881339        var down = FromHsva(
 581881340            hue: H, saturation: S, value: Math.Clamp(value: V - step, min: 0.0, max: 1.0), alpha: A.ToNormalized()
 581881341        );
 1342
 581881343        var ratioUp = up.ContrastRatio(background: background);
 581881344        var ratioDown = down.ContrastRatio(background: background);
 1345
 581881346        return ratioUp > ratioDown
 581881347            ? +1
 581881348            : -1;
 1349    }
 1350
 1351    /// <summary>Determines whether two <see cref="HexColor" /> values are equal.</summary>
 1352    /// <param name="left">The first theme to compare.</param>
 1353    /// <param name="right">The second theme to compare.</param>
 1354    /// <returns><see langword="true" /> if the values are equal; otherwise, <see langword="false" />.</returns>
 11355    public static bool operator ==(HexColor left, HexColor right) => left.Equals(other: right);
 1356
 1357    /// <summary>Determines whether one <see cref="HexColor" /> is greater than another.</summary>
 1358    /// <param name="left">The left operand.</param>
 1359    /// <param name="right">The right operand.</param>
 1360    /// <returns><see langword="true" /> if <paramref name="left" /> is greater; otherwise, <see langword="false" />.</r
 11361    public static bool operator >(HexColor left, HexColor right) => left.CompareTo(other: right) > 0;
 1362
 1363    /// <summary>Determines whether one <see cref="HexColor" /> is greater than or equal to another.</summary>
 1364    /// <param name="left">The left operand.</param>
 1365    /// <param name="right">The right operand.</param>
 1366    /// <returns>
 1367    /// <see langword="true" /> if <paramref name="left" /> is greater than or equal to <paramref name="right" />; other
 1368    /// <see langword="false" />.
 1369    /// </returns>
 11370    public static bool operator >=(HexColor left, HexColor right) => left.CompareTo(other: right) >= 0;
 1371
 1372    /// <summary>Implicitly converts a color string to a <see cref="HexColor" />.</summary>
 1373    /// <param name="hex">The color string to convert.</param>
 1374    /// <returns>The resulting <see cref="HexColor" />.</returns>
 311375    public static implicit operator HexColor(string hex) => new(value: hex);
 1376
 1377    /// <summary>Implicitly converts a <see cref="HexColor" /> to its string representation.</summary>
 1378    /// <param name="value">The color theme.</param>
 1379    /// <returns>The string representation.</returns>
 125521380    public static implicit operator string(HexColor value) => value.ToString();
 1381
 1382    /// <summary>Determines whether two <see cref="HexColor" /> values are not equal.</summary>
 1383    /// <param name="left">The first theme to compare.</param>
 1384    /// <param name="right">The second theme to compare.</param>
 1385    /// <returns><see langword="true" /> if the values are not equal; otherwise, <see langword="false" />.</returns>
 11386    public static bool operator !=(HexColor left, HexColor right) => !left.Equals(other: right);
 1387
 1388    /// <summary>Determines whether one <see cref="HexColor" /> is less than another.</summary>
 1389    /// <param name="left">The left operand.</param>
 1390    /// <param name="right">The right operand.</param>
 1391    /// <returns>
 1392    /// <see langword="true" /> if <paramref name="left" /> is less than <paramref name="right" />; otherwise,
 1393    /// <see langword="false" />.
 1394    /// </returns>
 11395    public static bool operator <(HexColor left, HexColor right) => left.CompareTo(other: right) < 0;
 1396
 1397    /// <summary>Determines whether one <see cref="HexColor" /> is less than or equal to another.</summary>
 1398    /// <param name="left">The left operand.</param>
 1399    /// <param name="right">The right operand.</param>
 1400    /// <returns>
 1401    /// <see langword="true" /> if <paramref name="left" /> is less than or equal to <paramref name="right" />; otherwis
 1402    /// <see langword="false" />.
 1403    /// </returns>
 11404    public static bool operator <=(HexColor left, HexColor right) => left.CompareTo(other: right) <= 0;
 1405}

Methods/Properties

.cctor()
.ctor()
.ctor(Allyaria.Theming.Types.HexByte,Allyaria.Theming.Types.HexByte,Allyaria.Theming.Types.HexByte,System.Nullable`1<Allyaria.Theming.Types.HexByte>)
.ctor(System.String)
get_A()
get_B()
get_G()
get_H()
get_R()
get_S()
get_V()
BlendValue(System.Double,System.Double)
ClampToByte(System.Double)
CompareTo(Allyaria.Theming.Types.HexColor)
ContrastRatio(Allyaria.Theming.Types.HexColor)
Desaturate(System.Double,System.Double)
EnsureContrast(Allyaria.Theming.Types.HexColor,System.Double)
Equals(Allyaria.Theming.Types.HexColor)
Equals(System.Object)
FromHsva(System.Double,System.Double,System.Double,System.Double)
GetHashCode()
HsvaToRgba(System.Double,System.Double,System.Double,Allyaria.Theming.Types.HexByte)
Invert()
IsDark()
IsLight()
IsOpaque()
IsTransparent()
Parse(System.String)
ParseAlpha(System.String)
ParseByte(System.String)
ParseChannel(System.String)
ParseHex(System.String,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&)
ParseHsva(System.String,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&)
ParseHue(System.String)
ParsePercent(System.String)
ParseRgba(System.String,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&)
RgbToHsv(System.Double&,System.Double&,System.Double&)
SearchTowardPole(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor,System.Double)
SearchValueRail(System.Int32,Allyaria.Theming.Types.HexColor,System.Double)
SetAlpha(System.Byte)
ShiftLightness(System.Double)
ToAccent()
ToDisabled()
ToDragged()
ToElevation1()
ToElevation2()
ToElevation3()
ToElevation4()
ToElevation5()
ToFocused()
ToForeground()
ToHovered()
ToLerpLinear(Allyaria.Theming.Types.HexColor,System.Double)
ToLerpLinearPreserveAlpha(Allyaria.Theming.Types.HexColor,System.Double)
ToPressed()
ToRelativeLuminance()
ToString()
ToVisited()
TryParse(System.String,Allyaria.Theming.Types.HexColor&)
TryParseColorName(System.String,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&,Allyaria.Theming.Types.HexByte&)
ValueDirection(Allyaria.Theming.Types.HexColor)
op_Equality(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor)
op_GreaterThan(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor)
op_GreaterThanOrEqual(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor)
op_Implicit(System.String)
op_Implicit(Allyaria.Theming.Types.HexColor)
op_Inequality(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor)
op_LessThan(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor)
op_LessThanOrEqual(Allyaria.Theming.Types.HexColor,Allyaria.Theming.Types.HexColor)