using System; using System.Numerics; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Utility; namespace Robust.Shared.ColorNaming; // color naming algorithim is inspired by https://react-spectrum.adobe.com/blog/accessible-color-descriptions.html public static class ColorNaming { private static readonly (float Hue, string Loc)[] HueNames = { (float.DegreesToRadians(0f), "color-pink"), (float.DegreesToRadians(15f), "color-red"), (float.DegreesToRadians(45f), "color-orange"), (float.DegreesToRadians(90f), "color-yellow"), (float.DegreesToRadians(135f), "color-green"), (float.DegreesToRadians(180f), "color-cyan"), (float.DegreesToRadians(240f), "color-blue"), (float.DegreesToRadians(285f), "color-purple"), (float.DegreesToRadians(330f), "color-pink"), }; // one past 360 because we're now inclusive on the upper for testing if we're out of bounds private static readonly (float Hue, string Loc) HueFallback = (float.DegreesToRadians(361f), "color-pink"); private const float BrownLightnessThreshold = 0.675f; private static readonly LocId OrangeString = "color-orange"; private static readonly LocId BrownString = "color-brown"; private const float VeryDarkLightnessThreshold = 0.25f; private const float DarkLightnessThreshold = 0.5f; private const float NeutralLightnessThreshold = 0.7f; private const float LightLightnessThreshold = 0.85f; private static readonly LocId VeryDarkString = "color-very-dark"; private static readonly LocId DarkString = "color-dark"; private static readonly LocId LightString = "color-light"; private static readonly LocId VeryLightString = "color-very-light"; private static readonly LocId MixedHueString = "color-mixed-hue"; private static readonly LocId LightLowChromaString = "color-pale"; private static readonly LocId DarkLowChromaString = "color-gray-adjective"; private static readonly LocId HighChromaString = "color-strong"; private static readonly LocId WhiteString = "color-white"; private static readonly LocId GrayString = "color-gray"; private static readonly LocId BlackString = "color-black"; private const float LowChromaThreshold = 0.07f; private const float HighChromaThreshold = 0.16f; private const float LightLowChromaThreshold = 0.6f; private const float WhiteLightnessThreshold = 0.99f; private const float BlackLightnessThreshold = 0.01f; private const float GrayChromaThreshold = 0.01f; private static (string Loc, float AdjustedLightness) DescribeHue(Vector4 oklch, ILocalizationManager localization) { var (lightness, _, hue, _) = oklch; for (var i = 0; i < HueNames.Length; i++) { var prevData = HueNames[i]; var nextData = i+1 < HueNames.Length ? HueNames[i+1] : HueFallback; if (prevData.Hue > hue || hue >= nextData.Hue) continue; var loc = prevData.Loc; var adjustedLightness = lightness; if (prevData.Loc == OrangeString && lightness <= BrownLightnessThreshold) loc = BrownString; else if (prevData.Loc == OrangeString) adjustedLightness = lightness - BrownLightnessThreshold + DarkLightnessThreshold; if (hue >= (prevData.Hue + nextData.Hue)/2f && prevData.Loc != nextData.Loc) { if (localization.TryGetString($"{loc}-{nextData.Loc}", out var hueName)) return (hueName!, adjustedLightness); else return (localization.GetString(MixedHueString, ("a", localization.GetString(loc)), ("b", localization.GetString(nextData.Loc))), adjustedLightness); } return (localization.GetString(loc), adjustedLightness); } DebugTools.Assert($"colour ({oklch}) hue {hue} is outside of expected bounds"); return (localization.GetString("color-unknown"), lightness); } private static string? DescribeChroma(Vector4 oklch, ILocalizationManager localization) { var (lightness, chroma, _, _) = oklch; if (chroma <= LowChromaThreshold) { if (lightness >= LightLowChromaThreshold) return localization.GetString(LightLowChromaString); else return localization.GetString(DarkLowChromaString); } else if (chroma >= HighChromaThreshold) { return localization.GetString(HighChromaString); } return null; } private static string? DescribeLightness(Vector4 oklch, ILocalizationManager localization) { return oklch.X switch { < VeryDarkLightnessThreshold => localization.GetString(VeryDarkString), < DarkLightnessThreshold => localization.GetString(DarkString), < NeutralLightnessThreshold => null, < LightLightnessThreshold => localization.GetString(LightString), _ => localization.GetString(VeryLightString) }; } /// /// Textually describes a color /// /// /// Returns a localized textual description of the provided color /// /// A Color that is assumed to be in SRGB (the default for most cases) public static string Describe(Color srgb, ILocalizationManager localization) { var oklch = Color.ToLch(Color.ToLab(Color.FromSrgb(srgb))); if (oklch.X >= WhiteLightnessThreshold) return localization.GetString(WhiteString); if (oklch.X <= BlackLightnessThreshold) return localization.GetString(BlackString); var (hueDescription, adjustedLightness) = DescribeHue(oklch, localization); oklch.X = adjustedLightness; var chromaDescription = DescribeChroma(oklch, localization); var lightnessDescription = DescribeLightness(oklch, localization); if (oklch.Y <= GrayChromaThreshold) { hueDescription = localization.GetString(GrayString); chromaDescription = null; } return (hueDescription, chromaDescription, lightnessDescription) switch { ({ } hue, { } chroma, { } lightness) => localization.GetString("color-hue-chroma-lightness", ("hue", hue), ("chroma", chroma), ("lightness", lightness)), ({ } hue, { } chroma, null) => localization.GetString("color-hue-chroma", ("hue", hue), ("chroma", chroma)), ({ } hue, null, { } lightness) => localization.GetString("color-hue-lightness", ("hue", hue), ("lightness", lightness)), ({ } hue, null, null) => hue, }; } }