omni-color 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +1042 -0
  3. package/dist/__test__/color-interop.test.d.ts +1 -0
  4. package/dist/__test__/color-interop.test.js +82 -0
  5. package/dist/__test__/comparison_culori.test.d.ts +1 -0
  6. package/dist/__test__/comparison_culori.test.js +169 -0
  7. package/dist/__test__/interop-chroma.test.d.ts +1 -0
  8. package/dist/__test__/interop-chroma.test.js +676 -0
  9. package/dist/__test__/interop-tinycolor.test.d.ts +1 -0
  10. package/dist/__test__/interop-tinycolor.test.js +499 -0
  11. package/dist/color/__test__/baseColor.test.d.ts +2 -0
  12. package/dist/color/__test__/baseColor.test.d.ts.map +1 -0
  13. package/dist/color/__test__/baseColor.test.js +34 -0
  14. package/dist/color/__test__/baseColor.test.js.map +1 -0
  15. package/dist/color/__test__/blank.d.ts +1 -0
  16. package/dist/color/__test__/blank.js +3 -0
  17. package/dist/color/__test__/color.test.d.ts +1 -0
  18. package/dist/color/__test__/color.test.js +1073 -0
  19. package/dist/color/__test__/colorSpaces.test.d.ts +1 -0
  20. package/dist/color/__test__/colorSpaces.test.js +69 -0
  21. package/dist/color/__test__/combinations.test.d.ts +1 -0
  22. package/dist/color/__test__/combinations.test.js +665 -0
  23. package/dist/color/__test__/conversions.test.d.ts +1 -0
  24. package/dist/color/__test__/conversions.test.js +719 -0
  25. package/dist/color/__test__/deltaE.test.d.ts +1 -0
  26. package/dist/color/__test__/deltaE.test.js +120 -0
  27. package/dist/color/__test__/formats.test.d.ts +1 -0
  28. package/dist/color/__test__/formats.test.js +514 -0
  29. package/dist/color/__test__/gradients.test.d.ts +1 -0
  30. package/dist/color/__test__/gradients.test.js +414 -0
  31. package/dist/color/__test__/harmonies.test.d.ts +1 -0
  32. package/dist/color/__test__/harmonies.test.js +676 -0
  33. package/dist/color/__test__/manipulations.test.d.ts +1 -0
  34. package/dist/color/__test__/manipulations.test.js +264 -0
  35. package/dist/color/__test__/names.test.d.ts +1 -0
  36. package/dist/color/__test__/names.test.js +67 -0
  37. package/dist/color/__test__/parse.test.d.ts +1 -0
  38. package/dist/color/__test__/parse.test.js +256 -0
  39. package/dist/color/__test__/random.test.d.ts +1 -0
  40. package/dist/color/__test__/random.test.js +262 -0
  41. package/dist/color/__test__/readability.test.d.ts +1 -0
  42. package/dist/color/__test__/readability.test.js +2596 -0
  43. package/dist/color/__test__/srgb.test.d.ts +1 -0
  44. package/dist/color/__test__/srgb.test.js +43 -0
  45. package/dist/color/__test__/swatch.test.d.ts +1 -0
  46. package/dist/color/__test__/swatch.test.js +267 -0
  47. package/dist/color/__test__/temperature.test.d.ts +1 -0
  48. package/dist/color/__test__/temperature.test.js +223 -0
  49. package/dist/color/__test__/utils.test.d.ts +1 -0
  50. package/dist/color/__test__/utils.test.js +409 -0
  51. package/dist/color/__test__/validations.test.d.ts +1 -0
  52. package/dist/color/__test__/validations.test.js +239 -0
  53. package/dist/color/color.constants.d.ts +4 -0
  54. package/dist/color/color.constants.d.ts.map +1 -0
  55. package/dist/color/color.constants.js +152 -0
  56. package/dist/color/color.constants.js.map +1 -0
  57. package/dist/color/color.consts.d.ts +4 -0
  58. package/dist/color/color.consts.js +152 -0
  59. package/dist/color/color.d.ts +788 -0
  60. package/dist/color/color.d.ts.map +1 -0
  61. package/dist/color/color.helpers.d.ts +6 -0
  62. package/dist/color/color.helpers.js +4 -0
  63. package/dist/color/color.js +919 -0
  64. package/dist/color/color.js.map +1 -0
  65. package/dist/color/colorSpaces.d.ts +15 -0
  66. package/dist/color/colorSpaces.js +127 -0
  67. package/dist/color/combinations.d.ts +45 -0
  68. package/dist/color/combinations.js +562 -0
  69. package/dist/color/conversions.d.ts +16 -0
  70. package/dist/color/conversions.d.ts.map +1 -0
  71. package/dist/color/conversions.js +705 -0
  72. package/dist/color/conversions.js.map +1 -0
  73. package/dist/color/deltaE.d.ts +59 -0
  74. package/dist/color/deltaE.js +110 -0
  75. package/dist/color/formats.d.ts +128 -0
  76. package/dist/color/formats.d.ts.map +1 -0
  77. package/dist/color/formats.js +153 -0
  78. package/dist/color/formats.js.map +1 -0
  79. package/dist/color/gradients.d.ts +49 -0
  80. package/dist/color/gradients.js +672 -0
  81. package/dist/color/harmonies.d.ts +18 -0
  82. package/dist/color/harmonies.js +128 -0
  83. package/dist/color/manipulations.d.ts +29 -0
  84. package/dist/color/manipulations.js +106 -0
  85. package/dist/color/names.d.ts +38 -0
  86. package/dist/color/names.js +108 -0
  87. package/dist/color/parse.d.ts +2 -0
  88. package/dist/color/parse.js +437 -0
  89. package/dist/color/random.d.ts +30 -0
  90. package/dist/color/random.js +65 -0
  91. package/dist/color/readability.d.ts +53 -0
  92. package/dist/color/readability.js +284 -0
  93. package/dist/color/srgb.d.ts +8 -0
  94. package/dist/color/srgb.js +33 -0
  95. package/dist/color/swatch.d.ts +49 -0
  96. package/dist/color/swatch.js +118 -0
  97. package/dist/color/temperature.d.ts +26 -0
  98. package/dist/color/temperature.js +141 -0
  99. package/dist/color/utils.d.ts +49 -0
  100. package/dist/color/utils.d.ts.map +1 -0
  101. package/dist/color/utils.js +104 -0
  102. package/dist/color/utils.js.map +1 -0
  103. package/dist/color/validations.d.ts +2 -0
  104. package/dist/color/validations.d.ts.map +1 -0
  105. package/dist/color/validations.js +178 -0
  106. package/dist/color/validations.js.map +1 -0
  107. package/dist/demo/src/AppFooter.d.ts +1 -0
  108. package/dist/demo/src/AppFooter.js +4 -0
  109. package/dist/demo/src/AppHeader.d.ts +5 -0
  110. package/dist/demo/src/AppHeader.js +6 -0
  111. package/dist/demo/src/components/Card.d.ts +9 -0
  112. package/dist/demo/src/components/Card.js +9 -0
  113. package/dist/demo/src/components/Chip.d.ts +21 -0
  114. package/dist/demo/src/components/Chip.js +23 -0
  115. package/dist/demo/src/components/ColorBox.d.ts +16 -0
  116. package/dist/demo/src/components/ColorBox.js +45 -0
  117. package/dist/demo/src/components/ColorInfoCard.d.ts +7 -0
  118. package/dist/demo/src/components/ColorInfoCard.js +50 -0
  119. package/dist/demo/src/components/ExpandableCodeSnippet.d.ts +5 -0
  120. package/dist/demo/src/components/ExpandableCodeSnippet.js +17 -0
  121. package/dist/demo/src/components/Icon.d.ts +11 -0
  122. package/dist/demo/src/components/Icon.js +50 -0
  123. package/dist/demo/src/components/Icon.types.d.ts +14 -0
  124. package/dist/demo/src/components/Icon.types.js +15 -0
  125. package/dist/demo/src/components/SectionContainer.d.ts +10 -0
  126. package/dist/demo/src/components/SectionContainer.js +12 -0
  127. package/dist/demo/src/components/VSpace.d.ts +5 -0
  128. package/dist/demo/src/components/VSpace.js +4 -0
  129. package/dist/demo/src/components/inputs/Checkbox.d.ts +7 -0
  130. package/dist/demo/src/components/inputs/Checkbox.js +4 -0
  131. package/dist/demo/src/components/inputs/InputGroup.d.ts +6 -0
  132. package/dist/demo/src/components/inputs/InputGroup.js +4 -0
  133. package/dist/demo/src/components/inputs/NumberInput.d.ts +10 -0
  134. package/dist/demo/src/components/inputs/NumberInput.js +8 -0
  135. package/dist/demo/src/components/inputs/Select.d.ts +12 -0
  136. package/dist/demo/src/components/inputs/Select.js +6 -0
  137. package/dist/demo/src/components/inputs/Slider.d.ts +10 -0
  138. package/dist/demo/src/components/inputs/Slider.js +4 -0
  139. package/dist/demo/src/components/utils.d.ts +17 -0
  140. package/dist/demo/src/components/utils.js +24 -0
  141. package/dist/demo/src/demo/ColorDemo.d.ts +1 -0
  142. package/dist/demo/src/demo/ColorDemo.js +45 -0
  143. package/dist/demo/src/demo/ColorHarmonyDemo.d.ts +6 -0
  144. package/dist/demo/src/demo/ColorHarmonyDemo.js +18 -0
  145. package/dist/demo/src/demo/ColorInfo.d.ts +6 -0
  146. package/dist/demo/src/demo/ColorInfo.js +14 -0
  147. package/dist/demo/src/demo/ColorInput.d.ts +7 -0
  148. package/dist/demo/src/demo/ColorInput.js +58 -0
  149. package/dist/demo/src/demo/ColorManipulationDemo.d.ts +6 -0
  150. package/dist/demo/src/demo/ColorManipulationDemo.js +63 -0
  151. package/dist/demo/src/demo/ColorSwatch.d.ts +13 -0
  152. package/dist/demo/src/demo/ColorSwatch.js +15 -0
  153. package/dist/demo/src/demo/ReadabilityDemo.d.ts +6 -0
  154. package/dist/demo/src/demo/ReadabilityDemo.js +24 -0
  155. package/dist/demo/src/demo/combinations/AverageColorsOptionInputs.d.ts +7 -0
  156. package/dist/demo/src/demo/combinations/AverageColorsOptionInputs.js +7 -0
  157. package/dist/demo/src/demo/combinations/BlendColorsOptionInputs.d.ts +7 -0
  158. package/dist/demo/src/demo/combinations/BlendColorsOptionInputs.js +13 -0
  159. package/dist/demo/src/demo/combinations/ColorCombinationDemo.d.ts +6 -0
  160. package/dist/demo/src/demo/combinations/ColorCombinationDemo.js +78 -0
  161. package/dist/demo/src/demo/combinations/MixColorsOptionInputs.d.ts +7 -0
  162. package/dist/demo/src/demo/combinations/MixColorsOptionInputs.js +10 -0
  163. package/dist/demo/src/demo/combinations/colorCombinationDemo.consts.d.ts +4 -0
  164. package/dist/demo/src/demo/combinations/colorCombinationDemo.consts.js +12 -0
  165. package/dist/demo/src/demo/gradients/GradientOptionInputs.d.ts +10 -0
  166. package/dist/demo/src/demo/gradients/GradientOptionInputs.js +42 -0
  167. package/dist/demo/src/demo/gradients/GradientThroughCard.d.ts +6 -0
  168. package/dist/demo/src/demo/gradients/GradientThroughCard.js +50 -0
  169. package/dist/demo/src/demo/gradients/GradientToCard.d.ts +6 -0
  170. package/dist/demo/src/demo/gradients/GradientToCard.js +54 -0
  171. package/dist/demo/src/demo/gradients/GradientsDemo.d.ts +6 -0
  172. package/dist/demo/src/demo/gradients/GradientsDemo.js +6 -0
  173. package/dist/demo/src/demo/gradients/gradientOptions.consts.d.ts +3 -0
  174. package/dist/demo/src/demo/gradients/gradientOptions.consts.js +16 -0
  175. package/dist/demo/src/demo/palette/ColorPaletteDemo.d.ts +6 -0
  176. package/dist/demo/src/demo/palette/ColorPaletteDemo.js +26 -0
  177. package/dist/demo/src/demo/palette/PaletteGenerationOptions.d.ts +8 -0
  178. package/dist/demo/src/demo/palette/PaletteGenerationOptions.js +53 -0
  179. package/dist/demo/src/demo/palette/PaletteHarmonyOptions.d.ts +7 -0
  180. package/dist/demo/src/demo/palette/PaletteHarmonyOptions.js +5 -0
  181. package/dist/demo/src/main.d.ts +1 -0
  182. package/dist/demo/src/main.js +14 -0
  183. package/dist/demo/src/pages/DemoPage.d.ts +1 -0
  184. package/dist/demo/src/pages/DemoPage.js +28 -0
  185. package/dist/demo/src/pages/PlaygroundPage.d.ts +1 -0
  186. package/dist/demo/src/pages/PlaygroundPage.js +24 -0
  187. package/dist/demo/src/playground/Playground.d.ts +1 -0
  188. package/dist/demo/src/playground/Playground.js +42 -0
  189. package/dist/demo/src/playground/playgroundUtils.d.ts +16 -0
  190. package/dist/demo/src/playground/playgroundUtils.js +202 -0
  191. package/dist/demo/src/seo/PageHead.d.ts +9 -0
  192. package/dist/demo/src/seo/PageHead.js +76 -0
  193. package/dist/demo/src/seo/StructuredData.d.ts +6 -0
  194. package/dist/demo/src/seo/StructuredData.js +13 -0
  195. package/dist/demo/src/toast/ToastProvider.d.ts +2 -0
  196. package/dist/demo/src/toast/ToastProvider.js +62 -0
  197. package/dist/demo/src/toast/index.d.ts +2 -0
  198. package/dist/demo/src/toast/index.js +2 -0
  199. package/dist/demo/src/toast/toastBus.d.ts +13 -0
  200. package/dist/demo/src/toast/toastBus.js +27 -0
  201. package/dist/index.d.ts +1233 -0
  202. package/dist/index.d.ts.map +1 -0
  203. package/dist/index.js +5235 -0
  204. package/dist/index.js.map +1 -0
  205. package/dist/palette/__test__/palette.test.d.ts +1 -0
  206. package/dist/palette/__test__/palette.test.js +397 -0
  207. package/dist/palette/palette.d.ts +41 -0
  208. package/dist/palette/palette.js +126 -0
  209. package/dist/src/__test__/interop-chroma.test.d.ts +1 -0
  210. package/dist/src/__test__/interop-chroma.test.js +673 -0
  211. package/dist/src/__test__/interop-tinycolor.test.d.ts +1 -0
  212. package/dist/src/__test__/interop-tinycolor.test.js +499 -0
  213. package/dist/src/color/__test__/color.test.d.ts +1 -0
  214. package/dist/src/color/__test__/color.test.js +1071 -0
  215. package/dist/src/color/__test__/colorSpaces.test.d.ts +1 -0
  216. package/dist/src/color/__test__/colorSpaces.test.js +69 -0
  217. package/dist/src/color/__test__/combinations.test.d.ts +1 -0
  218. package/dist/src/color/__test__/combinations.test.js +665 -0
  219. package/dist/src/color/__test__/conversions.test.d.ts +1 -0
  220. package/dist/src/color/__test__/conversions.test.js +719 -0
  221. package/dist/src/color/__test__/deltaE.test.d.ts +1 -0
  222. package/dist/src/color/__test__/deltaE.test.js +120 -0
  223. package/dist/src/color/__test__/formats.test.d.ts +1 -0
  224. package/dist/src/color/__test__/formats.test.js +470 -0
  225. package/dist/src/color/__test__/gradients.test.d.ts +1 -0
  226. package/dist/src/color/__test__/gradients.test.js +414 -0
  227. package/dist/src/color/__test__/harmonies.test.d.ts +1 -0
  228. package/dist/src/color/__test__/harmonies.test.js +734 -0
  229. package/dist/src/color/__test__/manipulations.test.d.ts +1 -0
  230. package/dist/src/color/__test__/manipulations.test.js +264 -0
  231. package/dist/src/color/__test__/names.test.d.ts +1 -0
  232. package/dist/src/color/__test__/names.test.js +67 -0
  233. package/dist/src/color/__test__/parse.test.d.ts +1 -0
  234. package/dist/src/color/__test__/parse.test.js +251 -0
  235. package/dist/src/color/__test__/random.test.d.ts +1 -0
  236. package/dist/src/color/__test__/random.test.js +262 -0
  237. package/dist/src/color/__test__/readability.test.d.ts +1 -0
  238. package/dist/src/color/__test__/readability.test.js +2596 -0
  239. package/dist/src/color/__test__/swatch.test.d.ts +1 -0
  240. package/dist/src/color/__test__/swatch.test.js +267 -0
  241. package/dist/src/color/__test__/temperature.test.d.ts +1 -0
  242. package/dist/src/color/__test__/temperature.test.js +223 -0
  243. package/dist/src/color/__test__/utils.test.d.ts +1 -0
  244. package/dist/src/color/__test__/utils.test.js +409 -0
  245. package/dist/src/color/__test__/validations.test.d.ts +1 -0
  246. package/dist/src/color/__test__/validations.test.js +239 -0
  247. package/dist/src/color/color.consts.d.ts +4 -0
  248. package/dist/src/color/color.consts.js +152 -0
  249. package/dist/src/color/color.d.ts +775 -0
  250. package/dist/src/color/color.js +903 -0
  251. package/dist/src/color/colorSpaces.d.ts +15 -0
  252. package/dist/src/color/colorSpaces.js +127 -0
  253. package/dist/src/color/combinations.d.ts +45 -0
  254. package/dist/src/color/combinations.js +557 -0
  255. package/dist/src/color/conversions.d.ts +16 -0
  256. package/dist/src/color/conversions.js +705 -0
  257. package/dist/src/color/deltaE.d.ts +59 -0
  258. package/dist/src/color/deltaE.js +110 -0
  259. package/dist/src/color/formats.d.ts +126 -0
  260. package/dist/src/color/formats.js +150 -0
  261. package/dist/src/color/gradients.d.ts +49 -0
  262. package/dist/src/color/gradients.js +673 -0
  263. package/dist/src/color/harmonies.d.ts +18 -0
  264. package/dist/src/color/harmonies.js +129 -0
  265. package/dist/src/color/manipulations.d.ts +29 -0
  266. package/dist/src/color/manipulations.js +107 -0
  267. package/dist/src/color/names.d.ts +38 -0
  268. package/dist/src/color/names.js +108 -0
  269. package/dist/src/color/parse.d.ts +2 -0
  270. package/dist/src/color/parse.js +438 -0
  271. package/dist/src/color/random.d.ts +30 -0
  272. package/dist/src/color/random.js +65 -0
  273. package/dist/src/color/readability.d.ts +53 -0
  274. package/dist/src/color/readability.js +284 -0
  275. package/dist/src/color/swatch.d.ts +49 -0
  276. package/dist/src/color/swatch.js +117 -0
  277. package/dist/src/color/temperature.d.ts +26 -0
  278. package/dist/src/color/temperature.js +142 -0
  279. package/dist/src/color/utils.d.ts +57 -0
  280. package/dist/src/color/utils.js +142 -0
  281. package/dist/src/color/validations.d.ts +2 -0
  282. package/dist/src/color/validations.js +178 -0
  283. package/dist/src/index.d.ts +20 -0
  284. package/dist/src/index.js +2 -0
  285. package/dist/src/palette/__test__/palette.test.d.ts +1 -0
  286. package/dist/src/palette/__test__/palette.test.js +397 -0
  287. package/dist/src/palette/palette.d.ts +41 -0
  288. package/dist/src/palette/palette.js +127 -0
  289. package/dist/src/utils.d.ts +20 -0
  290. package/dist/src/utils.js +42 -0
  291. package/dist/utils.d.ts +20 -0
  292. package/dist/utils.js +42 -0
  293. package/package.json +96 -0
@@ -0,0 +1,1071 @@
1
+ import { Color } from '../color';
2
+ import { CSS_COLOR_NAME_TO_HEX_MAP } from '../color.consts';
3
+ import { getRandomColorRGBA } from '../random';
4
+ import { getColorFromTemperatureLabel } from '../temperature';
5
+ jest.mock('../random', () => {
6
+ const actual = jest.requireActual('../random');
7
+ return {
8
+ ...actual,
9
+ getRandomColorRGBA: jest.fn(actual.getRandomColorRGBA),
10
+ };
11
+ });
12
+ const BASE_HEX = '#ff0000';
13
+ const BASE_RGB = { r: 255, g: 0, b: 0 };
14
+ const BASE_HSL = { h: 0, s: 100, l: 50 };
15
+ const BASE_HSV = { h: 0, s: 100, v: 100 };
16
+ const BASE_HWB = { h: 0, w: 0, b: 0 };
17
+ const BASE_CMYK = { c: 0, m: 100, y: 100, k: 0 };
18
+ const BASE_LAB = { l: 53.233, a: 80.109, b: 67.22 };
19
+ const BASE_LCH = { l: 53.233, c: 104.576, h: 40 };
20
+ const BASE_OKLAB = { l: 0.627955, a: 0.224863, b: 0.125846, format: 'OKLAB' };
21
+ const BASE_OKLCH = { l: 0.627955, c: 0.257683, h: 29.234 };
22
+ const HEX8_OPAQUE = '#ff0000ff';
23
+ const HEX8_SEMI_TRANSPARENT = '#ff000080';
24
+ const SHORT_HEX_ALPHA = 0.5333333333333333;
25
+ const HEX8_ALPHA = 0.5019607843137255;
26
+ function checkAllConversions(color, alpha, hex8) {
27
+ expect(color.toHex()).toBe(BASE_HEX);
28
+ expect(color.toHex8()).toBe(hex8);
29
+ expect(color.toRGB()).toEqual(BASE_RGB);
30
+ expect(color.toRGBA()).toEqual({ ...BASE_RGB, a: alpha });
31
+ expect(color.toHSL()).toEqual(BASE_HSL);
32
+ expect(color.toHSLA()).toEqual({ ...BASE_HSL, a: alpha });
33
+ expect(color.toHSV()).toEqual(BASE_HSV);
34
+ expect(color.toHSVA()).toEqual({ ...BASE_HSV, a: alpha });
35
+ expect(color.toHWB()).toEqual(BASE_HWB);
36
+ expect(color.toHWBA()).toEqual({ ...BASE_HWB, a: alpha });
37
+ expect(color.toCMYK()).toEqual(BASE_CMYK);
38
+ const lab = color.toLAB();
39
+ expect(lab.l).toBeCloseTo(BASE_LAB.l, 3);
40
+ expect(lab.a).toBeCloseTo(BASE_LAB.a, 3);
41
+ expect(lab.b).toBeCloseTo(BASE_LAB.b, 3);
42
+ const lch = color.toLCH();
43
+ expect(lch.l).toBeCloseTo(BASE_LCH.l, 3);
44
+ expect(lch.c).toBeCloseTo(BASE_LCH.c, 3);
45
+ expect(lch.h).toBeCloseTo(BASE_LCH.h, 3);
46
+ const oklab = color.toOKLAB();
47
+ expect(oklab.l).toBeCloseTo(BASE_OKLAB.l, 6);
48
+ expect(oklab.a).toBeCloseTo(BASE_OKLAB.a, 6);
49
+ expect(oklab.b).toBeCloseTo(BASE_OKLAB.b, 6);
50
+ const oklch = color.toOKLCH();
51
+ expect(oklch.l).toBeCloseTo(BASE_OKLCH.l, 6);
52
+ expect(oklch.c).toBeCloseTo(BASE_OKLCH.c, 6);
53
+ expect(oklch.h).toBeCloseTo(BASE_OKLCH.h, 3);
54
+ }
55
+ describe('Color constructor and conversion tests', () => {
56
+ it('correctly initializes from and converts hex input', () => {
57
+ const color = new Color(BASE_HEX);
58
+ checkAllConversions(color, 1, HEX8_OPAQUE);
59
+ });
60
+ it('correctly initializes from color name input', () => {
61
+ const color = new Color('red');
62
+ checkAllConversions(color, 1, HEX8_OPAQUE);
63
+ });
64
+ it('correctly initializes from and converts short hex input', () => {
65
+ const shortHex = '#fff';
66
+ const color = new Color(shortHex);
67
+ expect(color.toHex()).toBe('#ffffff');
68
+ expect(color.toHex8()).toBe('#ffffffff');
69
+ const rgb = { r: 255, g: 255, b: 255 };
70
+ expect(color.toRGB()).toEqual(rgb);
71
+ expect(color.toRGBA()).toEqual({ ...rgb, a: 1 });
72
+ const hsl = { h: 0, s: 0, l: 100 };
73
+ expect(color.toHSL()).toEqual(hsl);
74
+ expect(color.toHSLA()).toEqual({ ...hsl, a: 1 });
75
+ const hsv = { h: 0, s: 0, v: 100 };
76
+ expect(color.toHSV()).toEqual(hsv);
77
+ expect(color.toHSVA()).toEqual({ ...hsv, a: 1 });
78
+ const cmyk = { c: 0, m: 0, y: 0, k: 0 };
79
+ expect(color.toCMYK()).toEqual(cmyk);
80
+ const lch = color.toLCH();
81
+ expect(lch.l).toBeCloseTo(100, 3);
82
+ expect(lch.c).toBeCloseTo(0, 1);
83
+ const oklch = color.toOKLCH();
84
+ expect(oklch.l).toBeCloseTo(1, 6);
85
+ expect(oklch.c).toBeCloseTo(0, 6);
86
+ });
87
+ it('correctly initializes from and converts short hex with alpha input', () => {
88
+ const shortHex8 = '#f008';
89
+ const color = new Color(shortHex8);
90
+ checkAllConversions(color, SHORT_HEX_ALPHA, '#ff000088');
91
+ });
92
+ it('correctly initializes from and converts hex8 input', () => {
93
+ const color = new Color(HEX8_SEMI_TRANSPARENT);
94
+ checkAllConversions(color, HEX8_ALPHA, HEX8_SEMI_TRANSPARENT);
95
+ });
96
+ it('correctly initializes from and converts rgb input', () => {
97
+ const color = new Color(BASE_RGB);
98
+ checkAllConversions(color, 1, HEX8_OPAQUE);
99
+ });
100
+ it('correctly initializes from and converts rgba input', () => {
101
+ const color = new Color({ ...BASE_RGB, a: 0.5 });
102
+ checkAllConversions(color, 0.5, HEX8_SEMI_TRANSPARENT);
103
+ });
104
+ it('cleans decimal RGB inputs before returning values', () => {
105
+ const color = new Color({ r: 12.4, g: 100.6, b: 200.5 });
106
+ expect(color.toRGB()).toEqual({ r: 12, g: 101, b: 201 });
107
+ expect(color.toRGBA()).toEqual({ r: 12.4, g: 100.6, b: 200.5, a: 1 });
108
+ expect(color.toHex()).toBe('#0c65c9');
109
+ expect(color.toHex8()).toBe('#0c65c9ff');
110
+ });
111
+ it('correctly initializes from and converts hsl input', () => {
112
+ const color = new Color(BASE_HSL);
113
+ checkAllConversions(color, 1, HEX8_OPAQUE);
114
+ });
115
+ it('correctly initializes from and converts hsla input', () => {
116
+ const color = new Color({ ...BASE_HSL, a: 0.5 });
117
+ checkAllConversions(color, 0.5, HEX8_SEMI_TRANSPARENT);
118
+ });
119
+ it('correctly initializes from and converts hsv input', () => {
120
+ const color = new Color(BASE_HSV);
121
+ checkAllConversions(color, 1, HEX8_OPAQUE);
122
+ });
123
+ it('correctly initializes from and converts hsva input', () => {
124
+ const color = new Color({ ...BASE_HSV, a: 0.5 });
125
+ checkAllConversions(color, 0.5, HEX8_SEMI_TRANSPARENT);
126
+ });
127
+ it('correctly initializes from and converts cmyk input', () => {
128
+ const color = new Color(BASE_CMYK);
129
+ checkAllConversions(color, 1, HEX8_OPAQUE);
130
+ });
131
+ it('correctly initializes from and converts lch input', () => {
132
+ const color = new Color(BASE_LCH);
133
+ checkAllConversions(color, 1, HEX8_OPAQUE);
134
+ });
135
+ it('correctly initializes from and converts oklab input', () => {
136
+ const color = new Color(BASE_OKLAB);
137
+ checkAllConversions(color, 1, HEX8_OPAQUE);
138
+ });
139
+ it('correctly initializes from and converts oklch input', () => {
140
+ const color = new Color(BASE_OKLCH);
141
+ checkAllConversions(color, 1, HEX8_OPAQUE);
142
+ });
143
+ it('correctly initializes from another color instance', () => {
144
+ const color1 = new Color('#7c74f0');
145
+ const color2 = new Color(color1);
146
+ expect(color2.toRGB()).toEqual(color1.toRGB());
147
+ expect(color1).not.toBe(color2);
148
+ });
149
+ it('accepts color temperature label strings', () => {
150
+ let color = new Color('fluorescent');
151
+ expect(color.toHex()).toBe(getColorFromTemperatureLabel('Fluorescent lamp').toHex());
152
+ color = new Color('Daylight');
153
+ expect(color.toHex()).toBe(getColorFromTemperatureLabel('Daylight').toHex());
154
+ color = new Color(' shade ');
155
+ expect(color.toHex()).toBe(getColorFromTemperatureLabel('Shade').toHex());
156
+ color = new Color('blue sky');
157
+ expect(color.toHex()).toBe(getColorFromTemperatureLabel('Blue sky').toHex());
158
+ });
159
+ });
160
+ describe('Color.random', () => {
161
+ it('creates a Color instance with default options', () => {
162
+ const color = Color.random();
163
+ expect(color).toBeInstanceOf(Color);
164
+ });
165
+ it('respects provided options', () => {
166
+ const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
167
+ const anchoredColor = Color.random({ anchorColor: 'Blue' });
168
+ expect(anchoredColor.getName().name).toBe('Blue');
169
+ const paletteSuitableColor = Color.random({ paletteSuitable: true });
170
+ const paletteSuitableHSL = paletteSuitableColor.toHSL();
171
+ expect(paletteSuitableHSL.s).toBeGreaterThanOrEqual(40);
172
+ expect(paletteSuitableHSL.l).toBeGreaterThanOrEqual(25);
173
+ expect(paletteSuitableHSL.l).toBeLessThanOrEqual(75);
174
+ const alphaColor = Color.random({ alpha: 0.25 });
175
+ expect(alphaColor.getAlpha()).toBe(0.25);
176
+ const randomizedAlpha = Color.random({ randomizeAlpha: true });
177
+ expect(randomizedAlpha.getAlpha()).toBe(0.5);
178
+ spy.mockRestore();
179
+ });
180
+ });
181
+ describe('Random color pathways', () => {
182
+ const mockColor = { r: 10, g: 20, b: 30, a: 0.4 };
183
+ const randomSpy = getRandomColorRGBA;
184
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
185
+ const actualRandom = jest.requireActual('../random')
186
+ .getRandomColorRGBA;
187
+ beforeEach(() => {
188
+ randomSpy.mockImplementation(actualRandom);
189
+ randomSpy.mockClear();
190
+ });
191
+ afterEach(() => {
192
+ randomSpy.mockImplementation(actualRandom);
193
+ randomSpy.mockClear();
194
+ });
195
+ it('uses getRandomColorRGBA when constructed without arguments', () => {
196
+ randomSpy.mockReturnValue(mockColor);
197
+ const color = new Color();
198
+ expect(randomSpy).toHaveBeenCalledTimes(1);
199
+ expect(color.toRGB()).toEqual({ r: 10, g: 20, b: 30 });
200
+ expect(color.getAlpha()).toBe(0.4);
201
+ });
202
+ it('uses getRandomColorRGBA when constructed with null', () => {
203
+ randomSpy.mockReturnValue(mockColor);
204
+ const color = new Color(null);
205
+ expect(randomSpy).toHaveBeenCalledTimes(1);
206
+ expect(color.toRGB()).toEqual({ r: 10, g: 20, b: 30 });
207
+ expect(color.getAlpha()).toBe(0.4);
208
+ });
209
+ it('Color.random uses getRandomColorRGBA', () => {
210
+ randomSpy.mockReturnValue(mockColor);
211
+ const color = Color.random();
212
+ expect(randomSpy).toHaveBeenCalledTimes(1);
213
+ expect(color.toRGB()).toEqual({ r: 10, g: 20, b: 30 });
214
+ expect(color.getAlpha()).toBe(0.4);
215
+ });
216
+ });
217
+ describe('Color.toXString methods', () => {
218
+ it('returns string representations of the color', () => {
219
+ const color = new Color({ ...BASE_RGB, a: 0.5 });
220
+ expect(color.toRGBString()).toBe('rgb(255 0 0)');
221
+ expect(color.toRGBAString()).toBe('rgb(255 0 0 / 0.5)');
222
+ expect(color.toHSLString()).toBe('hsl(0 100% 50%)');
223
+ expect(color.toHSLAString()).toBe('hsl(0 100% 50% / 0.5)');
224
+ expect(color.toHWBString()).toBe('hwb(0 0% 0%)');
225
+ expect(color.toHWBAString()).toBe('hwb(0 0% 0% / 0.5)');
226
+ expect(color.toCMYKString()).toBe('device-cmyk(0% 100% 100% 0%)');
227
+ expect(color.toLABString()).toBe('lab(53.233% 80.109 67.22)');
228
+ expect(color.toLCHString()).toBe('lch(53.233% 104.576 40)');
229
+ expect(color.toOKLABString()).toBe('oklab(0.627955 0.224863 0.125846)');
230
+ expect(color.toOKLCHString()).toBe('oklch(0.627955 0.257683 29.234)');
231
+ expect(color.toColorString()).toBe('color(srgb 1 0 0 / 0.5)');
232
+ expect(color.toColorString({ space: 'display-p3' })).toBe('color(display-p3 0.917488 0.200287 0.138561 / 0.5)');
233
+ expect(color.toColorString({ space: 'REC2020' })).toBe('color(rec2020 0.791977 0.230976 0.073761 / 0.5)');
234
+ });
235
+ });
236
+ describe('Named color support', () => {
237
+ it('initializes from all named colors (case insensitive)', () => {
238
+ for (const [name, hex] of Object.entries(CSS_COLOR_NAME_TO_HEX_MAP)) {
239
+ expect(new Color(name).toHex()).toEqual(hex);
240
+ expect(new Color(name.toUpperCase()).toHex()).toEqual(hex);
241
+ }
242
+ // Check other named input formatting (ignore caps, whitespace):
243
+ expect(new Color('Red').toRGBA()).toEqual({ r: 255, g: 0, b: 0, a: 1 });
244
+ expect(new Color('blACK').toRGB()).toEqual({ r: 0, g: 0, b: 0 });
245
+ expect(new Color('light blue').toHex()).toEqual('#add8e6');
246
+ expect(new Color('light Golden rod YELLOW').toHex()).toEqual('#fafad2');
247
+ });
248
+ it('throws on unknown color names', () => {
249
+ expect(() => new Color('notacolor')).toThrow();
250
+ });
251
+ it('accepts CSS color format strings', () => {
252
+ expect(new Color('rgb(255, 0, 0)').toHex()).toEqual('#ff0000');
253
+ expect(new Color('hsla(0, 100%, 50%, 0.5)').toHex8()).toEqual('#ff000080');
254
+ });
255
+ it('throws on invalid color format strings', () => {
256
+ expect(() => new Color('rgb(255, 0)')).toThrow();
257
+ });
258
+ });
259
+ describe('Color.getAlpha', () => {
260
+ it('returns the alpha channel value', () => {
261
+ const color = new Color({ ...BASE_RGB, a: 0.5 });
262
+ expect(color.getAlpha()).toBe(0.5);
263
+ });
264
+ it('parses alpha from hex8 strings', () => {
265
+ const opaque = '#ffffffff';
266
+ const transparent = '#00000000';
267
+ const semiTransparent = '#123456cc';
268
+ expect(new Color(opaque).getAlpha()).toBe(1);
269
+ expect(new Color(transparent).getAlpha()).toBe(0);
270
+ expect(new Color(semiTransparent).getAlpha()).toBeCloseTo(0.8, 3);
271
+ });
272
+ it('handles alpha in hsla and hsva inputs', () => {
273
+ const hsla = { h: 210, s: 40, l: 60, a: 0.25 };
274
+ const hsva = { h: 120, s: 70, v: 80, a: 0.75 };
275
+ expect(new Color(hsla).getAlpha()).toBe(0.25);
276
+ expect(new Color(hsva).getAlpha()).toBe(0.75);
277
+ });
278
+ it('accepts fully transparent rgba values', () => {
279
+ const color = new Color({ r: 10, g: 20, b: 30, a: 0 });
280
+ expect(color.getAlpha()).toBe(0);
281
+ });
282
+ });
283
+ describe('Color.setAlpha', () => {
284
+ it('updates the alpha channel', () => {
285
+ const color = new Color(BASE_RGB);
286
+ const updated = color.setAlpha(0.5);
287
+ expect(updated.getAlpha()).toBe(0.5);
288
+ expect(updated.toHex8()).toBe(HEX8_SEMI_TRANSPARENT);
289
+ });
290
+ it('clamps correctly when alpha is out of range', () => {
291
+ const color = new Color({ ...BASE_RGB, a: 0.5 });
292
+ expect(color.getAlpha()).toBe(0.5);
293
+ const updated1 = color.setAlpha(1.5);
294
+ expect(updated1.getAlpha()).toBe(1);
295
+ expect(updated1.toRGBA()).toEqual({ ...BASE_RGB, a: 1 });
296
+ const updated2 = updated1.setAlpha(-0.1);
297
+ expect(updated2.getAlpha()).toBe(0);
298
+ expect(updated2.toRGBA()).toEqual({ ...BASE_RGB, a: 0 });
299
+ });
300
+ it('defaults to full opacity for non-finite alpha values', () => {
301
+ const invalidValues = [undefined, NaN, 'foo'];
302
+ for (const value of invalidValues) {
303
+ const color = new Color({ ...BASE_RGB, a: 0 });
304
+ const updated = color.setAlpha(value);
305
+ expect(updated.getAlpha()).toBe(1);
306
+ }
307
+ });
308
+ it('is chainable', () => {
309
+ const color = new Color(BASE_RGB).setAlpha(0.25);
310
+ expect(color.toRGBA()).toEqual({ ...BASE_RGB, a: 0.25 });
311
+ });
312
+ });
313
+ describe('Color.spin', () => {
314
+ it('returns a new color with the hue rotated', () => {
315
+ const red = new Color('#ff0000');
316
+ const spunForward = red.spin(180);
317
+ expect(spunForward).not.toBe(red);
318
+ expect(spunForward.toHex()).toBe('#00ffff');
319
+ expect(red.toHex()).toBe('#ff0000');
320
+ const spunBackward = red.spin(-180);
321
+ expect(spunBackward).not.toBe(red);
322
+ expect(spunBackward.toHex()).toBe('#00ffff');
323
+ expect(red.toHex()).toBe('#ff0000');
324
+ });
325
+ it('supports fractional rotations without losing hue precision', () => {
326
+ const red = new Color('#ff0000');
327
+ const spunForward = red.spin(30.5);
328
+ const spunBackward = red.spin(-30.7);
329
+ expect(spunForward.toHex()).toBe('#ff8200');
330
+ expect(spunBackward.toHex()).toBe('#ff0082');
331
+ expect(spunBackward.toHSL().h).toBeCloseTo(329, 0);
332
+ expect(red.toHex()).toBe('#ff0000');
333
+ });
334
+ it('preserves alpha when spinning hue', () => {
335
+ const translucentRed = new Color('rgba(255, 0, 0, 0.35)');
336
+ const spun = translucentRed.spin(60);
337
+ expect(spun.toHex8()).toBe('#ffff0059');
338
+ expect(spun.getAlpha()).toBeCloseTo(0.35, 5);
339
+ expect(translucentRed.getAlpha()).toBeCloseTo(0.35, 5);
340
+ });
341
+ });
342
+ describe('Color.brighten', () => {
343
+ it('lightens the color without mutating the original', () => {
344
+ const base = new Color('#808080');
345
+ const brighter = base.brighten();
346
+ expect(brighter.toHex()).toBe('#9a9a9a');
347
+ expect(base.toHex()).toBe('#808080');
348
+ });
349
+ it('supports manipulation options and preserves alpha', () => {
350
+ const navy = new Color('#001f3f');
351
+ expect(navy.brighten({ space: 'LAB', amount: 25 }).toHex8()).toBe('#7688b0ff');
352
+ expect(navy.toHex()).toBe('#001f3f');
353
+ const translucentTeal = new Color('rgba(0, 128, 128, 0.35)');
354
+ const labBrightened = translucentTeal.brighten({ space: 'LAB' });
355
+ expect(labBrightened.toHex8()).toBe('#4cb0af59');
356
+ expect(translucentTeal.toHex8()).toBe('#00808059');
357
+ });
358
+ it('applies a custom LAB step scale when provided', () => {
359
+ const navy = new Color('#001f3f');
360
+ expect(navy.brighten({ space: 'LAB', amount: 25, labScale: 10 }).toHex()).toBe('#43567c');
361
+ expect(navy.toHex()).toBe('#001f3f');
362
+ });
363
+ it('respects fractional HSL adjustments when an options object is provided', () => {
364
+ const base = new Color('rgba(18, 52, 86, 0.4)');
365
+ const brightened = base.brighten({ amount: 7.5, space: 'HSL' });
366
+ expect(brightened.toHex8()).toBe('#19477666');
367
+ expect(base.toHex8()).toBe('#12345666');
368
+ });
369
+ });
370
+ describe('Color.darken', () => {
371
+ it('darkens the color without mutating the original', () => {
372
+ const base = new Color('#808080');
373
+ const darker = base.darken();
374
+ expect(darker.toHex()).toBe('#676767');
375
+ expect(base.toHex()).toBe('#808080');
376
+ });
377
+ it('darkens LAB colors with custom scaling while keeping alpha', () => {
378
+ const base = new Color({ l: 45, a: -5, b: 15 }).setAlpha(0.65);
379
+ const darker = base.darken({ space: 'LAB', amount: 15, labScale: 5 });
380
+ expect(darker.toHex8()).toBe('#5a5a40a6');
381
+ expect(darker.toLAB().l).toBeCloseTo(37.583, 3);
382
+ expect(base.toHex8()).toBe('#6c6c51a6');
383
+ });
384
+ });
385
+ describe('Color.saturate', () => {
386
+ it('increases saturation without mutating the original', () => {
387
+ const base = new Color('#6699cc');
388
+ const saturated = base.saturate({ amount: 20 });
389
+ expect(saturated.toHex()).toBe('#5299e0');
390
+ expect(base.toHex()).toBe('#6699cc');
391
+ });
392
+ it('respects LCH options and keeps transparency intact', () => {
393
+ const translucentTeal = new Color('rgba(0, 128, 128, 0.35)');
394
+ const saturatedLch = translucentTeal.saturate({ space: 'LCH' });
395
+ expect(saturatedLch.toHex8()).toBe('#00868859');
396
+ expect(translucentTeal.toHex8()).toBe('#00808059');
397
+ });
398
+ it('supports a configurable LAB-like step scale for LCH saturation', () => {
399
+ const mutedTeal = new Color('hsl(190, 25%, 55%)');
400
+ const saturatedLch = mutedTeal.saturate({ space: 'LCH', amount: 40, labScale: 12 });
401
+ expect(saturatedLch.toHex()).toBe('#00b1dd');
402
+ expect(mutedTeal.toHex()).toBe('#709fa9');
403
+ });
404
+ it('returns a fresh instance when saturation change is zero', () => {
405
+ const base = new Color('rgba(170, 119, 51, 0.4)');
406
+ const saturated = base.saturate({ amount: 0 });
407
+ expect(saturated.toHex8()).toBe('#aa773366');
408
+ expect(saturated).not.toBe(base);
409
+ expect(base.toHex8()).toBe('#aa773366');
410
+ });
411
+ });
412
+ describe('Color.desaturate', () => {
413
+ it('decreases saturation without mutating the original', () => {
414
+ const base = new Color('#6699cc');
415
+ const desaturated = base.desaturate({ amount: 20 });
416
+ expect(desaturated.toHex()).toBe('#7a99b8');
417
+ expect(base.toHex()).toBe('#6699cc');
418
+ });
419
+ it('respects LAB-like scaling for LCH desaturation', () => {
420
+ const magenta = new Color('#ff00ff');
421
+ const defaultScale = magenta.desaturate({ space: 'LCH', amount: 30 });
422
+ const customScale = magenta.desaturate({ space: 'LCH', amount: 30, labScale: 8 });
423
+ expect(defaultScale.toHex()).toBe('#d06ccb');
424
+ expect(customScale.toHex()).toBe('#eb4be8');
425
+ expect(magenta.toHex()).toBe('#ff00ff');
426
+ });
427
+ it('clamps saturation to gray for very large reductions', () => {
428
+ const orange = new Color('#ff8800');
429
+ const gray = orange.desaturate({ amount: 200 });
430
+ expect(gray.toHex()).toBe('#808080');
431
+ expect(orange.toHex()).toBe('#ff8800');
432
+ });
433
+ });
434
+ describe('Color.grayscale', () => {
435
+ it('converts the color to grayscale', () => {
436
+ const red = new Color('#ff0000');
437
+ const gray = red.grayscale();
438
+ expect(gray.toHex()).toBe('#808080');
439
+ expect(red.toHex()).toBe('#ff0000');
440
+ });
441
+ });
442
+ describe('Color.getComplementaryColors', () => {
443
+ it('returns the original color and its complement', () => {
444
+ const red = new Color('#ff0000');
445
+ const [orig, comp] = red.getComplementaryColors();
446
+ expect(orig.toHex()).toBe('#ff0000');
447
+ expect(comp.toHex()).toBe('#00ffff');
448
+ expect(orig).not.toBe(red);
449
+ expect(comp).not.toBe(red);
450
+ });
451
+ it('respects grayscale handling mode for grayscale colors', () => {
452
+ const gray = new Color('#7f7f7f');
453
+ const [base, complement] = gray.getComplementaryColors({ grayscaleHandlingMode: 'IGNORE' });
454
+ expect(base.toHex()).toBe('#7f7f7f');
455
+ expect(complement.toHex()).toBe('#7f7f7f');
456
+ });
457
+ });
458
+ describe('Color.getSplitComplementaryColors', () => {
459
+ it('returns the split complementary colors', () => {
460
+ const red = new Color('#ff0000');
461
+ const [orig, comp1, comp2] = red.getSplitComplementaryColors();
462
+ expect(orig.toHex()).toBe('#ff0000');
463
+ expect(comp1.toHex()).toBe('#0080ff');
464
+ expect(comp2.toHex()).toBe('#00ff80');
465
+ });
466
+ });
467
+ describe('Color.getTriadicHarmonyColors', () => {
468
+ it('returns the triadic harmony colors', () => {
469
+ const red = new Color('#ff0000');
470
+ const [orig, triad1, triad2] = red.getTriadicHarmonyColors();
471
+ expect(orig.toHex()).toBe('#ff0000');
472
+ expect(triad1.toHex()).toBe('#0000ff');
473
+ expect(triad2.toHex()).toBe('#00ff00');
474
+ });
475
+ });
476
+ describe('Color.getSquareHarmonyColors', () => {
477
+ it('returns the square harmony colors', () => {
478
+ const red = new Color('#ff0000');
479
+ const [orig, sq1, sq2, sq3] = red.getSquareHarmonyColors();
480
+ expect(orig.toHex()).toBe('#ff0000');
481
+ expect(sq1.toHex()).toBe('#80ff00');
482
+ expect(sq2.toHex()).toBe('#00ffff');
483
+ expect(sq3.toHex()).toBe('#8000ff');
484
+ });
485
+ });
486
+ describe('Color.getTetradicHarmonyColors', () => {
487
+ it('returns the tetradic harmony colors', () => {
488
+ const red = new Color('#ff0000');
489
+ const [orig, t2, t3, t4] = red.getTetradicHarmonyColors();
490
+ expect(orig.toHex()).toBe('#ff0000');
491
+ expect(t2.toHex()).toBe('#ffff00');
492
+ expect(t3.toHex()).toBe('#00ffff');
493
+ expect(t4.toHex()).toBe('#0000ff');
494
+ });
495
+ });
496
+ describe('Color.getAnalogousHarmonyColors', () => {
497
+ it('returns the analogous harmony colors', () => {
498
+ const red = new Color('#ff0000');
499
+ const [orig, a2, a3, a4, a5] = red.getAnalogousHarmonyColors();
500
+ expect(orig.toHex()).toBe('#ff0000');
501
+ expect(a2.toHex()).toBe('#ff0080');
502
+ expect(a3.toHex()).toBe('#ff8000');
503
+ expect(a4.toHex()).toBe('#ff00ff');
504
+ expect(a5.toHex()).toBe('#ffff00');
505
+ });
506
+ });
507
+ describe('Color.getMonochromaticHarmonyColors', () => {
508
+ it('returns the monochromatic harmony colors', () => {
509
+ const red = new Color('#ff0000');
510
+ const [orig, mono2, mono3, mono4, mono5] = red.getMonochromaticHarmonyColors();
511
+ expect(orig.toHex()).toBe('#ff0000');
512
+ expect(mono2.toHex()).toBe('#ff6666');
513
+ expect(mono3.toHex()).toBe('#990000');
514
+ expect(mono4.toHex()).toBe('#ff0000');
515
+ expect(mono5.toHex()).toBe('#e61919');
516
+ });
517
+ });
518
+ describe('Color.getHarmonyColors', () => {
519
+ it('returns harmony colors for the requested algorithm', () => {
520
+ const base = new Color('#ff0000');
521
+ const harmony = base.getHarmonyColors('SQUARE');
522
+ expect(harmony.map((color) => color.toHex())).toEqual([
523
+ '#ff0000',
524
+ '#80ff00',
525
+ '#00ffff',
526
+ '#8000ff',
527
+ ]);
528
+ });
529
+ it('forwards grayscale handling mode', () => {
530
+ const gray = new Color('#606060');
531
+ const harmony = gray.getHarmonyColors('TRIADIC', { grayscaleHandlingMode: 'IGNORE' });
532
+ expect(harmony.map((color) => color.toHex())).toEqual(['#606060', '#606060', '#606060']);
533
+ });
534
+ it('accepts harmony algorithm names case-insensitively', () => {
535
+ const base = new Color('#1e90ff');
536
+ const triadic = base.getHarmonyColors('triadic');
537
+ expect(triadic.map((color) => color.toHex())).toEqual(['#1e90ff', '#90ff1e', '#ff1e90']);
538
+ const analogous = base.getHarmonyColors('analogous');
539
+ expect(analogous.map((color) => color.toHex())).toEqual([
540
+ '#1e90ff',
541
+ '#1efffe',
542
+ '#1e20ff',
543
+ '#1eff8d',
544
+ '#8d1eff',
545
+ ]);
546
+ });
547
+ it('supports harmonies from OKLAB inputs', () => {
548
+ const base = new Color({ ...BASE_OKLAB, format: 'OKLAB' });
549
+ const complementary = base.getHarmonyColors('COMPLEMENTARY');
550
+ expect(complementary.map((color) => color.toHex())).toEqual(['#ff0000', '#00ffff']);
551
+ });
552
+ });
553
+ describe('Color.getColorSwatch', () => {
554
+ it('returns the correct swatch for a color', () => {
555
+ const baseColor = new Color('#625aa5');
556
+ const swatch = baseColor.getColorSwatch();
557
+ expect(swatch.type).toBe('BASIC');
558
+ expect(swatch.mainStop).toBe(500);
559
+ expect(swatch[100].toHex()).toBe('#dcd9f2');
560
+ expect(swatch[200].toHex()).toBe('#bab5e3');
561
+ expect(swatch[300].toHex()).toBe('#9b94d1');
562
+ expect(swatch[400].toHex()).toBe('#7d76bc');
563
+ expect(swatch[500].toHex()).toBe('#625aa5');
564
+ expect(swatch[600].toHex()).toBe('#524d7f');
565
+ expect(swatch[700].toHex()).toBe('#413e5b');
566
+ expect(swatch[800].toHex()).toBe('#2d2c3a');
567
+ expect(swatch[900].toHex()).toBe('#18171c');
568
+ });
569
+ it('returns the extended swatch when requested', () => {
570
+ const baseColor = new Color('#625aa5');
571
+ const swatch = baseColor.getColorSwatch({ extended: true });
572
+ expect(swatch.type).toBe('EXTENDED');
573
+ expect(swatch.mainStop).toBe(500);
574
+ expect(swatch[50].toHex()).toBe('#edecf9');
575
+ expect(swatch[100].toHex()).toBe('#dcd9f2');
576
+ expect(swatch[150].toHex()).toBe('#cbc7eb');
577
+ expect(swatch[200].toHex()).toBe('#bab5e3');
578
+ expect(swatch[250].toHex()).toBe('#aaa5da');
579
+ expect(swatch[300].toHex()).toBe('#9b94d1');
580
+ expect(swatch[350].toHex()).toBe('#8c85c7');
581
+ expect(swatch[400].toHex()).toBe('#7d76bc');
582
+ expect(swatch[450].toHex()).toBe('#6f68b1');
583
+ expect(swatch[500].toHex()).toBe('#625aa5');
584
+ expect(swatch[550].toHex()).toBe('#5a5492');
585
+ expect(swatch[600].toHex()).toBe('#524d7f');
586
+ expect(swatch[650].toHex()).toBe('#4a466d');
587
+ expect(swatch[700].toHex()).toBe('#413e5b');
588
+ expect(swatch[750].toHex()).toBe('#37354b');
589
+ expect(swatch[800].toHex()).toBe('#2d2c3a');
590
+ expect(swatch[850].toHex()).toBe('#23222b');
591
+ expect(swatch[900].toHex()).toBe('#18171c');
592
+ expect(swatch[950].toHex()).toBe('#0c0c0e');
593
+ });
594
+ it('keeps main anchor shades identical between base and extended swatches', () => {
595
+ const baseColor = new Color('#625aa5');
596
+ const baseSwatch = baseColor.getColorSwatch();
597
+ const extendedSwatch = baseColor.getColorSwatch({ extended: true });
598
+ expect(extendedSwatch[100].toHex()).toBe(baseSwatch[100].toHex());
599
+ expect(extendedSwatch[200].toHex()).toBe(baseSwatch[200].toHex());
600
+ expect(extendedSwatch[300].toHex()).toBe(baseSwatch[300].toHex());
601
+ expect(extendedSwatch[400].toHex()).toBe(baseSwatch[400].toHex());
602
+ expect(extendedSwatch[500].toHex()).toBe(baseSwatch[500].toHex());
603
+ expect(extendedSwatch[600].toHex()).toBe(baseSwatch[600].toHex());
604
+ expect(extendedSwatch[700].toHex()).toBe(baseSwatch[700].toHex());
605
+ expect(extendedSwatch[800].toHex()).toBe(baseSwatch[800].toHex());
606
+ expect(extendedSwatch[900].toHex()).toBe(baseSwatch[900].toHex());
607
+ });
608
+ });
609
+ describe('Color.getColorPalette', () => {
610
+ it('generates palettes with different harmonies and options', () => {
611
+ const baseColor = new Color('#625aa5');
612
+ // Default complementary palette
613
+ const defaultPalette = baseColor.getColorPalette();
614
+ expect(defaultPalette.secondaryColors).toHaveLength(1);
615
+ const complement = baseColor.spin(180);
616
+ expect(defaultPalette.secondaryColors[0][500].toHex()).toBe(complement.toHex());
617
+ // Triadic palette
618
+ const triadicPalette = baseColor.getColorPalette('TRIADIC');
619
+ expect(triadicPalette.secondaryColors).toHaveLength(2);
620
+ const triadic1 = baseColor.spin(-120);
621
+ const triadic2 = baseColor.spin(120);
622
+ expect(triadicPalette.secondaryColors[0][500].toHex()).toBe(triadic1.toHex());
623
+ expect(triadicPalette.secondaryColors[1][500].toHex()).toBe(triadic2.toHex());
624
+ // Semantic color harmonization options
625
+ const noPullPalette = baseColor.getColorPalette('COMPLEMENTARY', {
626
+ semanticHarmonization: {
627
+ huePull: 0,
628
+ chromaRange: [0.02, 0.25],
629
+ },
630
+ });
631
+ const fullPullPalette = baseColor.getColorPalette('COMPLEMENTARY', {
632
+ semanticHarmonization: {
633
+ huePull: 1,
634
+ chromaRange: [0.02, 0.25],
635
+ },
636
+ });
637
+ const baseHue = baseColor.toOKLCH().h;
638
+ const infoHueDefault = noPullPalette.info[500].toOKLCH().h;
639
+ const infoHuePulled = fullPullPalette.info[500].toOKLCH().h;
640
+ expect(infoHueDefault).toBeCloseTo(265, 0);
641
+ expect(infoHuePulled).toBeCloseTo(baseHue, 0);
642
+ // Neutral color harmonization options
643
+ const neutralMatchPalette = baseColor.getColorPalette('COMPLEMENTARY', {
644
+ neutralHarmonization: {
645
+ tintChromaFactor: 0,
646
+ maxTintChroma: 0.04,
647
+ },
648
+ });
649
+ expect(neutralMatchPalette.tintedNeutrals[500].toHex()).toBe(neutralMatchPalette.neutrals[500].toHex());
650
+ const cappedTintPalette = baseColor.getColorPalette('COMPLEMENTARY', {
651
+ neutralHarmonization: {
652
+ tintChromaFactor: 1,
653
+ maxTintChroma: 0.02,
654
+ },
655
+ });
656
+ const cappedTintChroma = cappedTintPalette.tintedNeutrals[500].toOKLCH().c;
657
+ expect(cappedTintChroma).toBeGreaterThan(0);
658
+ expect(cappedTintChroma).toBeLessThanOrEqual(0.02);
659
+ // Semantic chroma range option
660
+ const defaultInfoChroma = defaultPalette.info[500].toOKLCH().c;
661
+ const limitedChromaPalette = baseColor.getColorPalette('COMPLEMENTARY', {
662
+ semanticHarmonization: {
663
+ huePull: 0,
664
+ chromaRange: [0.02, 0.05],
665
+ },
666
+ });
667
+ const limitedInfoChroma = limitedChromaPalette.info[500].toOKLCH().c;
668
+ expect(limitedInfoChroma).toBeGreaterThanOrEqual(0.02);
669
+ expect(limitedInfoChroma).toBeLessThanOrEqual(0.05);
670
+ expect(limitedInfoChroma).toBeLessThan(defaultInfoChroma);
671
+ });
672
+ });
673
+ describe('Color.isDark sanity check', () => {
674
+ it('identifies dark and light colors', () => {
675
+ expect(new Color('#000000').isDark()).toBe(true);
676
+ expect(new Color('#ffffff').isDark()).toBe(false);
677
+ });
678
+ });
679
+ describe('Color.isOffWhite sanity check', () => {
680
+ it('identifies off-white and non-off-white colors', () => {
681
+ expect(new Color('#ffffff').isOffWhite()).toBe(true);
682
+ expect(new Color('#f0f0f0').isOffWhite()).toBe(true);
683
+ expect(new Color('#dddddd').isOffWhite()).toBe(false);
684
+ });
685
+ });
686
+ describe('Color.isPaletteSuitable sanity check', () => {
687
+ it('matches the paletteSuitable threshold criteria', () => {
688
+ expect(new Color({ h: 0, s: 40, l: 25 }).isPaletteSuitable()).toBe(true);
689
+ expect(new Color({ h: 240, s: 70, l: 75 }).isPaletteSuitable()).toBe(true);
690
+ expect(new Color({ h: 0, s: 39, l: 25 }).isPaletteSuitable()).toBe(false);
691
+ expect(new Color({ h: 0, s: 40, l: 24 }).isPaletteSuitable()).toBe(false);
692
+ expect(new Color({ h: 0, s: 40, l: 76 }).isPaletteSuitable()).toBe(false);
693
+ });
694
+ });
695
+ describe('Color.getWCAGContrastRatio', () => {
696
+ it('calculates the WCAG contrast ratio between colors', () => {
697
+ const c1 = new Color('#444444');
698
+ const c2 = new Color('#bbbbbb');
699
+ expect(c1.getWCAGContrastRatio(c2)).toBeCloseTo(5.07, 2);
700
+ expect(c2.getWCAGContrastRatio(c1)).toBeCloseTo(5.07, 2);
701
+ });
702
+ });
703
+ describe('Color.getAPCAReadabilityScore', () => {
704
+ it('returns the APCA readability score against a background color', () => {
705
+ const fg = new Color('#ff0000');
706
+ const bg = new Color('#ffffff');
707
+ expect(fg.getAPCAReadabilityScore(bg)).toBeCloseTo(64.13, 2);
708
+ expect(bg.getAPCAReadabilityScore(fg)).toBeCloseTo(-69.62, 2);
709
+ });
710
+ });
711
+ describe('Color.getAPCAReadabilityReport', () => {
712
+ it('returns APCA advisory report by default and supports threshold policy options', () => {
713
+ const foreground = new Color('#555555');
714
+ const background = new Color('#aaaaaa');
715
+ const advisory = foreground.getAPCAReadabilityReport(background);
716
+ expect(advisory.isReadable).toBeNull();
717
+ expect(advisory.requiredLc).toBeNull();
718
+ expect(advisory.shortfall).toBeNull();
719
+ const presetReport = foreground.getAPCAReadabilityReport(background, {
720
+ policy: 'PRESET',
721
+ preset: 'LARGE_TEXT',
722
+ });
723
+ expect(presetReport.requiredLc).toBe(45);
724
+ expect(presetReport.isReadable).toBe(false);
725
+ const customReport = foreground.getAPCAReadabilityReport(background, {
726
+ policy: 'CUSTOM',
727
+ threshold: 35,
728
+ });
729
+ expect(customReport.requiredLc).toBe(35);
730
+ expect(customReport.isReadable).toBe(true);
731
+ expect(customReport.shortfall).toBe(0);
732
+ });
733
+ });
734
+ describe('Color.isReadableAsTextColor with APCA options', () => {
735
+ it('returns a boolean for default, APCA advisory, and APCA threshold modes', () => {
736
+ const foreground = new Color('#333333');
737
+ const background = new Color('#ffffff');
738
+ expect(foreground.isReadableAsTextColor(background)).toBe(true);
739
+ expect(foreground.isReadableAsTextColor(background, { algorithm: 'APCA' })).toBe(true);
740
+ expect(foreground.isReadableAsTextColor(background, {
741
+ algorithm: 'APCA',
742
+ apcaOptions: { policy: 'PRESET', preset: 'BODY' },
743
+ })).toBe(true);
744
+ });
745
+ });
746
+ describe('Color.getMostReadableTextColor', () => {
747
+ it('returns the text color with the strongest WCAG readability', () => {
748
+ const background = new Color('#ffffff');
749
+ const black = new Color('#000000');
750
+ const charcoal = new Color('#1a1a1a');
751
+ const gray = new Color('#777777');
752
+ const result = background.getMostReadableTextColor([gray, charcoal, black]);
753
+ expect(result.toHex()).toBe('#000000');
754
+ expect(result).not.toBe(black);
755
+ });
756
+ it('can evaluate candidates with APCA scoring', () => {
757
+ const background = new Color('#0a0a0a');
758
+ const cyan = new Color('#00ffff');
759
+ const orange = new Color('#ff9900');
760
+ const options = { algorithm: 'APCA' };
761
+ const result = background.getMostReadableTextColor([cyan, orange], options);
762
+ expect(result.toHex()).toBe('#00ffff');
763
+ });
764
+ it('accepts readonly candidate arrays', () => {
765
+ const background = new Color('#1b263b');
766
+ const slateBlue = new Color('#6b7b8c');
767
+ const seaside = new Color('#e0fbfc');
768
+ const ember = new Color('#e4572e');
769
+ const result = background.getMostReadableTextColor([slateBlue, seaside, ember]);
770
+ expect(result.toHex()).toBe(seaside.toHex());
771
+ });
772
+ it('resolves readability algorithms without regard to casing', () => {
773
+ const background = new Color('#f8f9fb');
774
+ const navy = new Color('#1b2a49');
775
+ const coral = new Color('#ff715b');
776
+ const result = background.getMostReadableTextColor([navy, coral], { algorithm: 'apca' });
777
+ expect(result.toHex()).toBe('#1b2a49');
778
+ });
779
+ it('returns the strongest readable color from a swatch', () => {
780
+ const background = new Color('#f8fafc');
781
+ const basicSwatch = new Color('#21352e').getColorSwatch({ centerOn500: true });
782
+ const resultBasic = background.getMostReadableTextColor(basicSwatch);
783
+ expect(resultBasic.equals(basicSwatch[900])).toBe(true);
784
+ expect(resultBasic).not.toBe(basicSwatch[900]);
785
+ const extendedSwatch = new Color('#0ea5e9ff').getColorSwatch({
786
+ extended: true,
787
+ centerOn500: true,
788
+ });
789
+ const resultExtended = background.getMostReadableTextColor(extendedSwatch);
790
+ expect(resultExtended.equals(extendedSwatch[950])).toBe(true);
791
+ expect(resultExtended).not.toBe(extendedSwatch[950]);
792
+ });
793
+ });
794
+ describe('Color.getWCAGReadabilityReport', () => {
795
+ it('provides readability details for color pairs', () => {
796
+ const c1 = new Color('#444444');
797
+ const c2 = new Color('#bbbbbb');
798
+ const report = c1.getWCAGReadabilityReport(c2);
799
+ expect(report.contrastRatio).toBeCloseTo(5.07, 2);
800
+ expect(report.requiredContrast).toBe(4.5);
801
+ expect(report.isReadable).toBe(true);
802
+ expect(report.shortfall).toBeCloseTo(0, 2);
803
+ const stricter = c1.getWCAGReadabilityReport(c2, { level: 'AAA' });
804
+ expect(stricter.isReadable).toBe(false);
805
+ expect(stricter.requiredContrast).toBe(7);
806
+ });
807
+ it('respects text size options', () => {
808
+ const c1 = new Color('#555555');
809
+ const c2 = new Color('#aaaaaa');
810
+ const report = c1.getWCAGReadabilityReport(c2, { size: 'LARGE' });
811
+ expect(report.contrastRatio).toBeCloseTo(3.21, 2);
812
+ expect(report.requiredContrast).toBe(3);
813
+ expect(report.isReadable).toBe(true);
814
+ expect(report.shortfall).toBeCloseTo(0, 2);
815
+ });
816
+ it('treats readability options in a case-insensitive way', () => {
817
+ const c1 = new Color('#555555');
818
+ const c2 = new Color('#aaaaaa');
819
+ const report = c1.getWCAGReadabilityReport(c2, { level: 'aaa', size: 'large' });
820
+ expect(report.contrastRatio).toBeCloseTo(3.21, 2);
821
+ expect(report.requiredContrast).toBe(4.5);
822
+ expect(report.isReadable).toBe(false);
823
+ expect(report.shortfall).toBeCloseTo(1.29, 2);
824
+ });
825
+ });
826
+ describe('Color.isReadableAsTextColor', () => {
827
+ it('checks readability for colors', () => {
828
+ const c1 = new Color('#444444');
829
+ const c2 = new Color('#bbbbbb');
830
+ expect(c1.isReadableAsTextColor(c2)).toBe(true);
831
+ expect(c2.isReadableAsTextColor(c1)).toBe(true);
832
+ expect(c1.isReadableAsTextColor(c2, { wcagOptions: { level: 'AAA' } })).toBe(false);
833
+ expect(c2.isReadableAsTextColor(c1, { wcagOptions: { level: 'AAA' } })).toBe(false);
834
+ });
835
+ });
836
+ describe('Color.getBestBackgroundColor', () => {
837
+ it('returns the most readable background color for a given text color', () => {
838
+ const textColor = new Color('#111111');
839
+ const white = new Color('#ffffff');
840
+ const darkGray = new Color('#222222');
841
+ const slate = new Color('#333333');
842
+ const result = textColor.getBestBackgroundColor([darkGray, slate, white]);
843
+ expect(result.toHex()).toBe(white.toHex());
844
+ expect(result).not.toBe(white);
845
+ });
846
+ it('respects WCAG options when picking the closest match', () => {
847
+ const textColor = new Color('#808080');
848
+ const paleGray = new Color('#c0c0c0');
849
+ const black = new Color('#000000');
850
+ const options = {
851
+ wcagOptions: { level: 'AAA', size: 'SMALL' },
852
+ };
853
+ const result = textColor.getBestBackgroundColor([paleGray, black], options);
854
+ expect(result.toHex()).toBe(black.toHex());
855
+ });
856
+ it('handles a single background color', () => {
857
+ const textColor = new Color('#121212');
858
+ const onlyBackground = new Color('#fafafa');
859
+ const result = textColor.getBestBackgroundColor([onlyBackground]);
860
+ expect(result.toHex()).toBe('#fafafa');
861
+ });
862
+ it('picks the strongest APCA background from multiple candidates', () => {
863
+ const textColor = new Color('#ffeeaa');
864
+ const slate = new Color('#1c1f24');
865
+ const teal = new Color('#0f4c5c');
866
+ const wheat = new Color('#f5deb3');
867
+ const result = textColor.getBestBackgroundColor([slate, teal, wheat], { algorithm: 'APCA' });
868
+ expect(result.toHex()).toBe('#1c1f24');
869
+ });
870
+ it('throws when no background colors are provided', () => {
871
+ const textColor = new Color('#123456');
872
+ expect(() => textColor.getBestBackgroundColor([])).toThrow('At least one background color must be provided.');
873
+ });
874
+ it('accepts readonly background candidates', () => {
875
+ const textColor = new Color('#fdf6e3');
876
+ const midnight = new Color('#0f172a');
877
+ const navy = new Color('#1e3a8a');
878
+ const driftwood = new Color('#b49b6b');
879
+ const result = textColor.getBestBackgroundColor([midnight, navy, driftwood]);
880
+ expect(result.toHex()).toBe(midnight.toHex());
881
+ });
882
+ it('selects the best background from a swatch', () => {
883
+ const textColor = new Color('#111111');
884
+ const basicSwatch = new Color('#3e3623ff').getColorSwatch();
885
+ const resultBasic = textColor.getBestBackgroundColor(basicSwatch);
886
+ expect(resultBasic.equals(basicSwatch[100])).toBe(true);
887
+ expect(resultBasic).not.toBe(basicSwatch[100]);
888
+ const extendedSwatch = new Color('#eab308').getColorSwatch({
889
+ extended: true,
890
+ centerOn500: true,
891
+ });
892
+ const resultExtended = textColor.getBestBackgroundColor(extendedSwatch);
893
+ expect(resultExtended.equals(extendedSwatch[50])).toBe(true);
894
+ expect(resultExtended).not.toBe(extendedSwatch[50]);
895
+ });
896
+ });
897
+ describe('Color temperature methods', () => {
898
+ it('estimates temperature and label', () => {
899
+ const color = new Color('#ffa757');
900
+ expect(color.getTemperature().label).toBe('Incandescent lamp');
901
+ });
902
+ it('returns a temperature string for off-white colors', () => {
903
+ expect(new Color('#ffffff').getTemperatureAsString()).toBe('6504 K (cloudy sky)');
904
+ expect(new Color('#ffffff').getTemperatureAsString({ formatNumber: true })).toBe('6,504 K (cloudy sky)');
905
+ });
906
+ it('creates colors from temperature values or labels', () => {
907
+ expect(Color.fromTemperature(1500).toHex()).toBe('#ff6c00');
908
+ expect(Color.fromTemperature('Shade').toHex()).toBe('#dde6ff');
909
+ });
910
+ });
911
+ describe('Color.getName', () => {
912
+ it('returns the base color name and lightness modifier', () => {
913
+ const red = new Color(BASE_HEX);
914
+ expect(red.getName()).toEqual({ name: 'Red', lightness: 'Normal' });
915
+ const lightGray = { h: 0, s: 0, l: 80 };
916
+ const gray = new Color(lightGray);
917
+ expect(gray.getName()).toEqual({ name: 'Gray', lightness: 'Light' });
918
+ });
919
+ });
920
+ describe('Color.getNameAsString', () => {
921
+ it('formats the color name as a string', () => {
922
+ const red = new Color(BASE_HEX);
923
+ expect(red.getNameAsString()).toBe('red');
924
+ const darkGreen = { h: 120, s: 100, l: 20 };
925
+ const green = new Color(darkGreen);
926
+ expect(green.getNameAsString()).toBe('dark green');
927
+ const lightGray = { h: 0, s: 0, l: 80 };
928
+ const gray = new Color(lightGray);
929
+ expect(gray.getNameAsString()).toBe('light gray');
930
+ });
931
+ });
932
+ describe('Color.clone', () => {
933
+ it('creates a copy of the color instance', () => {
934
+ const color = new Color(BASE_HEX);
935
+ const cloned = color.clone();
936
+ expect(cloned).toEqual(color);
937
+ expect(cloned).not.toBe(color);
938
+ });
939
+ });
940
+ describe('Color immutability', () => {
941
+ it('returns defensive copies from toRGBA', () => {
942
+ const color = new Color('#336699');
943
+ const firstRGBA = color.toRGBA();
944
+ const secondRGBA = color.toRGBA();
945
+ expect(firstRGBA).toEqual(secondRGBA);
946
+ expect(firstRGBA).not.toBe(secondRGBA);
947
+ firstRGBA.r = 255;
948
+ firstRGBA.g = 255;
949
+ firstRGBA.b = 255;
950
+ firstRGBA.a = 0;
951
+ expect(color.toHex()).toBe('#336699');
952
+ expect(color.getAlpha()).toBe(1);
953
+ });
954
+ it('does not expose mutable internal state through runtime property access', () => {
955
+ const original = new Color('#123456');
956
+ const sameValueClone = new Color(original);
957
+ const runtimePrivateColor = Reflect.get(original, 'color');
958
+ expect(runtimePrivateColor).toBeUndefined();
959
+ expect(original.toHex()).toBe('#123456');
960
+ expect(sameValueClone.toHex()).toBe('#123456');
961
+ expect(sameValueClone).not.toBe(original);
962
+ });
963
+ });
964
+ describe('Color.equals', () => {
965
+ it('compares colors for equality', () => {
966
+ const color = new Color('#123456');
967
+ const same = new Color('rgb(18, 52, 86)');
968
+ const different = new Color('#654321');
969
+ expect(color.equals(same)).toBe(true);
970
+ expect(color.equals(different)).toBe(false);
971
+ });
972
+ it('only permits floating-point drift, not channel differences', () => {
973
+ const base = new Color('rgba(10, 20, 30, 0.333)');
974
+ const drift = new Color({ r: 10.0000004, g: 20.0000004, b: 30.0000004, a: 0.3330004 });
975
+ const offByOne = new Color('rgba(11, 20, 30, 0.333)');
976
+ expect(base.equals(drift)).toBe(true);
977
+ expect(base.equals(offByOne)).toBe(false);
978
+ });
979
+ });
980
+ describe('Color.differenceFrom', () => {
981
+ it('returns 0 for identical colors', () => {
982
+ const color = new Color('#abcdef');
983
+ expect(color.differenceFrom(new Color('#abcdef'))).toBeCloseTo(0, 6);
984
+ });
985
+ it('calculates differences using different methods', () => {
986
+ const color = new Color('#ff0000');
987
+ const other = new Color('#00ff00');
988
+ expect(color.differenceFrom(other, { method: 'CIE76' })).toBeGreaterThan(0);
989
+ expect(color.differenceFrom(other, { method: 'CIE94' })).toBeCloseTo(73.432, 2);
990
+ expect(color.differenceFrom(other)).toBeCloseTo(86.615, 2);
991
+ });
992
+ });
993
+ describe('Color mixing and averaging', () => {
994
+ it('mixes colors from mutable inputs', () => {
995
+ const base = new Color('#030303');
996
+ const others = ['#060606', '#090909'];
997
+ const result = base.mix(others, { space: 'RGB' });
998
+ expect(result.toHex()).toBe('#121212');
999
+ });
1000
+ it('mixes colors in LINEAR_RGB space by default', () => {
1001
+ const base = new Color('#ff0000');
1002
+ const result = base.mix(['#0000ff']);
1003
+ expect(result.toHex()).toBe('#bc00bc');
1004
+ expect(result.toRGBA()).toEqual(new Color('#bc00bc').toRGBA());
1005
+ });
1006
+ it('mixes colors from readonly inputs', () => {
1007
+ const base = new Color('#123456');
1008
+ const others = ['#abcdef'];
1009
+ const result = base.mix(others, { space: 'RGB' });
1010
+ expect(result.toHex()).toBe('#bdffff');
1011
+ });
1012
+ it('averages colors from mutable inputs', () => {
1013
+ const base = new Color('#050a0f');
1014
+ const others = ['#0f0a05'];
1015
+ const result = base.average(others, { space: 'RGB' });
1016
+ expect(result.toHex()).toBe('#0a0a0a');
1017
+ });
1018
+ it('averages colors from readonly inputs', () => {
1019
+ const base = new Color('#112233');
1020
+ const others = ['#8899aa', '#223344'];
1021
+ const result = base.average(others, { space: 'RGB' });
1022
+ expect(result.toHex()).toBe('#3e4f60');
1023
+ });
1024
+ });
1025
+ describe('Color gradients', () => {
1026
+ it('creates OKLCH gradients through instance helpers', () => {
1027
+ const gradient = new Color('#ff6b6b').createGradientTo('#004cff', { stops: 3 });
1028
+ expect(gradient).toHaveLength(3);
1029
+ expect(gradient[0].toHex()).toBe('#ff6b6b');
1030
+ expect(gradient[1].toHex()).toBe('#c145d1');
1031
+ expect(gradient[2].toHex()).toBe('#004cff');
1032
+ });
1033
+ it('keeps intermediate anchors fixed when easing through multiple colors', () => {
1034
+ const base = new Color('#004cff');
1035
+ const middle = new Color('#00ffaa');
1036
+ const end = new Color('#ff6600');
1037
+ const gradient = base.createGradientThrough([middle, end], { stops: 3, easing: 'EASE_IN' });
1038
+ expect(gradient).toHaveLength(3);
1039
+ expect(gradient[0].toHex()).toBe('#004cff');
1040
+ expect(gradient[1].toHex()).toBe('#00ffaa');
1041
+ expect(gradient[2].toHex()).toBe('#ff6600');
1042
+ });
1043
+ it('builds gradients through readonly stop arrays', () => {
1044
+ const base = new Color('#1d3557');
1045
+ const anchors = [new Color('#2a9d8f'), new Color('#f4a261')];
1046
+ const gradient = base.createGradientThrough(anchors, { stops: 3, space: 'RGB' });
1047
+ expect(gradient.map((color) => color.toHex())).toEqual(['#1d3557', '#2a9d8f', '#f4a261']);
1048
+ });
1049
+ it('supports OKLAB gradients to avoid polar hue unwinding', () => {
1050
+ const start = new Color('#ff0000');
1051
+ const end = new Color('#0000ff');
1052
+ const gradient = start.createGradientTo(end, { stops: 3, space: 'OKLAB' });
1053
+ const mid = gradient[1].toOKLAB();
1054
+ expect(gradient).toHaveLength(3);
1055
+ expect(mid.l).toBeCloseTo(0.539339, 6);
1056
+ expect(mid.a).toBeCloseTo(0.095752, 6);
1057
+ expect(mid.b).toBeCloseTo(-0.092635, 6);
1058
+ });
1059
+ });
1060
+ describe('Color option validation', () => {
1061
+ it('throws when runtime option strings are invalid', () => {
1062
+ const base = new Color('#ff0000');
1063
+ const blue = new Color('#0000ff');
1064
+ expect(() => base.mix([blue], { space: 'INVALID' })).toThrow("Invalid 'space'");
1065
+ expect(() => base.blend(blue, { mode: 'INVALID' })).toThrow("Invalid 'mode'");
1066
+ expect(() => base.createGradientTo(blue, { hueInterpolationMode: 'INVALID' })).toThrow("Invalid 'hueInterpolationMode'");
1067
+ expect(() => base.isReadableAsTextColor('#ffffff', {
1068
+ algorithm: 'INVALID',
1069
+ })).toThrow("Invalid 'algorithm'");
1070
+ });
1071
+ });