A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Numerics;
6using NUnit.Framework;
7using osu.Framework.Graphics;
8
9namespace osu.Framework.Tests.Graphics
10{
11 [TestFixture]
12 public class ColourTest
13 {
14 [Test]
15 public void TestFromHSL()
16 {
17 // test FromHSL that black and white are only affected by luminance
18 testConvertFromHSL(Colour4.White, (0f, 0.5f, 1f, 1f));
19 testConvertFromHSL(Colour4.White, (1f, 1f, 1f, 1f));
20 testConvertFromHSL(Colour4.White, (0.5f, 0.75f, 1f, 1f));
21 testConvertFromHSL(Colour4.Black, (0f, 0.5f, 0f, 1f));
22 testConvertFromHSL(Colour4.Black, (1f, 1f, 0f, 1f));
23 testConvertFromHSL(Colour4.Black, (0.5f, 0.75f, 0f, 1f));
24
25 // test FromHSL that grey is not affected by hue
26 testConvertFromHSL(Colour4.Gray, (0f, 0f, 0.5f, 1f));
27 testConvertFromHSL(Colour4.Gray, (0.5f, 0f, 0.5f, 1f));
28 testConvertFromHSL(Colour4.Gray, (1f, 0f, 0.5f, 1f));
29
30 // test FromHSL that alpha is being passed through
31 testConvertFromHSL(Colour4.Black.Opacity(0.5f), (0f, 0f, 0f, 0.5f));
32
33 // test FromHSL with primaries
34 testConvertFromHSL(Colour4.Red, (0, 1f, 0.5f, 1f));
35 testConvertFromHSL(Colour4.Yellow, (1f / 6f, 1f, 0.5f, 1f));
36 testConvertFromHSL(Colour4.Lime, (2f / 6f, 1f, 0.5f, 1f));
37 testConvertFromHSL(Colour4.Cyan, (3f / 6f, 1f, 0.5f, 1f));
38 testConvertFromHSL(Colour4.Blue, (4f / 6f, 1f, 0.5f, 1f));
39 testConvertFromHSL(Colour4.Magenta, (5f / 6f, 1f, 0.5f, 1f));
40 testConvertFromHSL(Colour4.Red, (1f, 1f, 0.5f, 1f));
41
42 // test FromHSL with some other knowns
43 testConvertFromHSL(Colour4.CornflowerBlue, (219f / 360f, 0.792f, 0.661f, 1f));
44 testConvertFromHSL(Colour4.Tan.Opacity(0.5f), (34f / 360f, 0.437f, 0.686f, 0.5f));
45 }
46
47 [Test]
48 public void TestToHSL()
49 {
50 // test ToHSL that black, white, and grey always return constant 0f for hue and saturation
51 testConvertToHSL((0f, 0f, 1f, 1f), Colour4.White);
52 testConvertToHSL((0f, 0f, 0f, 1f), Colour4.Black);
53 testConvertToHSL((0f, 0f, 0.5f, 1f), Colour4.Gray);
54
55 // test ToHSL that alpha is being passed through
56 testConvertToHSL((0f, 0f, 0f, 0.5f), Colour4.Black.Opacity(0.5f));
57
58 // test ToHSL with primaries
59 testConvertToHSL((0, 1f, 0.5f, 1f), Colour4.Red);
60 testConvertToHSL((1f / 6f, 1f, 0.5f, 1f), Colour4.Yellow);
61 testConvertToHSL((2f / 6f, 1f, 0.5f, 1f), Colour4.Lime);
62 testConvertToHSL((3f / 6f, 1f, 0.5f, 1f), Colour4.Cyan);
63 testConvertToHSL((4f / 6f, 1f, 0.5f, 1f), Colour4.Blue);
64 testConvertToHSL((5f / 6f, 1f, 0.5f, 1f), Colour4.Magenta);
65
66 // test ToHSL with some other knowns
67 testConvertToHSL((219f / 360f, 0.792f, 0.661f, 1f), Colour4.CornflowerBlue);
68 testConvertToHSL((34f / 360f, 0.437f, 0.686f, 0.5f), Colour4.Tan.Opacity(0.5f));
69 }
70
71 private void testConvertFromHSL(Colour4 expected, (float, float, float, float) convert) =>
72 assertAlmostEqual(expected.Vector, Colour4.FromHSL(convert.Item1, convert.Item2, convert.Item3, convert.Item4).Vector);
73
74 private void testConvertToHSL((float, float, float, float) expected, Colour4 convert) =>
75 assertAlmostEqual(new Vector4(expected.Item1, expected.Item2, expected.Item3, expected.Item4), convert.ToHSL(), "HSLA");
76
77 [Test]
78 public void TestFromHSV()
79 {
80 // test FromHSV that black is only affected by luminance
81 testConvertFromHSV(Colour4.Black, (0f, 0.5f, 0f, 1f));
82 testConvertFromHSV(Colour4.Black, (1f, 1f, 0f, 1f));
83 testConvertFromHSV(Colour4.Black, (0.5f, 0.75f, 0f, 1f));
84
85 // test FromHSV that white and grey are not affected by hue
86 testConvertFromHSV(Colour4.White, (0f, 0f, 1f, 1f));
87 testConvertFromHSV(Colour4.White, (1f, 0f, 1f, 1f));
88 testConvertFromHSV(Colour4.White, (0.5f, 0f, 1f, 1f));
89 testConvertFromHSV(Colour4.Gray, (0f, 0f, 0.5f, 1f));
90 testConvertFromHSV(Colour4.Gray, (0.5f, 0f, 0.5f, 1f));
91 testConvertFromHSV(Colour4.Gray, (1f, 0f, 0.5f, 1f));
92
93 // test FromHSV that alpha is being passed through
94 testConvertFromHSV(Colour4.Black.Opacity(0.5f), (0f, 0f, 0f, 0.5f));
95
96 // test FromHSV with primaries
97 testConvertFromHSV(Colour4.Red, (0, 1f, 1f, 1f));
98 testConvertFromHSV(Colour4.Yellow, (1f / 6f, 1f, 1f, 1f));
99 testConvertFromHSV(Colour4.Lime, (2f / 6f, 1f, 1f, 1f));
100 testConvertFromHSV(Colour4.Cyan, (3f / 6f, 1f, 1f, 1f));
101 testConvertFromHSV(Colour4.Blue, (4f / 6f, 1f, 1f, 1f));
102 testConvertFromHSV(Colour4.Magenta, (5f / 6f, 1f, 1f, 1f));
103 testConvertFromHSV(Colour4.Red, (1f, 1f, 1f, 1f));
104
105 // test FromHSV with some other knowns
106 testConvertFromHSV(Colour4.CornflowerBlue, (219f / 360f, 0.578f, 0.929f, 1f));
107 testConvertFromHSV(Colour4.Tan.Opacity(0.5f), (34f / 360f, 0.333f, 0.824f, 0.5f));
108 }
109
110 [Test]
111 public void TestToHSV()
112 {
113 // test ToHSV that black, white, and grey always return constant 0f for hue and saturation
114 testConvertToHSV((0f, 0f, 1f, 1f), Colour4.White);
115 testConvertToHSV((0f, 0f, 0f, 1f), Colour4.Black);
116 testConvertToHSV((0f, 0f, 0.5f, 1f), Colour4.Gray);
117
118 // test ToHSV that alpha is being passed through
119 testConvertToHSV((0f, 0f, 1f, 0.5f), Colour4.White.Opacity(0.5f));
120
121 // test ToHSV with primaries
122 testConvertToHSV((0, 1f, 1f, 1f), Colour4.Red);
123 testConvertToHSV((1f / 6f, 1f, 1f, 1f), Colour4.Yellow);
124 testConvertToHSV((2f / 6f, 1f, 1f, 1f), Colour4.Lime);
125 testConvertToHSV((3f / 6f, 1f, 1f, 1f), Colour4.Cyan);
126 testConvertToHSV((4f / 6f, 1f, 1f, 1f), Colour4.Blue);
127 testConvertToHSV((5f / 6f, 1f, 1f, 1f), Colour4.Magenta);
128
129 // test ToHSV with some other knowns
130 testConvertToHSV((219f / 360f, 0.578f, 0.929f, 1f), Colour4.CornflowerBlue);
131 testConvertToHSV((34f / 360f, 0.333f, 0.824f, 0.5f), Colour4.Tan.Opacity(0.5f));
132 }
133
134 private void testConvertFromHSV(Colour4 expected, (float, float, float, float) convert) =>
135 assertAlmostEqual(expected.Vector, Colour4.FromHSV(convert.Item1, convert.Item2, convert.Item3, convert.Item4).Vector);
136
137 private void testConvertToHSV((float, float, float, float) expected, Colour4 convert) =>
138 assertAlmostEqual(new Vector4(expected.Item1, expected.Item2, expected.Item3, expected.Item4), convert.ToHSV(), "HSVA");
139
140 [Test]
141 public void TestToHex()
142 {
143 Assert.AreEqual("#D2B48C", Colour4.Tan.ToHex());
144 Assert.AreEqual("#D2B48CFF", Colour4.Tan.ToHex(true));
145 Assert.AreEqual("#6495ED80", Colour4.CornflowerBlue.Opacity(half_alpha).ToHex());
146 }
147
148 private static readonly object[][] valid_hex_colours =
149 {
150 new object[] { Colour4.White, "#fff" },
151 new object[] { Colour4.Red, "#ff0000" },
152 new object[] { Colour4.Yellow.Opacity(half_alpha), "ffff0080" },
153 new object[] { Colour4.Lime.Opacity(half_alpha), "00ff0080" },
154 new object[] { new Colour4(17, 34, 51, 255), "123" },
155 new object[] { new Colour4(17, 34, 51, 255), "#123" },
156 new object[] { new Colour4(17, 34, 51, 68), "1234" },
157 new object[] { new Colour4(17, 34, 51, 68), "#1234" },
158 new object[] { new Colour4(18, 52, 86, 255), "123456" },
159 new object[] { new Colour4(18, 52, 86, 255), "#123456" },
160 new object[] { new Colour4(18, 52, 86, 120), "12345678" },
161 new object[] { new Colour4(18, 52, 86, 120), "#12345678" }
162 };
163
164 [TestCaseSource(nameof(valid_hex_colours))]
165 public void TestFromHex(Colour4 expectedColour, string hexCode)
166 {
167 Assert.AreEqual(expectedColour, Colour4.FromHex(hexCode));
168
169 Assert.True(Colour4.TryParseHex(hexCode, out var actualColour));
170 Assert.AreEqual(expectedColour, actualColour);
171 }
172
173 [TestCase("1")]
174 [TestCase("#1")]
175 [TestCase("12")]
176 [TestCase("#12")]
177 [TestCase("12345")]
178 [TestCase("#12345")]
179 [TestCase("1234567")]
180 [TestCase("#1234567")]
181 [TestCase("123456789")]
182 [TestCase("#123456789")]
183 [TestCase("gg00zz")]
184 public void TestFromHexFailsOnInvalidColours(string invalidColour)
185 {
186 // Assert.Catch allows any exception type, contrary to .Throws<T>() (which expects exactly T)
187 Assert.Catch(() => Colour4.FromHex(invalidColour));
188
189 Assert.False(Colour4.TryParseHex(invalidColour, out _));
190 }
191
192 [Test]
193 public void TestChainingFunctions()
194 {
195 // test that Opacity replaces alpha channel rather than multiplying
196 var expected1 = new Colour4(1f, 0f, 0f, 0.5f);
197 Assert.AreEqual(expected1, Colour4.Red.Opacity(0.5f));
198 Assert.AreEqual(expected1, expected1.Opacity(0.5f));
199
200 // test that MultiplyAlpha multiplies existing alpha channel
201 var expected2 = new Colour4(1f, 0f, 0f, 0.25f);
202 Assert.AreEqual(expected2, expected1.MultiplyAlpha(0.5f));
203 Assert.Throws<ArgumentOutOfRangeException>(() => Colour4.White.MultiplyAlpha(-1f));
204
205 // test clamping all channels in either direction
206 Assert.AreEqual(Colour4.White, new Colour4(1.1f, 1.1f, 1.1f, 1.1f).Clamped());
207 Assert.AreEqual(Colour4.Black.Opacity(0f), new Colour4(-1.1f, -1.1f, -1.1f, -1.1f).Clamped());
208
209 // test lighten and darken
210 assertAlmostEqual(new Colour4(0.431f, 0.642f, 1f, 1f).Vector, Colour4.CornflowerBlue.Lighten(0.1f).Vector);
211 assertAlmostEqual(new Colour4(0.356f, 0.531f, 0.845f, 1f).Vector, Colour4.CornflowerBlue.Darken(0.1f).Vector);
212 }
213
214 [Test]
215 public void TestOperators()
216 {
217 var colour = new Colour4(0.5f, 0.5f, 0.5f, 0.5f);
218 assertAlmostEqual(new Vector4(0.6f, 0.7f, 0.8f, 0.9f), (colour + new Colour4(0.1f, 0.2f, 0.3f, 0.4f)).Vector);
219 assertAlmostEqual(new Vector4(0.4f, 0.3f, 0.2f, 0.1f), (colour - new Colour4(0.1f, 0.2f, 0.3f, 0.4f)).Vector);
220 assertAlmostEqual(new Vector4(0.25f, 0.25f, 0.25f, 0.25f), (colour * colour).Vector);
221 assertAlmostEqual(new Vector4(0.25f, 0.25f, 0.25f, 0.25f), (colour / 2f).Vector);
222 assertAlmostEqual(Colour4.White.Vector, (colour * 2f).Vector);
223 Assert.Throws<ArgumentOutOfRangeException>(() => _ = colour * -1f);
224 Assert.Throws<ArgumentOutOfRangeException>(() => _ = colour / -1f);
225 Assert.Throws<ArgumentOutOfRangeException>(() => _ = colour / 0f);
226 }
227
228 [Test]
229 public void TestOtherConversions()
230 {
231 // test uint conversions
232 Assert.AreEqual(0x6495ED80, Colour4.CornflowerBlue.Opacity(half_alpha).ToRGBA());
233 Assert.AreEqual(0x806495ED, Colour4.CornflowerBlue.Opacity(half_alpha).ToARGB());
234 Assert.AreEqual(Colour4.CornflowerBlue.Opacity(half_alpha), Colour4.FromRGBA(0x6495ED80));
235 Assert.AreEqual(Colour4.CornflowerBlue.Opacity(half_alpha), Colour4.FromARGB(0x806495ED));
236
237 // test SRGB
238 var srgb = new Vector4(0.659f, 0.788f, 0.968f, 1f);
239 assertAlmostEqual(srgb, Colour4.CornflowerBlue.ToSRGB().Vector);
240 assertAlmostEqual(Colour4.CornflowerBlue.Vector, new Colour4(srgb).ToLinear().Vector);
241 }
242
243 private void assertAlmostEqual(Vector4 expected, Vector4 actual, string type = "RGBA")
244 {
245 // note that we use a fairly high delta since the test constants are approximations
246 const float delta = 0.005f;
247 var message = $"({type}) Expected: {expected}, Actual: {actual}";
248 Assert.AreEqual(expected.X, actual.X, delta, message);
249 Assert.AreEqual(expected.Y, actual.Y, delta, message);
250 Assert.AreEqual(expected.Z, actual.Z, delta, message);
251 Assert.AreEqual(expected.W, actual.W, delta, message);
252 }
253
254 // 0x80 alpha is slightly more than half
255 private const float half_alpha = 128f / 255f;
256 }
257}