Dalamud/Dalamud/Interface/ColorHelpers.cs
srkizer 694b42a378
Fix UIColor handling per theme (#1995)
UIColor sheet has color sets per theme. Updated `UIColorWidget` to
reflect that, and added `SeStringDrawParams.ThemeIndex` to let users
choose which theme color set to use while drawing SeString from Dalamud.
2024-08-04 17:46:05 +02:00

334 lines
11 KiB
C#

using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Dalamud.Interface;
/// <summary>
/// Class containing various methods for manipulating colors.
/// </summary>
public static class ColorHelpers
{
/// <summary>
/// Pack a vector4 color into a uint for use in ImGui APIs.
/// </summary>
/// <param name="color">The color to pack.</param>
/// <returns>The packed color.</returns>
public static uint RgbaVector4ToUint(Vector4 color)
{
var r = (byte)(color.X * 255);
var g = (byte)(color.Y * 255);
var b = (byte)(color.Z * 255);
var a = (byte)(color.W * 255);
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
}
/// <summary>
/// Convert a RGBA color in the range of 0.f to 1.f to a uint.
/// </summary>
/// <param name="color">The color to pack.</param>
/// <returns>The packed color.</returns>
public static Vector4 RgbaUintToVector4(uint color)
{
var r = (color & 0x000000FF) / 255f;
var g = ((color & 0x0000FF00) >> 8) / 255f;
var b = ((color & 0x00FF0000) >> 16) / 255f;
var a = ((color & 0xFF000000) >> 24) / 255f;
return new Vector4(r, g, b, a);
}
/// <summary>
/// Convert a RGBA color in the range of 0.f to 1.f to a HSV color.
/// </summary>
/// <param name="color">The color to convert.</param>
/// <returns>The color in a HSV representation.</returns>
public static HsvaColor RgbaToHsv(Vector4 color)
{
var r = color.X;
var g = color.Y;
var b = color.Z;
var max = Math.Max(r, Math.Max(g, b));
var min = Math.Min(r, Math.Min(g, b));
var h = max;
var s = max;
var v = max;
var d = max - min;
s = max == 0 ? 0 : d / max;
if (max == min)
{
h = 0; // achromatic
}
else
{
if (max == r)
{
h = ((g - b) / d) + (g < b ? 6 : 0);
}
else if (max == g)
{
h = ((b - r) / d) + 2;
}
else if (max == b)
{
h = ((r - g) / d) + 4;
}
h /= 6;
}
return new HsvaColor(h, s, v, color.W);
}
/// <summary>
/// Convert a HSV color to a RGBA color in the range of 0.f to 1.f.
/// </summary>
/// <param name="hsv">The color to convert.</param>
/// <returns>The RGB color.</returns>
public static Vector4 HsvToRgb(HsvaColor hsv)
{
var h = hsv.H;
var s = hsv.S;
var v = hsv.V;
var r = 0f;
var g = 0f;
var b = 0f;
var i = (int)Math.Floor(h * 6);
var f = (h * 6) - i;
var p = v * (1 - s);
var q = v * (1 - (f * s));
var t = v * (1 - ((1 - f) * s));
switch (i % 6)
{
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
return new Vector4(r, g, b, hsv.A);
}
/// <summary>
/// Performs a swap of endianness
/// Exmaple:
/// (FFXIV) RGBA to ABGR (ImGui).
/// </summary>
/// <param name="rgba">Color value in byte order X Y Z W.</param>
/// <returns>Endian swapped color value with new byte order W Z Y X.</returns>
public static uint SwapEndianness(uint rgba)
=> BinaryPrimitives.ReverseEndianness(rgba);
/// <summary>
/// Lighten a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The lightened color.</returns>
public static Vector4 Lighten(this Vector4 color, float amount)
{
var hsv = RgbaToHsv(color);
hsv.V += amount;
return HsvToRgb(hsv);
}
/// <summary>
/// Lighten a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The lightened color.</returns>
public static uint Lighten(uint color, float amount)
=> RgbaVector4ToUint(Lighten(RgbaUintToVector4(color), amount));
/// <summary>
/// Darken a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The darkened color.</returns>
public static Vector4 Darken(this Vector4 color, float amount)
{
var hsv = RgbaToHsv(color);
hsv.V -= amount;
return HsvToRgb(hsv);
}
/// <summary>
/// Darken a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The darkened color.</returns>
public static uint Darken(uint color, float amount)
=> RgbaVector4ToUint(Darken(RgbaUintToVector4(color), amount));
/// <summary>
/// Saturate a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The saturated color.</returns>
public static Vector4 Saturate(this Vector4 color, float amount)
{
var hsv = RgbaToHsv(color);
hsv.S += amount;
return HsvToRgb(hsv);
}
/// <summary>
/// Saturate a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The saturated color.</returns>
public static uint Saturate(uint color, float amount)
=> RgbaVector4ToUint(Saturate(RgbaUintToVector4(color), amount));
/// <summary>
/// Desaturate a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The desaturated color.</returns>
public static Vector4 Desaturate(this Vector4 color, float amount)
{
var hsv = RgbaToHsv(color);
hsv.S -= amount;
return HsvToRgb(hsv);
}
/// <summary>
/// Desaturate a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The desaturated color.</returns>
public static uint Desaturate(uint color, float amount)
=> RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount));
/// <summary>Applies the given opacity value ranging from 0 to 1 to an uint value containing a RGBA value.</summary>
/// <param name="rgba">RGBA value to transform.</param>
/// <param name="opacity">Opacity to apply.</param>
/// <returns>Transformed value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint ApplyOpacity(uint rgba, float opacity) =>
((uint)MathF.Round((rgba >> 24) * opacity) << 24) | (rgba & 0xFFFFFFu);
/// <summary>Swaps red and blue channels of a given color in ARGB(BB GG RR AA) and ABGR(RR GG BB AA).</summary>
/// <param name="x">Color to process.</param>
/// <returns>Swapped color.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint SwapRedBlue(uint x) => (x & 0xFF00FF00u) | ((x >> 16) & 0xFF) | ((x & 0xFF) << 16);
/// <summary>
/// Fade a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The faded color.</returns>
public static Vector4 Fade(this Vector4 color, float amount)
{
var hsv = RgbaToHsv(color);
hsv.A -= amount;
return HsvToRgb(hsv);
}
/// <summary>
/// Set alpha of a color.
/// </summary>
/// <param name="color">The color.</param>
/// <param name="alpha">The alpha value to set.</param>
/// <returns>The color with the set alpha value.</returns>
public static Vector4 WithAlpha(this Vector4 color, float alpha)
=> color with { W = alpha };
/// <summary>
/// Fade a color.
/// </summary>
/// <param name="color">The color to lighten.</param>
/// <param name="amount">The amount to lighten.</param>
/// <returns>The faded color.</returns>
public static uint Fade(uint color, float amount)
=> RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount));
/// <summary>
/// Convert a KnownColor to a RGBA vector with values between 0.0f and 1.0f.
/// </summary>
/// <param name="knownColor">Known Color to convert.</param>
/// <returns>RGBA Vector with values between 0.0f and 1.0f.</returns>
public static Vector4 Vector(this KnownColor knownColor)
{
var rgbColor = Color.FromKnownColor(knownColor);
return new Vector4(rgbColor.R, rgbColor.G, rgbColor.B, rgbColor.A) / 255.0f;
}
/// <summary>
/// Normalizes a Vector4 with RGBA 255 color values to values between 0.0f and 1.0f
/// If values are out of RGBA 255 range, the original value is returned.
/// </summary>
/// <param name="color">The color vector to convert.</param>
/// <returns>A vector with values between 0.0f and 1.0f.</returns>
public static Vector4 NormalizeToUnitRange(this Vector4 color) => color switch
{
// If any components are out of range, return original value.
{ W: > 255.0f or < 0.0f } or { X: > 255.0f or < 0.0f } or { Y: > 255.0f or < 0.0f } or { Z: > 255.0f or < 0.0f } => color,
// If all components are already unit range, return original value.
{ W: >= 0.0f and <= 1.0f, X: >= 0.0f and <= 1.0f, Y: >= 0.0f and <= 1.0f, Z: >= 0.0f and <= 1.0f } => color,
_ => color / 255.0f,
};
/// <summary>
/// A struct representing a color using HSVA coordinates.
/// </summary>
/// <param name="H">The hue represented by this struct.</param>
/// <param name="S">The saturation represented by this struct.</param>
/// <param name="V">The value represented by this struct.</param>
/// <param name="A">The alpha represented by this struct.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter",
Justification = "I don't like it.")]
public record struct HsvaColor(float H, float S, float V, float A);
}