@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,204 @@
1
+ /**
2
+ * Color Harmonization
3
+ * Algorithms to blend and harmonize colors based on Material Design principles
4
+ */
5
+
6
+ import type { RGB } from "../types.js";
7
+
8
+ import { hct, hctToRgb, rgbToHct } from "./hct-solver.js";
9
+
10
+ /**
11
+ * Temperature-based color adjustment
12
+ * Warmer (positive) or cooler (negative) adjustment
13
+ */
14
+ export function adjustTemperature(color: RGB, amount: number): RGB {
15
+ const hctColor = rgbToHct(color);
16
+
17
+ // Warm colors: red-orange-yellow (0-60, 300-360)
18
+ // Cool colors: green-blue-purple (60-300)
19
+ const isWarm = hctColor.h <= 60 || hctColor.h >= 300;
20
+
21
+ let newHue: number;
22
+ if (amount > 0) {
23
+ // Make warmer - shift toward orange (30 degrees)
24
+ newHue = isWarm
25
+ ? circularInterpolate(hctColor.h, 30, Math.min(amount, 1))
26
+ : circularInterpolate(hctColor.h, 30, Math.min(amount * 2, 1));
27
+ } else {
28
+ // Make cooler - shift toward blue (210 degrees)
29
+ newHue = !isWarm
30
+ ? circularInterpolate(hctColor.h, 210, Math.min(-amount, 1))
31
+ : circularInterpolate(hctColor.h, 210, Math.min(-amount * 2, 1));
32
+ }
33
+
34
+ return hctToRgb(hct(newHue, hctColor.c, hctColor.t));
35
+ }
36
+
37
+ /**
38
+ * Find analogous colors (adjacent on color wheel)
39
+ */
40
+ export function analogous(
41
+ color: RGB,
42
+ angle: number = 30,
43
+ count: number = 2,
44
+ ): RGB[] {
45
+ const hctColor = rgbToHct(color);
46
+ const colors: RGB[] = [];
47
+
48
+ for (let i = 1; i <= count; i++) {
49
+ // Add colors on both sides
50
+ colors.push(
51
+ hctToRgb(
52
+ hct(
53
+ sanitizeDegreesDouble(hctColor.h + angle * i),
54
+ hctColor.c,
55
+ hctColor.t,
56
+ ),
57
+ ),
58
+ );
59
+
60
+ colors.push(
61
+ hctToRgb(
62
+ hct(
63
+ sanitizeDegreesDouble(hctColor.h - angle * i),
64
+ hctColor.c,
65
+ hctColor.t,
66
+ ),
67
+ ),
68
+ );
69
+ }
70
+
71
+ return colors;
72
+ }
73
+
74
+ /**
75
+ * Blend two colors in HCT space
76
+ * More perceptually uniform than RGB blending
77
+ */
78
+ export function blend(from: RGB, to: RGB, amount: number): RGB {
79
+ const fromHct = rgbToHct(from);
80
+ const toHct = rgbToHct(to);
81
+
82
+ // Interpolate in HCT space
83
+ const h = circularInterpolate(fromHct.h, toHct.h, amount);
84
+ const c = fromHct.c + (toHct.c - fromHct.c) * amount;
85
+ const t = fromHct.t + (toHct.t - fromHct.t) * amount;
86
+
87
+ return hctToRgb(hct(h, c, t));
88
+ }
89
+
90
+ /**
91
+ * Create a double complementary (rectangle) scheme
92
+ */
93
+ export function doubleComplementary(
94
+ color1: RGB,
95
+ color2: RGB,
96
+ ): [RGB, RGB, RGB, RGB] {
97
+ const hct1 = rgbToHct(color1);
98
+ const hct2 = rgbToHct(color2);
99
+
100
+ return [
101
+ color1,
102
+ color2,
103
+ hctToRgb(hct((hct1.h + 180) % 360, hct1.c, hct1.t)),
104
+ hctToRgb(hct((hct2.h + 180) % 360, hct2.c, hct2.t)),
105
+ ];
106
+ }
107
+
108
+ /**
109
+ * Create a gradient between two colors
110
+ */
111
+ export function gradient(from: RGB, to: RGB, steps: number): RGB[] {
112
+ const colors: RGB[] = [];
113
+
114
+ for (let i = 0; i < steps; i++) {
115
+ const amount = i / (steps - 1);
116
+ colors.push(blend(from, to, amount));
117
+ }
118
+
119
+ return colors;
120
+ }
121
+
122
+ /**
123
+ * Harmonize a color with a target, making them work better together
124
+ * Based on Material's blend algorithm
125
+ */
126
+ export function harmonize(design: RGB, source: RGB, factor: number = 0.5): RGB {
127
+ const fromHct = rgbToHct(design);
128
+ const toHct = rgbToHct(source);
129
+
130
+ const diffDegrees = differenceDegrees(fromHct.h, toHct.h);
131
+ const rotationDegrees = Math.min(diffDegrees * factor, 15);
132
+ const outputHue = sanitizeDegreesDouble(
133
+ fromHct.h + rotationDegrees * rotationDirection(fromHct.h, toHct.h),
134
+ );
135
+
136
+ return hctToRgb(hct(outputHue, fromHct.c, fromHct.t));
137
+ }
138
+
139
+ /**
140
+ * Find split-complementary colors
141
+ */
142
+ export function splitComplementary(
143
+ color: RGB,
144
+ angle: number = 30,
145
+ ): [RGB, RGB, RGB] {
146
+ const hctColor = rgbToHct(color);
147
+ const complement = (hctColor.h + 180) % 360;
148
+
149
+ return [
150
+ color,
151
+ hctToRgb(
152
+ hct(sanitizeDegreesDouble(complement - angle), hctColor.c, hctColor.t),
153
+ ),
154
+ hctToRgb(
155
+ hct(sanitizeDegreesDouble(complement + angle), hctColor.c, hctColor.t),
156
+ ),
157
+ ];
158
+ }
159
+
160
+ /**
161
+ * Find tetradic (square) colors - four colors evenly spaced
162
+ */
163
+ export function tetradic(color: RGB): [RGB, RGB, RGB, RGB] {
164
+ const hctColor = rgbToHct(color);
165
+
166
+ return [
167
+ color,
168
+ hctToRgb(hct((hctColor.h + 90) % 360, hctColor.c, hctColor.t)),
169
+ hctToRgb(hct((hctColor.h + 180) % 360, hctColor.c, hctColor.t)),
170
+ hctToRgb(hct((hctColor.h + 270) % 360, hctColor.c, hctColor.t)),
171
+ ];
172
+ }
173
+
174
+ // Helper functions
175
+
176
+ function circularInterpolate(from: number, to: number, amount: number): number {
177
+ const difference = to - from;
178
+ const distance = Math.abs(difference);
179
+
180
+ if (distance > 180) {
181
+ // Take the shorter path around the circle
182
+ if (difference > 0) {
183
+ from += 360;
184
+ } else {
185
+ to += 360;
186
+ }
187
+ }
188
+
189
+ return sanitizeDegreesDouble(from + (to - from) * amount);
190
+ }
191
+
192
+ function differenceDegrees(a: number, b: number): number {
193
+ return Math.abs(((a - b + 180) % 360) - 180);
194
+ }
195
+
196
+ function rotationDirection(from: number, to: number): number {
197
+ const difference = to - from;
198
+ const normalized = ((difference + 180) % 360) - 180;
199
+ return normalized >= 0 ? 1 : -1;
200
+ }
201
+
202
+ function sanitizeDegreesDouble(degrees: number): number {
203
+ return ((degrees % 360) + 360) % 360;
204
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * HCT Class wrapper for Material Color Utilities compatibility
3
+ */
4
+
5
+ import { argbToRgb, rgbToArgb } from "../conversions.js";
6
+ import { hctToRgb, rgbToHct } from "./hct-solver.js";
7
+
8
+ /**
9
+ * HCT class compatible with Material Color Utilities
10
+ */
11
+ export class Hct {
12
+ get chroma(): number {
13
+ return this._chroma;
14
+ }
15
+ /**
16
+ * Set the chroma of this color. Chroma may decrease because chroma has a
17
+ * different maximum for any given hue and tone.
18
+ * @param newChroma 0 <= newChroma < ?
19
+ */
20
+ set chroma(newChroma: number) {
21
+ const rgb = hctToRgb({ c: newChroma, h: this._hue, t: this._tone });
22
+ this._argb = rgbToArgb(rgb);
23
+ const hct = rgbToHct(rgb);
24
+ this._hue = hct.h;
25
+ this._chroma = hct.c;
26
+ this._tone = hct.t;
27
+ }
28
+ get hue(): number {
29
+ return this._hue;
30
+ }
31
+ /**
32
+ * Set the hue of this color. Chroma may decrease because chroma has a
33
+ * different maximum for any given hue and tone.
34
+ * @param newHue 0 <= newHue < 360; invalid values are corrected.
35
+ */
36
+ set hue(newHue: number) {
37
+ const rgb = hctToRgb({ c: this._chroma, h: newHue, t: this._tone });
38
+ this._argb = rgbToArgb(rgb);
39
+ const hct = rgbToHct(rgb);
40
+ this._hue = hct.h;
41
+ this._chroma = hct.c;
42
+ this._tone = hct.t;
43
+ }
44
+
45
+ get tone(): number {
46
+ return this._tone;
47
+ }
48
+
49
+ /**
50
+ * Set the tone of this color. Chroma may decrease because chroma has a
51
+ * different maximum for any given hue and tone.
52
+ * @param newTone 0 <= newTone <= 100; invalid values are corrected.
53
+ */
54
+ set tone(newTone: number) {
55
+ const rgb = hctToRgb({ c: this._chroma, h: this._hue, t: newTone });
56
+ this._argb = rgbToArgb(rgb);
57
+ const hct = rgbToHct(rgb);
58
+ this._hue = hct.h;
59
+ this._chroma = hct.c;
60
+ this._tone = hct.t;
61
+ }
62
+
63
+ private _argb: number;
64
+
65
+ private _chroma: number;
66
+
67
+ private _hue: number;
68
+
69
+ private _tone: number;
70
+
71
+ private constructor(argb: number) {
72
+ this._argb = argb;
73
+ const rgb = argbToRgb(argb);
74
+ const hct = rgbToHct(rgb);
75
+ this._hue = hct.h;
76
+ this._chroma = hct.c;
77
+ this._tone = hct.t;
78
+ }
79
+
80
+ /**
81
+ * Create an HCT color from hue, chroma, and tone.
82
+ * @param hue 0 <= hue < 360; invalid values are corrected.
83
+ * @param chroma 0 <= chroma < ?; Chroma may decrease because chroma has a
84
+ * different maximum for any given hue and tone.
85
+ * @param tone 0 <= tone <= 100; invalid values are corrected.
86
+ * @return HCT representation of a color in default viewing conditions.
87
+ */
88
+ static from(hue: number, chroma: number, tone: number): Hct {
89
+ const rgb = hctToRgb({ c: chroma, h: hue, t: tone });
90
+ const argb = rgbToArgb(rgb);
91
+ return new Hct(argb);
92
+ }
93
+
94
+ /**
95
+ * Create an HCT color from a color.
96
+ * @param argb ARGB representation of a color.
97
+ * @return HCT representation of a color in default viewing conditions
98
+ */
99
+ static fromInt(argb: number): Hct {
100
+ return new Hct(argb);
101
+ }
102
+
103
+ /**
104
+ * @return ARGB representation of an HCT color.
105
+ */
106
+ toInt(): number {
107
+ return this._argb;
108
+ }
109
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * HCT Solver - Converts between HCT and RGB color spaces
3
+ * Based on Material Color Utilities algorithms
4
+ */
5
+
6
+ import type { RGB } from "../types.js";
7
+ import type { HCT } from "./types.js";
8
+
9
+ import { labToXyz, rgbToXyz, xyzToLab, xyzToRgb } from "../conversions.js";
10
+
11
+ /**
12
+ * Create an HCT color from components
13
+ */
14
+ export function hct(hue: number, chroma: number, tone: number): HCT {
15
+ return {
16
+ c: Math.max(0, chroma),
17
+ h: sanitizeDegrees(hue),
18
+ t: Math.max(0, Math.min(100, tone)),
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Convert HCT to RGB using iterative solving
24
+ * This is a simplified version of Material's HCT solver
25
+ */
26
+ export function hctToRgb(hct: HCT): RGB {
27
+ const hue = sanitizeDegrees(hct.h);
28
+ const chroma = Math.max(0, hct.c);
29
+ const tone = Math.max(0, Math.min(100, hct.t));
30
+
31
+ // Special cases
32
+ if (chroma < 0.0001 || tone < 0.5 || tone > 99.5) {
33
+ const gray = (tone / 100) * 255;
34
+ return { b: gray, g: gray, r: gray };
35
+ }
36
+
37
+ // Convert tone to L*
38
+ const lstar = tone;
39
+
40
+ // Use iterative approach to find the right chroma
41
+ let bestRgb: null | RGB = null;
42
+
43
+ // Binary search for achievable chroma
44
+ let low = 0;
45
+ let high = chroma;
46
+ const epsilon = 0.01;
47
+
48
+ while (high - low > epsilon) {
49
+ const mid = (low + high) / 2;
50
+
51
+ // Convert to LAB using approximate chroma
52
+ const a = mid * Math.cos((hue * Math.PI) / 180);
53
+ const b = mid * Math.sin((hue * Math.PI) / 180);
54
+
55
+ // Convert LAB to XYZ to RGB
56
+ const xyz = labToXyz({ a, b, l: lstar });
57
+ const rgb = xyzToRgb(xyz);
58
+
59
+ // Check if RGB is in gamut
60
+ if (isInGamut(rgb)) {
61
+ bestRgb = rgb;
62
+ low = mid;
63
+ } else {
64
+ high = mid;
65
+ }
66
+ }
67
+
68
+ // If we found a valid RGB, use it
69
+ if (bestRgb) {
70
+ return clampRgb(bestRgb);
71
+ }
72
+
73
+ // Fallback: reduce chroma until in gamut
74
+ for (let c = chroma; c >= 0; c -= 1) {
75
+ const a = c * Math.cos((hue * Math.PI) / 180);
76
+ const b = c * Math.sin((hue * Math.PI) / 180);
77
+
78
+ const xyz = labToXyz({ a, b, l: lstar });
79
+ const rgb = xyzToRgb(xyz);
80
+
81
+ if (isInGamut(rgb)) {
82
+ return clampRgb(rgb);
83
+ }
84
+ }
85
+
86
+ // Final fallback: gray
87
+ const gray = (tone / 100) * 255;
88
+ return { b: gray, g: gray, r: gray };
89
+ }
90
+
91
+ /**
92
+ * Get maximum chroma for a given hue and tone
93
+ * This is an approximation - actual max varies
94
+ */
95
+ export function maxChroma(hue: number, tone: number): number {
96
+ // Edge cases
97
+ if (tone <= 0 || tone >= 100) return 0;
98
+
99
+ // Approximate maximum chroma
100
+ // This varies significantly based on hue and tone
101
+ // Red/magenta hues can achieve higher chroma
102
+ const hueCycle = (hue % 360) / 360;
103
+ const redBoost = Math.sin(hueCycle * Math.PI * 2) * 0.2 + 1;
104
+
105
+ // Chroma peaks around middle tones
106
+ const toneFactor = Math.sin((tone / 100) * Math.PI);
107
+
108
+ return toneFactor * 120 * redBoost;
109
+ }
110
+
111
+ /**
112
+ * Convert RGB to HCT
113
+ */
114
+ export function rgbToHct(rgb: RGB): HCT {
115
+ // Convert RGB to LAB
116
+ const xyz = rgbToXyz(rgb);
117
+ const lab = xyzToLab(xyz);
118
+
119
+ // Extract tone from L*
120
+ const tone = lab.l;
121
+
122
+ // Calculate chroma and hue from a* and b*
123
+ const chroma = Math.sqrt(lab.a * lab.a + lab.b * lab.b);
124
+ let hue = (Math.atan2(lab.b, lab.a) * 180) / Math.PI;
125
+
126
+ // Ensure hue is positive
127
+ if (hue < 0) {
128
+ hue += 360;
129
+ }
130
+
131
+ return {
132
+ c: chroma,
133
+ h: sanitizeDegrees(hue),
134
+ t: tone,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Clamp RGB values to valid range
140
+ */
141
+ function clampRgb(rgb: RGB): RGB {
142
+ return {
143
+ b: Math.round(Math.max(0, Math.min(255, rgb.b))),
144
+ g: Math.round(Math.max(0, Math.min(255, rgb.g))),
145
+ r: Math.round(Math.max(0, Math.min(255, rgb.r))),
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Check if RGB values are in valid gamut
151
+ */
152
+ function isInGamut(rgb: RGB): boolean {
153
+ return (
154
+ rgb.r >= 0 &&
155
+ rgb.r <= 255 &&
156
+ rgb.g >= 0 &&
157
+ rgb.g <= 255 &&
158
+ rgb.b >= 0 &&
159
+ rgb.b <= 255
160
+ );
161
+ }
162
+
163
+ /**
164
+ * Sanitize degrees to be in [0, 360)
165
+ */
166
+ function sanitizeDegrees(degrees: number): number {
167
+ return ((degrees % 360) + 360) % 360;
168
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * HCT Color Module
3
+ * Export all HCT-related functionality
4
+ */
5
+
6
+ // Harmonization
7
+ export {
8
+ adjustTemperature,
9
+ analogous,
10
+ blend,
11
+ doubleComplementary,
12
+ gradient,
13
+ harmonize,
14
+ splitComplementary,
15
+ tetradic,
16
+ } from "./harmonization.js";
17
+ // HCT Class (Material Color Utilities compatible)
18
+ export { Hct } from "./hct-class.js";
19
+
20
+ // HCT Solver
21
+ export { hct, hctToRgb, maxChroma, rgbToHct } from "./hct-solver.js";
22
+
23
+ // Tonal Palettes
24
+ export {
25
+ analogousPalette,
26
+ complementaryPalette,
27
+ type CorePalette,
28
+ corePaletteFromRgb,
29
+ EXTENDED_TONES,
30
+ MATERIAL_TONES,
31
+ monochromaticPalette,
32
+ TonalPalette,
33
+ triadicPalette,
34
+ } from "./tonal-palette.js";
35
+
36
+ // Types
37
+ export type { CAM16, HCT, ViewingConditions } from "./types.js";
38
+
39
+ export { STANDARD_CONDITIONS } from "./types.js";