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,
};
}
}