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 _rgbToLab = new ConverterBuilder().FromRGB().ToLab().Build(); private static readonly IColorConverter _labToRgb = new ConverterBuilder().FromLab().ToRGB().Build(); private static readonly IColorConverter _rgbToLChab = new ConverterBuilder().FromRGB().ToLChab().Build(); private static readonly IColorConverter _lchabToRgb = new ConverterBuilder().FromLChab().ToRGB().Build(); private static readonly IColorConverter _rgbToXyz = new ConverterBuilder().FromRGB(RGBWorkingSpaces.sRGB).ToXYZ(Illuminants.D65).Build(); private static readonly IColorConverter _xyzToRgb = new ConverterBuilder().FromXYZ(Illuminants.D65).ToRGB(RGBWorkingSpaces.sRGB).Build(); private static readonly IColorConverter _rgbToLChuv = new ConverterBuilder().FromRGB().ToLChuv().Build(); private static readonly IColorConverter _lchuvToRgb = new ConverterBuilder().FromLChuv().ToRGB().Build(); private static readonly IColorConverter _rgbToLuv = new ConverterBuilder().FromRGB().ToLuv().Build(); private static readonly IColorConverter _luvToRgb = new ConverterBuilder().FromLuv().ToRGB().Build(); private static readonly IColorConverter _rgbToJzazbz = new ConverterBuilder().FromRGB().ToJzazbz().Build(); private static readonly IColorConverter _jzazbzToRgb = new ConverterBuilder().FromJzazbz().ToRGB().Build(); private static readonly IColorConverter _rgbToJzCzhz = new ConverterBuilder().FromRGB().ToJzCzhz().Build(); private static readonly IColorConverter _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; } } }