@trishchuk/coolors-mcp 1.0.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 (197) hide show
  1. package/.claude/settings.local.json +39 -0
  2. package/.env +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
  5. package/.github/pull_request_template.md +97 -0
  6. package/.github/workflows/ci.yml +127 -0
  7. package/.github/workflows/deploy-docs.yml +56 -0
  8. package/.github/workflows/release.yml +99 -0
  9. package/.mcp.json +12 -0
  10. package/.prettierignore +1 -0
  11. package/CLAUDE.md +201 -0
  12. package/DOCUMENTATION.md +274 -0
  13. package/GEMINI.md +54 -0
  14. package/LICENSE +21 -0
  15. package/README.md +401 -0
  16. package/demo/content_based_color.png +0 -0
  17. package/demo/music-player.html +621 -0
  18. package/demo/podcast-player.html +903 -0
  19. package/dist/bin/coolors-mcp.d.ts +1 -0
  20. package/dist/bin/coolors-mcp.js +154 -0
  21. package/dist/bin/coolors-mcp.js.map +1 -0
  22. package/dist/bin/server.d.ts +1 -0
  23. package/dist/bin/server.js +3292 -0
  24. package/dist/bin/server.js.map +1 -0
  25. package/dist/chunk-IQ7NN26V.js +114 -0
  26. package/dist/chunk-IQ7NN26V.js.map +1 -0
  27. package/dist/chunk-P3ARRKLS.js +1214 -0
  28. package/dist/chunk-P3ARRKLS.js.map +1 -0
  29. package/dist/color/index.d.ts +716 -0
  30. package/dist/color/index.js +153 -0
  31. package/dist/color/index.js.map +1 -0
  32. package/dist/coolors-mcp.d.ts +136 -0
  33. package/dist/coolors-mcp.js +7 -0
  34. package/dist/coolors-mcp.js.map +1 -0
  35. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  36. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  37. package/docs/.vitepress/cache/deps/_metadata.json +127 -0
  38. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  39. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  40. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
  41. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
  42. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
  43. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
  44. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  45. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  46. package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
  47. package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
  48. package/docs/.vitepress/cache/deps/dayjs.js +285 -0
  49. package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
  50. package/docs/.vitepress/cache/deps/debug.js +468 -0
  51. package/docs/.vitepress/cache/deps/debug.js.map +7 -0
  52. package/docs/.vitepress/cache/deps/package.json +3 -0
  53. package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
  54. package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
  55. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
  56. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
  57. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
  58. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
  59. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
  60. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
  61. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
  62. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
  63. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
  64. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
  65. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
  66. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
  67. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  68. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  69. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  70. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  71. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
  72. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  73. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  74. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  75. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
  76. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  77. package/docs/.vitepress/cache/deps/vue.js +344 -0
  78. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  79. package/docs/.vitepress/components/ClientGrid.vue +125 -0
  80. package/docs/.vitepress/components/CodeBlock.vue +231 -0
  81. package/docs/.vitepress/components/ConfigModal.vue +477 -0
  82. package/docs/.vitepress/components/DiagramModal.vue +528 -0
  83. package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
  84. package/docs/.vitepress/config.js +162 -0
  85. package/docs/.vitepress/theme/FundingLayout.vue +251 -0
  86. package/docs/.vitepress/theme/Layout.vue +134 -0
  87. package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
  88. package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
  89. package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
  90. package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
  91. package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
  92. package/docs/.vitepress/theme/custom-app.css +339 -0
  93. package/docs/.vitepress/theme/custom.css +699 -0
  94. package/docs/.vitepress/theme/index.js +25 -0
  95. package/docs/README.md +198 -0
  96. package/docs/concepts/accessibility.md +473 -0
  97. package/docs/concepts/color-spaces.md +222 -0
  98. package/docs/concepts/distance-metrics.md +384 -0
  99. package/docs/concepts/hct.md +261 -0
  100. package/docs/concepts/image-analysis.md +396 -0
  101. package/docs/concepts/material-design.md +306 -0
  102. package/docs/concepts/theme-matching.md +399 -0
  103. package/docs/examples/basic-colors.md +490 -0
  104. package/docs/examples/creating-themes.md +898 -0
  105. package/docs/examples/css-refactoring.md +824 -0
  106. package/docs/examples/image-extraction.md +882 -0
  107. package/docs/getting-started.md +366 -0
  108. package/docs/index.md +190 -0
  109. package/docs/installation.md +157 -0
  110. package/docs/tools/README.md +234 -0
  111. package/docs/tools/accessibility.md +614 -0
  112. package/docs/tools/color-operations.md +374 -0
  113. package/docs/tools/image-extraction.md +624 -0
  114. package/docs/tools/material-design.md +347 -0
  115. package/docs/tools/theme-matching.md +552 -0
  116. package/eslint.config.ts +14 -0
  117. package/examples/theme-matching.md +113 -0
  118. package/jsr.json +7 -0
  119. package/mcp-config.json +8 -0
  120. package/note.md +35 -0
  121. package/package.json +122 -0
  122. package/research_results.md +53 -0
  123. package/src/bin/coolors-mcp.ts +194 -0
  124. package/src/bin/server.ts +61 -0
  125. package/src/color/__tests__/conversions-argb.test.ts +198 -0
  126. package/src/color/__tests__/extract-colors.test.ts +360 -0
  127. package/src/color/__tests__/image-utils.test.ts +242 -0
  128. package/src/color/__tests__/reference-colors.test.ts +278 -0
  129. package/src/color/__tests__/round-trip.test.ts +197 -0
  130. package/src/color/conversions.test.ts +402 -0
  131. package/src/color/conversions.ts +393 -0
  132. package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
  133. package/src/color/dislike/dislike-analyzer.ts +114 -0
  134. package/src/color/extract-colors.ts +228 -0
  135. package/src/color/hct/__tests__/hct-class.test.ts +232 -0
  136. package/src/color/hct/harmonization.ts +204 -0
  137. package/src/color/hct/hct-class.ts +109 -0
  138. package/src/color/hct/hct-solver.ts +168 -0
  139. package/src/color/hct/index.ts +39 -0
  140. package/src/color/hct/tonal-palette.ts +211 -0
  141. package/src/color/hct/types.ts +88 -0
  142. package/src/color/image-utils.ts +79 -0
  143. package/src/color/index.ts +87 -0
  144. package/src/color/material-theme.ts +157 -0
  145. package/src/color/metrics.test.ts +276 -0
  146. package/src/color/metrics.ts +281 -0
  147. package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
  148. package/src/color/quantize/lab_point_provider.ts +55 -0
  149. package/src/color/quantize/point_provider.ts +27 -0
  150. package/src/color/quantize/quantizer_celebi.ts +51 -0
  151. package/src/color/quantize/quantizer_celebi_test.ts +71 -0
  152. package/src/color/quantize/quantizer_map.ts +47 -0
  153. package/src/color/quantize/quantizer_wsmeans.ts +232 -0
  154. package/src/color/quantize/quantizer_wu.ts +472 -0
  155. package/src/color/score/__tests__/score.test.ts +224 -0
  156. package/src/color/score/score.ts +175 -0
  157. package/src/color/types.ts +151 -0
  158. package/src/color/utils/color_utils.ts +292 -0
  159. package/src/color/utils/math_utils.ts +145 -0
  160. package/src/color/utils.test.ts +403 -0
  161. package/src/color/utils.ts +315 -0
  162. package/src/constants.ts +5 -0
  163. package/src/coolors-mcp.ts +37 -0
  164. package/src/examples/addition.ts +333 -0
  165. package/src/examples/color-demo.ts +125 -0
  166. package/src/examples/custom-logger.ts +201 -0
  167. package/src/examples/oauth-server.ts +113 -0
  168. package/src/examples/session-context.ts +269 -0
  169. package/src/session.ts +116 -0
  170. package/src/theme/__tests__/matcher.test.ts +180 -0
  171. package/src/theme/__tests__/parser.test.ts +148 -0
  172. package/src/theme/__tests__/refactor.test.ts +224 -0
  173. package/src/theme/index.ts +34 -0
  174. package/src/theme/matcher.ts +395 -0
  175. package/src/theme/parser.ts +392 -0
  176. package/src/theme/refactor.ts +360 -0
  177. package/src/theme/types.ts +152 -0
  178. package/src/tools/__tests__/gradient-generator.test.ts +206 -0
  179. package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
  180. package/src/tools/color-conversion.tool.ts +54 -0
  181. package/src/tools/color-distance.tool.ts +41 -0
  182. package/src/tools/colors.ts +31 -0
  183. package/src/tools/contrast-checker.tool.ts +37 -0
  184. package/src/tools/dislike-analyzer.tool.ts +247 -0
  185. package/src/tools/gradient-generator.tool.ts +250 -0
  186. package/src/tools/image-extraction.tools.ts +289 -0
  187. package/src/tools/index.ts +39 -0
  188. package/src/tools/material-theme.tools.ts +250 -0
  189. package/src/tools/palette-generator.tool.ts +135 -0
  190. package/src/tools/palette-with-locks.tool.ts +221 -0
  191. package/src/tools/registry.ts +142 -0
  192. package/src/tools/simple-tools.ts +37 -0
  193. package/src/tools/theme-matching.tools.ts +334 -0
  194. package/src/types.ts +182 -0
  195. package/src/utils.ts +22 -0
  196. package/tsconfig.json +8 -0
  197. package/vitest.config.js +15 -0
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Reference color values from official sources for testing
3
+ * These values are from:
4
+ * - Material Design color system
5
+ * - CSS named colors (W3C standard)
6
+ * - WCAG contrast ratio examples
7
+ */
8
+
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { hexToRgb, rgbToHex, rgbToHsl } from "../conversions";
12
+ import { rgbToHct } from "../hct";
13
+ import { Hct } from "../hct/hct-class";
14
+ import * as utils from "../utils/color_utils";
15
+
16
+ // Helper function to calculate WCAG contrast ratio
17
+ function calculateContrast(
18
+ rgb1: { b: number; g: number; r: number },
19
+ rgb2: { b: number; g: number; r: number },
20
+ ): number {
21
+ // Calculate relative luminance
22
+ function getLuminance(rgb: { b: number; g: number; r: number }): number {
23
+ const { b, g, r } = rgb;
24
+ const sRGB = [r, g, b].map((val) => {
25
+ if (val <= 0.03928) {
26
+ return val / 12.92;
27
+ }
28
+ return Math.pow((val + 0.055) / 1.055, 2.4);
29
+ });
30
+ return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
31
+ }
32
+
33
+ const l1 = getLuminance(rgb1);
34
+ const l2 = getLuminance(rgb2);
35
+ const lighter = Math.max(l1, l2);
36
+ const darker = Math.min(l1, l2);
37
+
38
+ return (lighter + 0.05) / (darker + 0.05);
39
+ }
40
+
41
+ describe("Reference Color Values", () => {
42
+ describe("Material Design Colors", () => {
43
+ it("should correctly convert Material Design Blue 500", () => {
44
+ const blue500 = "#2196F3";
45
+ const rgb = hexToRgb(blue500);
46
+
47
+ // Official RGB values from Material Design
48
+ expect(rgb.r).toBe(33);
49
+ expect(rgb.g).toBe(150);
50
+ expect(rgb.b).toBe(243);
51
+
52
+ // Convert to HCT
53
+ const hct = rgbToHct(rgb);
54
+ expect(hct.h).toBeCloseTo(272, -1); // Blue hue in HCT (different from HSL)
55
+ expect(hct.c).toBeGreaterThan(50); // High chroma for vibrant blue
56
+ expect(hct.t).toBeCloseTo(58, -1); // Medium tone
57
+ });
58
+
59
+ it("should handle Material Design color tones correctly", () => {
60
+ // Material Design 3 standard tones
61
+ const standardTones = [
62
+ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100,
63
+ ];
64
+
65
+ // Create a tonal palette from a primary color
66
+ const primaryHue = 260; // Purple
67
+ const primaryChroma = 48;
68
+
69
+ for (const tone of standardTones) {
70
+ const hct = Hct.from(primaryHue, primaryChroma, tone);
71
+ // Allow some variation in extreme tones due to color space limitations
72
+ if (tone === 0 || tone === 100) {
73
+ expect(hct.tone).toBeCloseTo(tone, -1);
74
+ } else if (tone <= 40) {
75
+ // Lower tones have more variation due to gamut limitations
76
+ expect(Math.abs(hct.tone - tone)).toBeLessThan(10);
77
+ } else {
78
+ expect(hct.tone).toBeCloseTo(tone, -1); // Within 10 units
79
+ }
80
+
81
+ // Verify tone creates proper contrast
82
+ if (tone <= 50) {
83
+ // Dark tones should have good contrast with white
84
+ const argb = hct.toInt();
85
+ const rgb = {
86
+ b: utils.blueFromArgb(argb) / 255,
87
+ g: utils.greenFromArgb(argb) / 255,
88
+ r: utils.redFromArgb(argb) / 255,
89
+ };
90
+ const contrastWithWhite = calculateContrast(rgb, {
91
+ b: 1,
92
+ g: 1,
93
+ r: 1,
94
+ });
95
+
96
+ if (tone <= 40) {
97
+ expect(contrastWithWhite).toBeGreaterThan(3); // WCAG AA for large text
98
+ }
99
+ if (tone <= 30) {
100
+ expect(contrastWithWhite).toBeGreaterThan(4.5); // WCAG AA for normal text
101
+ }
102
+ }
103
+ }
104
+ });
105
+ });
106
+
107
+ describe("CSS Named Colors", () => {
108
+ const namedColors = [
109
+ { hex: "#ff6347", name: "tomato", rgb: { b: 71, g: 99, r: 255 } },
110
+ { hex: "#1e90ff", name: "dodgerblue", rgb: { b: 255, g: 144, r: 30 } },
111
+ { hex: "#663399", name: "rebeccapurple", rgb: { b: 153, g: 51, r: 102 } },
112
+ { hex: "#ff7f50", name: "coral", rgb: { b: 80, g: 127, r: 255 } },
113
+ { hex: "#ffd700", name: "gold", rgb: { b: 0, g: 215, r: 255 } },
114
+ { hex: "#dc143c", name: "crimson", rgb: { b: 60, g: 20, r: 220 } },
115
+ {
116
+ hex: "#3cb371",
117
+ name: "mediumseagreen",
118
+ rgb: { b: 113, g: 179, r: 60 },
119
+ },
120
+ ];
121
+
122
+ namedColors.forEach(({ hex, name, rgb: expectedRgb }) => {
123
+ it(`should correctly convert CSS named color: ${name}`, () => {
124
+ const rgb = hexToRgb(hex);
125
+
126
+ // Verify RGB values
127
+ expect(rgb.r).toBe(expectedRgb.r);
128
+ expect(rgb.g).toBe(expectedRgb.g);
129
+ expect(rgb.b).toBe(expectedRgb.b);
130
+
131
+ // Verify round-trip conversion
132
+ const hexBack = rgbToHex(rgb);
133
+ expect(hexBack.toLowerCase()).toBe(hex.toLowerCase());
134
+ });
135
+ });
136
+
137
+ it("should convert rebeccapurple to correct HCT values", () => {
138
+ // rebeccapurple was added to CSS to honor Eric Meyer
139
+ const rgb = hexToRgb("#663399");
140
+ const hct = rgbToHct(rgb);
141
+
142
+ expect(hct.h).toBeCloseTo(309, -1); // Purple hue in HCT
143
+ expect(hct.c).toBeGreaterThan(40); // Moderate chroma
144
+ expect(hct.t).toBeCloseTo(32, -1); // Dark tone
145
+ });
146
+ });
147
+
148
+ describe("WCAG Contrast Ratios", () => {
149
+ it("should verify maximum contrast ratio (black on white)", () => {
150
+ const black = { b: 0, g: 0, r: 0 };
151
+ const white = { b: 1, g: 1, r: 1 };
152
+
153
+ const contrast = calculateContrast(black, white);
154
+ expect(contrast).toBeCloseTo(21, 0); // Maximum possible contrast
155
+ });
156
+
157
+ it("should verify WCAG AA compliance examples", () => {
158
+ // #777777 on white achieves exactly 4.5:1
159
+ const gray777 = hexToRgb("#777777");
160
+ const white = { b: 1, g: 1, r: 1 };
161
+
162
+ const contrast = calculateContrast(
163
+ { b: gray777.b / 255, g: gray777.g / 255, r: gray777.r / 255 },
164
+ white,
165
+ );
166
+ expect(contrast).toBeCloseTo(4.5, 1); // WCAG AA minimum
167
+ });
168
+
169
+ it("should verify WCAG AAA compliance examples", () => {
170
+ // #595959 on white achieves approximately 7:1
171
+ const gray595 = hexToRgb("#595959");
172
+ const white = { b: 1, g: 1, r: 1 };
173
+
174
+ const contrast = calculateContrast(
175
+ { b: gray595.b / 255, g: gray595.g / 255, r: gray595.r / 255 },
176
+ white,
177
+ );
178
+ expect(contrast).toBeCloseTo(7, 1); // WCAG AAA minimum
179
+ });
180
+
181
+ it("should verify specific color contrasts on white", () => {
182
+ const white = { b: 1, g: 1, r: 1 };
183
+
184
+ // Pure red on white
185
+ const red = { b: 0, g: 0, r: 1 };
186
+ const redContrast = calculateContrast(red, white);
187
+ expect(redContrast).toBeCloseTo(4, 0.5); // ~4:1
188
+
189
+ // Pure green on white (poor contrast)
190
+ const green = { b: 0, g: 1, r: 0 };
191
+ const greenContrast = calculateContrast(green, white);
192
+ expect(greenContrast).toBeLessThan(2); // ~1.4:1, fails WCAG
193
+
194
+ // Pure blue on white
195
+ const blue = { b: 1, g: 0, r: 0 };
196
+ const blueContrast = calculateContrast(blue, white);
197
+ expect(blueContrast).toBeCloseTo(8.6, 0.5); // ~8.6:1
198
+ });
199
+ });
200
+
201
+ describe("HCT Color Space Properties", () => {
202
+ it("should maintain tone-based contrast guarantees", () => {
203
+ // HCT tone differences guarantee specific contrast ratios
204
+ const hue = 180;
205
+ const chroma = 40;
206
+
207
+ // Tone 40 vs white should give ~3:1 contrast
208
+ const tone40 = Hct.from(hue, chroma, 40);
209
+ const argb40 = tone40.toInt();
210
+ const rgb40 = {
211
+ b: utils.blueFromArgb(argb40) / 255,
212
+ g: utils.greenFromArgb(argb40) / 255,
213
+ r: utils.redFromArgb(argb40) / 255,
214
+ };
215
+ const contrast40 = calculateContrast(rgb40, { b: 1, g: 1, r: 1 });
216
+ expect(contrast40).toBeGreaterThan(2.5); // Close to 3:1
217
+
218
+ // Tone 50 vs white should give ~4.5:1 contrast
219
+ const tone50 = Hct.from(hue, chroma, 50);
220
+ const argb50 = tone50.toInt();
221
+ const rgb50 = {
222
+ b: utils.blueFromArgb(argb50) / 255,
223
+ g: utils.greenFromArgb(argb50) / 255,
224
+ r: utils.redFromArgb(argb50) / 255,
225
+ };
226
+ const contrast50 = calculateContrast(rgb50, { b: 1, g: 1, r: 1 });
227
+ expect(contrast50).toBeGreaterThan(4); // Close to 4.5:1
228
+ });
229
+
230
+ it("should handle HCT chroma clamping for impossible colors", () => {
231
+ // Very high chroma at extreme tones is impossible
232
+ // Very high chroma at extreme tones gets adjusted
233
+ const hct1 = Hct.from(0, 200, 95); // High tone, high chroma
234
+ expect(hct1.chroma).toBeLessThan(120); // Chroma gets clamped
235
+ // Tone might be adjusted when chroma is impossible
236
+ expect(hct1.tone).toBeGreaterThan(50); // Tone adjusted
237
+
238
+ const hct2 = Hct.from(180, 200, 5); // Low tone, high chroma
239
+ expect(hct2.chroma).toBeLessThan(120); // Chroma gets clamped
240
+ // Tone might be adjusted when chroma is impossible
241
+ expect(hct2.tone).toBeLessThan(50); // Tone adjusted
242
+ });
243
+ });
244
+
245
+ describe("Color Space Conversions", () => {
246
+ it("should accurately convert between color spaces", () => {
247
+ // Test with Material Design Indigo 500
248
+ const indigo500 = "#3F51B5";
249
+ const rgb = hexToRgb(indigo500);
250
+
251
+ // To HSL
252
+ const hsl = rgbToHsl(rgb);
253
+ expect(hsl.h).toBeCloseTo(231, -1); // Blue-purple hue
254
+ expect(hsl.s).toBeCloseTo(48, -1); // Moderate saturation (percentage)
255
+ expect(hsl.l).toBeCloseTo(48, -1); // Medium lightness (percentage)
256
+
257
+ // To HCT
258
+ const hct = rgbToHct(rgb);
259
+ expect(hct.h).toBeCloseTo(295, -1); // Blue-purple in HCT
260
+ expect(hct.c).toBeGreaterThan(30); // Moderate chroma
261
+ expect(hct.t).toBeCloseTo(40, -1); // Medium-dark tone
262
+
263
+ // Round-trip test
264
+ const hctObj = Hct.from(hct.h, hct.c, hct.t);
265
+ const argb = hctObj.toInt();
266
+ const rgbBack = {
267
+ b: utils.blueFromArgb(argb),
268
+ g: utils.greenFromArgb(argb),
269
+ r: utils.redFromArgb(argb),
270
+ };
271
+
272
+ // Allow some tolerance due to color space conversions
273
+ expect(rgbBack.r).toBeCloseTo(rgb.r, -1);
274
+ expect(rgbBack.g).toBeCloseTo(rgb.g, -1);
275
+ expect(rgbBack.b).toBeCloseTo(rgb.b, -1);
276
+ });
277
+ });
278
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Round-trip conversion tests
3
+ * Inspired by chromaticity-color-utilities tests
4
+ * Tests that converting from one color space to another and back preserves values
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+
9
+ import {
10
+ hexToRgb,
11
+ hslToRgb,
12
+ hsvToRgb,
13
+ labToRgb,
14
+ rgbToHex,
15
+ rgbToHsl,
16
+ rgbToHsv,
17
+ rgbToLab,
18
+ rgbToXyz,
19
+ xyzToRgb,
20
+ } from "../conversions";
21
+
22
+ describe("Round-trip Conversions", () => {
23
+ // Test a range of colors with step to avoid too many tests
24
+ const testColors = [
25
+ { b: 0, g: 0, r: 0 }, // Black
26
+ { b: 255, g: 255, r: 255 }, // White
27
+ { b: 0, g: 0, r: 255 }, // Red
28
+ { b: 0, g: 255, r: 0 }, // Green
29
+ { b: 255, g: 0, r: 0 }, // Blue
30
+ { b: 255, g: 0, r: 255 }, // Magenta
31
+ { b: 0, g: 255, r: 255 }, // Yellow
32
+ { b: 255, g: 255, r: 0 }, // Cyan
33
+ { b: 128, g: 128, r: 128 }, // Gray
34
+ { b: 0, g: 128, r: 255 }, // Orange
35
+ { b: 128, g: 0, r: 128 }, // Purple
36
+ { b: 208, g: 224, r: 64 }, // Turquoise
37
+ ];
38
+
39
+ // Additional random colors for broader testing
40
+ for (let i = 0; i < 20; i++) {
41
+ testColors.push({
42
+ b: Math.floor(Math.random() * 256),
43
+ g: Math.floor(Math.random() * 256),
44
+ r: Math.floor(Math.random() * 256),
45
+ });
46
+ }
47
+
48
+ describe("RGB ↔ HSL", () => {
49
+ testColors.forEach((color, index) => {
50
+ it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
51
+ const hsl = rgbToHsl(color);
52
+ const rgbBack = hslToRgb(hsl);
53
+
54
+ expect(rgbBack.r).toBeCloseTo(color.r, -1); // Within 10
55
+ expect(rgbBack.g).toBeCloseTo(color.g, -1);
56
+ expect(rgbBack.b).toBeCloseTo(color.b, -1);
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("RGB ↔ HSV", () => {
62
+ testColors.forEach((color, index) => {
63
+ it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
64
+ const hsv = rgbToHsv(color);
65
+ const rgbBack = hsvToRgb(hsv);
66
+
67
+ expect(rgbBack.r).toBeCloseTo(color.r, -1);
68
+ expect(rgbBack.g).toBeCloseTo(color.g, -1);
69
+ expect(rgbBack.b).toBeCloseTo(color.b, -1);
70
+ });
71
+ });
72
+ });
73
+
74
+ describe("RGB ↔ LAB", () => {
75
+ testColors.forEach((color, index) => {
76
+ it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
77
+ const lab = rgbToLab(color);
78
+ const rgbBack = labToRgb(lab);
79
+
80
+ // LAB conversions have more variance
81
+ expect(rgbBack.r).toBeCloseTo(color.r, -1.5); // Within 32
82
+ expect(rgbBack.g).toBeCloseTo(color.g, -1.5);
83
+ expect(rgbBack.b).toBeCloseTo(color.b, -1.5);
84
+ });
85
+ });
86
+ });
87
+
88
+ describe("RGB ↔ XYZ", () => {
89
+ testColors.forEach((color, index) => {
90
+ it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
91
+ const xyz = rgbToXyz(color);
92
+ const rgbBack = xyzToRgb(xyz);
93
+
94
+ expect(rgbBack.r).toBeCloseTo(color.r, -1);
95
+ expect(rgbBack.g).toBeCloseTo(color.g, -1);
96
+ expect(rgbBack.b).toBeCloseTo(color.b, -1);
97
+ });
98
+ });
99
+ });
100
+
101
+ describe("RGB ↔ HEX", () => {
102
+ testColors.forEach((color, index) => {
103
+ it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
104
+ const hex = rgbToHex(color);
105
+ const rgbBack = hexToRgb(hex);
106
+
107
+ // HEX should be exact
108
+ expect(rgbBack.r).toBe(color.r);
109
+ expect(rgbBack.g).toBe(color.g);
110
+ expect(rgbBack.b).toBe(color.b);
111
+ });
112
+ });
113
+ });
114
+
115
+ describe("Reference colors from chromaticity", () => {
116
+ it("should convert magenta correctly in LAB", () => {
117
+ const magenta = { b: 255, g: 0, r: 255 };
118
+ const lab = rgbToLab(magenta);
119
+
120
+ // Chromaticity reference: L=60, a=98, b=-61
121
+ expect(lab.l).toBeCloseTo(60, -1);
122
+ expect(lab.a).toBeCloseTo(98, -1);
123
+ expect(lab.b).toBeCloseTo(-61, -1);
124
+ });
125
+ });
126
+
127
+ describe("Grayscale preservation", () => {
128
+ it("should preserve grayscale in all color spaces", () => {
129
+ const grays = [
130
+ { b: 0, g: 0, r: 0 },
131
+ { b: 64, g: 64, r: 64 },
132
+ { b: 128, g: 128, r: 128 },
133
+ { b: 192, g: 192, r: 192 },
134
+ { b: 255, g: 255, r: 255 },
135
+ ];
136
+
137
+ grays.forEach((gray) => {
138
+ // HSL
139
+ const hsl = rgbToHsl(gray);
140
+ expect(hsl.s).toBe(0); // No saturation for grays
141
+
142
+ // HSV
143
+ const hsv = rgbToHsv(gray);
144
+ expect(hsv.s).toBe(0); // No saturation for grays
145
+
146
+ // LAB
147
+ const lab = rgbToLab(gray);
148
+ expect(Math.abs(lab.a)).toBeLessThan(1); // Near zero
149
+ expect(Math.abs(lab.b)).toBeLessThan(1); // Near zero
150
+ });
151
+ });
152
+ });
153
+
154
+ describe("Primary colors", () => {
155
+ it("should handle pure red correctly", () => {
156
+ const red = { b: 0, g: 0, r: 255 };
157
+
158
+ const hsl = rgbToHsl(red);
159
+ expect(hsl.h).toBe(0);
160
+ expect(hsl.s).toBe(100);
161
+ expect(hsl.l).toBe(50);
162
+
163
+ const hsv = rgbToHsv(red);
164
+ expect(hsv.h).toBe(0);
165
+ expect(hsv.s).toBe(100);
166
+ expect(hsv.v).toBe(100);
167
+ });
168
+
169
+ it("should handle pure green correctly", () => {
170
+ const green = { b: 0, g: 255, r: 0 };
171
+
172
+ const hsl = rgbToHsl(green);
173
+ expect(hsl.h).toBe(120);
174
+ expect(hsl.s).toBe(100);
175
+ expect(hsl.l).toBe(50);
176
+
177
+ const hsv = rgbToHsv(green);
178
+ expect(hsv.h).toBe(120);
179
+ expect(hsv.s).toBe(100);
180
+ expect(hsv.v).toBe(100);
181
+ });
182
+
183
+ it("should handle pure blue correctly", () => {
184
+ const blue = { b: 255, g: 0, r: 0 };
185
+
186
+ const hsl = rgbToHsl(blue);
187
+ expect(hsl.h).toBe(240);
188
+ expect(hsl.s).toBe(100);
189
+ expect(hsl.l).toBe(50);
190
+
191
+ const hsv = rgbToHsv(blue);
192
+ expect(hsv.h).toBe(240);
193
+ expect(hsv.s).toBe(100);
194
+ expect(hsv.v).toBe(100);
195
+ });
196
+ });
197
+ });