Files

326 lines
16 KiB
C#

using Colourful;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Interface.GeneralElements;
using System;
using System.Numerics;
namespace HSUI.Helpers
{
public static class ColorUtils
{
//Build our converter objects and store them in a field. This will be used to convert our PluginConfigColors into different color spaces to be used for interpolation
private static readonly IColorConverter<RGBColor, LabColor> _rgbToLab = new ConverterBuilder().FromRGB().ToLab().Build();
private static readonly IColorConverter<LabColor, RGBColor> _labToRgb = new ConverterBuilder().FromLab().ToRGB().Build();
private static readonly IColorConverter<RGBColor, LChabColor> _rgbToLChab = new ConverterBuilder().FromRGB().ToLChab().Build();
private static readonly IColorConverter<LChabColor, RGBColor> _lchabToRgb = new ConverterBuilder().FromLChab().ToRGB().Build();
private static readonly IColorConverter<RGBColor, XYZColor> _rgbToXyz = new ConverterBuilder().FromRGB(RGBWorkingSpaces.sRGB).ToXYZ(Illuminants.D65).Build();
private static readonly IColorConverter<XYZColor, RGBColor> _xyzToRgb = new ConverterBuilder().FromXYZ(Illuminants.D65).ToRGB(RGBWorkingSpaces.sRGB).Build();
private static readonly IColorConverter<RGBColor, LChuvColor> _rgbToLChuv = new ConverterBuilder().FromRGB().ToLChuv().Build();
private static readonly IColorConverter<LChuvColor, RGBColor> _lchuvToRgb = new ConverterBuilder().FromLChuv().ToRGB().Build();
private static readonly IColorConverter<RGBColor, LuvColor> _rgbToLuv = new ConverterBuilder().FromRGB().ToLuv().Build();
private static readonly IColorConverter<LuvColor, RGBColor> _luvToRgb = new ConverterBuilder().FromLuv().ToRGB().Build();
private static readonly IColorConverter<RGBColor, JzazbzColor> _rgbToJzazbz = new ConverterBuilder().FromRGB().ToJzazbz().Build();
private static readonly IColorConverter<JzazbzColor, RGBColor> _jzazbzToRgb = new ConverterBuilder().FromJzazbz().ToRGB().Build();
private static readonly IColorConverter<RGBColor, JzCzhzColor> _rgbToJzCzhz = new ConverterBuilder().FromRGB().ToJzCzhz().Build();
private static readonly IColorConverter<JzCzhzColor, RGBColor> _jzCzhzToRgb = new ConverterBuilder().FromJzCzhz().ToRGB().Build();
//Simple LinearInterpolation method. T = [0 , 1]
private static float LinearInterpolation(float left, float right, float t)
=> left + ((right - left) * t);
public static PluginConfigColor GetColorByScale(float i, ColorByHealthValueConfig config) =>
GetColorByScale(i, config.LowHealthColorThreshold / 100f, config.FullHealthColorThreshold / 100f, config.LowHealthColor, config.FullHealthColor, config.MaxHealthColor, config.UseMaxHealthColor, config.BlendMode);
//Method used to interpolate two PluginConfigColors
//i is scale [0 , 1]
//min and max are used for color thresholds. for instance return colorLeft if i < min or return ColorRight if i > max
public static PluginConfigColor GetColorByScale(float i, float min, float max, PluginConfigColor colorLeft, PluginConfigColor colorRight, PluginConfigColor colorMax, bool useMaxColor, BlendMode blendMode)
{
try
{
// Guard against null/invalid config (can happen with corrupted or migrated config)
if (colorLeft == null || colorRight == null || colorMax == null)
return PluginConfigColor.FromHex(0xFF808080);
//Set our thresholds where the ratio is the range of values we will use for interpolation.
//Values outside this range will either return colorLeft or colorRight
float ratio = i;
if (min > 0 || max < 1)
{
if (i < min)
{
ratio = 0;
}
else if (i > max)
{
ratio = 1;
}
else
{
float range = max - min;
if (MathF.Abs(range) < 0.0001f) ratio = 1; // avoid divide-by-zero when min==max
else ratio = (i - min) / range;
}
}
//Convert our PluginConfigColor to RGBColor
RGBColor rgbColorLeft = new RGBColor(colorLeft.Vector.X, colorLeft.Vector.Y, colorLeft.Vector.Z);
RGBColor rgbColorRight = new RGBColor(colorRight.Vector.X, colorRight.Vector.Y, colorRight.Vector.Z);
//Interpolate our Alpha now
float alpha = LinearInterpolation(colorLeft.Vector.W, colorRight.Vector.W, ratio);
if (ratio >= 1 && useMaxColor)
{
return new PluginConfigColor(new Vector4((float)colorMax.Vector.X, (float)colorMax.Vector.Y, (float)colorMax.Vector.Z, colorMax.Vector.W));
}
//Allow the users to select different blend modes since interpolating between two colors can result in different blending depending on the color space
//We convert our RGBColor values into different color spaces. We then interpolate each channel before converting the color back into RGBColor space
switch (blendMode)
{
case BlendMode.LAB:
{
//convert RGB to LAB
LabColor LabLeft = _rgbToLab.Convert(rgbColorLeft);
LabColor LabRight = _rgbToLab.Convert(rgbColorRight);
RGBColor Lab2RGB = _labToRgb.Convert(
new LabColor(
LinearInterpolation((float)LabLeft.L, (float)LabRight.L, ratio),
LinearInterpolation((float)LabLeft.a, (float)LabRight.a, ratio),
LinearInterpolation((float)LabLeft.b, (float)LabRight.b, ratio)
)
);
Lab2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)Lab2RGB.R, (float)Lab2RGB.G, (float)Lab2RGB.B, alpha));
}
case BlendMode.LChab:
{
//convert RGB to LChab
LChabColor LChabLeft = _rgbToLChab.Convert(rgbColorLeft);
LChabColor LChabRight = _rgbToLChab.Convert(rgbColorRight);
RGBColor LChab2RGB = _lchabToRgb.Convert(
new LChabColor(
LinearInterpolation((float)LChabLeft.L, (float)LChabRight.L, ratio),
LinearInterpolation((float)LChabLeft.C, (float)LChabRight.C, ratio),
LinearInterpolation((float)LChabLeft.h, (float)LChabRight.h, ratio)
)
);
LChab2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)LChab2RGB.R, (float)LChab2RGB.G, (float)LChab2RGB.B, alpha));
}
case BlendMode.XYZ:
{
//convert RGB to XYZ
XYZColor XYZLeft = _rgbToXyz.Convert(rgbColorLeft);
XYZColor XYZRight = _rgbToXyz.Convert(rgbColorRight);
RGBColor XYZ2RGB = _xyzToRgb.Convert(
new XYZColor(
LinearInterpolation((float)XYZLeft.X, (float)XYZRight.X, ratio),
LinearInterpolation((float)XYZLeft.Y, (float)XYZRight.Y, ratio),
LinearInterpolation((float)XYZLeft.Z, (float)XYZRight.Z, ratio)
)
);
XYZ2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)XYZ2RGB.R, (float)XYZ2RGB.G, (float)XYZ2RGB.B, alpha));
}
case BlendMode.RGB:
{
//No conversion needed here because we are already working in RGB space
RGBColor newRGB = new RGBColor(
LinearInterpolation((float)rgbColorLeft.R, (float)rgbColorRight.R, ratio),
LinearInterpolation((float)rgbColorLeft.G, (float)rgbColorRight.G, ratio),
LinearInterpolation((float)rgbColorLeft.B, (float)rgbColorRight.B, ratio)
);
return new PluginConfigColor(new Vector4((float)newRGB.R, (float)newRGB.G, (float)newRGB.B, alpha));
}
case BlendMode.LChuv:
{
//convert RGB to LChuv
LChuvColor LChuvLeft = _rgbToLChuv.Convert(rgbColorLeft);
LChuvColor LChuvRight = _rgbToLChuv.Convert(rgbColorRight);
RGBColor LChuv2RGB = _lchuvToRgb.Convert(
new LChuvColor(
LinearInterpolation((float)LChuvLeft.L, (float)LChuvRight.L, ratio),
LinearInterpolation((float)LChuvLeft.C, (float)LChuvRight.C, ratio),
LinearInterpolation((float)LChuvLeft.h, (float)LChuvRight.h, ratio)
)
);
LChuv2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)LChuv2RGB.R, (float)LChuv2RGB.G, (float)LChuv2RGB.B, alpha));
}
case BlendMode.Luv:
{
//convert RGB to Luv
LuvColor LuvLeft = _rgbToLuv.Convert(rgbColorLeft);
LuvColor LuvRight = _rgbToLuv.Convert(rgbColorRight);
RGBColor Luv2RGB = _luvToRgb.Convert(
new LuvColor(
LinearInterpolation((float)LuvLeft.L, (float)LuvRight.L, ratio),
LinearInterpolation((float)LuvLeft.u, (float)LuvRight.u, ratio),
LinearInterpolation((float)LuvLeft.v, (float)LuvRight.v, ratio)
)
);
Luv2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)Luv2RGB.R, (float)Luv2RGB.G, (float)Luv2RGB.B, alpha));
}
case BlendMode.Jzazbz:
{
//convert RGB to Jzazbz
JzazbzColor JzazbzLeft = _rgbToJzazbz.Convert(rgbColorLeft);
JzazbzColor JzazbzRight = _rgbToJzazbz.Convert(rgbColorRight);
RGBColor Jzazbz2RGB = _jzazbzToRgb.Convert(
new JzazbzColor(
LinearInterpolation((float)JzazbzLeft.Jz, (float)JzazbzRight.Jz, ratio),
LinearInterpolation((float)JzazbzLeft.az, (float)JzazbzRight.az, ratio),
LinearInterpolation((float)JzazbzLeft.bz, (float)JzazbzRight.bz, ratio)
)
);
Jzazbz2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)Jzazbz2RGB.R, (float)Jzazbz2RGB.G, (float)Jzazbz2RGB.B, alpha));
}
case BlendMode.JzCzhz:
{
//convert RGB to JzCzhz
JzCzhzColor JzCzhzLeft = _rgbToJzCzhz.Convert(rgbColorLeft);
JzCzhzColor JzCzhzRight = _rgbToJzCzhz.Convert(rgbColorRight);
RGBColor JzCzhz2RGB = _jzCzhzToRgb.Convert(
new JzCzhzColor(
LinearInterpolation((float)JzCzhzLeft.Jz, (float)JzCzhzRight.Jz, ratio),
LinearInterpolation((float)JzCzhzLeft.Cz, (float)JzCzhzRight.Cz, ratio),
LinearInterpolation((float)JzCzhzLeft.hz, (float)JzCzhzRight.hz, ratio)
)
);
JzCzhz2RGB.NormalizeIntensity();
return new PluginConfigColor(new Vector4((float)JzCzhz2RGB.R, (float)JzCzhz2RGB.G, (float)JzCzhz2RGB.B, alpha));
}
}
return new(Vector4.One);
}
catch (Exception ex)
{
Plugin.Logger.Warning($"[HSUI] GetColorByScale failed (config may be corrupted): {ex.Message}");
return PluginConfigColor.FromHex(0xFF808080);
}
}
public static PluginConfigColor ColorForActor(IGameObject? actor)
{
if (actor == null || actor is not ICharacter character)
{
return GlobalColors.Instance.NPCNeutralColor;
}
if (character.ObjectKind == ObjectKind.Player ||
character.SubKind == 9 && character.ClassJob.RowId > 0)
{
return GlobalColors.Instance.SafeColorForJobId(character.ClassJob.RowId);
}
bool isHostile = Utils.IsHostile(character);
if (character is IBattleNpc npc)
{
if ((npc.BattleNpcKind == BattleNpcSubKind.Enemy || npc.BattleNpcKind == BattleNpcSubKind.BattleNpcPart) && isHostile)
{
return GlobalColors.Instance.NPCHostileColor;
}
else
{
return GlobalColors.Instance.NPCFriendlyColor;
}
}
return isHostile ? GlobalColors.Instance.NPCNeutralColor : GlobalColors.Instance.NPCFriendlyColor;
}
public static PluginConfigColor? ColorForCharacter(
IGameObject? gameObject,
uint currentHp = 0,
uint maxHp = 0,
bool useJobColor = false,
bool useRoleColor = false,
ColorByHealthValueConfig? colorByHealthConfig = null)
{
ICharacter? character = gameObject as ICharacter;
if (useJobColor && character != null)
{
return ColorForActor(character);
}
else if (useRoleColor)
{
return character is IPlayerCharacter ?
GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId) :
ColorForActor(character);
}
else if (colorByHealthConfig != null && colorByHealthConfig.Enabled && character != null)
{
var scale = (float)currentHp / Math.Max(1, maxHp);
if (colorByHealthConfig.UseJobColorAsMaxHealth)
{
return GetColorByScale(
scale,
colorByHealthConfig.LowHealthColorThreshold / 100f,
colorByHealthConfig.FullHealthColorThreshold / 100f,
colorByHealthConfig.LowHealthColor,
colorByHealthConfig.FullHealthColor,
ColorForActor(character),
colorByHealthConfig.UseMaxHealthColor,
colorByHealthConfig.BlendMode
);
}
else if (colorByHealthConfig.UseRoleColorAsMaxHealth)
{
return GetColorByScale(scale,
colorByHealthConfig.LowHealthColorThreshold / 100f,
colorByHealthConfig.FullHealthColorThreshold / 100f,
colorByHealthConfig.LowHealthColor, colorByHealthConfig.FullHealthColor,
character is IPlayerCharacter ? GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId) : ColorForActor(character),
colorByHealthConfig.UseMaxHealthColor,
colorByHealthConfig.BlendMode
);
}
return GetColorByScale(scale, colorByHealthConfig);
}
return null;
}
}
}