You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
112 lines
4.1 KiB
112 lines
4.1 KiB
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<Color> |
|
{ |
|
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); |
|
|
|
|
|
/// <summary> Converts a float clamped to range [0.0, 1.0] into a byte in range [0, 255]. </summary> |
|
private static byte F2B(float f) => (byte)(Math.Clamp(f, 0.0f, 1.0f) * 255); |
|
|
|
/// <summary> Converts a byte in range [0, 255] into a float in range [0.0, 1.0]. </summary> |
|
private static float B2F(byte b) => b / 255f; |
|
}
|
|
|