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,665 @@
1
+ import { Color, createColorInstance } from '../color';
2
+ import { averageColors, blendColors, mixColors } from '../combinations';
3
+ describe('mixColors', () => {
4
+ it('mixes colors additively in RGB space', () => {
5
+ const red = new Color('#ff0000');
6
+ const green = new Color('#00ff00');
7
+ const result = mixColors([red, green], {
8
+ type: 'ADDITIVE',
9
+ space: 'RGB',
10
+ }, createColorInstance);
11
+ expect(result.toHex()).toBe('#ffff00');
12
+ });
13
+ it('mixes red, green, and blue additively to white', () => {
14
+ const red = new Color('#ff0000');
15
+ const green = new Color('#00ff00');
16
+ const blue = new Color('#0000ff');
17
+ const result = mixColors([red, green, blue], {
18
+ type: 'ADDITIVE',
19
+ space: 'RGB',
20
+ }, createColorInstance);
21
+ expect(result.toHex()).toBe('#ffffff');
22
+ });
23
+ it('mixes colors subtractively in CMYK space', () => {
24
+ const cyan = new Color('#00ffff');
25
+ const yellow = new Color('#ffff00');
26
+ const result = mixColors([cyan, yellow], { type: 'SUBTRACTIVE' }, createColorInstance);
27
+ expect(result.toHex()).toBe('#00ff00');
28
+ });
29
+ it('mixes cyan, magenta, and yellow subtractively to black', () => {
30
+ const cyan = new Color('#00ffff');
31
+ const magenta = new Color('#ff00ff');
32
+ const yellow = new Color('#ffff00');
33
+ const result = mixColors([cyan, magenta, yellow], {
34
+ type: 'SUBTRACTIVE',
35
+ }, createColorInstance);
36
+ expect(result.toHex()).toBe('#000000');
37
+ });
38
+ it('mixes colors in HSL space with weighted interpolation', () => {
39
+ const red = new Color('#ff0000');
40
+ const blue = new Color('#0000ff');
41
+ const result = mixColors([red, blue], {
42
+ space: 'HSL',
43
+ weights: [1, 3],
44
+ }, createColorInstance);
45
+ expect(result.toHex()).toBe('#5100ff');
46
+ const { h, s, l } = result.toHSL();
47
+ expect(h).toBeCloseTo(259, 0);
48
+ expect(s).toBe(100);
49
+ expect(l).toBe(50);
50
+ });
51
+ it('accepts mixed case mix space', () => {
52
+ const c1 = new Color('red');
53
+ const c2 = new Color('blue');
54
+ const mix1 = mixColors([c1, c2], { space: 'RGB' }, createColorInstance);
55
+ const mix2 = mixColors([c1, c2], { space: 'rgb' }, createColorInstance);
56
+ // @ts-expect-error - testing mixed casing not covered by type but valid runtime
57
+ const mix3 = mixColors([c1, c2], { space: 'rGb' }, createColorInstance);
58
+ expect(mix1.toHex()).toBe(mix2.toHex());
59
+ expect(mix1.toHex()).toBe(mix3.toHex());
60
+ });
61
+ it('accepts mixed case mix type', () => {
62
+ const c1 = new Color('red');
63
+ const c2 = new Color('blue');
64
+ const sub1 = mixColors([c1, c2], { type: 'SUBTRACTIVE' }, createColorInstance);
65
+ const sub2 = mixColors([c1, c2], { type: 'subtractive' }, createColorInstance);
66
+ expect(sub1.toHex()).toBe(sub2.toHex());
67
+ });
68
+ it('defaults to additive Linear RGB mixing', () => {
69
+ const red = new Color('#ff0000');
70
+ const green = new Color('#00ff00');
71
+ const result = mixColors([red, green], {}, createColorInstance);
72
+ expect(result.toHex()).toBe('#bcbc00');
73
+ });
74
+ it('mixes colors additively in Linear RGB space (brighter mix)', () => {
75
+ const red = new Color('#cc0000');
76
+ const green = new Color('#00cc00');
77
+ // In sRGB: cc(204) + 0 = 204. Result #cccc00.
78
+ // In Linear:
79
+ // 204/255 = 0.8. Linear(0.8) ~= 0.6.
80
+ // 0.6 + 0 = 0.6.
81
+ // 0 + 0.6 = 0.6.
82
+ // Linear(0.6) -> sRGB(0.8) -> 204.
83
+ // Wait, pure additive of non-clipping colors should be similar?
84
+ // Let's test averaging behavior via weights summing to 1.
85
+ const result = mixColors([red, green], { space: 'LINEAR_RGB', weights: [0.5, 0.5] }, createColorInstance);
86
+ // sRGB Avg: 102, 102, 0 -> #666600
87
+ // Linear Avg: 0.6*0.5 + 0 = 0.3. sRGB(0.3) -> ~150 (#96)
88
+ expect(result.toHex()).not.toBe('#666600');
89
+ // Approx #969600
90
+ const { r, g } = result.toRGBA();
91
+ expect(r).toBeGreaterThan(140);
92
+ expect(g).toBeGreaterThan(140);
93
+ });
94
+ it('throws when fewer than two colors are provided', () => {
95
+ const red = new Color('#ff0000');
96
+ expect(() => mixColors([red], undefined, createColorInstance)).toThrow('at least two colors are required for mixing');
97
+ });
98
+ it('defaults weights to 1 when provided weights sum to 0', () => {
99
+ const red = new Color('#ff0000');
100
+ const blue = new Color('#0000ff');
101
+ const result = mixColors([red, blue], { weights: [1, -1] }, createColorInstance);
102
+ expect(result.toHex()).toBe('#bc00bc');
103
+ });
104
+ it('mixes colors with alpha channel in RGB space', () => {
105
+ const semiRed = new Color({ r: 255, g: 0, b: 0, a: 0.5 });
106
+ const blue = new Color({ r: 0, g: 0, b: 255, a: 1 });
107
+ const result = mixColors([semiRed, blue], { type: 'ADDITIVE', space: 'RGB' }, createColorInstance);
108
+ expect(result.toHex()).toBe('#ff00ff');
109
+ expect(result.toRGBA().a).toBe(0.75);
110
+ });
111
+ it('mixes colors in LCH space', () => {
112
+ const black = new Color('#000000');
113
+ const white = new Color('#ffffff');
114
+ const result = mixColors([black, white], { space: 'LCH' }, createColorInstance);
115
+ expect(result.toHex()).toBe('#777777');
116
+ });
117
+ it('mixes colors in OKLCH space', () => {
118
+ const black = new Color('#000000');
119
+ const white = new Color('#ffffff');
120
+ const result = mixColors([black, white], { space: 'OKLCH' }, createColorInstance);
121
+ expect(result.toHex()).toBe('#636363');
122
+ });
123
+ it('mixes colors across the red hue boundary using linear addition', () => {
124
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
125
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
126
+ const result = mixColors([h1, h2], { space: 'HSL' }, createColorInstance);
127
+ expect(result.toHex()).toBe('#ff0000');
128
+ });
129
+ it('respects weights in additive mixing for polar spaces', () => {
130
+ // Dark Red (sRGB 128, 0, 0) -> Linear ~0.21
131
+ // Dark Blue (sRGB 0, 0, 128) -> Linear ~0.21
132
+ const darkRed = new Color('#800000');
133
+ const darkBlue = new Color('#000080');
134
+ // Weights [2, 1]
135
+ // Red: 0.21 * 2 = 0.42
136
+ // Blue: 0.21 * 1 = 0.21
137
+ // Result Linear: (0.42, 0, 0.21)
138
+ // Result sRGB:
139
+ // R: 0.42 -> ~0.68 -> ~174
140
+ // B: 0.21 -> ~0.50 -> ~128
141
+ const result = mixColors([darkRed, darkBlue], {
142
+ space: 'HSL',
143
+ weights: [2, 1],
144
+ }, createColorInstance);
145
+ const { r, b } = result.toRGBA();
146
+ expect(result.toHex()).toBe('#800040');
147
+ expect(r).toBeCloseTo(128, 0);
148
+ expect(b).toBeCloseTo(64, 0);
149
+ expect(result.toRGBA().g).toBe(0);
150
+ });
151
+ it('handles hue wrap-around in LCH space', () => {
152
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
153
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
154
+ const result = mixColors([h1, h2], { space: 'LCH' }, createColorInstance);
155
+ const hue = result.toLCH().h;
156
+ const distanceFromZero = Math.min(hue, 360 - hue);
157
+ expect(distanceFromZero).toBeLessThan(45);
158
+ expect(Math.abs(hue - 180)).toBeGreaterThan(90);
159
+ });
160
+ it('handles hue wrap-around in OKLCH space', () => {
161
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
162
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
163
+ const result = mixColors([h1, h2], { space: 'OKLCH' }, createColorInstance);
164
+ const hue = result.toOKLCH().h;
165
+ const distanceFromZero = Math.min(hue, 360 - hue);
166
+ expect(distanceFromZero).toBeLessThan(45);
167
+ expect(Math.abs(hue - 180)).toBeGreaterThan(90);
168
+ });
169
+ it('defaults to equal weights when weights length mismatches', () => {
170
+ const red = new Color('#ff0000');
171
+ const blue = new Color('#0000ff');
172
+ const result = mixColors([red, blue], { weights: [2] }, createColorInstance);
173
+ expect(result.toHex()).toBe('#bc00bc');
174
+ });
175
+ it('ignores colors with zero weight', () => {
176
+ const red = new Color('#ff0000');
177
+ const blue = new Color('#0000ff');
178
+ const result = mixColors([red, blue], { weights: [0, 1] }, createColorInstance);
179
+ expect(result.toHex()).toBe('#0000ff');
180
+ });
181
+ it('accumulates light when mixing identical colors in HSL space', () => {
182
+ const darkRed = new Color('#800000');
183
+ const result = mixColors([darkRed, darkRed], { space: 'HSL' }, createColorInstance);
184
+ expect(result.toHex()).toBe('#800000');
185
+ });
186
+ it('produces distinct results when mixing in HSL, LCH, and OKLCH spaces', () => {
187
+ const red = new Color('#ff0000');
188
+ const cyan = new Color('#00ffff');
189
+ const hslMix = mixColors([red, cyan], { space: 'HSL' }, createColorInstance);
190
+ const lchMix = mixColors([red, cyan], { space: 'LCH' }, createColorInstance);
191
+ const oklchMix = mixColors([red, cyan], { space: 'OKLCH' }, createColorInstance);
192
+ expect(hslMix.toHex()).toBe('#80ff00');
193
+ expect(lchMix.toHex()).toBe('#dda581');
194
+ expect(oklchMix.toHex()).toBe('#d2a993');
195
+ });
196
+ it('mixes colors subtractively in RGB space using normalized absorption', () => {
197
+ const cyan = new Color('#00ffff');
198
+ const yellow = new Color('#ffff00');
199
+ const result = mixColors([cyan, yellow], { type: 'SUBTRACTIVE', space: 'RGB' }, createColorInstance);
200
+ expect(result.toHex()).toBe('#00ff00');
201
+ expect(result.toRGBA().a).toBe(1);
202
+ });
203
+ it('mixes colors subtractively in Linear RGB space and respects weight ratios', () => {
204
+ const green = new Color('#00ff00');
205
+ const blue = new Color('#0000ff');
206
+ const mixA = mixColors([green, blue], {
207
+ type: 'SUBTRACTIVE',
208
+ space: 'LINEAR_RGB',
209
+ weights: [2, 1],
210
+ }, createColorInstance);
211
+ const mixB = mixColors([green, blue], {
212
+ type: 'SUBTRACTIVE',
213
+ space: 'LINEAR_RGB',
214
+ weights: [200, 100],
215
+ }, createColorInstance);
216
+ expect(mixA.toHex()).toBe(mixB.toHex());
217
+ });
218
+ it('mixes colors subtractively in HSL space while handling hue wrap-around', () => {
219
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
220
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
221
+ const result = mixColors([h1, h2], { type: 'SUBTRACTIVE', space: 'HSL' }, createColorInstance);
222
+ expect(result.toHex()).toBe('#ff0000');
223
+ expect(result.toHSL().h).toBe(0);
224
+ });
225
+ it('mixes colors subtractively in LCH space with chroma-driven hue', () => {
226
+ const red = new Color('#ff0000');
227
+ const blue = new Color('#0000ff');
228
+ const result = mixColors([red, blue], { type: 'SUBTRACTIVE', space: 'LCH' }, createColorInstance);
229
+ const { h, c } = result.toLCH();
230
+ expect(c).toBeGreaterThan(0);
231
+ const distanceFromGreen = Math.min(Math.abs(h - 120), Math.abs(h - 240));
232
+ expect(h > 270 || h < 90).toBe(true);
233
+ expect(distanceFromGreen).toBeGreaterThan(60);
234
+ const { r, g, b } = result.toRGBA();
235
+ expect(r).toBeGreaterThan(200);
236
+ expect(g).toBe(0);
237
+ expect(b).toBeGreaterThan(120);
238
+ });
239
+ it('mixes colors subtractively in OKLCH space with clamped lightness', () => {
240
+ const black = new Color('#000000');
241
+ const white = new Color('#ffffff');
242
+ const result = mixColors([black, white], { type: 'SUBTRACTIVE', space: 'OKLCH' }, createColorInstance);
243
+ expect(result.toHex()).toBe('#000000');
244
+ expect(result.toRGBA().a).toBe(1);
245
+ });
246
+ it('handles alpha when mixing subtractively', () => {
247
+ const yellow = new Color({ r: 255, g: 255, b: 0, a: 0.5 });
248
+ const magenta = new Color({ r: 255, g: 0, b: 255, a: 1 });
249
+ const result = mixColors([yellow, magenta], { type: 'SUBTRACTIVE', space: 'RGB' }, createColorInstance);
250
+ expect(result.toHex()).toBe('#ff0000');
251
+ expect(result.toRGBA().a).toBe(0.75);
252
+ });
253
+ it('defaults to Linear RGB subtractive mixing when type is subtractive but space omitted', () => {
254
+ const cyan = new Color('#00ffff');
255
+ const yellow = new Color('#ffff00');
256
+ const result = mixColors([cyan, yellow], { type: 'SUBTRACTIVE' }, createColorInstance);
257
+ expect(result.toHex()).toBe('#00ff00');
258
+ });
259
+ });
260
+ describe('averageColors', () => {
261
+ it('averages colors in RGB space', () => {
262
+ const red = new Color('#ff0000');
263
+ const blue = new Color('#0000ff');
264
+ const result = averageColors([red, blue], { space: 'RGB' }, createColorInstance);
265
+ expect(result.toHex()).toBe('#800080');
266
+ });
267
+ it('averages colors in Linear RGB space (brighter purple)', () => {
268
+ const red = new Color('#ff0000');
269
+ const blue = new Color('#0000ff');
270
+ const result = averageColors([red, blue], { space: 'LINEAR_RGB' }, createColorInstance);
271
+ // RGB avg: 128, 0, 128 -> #800080
272
+ // Linear avg: #bc00bc
273
+ expect(result.toHex()).toBe('#bc00bc');
274
+ });
275
+ it('averages red, green, and blue to gray in RGB space', () => {
276
+ const red = new Color('#ff0000');
277
+ const green = new Color('#00ff00');
278
+ const blue = new Color('#0000ff');
279
+ const result = averageColors([red, green, blue], { space: 'RGB' }, createColorInstance);
280
+ expect(result.toHex()).toBe('#555555');
281
+ });
282
+ it('averages colors in HSL space with weights', () => {
283
+ const red = new Color('#ff0000');
284
+ const green = new Color('#00ff00');
285
+ const result = averageColors([red, green], {
286
+ space: 'HSL',
287
+ weights: [1, 2],
288
+ }, createColorInstance);
289
+ expect(result.toHex()).toBe('#80c936');
290
+ });
291
+ it('defaults to Linear RGB averaging', () => {
292
+ const red = new Color('#ff0000');
293
+ const green = new Color('#00ff00');
294
+ const result = averageColors([red, green], undefined, createColorInstance);
295
+ // Expect brighter yellow than #808000
296
+ expect(result.toHex()).toBe('#bcbc00');
297
+ });
298
+ it('throws when fewer than two colors are provided', () => {
299
+ const red = new Color('#ff0000');
300
+ expect(() => averageColors([red], {}, createColorInstance)).toThrow('at least two colors are required for averaging');
301
+ });
302
+ it('averages colors with alpha channel', () => {
303
+ const semiRed = new Color({ r: 255, g: 0, b: 0, a: 0.5 });
304
+ const blue = new Color({ r: 0, g: 0, b: 255, a: 1 });
305
+ const result = averageColors([semiRed, blue], { space: 'RGB' }, createColorInstance);
306
+ expect(result.toHex()).toBe('#800080');
307
+ expect(result.toRGBA().a).toBe(0.75);
308
+ });
309
+ it('averages black and white in LCH space', () => {
310
+ const black = new Color('#000000');
311
+ const white = new Color('#ffffff');
312
+ const result = averageColors([black, white], { space: 'LCH' }, createColorInstance);
313
+ expect(result.toHex()).toBe('#777777');
314
+ });
315
+ it('averages black and white in OKLCH space', () => {
316
+ const black = new Color('#000000');
317
+ const white = new Color('#ffffff');
318
+ const result = averageColors([black, white], { space: 'OKLCH' }, createColorInstance);
319
+ expect(result.toHex()).toBe('#636363');
320
+ });
321
+ it('averages hues correctly across the 360° boundary', () => {
322
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
323
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
324
+ const result = averageColors([h1, h2], { space: 'HSL' }, createColorInstance);
325
+ expect(result.toHex()).toBe('#fc0303');
326
+ expect(result.toHSL().h).toBe(0);
327
+ });
328
+ it('returns a neutral result when opposing saturated hues cancel out in HSL space', () => {
329
+ const h1 = new Color({ h: 0, s: 100, l: 50 });
330
+ const h2 = new Color({ h: 180, s: 100, l: 50 });
331
+ const result = averageColors([h1, h2], { space: 'HSL' }, createColorInstance);
332
+ const hsl = result.toHSL();
333
+ expect(Number.isNaN(hsl.h)).toBe(false);
334
+ expect(hsl.h).toBe(0);
335
+ expect(hsl.s).toBe(0);
336
+ expect(result.toHex()).toBe('#808080');
337
+ });
338
+ it('minimizes saturation when averaging opposing low-saturation hues in HSL space', () => {
339
+ const warmGray = new Color({ h: 30, s: 40, l: 60 });
340
+ const coolGray = new Color({ h: 210, s: 40, l: 40 });
341
+ const result = averageColors([warmGray, coolGray], { space: 'HSL' }, createColorInstance);
342
+ const hsl = result.toHSL();
343
+ expect(hsl.h).toBe(0);
344
+ expect(hsl.s).toBeLessThanOrEqual(1);
345
+ expect(hsl.l).toBe(50);
346
+ expect(result.toHex()).toBe('#808080');
347
+ });
348
+ it('averages LCH hues correctly across 360 boundary', () => {
349
+ const h1 = new Color({ l: 50, c: 50, h: 350 });
350
+ const h2 = new Color({ l: 50, c: 50, h: 10 });
351
+ const result = averageColors([h1, h2], { space: 'LCH' }, createColorInstance);
352
+ const h = result.toLCH().h;
353
+ const distTo0 = Math.min(h, 360 - h);
354
+ expect(distTo0).toBeLessThan(10);
355
+ });
356
+ it('returns a deterministic hue when opposing hues cancel out in LCH space', () => {
357
+ const h1 = new Color({ l: 60, c: 40, h: 0 });
358
+ const h2 = new Color({ l: 60, c: 40, h: 180 });
359
+ const result = averageColors([h1, h2], { space: 'LCH' }, createColorInstance);
360
+ const lch = result.toLCH();
361
+ const { r, g, b } = result.toRGBA();
362
+ expect(lch.c).toBeLessThan(1);
363
+ expect(Math.max(r, g, b) - Math.min(r, g, b)).toBeLessThanOrEqual(2);
364
+ });
365
+ it('averages OKLCH hues correctly across 360 boundary', () => {
366
+ const h1 = new Color({ l: 0.5, c: 0.1, h: 350 });
367
+ const h2 = new Color({ l: 0.5, c: 0.1, h: 10 });
368
+ const result = averageColors([h1, h2], { space: 'OKLCH' }, createColorInstance);
369
+ const h = result.toOKLCH().h;
370
+ const distTo0 = Math.min(h, 360 - h);
371
+ expect(distTo0).toBeLessThan(10);
372
+ });
373
+ it('returns a deterministic hue when opposing hues cancel out in OKLCH space', () => {
374
+ const h1 = new Color({ l: 0.6, c: 0.05, h: 0 });
375
+ const h2 = new Color({ l: 0.6, c: 0.05, h: 180 });
376
+ const result = averageColors([h1, h2], { space: 'OKLCH' }, createColorInstance);
377
+ const oklch = result.toOKLCH();
378
+ expect(oklch.c).toBeLessThan(0.001);
379
+ expect(Number.isNaN(oklch.h)).toBe(false);
380
+ });
381
+ it('keeps hue stable when chroma is negligible in LCH space', () => {
382
+ const saturated = new Color({ l: 60, c: 40, h: 0 });
383
+ const desaturatedOpponent = new Color({ l: 60, c: 39, h: 180 });
384
+ const result = averageColors([saturated, desaturatedOpponent], { space: 'LCH' }, createColorInstance);
385
+ const lch = result.toLCH();
386
+ expect(lch.c).toBeLessThan(1);
387
+ expect(Math.min(lch.h, 360 - lch.h)).toBeLessThan(30);
388
+ });
389
+ it('keeps hue stable when chroma is negligible in OKLCH space', () => {
390
+ const saturated = new Color({ l: 0.6, c: 0.05, h: 0 });
391
+ const desaturatedOpponent = new Color({ l: 0.6, c: 0.045, h: 180 });
392
+ const result = averageColors([saturated, desaturatedOpponent], { space: 'OKLCH' }, createColorInstance);
393
+ const oklch = result.toOKLCH();
394
+ expect(oklch.c).toBeLessThan(0.005);
395
+ expect(Math.min(oklch.h, 360 - oklch.h)).toBeLessThan(15);
396
+ });
397
+ it('preserves alpha when averaging in HSL, LCH, and OKLCH spaces', () => {
398
+ const translucentRed = new Color({ h: 0, s: 100, l: 50, a: 0.2 });
399
+ const translucentBlue = new Color({ h: 240, s: 100, l: 50, a: 0.8 });
400
+ const hslAverage = averageColors([translucentRed, translucentBlue], { space: 'HSL' }, createColorInstance);
401
+ const lchAverage = averageColors([translucentRed, translucentBlue], { space: 'LCH' }, createColorInstance);
402
+ const oklchAverage = averageColors([translucentRed, translucentBlue], { space: 'OKLCH' }, createColorInstance);
403
+ expect(hslAverage.toRGBA().a).toBeCloseTo(0.5, 5);
404
+ expect(lchAverage.toRGBA().a).toBeCloseTo(0.5, 5);
405
+ expect(oklchAverage.toRGBA().a).toBeCloseTo(0.5, 5);
406
+ });
407
+ it('averages alpha across all spaces using normalized weights', () => {
408
+ const lowAlphaRed = new Color({ r: 255, g: 0, b: 0, a: 0.25 });
409
+ const opaqueBlue = new Color({ r: 0, g: 0, b: 255, a: 1 });
410
+ const weights = [1, 3];
411
+ const expectedAlpha = 0.25 * 0.25 + 1 * 0.75;
412
+ const rgb = averageColors([lowAlphaRed, opaqueBlue], { space: 'RGB', weights }, createColorInstance);
413
+ const linearRgb = averageColors([lowAlphaRed, opaqueBlue], { space: 'LINEAR_RGB', weights }, createColorInstance);
414
+ const hsl = averageColors([lowAlphaRed, opaqueBlue], { space: 'HSL', weights }, createColorInstance);
415
+ const lch = averageColors([lowAlphaRed, opaqueBlue], { space: 'LCH', weights }, createColorInstance);
416
+ const oklch = averageColors([lowAlphaRed, opaqueBlue], { space: 'OKLCH', weights }, createColorInstance);
417
+ expect(rgb.toRGBA().a).toBeCloseTo(expectedAlpha, 3);
418
+ expect(linearRgb.toRGBA().a).toBeCloseTo(expectedAlpha, 3);
419
+ expect(hsl.toRGBA().a).toBeCloseTo(expectedAlpha, 3);
420
+ expect(lch.toRGBA().a).toBeCloseTo(expectedAlpha, 3);
421
+ expect(oklch.toRGBA().a).toBeCloseTo(expectedAlpha, 3);
422
+ });
423
+ it('defaults to equal weights when weights length mismatches', () => {
424
+ const red = new Color('#ff0000');
425
+ const blue = new Color('#0000ff');
426
+ const result = averageColors([red, blue], { weights: [2] }, createColorInstance);
427
+ expect(result.toHex()).toBe('#bc00bc');
428
+ });
429
+ it('defaults weights to equal when provided weights sum to 0', () => {
430
+ const red = new Color('#ff0000');
431
+ const blue = new Color('#0000ff');
432
+ const result = averageColors([red, blue], { weights: [1, -1] }, createColorInstance);
433
+ expect(result.toHex()).toBe('#bc00bc');
434
+ });
435
+ });
436
+ describe('blendColors', () => {
437
+ it('blends using multiply mode', () => {
438
+ const red = new Color('#ff0000');
439
+ const blue = new Color('#0000ff');
440
+ const result = blendColors(red, blue, { mode: 'MULTIPLY', ratio: 1 }, createColorInstance);
441
+ expect(result.toHex()).toBe('#000000');
442
+ });
443
+ it('accepts mixed case blend mode', () => {
444
+ const c1 = new Color('red');
445
+ const c2 = new Color('blue');
446
+ const b1 = blendColors(c1, c2, { mode: 'MULTIPLY' }, createColorInstance);
447
+ const b2 = blendColors(c1, c2, { mode: 'multiply' }, createColorInstance);
448
+ expect(b1.toHex()).toBe(b2.toHex());
449
+ });
450
+ it('accepts mixed case blend space', () => {
451
+ const c1 = new Color('red');
452
+ const c2 = new Color('blue');
453
+ const b1 = blendColors(c1, c2, { space: 'HSL' }, createColorInstance);
454
+ const b2 = blendColors(c1, c2, { space: 'hsl' }, createColorInstance);
455
+ expect(b1.toHex()).toBe(b2.toHex());
456
+ });
457
+ it('partially blends using multiply mode', () => {
458
+ const red = new Color('#ff0000');
459
+ const blue = new Color('#0000ff');
460
+ const result = blendColors(red, blue, {
461
+ mode: 'MULTIPLY',
462
+ ratio: 0.5,
463
+ }, createColorInstance);
464
+ expect(result.toHex()).toBe('#800000');
465
+ });
466
+ it('blends using screen mode', () => {
467
+ const red = new Color('#ff0000');
468
+ const blue = new Color('#0000ff');
469
+ const result = blendColors(red, blue, { mode: 'SCREEN', ratio: 1 }, createColorInstance);
470
+ expect(result.toHex()).toBe('#ff00ff');
471
+ });
472
+ it('partially blends using screen mode', () => {
473
+ const red = new Color('#ff0000');
474
+ const blue = new Color('#0000ff');
475
+ const result = blendColors(red, blue, {
476
+ mode: 'SCREEN',
477
+ ratio: 0.5,
478
+ }, createColorInstance);
479
+ expect(result.toHex()).toBe('#ff0080');
480
+ });
481
+ it('blends using overlay mode', () => {
482
+ const gray = new Color('#808080');
483
+ const white = new Color('#ffffff');
484
+ const result = blendColors(gray, white, { mode: 'OVERLAY', ratio: 1 }, createColorInstance);
485
+ expect(result.toHex()).toBe('#ffffff');
486
+ });
487
+ it('returns base color when ratio is 0', () => {
488
+ const red = new Color('#ff0000');
489
+ const blue = new Color('#0000ff');
490
+ const result = blendColors(red, blue, { ratio: 0 }, createColorInstance);
491
+ expect(result.toHex()).toBe('#ff0000');
492
+ });
493
+ it('returns blend color when ratio is 1 in normal mode', () => {
494
+ const red = new Color('#ff0000');
495
+ const blue = new Color('#0000ff');
496
+ const result = blendColors(red, blue, { mode: 'NORMAL', ratio: 1 }, createColorInstance);
497
+ expect(result.toHex()).toBe('#0000ff');
498
+ });
499
+ it('blends in HSL space', () => {
500
+ const red = new Color('#ff0000');
501
+ const blue = new Color('#0000ff');
502
+ const result = blendColors(red, blue, {
503
+ space: 'HSL',
504
+ ratio: 0.5,
505
+ }, createColorInstance);
506
+ expect(result.toHex()).toBe('#ff00ff');
507
+ });
508
+ it('clamps ratio outside of range 0-1', () => {
509
+ const red = new Color('#ff0000');
510
+ const blue = new Color('#0000ff');
511
+ expect(blendColors(red, blue, { ratio: -1 }, createColorInstance).toHex()).toBe('#ff0000');
512
+ expect(blendColors(red, blue, { ratio: 2 }, createColorInstance).toHex()).toBe('#0000ff');
513
+ });
514
+ it('blends colors with alpha channel in RGB space', () => {
515
+ const semiRed = new Color({ r: 255, g: 0, b: 0, a: 0.5 });
516
+ const blue = new Color({ r: 0, g: 0, b: 255, a: 0.2 });
517
+ const result = blendColors(semiRed, blue, { ratio: 0.5 }, createColorInstance);
518
+ expect(result.toHex()).toBe('#800080');
519
+ expect(result.toRGBA().a).toBe(0.35);
520
+ });
521
+ it('handles hue wrap-around when blending in HSL space', () => {
522
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
523
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
524
+ const result = blendColors(h1, h2, { space: 'HSL', ratio: 0.5 }, createColorInstance);
525
+ expect(result.toHex()).toBe('#ff0000');
526
+ });
527
+ it('blends using overlay mode with dark base color', () => {
528
+ const darkGray = new Color('#404040');
529
+ const red = new Color('#ff0000');
530
+ const result = blendColors(darkGray, red, { mode: 'OVERLAY', ratio: 1 }, createColorInstance);
531
+ expect(result.toHex()).toBe('#800000');
532
+ });
533
+ it('applies blend modes within HSL space', () => {
534
+ const red = new Color('#ff0000');
535
+ const blue = new Color('#0000ff');
536
+ const normal = blendColors(red, blue, { space: 'HSL', mode: 'NORMAL', ratio: 0.5 }, createColorInstance);
537
+ const multiply = blendColors(red, blue, { space: 'HSL', mode: 'MULTIPLY', ratio: 0.5 }, createColorInstance);
538
+ const screen = blendColors(red, blue, { space: 'HSL', mode: 'SCREEN', ratio: 0.5 }, createColorInstance);
539
+ const overlay = blendColors(red, blue, { space: 'HSL', mode: 'OVERLAY', ratio: 0.5 }, createColorInstance);
540
+ expect(normal.toHex()).toBe('#ff00ff');
541
+ expect(multiply.toHex()).toBe('#b900bf');
542
+ expect(screen.toHex()).toBe('#ff4640');
543
+ expect(overlay.toHex()).toBe('#ff0064');
544
+ expect(new Set([normal.toHex(), multiply.toHex(), screen.toHex(), overlay.toHex()]).size).toBe(4);
545
+ });
546
+ it('handles hue wrap-around for multiple HSL blend modes', () => {
547
+ const h1 = new Color({ h: 350, s: 100, l: 50 });
548
+ const h2 = new Color({ h: 10, s: 100, l: 50 });
549
+ const multiply = blendColors(h1, h2, { space: 'HSL', mode: 'MULTIPLY', ratio: 0.5 }, createColorInstance);
550
+ const screen = blendColors(h1, h2, { space: 'HSL', mode: 'SCREEN', ratio: 0.5 }, createColorInstance);
551
+ const overlay = blendColors(h1, h2, { space: 'HSL', mode: 'OVERLAY', ratio: 0.5 }, createColorInstance);
552
+ expect(multiply.toHex()).toBe('#bf003e');
553
+ expect(screen.toHex()).toBe('#ff5f40');
554
+ expect(overlay.toHex()).toBe('#ff0019');
555
+ expect(multiply.toHSL().h).toBeCloseTo(341, 0);
556
+ expect(overlay.toHSL().h).toBeCloseTo(354, 0);
557
+ });
558
+ it('preserves alpha handling when blending in HSL space', () => {
559
+ const semiRed = new Color({ r: 255, g: 0, b: 0, a: 0.5 });
560
+ const blue = new Color({ r: 0, g: 0, b: 255, a: 0.2 });
561
+ const result = blendColors(semiRed, blue, { space: 'HSL', mode: 'MULTIPLY', ratio: 0.5 }, createColorInstance);
562
+ expect(result.toHex()).toBe('#b900bf');
563
+ expect(result.toRGBA().a).toBe(0.35);
564
+ });
565
+ });
566
+ describe('LINEAR_RGB robustness', () => {
567
+ it('produces physically accurate middle gray from black and white', () => {
568
+ const black = new Color('#000000');
569
+ const white = new Color('#ffffff');
570
+ const result = averageColors([black, white], { space: 'LINEAR_RGB' }, createColorInstance);
571
+ // In sRGB averaging, (0+255)/2 = 127.5 -> #808080
572
+ // In Linear averaging, light intensity is averaged.
573
+ // 0 + 1 = 1. / 2 = 0.5 intensity.
574
+ // 0.5 linear intensity -> ~0.735 sRGB -> ~188.
575
+ expect(result.toHex()).toBe('#bcbcbc');
576
+ });
577
+ it('mixes blue and yellow to a lighter gray than sRGB', () => {
578
+ const blue = new Color('#0000ff');
579
+ const yellow = new Color('#ffff00');
580
+ const srgbResult = averageColors([blue, yellow], { space: 'RGB' }, createColorInstance);
581
+ expect(srgbResult.toHex()).toBe('#808080'); // Dark gray
582
+ const linearResult = averageColors([blue, yellow], { space: 'LINEAR_RGB' }, createColorInstance);
583
+ expect(linearResult.toHex()).toBe('#bcbcbc'); // Lighter gray (physically correct)
584
+ });
585
+ it('uses linear light addition for channels when mixing additively', () => {
586
+ const red = new Color('#ff0000');
587
+ const green = new Color('#00ff00');
588
+ const srgbMix = mixColors([red, green], { space: 'RGB', weights: [0.5, 0.5] }, createColorInstance);
589
+ const linearMix = mixColors([red, green], { space: 'LINEAR_RGB', weights: [0.5, 0.5] }, createColorInstance);
590
+ expect(srgbMix.toHex()).toBe('#808000');
591
+ expect(linearMix.toHex()).toBe('#bcbc00');
592
+ expect(linearMix.toHex()).not.toBe(srgbMix.toHex());
593
+ });
594
+ it('handles weighted mixing correctly', () => {
595
+ const red = new Color('#ff0000'); // Linear (1, 0, 0)
596
+ const blue = new Color('#0000ff'); // Linear (0, 0, 1)
597
+ // 75% Red, 25% Blue. LINEAR_RGB mixing normalizes weights to preserve the intended ratio.
598
+ const result = mixColors([red, blue], {
599
+ space: 'LINEAR_RGB',
600
+ weights: [0.75, 0.25],
601
+ }, createColorInstance);
602
+ expect(result.toHex()).toBe('#e10089');
603
+ expect(result.toRGBA().a).toBe(1);
604
+ });
605
+ it('handles alpha blending linearly', () => {
606
+ // Linear RGB mixing currently interpolates alpha linearly, just like RGB.
607
+ const transparentRed = new Color('rgba(255, 0, 0, 0)');
608
+ const opaqueGreen = new Color('rgba(0, 255, 0, 1)');
609
+ // Using averageColors for equal weighting
610
+ const result = averageColors([transparentRed, opaqueGreen], { space: 'LINEAR_RGB' }, createColorInstance);
611
+ // Alpha should be 0.5
612
+ expect(result.toRGBA().a).toBe(0.5);
613
+ // Color channels:
614
+ // Red: 0.5 * 1 + 0.5 * 0 = 0.5 linear -> 188 sRGB.
615
+ // Green: 0.5 * 0 + 0.5 * 1 = 0.5 linear -> 188 sRGB.
616
+ // Blue: 0.
617
+ const { r, g, b } = result.toRGBA();
618
+ expect(r).toBe(188);
619
+ expect(g).toBe(188);
620
+ expect(b).toBe(0);
621
+ });
622
+ it('verifies that many random colors mix to brighter values in Linear than sRGB', () => {
623
+ // Statistical test: Linear average should generally be >= sRGB average for same inputs
624
+ for (let i = 0; i < 20; i++) {
625
+ const c1 = Color.random();
626
+ const c2 = Color.random();
627
+ const linear = averageColors([c1, c2], { space: 'LINEAR_RGB' }, createColorInstance);
628
+ const srgb = averageColors([c1, c2], { space: 'RGB' }, createColorInstance);
629
+ const lLum = linear.toRGBA().r + linear.toRGBA().g + linear.toRGBA().b;
630
+ const sLum = srgb.toRGBA().r + srgb.toRGBA().g + srgb.toRGBA().b;
631
+ // Linear mix preserves energy, so result is often brighter or equal in sRGB values
632
+ // (because sRGB gamma curve is concave up x^2.4)
633
+ // Average of x^2.4 is > (Average x)^2.4 is FALSE.
634
+ // But here we are doing: sRGB_out = (Avg(Linear))^(1/2.4) vs sRGB_out = Avg(sRGB).
635
+ // Let y = sRGB input. Linear = y^2.4.
636
+ // LHS: ( (y1^2.4 + y2^2.4)/2 ) ^ (1/2.4).
637
+ // RHS: (y1 + y2) / 2.
638
+ // Let f(x) = x^(1/2.4) = x^0.41 (Concave).
639
+ // Jensen: f(Avg) >= Avg(f).
640
+ // LHS = f(Avg(Linear)).
641
+ // RHS = Avg(f(Linear)) ?? No.
642
+ // RHS is Avg(sRGB). sRGB = f(Linear).
643
+ // So RHS = Avg(f(Linear)).
644
+ // So LHS >= RHS.
645
+ expect(lLum).toBeGreaterThanOrEqual(sLum - 3); // Allow small rounding error
646
+ }
647
+ });
648
+ it('throws for invalid mix option values', () => {
649
+ const red = new Color('#ff0000');
650
+ const blue = new Color('#0000ff');
651
+ expect(() => mixColors([red, blue], { type: 'INVALID' }, createColorInstance)).toThrow("Invalid 'type'");
652
+ expect(() => mixColors([red, blue], { space: 'INVALID' }, createColorInstance)).toThrow("Invalid 'space'");
653
+ });
654
+ it('throws for invalid blend option values', () => {
655
+ const red = new Color('#ff0000');
656
+ const blue = new Color('#0000ff');
657
+ expect(() => blendColors(red, blue, { mode: 'INVALID' }, createColorInstance)).toThrow("Invalid 'mode'");
658
+ expect(() => blendColors(red, blue, { space: 'INVALID' }, createColorInstance)).toThrow("Invalid 'space'");
659
+ });
660
+ it('throws for invalid average option values', () => {
661
+ const red = new Color('#ff0000');
662
+ const blue = new Color('#0000ff');
663
+ expect(() => averageColors([red, blue], { space: 'INVALID' }, createColorInstance)).toThrow("Invalid 'space'");
664
+ });
665
+ });
@@ -0,0 +1 @@
1
+ export {};