| | | 1 | | namespace Allyaria.Theming.Types; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Represents a single 8-bit color channel theme (0–255) used in RGBA color models. Provides parsing, formatting, |
| | | 5 | | /// comparisons, normalized conversions, and interpolation helpers. Interpolation helpers include a gamma-correct |
| | | 6 | | /// (linear-light) implementation for higher visual accuracy. |
| | | 7 | | /// </summary> |
| | | 8 | | public readonly struct HexByte : IComparable<HexByte>, IEquatable<HexByte> |
| | | 9 | | { |
| | | 10 | | /// <summary> |
| | | 11 | | /// Initializes a new instance of the <see cref="HexByte" /> struct with a theme of 0 (string form <c>"00"</c>). |
| | | 12 | | /// </summary> |
| | | 13 | | public HexByte() |
| | 82 | 14 | | : this(value: 0) { } |
| | | 15 | | |
| | | 16 | | /// <summary>Initializes a new instance of the <see cref="HexByte" /> struct using a byte theme.</summary> |
| | | 17 | | /// <param name="value">The byte theme to represent as hexadecimal.</param> |
| | 5206818 | 18 | | public HexByte(byte value) => Value = value; |
| | | 19 | | |
| | | 20 | | /// <summary>Initializes a new instance of the <see cref="HexByte" /> struct using a hexadecimal string.</summary> |
| | | 21 | | /// <param name="value"> |
| | | 22 | | /// The hexadecimal string representing the byte theme; accepts 1–2 hex characters (e.g., <c>"F"</c>, <c>"0A"</c>, |
| | | 23 | | /// <c>"ff"</c>). Whitespace is allowed and ignored. |
| | | 24 | | /// </param> |
| | | 25 | | /// <exception cref="AryArgumentException"> |
| | | 26 | | /// Thrown when <paramref name="value" /> is <see langword="null" />, whitespace only, contains non-hex characters, |
| | | 27 | | /// more than two hex characters after trimming. |
| | | 28 | | /// </exception> |
| | | 29 | | public HexByte(string value) |
| | | 30 | | { |
| | 12 | 31 | | AryGuard.NotNullOrWhiteSpace(value: value); |
| | | 32 | | |
| | 9 | 33 | | var span = value.AsSpan().Trim(); |
| | 9 | 34 | | AryGuard.InRange(value: span.Length, min: 1, max: 2, argName: nameof(value)); |
| | | 35 | | |
| | 8 | 36 | | Value = byte.TryParse( |
| | 8 | 37 | | s: span, style: NumberStyles.HexNumber, provider: CultureInfo.InvariantCulture, result: out var parsed |
| | 8 | 38 | | ) |
| | 8 | 39 | | ? parsed |
| | 8 | 40 | | : throw new AryArgumentException(message: $"Invalid hexadecimal string: '{value}'."); |
| | 6 | 41 | | } |
| | | 42 | | |
| | | 43 | | /// <summary>Gets the byte representation of the hexadecimal theme.</summary> |
| | 13583550 | 44 | | public byte Value { get; } |
| | | 45 | | |
| | | 46 | | /// <summary> |
| | | 47 | | /// Clamps a normalized alpha theme between 0.0 and 1.0 and converts it to a <see cref="HexByte" /> representation. |
| | | 48 | | /// </summary> |
| | | 49 | | /// <param name="value">The alpha theme to clamp (expected 0.0–1.0; values outside are clamped).</param> |
| | | 50 | | /// <returns>A <see cref="HexByte" /> corresponding to the clamped theme.</returns> |
| | | 51 | | public static HexByte ClampAlpha(double value) |
| | 3 | 52 | | => FromNormalized(value: Math.Clamp(value: value, min: 0.0, max: 1.0)); |
| | | 53 | | |
| | | 54 | | /// <summary>Compares this <see cref="HexByte" /> instance to another based on their byte values.</summary> |
| | | 55 | | /// <param name="other">The other <see cref="HexByte" /> instance to compare with.</param> |
| | | 56 | | /// <returns>An integer indicating the relative order of the objects being compared.</returns> |
| | 28 | 57 | | public int CompareTo(HexByte other) => Value.CompareTo(value: other.Value); |
| | | 58 | | |
| | | 59 | | /// <summary>Indicates whether the current object is equal to another object of the same type.</summary> |
| | | 60 | | /// <param name="other">An object to compare with this object.</param> |
| | | 61 | | /// <returns> |
| | | 62 | | /// <see langword="true" /> if the current object is equal to the <paramref name="other" /> parameter; otherwise, |
| | | 63 | | /// <see langword="false" />. |
| | | 64 | | /// </returns> |
| | 8986 | 65 | | public bool Equals(HexByte other) => Value == other.Value; |
| | | 66 | | |
| | | 67 | | /// <summary>Indicates whether this instance and a specified object are equal.</summary> |
| | | 68 | | /// <param name="obj">The object to compare with the current instance.</param> |
| | | 69 | | /// <returns> |
| | | 70 | | /// <see langword="true" /> if <paramref name="obj" /> and this instance are the same type and represent the same th |
| | | 71 | | /// otherwise, <see langword="false" />. |
| | | 72 | | /// </returns> |
| | 6 | 73 | | public override bool Equals(object? obj) => obj is HexByte other && Equals(other: other); |
| | | 74 | | |
| | | 75 | | /// <summary>Creates a <see cref="HexByte" /> from a normalized theme in the range [0, 1].</summary> |
| | | 76 | | /// <param name="value">A normalized channel theme between 0.0 and 1.0 inclusive.</param> |
| | | 77 | | /// <returns>A <see cref="HexByte" /> whose numeric theme corresponds to <paramref name="value" />·255.</returns> |
| | | 78 | | /// <exception cref="AryArgumentException"> |
| | | 79 | | /// Thrown when <paramref name="value" /> is not finite or lies outside the [0, 1] |
| | | 80 | | /// range. |
| | | 81 | | /// </exception> |
| | | 82 | | public static HexByte FromNormalized(double value) |
| | | 83 | | { |
| | 1167239 | 84 | | if (!double.IsFinite(d: value)) |
| | | 85 | | { |
| | 4 | 86 | | throw new AryArgumentException( |
| | 4 | 87 | | message: "Normalized theme must be a finite number.", argName: nameof(value) |
| | 4 | 88 | | ); |
| | | 89 | | } |
| | | 90 | | |
| | 1167235 | 91 | | AryGuard.InRange(value: value, min: 0.0, max: 1.0); |
| | | 92 | | |
| | 1167233 | 93 | | var b = (byte)Math.Clamp( |
| | 1167233 | 94 | | value: Math.Round(value: value * 255.0, mode: MidpointRounding.ToEven), min: 0, max: 255 |
| | 1167233 | 95 | | ); |
| | | 96 | | |
| | 1167233 | 97 | | return new HexByte(value: b); |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | /// <summary>Returns the hash code for this instance.</summary> |
| | | 101 | | /// <returns>A 32-bit signed integer that is the hash code for this instance.</returns> |
| | 10 | 102 | | public override int GetHashCode() => Value.GetHashCode(); |
| | | 103 | | |
| | | 104 | | /// <summary>Parses a hexadecimal string (1–2 hex characters) into a <see cref="HexByte" />.</summary> |
| | | 105 | | /// <param name="value">The hexadecimal string to parse. Whitespace is allowed and ignored.</param> |
| | | 106 | | /// <returns>A new <see cref="HexByte" /> representing the parsed theme.</returns> |
| | | 107 | | /// <exception cref="AryArgumentException"> |
| | | 108 | | /// Thrown if <paramref name="value" /> is <see langword="null" />, whitespace, or not a valid 1–2 character hex str |
| | | 109 | | /// </exception> |
| | 1 | 110 | | public static HexByte Parse(string value) => new(value: value); |
| | | 111 | | |
| | | 112 | | /// <summary> |
| | | 113 | | /// Linearly interpolates this channel in byte space (no gamma). Suitable for alpha coverage and UI opacity. |
| | | 114 | | /// </summary> |
| | | 115 | | /// <param name="end">The end byte theme (0–255).</param> |
| | | 116 | | /// <param name="factor">The interpolation factor; values are clamped to [0, 1]. Non-finite values are treated as 0. |
| | | 117 | | /// <returns>The interpolated sRGB channel byte.</returns> |
| | | 118 | | public byte ToLerpByte(byte end, double factor) |
| | | 119 | | { |
| | 10 | 120 | | var t = double.IsFinite(d: factor) |
| | 10 | 121 | | ? Math.Clamp(value: factor, min: 0.0, max: 1.0) |
| | 10 | 122 | | : 0.0; |
| | | 123 | | |
| | 10 | 124 | | return (byte)Math.Clamp( |
| | 10 | 125 | | value: Math.Round(value: Value + (end - Value) * t, mode: MidpointRounding.ToEven), min: 0, max: 255 |
| | 10 | 126 | | ); |
| | | 127 | | } |
| | | 128 | | |
| | | 129 | | /// <summary>Convenience alias of ToLerpByte; linear (not gamma-correct). Prefer this for alpha.</summary> |
| | | 130 | | /// <param name="end">The end byte theme (0–255).</param> |
| | | 131 | | /// <param name="factor">The interpolation factor; values are clamped to [0, 1]. Non-finite values are treated as 0. |
| | | 132 | | /// <returns>A new <see cref="HexByte" /> representing the interpolated channel theme.</returns> |
| | 2 | 133 | | public HexByte ToLerpHexByte(byte end, double factor) => new(value: ToLerpByte(end: end, factor: factor)); |
| | | 134 | | |
| | | 135 | | /// <summary> |
| | | 136 | | /// Computes a gamma-correct (linear-light) interpolation from this channel theme to <paramref name="end" />, return |
| | | 137 | | /// resulting sRGB 8-bit theme. |
| | | 138 | | /// </summary> |
| | | 139 | | /// <param name="end">The end byte theme (0–255).</param> |
| | | 140 | | /// <param name="factor">The interpolation factor; values are clamped to [0, 1]. Non-finite values are treated as 0. |
| | | 141 | | /// <returns>The interpolated sRGB channel byte.</returns> |
| | | 142 | | public byte ToLerpLinearByte(byte end, double factor) |
| | | 143 | | { |
| | 419765 | 144 | | var t = double.IsFinite(d: factor) |
| | 419765 | 145 | | ? Math.Clamp(value: factor, min: 0.0, max: 1.0) |
| | 419765 | 146 | | : 0.0; |
| | | 147 | | |
| | | 148 | | // sRGB -> linear |
| | | 149 | | static double ToLinear(byte b) |
| | | 150 | | { |
| | 839530 | 151 | | var c = b / 255.0; |
| | | 152 | | |
| | 839530 | 153 | | return c <= 0.04045 |
| | 839530 | 154 | | ? c / 12.92 |
| | 839530 | 155 | | : Math.Pow(x: (c + 0.055) / 1.055, y: 2.4); |
| | | 156 | | } |
| | | 157 | | |
| | | 158 | | // linear lerp |
| | 419765 | 159 | | var aL = ToLinear(b: Value); |
| | 419765 | 160 | | var bL = ToLinear(b: end); |
| | 419765 | 161 | | var l = aL + (bL - aL) * t; |
| | | 162 | | |
| | | 163 | | // linear -> sRGB |
| | | 164 | | static byte FromLinear(double l) |
| | | 165 | | { |
| | 419765 | 166 | | l = Math.Clamp(value: l, min: 0.0, max: 1.0); |
| | | 167 | | |
| | 419765 | 168 | | var c = l <= 0.0031308 |
| | 419765 | 169 | | ? l * 12.92 |
| | 419765 | 170 | | : 1.055 * Math.Pow(x: l, y: 1.0 / 2.4) - 0.055; |
| | | 171 | | |
| | 419765 | 172 | | return (byte)Math.Clamp( |
| | 419765 | 173 | | value: Math.Round(value: c * 255.0, mode: MidpointRounding.ToEven), min: 0, max: 255 |
| | 419765 | 174 | | ); |
| | | 175 | | } |
| | | 176 | | |
| | 419765 | 177 | | return FromLinear(l: l); |
| | | 178 | | } |
| | | 179 | | |
| | | 180 | | /// <summary> |
| | | 181 | | /// Produces an interpolated channel using gamma-correct (linear-light) interpolation. Use for sRGB color channels ( |
| | | 182 | | /// Not suitable for alpha coverage; use ToLerpHexByte for alpha. |
| | | 183 | | /// </summary> |
| | | 184 | | /// <param name="end">The target channel theme.</param> |
| | | 185 | | /// <param name="factor"> |
| | | 186 | | /// The interpolation factor; values are clamped to the range [0, 1]. Non-finite values are treated as 0. |
| | | 187 | | /// </param> |
| | | 188 | | /// <returns>A new <see cref="HexByte" /> representing the interpolated channel theme.</returns> |
| | | 189 | | public HexByte ToLerpLinearHexByte(HexByte end, double factor) |
| | 419761 | 190 | | => new(value: ToLerpLinearByte(end: end.Value, factor: factor)); |
| | | 191 | | |
| | | 192 | | /// <summary>Converts this channel theme to a normalized theme in the range [0, 1] via <c>Value / 255.0</c>.</summar |
| | | 193 | | /// <returns>The normalized channel theme.</returns> |
| | 1167209 | 194 | | public double ToNormalized() => Value / 255.0; |
| | | 195 | | |
| | | 196 | | /// <summary> |
| | | 197 | | /// Converts this sRGB channel theme to linear-light in the range [0, 1] using the sRGB electro-optical transfer fun |
| | | 198 | | /// </summary> |
| | | 199 | | /// <returns>The linear-light channel theme.</returns> |
| | | 200 | | public double ToSrgbLinearValue() |
| | | 201 | | { |
| | 7264905 | 202 | | var channel = Value / 255.0; |
| | | 203 | | |
| | 7264905 | 204 | | return channel <= 0.04045 |
| | 7264905 | 205 | | ? channel / 12.92 |
| | 7264905 | 206 | | : Math.Pow(x: (channel + 0.055) / 1.055, y: 2.4); |
| | | 207 | | } |
| | | 208 | | |
| | | 209 | | /// <summary>Returns the string representation of the HexByte theme.</summary> |
| | | 210 | | /// <returns>The formatted two-character uppercase hexadecimal string.</returns> |
| | 122498 | 211 | | public override string ToString() => Value.ToString(format: "X2", provider: CultureInfo.InvariantCulture); |
| | | 212 | | |
| | | 213 | | /// <summary> |
| | | 214 | | /// Attempts to parse a hexadecimal string into a <see cref="HexByte" />. Accepts 1–2 hex characters after trimming; |
| | | 215 | | /// parsing is case-insensitive. |
| | | 216 | | /// </summary> |
| | | 217 | | /// <param name="value">The hexadecimal string to parse; may be <see langword="null" />.</param> |
| | | 218 | | /// <param name="result"> |
| | | 219 | | /// When this method returns, contains the parsed <see cref="HexByte" /> if parsing succeeded; otherwise the default |
| | | 220 | | /// </param> |
| | | 221 | | /// <returns><see langword="true" /> if parsing succeeded; otherwise <see langword="false" />.</returns> |
| | | 222 | | public static bool TryParse(string? value, out HexByte result) |
| | | 223 | | { |
| | 7 | 224 | | result = default(HexByte); |
| | | 225 | | |
| | 7 | 226 | | if (string.IsNullOrWhiteSpace(value: value)) |
| | | 227 | | { |
| | 3 | 228 | | return false; |
| | | 229 | | } |
| | | 230 | | |
| | 4 | 231 | | var span = value.AsSpan().Trim(); |
| | | 232 | | |
| | 4 | 233 | | if (span.Length > 2 || span.Length < 1) |
| | | 234 | | { |
| | 1 | 235 | | return false; |
| | | 236 | | } |
| | | 237 | | |
| | 3 | 238 | | if (byte.TryParse( |
| | 3 | 239 | | s: span, style: NumberStyles.HexNumber, provider: CultureInfo.InvariantCulture, result: out var parsed |
| | 3 | 240 | | )) |
| | | 241 | | { |
| | 2 | 242 | | result = new HexByte(value: parsed); |
| | | 243 | | |
| | 2 | 244 | | return true; |
| | | 245 | | } |
| | | 246 | | |
| | 1 | 247 | | return false; |
| | | 248 | | } |
| | | 249 | | |
| | | 250 | | /// <summary>Returns a theme that indicates whether the values of two <see cref="HexByte" /> objects are equal.</sum |
| | | 251 | | /// <param name="left">The first theme to compare.</param> |
| | | 252 | | /// <param name="right">The second theme to compare.</param> |
| | | 253 | | /// <returns><see langword="true" /> if both have the same theme; otherwise, <see langword="false" />.</returns> |
| | 2 | 254 | | public static bool operator ==(HexByte left, HexByte right) => left.Equals(other: right); |
| | | 255 | | |
| | | 256 | | /// <summary>Determines whether one <see cref="HexByte" /> theme is greater than another.</summary> |
| | | 257 | | /// <param name="left">The left operand.</param> |
| | | 258 | | /// <param name="right">The right operand.</param> |
| | | 259 | | /// <returns> |
| | | 260 | | /// <see langword="true" /> if <paramref name="left" /> is greater than <paramref name="right" />; otherwise, |
| | | 261 | | /// <see langword="false" />. |
| | | 262 | | /// </returns> |
| | 1 | 263 | | public static bool operator >(HexByte left, HexByte right) => left.CompareTo(other: right) > 0; |
| | | 264 | | |
| | | 265 | | /// <summary>Determines whether one <see cref="HexByte" /> theme is greater than or equal to another.</summary> |
| | | 266 | | /// <param name="left">The left operand.</param> |
| | | 267 | | /// <param name="right">The right operand.</param> |
| | | 268 | | /// <returns> |
| | | 269 | | /// <see langword="true" /> if <paramref name="left" /> is greater than or equal to <paramref name="right" />; other |
| | | 270 | | /// <see langword="false" />. |
| | | 271 | | /// </returns> |
| | 2 | 272 | | public static bool operator >=(HexByte left, HexByte right) => left.CompareTo(other: right) >= 0; |
| | | 273 | | |
| | | 274 | | /// <summary>Converts a hexadecimal string to a <see cref="HexByte" /> instance.</summary> |
| | | 275 | | /// <param name="value">The hexadecimal string; must be valid (1–2 hex characters after trimming).</param> |
| | | 276 | | /// <returns>A <see cref="HexByte" /> representing the parsed theme.</returns> |
| | | 277 | | /// <exception cref="AryArgumentException">Thrown if the string is invalid.</exception> |
| | 1 | 278 | | public static implicit operator HexByte(string value) => new(value: value); |
| | | 279 | | |
| | | 280 | | /// <summary>Converts a <see cref="HexByte" /> instance to its two-character uppercase hexadecimal string.</summary> |
| | | 281 | | /// <param name="value">The theme to convert.</param> |
| | | 282 | | /// <returns>The two-character uppercase hexadecimal string.</returns> |
| | 1 | 283 | | public static implicit operator string(HexByte value) => value.ToString(); |
| | | 284 | | |
| | | 285 | | /// <summary>Converts a byte to a <see cref="HexByte" /> instance.</summary> |
| | | 286 | | /// <param name="value">The byte theme.</param> |
| | | 287 | | /// <returns>A <see cref="HexByte" /> representing the byte.</returns> |
| | 3501682 | 288 | | public static implicit operator HexByte(byte value) => new(value: value); |
| | | 289 | | |
| | | 290 | | /// <summary>Converts a <see cref="HexByte" /> instance to its byte theme.</summary> |
| | | 291 | | /// <param name="value">The theme to convert.</param> |
| | | 292 | | /// <returns>The underlying byte theme.</returns> |
| | 5 | 293 | | public static implicit operator byte(HexByte value) => value.Value; |
| | | 294 | | |
| | | 295 | | /// <summary>Returns a theme that indicates whether two <see cref="HexByte" /> objects have different values.</summa |
| | | 296 | | /// <param name="left">The first theme to compare.</param> |
| | | 297 | | /// <param name="right">The second theme to compare.</param> |
| | | 298 | | /// <returns> |
| | | 299 | | /// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are not equal; otherwise, |
| | | 300 | | /// <see langword="false" />. |
| | | 301 | | /// </returns> |
| | 2 | 302 | | public static bool operator !=(HexByte left, HexByte right) => !left.Equals(other: right); |
| | | 303 | | |
| | | 304 | | /// <summary>Determines whether one <see cref="HexByte" /> theme is less than another.</summary> |
| | | 305 | | /// <param name="left">The left operand.</param> |
| | | 306 | | /// <param name="right">The right operand.</param> |
| | | 307 | | /// <returns> |
| | | 308 | | /// <see langword="true" /> if <paramref name="left" /> is less than <paramref name="right" />; otherwise, |
| | | 309 | | /// <see langword="false" />. |
| | | 310 | | /// </returns> |
| | 1 | 311 | | public static bool operator <(HexByte left, HexByte right) => left.CompareTo(other: right) < 0; |
| | | 312 | | |
| | | 313 | | /// <summary>Determines whether one <see cref="HexByte" /> theme is less than or equal to another.</summary> |
| | | 314 | | /// <param name="left">The left operand.</param> |
| | | 315 | | /// <param name="right">The right operand.</param> |
| | | 316 | | /// <returns> |
| | | 317 | | /// <see langword="true" /> if <paramref name="left" /> is less than or equal to <paramref name="right" />; otherwis |
| | | 318 | | /// <see langword="false" />. |
| | | 319 | | /// </returns> |
| | 2 | 320 | | public static bool operator <=(HexByte left, HexByte right) => left.CompareTo(other: right) <= 0; |
| | | 321 | | } |