From 51397d78a709b8f73dacdc79f1ee8a11de8c4d5b Mon Sep 17 00:00:00 2001 From: Az <56699741+AlchlcDvl@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:53:02 -0400 Subject: [PATCH] color improvements --- MiraAPI/Colors/CustomColor.cs | 4 +- MiraAPI/Colors/VariableColor.cs | 4 +- .../Attributes/ModdedNumberOptionAttribute.cs | 2 +- .../ModdedNumberOptionListAttribute.cs | 2 +- MiraAPI/Modifiers/Types/TimedModifier.cs | 2 +- .../Patches/Colors/VariableColorPatches.cs | 8 +- .../Patches/Options/LobbyViewPanePatches.cs | 2 +- MiraAPI/Utilities/ColorExtensions.cs | 308 +++++++++++++++++- MiraAPI/Utilities/Extensions.cs | 22 +- 9 files changed, 316 insertions(+), 38 deletions(-) diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index db913562..996bb467 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -58,13 +58,13 @@ public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColo /// public CustomColor(StringNames name, Color32 mainColor) - : this(name, mainColor, mainColor.GetShadowColor(60)) + : this(name, mainColor, mainColor.DarkenAbsolute(60)) { } /// public CustomColor(string name, Color32 mainColor) - : this(name, mainColor, mainColor.GetShadowColor(60)) + : this(name, mainColor, mainColor.DarkenAbsolute(60)) { } diff --git a/MiraAPI/Colors/VariableColor.cs b/MiraAPI/Colors/VariableColor.cs index bfb97176..84a38358 100644 --- a/MiraAPI/Colors/VariableColor.cs +++ b/MiraAPI/Colors/VariableColor.cs @@ -16,7 +16,7 @@ namespace MiraAPI.Colors; /// The static shadow color used when dynamic colors are disabled. /// The default time that is used to automatically fill in the static colors when the latter is not provided. public sealed class VariableColor(StringNames name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) - : CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).GetShadowColor(60)) + : CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).DarkenAbsolute(60)) { /// /// Gets or sets a delegate that takes in a time value and returns the dynamic main color. @@ -26,7 +26,7 @@ public sealed class VariableColor(StringNames name, Func mainEvalu /// /// Gets or sets a delegate that takes in a time value and returns the dynamic shadow color. /// - public Func ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(0.24f)); + public Func ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).DarkenAbsolute(0.24f)); /// public VariableColor(StringNames name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) diff --git a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs index f43773e1..55cd205a 100644 --- a/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/ModdedNumberOptionAttribute.cs @@ -97,7 +97,7 @@ public class ModdedNumberOptionAttribute( var value = opt.Value; if (ValueType == typeof(int)) - return (int)Math.Round(value); + return (int)MathF.Round(value); return value; } diff --git a/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs index c393db3e..2f761faa 100644 --- a/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs +++ b/MiraAPI/GameOptions/Attributes/OptionList/ModdedNumberOptionListAttribute.cs @@ -101,7 +101,7 @@ public class ModdedNumberOptionListAttribute( var value = opt.Value; if (ValueType == typeof(int)) - return (int)Math.Round(value); + return (int)MathF.Round(value); return value; } diff --git a/MiraAPI/Modifiers/Types/TimedModifier.cs b/MiraAPI/Modifiers/Types/TimedModifier.cs index 526a4520..a3e50854 100644 --- a/MiraAPI/Modifiers/Types/TimedModifier.cs +++ b/MiraAPI/Modifiers/Types/TimedModifier.cs @@ -51,7 +51,7 @@ public abstract class TimedModifier : BaseModifier /// A string with the time remaining. public override string GetDescription() { - return $"Remaining Time: {Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s"; + return $"Remaining Time: {MathF.Round(Duration - TimeRemaining, 0)}s/{Duration}s"; } /// diff --git a/MiraAPI/Patches/Colors/VariableColorPatches.cs b/MiraAPI/Patches/Colors/VariableColorPatches.cs index 98bed64d..3a2a2b6b 100644 --- a/MiraAPI/Patches/Colors/VariableColorPatches.cs +++ b/MiraAPI/Patches/Colors/VariableColorPatches.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using AmongUs.Data; using HarmonyLib; @@ -42,7 +41,7 @@ internal static class PlayerTabPatch } [HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))] -internal static class ChatNotifRainbowPatch +internal static class ChatNotifVariablePatch { private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout); @@ -60,14 +59,13 @@ internal static class ChatNotifRainbowPatch var str = color.ToHtmlStringRGBA(); var nameText = __instance.playerNameText; nameText.text = "" + RichTags.Replace(nameText.text, string.Empty); - nameText.outlineColor = color.GetShadowColor(0.5f); + nameText.outlineColor = color.DarkenAbsolute(0.5f); } } [HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))] -internal static class RainbowLobbyInfoPanePatch +internal static class VariableLobbyInfoPanePatch { - [SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")] public static void Postfix(HostInfoPanel __instance) { if (!__instance.gameObject.activeInHierarchy) diff --git a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs index 5f5ca9b1..ee26a263 100644 --- a/MiraAPI/Patches/Options/LobbyViewPanePatches.cs +++ b/MiraAPI/Patches/Options/LobbyViewPanePatches.cs @@ -464,7 +464,7 @@ public static class LobbyViewPanePatches if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader)) { - var veryDarkColor = group.Color.DarkenColor(.35f); + var veryDarkColor = group.Color.DarkenRelative(.35f); categoryHeaderRoleVariant.Title.color = veryDarkColor; categoryHeaderRoleVariant.Background.color = group.Color; } diff --git a/MiraAPI/Utilities/ColorExtensions.cs b/MiraAPI/Utilities/ColorExtensions.cs index b077770e..95c5d662 100644 --- a/MiraAPI/Utilities/ColorExtensions.cs +++ b/MiraAPI/Utilities/ColorExtensions.cs @@ -1,12 +1,26 @@ -using UnityEngine; +using System; +using System.Runtime.CompilerServices; +using UnityEngine; namespace MiraAPI.Utilities; /// -/// Extension methods for the struct. +/// Extension methods for the and structs. /// public static class ColorExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float ToFloat(this byte @byte) + { + return @byte / 255f; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ToByte(this float @float) + { + return (byte)MathF.Round(@float * 255f); + } + /// /// Gets the relative luminance of a (WCAG 2.1 specification). 0 is black, 1 is white. /// @@ -19,6 +33,18 @@ public static class ColorExtensions (0.0722f * Linearize(color.b)); } + /// + /// Gets the relative luminance of a (WCAG 2.1 specification). 0 is black, 1 is white. + /// + /// to calculate luminance for. + /// Luminance value between 0 and 1. + public static float GetRelativeLuminance(this Color32 color) + { + return (0.2126f * Linearize(color.r.ToFloat())) + + (0.7152f * Linearize(color.g.ToFloat())) + + (0.0722f * Linearize(color.b.ToFloat())); + } + /// /// Linearizes an sRGB component into linear space. /// @@ -26,7 +52,7 @@ public static class ColorExtensions /// The linearized result. public static float Linearize(float c) { - return c <= 0.03928f ? c / 12.92f : Mathf.Pow((c + 0.055f) / 1.055f, 2.4f); + return c <= 0.03928f ? c / 12.92f : MathF.Pow((c + 0.055f) / 1.055f, 2.4f); } /// @@ -39,11 +65,27 @@ public static class ColorExtensions { var luminanceA = colorA.GetRelativeLuminance(); var luminanceB = colorB.GetRelativeLuminance(); - return (Mathf.Max(luminanceA, luminanceB) + 0.05f) / (Mathf.Min(luminanceA, luminanceB) + 0.05f); + + return (Math.Max(luminanceA, luminanceB) + 0.05f) / (Math.Min(luminanceA, luminanceB) + 0.05f); } /// - /// Generates an accessible alternate with sufficient contrast. + /// Calculates the contrast ratio between two s. + /// + /// First . + /// Second . + /// Contrast ratio between the two s. + public static float GetContrastRatio(this Color32 colorA, Color32 colorB) + { + var luminanceA = colorA.GetRelativeLuminance(); + var luminanceB = colorB.GetRelativeLuminance(); + + return (Math.Max(luminanceA, luminanceB) + 0.05f) / (Math.Min(luminanceA, luminanceB) + 0.05f); + } + + /// + /// Generates an accessible alternate by shifting towards black or white + /// based on which extreme offers the highest potential contrast from the original color. /// /// Base . /// Target contrast ratio (default 4.5f for AA). @@ -57,10 +99,120 @@ public static class ColorExtensions } /// - /// Finds the with the minimum contrast ratio to the target . + /// Generates an accessible alternate with sufficient contrast. + /// + /// Base . + /// Target contrast ratio (default 4.5f for AA). + /// Alternate with sufficient contrast. + public static Color32 FindAlternateColor(this Color32 color, float desiredRatio = 4.5f) + { + var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat()); + var result = colorF.FindAlternateColor(desiredRatio); + return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a); + } + + /// + /// Darkens a towards black until it reaches the target WCAG contrast ratio against a background. + /// + /// The foreground to modify. + /// The background it sits on. + /// Target contrast ratio (default 4.5f for AA). + /// A darkened meeting the contrast ratio, or black if impossible. + public static Color DarkenToWCAG(this Color color, Color background, float targetRatio = 4.5f) + { + return color.GetContrastRatio(background) >= targetRatio + ? color + : FindMinimumContrastColor(color, Color.black, targetRatio); + } + + /// + /// Darkens a towards black until it reaches the target WCAG contrast ratio against a background. + /// + /// The foreground to modify. + /// The background it sits on. + /// Target contrast ratio (default 4.5f for AA). + /// A darkened meeting the contrast ratio, or black if impossible. + public static Color32 DarkenToWCAG(this Color32 color, Color32 background, float targetRatio = 4.5f) + { + var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat(), color.a.ToFloat()); + var bgF = new Color(background.r.ToFloat(), background.g.ToFloat(), background.b.ToFloat(), background.a.ToFloat()); + + var result = colorF.DarkenToWCAG(bgF, targetRatio); + return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a); + } + + /// + /// Lightens a towards white until it reaches the target WCAG contrast ratio against a background. + /// + /// The foreground to modify. + /// The background it sits on. + /// Target contrast ratio (default 4.5f for AA). + /// A lightened meeting the contrast ratio, or white if impossible. + public static Color LightenToWCAG(this Color color, Color background, float targetRatio = 4.5f) + { + return color.GetContrastRatio(background) >= targetRatio + ? color + : FindMinimumContrastColor(color, Color.white, targetRatio); + } + + /// + /// Lightens a towards white until it reaches the target WCAG contrast ratio against a background. + /// + /// The foreground to modify. + /// The background it sits on. + /// Target contrast ratio (default 4.5f for AA). + /// A lightened meeting the contrast ratio, or white if impossible. + public static Color32 LightenToWCAG(this Color32 color, Color32 background, float targetRatio = 4.5f) + { + var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat(), color.a.ToFloat()); + var bgF = new Color(background.r.ToFloat(), background.g.ToFloat(), background.b.ToFloat(), background.a.ToFloat()); + + var result = colorF.LightenToWCAG(bgF, targetRatio); + return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a); + } + + /// + /// Automatically decides whether to lighten or darken the to achieve WCAG compliance + /// based on which extreme offers the highest potential contrast against the provided background. + /// + /// The foreground to modify. + /// The background it sits on. + /// Target contrast ratio (default 4.5f for AA). + /// A modified meeting the WCAG contrast requirement. + public static Color EnsureWCAGContrast(this Color color, Color background, float targetRatio = 4.5f) + { + if (color.GetContrastRatio(background) >= targetRatio) + return color; + + var blackContrast = Color.black.GetContrastRatio(background); + var whiteContrast = Color.white.GetContrastRatio(background); + + return blackContrast > whiteContrast + ? color.DarkenToWCAG(background, targetRatio) + : color.LightenToWCAG(background, targetRatio); + } + + /// + /// Automatically decides whether to lighten or darken the to achieve WCAG compliance. + /// + /// The foreground to modify. + /// The background it sits on. + /// Target contrast ratio (default 4.5f for AA). + /// A modified meeting the WCAG contrast requirement. + public static Color32 EnsureWCAGContrast(this Color32 color, Color32 background, float targetRatio = 4.5f) + { + var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat(), color.a.ToFloat()); + var bgF = new Color(background.r.ToFloat(), background.g.ToFloat(), background.b.ToFloat(), background.a.ToFloat()); + + var result = colorF.EnsureWCAGContrast(bgF, targetRatio); + return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a); + } + + /// + /// Finds the with the minimum contrast ratio to the target using binary search. /// /// The base to start from. - /// The target to compare against. + /// The target to approach (usually white or black). /// The desired contrast ratio. /// The with the minimum contrast ratio to the target . public static Color FindMinimumContrastColor(Color baseColor, Color targetColor, float desiredRatio) @@ -72,18 +224,28 @@ public static class ColorExtensions ? desiredRatio * (baseLuminance + 0.05f) - 0.05f : (baseLuminance + 0.05f) / desiredRatio - 0.05f; - if (isLightening && targetLuminance > 1f) return Color.white; - if (!isLightening && targetLuminance < 0f) return Color.black; + if (isLightening && targetLuminance > 1f) + return Color.white; + + if (!isLightening && targetLuminance < 0f) + return Color.black; float low = 0f, high = 1f; var bestColor = targetColor; + for (var i = 0; i < 10; i++) { var t = (low + high) / 2f; - var testColor = Color.Lerp(baseColor, targetColor, t); + + var testColor = new Color( + baseColor.r + (targetColor.r - baseColor.r) * t, + baseColor.g + (targetColor.g - baseColor.g) * t, + baseColor.b + (targetColor.b - baseColor.b) * t, + baseColor.a); + var testLuminance = testColor.GetRelativeLuminance(); - if ((isLightening && testLuminance >= targetLuminance) || (!isLightening && testLuminance <= targetLuminance)) + if (isLightening ? testLuminance >= targetLuminance : testLuminance <= targetLuminance) { high = t; bestColor = testColor; @@ -96,4 +258,128 @@ public static class ColorExtensions return bestColor; } + + /// + /// Subtracts a flat amount from the 's RGB channels. + /// + /// The original color. + /// The flat amount to subtract (typically 0.0 to 1.0). + /// A uniformly darkened . + public static Color DarkenAbsolute(this Color color, float amount) + { + return new Color( + Math.Max(0f, color.r - amount), + Math.Max(0f, color.g - amount), + Math.Max(0f, color.b - amount), + color.a); + } + + /// + /// Subtracts a flat byte amount from the 's RGB channels. + /// + /// The original . + /// The flat amount to subtract (0 to 255). + /// A uniformly darkened . + public static Color32 DarkenAbsolute(this Color32 color, byte amount) + { + return new Color32( + (byte)Math.Max(0, color.r - amount), + (byte)Math.Max(0, color.g - amount), + (byte)Math.Max(0, color.b - amount), + color.a); + } + + /// + /// Adds a flat amount to the 's RGB channels. + /// + /// The original color. + /// The flat amount to add (typically 0.0 to 1.0). + /// A uniformly lightened . + public static Color LightenAbsolute(this Color color, float amount) + { + return new Color( + Math.Min(1f, color.r + amount), + Math.Min(1f, color.g + amount), + Math.Min(1f, color.b + amount), + color.a); + } + + /// + /// Adds a flat byte amount to the 's RGB channels. + /// + /// The original . + /// The flat amount to add (0 to 255). + /// A uniformly lightened . + public static Color32 LightenAbsolute(this Color32 color, byte amount) + { + return new Color32( + (byte)Math.Min(255, color.r + amount), + (byte)Math.Min(255, color.g + amount), + (byte)Math.Min(255, color.b + amount), + color.a); + } + + /// + /// Darkens the multiplicatively, preserving the ratio between RGB channels to maintain hue. + /// + /// The original color. + /// The percentage to darken by (e.g., 0.2 for 20% darker). + /// A ratio-preserved darkened . + public static Color DarkenRelative(this Color color, float percentage) + { + var multiplier = Math.Max(0f, Math.Min(1f, 1f - percentage)); + return new Color( + color.r * multiplier, + color.g * multiplier, + color.b * multiplier, + color.a); + } + + /// + /// Darkens the multiplicatively, preserving the ratio between RGB channels. + /// + /// The original . + /// The percentage to darken by (e.g., 0.2 for 20% darker). + /// A ratio-preserved darkened . + public static Color32 DarkenRelative(this Color32 color, float percentage) + { + var multiplier = Math.Max(0f, Math.Min(1f, 1f - percentage)); + return new Color32( + (color.r * multiplier).ToByte(), + (color.g * multiplier).ToByte(), + (color.b * multiplier).ToByte(), + color.a); + } + + /// + /// Lightens the multiplicatively, preserving the ratio between RGB channels. + /// + /// The original color. + /// The percentage to lighten by (e.g., 0.2 for 20% lighter). + /// A ratio-preserved lightened . + public static Color LightenRelative(this Color color, float percentage) + { + var multiplier = 1f + percentage; + return new Color( + Math.Min(1f, color.r * multiplier), + Math.Min(1f, color.g * multiplier), + Math.Min(1f, color.b * multiplier), + color.a); + } + + /// + /// Lightens the multiplicatively, preserving the ratio between RGB channels. + /// + /// The original . + /// The percentage to lighten by (e.g., 0.2 for 20% lighter). + /// A ratio-preserved lightened . + public static Color32 LightenRelative(this Color32 color, float percentage) + { + var multiplier = 1f + percentage; + return new Color32( + Math.Min(255f, color.r * 255f * multiplier / 255f).ToByte(), + Math.Min(255f, color.g * 255f * multiplier / 255f).ToByte(), + Math.Min(255f, color.b * 255f * multiplier / 255f).ToByte(), + color.a); + } } diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index ae5c6af6..808faef7 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -607,14 +607,10 @@ public static class Extensions /// The original . /// A darkness amount between 0 and 255. /// The darkened . + [Obsolete("Use ColorExtensions.DarkenAbsolute instead. This will be removed in a future version.")] public static Color32 GetShadowColor(this Color32 color, byte darknessAmount) { - return - new Color32( - (byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), - (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), - (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), - color.a); + return color.DarkenAbsolute(darknessAmount); } /// @@ -623,14 +619,10 @@ public static class Extensions /// The original . /// A darkness amount between 0 and 1. /// The darkened . + [Obsolete("Use ColorExtensions.DarkenAbsolute instead. This will be removed in a future version.")] public static Color GetShadowColor(this Color color, float darknessAmount) { - return - new Color( - MathUtilities.Clamp01(color.r - darknessAmount), - MathUtilities.Clamp01(color.g - darknessAmount), - MathUtilities.Clamp01(color.b - darknessAmount), - color.a); + return color.DarkenAbsolute(darknessAmount); } /// @@ -719,9 +711,10 @@ public static class Extensions /// The original . /// A amount between 0 and 1. /// The darkened . + [Obsolete("Use ColorExtensions.DarkenAbsolute instead. This will be removed in a future version.")] public static Color DarkenColor(this Color color, float amount = 0.45f) { - return new Color(color.r - amount, color.g - amount, color.b - amount); + return color.DarkenAbsolute(amount); } /// @@ -730,9 +723,10 @@ public static class Extensions /// The original . /// A amount between 0.0 and 1.0. /// The lightened . + [Obsolete("Use ColorExtensions.LightenAbsolute instead. This will be removed in a future version.")] public static Color LightenColor(this Color color, float amount = 0.45f) { - return new Color(color.r + amount, color.g + amount, color.b + amount); + return color.LightenAbsolute(amount); } ///