using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace gaemstone.Client; [StructLayout(LayoutKind.Explicit)] public readonly struct Color : IEquatable { public static readonly Color Transparent = default; public static readonly Color Black = FromRGB(0.0f, 0.0f, 0.0f); public static readonly Color White = FromRGB(1.0f, 1.0f, 1.0f); [FieldOffset(0)] public readonly uint RGBA; [FieldOffset(0)] public readonly byte R; [FieldOffset(1)] public readonly byte G; [FieldOffset(2)] public readonly byte B; [FieldOffset(3)] public readonly byte A; private Color(uint rgba) => RGBA = rgba; private Color(byte r, byte g, byte b, byte a) { R = r; G = g; B = b; A = a; } public static Color FromRGBA(uint value) => new((byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value); public static Color FromRGBA(float r, float g, float b, float a = 1.0f) => new(F2B(r), F2B(g), F2B(b), F2B(a)); public static Color FromRGBA(Vector4 vec) => FromRGBA(vec.X, vec.Y, vec.Z, vec.W); public static Color FromRGB(uint value) => new((byte)(value >> 16), (byte)(value >> 8), (byte)value, 0xFF); public static Color FromRGB(float r, float g, float b) => new(F2B(r), F2B(g), F2B(b), 0xFF); public static Color FromRGB(Vector3 vec) => FromRGB(vec.X, vec.Y, vec.Z); public static Color FromGrayscale(float value, float alpha = 1.0f) => FromRGBA(value, value, value, alpha); public static Color FromHSV(float hue, float saturation, float value, float alpha = 1.0f) { if (saturation <= 0.0f) return FromGrayscale(value, alpha); var h = (((hue % 360f) + 360f) % 360f) / 60f; var s = Math.Clamp(saturation, 0.0f, 1.0f); var v = Math.Clamp(value , 0.0f, 1.0f); var f = h % 1f; var p = v * (1.0f - s); var q = v * (1.0f - s * f); var t = v * (1.0f - s * (1.0f - f)); return (int)h switch { 0 => FromRGBA(v, t, p, alpha), 1 => FromRGBA(q, v, p, alpha), 2 => FromRGBA(p, v, t, alpha), 3 => FromRGBA(p, q, v, alpha), 4 => FromRGBA(t, p, v, alpha), 5 => FromRGBA(v, p, q, alpha), _ => throw new InvalidOperationException() }; } private static readonly Regex _hexRegex = new("^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"); public static Color? TryParseHex(string? str) { if (str == null) return null; var match = _hexRegex.Match(str); if (!match.Success) return null; var hex = match.Value.AsSpan()[1..]; var value = uint.Parse(hex, NumberStyles.HexNumber); return (hex.Length == 8) ? FromRGBA(value) : FromRGB(value); } public Color WithAlpha(float alpha) => new(R, G, B, F2B(alpha)); public static Color Mix(Color a, Color b, float ratio) => FromRGBA((B2F(a.R) * (1 - ratio) + B2F(b.R) * ratio) / 2, (B2F(a.G) * (1 - ratio) + B2F(b.G) * ratio) / 2, (B2F(a.B) * (1 - ratio) + B2F(b.B) * ratio) / 2, (B2F(a.A) * (1 - ratio) + B2F(b.A) * ratio) / 2); public bool Equals(Color other) => RGBA == other.RGBA; public override bool Equals([NotNullWhen(true)] object? obj) => (obj is Color color) && Equals(color); public override int GetHashCode() => RGBA.GetHashCode(); public override string ToString() => $"Color(0x{RGBA:X8})"; public Vector4 ToVector4() => new(B2F(R), B2F(G), B2F(B), B2F(A)); public Vector3 ToVector3() => new(B2F(R), B2F(G), B2F(B)); public string ToHexString() => (A < byte.MaxValue) ? $"#{RGBA:X8}" : $"#{R:X2}{G:X2}{B:X2}"; public static bool operator ==(Color left, Color right) => left.Equals(right); public static bool operator !=(Color left, Color right) => !left.Equals(right); public static implicit operator System.Drawing.Color(Color color) => System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); /// Converts a float clamped to range [0.0, 1.0] into a byte in range [0, 255]. private static byte F2B(float f) => (byte)(Math.Clamp(f, 0.0f, 1.0f) * 255); /// Converts a byte in range [0, 255] into a float in range [0.0, 1.0]. private static float B2F(byte b) => b / 255f; }