@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.
- package/.claude/settings.local.json +39 -0
- package/.env +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
- package/.github/pull_request_template.md +97 -0
- package/.github/workflows/ci.yml +127 -0
- package/.github/workflows/deploy-docs.yml +56 -0
- package/.github/workflows/release.yml +99 -0
- package/.mcp.json +12 -0
- package/.prettierignore +1 -0
- package/CLAUDE.md +201 -0
- package/DOCUMENTATION.md +274 -0
- package/GEMINI.md +54 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/demo/content_based_color.png +0 -0
- package/demo/music-player.html +621 -0
- package/demo/podcast-player.html +903 -0
- package/dist/bin/coolors-mcp.d.ts +1 -0
- package/dist/bin/coolors-mcp.js +154 -0
- package/dist/bin/coolors-mcp.js.map +1 -0
- package/dist/bin/server.d.ts +1 -0
- package/dist/bin/server.js +3292 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/chunk-IQ7NN26V.js +114 -0
- package/dist/chunk-IQ7NN26V.js.map +1 -0
- package/dist/chunk-P3ARRKLS.js +1214 -0
- package/dist/chunk-P3ARRKLS.js.map +1 -0
- package/dist/color/index.d.ts +716 -0
- package/dist/color/index.js +153 -0
- package/dist/color/index.js.map +1 -0
- package/dist/coolors-mcp.d.ts +136 -0
- package/dist/coolors-mcp.js +7 -0
- package/dist/coolors-mcp.js.map +1 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/docs/.vitepress/cache/deps/_metadata.json +127 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/docs/.vitepress/cache/deps/dayjs.js +285 -0
- package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/debug.js +468 -0
- package/docs/.vitepress/cache/deps/debug.js.map +7 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
- package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps/vue.js +344 -0
- package/docs/.vitepress/cache/deps/vue.js.map +7 -0
- package/docs/.vitepress/components/ClientGrid.vue +125 -0
- package/docs/.vitepress/components/CodeBlock.vue +231 -0
- package/docs/.vitepress/components/ConfigModal.vue +477 -0
- package/docs/.vitepress/components/DiagramModal.vue +528 -0
- package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
- package/docs/.vitepress/config.js +162 -0
- package/docs/.vitepress/theme/FundingLayout.vue +251 -0
- package/docs/.vitepress/theme/Layout.vue +134 -0
- package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
- package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
- package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
- package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
- package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
- package/docs/.vitepress/theme/custom-app.css +339 -0
- package/docs/.vitepress/theme/custom.css +699 -0
- package/docs/.vitepress/theme/index.js +25 -0
- package/docs/README.md +198 -0
- package/docs/concepts/accessibility.md +473 -0
- package/docs/concepts/color-spaces.md +222 -0
- package/docs/concepts/distance-metrics.md +384 -0
- package/docs/concepts/hct.md +261 -0
- package/docs/concepts/image-analysis.md +396 -0
- package/docs/concepts/material-design.md +306 -0
- package/docs/concepts/theme-matching.md +399 -0
- package/docs/examples/basic-colors.md +490 -0
- package/docs/examples/creating-themes.md +898 -0
- package/docs/examples/css-refactoring.md +824 -0
- package/docs/examples/image-extraction.md +882 -0
- package/docs/getting-started.md +366 -0
- package/docs/index.md +190 -0
- package/docs/installation.md +157 -0
- package/docs/tools/README.md +234 -0
- package/docs/tools/accessibility.md +614 -0
- package/docs/tools/color-operations.md +374 -0
- package/docs/tools/image-extraction.md +624 -0
- package/docs/tools/material-design.md +347 -0
- package/docs/tools/theme-matching.md +552 -0
- package/eslint.config.ts +14 -0
- package/examples/theme-matching.md +113 -0
- package/jsr.json +7 -0
- package/mcp-config.json +8 -0
- package/note.md +35 -0
- package/package.json +122 -0
- package/research_results.md +53 -0
- package/src/bin/coolors-mcp.ts +194 -0
- package/src/bin/server.ts +61 -0
- package/src/color/__tests__/conversions-argb.test.ts +198 -0
- package/src/color/__tests__/extract-colors.test.ts +360 -0
- package/src/color/__tests__/image-utils.test.ts +242 -0
- package/src/color/__tests__/reference-colors.test.ts +278 -0
- package/src/color/__tests__/round-trip.test.ts +197 -0
- package/src/color/conversions.test.ts +402 -0
- package/src/color/conversions.ts +393 -0
- package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
- package/src/color/dislike/dislike-analyzer.ts +114 -0
- package/src/color/extract-colors.ts +228 -0
- package/src/color/hct/__tests__/hct-class.test.ts +232 -0
- package/src/color/hct/harmonization.ts +204 -0
- package/src/color/hct/hct-class.ts +109 -0
- package/src/color/hct/hct-solver.ts +168 -0
- package/src/color/hct/index.ts +39 -0
- package/src/color/hct/tonal-palette.ts +211 -0
- package/src/color/hct/types.ts +88 -0
- package/src/color/image-utils.ts +79 -0
- package/src/color/index.ts +87 -0
- package/src/color/material-theme.ts +157 -0
- package/src/color/metrics.test.ts +276 -0
- package/src/color/metrics.ts +281 -0
- package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
- package/src/color/quantize/lab_point_provider.ts +55 -0
- package/src/color/quantize/point_provider.ts +27 -0
- package/src/color/quantize/quantizer_celebi.ts +51 -0
- package/src/color/quantize/quantizer_celebi_test.ts +71 -0
- package/src/color/quantize/quantizer_map.ts +47 -0
- package/src/color/quantize/quantizer_wsmeans.ts +232 -0
- package/src/color/quantize/quantizer_wu.ts +472 -0
- package/src/color/score/__tests__/score.test.ts +224 -0
- package/src/color/score/score.ts +175 -0
- package/src/color/types.ts +151 -0
- package/src/color/utils/color_utils.ts +292 -0
- package/src/color/utils/math_utils.ts +145 -0
- package/src/color/utils.test.ts +403 -0
- package/src/color/utils.ts +315 -0
- package/src/constants.ts +5 -0
- package/src/coolors-mcp.ts +37 -0
- package/src/examples/addition.ts +333 -0
- package/src/examples/color-demo.ts +125 -0
- package/src/examples/custom-logger.ts +201 -0
- package/src/examples/oauth-server.ts +113 -0
- package/src/examples/session-context.ts +269 -0
- package/src/session.ts +116 -0
- package/src/theme/__tests__/matcher.test.ts +180 -0
- package/src/theme/__tests__/parser.test.ts +148 -0
- package/src/theme/__tests__/refactor.test.ts +224 -0
- package/src/theme/index.ts +34 -0
- package/src/theme/matcher.ts +395 -0
- package/src/theme/parser.ts +392 -0
- package/src/theme/refactor.ts +360 -0
- package/src/theme/types.ts +152 -0
- package/src/tools/__tests__/gradient-generator.test.ts +206 -0
- package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
- package/src/tools/color-conversion.tool.ts +54 -0
- package/src/tools/color-distance.tool.ts +41 -0
- package/src/tools/colors.ts +31 -0
- package/src/tools/contrast-checker.tool.ts +37 -0
- package/src/tools/dislike-analyzer.tool.ts +247 -0
- package/src/tools/gradient-generator.tool.ts +250 -0
- package/src/tools/image-extraction.tools.ts +289 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/material-theme.tools.ts +250 -0
- package/src/tools/palette-generator.tool.ts +135 -0
- package/src/tools/palette-with-locks.tool.ts +221 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/simple-tools.ts +37 -0
- package/src/tools/theme-matching.tools.ts +334 -0
- package/src/types.ts +182 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +8 -0
- 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
|
+
};
|