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,676 @@
1
+ import chroma from 'chroma-js';
2
+ import { Color } from '../index';
3
+ function chromaRGBArrayToObj(values) {
4
+ if (values.length < 3) {
5
+ throw new Error('Invalid RGB array from chroma-js');
6
+ }
7
+ return {
8
+ r: values[0],
9
+ g: values[1],
10
+ b: values[2],
11
+ a: values.length > 3 ? expect.closeTo(values[3], 2) : undefined,
12
+ };
13
+ }
14
+ function expectSimilarRGBAValues(omniValues, chromaValues, tolerance = 1) {
15
+ const epsilon = 1e-6;
16
+ expect(Math.abs(omniValues.r - chromaValues[0])).toBeLessThanOrEqual(tolerance + epsilon);
17
+ expect(Math.abs(omniValues.g - chromaValues[1])).toBeLessThanOrEqual(tolerance + epsilon);
18
+ expect(Math.abs(omniValues.b - chromaValues[2])).toBeLessThanOrEqual(tolerance + epsilon);
19
+ expect(omniValues.a ?? 1).toBeCloseTo(chromaValues[3] ?? 1, 2);
20
+ }
21
+ function compositeRgba(foreground, background) {
22
+ const compositeAlpha = foreground.a + background.a * (1 - foreground.a);
23
+ if (compositeAlpha === 0) {
24
+ return { ...background, a: 0 };
25
+ }
26
+ const r = (foreground.r * foreground.a + background.r * background.a * (1 - foreground.a)) /
27
+ compositeAlpha;
28
+ const g = (foreground.g * foreground.a + background.g * background.a * (1 - foreground.a)) /
29
+ compositeAlpha;
30
+ const b = (foreground.b * foreground.a + background.b * background.a * (1 - foreground.a)) /
31
+ compositeAlpha;
32
+ return { r, g, b, a: compositeAlpha };
33
+ }
34
+ describe('Color interoperability with chroma-js', () => {
35
+ describe('parses and normalizes inputs the same way', () => {
36
+ it('matches chroma-js hex and rgb outputs across formats', () => {
37
+ const baseHex = new Color('#336699');
38
+ const baseHexChroma = chroma('#336699');
39
+ expect(baseHex.toHex()).toBe(baseHexChroma.hex().toLowerCase());
40
+ expect(baseHex.toRGB()).toEqual(chromaRGBArrayToObj(baseHexChroma.rgb()));
41
+ const namedColor = new Color('rebeccapurple');
42
+ const namedColorChroma = chroma('rebeccapurple');
43
+ expect(namedColor.toHex()).toBe(namedColorChroma.hex().toLowerCase());
44
+ expect(namedColor.toRGB()).toEqual(chromaRGBArrayToObj(namedColorChroma.rgb()));
45
+ const hslStringColor = new Color('hsl(210, 50%, 40%)');
46
+ const hslStringChroma = chroma('hsl(210, 50%, 40%)');
47
+ expect(hslStringColor.toHex()).toBe(hslStringChroma.hex().toLowerCase());
48
+ expect(hslStringColor.toRGB()).toEqual(chromaRGBArrayToObj(hslStringChroma.rgb()));
49
+ });
50
+ });
51
+ describe('handles alpha channels similarly', () => {
52
+ it('keeps alpha values aligned for hex8 and rgba inputs', () => {
53
+ const hexWithAlpha = new Color('#1e90ff80');
54
+ const hexWithAlphaChroma = chroma('#1e90ff80');
55
+ expect(hexWithAlpha.toHex8()).toBe(hexWithAlphaChroma.hex('rgba').toLowerCase());
56
+ expect(hexWithAlpha.toRGBA()).toEqual(chromaRGBArrayToObj(hexWithAlphaChroma.rgba()));
57
+ const rgbaStringColor = new Color('rgba(12, 200, 180, 0.35)');
58
+ const rgbaStringChroma = chroma('rgba(12, 200, 180, 0.35)');
59
+ expect(rgbaStringColor.toHex8()).toBe(rgbaStringChroma.hex('rgba').toLowerCase());
60
+ expect(rgbaStringColor.toRGBA()).toEqual(chromaRGBArrayToObj(rgbaStringChroma.rgba()));
61
+ });
62
+ });
63
+ describe('CMYK parity with chroma-js', () => {
64
+ it('keeps CMYK channels and string output aligned for representative hex inputs', () => {
65
+ const pastelBlue = new Color('#abcdef');
66
+ const pastelBlueChroma = chroma('#abcdef');
67
+ const pastelBlueCmyk = pastelBlue.toCMYK();
68
+ const pastelBlueChromaCmyk = pastelBlueChroma.cmyk();
69
+ expect(pastelBlueCmyk.c).toBeCloseTo(pastelBlueChromaCmyk[0] * 100, 0);
70
+ expect(pastelBlueCmyk.m).toBeCloseTo(pastelBlueChromaCmyk[1] * 100, 0);
71
+ expect(pastelBlueCmyk.y).toBeCloseTo(pastelBlueChromaCmyk[2] * 100, 0);
72
+ expect(pastelBlueCmyk.k).toBeCloseTo(pastelBlueChromaCmyk[3] * 100, 0);
73
+ const pastelBlueChromaCmykString = `device-cmyk(${Math.round(pastelBlueChromaCmyk[0] * 100)}% ${Math.round(pastelBlueChromaCmyk[1] * 100)}% ${Math.round(pastelBlueChromaCmyk[2] * 100)}% ${Math.round(pastelBlueChromaCmyk[3] * 100)}%)`;
74
+ expect(pastelBlue.toCMYKString()).toBe(pastelBlueChromaCmykString);
75
+ const warmYellow = new Color('#ffcc00');
76
+ const warmYellowChroma = chroma('#ffcc00');
77
+ const warmYellowCmyk = warmYellow.toCMYK();
78
+ const warmYellowChromaCmyk = warmYellowChroma.cmyk();
79
+ expect(warmYellowCmyk.c).toBeCloseTo(warmYellowChromaCmyk[0] * 100, 0);
80
+ expect(warmYellowCmyk.m).toBeCloseTo(warmYellowChromaCmyk[1] * 100, 0);
81
+ expect(warmYellowCmyk.y).toBeCloseTo(warmYellowChromaCmyk[2] * 100, 0);
82
+ expect(warmYellowCmyk.k).toBeCloseTo(warmYellowChromaCmyk[3] * 100, 0);
83
+ const warmYellowChromaCmykString = `device-cmyk(${Math.round(warmYellowChromaCmyk[0] * 100)}% ${Math.round(warmYellowChromaCmyk[1] * 100)}% ${Math.round(warmYellowChromaCmyk[2] * 100)}% ${Math.round(warmYellowChromaCmyk[3] * 100)}%)`;
84
+ expect(warmYellow.toCMYKString()).toBe(warmYellowChromaCmykString);
85
+ });
86
+ it('parses CMYK strings consistently when converting back to hex', () => {
87
+ const lightCmykString = 'cmyk(20%, 10%, 0%, 0%)';
88
+ const lightCmykColor = new Color(lightCmykString);
89
+ const chromaLightCmyk = chroma.cmyk(0.2, 0.1, 0, 0);
90
+ expect(lightCmykColor.toHex()).toBe(chromaLightCmyk.hex().toLowerCase());
91
+ expect(lightCmykColor.toHex8()).toBe(chromaLightCmyk.hex('rgba').toLowerCase());
92
+ const richCmykString = 'cmyk(5% 0% 60% 20%)';
93
+ const richCmykColor = new Color(richCmykString);
94
+ const chromaRichCmyk = chroma.cmyk(0.05, 0, 0.6, 0.2);
95
+ expect(richCmykColor.toHex()).toBe(chromaRichCmyk.hex().toLowerCase());
96
+ expect(richCmykColor.toHex8()).toBe(chromaRichCmyk.hex('rgba').toLowerCase());
97
+ });
98
+ });
99
+ describe('mixing parity with chroma-js', () => {
100
+ it('mixes LINEAR_RGB colors using sRGB companding (close to chroma-js lrgb)', () => {
101
+ // chroma-js uses an lrgb approximation (gamma ≈ 2.2 power curve) instead of the sRGB
102
+ // piecewise transfer function (gamma 2.4 with a linear toe below 0.04045). Our LINEAR_RGB
103
+ // path follows the sRGB spec, so results are slightly brighter than chroma’s approximation.
104
+ const omniLinear = new Color('#ff0000').mixWith(['#0000ff'], { space: 'LINEAR_RGB' });
105
+ const chromaLinear = chroma.mix('#ff0000', '#0000ff', 0.5, 'lrgb');
106
+ expect(omniLinear.toHex()).toBe('#bc00bc');
107
+ expectSimilarRGBAValues(omniLinear.toRGBA(), chromaLinear.rgba(), 10);
108
+ });
109
+ it('aligns LINEAR_RGB weight handling with chroma-js lrgb interpolation within tolerance', () => {
110
+ // Using the sRGB transfer function means omni-color prioritizes physical correctness; we keep
111
+ // loose tolerances here because chroma’s simplified gamma curve yields slightly darker values.
112
+ const omniLinear = new Color('#ff0000').mixWith(['#0000ff'], {
113
+ space: 'LINEAR_RGB',
114
+ weights: [3, 1],
115
+ });
116
+ const chromaLinear = chroma.mix('#ff0000', '#0000ff', 0.25, 'lrgb');
117
+ expect(omniLinear.toHex()).toBe('#e10089');
118
+ expectSimilarRGBAValues(omniLinear.toRGBA(), chromaLinear.rgba(), 10);
119
+ });
120
+ it('mixes multiple LINEAR_RGB inputs with normalized weights (brighter than chroma-js)', () => {
121
+ // The same spec-accurate companding vs. approximation difference applies to multi-input mixes.
122
+ // We assert brightness within a generous tolerance to document the expected divergence.
123
+ const omniLinear = new Color('#ff0000').mixWith(['#00ff00', '#0000ff'], {
124
+ space: 'LINEAR_RGB',
125
+ weights: [0.5, 0.25, 0.25],
126
+ });
127
+ const chromaLinear = chroma.average(['#ff0000', '#00ff00', '#0000ff'], 'lrgb', [0.5, 0.25, 0.25]);
128
+ expect(omniLinear.toHex()).toBe('#bc8989');
129
+ expectSimilarRGBAValues(omniLinear.toRGBA(), chromaLinear.rgba(), 70);
130
+ });
131
+ it('matches chroma-js sRGB mixing when the same weights are provided', () => {
132
+ const omniRgb = new Color('#ff0000').mixWith(['#0000ff'], {
133
+ space: 'RGB',
134
+ weights: [0.5, 0.5],
135
+ });
136
+ const chromaRgb = chroma.mix('#ff0000', '#0000ff', 0.5, 'rgb');
137
+ expect(omniRgb.toHex()).toBe(chromaRgb.hex().toLowerCase());
138
+ expect(omniRgb.toRGBA()).toEqual(chromaRGBArrayToObj(chromaRgb.rgba()));
139
+ });
140
+ it('aligns multi-input weight handling with chroma average calculations in RGB space', () => {
141
+ const omniMix = new Color('#ff0000').mixWith(['#00ff00', '#0000ff'], {
142
+ space: 'RGB',
143
+ weights: [0.5, 0.25, 0.25],
144
+ });
145
+ const chromaMix = chroma.average(['#ff0000', '#00ff00', '#0000ff'], 'rgb', [0.5, 0.25, 0.25]);
146
+ expect(omniMix.toHex()).toBe(chromaMix.hex().toLowerCase());
147
+ expect(omniMix.toRGBA()).toEqual(chromaRGBArrayToObj(chromaMix.rgba()));
148
+ });
149
+ it('documents the deliberate divergence between subtractive mixing approaches', () => {
150
+ const omniSubtractive = new Color('#00ffff').mixWith(['#ffff00'], { type: 'SUBTRACTIVE' });
151
+ const cyanCmyk = chroma('#00ffff').cmyk();
152
+ const yellowCmyk = chroma('#ffff00').cmyk();
153
+ const averagedCmyk = cyanCmyk.map((channel, index) => 0.5 * channel + 0.5 * yellowCmyk[index]);
154
+ const [c, m, y, k] = averagedCmyk;
155
+ const chromaSubtractiveApproximation = chroma.cmyk(c, m, y, k);
156
+ expect(omniSubtractive.toHex()).toBe('#00ff00');
157
+ expect(chromaSubtractiveApproximation.hex().toLowerCase()).toBe('#80ff80');
158
+ expect(omniSubtractive.toHex()).not.toBe(chromaSubtractiveApproximation.hex().toLowerCase());
159
+ });
160
+ });
161
+ describe('gradient parity with chroma-js', () => {
162
+ it('matches linear LCH gradients for two-stop anchors', () => {
163
+ const twoStopOmniGradient = Color.createInterpolatedGradient(['#ff0000', '#0000ff'], {
164
+ space: 'LCH',
165
+ stops: 5,
166
+ });
167
+ const twoStopChromaGradient = chroma.scale(['#ff0000', '#0000ff']).mode('lch').colors(5);
168
+ twoStopOmniGradient.forEach((color, index) => {
169
+ expectSimilarRGBAValues(color.toRGBA(), chroma(twoStopChromaGradient[index]).rgba(), 1);
170
+ });
171
+ });
172
+ it('matches linear LCH gradients for three-stop anchors', () => {
173
+ const threeStopOmniGradient = Color.createInterpolatedGradient(['#ff0000', '#00ff00', '#0000ff'], { space: 'LCH', stops: 7 });
174
+ const threeStopChromaGradient = chroma
175
+ .scale(['#ff0000', '#00ff00', '#0000ff'])
176
+ .mode('lch')
177
+ .colors(7);
178
+ expect(threeStopOmniGradient.map((color) => color.toHex())).toEqual(threeStopChromaGradient.map((hex) => hex.toLowerCase()));
179
+ });
180
+ it('matches LCH bezier gradients against chroma-js bezier scale', () => {
181
+ const bezierAnchors = ['#f43f5e', '#fbbf24', '#22d3ee'];
182
+ const bezierOmniGradient = Color.createInterpolatedGradient(bezierAnchors, {
183
+ interpolation: 'BEZIER',
184
+ space: 'LCH',
185
+ stops: 6,
186
+ });
187
+ const bezierChromaGradient = chroma.bezier(bezierAnchors).scale().mode('lch').colors(6);
188
+ expect(bezierOmniGradient.map((color) => color.toHex())).toEqual(bezierChromaGradient.map((hex) => hex.toLowerCase()));
189
+ });
190
+ it('wraps hues across 0° when using shortest-path HSL interpolation', () => {
191
+ const hueWrappedAnchors = ['hsl(350, 100%, 50%)', 'hsl(10, 100%, 50%)'];
192
+ const wrappedOmniGradient = Color.createInterpolatedGradient(hueWrappedAnchors, {
193
+ space: 'HSL',
194
+ stops: 5,
195
+ hueInterpolationMode: 'SHORTEST',
196
+ });
197
+ const wrappedChromaGradient = chroma.scale(hueWrappedAnchors).mode('hsl').colors(5);
198
+ wrappedOmniGradient.forEach((color, index) => {
199
+ expectSimilarRGBAValues(color.toRGBA(), chroma(wrappedChromaGradient[index]).rgba(), 1);
200
+ });
201
+ });
202
+ });
203
+ describe('delta E parity with chroma-js', () => {
204
+ it('matches chroma-js deltaE for near-match, midrange, and high-contrast pairs', () => {
205
+ const nearMatchOmniDelta = new Color('#ededee').differenceFrom('#edeeed');
206
+ const nearMatchChromaDelta = chroma.deltaE('#ededee', '#edeeed');
207
+ expect(nearMatchOmniDelta).toBeCloseTo(nearMatchChromaDelta, 2);
208
+ const midrangeOmniDelta = new Color('#0f4c81').differenceFrom('#f97316');
209
+ const midrangeChromaDelta = chroma.deltaE('#0f4c81', '#f97316');
210
+ expect(midrangeOmniDelta).toBeCloseTo(midrangeChromaDelta, 2);
211
+ const highContrastOmniDelta = new Color('#000000').differenceFrom('#ffffff');
212
+ const highContrastChromaDelta = chroma.deltaE('#000000', '#ffffff');
213
+ expect(highContrastOmniDelta).toBeCloseTo(highContrastChromaDelta, 5);
214
+ });
215
+ it('honors CIE94 weighting factors comparable to chroma-js Kl/Kc/Kh inputs', () => {
216
+ const weightedOmniDelta = new Color('#ff6666').differenceFrom('#aa0000', {
217
+ method: 'CIE94',
218
+ cie94Options: { kL: 1.5, kC: 1, kH: 1 },
219
+ });
220
+ const weightedChromaDelta = chroma.deltaE('#ff6666', '#aa0000', 1.5, 1, 1);
221
+ expect(weightedOmniDelta).toBeCloseTo(weightedChromaDelta, 0);
222
+ });
223
+ });
224
+ describe('temperature parity with chroma-js', () => {
225
+ it('produces similar Kelvin-to-hex conversions for warm and cool temperatures', () => {
226
+ const warmKelvin = 3000;
227
+ const warmFromTemperature = Color.fromTemperature(warmKelvin);
228
+ const warmChromaTemperature = chroma.temperature(warmKelvin);
229
+ expect(warmFromTemperature.differenceFrom(warmChromaTemperature.hex())).toBeLessThan(2);
230
+ const coolKelvin = 9000;
231
+ const coolFromTemperature = Color.fromTemperature(coolKelvin);
232
+ const coolChromaTemperature = chroma.temperature(coolKelvin);
233
+ expect(coolFromTemperature.differenceFrom(coolChromaTemperature.hex())).toBeLessThan(2);
234
+ });
235
+ it('estimates correlated color temperature similarly for representative colors', () => {
236
+ const warmNeutralHex = '#ffdabb';
237
+ const warmNeutralOmni = new Color(warmNeutralHex).getTemperature().temperature;
238
+ const warmNeutralChroma = Math.round(chroma(warmNeutralHex).temperature());
239
+ expect(Math.abs(warmNeutralOmni - warmNeutralChroma)).toBeLessThan(120);
240
+ const coolNeutralHex = '#f3f2ff';
241
+ const coolNeutralOmni = new Color(coolNeutralHex).getTemperature().temperature;
242
+ const coolNeutralChroma = Math.round(chroma(coolNeutralHex).temperature());
243
+ expect(Math.abs(coolNeutralOmni - coolNeutralChroma)).toBeLessThan(50);
244
+ });
245
+ });
246
+ describe('readability and contrast interoperability', () => {
247
+ it('matches chroma-js contrast ratios for opaque colors', () => {
248
+ const foreground = new Color('#1a1a1a');
249
+ const background = new Color('#fafafa');
250
+ const omniContrast = foreground.getWCAGContrastRatio(background);
251
+ const chromaContrast = chroma.contrast('#1a1a1a', '#fafafa');
252
+ expect(omniContrast).toBeCloseTo(chromaContrast, 2);
253
+ });
254
+ it('composites transparent inputs before calculating contrast', () => {
255
+ const foreground = new Color('rgba(0, 0, 0, 0.5)');
256
+ const background = new Color('rgba(255, 255, 255, 0.6)');
257
+ const omniContrast = foreground.getWCAGContrastRatio(background);
258
+ const chromaDirectContrast = chroma.contrast('rgba(0, 0, 0, 0.5)', 'rgba(255, 255, 255, 0.6)');
259
+ const compositeForeground = compositeRgba(foreground.toRGBA(), background.toRGBA());
260
+ const compositeBackground = compositeRgba(background.toRGBA(), foreground.toRGBA());
261
+ const chromaCompositedContrast = chroma.contrast(chroma.rgb(compositeForeground.r, compositeForeground.g, compositeForeground.b), chroma.rgb(compositeBackground.r, compositeBackground.g, compositeBackground.b));
262
+ expect(omniContrast).toBeCloseTo(chromaCompositedContrast, 2);
263
+ expect(omniContrast).not.toBeCloseTo(chromaDirectContrast, 2);
264
+ });
265
+ it('selects the most readable text and background colors by chroma contrast', () => {
266
+ const background = new Color('#102542');
267
+ const textCandidates = ['#f8fafc', '#0f172a', '#f97316', '#16a34a'];
268
+ let bestTextCandidate = textCandidates[0];
269
+ let bestTextContrast = chroma.contrast(textCandidates[0], background.toHex());
270
+ textCandidates.slice(1).forEach((candidate) => {
271
+ const contrast = chroma.contrast(candidate, background.toHex());
272
+ if (contrast > bestTextContrast) {
273
+ bestTextContrast = contrast;
274
+ bestTextCandidate = candidate;
275
+ }
276
+ });
277
+ const mostReadableTextColor = background.getMostReadableTextColor(textCandidates).toHex();
278
+ expect(mostReadableTextColor).toBe(new Color(bestTextCandidate).toHex());
279
+ const textColor = new Color('#0b1221');
280
+ const backgroundCandidates = ['#f8fafc', '#111827', '#334155', '#e11d48'];
281
+ let bestBackgroundCandidate = backgroundCandidates[0];
282
+ let bestBackgroundContrast = chroma.contrast(textColor.toHex(), backgroundCandidates[0]);
283
+ backgroundCandidates.slice(1).forEach((candidate) => {
284
+ const contrast = chroma.contrast(textColor.toHex(), candidate);
285
+ if (contrast > bestBackgroundContrast) {
286
+ bestBackgroundContrast = contrast;
287
+ bestBackgroundCandidate = candidate;
288
+ }
289
+ });
290
+ const mostReadableBackgroundColor = textColor
291
+ .getBestBackgroundColor(backgroundCandidates)
292
+ .toHex();
293
+ expect(mostReadableBackgroundColor).toBe(new Color(bestBackgroundCandidate).toHex());
294
+ });
295
+ it('applies WCAG thresholds consistently across AA/AAA and text sizes', () => {
296
+ const background = new Color('#ffffff');
297
+ const midGray = new Color('#7a7a7a');
298
+ const darkGray = new Color('#555555');
299
+ const midGrayContrast = chroma.contrast(midGray.toHex(), background.toHex());
300
+ const darkGrayContrast = chroma.contrast(darkGray.toHex(), background.toHex());
301
+ const aaSmallReport = midGray.getWCAGReadabilityReport(background, {
302
+ level: 'AA',
303
+ size: 'SMALL',
304
+ });
305
+ expect(aaSmallReport.contrastRatio).toBeCloseTo(midGrayContrast, 2);
306
+ expect(aaSmallReport.requiredContrast).toBe(4.5);
307
+ expect(aaSmallReport.isReadable).toBe(false);
308
+ const aaLargeReport = midGray.getWCAGReadabilityReport(background, {
309
+ level: 'AA',
310
+ size: 'LARGE',
311
+ });
312
+ expect(aaLargeReport.contrastRatio).toBeCloseTo(midGrayContrast, 2);
313
+ expect(aaLargeReport.requiredContrast).toBe(3);
314
+ expect(aaLargeReport.isReadable).toBe(true);
315
+ const aaaSmallReport = darkGray.getWCAGReadabilityReport(background, {
316
+ level: 'AAA',
317
+ size: 'SMALL',
318
+ });
319
+ expect(aaaSmallReport.contrastRatio).toBeCloseTo(darkGrayContrast, 2);
320
+ expect(aaaSmallReport.requiredContrast).toBe(7);
321
+ expect(aaaSmallReport.isReadable).toBe(true);
322
+ const aaaLargeReport = midGray.getWCAGReadabilityReport(background, {
323
+ level: 'AAA',
324
+ size: 'LARGE',
325
+ });
326
+ expect(aaaLargeReport.contrastRatio).toBeCloseTo(midGrayContrast, 2);
327
+ expect(aaaLargeReport.requiredContrast).toBe(4.5);
328
+ expect(aaaLargeReport.isReadable).toBe(false);
329
+ });
330
+ });
331
+ describe('average parity with chroma-js', () => {
332
+ it('keeps circular HSL averaging close to chroma-js while weighting saturation', () => {
333
+ const omniAverage = new Color('hsl(350, 100%, 50%)').averageWith(['hsl(10, 100%, 50%)', 'hsl(30, 60%, 50%)'], { space: 'HSL' });
334
+ const chromaAverage = chroma.average(['hsl(350, 100%, 50%)', 'hsl(10, 100%, 50%)', 'hsl(30, 60%, 50%)'], 'hsl');
335
+ expect(omniAverage.toHex()).toBe('#eb2d14');
336
+ const omniHsl = omniAverage.toHSL();
337
+ const chromaHsl = chromaAverage.hsl();
338
+ const hueDelta = Math.min(Math.abs(omniHsl.h - chromaHsl[0]), 360 - Math.abs(omniHsl.h - chromaHsl[0]));
339
+ expect(hueDelta).toBeLessThan(5);
340
+ expect(Math.abs(omniHsl.s - chromaHsl[1] * 100)).toBeLessThan(5);
341
+ expect(Math.abs(omniHsl.l - chromaHsl[2] * 100)).toBeLessThan(1);
342
+ });
343
+ });
344
+ describe('blend parity with chroma-js', () => {
345
+ it('aligns RGB blend modes with chroma-js outputs', () => {
346
+ const multiplyBlend = new Color('#336699').blendWith(new Color('#ffcc00'), {
347
+ mode: 'MULTIPLY',
348
+ ratio: 1,
349
+ });
350
+ const chromaMultiply = chroma.blend('#336699', '#ffcc00', 'multiply');
351
+ expect(multiplyBlend.toRGBA()).toEqual(chromaRGBArrayToObj(chromaMultiply.rgba()));
352
+ const screenBlend = new Color('#336699').blendWith(new Color('#ffcc00'), {
353
+ mode: 'SCREEN',
354
+ ratio: 1,
355
+ });
356
+ const chromaScreen = chroma.blend('#336699', '#ffcc00', 'screen');
357
+ expect(screenBlend.toRGBA()).toEqual(chromaRGBArrayToObj(chromaScreen.rgba()));
358
+ const overlayBlend = new Color('#336699').blendWith(new Color('#ffcc00'), {
359
+ mode: 'OVERLAY',
360
+ ratio: 1,
361
+ });
362
+ const chromaOverlay = chroma.blend('#336699', '#ffcc00', 'overlay');
363
+ expect(overlayBlend.toRGBA()).toEqual(chromaRGBArrayToObj(chromaOverlay.rgba()));
364
+ const normalBlend = new Color('#336699').blendWith(new Color('#ffcc00'), {
365
+ mode: 'NORMAL',
366
+ ratio: 1,
367
+ });
368
+ const chromaNormal = chroma.mix('#336699', '#ffcc00', 1, 'rgb');
369
+ expect(normalBlend.toRGBA()).toEqual(chromaRGBArrayToObj(chromaNormal.rgba()));
370
+ });
371
+ it('matches blend modes for a secondary palette at partial ratios', () => {
372
+ const multiplyBlend = new Color('#112233')
373
+ .blendWith(new Color('#ffaaff'), { mode: 'MULTIPLY', ratio: 0.4 })
374
+ .toRGBA();
375
+ const chromaMultiply = chroma
376
+ .mix('#112233', chroma.blend('#112233', '#ffaaff', 'multiply').hex(), 0.4, 'rgb')
377
+ .rgba();
378
+ expectSimilarRGBAValues(multiplyBlend, chromaMultiply);
379
+ const screenBlend = new Color('#112233')
380
+ .blendWith(new Color('#ffaaff'), { mode: 'SCREEN', ratio: 0.4 })
381
+ .toRGBA();
382
+ const chromaScreen = chroma
383
+ .mix('#112233', chroma.blend('#112233', '#ffaaff', 'screen').hex(), 0.4, 'rgb')
384
+ .rgba();
385
+ expectSimilarRGBAValues(screenBlend, chromaScreen);
386
+ const overlayBlend = new Color('#112233')
387
+ .blendWith(new Color('#ffaaff'), { mode: 'OVERLAY', ratio: 0.4 })
388
+ .toRGBA();
389
+ const chromaOverlay = chroma
390
+ .mix('#112233', chroma.blend('#112233', '#ffaaff', 'overlay').hex(), 0.4, 'rgb')
391
+ .rgba();
392
+ expectSimilarRGBAValues(overlayBlend, chromaOverlay);
393
+ });
394
+ it('aligns blend modes for a high-contrast neutral and light pair', () => {
395
+ const multiplyBlend = new Color('#0f0f0f').blendWith(new Color('#fefefe'), {
396
+ mode: 'MULTIPLY',
397
+ ratio: 0.5,
398
+ });
399
+ const chromaMultiply = chroma
400
+ .mix('#0f0f0f', chroma.blend('#0f0f0f', '#fefefe', 'multiply').hex(), 0.5, 'rgb')
401
+ .rgba();
402
+ expectSimilarRGBAValues(multiplyBlend.toRGBA(), chromaMultiply);
403
+ const screenBlend = new Color('#0f0f0f').blendWith(new Color('#fefefe'), {
404
+ mode: 'SCREEN',
405
+ ratio: 0.5,
406
+ });
407
+ const chromaScreen = chroma
408
+ .mix('#0f0f0f', chroma.blend('#0f0f0f', '#fefefe', 'screen').hex(), 0.5, 'rgb')
409
+ .rgba();
410
+ expectSimilarRGBAValues(screenBlend.toRGBA(), chromaScreen);
411
+ const overlayBlend = new Color('#0f0f0f').blendWith(new Color('#fefefe'), {
412
+ mode: 'OVERLAY',
413
+ ratio: 0.5,
414
+ });
415
+ const chromaOverlay = chroma
416
+ .mix('#0f0f0f', chroma.blend('#0f0f0f', '#fefefe', 'overlay').hex(), 0.5, 'rgb')
417
+ .rgba();
418
+ expectSimilarRGBAValues(overlayBlend.toRGBA(), chromaOverlay);
419
+ });
420
+ it('preserves blend ratio weighting compared to chroma-js', () => {
421
+ const multiplyBlend = new Color('#336699').blendWith(new Color('#ffcc00'), {
422
+ mode: 'MULTIPLY',
423
+ ratio: 0.25,
424
+ });
425
+ const chromaMultiply = chroma.blend('#336699', '#ffcc00', 'multiply');
426
+ const chromaWeighted = chroma.mix('#336699', chromaMultiply.hex(), 0.25, 'rgb');
427
+ expect(multiplyBlend.toRGBA()).toEqual(chromaRGBArrayToObj(chromaWeighted.rgba()));
428
+ });
429
+ it('preserves HSL hue interpolation relative to chroma-js mix', () => {
430
+ const hslBlend = new Color('#336699').blendWith(new Color('#ffcc00'), {
431
+ space: 'HSL',
432
+ ratio: 0.35,
433
+ });
434
+ const chromaHslBlend = chroma.mix('#336699', '#ffcc00', 0.35, 'hsl');
435
+ const hslFromOmni = hslBlend.toHSL();
436
+ const chromaHsl = chromaHslBlend.hsl();
437
+ const roundedChromaHue = Math.round(chromaHsl[0]);
438
+ const roundedChromaSaturation = Math.round(chromaHsl[1] * 100);
439
+ const roundedChromaLightness = Math.round(chromaHsl[2] * 100);
440
+ expect(hslFromOmni.h).toBeCloseTo(roundedChromaHue, 0);
441
+ expect(Math.abs(hslFromOmni.s - roundedChromaSaturation)).toBeLessThanOrEqual(0.5);
442
+ expect(Math.abs(hslFromOmni.l - roundedChromaLightness)).toBeLessThanOrEqual(0.5);
443
+ });
444
+ it('handles hue wrapping consistently when interpolating in HSL space', () => {
445
+ const base = new Color('#ff0000');
446
+ const blend = new Color('#00ff00');
447
+ const hslBlend = base.blendWith(blend, { space: 'HSL', ratio: 0.75 });
448
+ const chromaHslBlend = chroma.mix('#ff0000', '#00ff00', 0.75, 'hsl');
449
+ const omniHsl = hslBlend.toHSL();
450
+ const chromaHsl = chromaHslBlend.hsl();
451
+ expect(omniHsl.h).toBeCloseTo(chromaHsl[0], 0);
452
+ expect(omniHsl.s).toBeCloseTo(chromaHsl[1] * 100, 0);
453
+ expect(omniHsl.l).toBeCloseTo(chromaHsl[2] * 100, 0);
454
+ });
455
+ it('keeps hue alignment when blending across the 360° boundary', () => {
456
+ const base = new Color('#ff00ff');
457
+ const blend = new Color('#00ffff');
458
+ const omniHsl = base.blendWith(blend, { space: 'HSL', ratio: 0.6 }).toHSL();
459
+ const chromaHsl = chroma.mix('#ff00ff', '#00ffff', 0.6, 'hsl').hsl();
460
+ expect(omniHsl.h).toBeCloseTo(chromaHsl[0], 0);
461
+ expect(omniHsl.s).toBeCloseTo(chromaHsl[1] * 100, 0);
462
+ expect(omniHsl.l).toBeCloseTo(chromaHsl[2] * 100, 0);
463
+ });
464
+ it('keeps alpha ratios consistent with chroma-js expectations', () => {
465
+ const omniAlphaBlend = new Color('rgba(51, 102, 153, 0.6)')
466
+ .blendWith(new Color('rgba(255, 204, 0, 0.3)'), {
467
+ mode: 'NORMAL',
468
+ ratio: 0.65,
469
+ })
470
+ .toRGBA();
471
+ const chromaAlphaBlend = chroma
472
+ .mix('rgba(51, 102, 153, 0.6)', 'rgba(255, 204, 0, 0.3)', 0.65, 'rgb')
473
+ .rgba();
474
+ expect(omniAlphaBlend).toEqual(chromaRGBArrayToObj(chromaAlphaBlend));
475
+ expect(omniAlphaBlend.a).toBeCloseTo((1 - 0.65) * 0.6 + 0.65 * 0.3, 3);
476
+ });
477
+ it('blends when the top color is fully transparent', () => {
478
+ const omniBlend = new Color('rgba(51, 102, 153, 0.8)')
479
+ .blendWith(new Color('rgba(255, 204, 0, 0)'), { mode: 'NORMAL', ratio: 0.4 })
480
+ .toRGBA();
481
+ const chromaBlend = chroma
482
+ .mix('rgba(51, 102, 153, 0.8)', 'rgba(255, 204, 0, 0)', 0.4, 'rgb')
483
+ .rgba();
484
+ expect(omniBlend).toEqual(chromaRGBArrayToObj(chromaBlend));
485
+ expect(omniBlend.a).toBeCloseTo(0.8 * (1 - 0.4), 3);
486
+ });
487
+ });
488
+ describe('LAB/LCH conversions align with chroma-js', () => {
489
+ const labTolerance = 0.5;
490
+ const chromaTolerance = 0.5;
491
+ const hueTolerance = 1;
492
+ it('keeps LAB/LCH outputs aligned for #1a73e8', () => {
493
+ const omniColor = new Color('#1a73e8');
494
+ const chromaColor = chroma('#1a73e8');
495
+ const labFromOmni = omniColor.toLAB();
496
+ const chromaLab = chromaColor.lab();
497
+ expect(Math.abs(labFromOmni.l - chromaLab[0])).toBeLessThanOrEqual(labTolerance);
498
+ expect(Math.abs(labFromOmni.a - chromaLab[1])).toBeLessThanOrEqual(labTolerance);
499
+ expect(Math.abs(labFromOmni.b - chromaLab[2])).toBeLessThanOrEqual(labTolerance);
500
+ const lchFromOmni = omniColor.toLCH();
501
+ const chromaLch = chromaColor.lch();
502
+ expect(Math.abs(lchFromOmni.l - chromaLch[0])).toBeLessThanOrEqual(labTolerance);
503
+ expect(Math.abs(lchFromOmni.c - chromaLch[1])).toBeLessThanOrEqual(chromaTolerance);
504
+ const lchHueDelta = Math.min(Math.abs(lchFromOmni.h - chromaLch[2]), 360 - Math.abs(lchFromOmni.h - chromaLch[2]));
505
+ expect(lchHueDelta).toBeLessThanOrEqual(hueTolerance);
506
+ const oklchFromOmni = omniColor.toOKLCH();
507
+ const chromaOklch = chromaColor.oklch();
508
+ expect(Math.abs(oklchFromOmni.l - chromaOklch[0])).toBeLessThanOrEqual(0.001);
509
+ expect(Math.abs(oklchFromOmni.c - chromaOklch[1])).toBeLessThanOrEqual(0.001);
510
+ const oklchHueDelta = Math.min(Math.abs(oklchFromOmni.h - chromaOklch[2]), 360 - Math.abs(oklchFromOmni.h - chromaOklch[2]));
511
+ expect(oklchHueDelta).toBeLessThanOrEqual(hueTolerance);
512
+ });
513
+ it('keeps LAB/LCH outputs aligned for #f43f5e', () => {
514
+ const omniColor = new Color('#f43f5e');
515
+ const chromaColor = chroma('#f43f5e');
516
+ const labFromOmni = omniColor.toLAB();
517
+ const chromaLab = chromaColor.lab();
518
+ expect(Math.abs(labFromOmni.l - chromaLab[0])).toBeLessThanOrEqual(labTolerance);
519
+ expect(Math.abs(labFromOmni.a - chromaLab[1])).toBeLessThanOrEqual(labTolerance);
520
+ expect(Math.abs(labFromOmni.b - chromaLab[2])).toBeLessThanOrEqual(labTolerance);
521
+ const lchFromOmni = omniColor.toLCH();
522
+ const chromaLch = chromaColor.lch();
523
+ expect(Math.abs(lchFromOmni.l - chromaLch[0])).toBeLessThanOrEqual(labTolerance);
524
+ expect(Math.abs(lchFromOmni.c - chromaLch[1])).toBeLessThanOrEqual(chromaTolerance);
525
+ const lchHueDelta = Math.min(Math.abs(lchFromOmni.h - chromaLch[2]), 360 - Math.abs(lchFromOmni.h - chromaLch[2]));
526
+ expect(lchHueDelta).toBeLessThanOrEqual(hueTolerance);
527
+ const oklchFromOmni = omniColor.toOKLCH();
528
+ const chromaOklch = chromaColor.oklch();
529
+ expect(Math.abs(oklchFromOmni.l - chromaOklch[0])).toBeLessThanOrEqual(0.001);
530
+ expect(Math.abs(oklchFromOmni.c - chromaOklch[1])).toBeLessThanOrEqual(0.001);
531
+ const oklchHueDelta = Math.min(Math.abs(oklchFromOmni.h - chromaOklch[2]), 360 - Math.abs(oklchFromOmni.h - chromaOklch[2]));
532
+ expect(oklchHueDelta).toBeLessThanOrEqual(hueTolerance);
533
+ });
534
+ it('keeps LAB/LCH outputs aligned for #00aa88', () => {
535
+ const omniColor = new Color('#00aa88');
536
+ const chromaColor = chroma('#00aa88');
537
+ const labFromOmni = omniColor.toLAB();
538
+ const chromaLab = chromaColor.lab();
539
+ expect(Math.abs(labFromOmni.l - chromaLab[0])).toBeLessThanOrEqual(labTolerance);
540
+ expect(Math.abs(labFromOmni.a - chromaLab[1])).toBeLessThanOrEqual(labTolerance);
541
+ expect(Math.abs(labFromOmni.b - chromaLab[2])).toBeLessThanOrEqual(labTolerance);
542
+ const lchFromOmni = omniColor.toLCH();
543
+ const chromaLch = chromaColor.lch();
544
+ expect(Math.abs(lchFromOmni.l - chromaLch[0])).toBeLessThanOrEqual(labTolerance);
545
+ expect(Math.abs(lchFromOmni.c - chromaLch[1])).toBeLessThanOrEqual(chromaTolerance);
546
+ const lchHueDelta = Math.min(Math.abs(lchFromOmni.h - chromaLch[2]), 360 - Math.abs(lchFromOmni.h - chromaLch[2]));
547
+ expect(lchHueDelta).toBeLessThanOrEqual(hueTolerance);
548
+ const oklchFromOmni = omniColor.toOKLCH();
549
+ const chromaOklch = chromaColor.oklch();
550
+ expect(Math.abs(oklchFromOmni.l - chromaOklch[0])).toBeLessThanOrEqual(0.001);
551
+ expect(Math.abs(oklchFromOmni.c - chromaOklch[1])).toBeLessThanOrEqual(0.001);
552
+ const oklchHueDelta = Math.min(Math.abs(oklchFromOmni.h - chromaOklch[2]), 360 - Math.abs(oklchFromOmni.h - chromaOklch[2]));
553
+ expect(oklchHueDelta).toBeLessThanOrEqual(hueTolerance);
554
+ });
555
+ });
556
+ describe('agrees on HSL and HSV math', () => {
557
+ it('matches hue, saturation, and lightness/brightness values', () => {
558
+ const vividOrange = new Color('#ff7f0e');
559
+ const vividOrangeChroma = chroma('#ff7f0e');
560
+ const vividOrangeHsl = vividOrangeChroma.hsl();
561
+ const vividOrangeHslFromOmniColor = vividOrange.toHSL();
562
+ expect(vividOrangeHslFromOmniColor.h).toBeCloseTo(vividOrangeHsl[0], 0);
563
+ expect(vividOrangeHslFromOmniColor.s).toBeCloseTo(vividOrangeHsl[1] * 100, 0);
564
+ expect(vividOrangeHslFromOmniColor.l).toBeCloseTo(vividOrangeHsl[2] * 100, 0);
565
+ const vividOrangeHsv = vividOrangeChroma.hsv();
566
+ const vividOrangeHsvFromOmniColor = vividOrange.toHSV();
567
+ expect(vividOrangeHsvFromOmniColor.h).toBeCloseTo(vividOrangeHsv[0], 0);
568
+ expect(vividOrangeHsvFromOmniColor.s).toBeCloseTo(vividOrangeHsv[1] * 100, 0);
569
+ expect(vividOrangeHsvFromOmniColor.v).toBeCloseTo(vividOrangeHsv[2] * 100, 0);
570
+ const mutedTeal = new Color('rgb(32, 160, 150)');
571
+ const mutedTealChroma = chroma('rgb(32, 160, 150)');
572
+ const mutedTealHsl = mutedTealChroma.hsl();
573
+ const mutedTealHslFromOmniColor = mutedTeal.toHSL();
574
+ expect(mutedTealHslFromOmniColor.h).toBeCloseTo(mutedTealHsl[0], 0);
575
+ expect(mutedTealHslFromOmniColor.s).toBeCloseTo(mutedTealHsl[1] * 100, 0);
576
+ expect(mutedTealHslFromOmniColor.l).toBeCloseTo(mutedTealHsl[2] * 100, 0);
577
+ const mutedTealHsv = mutedTealChroma.hsv();
578
+ const mutedTealHsvFromOmniColor = mutedTeal.toHSV();
579
+ expect(mutedTealHsvFromOmniColor.h).toBeCloseTo(mutedTealHsv[0], 0);
580
+ expect(mutedTealHsvFromOmniColor.s).toBeCloseTo(mutedTealHsv[1] * 100, 0);
581
+ expect(mutedTealHsvFromOmniColor.v).toBeCloseTo(mutedTealHsv[2] * 100, 0);
582
+ });
583
+ });
584
+ describe('manipulation helpers align with chroma-js LAB-backed operations', () => {
585
+ describe('brightens colors by the same HSL lightness delta', () => {
586
+ it('brightens deep navy by 25%', () => {
587
+ const omniRgba = new Color('#001f3f').brighten({ space: 'LAB', amount: 25 }).toRGBA();
588
+ const chromaRgba = chroma('#001f3f').brighten(2.5).rgba();
589
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
590
+ });
591
+ it('brightens pure black significantly', () => {
592
+ const omniRgba = new Color('#000000').brighten({ space: 'LAB', amount: 50 }).toRGBA();
593
+ const chromaRgba = chroma('#000000').brighten(5).rgba();
594
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
595
+ });
596
+ it('brightens a nearly white tone without overshooting', () => {
597
+ const omniRgba = new Color('#fafafa').brighten({ space: 'LAB', amount: 30 }).toRGBA();
598
+ const chromaRgba = chroma('#fafafa').brighten(3).rgba();
599
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
600
+ });
601
+ it('brightens a translucent teal with the default amount', () => {
602
+ const omniRgba = new Color('rgba(0, 128, 128, 0.35)').brighten({ space: 'LAB' }).toRGBA();
603
+ const chromaRgba = chroma('rgba(0, 128, 128, 0.35)').brighten().rgba();
604
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
605
+ });
606
+ });
607
+ describe('darkens colors by decreasing HSL lightness identically', () => {
608
+ it('darkens a coral shade slightly', () => {
609
+ const omniRgba = new Color('#ff7f50').darken({ space: 'LAB', amount: 15 }).toRGBA();
610
+ const chromaRgba = chroma('#ff7f50').darken(1.5).rgba();
611
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
612
+ });
613
+ it('darkens a near-black charcoal heavily', () => {
614
+ const omniRgba = new Color('#222222').darken({ space: 'LAB', amount: 60 }).toRGBA();
615
+ const chromaRgba = chroma('#222222').darken(6).rgba();
616
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
617
+ });
618
+ it('darkens pure white dramatically', () => {
619
+ const omniRgba = new Color('#ffffff').darken({ space: 'LAB', amount: 80 }).toRGBA();
620
+ const chromaRgba = chroma('#ffffff').darken(8).rgba();
621
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
622
+ });
623
+ it('darkens a warm translucent yellow with the default delta', () => {
624
+ const omniRgba = new Color('rgba(255, 200, 0, 0.75)').darken({ space: 'LAB' }).toRGBA();
625
+ const chromaRgba = chroma('rgba(255, 200, 0, 0.75)').darken().rgba();
626
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
627
+ });
628
+ });
629
+ describe('saturates colors by increasing HSL saturation with clamping', () => {
630
+ it('saturates a muted teal by 40%', () => {
631
+ const omniRgba = new Color('hsl(190, 25%, 55%)')
632
+ .saturate({ space: 'LCH', amount: 40 })
633
+ .toRGBA();
634
+ const chromaRgba = chroma('hsl(190, 25%, 55%)').saturate(4).rgba();
635
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
636
+ });
637
+ it('saturates a flat gray noticeably', () => {
638
+ const omniRgba = new Color('#808080').saturate({ space: 'LCH', amount: 30 }).toRGBA();
639
+ const chromaRgba = chroma('#808080').saturate(3).rgba();
640
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
641
+ });
642
+ it('attempts to saturate pure black', () => {
643
+ const omniRgba = new Color('#000000').saturate({ space: 'LCH', amount: 90 }).toRGBA();
644
+ const chromaRgba = chroma('#000000').saturate(9).rgba();
645
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
646
+ });
647
+ it('saturates a translucent violet using the default amount', () => {
648
+ const omniRgba = new Color('rgba(120, 80, 200, 0.5)').saturate({ space: 'LCH' }).toRGBA();
649
+ const chromaRgba = chroma('rgba(120, 80, 200, 0.5)').saturate().rgba();
650
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
651
+ });
652
+ });
653
+ describe('desaturates colors by decreasing HSL saturation identically', () => {
654
+ it('desaturates a vivid pink by half', () => {
655
+ const omniRgba = new Color('#ff69b4').desaturate({ space: 'LCH', amount: 50 }).toRGBA();
656
+ const chromaRgba = chroma('#ff69b4').desaturate(5).rgba();
657
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
658
+ });
659
+ it('desaturates pure green with the default amount', () => {
660
+ const omniRgba = new Color('#00ff00').desaturate({ space: 'LCH' }).toRGBA();
661
+ const chromaRgba = chroma('#00ff00').desaturate().rgba();
662
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
663
+ });
664
+ it('desaturates pure blue completely', () => {
665
+ const omniRgba = new Color('#0000ff').desaturate({ space: 'LCH', amount: 100 }).toRGBA();
666
+ const chromaRgba = chroma('#0000ff').desaturate(10).rgba();
667
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
668
+ });
669
+ it('desaturates a neutral gray slightly', () => {
670
+ const omniRgba = new Color('#7a7a7a').desaturate({ space: 'LCH', amount: 25 }).toRGBA();
671
+ const chromaRgba = chroma('#7a7a7a').desaturate(2.5).rgba();
672
+ expect(omniRgba).toEqual(chromaRGBArrayToObj(chromaRgba));
673
+ });
674
+ });
675
+ });
676
+ });