|
|
|
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;
|
|
|
|
}
|