@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,250 @@
1
+ /**
2
+ * Material Design Theme Tools
3
+ * Tools for Material Design 3 color theme generation and management
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ adjustTemperature,
10
+ blend,
11
+ corePaletteFromRgb,
12
+ harmonize,
13
+ parseColor,
14
+ type RGB,
15
+ rgbToHct,
16
+ rgbToHex,
17
+ TonalPalette,
18
+ } from "../color/index.js";
19
+
20
+ /**
21
+ * Generate Material Design 3 Theme
22
+ */
23
+ export const generateMaterialThemeTool = {
24
+ description:
25
+ "Generate a complete Material Design 3 color theme from a source color",
26
+ execute: async (args: {
27
+ includeCustomColors?: boolean;
28
+ sourceColor: string;
29
+ }) => {
30
+ const source = parseColor(args.sourceColor);
31
+ if (!source) {
32
+ return `Invalid color format: ${args.sourceColor}`;
33
+ }
34
+
35
+ const corePalette = corePaletteFromRgb(source);
36
+
37
+ // Generate key colors for light theme
38
+ const lightTheme = {
39
+ background: rgbToHex(corePalette.neutral.tone(99)),
40
+ error: rgbToHex(corePalette.error.tone(40)),
41
+ errorContainer: rgbToHex(corePalette.error.tone(90)),
42
+ onBackground: rgbToHex(corePalette.neutral.tone(10)),
43
+
44
+ onError: rgbToHex(corePalette.error.tone(100)),
45
+ onErrorContainer: rgbToHex(corePalette.error.tone(10)),
46
+ onPrimary: rgbToHex(corePalette.primary.tone(100)),
47
+ onPrimaryContainer: rgbToHex(corePalette.primary.tone(10)),
48
+
49
+ onSecondary: rgbToHex(corePalette.secondary.tone(100)),
50
+ onSecondaryContainer: rgbToHex(corePalette.secondary.tone(10)),
51
+ onSurface: rgbToHex(corePalette.neutral.tone(10)),
52
+ onSurfaceVariant: rgbToHex(corePalette.neutralVariant.tone(30)),
53
+
54
+ onTertiary: rgbToHex(corePalette.tertiary.tone(100)),
55
+ onTertiaryContainer: rgbToHex(corePalette.tertiary.tone(10)),
56
+ outline: rgbToHex(corePalette.neutralVariant.tone(50)),
57
+ primary: rgbToHex(corePalette.primary.tone(40)),
58
+
59
+ primaryContainer: rgbToHex(corePalette.primary.tone(90)),
60
+ secondary: rgbToHex(corePalette.secondary.tone(40)),
61
+ secondaryContainer: rgbToHex(corePalette.secondary.tone(90)),
62
+ surface: rgbToHex(corePalette.neutral.tone(99)),
63
+
64
+ surfaceVariant: rgbToHex(corePalette.neutralVariant.tone(90)),
65
+ tertiary: rgbToHex(corePalette.tertiary.tone(40)),
66
+ tertiaryContainer: rgbToHex(corePalette.tertiary.tone(90)),
67
+ };
68
+
69
+ // Generate key colors for dark theme
70
+ const darkTheme = {
71
+ background: rgbToHex(corePalette.neutral.tone(10)),
72
+ error: rgbToHex(corePalette.error.tone(80)),
73
+ errorContainer: rgbToHex(corePalette.error.tone(30)),
74
+ onBackground: rgbToHex(corePalette.neutral.tone(90)),
75
+
76
+ onError: rgbToHex(corePalette.error.tone(20)),
77
+ onErrorContainer: rgbToHex(corePalette.error.tone(90)),
78
+ onPrimary: rgbToHex(corePalette.primary.tone(20)),
79
+ onPrimaryContainer: rgbToHex(corePalette.primary.tone(90)),
80
+
81
+ onSecondary: rgbToHex(corePalette.secondary.tone(20)),
82
+ onSecondaryContainer: rgbToHex(corePalette.secondary.tone(90)),
83
+ onSurface: rgbToHex(corePalette.neutral.tone(90)),
84
+ onSurfaceVariant: rgbToHex(corePalette.neutralVariant.tone(80)),
85
+
86
+ onTertiary: rgbToHex(corePalette.tertiary.tone(20)),
87
+ onTertiaryContainer: rgbToHex(corePalette.tertiary.tone(90)),
88
+ outline: rgbToHex(corePalette.neutralVariant.tone(60)),
89
+ primary: rgbToHex(corePalette.primary.tone(80)),
90
+
91
+ primaryContainer: rgbToHex(corePalette.primary.tone(30)),
92
+ secondary: rgbToHex(corePalette.secondary.tone(80)),
93
+ secondaryContainer: rgbToHex(corePalette.secondary.tone(30)),
94
+ surface: rgbToHex(corePalette.neutral.tone(10)),
95
+
96
+ surfaceVariant: rgbToHex(corePalette.neutralVariant.tone(30)),
97
+ tertiary: rgbToHex(corePalette.tertiary.tone(80)),
98
+ tertiaryContainer: rgbToHex(corePalette.tertiary.tone(30)),
99
+ };
100
+
101
+ let result = `Material Design 3 Theme
102
+ Source Color: ${args.sourceColor}
103
+
104
+ LIGHT THEME:
105
+ ${Object.entries(lightTheme)
106
+ .map(([key, value]) => ` ${key}: ${value}`)
107
+ .join("\n")}
108
+
109
+ DARK THEME:
110
+ ${Object.entries(darkTheme)
111
+ .map(([key, value]) => ` ${key}: ${value}`)
112
+ .join("\n")}`;
113
+
114
+ if (args.includeCustomColors) {
115
+ result += `\n\nTONAL PALETTES:
116
+ Primary: ${[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100]
117
+ .map((t) => rgbToHex(corePalette.primary.tone(t)))
118
+ .join(", ")}
119
+ Secondary: ${[40, 80].map((t) => rgbToHex(corePalette.secondary.tone(t))).join(", ")}
120
+ Tertiary: ${[40, 80].map((t) => rgbToHex(corePalette.tertiary.tone(t))).join(", ")}`;
121
+ }
122
+
123
+ return result;
124
+ },
125
+ name: "generate_material_theme",
126
+ parameters: z.object({
127
+ includeCustomColors: z
128
+ .boolean()
129
+ .optional()
130
+ .default(false)
131
+ .describe("Include custom color palettes"),
132
+ sourceColor: z.string().describe("Source color for theme generation"),
133
+ }),
134
+ };
135
+
136
+ /**
137
+ * Harmonize Colors Tool
138
+ */
139
+ export const harmonizeColorsTool = {
140
+ description:
141
+ "Harmonize colors to work better together using Material Design algorithms",
142
+ execute: async (args: {
143
+ colors: string[];
144
+ factor?: number;
145
+ method?: "blend" | "harmonize" | "temperature";
146
+ }) => {
147
+ const colors = args.colors.map((c) => parseColor(c));
148
+ if (colors.some((c) => c === null)) {
149
+ return "One or more invalid color formats";
150
+ }
151
+
152
+ const validColors = colors as RGB[];
153
+ const method = args.method || "harmonize";
154
+ const factor = args.factor || 0.5;
155
+ const results: string[] = [];
156
+
157
+ switch (method) {
158
+ case "blend": {
159
+ // Blend all colors together
160
+ let result = validColors[0];
161
+ for (let i = 1; i < validColors.length; i++) {
162
+ result = blend(result, validColors[i], factor);
163
+ }
164
+ results.push(rgbToHex(result));
165
+ break;
166
+ }
167
+
168
+ case "harmonize": {
169
+ // Harmonize all colors with the first one
170
+ const source = validColors[0];
171
+ results.push(rgbToHex(source)); // Keep source unchanged
172
+
173
+ for (let i = 1; i < validColors.length; i++) {
174
+ results.push(rgbToHex(harmonize(validColors[i], source, factor)));
175
+ }
176
+ break;
177
+ }
178
+
179
+ case "temperature": {
180
+ // Adjust temperature of all colors
181
+ const amount = (factor - 0.5) * 2; // Convert to -1 to 1
182
+ for (const color of validColors) {
183
+ results.push(rgbToHex(adjustTemperature(color, amount)));
184
+ }
185
+ break;
186
+ }
187
+ }
188
+
189
+ return `Harmonized Colors (${method}):
190
+ Original: ${args.colors.join(", ")}
191
+ Result: ${results.join(", ")}`;
192
+ },
193
+ name: "harmonize_colors",
194
+ parameters: z.object({
195
+ colors: z
196
+ .array(z.string())
197
+ .min(2)
198
+ .max(10)
199
+ .describe("Array of colors to harmonize"),
200
+ factor: z
201
+ .number()
202
+ .min(0)
203
+ .max(1)
204
+ .optional()
205
+ .default(0.5)
206
+ .describe("Harmonization strength (0-1)"),
207
+ method: z
208
+ .enum(["blend", "harmonize", "temperature"])
209
+ .optional()
210
+ .default("harmonize")
211
+ .describe("Harmonization method"),
212
+ }),
213
+ };
214
+
215
+ /**
216
+ * Generate Tonal Palette Tool
217
+ */
218
+ export const generateTonalPaletteTool = {
219
+ description: "Generate a Material Design tonal palette from a color",
220
+ execute: async (args: { color: string; tones?: number[] }) => {
221
+ const rgb = parseColor(args.color);
222
+ if (!rgb) {
223
+ return `Invalid color format: ${args.color}`;
224
+ }
225
+
226
+ const palette = TonalPalette.fromRgb(rgb);
227
+ const tones = args.tones || [
228
+ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100,
229
+ ];
230
+
231
+ const hct = rgbToHct(rgb);
232
+ const colors = tones.map((tone) => ({
233
+ hex: rgbToHex(palette.tone(tone)),
234
+ tone,
235
+ }));
236
+
237
+ return `Tonal Palette for ${args.color}
238
+ HCT: h=${hct.h.toFixed(1)}°, c=${hct.c.toFixed(1)}, t=${hct.t.toFixed(1)}
239
+
240
+ ${colors.map(({ hex, tone }) => `Tone ${tone}: ${hex}`).join("\n")}`;
241
+ },
242
+ name: "generate_tonal_palette",
243
+ parameters: z.object({
244
+ color: z.string().describe("Base color for palette"),
245
+ tones: z
246
+ .array(z.number())
247
+ .optional()
248
+ .describe("Custom tone values (default: Material standard tones)"),
249
+ }),
250
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Palette Generator Tool
3
+ * Generate color palettes from a base color
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ import { parseColor, rgbToHex, rgbToHsl } from "../color/index.js";
9
+
10
+ export const paletteGeneratorTool = {
11
+ description: "Generate a color palette from a base color",
12
+ execute: async (args: {
13
+ baseColor: string;
14
+ count?: number;
15
+ type?:
16
+ | "analogous"
17
+ | "complementary"
18
+ | "monochromatic"
19
+ | "tetradic"
20
+ | "triadic";
21
+ }) => {
22
+ const base = parseColor(args.baseColor);
23
+ if (!base) {
24
+ return `Invalid color format: ${args.baseColor}`;
25
+ }
26
+
27
+ const type = args.type || "monochromatic";
28
+ const count = args.count || 5;
29
+ const hsl = rgbToHsl(base);
30
+ const palette: string[] = [];
31
+
32
+ switch (type) {
33
+ case "analogous": {
34
+ // Colors adjacent on the color wheel
35
+ const step = 30;
36
+ for (let i = 0; i < count; i++) {
37
+ const h = (hsl.h + (i - Math.floor(count / 2)) * step + 360) % 360;
38
+ const color = parseColor(`hsl(${h}, ${hsl.s}%, ${hsl.l}%)`);
39
+ if (color) palette.push(rgbToHex(color));
40
+ }
41
+ break;
42
+ }
43
+ case "complementary": {
44
+ // Base color and its complement
45
+ palette.push(rgbToHex(base));
46
+ const complement = parseColor(
47
+ `hsl(${(hsl.h + 180) % 360}, ${hsl.s}%, ${hsl.l}%)`,
48
+ );
49
+ if (complement) palette.push(rgbToHex(complement));
50
+
51
+ // Add variations
52
+ for (let i = 2; i < count; i++) {
53
+ const l = hsl.l + (i % 2 === 0 ? 20 : -20);
54
+ const h = i < count / 2 ? hsl.h : (hsl.h + 180) % 360;
55
+ const color = parseColor(
56
+ `hsl(${h}, ${hsl.s}%, ${Math.max(10, Math.min(90, l))}%)`,
57
+ );
58
+ if (color) palette.push(rgbToHex(color));
59
+ }
60
+ break;
61
+ }
62
+ case "monochromatic": {
63
+ // Generate different lightness values
64
+ const step = 80 / (count - 1);
65
+ for (let i = 0; i < count; i++) {
66
+ const l = 10 + i * step;
67
+ const color = parseColor(`hsl(${hsl.h}, ${hsl.s}%, ${l}%)`);
68
+ if (color) palette.push(rgbToHex(color));
69
+ }
70
+ break;
71
+ }
72
+ case "tetradic": {
73
+ // Four colors in rectangle on color wheel
74
+ for (let i = 0; i < Math.min(4, count); i++) {
75
+ const h = (hsl.h + i * 90) % 360;
76
+ const color = parseColor(`hsl(${h}, ${hsl.s}%, ${hsl.l}%)`);
77
+ if (color) palette.push(rgbToHex(color));
78
+ }
79
+ // Add variations for remaining colors
80
+ for (let i = 4; i < count; i++) {
81
+ const baseIdx = i % 4;
82
+ const h = (hsl.h + baseIdx * 90) % 360;
83
+ const l = hsl.l + (i < 8 ? 15 : -15);
84
+ const color = parseColor(
85
+ `hsl(${h}, ${hsl.s}%, ${Math.max(10, Math.min(90, l))}%)`,
86
+ );
87
+ if (color) palette.push(rgbToHex(color));
88
+ }
89
+ break;
90
+ }
91
+ case "triadic": {
92
+ // Three colors evenly spaced on color wheel
93
+ for (let i = 0; i < Math.min(3, count); i++) {
94
+ const h = (hsl.h + i * 120) % 360;
95
+ const color = parseColor(`hsl(${h}, ${hsl.s}%, ${hsl.l}%)`);
96
+ if (color) palette.push(rgbToHex(color));
97
+ }
98
+ // Add variations for remaining colors
99
+ for (let i = 3; i < count; i++) {
100
+ const baseIdx = i % 3;
101
+ const h = (hsl.h + baseIdx * 120) % 360;
102
+ const l = hsl.l + (i < 6 ? 20 : -20);
103
+ const color = parseColor(
104
+ `hsl(${h}, ${hsl.s}%, ${Math.max(10, Math.min(90, l))}%)`,
105
+ );
106
+ if (color) palette.push(rgbToHex(color));
107
+ }
108
+ break;
109
+ }
110
+ }
111
+
112
+ return `Generated ${type} palette:
113
+ ${palette.map((color, i) => `${i + 1}. ${color}`).join("\n")}`;
114
+ },
115
+ name: "generate_palette",
116
+ parameters: z.object({
117
+ baseColor: z.string().describe("Base color for palette generation"),
118
+ count: z
119
+ .number()
120
+ .min(3)
121
+ .max(10)
122
+ .default(5)
123
+ .describe("Number of colors to generate"),
124
+ type: z
125
+ .enum([
126
+ "monochromatic",
127
+ "analogous",
128
+ "complementary",
129
+ "triadic",
130
+ "tetradic",
131
+ ])
132
+ .default("monochromatic")
133
+ .describe("Type of color palette"),
134
+ }),
135
+ };
@@ -0,0 +1,221 @@
1
+ import { z } from "zod";
2
+
3
+ import {
4
+ colorDistance,
5
+ hslToRgb,
6
+ labToRgb,
7
+ parseColor,
8
+ rgbToHex,
9
+ rgbToHsl,
10
+ rgbToLab,
11
+ } from "../color/index.js";
12
+ import { HSL, RGB } from "../color/types.js";
13
+
14
+ export const paletteWithLocksTool = {
15
+ description:
16
+ "Generate a color palette while preserving specific locked colors",
17
+ execute: async (args: {
18
+ colorSpace?: "hsl" | "lab";
19
+ lockedColors: string[];
20
+ mode?: "contrast" | "gradient" | "harmony";
21
+ totalColors: number;
22
+ }) => {
23
+ const {
24
+ colorSpace = "hsl",
25
+ lockedColors,
26
+ mode = "harmony",
27
+ totalColors,
28
+ } = args;
29
+
30
+ if (lockedColors.length >= totalColors) {
31
+ return `Error: Number of locked colors (${lockedColors.length}) must be less than total colors (${totalColors})`;
32
+ }
33
+
34
+ // Parse and validate locked colors
35
+ const parsedLocked: RGB[] = [];
36
+ for (const color of lockedColors) {
37
+ const parsed = parseColor(color);
38
+ if (!parsed) {
39
+ return `Invalid color format: ${color}`;
40
+ }
41
+ parsedLocked.push(parsed);
42
+ }
43
+
44
+ const palette: string[] = [];
45
+ const remainingSlots = totalColors - lockedColors.length;
46
+
47
+ // Add locked colors to palette
48
+ lockedColors.forEach((color) => palette.push(color));
49
+
50
+ switch (mode) {
51
+ case "contrast": {
52
+ // Generate contrasting colors
53
+ for (let i = 0; i < remainingSlots; i++) {
54
+ let bestColor: null | RGB = null;
55
+ let maxMinDistance = 0;
56
+
57
+ // Try random colors and pick the one with maximum minimum distance
58
+ for (let attempt = 0; attempt < 100; attempt++) {
59
+ const candidate: RGB = {
60
+ b: Math.floor(Math.random() * 256),
61
+ g: Math.floor(Math.random() * 256),
62
+ r: Math.floor(Math.random() * 256),
63
+ };
64
+
65
+ let minDistance = Infinity;
66
+ for (const locked of parsedLocked) {
67
+ const dist = colorDistance(candidate, locked, "deltaE2000");
68
+ if (dist < minDistance) minDistance = dist;
69
+ }
70
+
71
+ if (minDistance > maxMinDistance) {
72
+ maxMinDistance = minDistance;
73
+ bestColor = candidate;
74
+ }
75
+ }
76
+
77
+ if (bestColor) {
78
+ palette.push(rgbToHex(bestColor));
79
+ parsedLocked.push(bestColor);
80
+ }
81
+ }
82
+ break;
83
+ }
84
+
85
+ case "gradient": {
86
+ // Create gradient between locked colors
87
+ if (parsedLocked.length < 2) {
88
+ return "Gradient mode requires at least 2 locked colors";
89
+ }
90
+
91
+ const steps = Math.floor(remainingSlots / (parsedLocked.length - 1));
92
+
93
+ for (let i = 0; i < parsedLocked.length - 1; i++) {
94
+ const start = parsedLocked[i];
95
+ const end = parsedLocked[i + 1];
96
+
97
+ for (let step = 1; step <= steps; step++) {
98
+ const t = step / (steps + 1);
99
+
100
+ if (colorSpace === "lab") {
101
+ // Interpolate in LAB space
102
+ const startLab = rgbToLab(start);
103
+ const endLab = rgbToLab(end);
104
+
105
+ const interpolated = labToRgb({
106
+ a: startLab.a + (endLab.a - startLab.a) * t,
107
+ b: startLab.b + (endLab.b - startLab.b) * t,
108
+ l: startLab.l + (endLab.l - startLab.l) * t,
109
+ });
110
+
111
+ palette.push(rgbToHex(interpolated));
112
+ } else {
113
+ // Interpolate in HSL space
114
+ const startHsl = rgbToHsl(start);
115
+ const endHsl = rgbToHsl(end);
116
+
117
+ // Handle hue interpolation (shortest path)
118
+ let hueDiff = endHsl.h - startHsl.h;
119
+ if (hueDiff > 180) hueDiff -= 360;
120
+ if (hueDiff < -180) hueDiff += 360;
121
+
122
+ const interpolated = hslToRgb({
123
+ h: (startHsl.h + hueDiff * t + 360) % 360,
124
+ l: startHsl.l + (endHsl.l - startHsl.l) * t,
125
+ s: startHsl.s + (endHsl.s - startHsl.s) * t,
126
+ });
127
+
128
+ palette.push(rgbToHex(interpolated));
129
+ }
130
+ }
131
+ }
132
+ break;
133
+ }
134
+
135
+ case "harmony": {
136
+ // Generate harmonious colors based on locked colors
137
+ const baseHsl = rgbToHsl(parsedLocked[0]);
138
+ const hueStep = 360 / totalColors;
139
+
140
+ for (let i = 0; i < remainingSlots; i++) {
141
+ let newHue = baseHsl.h;
142
+ let attempts = 0;
143
+ let bestColor: null | RGB = null;
144
+ let maxMinDistance = 0;
145
+
146
+ // Try different hues to find one that's not too close to locked colors
147
+ while (attempts < 36) {
148
+ newHue = (baseHsl.h + attempts * 10) % 360;
149
+ const candidate = hslToRgb({
150
+ h: newHue,
151
+ l: baseHsl.l + (Math.random() - 0.5) * 20,
152
+ s: baseHsl.s + (Math.random() - 0.5) * 20,
153
+ });
154
+
155
+ // Check minimum distance to all locked colors
156
+ let minDistance = Infinity;
157
+ for (const locked of parsedLocked) {
158
+ const dist = colorDistance(candidate, locked, "deltaE2000");
159
+ if (dist < minDistance) minDistance = dist;
160
+ }
161
+
162
+ // Keep the candidate with the largest minimum distance
163
+ if (minDistance > maxMinDistance && minDistance > 10) {
164
+ maxMinDistance = minDistance;
165
+ bestColor = candidate;
166
+ }
167
+
168
+ attempts++;
169
+ }
170
+
171
+ if (bestColor) {
172
+ palette.push(rgbToHex(bestColor));
173
+ parsedLocked.push(bestColor); // Add to locked for next iteration
174
+ }
175
+ }
176
+ break;
177
+ }
178
+ }
179
+
180
+ // Sort palette to have locked colors marked
181
+ const result: string[] = [];
182
+ const lockedSet = new Set(lockedColors.map((c) => c.toLowerCase()));
183
+
184
+ palette.forEach((color) => {
185
+ if (lockedSet.has(color.toLowerCase())) {
186
+ result.push(`${color} (locked)`);
187
+ } else {
188
+ result.push(color);
189
+ }
190
+ });
191
+
192
+ return `Generated palette with ${lockedColors.length} locked colors:
193
+ ${result.map((color, i) => `${i + 1}. ${color}`).join("\n")}
194
+
195
+ Mode: ${mode}
196
+ Color space: ${colorSpace}
197
+ Total colors: ${totalColors}`;
198
+ },
199
+ name: "generate_palette_with_locks",
200
+ parameters: z.object({
201
+ colorSpace: z
202
+ .enum(["hsl", "lab"])
203
+ .default("hsl")
204
+ .describe("Color space for interpolation (affects gradient smoothness)"),
205
+ lockedColors: z
206
+ .array(z.string())
207
+ .min(1)
208
+ .describe("Colors that must be included in the palette"),
209
+ mode: z
210
+ .enum(["harmony", "contrast", "gradient"])
211
+ .default("harmony")
212
+ .describe(
213
+ "Generation mode: harmony (similar), contrast (different), or gradient (smooth transition)",
214
+ ),
215
+ totalColors: z
216
+ .number()
217
+ .min(2)
218
+ .max(20)
219
+ .describe("Total number of colors in the final palette"),
220
+ }),
221
+ };