diff --git a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs new file mode 100644 index 00000000000..2ae74f3157b --- /dev/null +++ b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Shared.Tests.Utility; + +[Parallelizable(ParallelScope.All)] +[TestOf(typeof(ColorExtensions))] +internal sealed class ColorExtensionsTest +{ + [Test] + public void TestAnalogousPalette() + { + var palette = ColorExtensions.GetAnalogousComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 30f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 30f / 360f)); + } + } + + [Test] + public void TestTriadicPalette() + { + var palette = ColorExtensions.GetTriadicComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 120f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 120f / 360f)); + } + } + + [Test] + public void TestSplitComplementaryPalette() + { + var palette = ColorExtensions.GetSplitComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 150f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 150f / 360f)); + } + } + + [Test] + public void TestComplementaryPalette() + { + var palette = ColorExtensions.GetOneComplementary(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 180f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 180f / 360f)); + } + } +} diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs new file mode 100644 index 00000000000..007914540c9 --- /dev/null +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Numerics; +using Robust.Shared.Random; +using Robust.Shared.Maths; + +namespace Robust.Shared.Utility; + +public static class ColorExtensions +{ + public static readonly float AnalogousHueDelta = 30f / 360f; // +/- 1/12. 30 degrees, 0.08333... over hue + public static readonly float TriadicHueDelta = 120f / 360f; // +/- 1/3. 120 degrees, 0.333 over hue + public static readonly float SplitComplementaryHueDelta = 150f / 360f; // +/- 5/12. 150 degrees, 0.4166... over hue + public static readonly float ComplementaryHueDelta = 180f / 360f; // +/- 1/2. 180 degrees + + /// + /// Generates a list of analogous complementary colors + /// + public static Color[] GetAnalogousComplementaries(this Color color) + { + return GetComplementaryColors(color, AnalogousHueDelta); + } + + /// + /// Generates a list of triadic complementary colors + /// + public static Color[] GetTriadicComplementaries(this Color color) + { + return GetComplementaryColors(color, TriadicHueDelta); + } + + /// + /// Generates a list of split complementary colors + /// + public static Color[] GetSplitComplementaries(this Color color) + { + return GetComplementaryColors(color, SplitComplementaryHueDelta); + } + + /// + /// Generates a list containing the base color and two copies of a single complementary color + /// + public static Color[] GetOneComplementary(this Color color) + { + return GetComplementaryColors(color, ComplementaryHueDelta); + } + + /// + /// Generates a complementary color palette for a provided + /// color by rotating a set amount of degrees around the + /// color wheel, and then varying the value and saturation + /// slightly. + /// + /// + /// A list of 3 colors. + /// + public static Color[] GetComplementaryColors(Color color, float hueDelta) + { + var hsl = Color.ToHsl(color); + var random = new RobustRandom(); + + // sorry about how messy these are, but to get all random values we need to reroll for positive and negative HSL. + // since we want to rotate x degrees around the colour wheel, we need to do so in both directions- doing x + x degrees will give us the wrong hue! + + // also varying the saturation and lightness just a little to add some contrast. + // since 'color' is our main color, we are desaturating our secondary colors. + // this means our main color will always stand out the most. + + var hVal = hsl.X + hueDelta; + hVal -= MathF.Floor(hVal); + var positiveHSL = new Vector4( + hVal, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), + hsl.W); + + var hVal1 = hsl.X - hueDelta; + if (hVal1 < 0f) + hVal1 += 1f; + var negativeHSL = new Vector4( + hVal1, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), + hsl.W); + + var c0 = Color.FromHsl(positiveHSL); + var c1 = Color.FromHsl(negativeHSL); + + var palette = new Color[] { color, c0, c1 }; + return palette; + } +}