@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,3292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CoolorsMcp
|
|
4
|
+
} from "../chunk-IQ7NN26V.js";
|
|
5
|
+
import {
|
|
6
|
+
DislikeAnalyzer,
|
|
7
|
+
Hct,
|
|
8
|
+
TonalPalette,
|
|
9
|
+
adjustTemperature,
|
|
10
|
+
blend,
|
|
11
|
+
colorDistance,
|
|
12
|
+
corePaletteFromRgb,
|
|
13
|
+
getContrastRatio,
|
|
14
|
+
harmonize,
|
|
15
|
+
hexToRgb,
|
|
16
|
+
hslToRgb,
|
|
17
|
+
labToRgb,
|
|
18
|
+
parseColor,
|
|
19
|
+
rgbToArgb,
|
|
20
|
+
rgbToHct,
|
|
21
|
+
rgbToHex,
|
|
22
|
+
rgbToHsl,
|
|
23
|
+
rgbToLab
|
|
24
|
+
} from "../chunk-P3ARRKLS.js";
|
|
25
|
+
|
|
26
|
+
// src/tools/color-conversion.tool.ts
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
var colorConversionTool = {
|
|
29
|
+
description: "Convert colors between different formats (hex, rgb, hsl, lab, hct)",
|
|
30
|
+
execute: async (args) => {
|
|
31
|
+
const rgb = parseColor(args.color);
|
|
32
|
+
if (!rgb) {
|
|
33
|
+
return `Invalid color format: ${args.color}`;
|
|
34
|
+
}
|
|
35
|
+
switch (args.to) {
|
|
36
|
+
case "hct": {
|
|
37
|
+
const hct = rgbToHct(rgb);
|
|
38
|
+
return `hct(${hct.h.toFixed(1)}, ${hct.c.toFixed(1)}, ${hct.t.toFixed(1)})`;
|
|
39
|
+
}
|
|
40
|
+
case "hex":
|
|
41
|
+
return rgbToHex(rgb);
|
|
42
|
+
case "hsl": {
|
|
43
|
+
const hsl = rgbToHsl(rgb);
|
|
44
|
+
return `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)`;
|
|
45
|
+
}
|
|
46
|
+
case "lab": {
|
|
47
|
+
const lab = rgbToLab(rgb);
|
|
48
|
+
return `lab(${lab.l.toFixed(2)}, ${lab.a.toFixed(2)}, ${lab.b.toFixed(2)})`;
|
|
49
|
+
}
|
|
50
|
+
case "rgb":
|
|
51
|
+
return `rgb(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)})`;
|
|
52
|
+
default:
|
|
53
|
+
return `Invalid format: ${args.to}`;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
name: "convert_color",
|
|
57
|
+
parameters: z.object({
|
|
58
|
+
color: z.string().describe("Color to convert (hex, rgb(), or hsl())"),
|
|
59
|
+
to: z.enum(["hex", "rgb", "hsl", "lab", "hct"]).describe("Target format")
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/tools/color-distance.tool.ts
|
|
64
|
+
import { z as z2 } from "zod";
|
|
65
|
+
var colorDistanceTool = {
|
|
66
|
+
description: "Calculate perceptual distance between two colors using Delta E 2000",
|
|
67
|
+
execute: async (args) => {
|
|
68
|
+
const rgb1 = parseColor(args.color1);
|
|
69
|
+
const rgb2 = parseColor(args.color2);
|
|
70
|
+
if (!rgb1 || !rgb2) {
|
|
71
|
+
return `Invalid color format: ${!rgb1 ? args.color1 : args.color2}`;
|
|
72
|
+
}
|
|
73
|
+
const distance = colorDistance(rgb1, rgb2, {
|
|
74
|
+
metric: args.metric || "deltaE2000"
|
|
75
|
+
});
|
|
76
|
+
return `Distance between ${args.color1} and ${args.color2}: ${distance.toFixed(2)}`;
|
|
77
|
+
},
|
|
78
|
+
name: "color_distance",
|
|
79
|
+
parameters: z2.object({
|
|
80
|
+
color1: z2.string().describe("First color (hex, rgb, or hsl)"),
|
|
81
|
+
color2: z2.string().describe("Second color (hex, rgb, or hsl)"),
|
|
82
|
+
metric: z2.enum(["euclidean", "deltaE76", "deltaE94", "deltaE2000", "weighted"]).optional().default("deltaE2000").describe("Distance metric to use")
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/tools/colors.ts
|
|
87
|
+
import { formatHex, formatHsl, formatRgb, parse } from "culori";
|
|
88
|
+
import { z as z3 } from "zod";
|
|
89
|
+
|
|
90
|
+
// src/constants.ts
|
|
91
|
+
var ColorFormat = /* @__PURE__ */ ((ColorFormat2) => {
|
|
92
|
+
ColorFormat2["HEX"] = "hex";
|
|
93
|
+
ColorFormat2["HSL"] = "hsl";
|
|
94
|
+
ColorFormat2["RGB"] = "rgb";
|
|
95
|
+
return ColorFormat2;
|
|
96
|
+
})(ColorFormat || {});
|
|
97
|
+
|
|
98
|
+
// src/tools/colors.ts
|
|
99
|
+
var convertColor = {
|
|
100
|
+
description: "Convert a color from one format to another",
|
|
101
|
+
execute: async (args) => {
|
|
102
|
+
const { color, to = "hex" /* HEX */ } = args;
|
|
103
|
+
const parsed = parse(color);
|
|
104
|
+
if (!parsed) {
|
|
105
|
+
throw new Error(`Invalid color: ${color}`);
|
|
106
|
+
}
|
|
107
|
+
switch (to) {
|
|
108
|
+
case "hex" /* HEX */:
|
|
109
|
+
return formatHex(parsed);
|
|
110
|
+
case "hsl" /* HSL */:
|
|
111
|
+
return formatHsl(parsed);
|
|
112
|
+
case "rgb" /* RGB */:
|
|
113
|
+
return formatRgb(parsed);
|
|
114
|
+
default:
|
|
115
|
+
throw new Error(`Invalid output format: ${to}`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
name: "convertColor",
|
|
119
|
+
parameters: z3.object({
|
|
120
|
+
color: z3.string().describe("The color to convert"),
|
|
121
|
+
to: z3.nativeEnum(ColorFormat).optional().describe("The output format")
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/tools/contrast-checker.tool.ts
|
|
126
|
+
import { z as z4 } from "zod";
|
|
127
|
+
var contrastCheckerTool = {
|
|
128
|
+
description: "Check WCAG contrast ratio between two colors",
|
|
129
|
+
execute: async (args) => {
|
|
130
|
+
const fg = parseColor(args.foreground);
|
|
131
|
+
const bg = parseColor(args.background);
|
|
132
|
+
if (!fg || !bg) {
|
|
133
|
+
return `Invalid color format: ${!fg ? args.foreground : args.background}`;
|
|
134
|
+
}
|
|
135
|
+
const ratio = getContrastRatio(fg, bg);
|
|
136
|
+
const aa = ratio >= 4.5;
|
|
137
|
+
const aaLarge = ratio >= 3;
|
|
138
|
+
const aaa = ratio >= 7;
|
|
139
|
+
const aaaLarge = ratio >= 4.5;
|
|
140
|
+
return `Contrast Ratio: ${ratio.toFixed(2)}:1
|
|
141
|
+
WCAG AA: ${aa ? "\u2713 Pass" : "\u2717 Fail"} (normal text)
|
|
142
|
+
WCAG AA: ${aaLarge ? "\u2713 Pass" : "\u2717 Fail"} (large text)
|
|
143
|
+
WCAG AAA: ${aaa ? "\u2713 Pass" : "\u2717 Fail"} (normal text)
|
|
144
|
+
WCAG AAA: ${aaaLarge ? "\u2713 Pass" : "\u2717 Fail"} (large text)`;
|
|
145
|
+
},
|
|
146
|
+
name: "check_contrast",
|
|
147
|
+
parameters: z4.object({
|
|
148
|
+
background: z4.string().describe("Background color"),
|
|
149
|
+
foreground: z4.string().describe("Foreground/text color")
|
|
150
|
+
})
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/tools/dislike-analyzer.tool.ts
|
|
154
|
+
var analyzeColorLikabilityTool = {
|
|
155
|
+
description: "Check if a color is universally disliked (dark yellow-green associated with biological waste) and get a fixed version if needed",
|
|
156
|
+
execute: async (args, _context) => {
|
|
157
|
+
const { autoFix = true, color } = args;
|
|
158
|
+
try {
|
|
159
|
+
const rgb = parseColor(color);
|
|
160
|
+
if (!rgb) {
|
|
161
|
+
return `Error: Invalid color format: ${color}`;
|
|
162
|
+
}
|
|
163
|
+
const argb = rgbToArgb(rgb);
|
|
164
|
+
const hct = Hct.fromInt(argb);
|
|
165
|
+
const isDisliked = DislikeAnalyzer.isDisliked(hct);
|
|
166
|
+
let output = `# Color Likability Analysis
|
|
167
|
+
|
|
168
|
+
`;
|
|
169
|
+
output += `## Input Color: ${color}
|
|
170
|
+
|
|
171
|
+
`;
|
|
172
|
+
output += `### HCT Values
|
|
173
|
+
`;
|
|
174
|
+
output += `- Hue: ${hct.hue.toFixed(1)}\xB0
|
|
175
|
+
`;
|
|
176
|
+
output += `- Chroma: ${hct.chroma.toFixed(1)}
|
|
177
|
+
`;
|
|
178
|
+
output += `- Tone: ${hct.tone.toFixed(1)}
|
|
179
|
+
|
|
180
|
+
`;
|
|
181
|
+
output += `### Analysis Result
|
|
182
|
+
`;
|
|
183
|
+
if (isDisliked) {
|
|
184
|
+
output += `\u26A0\uFE0F **This color is universally disliked**
|
|
185
|
+
|
|
186
|
+
`;
|
|
187
|
+
output += `This color falls in the "bile zone" - dark yellow-greens that are universally associated with biological waste and rotting food.
|
|
188
|
+
|
|
189
|
+
`;
|
|
190
|
+
output += `**Reason:**
|
|
191
|
+
`;
|
|
192
|
+
output += `- Hue is in yellow-green range (90-111\xB0): ${Math.round(hct.hue)}\xB0
|
|
193
|
+
`;
|
|
194
|
+
output += `- Chroma is above threshold (>16): ${Math.round(hct.chroma)}
|
|
195
|
+
`;
|
|
196
|
+
output += `- Tone is dark (<65): ${Math.round(hct.tone)}
|
|
197
|
+
|
|
198
|
+
`;
|
|
199
|
+
if (autoFix) {
|
|
200
|
+
const fixed = DislikeAnalyzer.fixIfDisliked(hct);
|
|
201
|
+
const fixedArgb = fixed.toInt();
|
|
202
|
+
const fixedHex = "#" + [
|
|
203
|
+
fixedArgb >> 16 & 255,
|
|
204
|
+
fixedArgb >> 8 & 255,
|
|
205
|
+
fixedArgb & 255
|
|
206
|
+
].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
207
|
+
output += `### Recommended Fix
|
|
208
|
+
`;
|
|
209
|
+
output += `**Fixed Color:** ${fixedHex}
|
|
210
|
+
`;
|
|
211
|
+
output += `- Hue: ${fixed.hue.toFixed(1)}\xB0 (preserved)
|
|
212
|
+
`;
|
|
213
|
+
output += `- Chroma: ${fixed.chroma.toFixed(1)} (preserved)
|
|
214
|
+
`;
|
|
215
|
+
output += `- Tone: ${fixed.tone.toFixed(1)} (lightened to 70)
|
|
216
|
+
|
|
217
|
+
`;
|
|
218
|
+
output += `The fix preserves the hue and saturation but lightens the color to make it more pleasant.
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
output += `\u2705 **This color is likable**
|
|
223
|
+
|
|
224
|
+
`;
|
|
225
|
+
output += `This color does not fall in the universally disliked range.
|
|
226
|
+
`;
|
|
227
|
+
if (Math.round(hct.hue) >= 90 && Math.round(hct.hue) <= 111) {
|
|
228
|
+
if (Math.round(hct.chroma) <= 16) {
|
|
229
|
+
output += `- Although in yellow-green hue range, the low chroma (${Math.round(hct.chroma)}) makes it neutral and acceptable.
|
|
230
|
+
`;
|
|
231
|
+
} else if (Math.round(hct.tone) >= 65) {
|
|
232
|
+
output += `- Although in yellow-green hue range, the light tone (${Math.round(hct.tone)}) makes it pleasant.
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
output += `- Hue (${Math.round(hct.hue)}\xB0) is outside the problematic yellow-green range.
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return output;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return `Error analyzing color: ${error instanceof Error ? error.message : String(error)}`;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
inputSchema: {
|
|
246
|
+
properties: {
|
|
247
|
+
autoFix: {
|
|
248
|
+
description: "Automatically return fixed version if disliked (default: true)",
|
|
249
|
+
type: "boolean"
|
|
250
|
+
},
|
|
251
|
+
color: {
|
|
252
|
+
description: "Color to analyze (hex, rgb, hsl, etc.)",
|
|
253
|
+
type: "string"
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
required: ["color"],
|
|
257
|
+
type: "object"
|
|
258
|
+
},
|
|
259
|
+
name: "analyze_color_likability"
|
|
260
|
+
};
|
|
261
|
+
var fixDislikedColorsBatchTool = {
|
|
262
|
+
description: "Analyze and fix multiple colors, returning only liked versions",
|
|
263
|
+
execute: async (args, _context) => {
|
|
264
|
+
const { colors, includeAnalysis = false } = args;
|
|
265
|
+
try {
|
|
266
|
+
const results = [];
|
|
267
|
+
let dislikedCount = 0;
|
|
268
|
+
for (const color of colors) {
|
|
269
|
+
const rgb = parseColor(color);
|
|
270
|
+
if (!rgb) {
|
|
271
|
+
results.push({
|
|
272
|
+
error: "Invalid color format",
|
|
273
|
+
original: color
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const argb = rgbToArgb(rgb);
|
|
278
|
+
const hct = Hct.fromInt(argb);
|
|
279
|
+
const isDisliked = DislikeAnalyzer.isDisliked(hct);
|
|
280
|
+
if (isDisliked) {
|
|
281
|
+
dislikedCount++;
|
|
282
|
+
const fixed = DislikeAnalyzer.fixIfDisliked(hct);
|
|
283
|
+
const fixedArgb = fixed.toInt();
|
|
284
|
+
const fixedHex = "#" + [
|
|
285
|
+
fixedArgb >> 16 & 255,
|
|
286
|
+
fixedArgb >> 8 & 255,
|
|
287
|
+
fixedArgb & 255
|
|
288
|
+
].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
289
|
+
results.push({
|
|
290
|
+
fixed: fixedHex,
|
|
291
|
+
hct: includeAnalysis ? {
|
|
292
|
+
fixed: { c: fixed.chroma, h: fixed.hue, t: fixed.tone },
|
|
293
|
+
original: { c: hct.chroma, h: hct.hue, t: hct.tone }
|
|
294
|
+
} : void 0,
|
|
295
|
+
original: color,
|
|
296
|
+
wasDisliked: true
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
results.push({
|
|
300
|
+
fixed: color,
|
|
301
|
+
hct: includeAnalysis ? {
|
|
302
|
+
c: hct.chroma,
|
|
303
|
+
h: hct.hue,
|
|
304
|
+
t: hct.tone
|
|
305
|
+
} : void 0,
|
|
306
|
+
original: color,
|
|
307
|
+
wasDisliked: false
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let output = `# Batch Color Likability Analysis
|
|
312
|
+
|
|
313
|
+
`;
|
|
314
|
+
output += `## Summary
|
|
315
|
+
`;
|
|
316
|
+
output += `- Total colors: ${colors.length}
|
|
317
|
+
`;
|
|
318
|
+
output += `- Disliked colors found: ${dislikedCount}
|
|
319
|
+
`;
|
|
320
|
+
output += `- Percentage disliked: ${(dislikedCount / colors.length * 100).toFixed(1)}%
|
|
321
|
+
|
|
322
|
+
`;
|
|
323
|
+
output += `## Results
|
|
324
|
+
|
|
325
|
+
`;
|
|
326
|
+
for (const result of results) {
|
|
327
|
+
if (result.error) {
|
|
328
|
+
output += `- **${result.original}**: \u274C ${result.error}
|
|
329
|
+
`;
|
|
330
|
+
} else if (result.wasDisliked) {
|
|
331
|
+
output += `- **${result.original}** \u2192 **${result.fixed}** (fixed)
|
|
332
|
+
`;
|
|
333
|
+
if (includeAnalysis && result.hct) {
|
|
334
|
+
output += ` - Original HCT: (${result.hct.original.h.toFixed(0)}\xB0, ${result.hct.original.c.toFixed(0)}, ${result.hct.original.t.toFixed(0)})
|
|
335
|
+
`;
|
|
336
|
+
output += ` - Fixed HCT: (${result.hct.fixed.h.toFixed(0)}\xB0, ${result.hct.fixed.c.toFixed(0)}, ${result.hct.fixed.t.toFixed(0)})
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
output += `- **${result.original}** \u2713 (already liked)
|
|
341
|
+
`;
|
|
342
|
+
if (includeAnalysis && result.hct) {
|
|
343
|
+
output += ` - HCT: (${result.hct.h.toFixed(0)}\xB0, ${result.hct.c.toFixed(0)}, ${result.hct.t.toFixed(0)})
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (dislikedCount > 0) {
|
|
349
|
+
output += `
|
|
350
|
+
## Note
|
|
351
|
+
`;
|
|
352
|
+
output += `Disliked colors have been automatically fixed by lightening their tone to 70 while preserving hue and chroma. `;
|
|
353
|
+
output += `This makes them more pleasant while maintaining their essential character.
|
|
354
|
+
`;
|
|
355
|
+
}
|
|
356
|
+
return output;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return `Error processing colors: ${error instanceof Error ? error.message : String(error)}`;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
inputSchema: {
|
|
362
|
+
properties: {
|
|
363
|
+
colors: {
|
|
364
|
+
description: "Array of colors to analyze (hex, rgb, hsl, etc.)",
|
|
365
|
+
items: {
|
|
366
|
+
type: "string"
|
|
367
|
+
},
|
|
368
|
+
type: "array"
|
|
369
|
+
},
|
|
370
|
+
includeAnalysis: {
|
|
371
|
+
description: "Include detailed analysis for each color (default: false)",
|
|
372
|
+
type: "boolean"
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
required: ["colors"],
|
|
376
|
+
type: "object"
|
|
377
|
+
},
|
|
378
|
+
name: "fix_disliked_colors_batch"
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/tools/gradient-generator.tool.ts
|
|
382
|
+
import { z as z5 } from "zod";
|
|
383
|
+
var gradientGeneratorTool = {
|
|
384
|
+
description: "Generate a smooth gradient between colors with multiple interpolation methods",
|
|
385
|
+
execute: async (args) => {
|
|
386
|
+
const {
|
|
387
|
+
colors,
|
|
388
|
+
easing = "linear",
|
|
389
|
+
format = "array",
|
|
390
|
+
interpolation = "lab",
|
|
391
|
+
steps
|
|
392
|
+
} = args;
|
|
393
|
+
if (colors.length < 2) {
|
|
394
|
+
return "Error: At least 2 colors are required for a gradient";
|
|
395
|
+
}
|
|
396
|
+
const parsedColors = [];
|
|
397
|
+
for (const color of colors) {
|
|
398
|
+
const parsed = parseColor(color);
|
|
399
|
+
if (!parsed) {
|
|
400
|
+
return `Invalid color format: ${color}`;
|
|
401
|
+
}
|
|
402
|
+
parsedColors.push(parsed);
|
|
403
|
+
}
|
|
404
|
+
const applyEasing = (t) => {
|
|
405
|
+
switch (easing) {
|
|
406
|
+
case "ease-in":
|
|
407
|
+
return t * t;
|
|
408
|
+
case "ease-in-out":
|
|
409
|
+
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
410
|
+
case "ease-out":
|
|
411
|
+
return t * (2 - t);
|
|
412
|
+
default:
|
|
413
|
+
return t;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const gradient = [];
|
|
417
|
+
const segmentSteps = Math.floor(steps / (colors.length - 1));
|
|
418
|
+
const extraSteps = steps % (colors.length - 1);
|
|
419
|
+
for (let segment = 0; segment < colors.length - 1; segment++) {
|
|
420
|
+
const start = parsedColors[segment];
|
|
421
|
+
const end = parsedColors[segment + 1];
|
|
422
|
+
const currentSteps = segmentSteps + (segment < extraSteps ? 1 : 0);
|
|
423
|
+
for (let step = 0; step < currentSteps; step++) {
|
|
424
|
+
const t = applyEasing(step / currentSteps);
|
|
425
|
+
let interpolatedColor;
|
|
426
|
+
switch (interpolation) {
|
|
427
|
+
case "hct": {
|
|
428
|
+
const startHct = Hct.fromInt(
|
|
429
|
+
255 << 24 | start.r << 16 | start.g << 8 | start.b
|
|
430
|
+
);
|
|
431
|
+
const endHct = Hct.fromInt(
|
|
432
|
+
255 << 24 | end.r << 16 | end.g << 8 | end.b
|
|
433
|
+
);
|
|
434
|
+
let hueDiff = endHct.hue - startHct.hue;
|
|
435
|
+
if (hueDiff > 180) hueDiff -= 360;
|
|
436
|
+
if (hueDiff < -180) hueDiff += 360;
|
|
437
|
+
const interpolatedHct = Hct.from(
|
|
438
|
+
(startHct.hue + hueDiff * t + 360) % 360,
|
|
439
|
+
startHct.chroma + (endHct.chroma - startHct.chroma) * t,
|
|
440
|
+
startHct.tone + (endHct.tone - startHct.tone) * t
|
|
441
|
+
);
|
|
442
|
+
const argb = interpolatedHct.toInt();
|
|
443
|
+
interpolatedColor = {
|
|
444
|
+
b: argb & 255,
|
|
445
|
+
g: argb >> 8 & 255,
|
|
446
|
+
r: argb >> 16 & 255
|
|
447
|
+
};
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case "hsl": {
|
|
451
|
+
const startHsl = rgbToHsl(start);
|
|
452
|
+
const endHsl = rgbToHsl(end);
|
|
453
|
+
let hueDiff = endHsl.h - startHsl.h;
|
|
454
|
+
if (hueDiff > 180) hueDiff -= 360;
|
|
455
|
+
if (hueDiff < -180) hueDiff += 360;
|
|
456
|
+
interpolatedColor = hslToRgb({
|
|
457
|
+
h: (startHsl.h + hueDiff * t + 360) % 360,
|
|
458
|
+
l: startHsl.l + (endHsl.l - startHsl.l) * t,
|
|
459
|
+
s: startHsl.s + (endHsl.s - startHsl.s) * t
|
|
460
|
+
});
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case "lab": {
|
|
464
|
+
const startLab = rgbToLab(start);
|
|
465
|
+
const endLab = rgbToLab(end);
|
|
466
|
+
interpolatedColor = labToRgb({
|
|
467
|
+
a: startLab.a + (endLab.a - startLab.a) * t,
|
|
468
|
+
b: startLab.b + (endLab.b - startLab.b) * t,
|
|
469
|
+
l: startLab.l + (endLab.l - startLab.l) * t
|
|
470
|
+
});
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case "lch": {
|
|
474
|
+
const startLab = rgbToLab(start);
|
|
475
|
+
const endLab = rgbToLab(end);
|
|
476
|
+
const startL = startLab.l;
|
|
477
|
+
const startC = Math.sqrt(
|
|
478
|
+
startLab.a * startLab.a + startLab.b * startLab.b
|
|
479
|
+
);
|
|
480
|
+
const startH = Math.atan2(startLab.b, startLab.a) * 180 / Math.PI;
|
|
481
|
+
const endL = endLab.l;
|
|
482
|
+
const endC = Math.sqrt(endLab.a * endLab.a + endLab.b * endLab.b);
|
|
483
|
+
const endH = Math.atan2(endLab.b, endLab.a) * 180 / Math.PI;
|
|
484
|
+
let hueDiff = endH - startH;
|
|
485
|
+
if (hueDiff > 180) hueDiff -= 360;
|
|
486
|
+
if (hueDiff < -180) hueDiff += 360;
|
|
487
|
+
const l = startL + (endL - startL) * t;
|
|
488
|
+
const c = startC + (endC - startC) * t;
|
|
489
|
+
const h = (startH + hueDiff * t) * Math.PI / 180;
|
|
490
|
+
interpolatedColor = labToRgb({
|
|
491
|
+
a: c * Math.cos(h),
|
|
492
|
+
b: c * Math.sin(h),
|
|
493
|
+
l
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case "rgb": {
|
|
498
|
+
interpolatedColor = {
|
|
499
|
+
b: Math.round(start.b + (end.b - start.b) * t),
|
|
500
|
+
g: Math.round(start.g + (end.g - start.g) * t),
|
|
501
|
+
r: Math.round(start.r + (end.r - start.r) * t)
|
|
502
|
+
};
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
default:
|
|
506
|
+
interpolatedColor = start;
|
|
507
|
+
}
|
|
508
|
+
gradient.push(interpolatedColor);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
gradient.push(parsedColors[parsedColors.length - 1]);
|
|
512
|
+
const hexColors = gradient.map(rgbToHex);
|
|
513
|
+
switch (format) {
|
|
514
|
+
case "css-linear": {
|
|
515
|
+
const stops = hexColors.map((color, i) => {
|
|
516
|
+
const percent = i / (hexColors.length - 1) * 100;
|
|
517
|
+
return `${color} ${percent.toFixed(1)}%`;
|
|
518
|
+
});
|
|
519
|
+
return `linear-gradient(90deg, ${stops.join(", ")})`;
|
|
520
|
+
}
|
|
521
|
+
case "css-radial": {
|
|
522
|
+
const stops = hexColors.map((color, i) => {
|
|
523
|
+
const percent = i / (hexColors.length - 1) * 100;
|
|
524
|
+
return `${color} ${percent.toFixed(1)}%`;
|
|
525
|
+
});
|
|
526
|
+
return `radial-gradient(circle, ${stops.join(", ")})`;
|
|
527
|
+
}
|
|
528
|
+
case "hex":
|
|
529
|
+
return hexColors.join(", ");
|
|
530
|
+
case "array":
|
|
531
|
+
default:
|
|
532
|
+
return `Generated gradient with ${steps} steps:
|
|
533
|
+
${hexColors.map((color, i) => `${i + 1}. ${color}`).join("\n")}
|
|
534
|
+
|
|
535
|
+
Interpolation: ${interpolation}
|
|
536
|
+
Easing: ${easing}
|
|
537
|
+
Input colors: ${colors.join(" \u2192 ")}`;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
name: "generate_gradient",
|
|
541
|
+
parameters: z5.object({
|
|
542
|
+
colors: z5.array(z5.string()).min(2).describe("Colors to create gradient between (minimum 2)"),
|
|
543
|
+
easing: z5.enum(["linear", "ease-in", "ease-out", "ease-in-out"]).default("linear").describe("Easing function for gradient transition"),
|
|
544
|
+
format: z5.enum(["hex", "css-linear", "css-radial", "array"]).default("array").describe("Output format for the gradient"),
|
|
545
|
+
interpolation: z5.enum(["rgb", "hsl", "lab", "lch", "hct"]).default("lab").describe(
|
|
546
|
+
"Color space for interpolation (lab/lch/hct are perceptually smooth)"
|
|
547
|
+
),
|
|
548
|
+
steps: z5.number().min(3).max(100).describe("Number of colors in the gradient")
|
|
549
|
+
})
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/color/utils/math_utils.ts
|
|
553
|
+
function clampInt(min, max, input) {
|
|
554
|
+
if (input < min) {
|
|
555
|
+
return min;
|
|
556
|
+
} else if (input > max) {
|
|
557
|
+
return max;
|
|
558
|
+
}
|
|
559
|
+
return input;
|
|
560
|
+
}
|
|
561
|
+
function differenceDegrees(a, b) {
|
|
562
|
+
return 180 - Math.abs(Math.abs(a - b) - 180);
|
|
563
|
+
}
|
|
564
|
+
function sanitizeDegreesInt(degrees) {
|
|
565
|
+
degrees = degrees % 360;
|
|
566
|
+
if (degrees < 0) {
|
|
567
|
+
degrees = degrees + 360;
|
|
568
|
+
}
|
|
569
|
+
return degrees;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/color/utils/color_utils.ts
|
|
573
|
+
var SRGB_TO_XYZ = [
|
|
574
|
+
[0.41233895, 0.35762064, 0.18051042],
|
|
575
|
+
[0.2126, 0.7152, 0.0722],
|
|
576
|
+
[0.01932141, 0.11916382, 0.95034478]
|
|
577
|
+
];
|
|
578
|
+
var XYZ_TO_SRGB = [
|
|
579
|
+
[3.2413774792388685, -1.5376652402851851, -0.49885366846268053],
|
|
580
|
+
[-0.9691452513005321, 1.8758853451067872, 0.04156585616912061],
|
|
581
|
+
[0.05562093689691305, -0.20395524564742123, 1.0571799111220335]
|
|
582
|
+
];
|
|
583
|
+
var WHITE_POINT_D65 = [95.047, 100, 108.883];
|
|
584
|
+
function alphaFromArgb(argb) {
|
|
585
|
+
return argb >> 24 & 255;
|
|
586
|
+
}
|
|
587
|
+
function argbFromLab(l, a, b) {
|
|
588
|
+
const whitePoint = WHITE_POINT_D65;
|
|
589
|
+
const fy = (l + 16) / 116;
|
|
590
|
+
const fx = a / 500 + fy;
|
|
591
|
+
const fz = fy - b / 200;
|
|
592
|
+
const xNormalized = labInvf(fx);
|
|
593
|
+
const yNormalized = labInvf(fy);
|
|
594
|
+
const zNormalized = labInvf(fz);
|
|
595
|
+
const x = xNormalized * whitePoint[0];
|
|
596
|
+
const y = yNormalized * whitePoint[1];
|
|
597
|
+
const z10 = zNormalized * whitePoint[2];
|
|
598
|
+
return argbFromXyz(x, y, z10);
|
|
599
|
+
}
|
|
600
|
+
function argbFromRgb(red, green, blue) {
|
|
601
|
+
return (255 << 24 | (red & 255) << 16 | (green & 255) << 8 | blue & 255) >>> 0;
|
|
602
|
+
}
|
|
603
|
+
function argbFromXyz(x, y, z10) {
|
|
604
|
+
const matrix = XYZ_TO_SRGB;
|
|
605
|
+
const linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z10;
|
|
606
|
+
const linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z10;
|
|
607
|
+
const linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z10;
|
|
608
|
+
const r = delinearized(linearR);
|
|
609
|
+
const g = delinearized(linearG);
|
|
610
|
+
const b = delinearized(linearB);
|
|
611
|
+
return argbFromRgb(r, g, b);
|
|
612
|
+
}
|
|
613
|
+
function blueFromArgb(argb) {
|
|
614
|
+
return argb & 255;
|
|
615
|
+
}
|
|
616
|
+
function delinearized(rgbComponent) {
|
|
617
|
+
const normalized = rgbComponent / 100;
|
|
618
|
+
let delinearized2 = 0;
|
|
619
|
+
if (normalized <= 31308e-7) {
|
|
620
|
+
delinearized2 = normalized * 12.92;
|
|
621
|
+
} else {
|
|
622
|
+
delinearized2 = 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;
|
|
623
|
+
}
|
|
624
|
+
return clampInt(0, 255, Math.round(delinearized2 * 255));
|
|
625
|
+
}
|
|
626
|
+
function greenFromArgb(argb) {
|
|
627
|
+
return argb >> 8 & 255;
|
|
628
|
+
}
|
|
629
|
+
function labFromArgb(argb) {
|
|
630
|
+
const linearR = linearized(redFromArgb(argb));
|
|
631
|
+
const linearG = linearized(greenFromArgb(argb));
|
|
632
|
+
const linearB = linearized(blueFromArgb(argb));
|
|
633
|
+
const matrix = SRGB_TO_XYZ;
|
|
634
|
+
const x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB;
|
|
635
|
+
const y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB;
|
|
636
|
+
const z10 = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB;
|
|
637
|
+
const whitePoint = WHITE_POINT_D65;
|
|
638
|
+
const xNormalized = x / whitePoint[0];
|
|
639
|
+
const yNormalized = y / whitePoint[1];
|
|
640
|
+
const zNormalized = z10 / whitePoint[2];
|
|
641
|
+
const fx = labF(xNormalized);
|
|
642
|
+
const fy = labF(yNormalized);
|
|
643
|
+
const fz = labF(zNormalized);
|
|
644
|
+
const l = 116 * fy - 16;
|
|
645
|
+
const a = 500 * (fx - fy);
|
|
646
|
+
const b = 200 * (fy - fz);
|
|
647
|
+
return [l, a, b];
|
|
648
|
+
}
|
|
649
|
+
function linearized(rgbComponent) {
|
|
650
|
+
const normalized = rgbComponent / 255;
|
|
651
|
+
if (normalized <= 0.040449936) {
|
|
652
|
+
return normalized / 12.92 * 100;
|
|
653
|
+
} else {
|
|
654
|
+
return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function redFromArgb(argb) {
|
|
658
|
+
return argb >> 16 & 255;
|
|
659
|
+
}
|
|
660
|
+
function labF(t) {
|
|
661
|
+
const e = 216 / 24389;
|
|
662
|
+
const kappa = 24389 / 27;
|
|
663
|
+
if (t > e) {
|
|
664
|
+
return Math.pow(t, 1 / 3);
|
|
665
|
+
} else {
|
|
666
|
+
return (kappa * t + 16) / 116;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function labInvf(ft) {
|
|
670
|
+
const e = 216 / 24389;
|
|
671
|
+
const kappa = 24389 / 27;
|
|
672
|
+
const ft3 = ft * ft * ft;
|
|
673
|
+
if (ft3 > e) {
|
|
674
|
+
return ft3;
|
|
675
|
+
} else {
|
|
676
|
+
return (116 * ft - 16) / kappa;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/color/image-utils.ts
|
|
681
|
+
function filterExtremeTones(pixels) {
|
|
682
|
+
return pixels.filter((pixel) => {
|
|
683
|
+
const r = redFromArgb(pixel);
|
|
684
|
+
const g = greenFromArgb(pixel);
|
|
685
|
+
const b = blueFromArgb(pixel);
|
|
686
|
+
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
687
|
+
return luminance > 12.75 && luminance < 242.25;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function imageDataToPixels(imageData) {
|
|
691
|
+
const pixels = [];
|
|
692
|
+
const data = imageData.data;
|
|
693
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
694
|
+
const r = data[i];
|
|
695
|
+
const g = data[i + 1];
|
|
696
|
+
const b = data[i + 2];
|
|
697
|
+
const a = data[i + 3];
|
|
698
|
+
if (a < 255 * 0.01) continue;
|
|
699
|
+
const argb = argbFromRgb(r, g, b);
|
|
700
|
+
pixels.push(argb);
|
|
701
|
+
}
|
|
702
|
+
return pixels;
|
|
703
|
+
}
|
|
704
|
+
function samplePixels(pixels, maxPixels = 1e4) {
|
|
705
|
+
if (pixels.length <= maxPixels) {
|
|
706
|
+
return pixels;
|
|
707
|
+
}
|
|
708
|
+
const sampled = [];
|
|
709
|
+
const step = Math.ceil(pixels.length / maxPixels);
|
|
710
|
+
for (let i = 0; i < pixels.length; i += step) {
|
|
711
|
+
sampled.push(pixels[i]);
|
|
712
|
+
}
|
|
713
|
+
return sampled;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/color/quantize/lab_point_provider.ts
|
|
717
|
+
var LabPointProvider = class {
|
|
718
|
+
/**
|
|
719
|
+
* Standard CIE 1976 delta E formula also takes the square root, unneeded
|
|
720
|
+
* here. This method is used by quantization algorithms to compare distance,
|
|
721
|
+
* and the relative ordering is the same, with or without a square root.
|
|
722
|
+
*
|
|
723
|
+
* This relatively minor optimization is helpful because this method is
|
|
724
|
+
* called at least once for each pixel in an image.
|
|
725
|
+
*/
|
|
726
|
+
distance(from, to) {
|
|
727
|
+
const dL = from[0] - to[0];
|
|
728
|
+
const dA = from[1] - to[1];
|
|
729
|
+
const dB = from[2] - to[2];
|
|
730
|
+
return dL * dL + dA * dA + dB * dB;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Convert a color represented in ARGB to a 3-element array of L*a*b*
|
|
734
|
+
* coordinates of the color.
|
|
735
|
+
*/
|
|
736
|
+
fromInt(argb) {
|
|
737
|
+
return labFromArgb(argb);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Convert a 3-element array to a color represented in ARGB.
|
|
741
|
+
*/
|
|
742
|
+
toInt(point) {
|
|
743
|
+
return argbFromLab(point[0], point[1], point[2]);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// src/color/quantize/quantizer_wsmeans.ts
|
|
748
|
+
var MAX_ITERATIONS = 10;
|
|
749
|
+
var MIN_MOVEMENT_DISTANCE = 3;
|
|
750
|
+
var DistanceAndIndex = class {
|
|
751
|
+
distance = -1;
|
|
752
|
+
index = -1;
|
|
753
|
+
};
|
|
754
|
+
var QuantizerWsmeans = class {
|
|
755
|
+
/**
|
|
756
|
+
* @param inputPixels Colors in ARGB format.
|
|
757
|
+
* @param startingClusters Defines the initial state of the quantizer. Passing
|
|
758
|
+
* an empty array is fine, the implementation will create its own initial
|
|
759
|
+
* state that leads to reproducible results for the same inputs.
|
|
760
|
+
* Passing an array that is the result of Wu quantization leads to higher
|
|
761
|
+
* quality results.
|
|
762
|
+
* @param maxColors The number of colors to divide the image into. A lower
|
|
763
|
+
* number of colors may be returned.
|
|
764
|
+
* @return Colors in ARGB format.
|
|
765
|
+
*/
|
|
766
|
+
static quantize(inputPixels, startingClusters, maxColors) {
|
|
767
|
+
const pixelToCount = /* @__PURE__ */ new Map();
|
|
768
|
+
const points = new Array();
|
|
769
|
+
const pixels = new Array();
|
|
770
|
+
const pointProvider = new LabPointProvider();
|
|
771
|
+
let pointCount = 0;
|
|
772
|
+
for (let i = 0; i < inputPixels.length; i++) {
|
|
773
|
+
const inputPixel = inputPixels[i];
|
|
774
|
+
const pixelCount = pixelToCount.get(inputPixel);
|
|
775
|
+
if (pixelCount === void 0) {
|
|
776
|
+
pointCount++;
|
|
777
|
+
points.push(pointProvider.fromInt(inputPixel));
|
|
778
|
+
pixels.push(inputPixel);
|
|
779
|
+
pixelToCount.set(inputPixel, 1);
|
|
780
|
+
} else {
|
|
781
|
+
pixelToCount.set(inputPixel, pixelCount + 1);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const counts = new Array();
|
|
785
|
+
for (let i = 0; i < pointCount; i++) {
|
|
786
|
+
const pixel = pixels[i];
|
|
787
|
+
const count = pixelToCount.get(pixel);
|
|
788
|
+
if (count !== void 0) {
|
|
789
|
+
counts[i] = count;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
let clusterCount = Math.min(maxColors, pointCount);
|
|
793
|
+
if (startingClusters.length > 0) {
|
|
794
|
+
clusterCount = Math.min(clusterCount, startingClusters.length);
|
|
795
|
+
}
|
|
796
|
+
const clusters = new Array();
|
|
797
|
+
for (let i = 0; i < startingClusters.length; i++) {
|
|
798
|
+
clusters.push(pointProvider.fromInt(startingClusters[i]));
|
|
799
|
+
}
|
|
800
|
+
const additionalClustersNeeded = clusterCount - clusters.length;
|
|
801
|
+
if (startingClusters.length === 0 && additionalClustersNeeded > 0) {
|
|
802
|
+
for (let i = 0; i < additionalClustersNeeded; i++) {
|
|
803
|
+
const l = Math.random() * 100;
|
|
804
|
+
const a = Math.random() * (100 - -100 + 1) + -100;
|
|
805
|
+
const b = Math.random() * (100 - -100 + 1) + -100;
|
|
806
|
+
clusters.push([l, a, b]);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const clusterIndices = new Array();
|
|
810
|
+
for (let i = 0; i < pointCount; i++) {
|
|
811
|
+
clusterIndices.push(Math.floor(Math.random() * clusterCount));
|
|
812
|
+
}
|
|
813
|
+
const indexMatrix = new Array();
|
|
814
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
815
|
+
indexMatrix.push(new Array());
|
|
816
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
817
|
+
indexMatrix[i].push(0);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const distanceToIndexMatrix = new Array();
|
|
821
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
822
|
+
distanceToIndexMatrix.push(new Array());
|
|
823
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
824
|
+
distanceToIndexMatrix[i].push(new DistanceAndIndex());
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const pixelCountSums = new Array();
|
|
828
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
829
|
+
pixelCountSums.push(0);
|
|
830
|
+
}
|
|
831
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
832
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
833
|
+
for (let j = i + 1; j < clusterCount; j++) {
|
|
834
|
+
const distance = pointProvider.distance(clusters[i], clusters[j]);
|
|
835
|
+
distanceToIndexMatrix[j][i].distance = distance;
|
|
836
|
+
distanceToIndexMatrix[j][i].index = i;
|
|
837
|
+
distanceToIndexMatrix[i][j].distance = distance;
|
|
838
|
+
distanceToIndexMatrix[i][j].index = j;
|
|
839
|
+
}
|
|
840
|
+
distanceToIndexMatrix[i].sort();
|
|
841
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
842
|
+
indexMatrix[i][j] = distanceToIndexMatrix[i][j].index;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
let pointsMoved = 0;
|
|
846
|
+
for (let i = 0; i < pointCount; i++) {
|
|
847
|
+
const point = points[i];
|
|
848
|
+
const previousClusterIndex = clusterIndices[i];
|
|
849
|
+
const previousCluster = clusters[previousClusterIndex];
|
|
850
|
+
const previousDistance = pointProvider.distance(point, previousCluster);
|
|
851
|
+
let minimumDistance = previousDistance;
|
|
852
|
+
let newClusterIndex = -1;
|
|
853
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
854
|
+
if (distanceToIndexMatrix[previousClusterIndex][j].distance >= 4 * previousDistance) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const distance = pointProvider.distance(point, clusters[j]);
|
|
858
|
+
if (distance < minimumDistance) {
|
|
859
|
+
minimumDistance = distance;
|
|
860
|
+
newClusterIndex = j;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (newClusterIndex !== -1) {
|
|
864
|
+
const distanceChange = Math.abs(
|
|
865
|
+
Math.sqrt(minimumDistance) - Math.sqrt(previousDistance)
|
|
866
|
+
);
|
|
867
|
+
if (distanceChange > MIN_MOVEMENT_DISTANCE) {
|
|
868
|
+
pointsMoved++;
|
|
869
|
+
clusterIndices[i] = newClusterIndex;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (pointsMoved === 0 && iteration !== 0) {
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
const componentASums = new Array(clusterCount).fill(0);
|
|
877
|
+
const componentBSums = new Array(clusterCount).fill(0);
|
|
878
|
+
const componentCSums = new Array(clusterCount).fill(0);
|
|
879
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
880
|
+
pixelCountSums[i] = 0;
|
|
881
|
+
}
|
|
882
|
+
for (let i = 0; i < pointCount; i++) {
|
|
883
|
+
const clusterIndex = clusterIndices[i];
|
|
884
|
+
const point = points[i];
|
|
885
|
+
const count = counts[i];
|
|
886
|
+
pixelCountSums[clusterIndex] += count;
|
|
887
|
+
componentASums[clusterIndex] += point[0] * count;
|
|
888
|
+
componentBSums[clusterIndex] += point[1] * count;
|
|
889
|
+
componentCSums[clusterIndex] += point[2] * count;
|
|
890
|
+
}
|
|
891
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
892
|
+
const count = pixelCountSums[i];
|
|
893
|
+
if (count === 0) {
|
|
894
|
+
clusters[i] = [0, 0, 0];
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const a = componentASums[i] / count;
|
|
898
|
+
const b = componentBSums[i] / count;
|
|
899
|
+
const c = componentCSums[i] / count;
|
|
900
|
+
clusters[i] = [a, b, c];
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const argbToPopulation = /* @__PURE__ */ new Map();
|
|
904
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
905
|
+
const count = pixelCountSums[i];
|
|
906
|
+
if (count === 0) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const possibleNewCluster = pointProvider.toInt(clusters[i]);
|
|
910
|
+
const existingCount = argbToPopulation.get(possibleNewCluster) || 0;
|
|
911
|
+
argbToPopulation.set(possibleNewCluster, existingCount + count);
|
|
912
|
+
}
|
|
913
|
+
return argbToPopulation;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// src/color/quantize/quantizer_map.ts
|
|
918
|
+
var QuantizerMap = class {
|
|
919
|
+
/**
|
|
920
|
+
* @param pixels Colors in ARGB format.
|
|
921
|
+
* @return A Map with keys of ARGB colors, and values of the number of times
|
|
922
|
+
* the color appears in the image.
|
|
923
|
+
*/
|
|
924
|
+
static quantize(pixels) {
|
|
925
|
+
const countByColor = /* @__PURE__ */ new Map();
|
|
926
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
927
|
+
const pixel = pixels[i];
|
|
928
|
+
const alpha = alphaFromArgb(pixel);
|
|
929
|
+
if (alpha < 255) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
countByColor.set(pixel, (countByColor.get(pixel) ?? 0) + 1);
|
|
933
|
+
}
|
|
934
|
+
return countByColor;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// src/color/quantize/quantizer_wu.ts
|
|
939
|
+
var INDEX_BITS = 5;
|
|
940
|
+
var SIDE_LENGTH = 33;
|
|
941
|
+
var TOTAL_SIZE = 35937;
|
|
942
|
+
var directions = {
|
|
943
|
+
BLUE: "blue",
|
|
944
|
+
GREEN: "green",
|
|
945
|
+
RED: "red"
|
|
946
|
+
};
|
|
947
|
+
var Box = class {
|
|
948
|
+
constructor(r0 = 0, r1 = 0, g0 = 0, g1 = 0, b0 = 0, b1 = 0, vol = 0) {
|
|
949
|
+
this.r0 = r0;
|
|
950
|
+
this.r1 = r1;
|
|
951
|
+
this.g0 = g0;
|
|
952
|
+
this.g1 = g1;
|
|
953
|
+
this.b0 = b0;
|
|
954
|
+
this.b1 = b1;
|
|
955
|
+
this.vol = vol;
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
var CreateBoxesResult = class {
|
|
959
|
+
/**
|
|
960
|
+
* @param requestedCount how many colors the caller asked to be returned from
|
|
961
|
+
* quantization.
|
|
962
|
+
* @param resultCount the actual number of colors achieved from quantization.
|
|
963
|
+
* May be lower than the requested count.
|
|
964
|
+
*/
|
|
965
|
+
constructor(requestedCount, resultCount) {
|
|
966
|
+
this.requestedCount = requestedCount;
|
|
967
|
+
this.resultCount = resultCount;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
var MaximizeResult = class {
|
|
971
|
+
constructor(cutLocation, maximum) {
|
|
972
|
+
this.cutLocation = cutLocation;
|
|
973
|
+
this.maximum = maximum;
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
var QuantizerWu = class {
|
|
977
|
+
constructor(weights = [], momentsR = [], momentsG = [], momentsB = [], moments = [], cubes = []) {
|
|
978
|
+
this.weights = weights;
|
|
979
|
+
this.momentsR = momentsR;
|
|
980
|
+
this.momentsG = momentsG;
|
|
981
|
+
this.momentsB = momentsB;
|
|
982
|
+
this.moments = moments;
|
|
983
|
+
this.cubes = cubes;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* @param pixels Colors in ARGB format.
|
|
987
|
+
* @param maxColors The number of colors to divide the image into. A lower
|
|
988
|
+
* number of colors may be returned.
|
|
989
|
+
* @return Colors in ARGB format.
|
|
990
|
+
*/
|
|
991
|
+
quantize(pixels, maxColors) {
|
|
992
|
+
this.constructHistogram(pixels);
|
|
993
|
+
this.computeMoments();
|
|
994
|
+
const createBoxesResult = this.createBoxes(maxColors);
|
|
995
|
+
const results = this.createResult(createBoxesResult.resultCount);
|
|
996
|
+
return results;
|
|
997
|
+
}
|
|
998
|
+
bottom(cube, direction, moment) {
|
|
999
|
+
switch (direction) {
|
|
1000
|
+
case directions.BLUE:
|
|
1001
|
+
return -moment[this.getIndex(cube.r1, cube.g1, cube.b0)] + moment[this.getIndex(cube.r1, cube.g0, cube.b0)] + moment[this.getIndex(cube.r0, cube.g1, cube.b0)] - moment[this.getIndex(cube.r0, cube.g0, cube.b0)];
|
|
1002
|
+
case directions.GREEN:
|
|
1003
|
+
return -moment[this.getIndex(cube.r1, cube.g0, cube.b1)] + moment[this.getIndex(cube.r1, cube.g0, cube.b0)] + moment[this.getIndex(cube.r0, cube.g0, cube.b1)] - moment[this.getIndex(cube.r0, cube.g0, cube.b0)];
|
|
1004
|
+
case directions.RED:
|
|
1005
|
+
return -moment[this.getIndex(cube.r0, cube.g1, cube.b1)] + moment[this.getIndex(cube.r0, cube.g1, cube.b0)] + moment[this.getIndex(cube.r0, cube.g0, cube.b1)] - moment[this.getIndex(cube.r0, cube.g0, cube.b0)];
|
|
1006
|
+
default:
|
|
1007
|
+
throw new Error("unexpected direction $direction");
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
computeMoments() {
|
|
1011
|
+
for (let r = 1; r < SIDE_LENGTH; r++) {
|
|
1012
|
+
const area = Array.from({ length: SIDE_LENGTH }).fill(0);
|
|
1013
|
+
const areaR = Array.from({ length: SIDE_LENGTH }).fill(0);
|
|
1014
|
+
const areaG = Array.from({ length: SIDE_LENGTH }).fill(0);
|
|
1015
|
+
const areaB = Array.from({ length: SIDE_LENGTH }).fill(0);
|
|
1016
|
+
const area2 = Array.from({ length: SIDE_LENGTH }).fill(0);
|
|
1017
|
+
for (let g = 1; g < SIDE_LENGTH; g++) {
|
|
1018
|
+
let line = 0;
|
|
1019
|
+
let lineR = 0;
|
|
1020
|
+
let lineG = 0;
|
|
1021
|
+
let lineB = 0;
|
|
1022
|
+
let line2 = 0;
|
|
1023
|
+
for (let b = 1; b < SIDE_LENGTH; b++) {
|
|
1024
|
+
const index = this.getIndex(r, g, b);
|
|
1025
|
+
line += this.weights[index];
|
|
1026
|
+
lineR += this.momentsR[index];
|
|
1027
|
+
lineG += this.momentsG[index];
|
|
1028
|
+
lineB += this.momentsB[index];
|
|
1029
|
+
line2 += this.moments[index];
|
|
1030
|
+
area[b] += line;
|
|
1031
|
+
areaR[b] += lineR;
|
|
1032
|
+
areaG[b] += lineG;
|
|
1033
|
+
areaB[b] += lineB;
|
|
1034
|
+
area2[b] += line2;
|
|
1035
|
+
const previousIndex = this.getIndex(r - 1, g, b);
|
|
1036
|
+
this.weights[index] = this.weights[previousIndex] + area[b];
|
|
1037
|
+
this.momentsR[index] = this.momentsR[previousIndex] + areaR[b];
|
|
1038
|
+
this.momentsG[index] = this.momentsG[previousIndex] + areaG[b];
|
|
1039
|
+
this.momentsB[index] = this.momentsB[previousIndex] + areaB[b];
|
|
1040
|
+
this.moments[index] = this.moments[previousIndex] + area2[b];
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
constructHistogram(pixels) {
|
|
1046
|
+
this.weights = Array.from({ length: TOTAL_SIZE }).fill(0);
|
|
1047
|
+
this.momentsR = Array.from({ length: TOTAL_SIZE }).fill(0);
|
|
1048
|
+
this.momentsG = Array.from({ length: TOTAL_SIZE }).fill(0);
|
|
1049
|
+
this.momentsB = Array.from({ length: TOTAL_SIZE }).fill(0);
|
|
1050
|
+
this.moments = Array.from({ length: TOTAL_SIZE }).fill(0);
|
|
1051
|
+
const countByColor = QuantizerMap.quantize(pixels);
|
|
1052
|
+
for (const [pixel, count] of countByColor.entries()) {
|
|
1053
|
+
const red = redFromArgb(pixel);
|
|
1054
|
+
const green = greenFromArgb(pixel);
|
|
1055
|
+
const blue = blueFromArgb(pixel);
|
|
1056
|
+
const bitsToRemove = 8 - INDEX_BITS;
|
|
1057
|
+
const iR = (red >> bitsToRemove) + 1;
|
|
1058
|
+
const iG = (green >> bitsToRemove) + 1;
|
|
1059
|
+
const iB = (blue >> bitsToRemove) + 1;
|
|
1060
|
+
const index = this.getIndex(iR, iG, iB);
|
|
1061
|
+
this.weights[index] = (this.weights[index] ?? 0) + count;
|
|
1062
|
+
this.momentsR[index] += count * red;
|
|
1063
|
+
this.momentsG[index] += count * green;
|
|
1064
|
+
this.momentsB[index] += count * blue;
|
|
1065
|
+
this.moments[index] += count * (red * red + green * green + blue * blue);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
createBoxes(maxColors) {
|
|
1069
|
+
this.cubes = Array.from({ length: maxColors }).fill(0).map(() => new Box());
|
|
1070
|
+
const volumeVariance = Array.from({ length: maxColors }).fill(0);
|
|
1071
|
+
this.cubes[0].r0 = 0;
|
|
1072
|
+
this.cubes[0].g0 = 0;
|
|
1073
|
+
this.cubes[0].b0 = 0;
|
|
1074
|
+
this.cubes[0].r1 = SIDE_LENGTH - 1;
|
|
1075
|
+
this.cubes[0].g1 = SIDE_LENGTH - 1;
|
|
1076
|
+
this.cubes[0].b1 = SIDE_LENGTH - 1;
|
|
1077
|
+
let generatedColorCount = maxColors;
|
|
1078
|
+
let next = 0;
|
|
1079
|
+
for (let i = 1; i < maxColors; i++) {
|
|
1080
|
+
if (this.cut(this.cubes[next], this.cubes[i])) {
|
|
1081
|
+
volumeVariance[next] = this.cubes[next].vol > 1 ? this.variance(this.cubes[next]) : 0;
|
|
1082
|
+
volumeVariance[i] = this.cubes[i].vol > 1 ? this.variance(this.cubes[i]) : 0;
|
|
1083
|
+
} else {
|
|
1084
|
+
volumeVariance[next] = 0;
|
|
1085
|
+
i--;
|
|
1086
|
+
}
|
|
1087
|
+
next = 0;
|
|
1088
|
+
let temp = volumeVariance[0];
|
|
1089
|
+
for (let j = 1; j <= i; j++) {
|
|
1090
|
+
if (volumeVariance[j] > temp) {
|
|
1091
|
+
temp = volumeVariance[j];
|
|
1092
|
+
next = j;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (temp <= 0) {
|
|
1096
|
+
generatedColorCount = i + 1;
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return new CreateBoxesResult(maxColors, generatedColorCount);
|
|
1101
|
+
}
|
|
1102
|
+
createResult(colorCount) {
|
|
1103
|
+
const colors = [];
|
|
1104
|
+
for (let i = 0; i < colorCount; ++i) {
|
|
1105
|
+
const cube = this.cubes[i];
|
|
1106
|
+
const weight = this.volume(cube, this.weights);
|
|
1107
|
+
if (weight > 0) {
|
|
1108
|
+
const r = Math.round(this.volume(cube, this.momentsR) / weight);
|
|
1109
|
+
const g = Math.round(this.volume(cube, this.momentsG) / weight);
|
|
1110
|
+
const b = Math.round(this.volume(cube, this.momentsB) / weight);
|
|
1111
|
+
const color = 255 << 24 | (r & 255) << 16 | (g & 255) << 8 | b & 255;
|
|
1112
|
+
colors.push(color);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return colors;
|
|
1116
|
+
}
|
|
1117
|
+
cut(one, two) {
|
|
1118
|
+
const wholeR = this.volume(one, this.momentsR);
|
|
1119
|
+
const wholeG = this.volume(one, this.momentsG);
|
|
1120
|
+
const wholeB = this.volume(one, this.momentsB);
|
|
1121
|
+
const wholeW = this.volume(one, this.weights);
|
|
1122
|
+
const maxRResult = this.maximize(
|
|
1123
|
+
one,
|
|
1124
|
+
directions.RED,
|
|
1125
|
+
one.r0 + 1,
|
|
1126
|
+
one.r1,
|
|
1127
|
+
wholeR,
|
|
1128
|
+
wholeG,
|
|
1129
|
+
wholeB,
|
|
1130
|
+
wholeW
|
|
1131
|
+
);
|
|
1132
|
+
const maxGResult = this.maximize(
|
|
1133
|
+
one,
|
|
1134
|
+
directions.GREEN,
|
|
1135
|
+
one.g0 + 1,
|
|
1136
|
+
one.g1,
|
|
1137
|
+
wholeR,
|
|
1138
|
+
wholeG,
|
|
1139
|
+
wholeB,
|
|
1140
|
+
wholeW
|
|
1141
|
+
);
|
|
1142
|
+
const maxBResult = this.maximize(
|
|
1143
|
+
one,
|
|
1144
|
+
directions.BLUE,
|
|
1145
|
+
one.b0 + 1,
|
|
1146
|
+
one.b1,
|
|
1147
|
+
wholeR,
|
|
1148
|
+
wholeG,
|
|
1149
|
+
wholeB,
|
|
1150
|
+
wholeW
|
|
1151
|
+
);
|
|
1152
|
+
let direction;
|
|
1153
|
+
const maxR = maxRResult.maximum;
|
|
1154
|
+
const maxG = maxGResult.maximum;
|
|
1155
|
+
const maxB = maxBResult.maximum;
|
|
1156
|
+
if (maxR >= maxG && maxR >= maxB) {
|
|
1157
|
+
if (maxRResult.cutLocation < 0) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
direction = directions.RED;
|
|
1161
|
+
} else if (maxG >= maxR && maxG >= maxB) {
|
|
1162
|
+
direction = directions.GREEN;
|
|
1163
|
+
} else {
|
|
1164
|
+
direction = directions.BLUE;
|
|
1165
|
+
}
|
|
1166
|
+
two.r1 = one.r1;
|
|
1167
|
+
two.g1 = one.g1;
|
|
1168
|
+
two.b1 = one.b1;
|
|
1169
|
+
switch (direction) {
|
|
1170
|
+
case directions.BLUE:
|
|
1171
|
+
one.b1 = maxBResult.cutLocation;
|
|
1172
|
+
two.r0 = one.r0;
|
|
1173
|
+
two.g0 = one.g0;
|
|
1174
|
+
two.b0 = one.b1;
|
|
1175
|
+
break;
|
|
1176
|
+
case directions.GREEN:
|
|
1177
|
+
one.g1 = maxGResult.cutLocation;
|
|
1178
|
+
two.r0 = one.r0;
|
|
1179
|
+
two.g0 = one.g1;
|
|
1180
|
+
two.b0 = one.b0;
|
|
1181
|
+
break;
|
|
1182
|
+
case directions.RED:
|
|
1183
|
+
one.r1 = maxRResult.cutLocation;
|
|
1184
|
+
two.r0 = one.r1;
|
|
1185
|
+
two.g0 = one.g0;
|
|
1186
|
+
two.b0 = one.b0;
|
|
1187
|
+
break;
|
|
1188
|
+
default:
|
|
1189
|
+
throw new Error("unexpected direction " + direction);
|
|
1190
|
+
}
|
|
1191
|
+
one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
|
|
1192
|
+
two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
getIndex(r, g, b) {
|
|
1196
|
+
return (r << INDEX_BITS * 2) + (r << INDEX_BITS + 1) + r + (g << INDEX_BITS) + g + b;
|
|
1197
|
+
}
|
|
1198
|
+
maximize(cube, direction, first, last, wholeR, wholeG, wholeB, wholeW) {
|
|
1199
|
+
const bottomR = this.bottom(cube, direction, this.momentsR);
|
|
1200
|
+
const bottomG = this.bottom(cube, direction, this.momentsG);
|
|
1201
|
+
const bottomB = this.bottom(cube, direction, this.momentsB);
|
|
1202
|
+
const bottomW = this.bottom(cube, direction, this.weights);
|
|
1203
|
+
let max = 0;
|
|
1204
|
+
let cut = -1;
|
|
1205
|
+
let halfR = 0;
|
|
1206
|
+
let halfG = 0;
|
|
1207
|
+
let halfB = 0;
|
|
1208
|
+
let halfW = 0;
|
|
1209
|
+
for (let i = first; i < last; i++) {
|
|
1210
|
+
halfR = bottomR + this.top(cube, direction, i, this.momentsR);
|
|
1211
|
+
halfG = bottomG + this.top(cube, direction, i, this.momentsG);
|
|
1212
|
+
halfB = bottomB + this.top(cube, direction, i, this.momentsB);
|
|
1213
|
+
halfW = bottomW + this.top(cube, direction, i, this.weights);
|
|
1214
|
+
if (halfW === 0) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
let tempNumerator = (halfR * halfR + halfG * halfG + halfB * halfB) * 1;
|
|
1218
|
+
let tempDenominator = halfW * 1;
|
|
1219
|
+
let temp = tempNumerator / tempDenominator;
|
|
1220
|
+
halfR = wholeR - halfR;
|
|
1221
|
+
halfG = wholeG - halfG;
|
|
1222
|
+
halfB = wholeB - halfB;
|
|
1223
|
+
halfW = wholeW - halfW;
|
|
1224
|
+
if (halfW === 0) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
tempNumerator = (halfR * halfR + halfG * halfG + halfB * halfB) * 1;
|
|
1228
|
+
tempDenominator = halfW * 1;
|
|
1229
|
+
temp += tempNumerator / tempDenominator;
|
|
1230
|
+
if (temp > max) {
|
|
1231
|
+
max = temp;
|
|
1232
|
+
cut = i;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return new MaximizeResult(cut, max);
|
|
1236
|
+
}
|
|
1237
|
+
top(cube, direction, position, moment) {
|
|
1238
|
+
switch (direction) {
|
|
1239
|
+
case directions.BLUE:
|
|
1240
|
+
return moment[this.getIndex(cube.r1, cube.g1, position)] - moment[this.getIndex(cube.r1, cube.g0, position)] - moment[this.getIndex(cube.r0, cube.g1, position)] + moment[this.getIndex(cube.r0, cube.g0, position)];
|
|
1241
|
+
case directions.GREEN:
|
|
1242
|
+
return moment[this.getIndex(cube.r1, position, cube.b1)] - moment[this.getIndex(cube.r1, position, cube.b0)] - moment[this.getIndex(cube.r0, position, cube.b1)] + moment[this.getIndex(cube.r0, position, cube.b0)];
|
|
1243
|
+
case directions.RED:
|
|
1244
|
+
return moment[this.getIndex(position, cube.g1, cube.b1)] - moment[this.getIndex(position, cube.g1, cube.b0)] - moment[this.getIndex(position, cube.g0, cube.b1)] + moment[this.getIndex(position, cube.g0, cube.b0)];
|
|
1245
|
+
default:
|
|
1246
|
+
throw new Error("unexpected direction $direction");
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
variance(cube) {
|
|
1250
|
+
const dr = this.volume(cube, this.momentsR);
|
|
1251
|
+
const dg = this.volume(cube, this.momentsG);
|
|
1252
|
+
const db = this.volume(cube, this.momentsB);
|
|
1253
|
+
const xx = this.moments[this.getIndex(cube.r1, cube.g1, cube.b1)] - this.moments[this.getIndex(cube.r1, cube.g1, cube.b0)] - this.moments[this.getIndex(cube.r1, cube.g0, cube.b1)] + this.moments[this.getIndex(cube.r1, cube.g0, cube.b0)] - this.moments[this.getIndex(cube.r0, cube.g1, cube.b1)] + this.moments[this.getIndex(cube.r0, cube.g1, cube.b0)] + this.moments[this.getIndex(cube.r0, cube.g0, cube.b1)] - this.moments[this.getIndex(cube.r0, cube.g0, cube.b0)];
|
|
1254
|
+
const hypotenuse = dr * dr + dg * dg + db * db;
|
|
1255
|
+
const volume = this.volume(cube, this.weights);
|
|
1256
|
+
return xx - hypotenuse / volume;
|
|
1257
|
+
}
|
|
1258
|
+
volume(cube, moment) {
|
|
1259
|
+
return moment[this.getIndex(cube.r1, cube.g1, cube.b1)] - moment[this.getIndex(cube.r1, cube.g1, cube.b0)] - moment[this.getIndex(cube.r1, cube.g0, cube.b1)] + moment[this.getIndex(cube.r1, cube.g0, cube.b0)] - moment[this.getIndex(cube.r0, cube.g1, cube.b1)] + moment[this.getIndex(cube.r0, cube.g1, cube.b0)] + moment[this.getIndex(cube.r0, cube.g0, cube.b1)] - moment[this.getIndex(cube.r0, cube.g0, cube.b0)];
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
// src/color/quantize/quantizer_celebi.ts
|
|
1264
|
+
var QuantizerCelebi = class {
|
|
1265
|
+
/**
|
|
1266
|
+
* @param pixels Colors in ARGB format.
|
|
1267
|
+
* @param maxColors The number of colors to divide the image into. A lower
|
|
1268
|
+
* number of colors may be returned.
|
|
1269
|
+
* @return Map with keys of colors in ARGB format, and values of number of
|
|
1270
|
+
* pixels in the original image that correspond to the color in the
|
|
1271
|
+
* quantized image.
|
|
1272
|
+
*/
|
|
1273
|
+
static quantize(pixels, maxColors) {
|
|
1274
|
+
const wu = new QuantizerWu();
|
|
1275
|
+
const wuResult = wu.quantize(pixels, maxColors);
|
|
1276
|
+
return QuantizerWsmeans.quantize(pixels, wuResult, maxColors);
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// src/color/score/score.ts
|
|
1281
|
+
var SCORE_OPTION_DEFAULTS = {
|
|
1282
|
+
desired: 4,
|
|
1283
|
+
// 4 colors matches what Android wallpaper picker.
|
|
1284
|
+
fallbackColorARGB: 4282549748,
|
|
1285
|
+
// Google Blue.
|
|
1286
|
+
filter: true
|
|
1287
|
+
// Avoid unsuitable colors.
|
|
1288
|
+
};
|
|
1289
|
+
function compare(a, b) {
|
|
1290
|
+
if (a.score > b.score) {
|
|
1291
|
+
return -1;
|
|
1292
|
+
} else if (a.score < b.score) {
|
|
1293
|
+
return 1;
|
|
1294
|
+
}
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
var Score = class _Score {
|
|
1298
|
+
static CUTOFF_CHROMA = 5;
|
|
1299
|
+
static CUTOFF_EXCITED_PROPORTION = 0.01;
|
|
1300
|
+
static TARGET_CHROMA = 48;
|
|
1301
|
+
// A1 Chroma
|
|
1302
|
+
static WEIGHT_CHROMA_ABOVE = 0.3;
|
|
1303
|
+
static WEIGHT_CHROMA_BELOW = 0.1;
|
|
1304
|
+
static WEIGHT_PROPORTION = 0.7;
|
|
1305
|
+
constructor() {
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Given a map with keys of colors and values of how often the color appears,
|
|
1309
|
+
* rank the colors based on suitability for being used for a UI theme.
|
|
1310
|
+
*
|
|
1311
|
+
* @param colorsToPopulation map with keys of colors and values of how often
|
|
1312
|
+
* the color appears, usually from a source image.
|
|
1313
|
+
* @param {ScoreOptions} options optional parameters.
|
|
1314
|
+
* @return Colors sorted by suitability for a UI theme. The most suitable
|
|
1315
|
+
* color is the first item, the least suitable is the last. There will
|
|
1316
|
+
* always be at least one color returned. If all the input colors
|
|
1317
|
+
* were not suitable for a theme, a default fallback color will be
|
|
1318
|
+
* provided, Google Blue.
|
|
1319
|
+
*/
|
|
1320
|
+
static score(colorsToPopulation, options) {
|
|
1321
|
+
const { desired, fallbackColorARGB, filter } = {
|
|
1322
|
+
...SCORE_OPTION_DEFAULTS,
|
|
1323
|
+
...options
|
|
1324
|
+
};
|
|
1325
|
+
const colorsHct = [];
|
|
1326
|
+
const huePopulation = new Array(360).fill(0);
|
|
1327
|
+
let populationSum = 0;
|
|
1328
|
+
for (const [argb, population] of colorsToPopulation.entries()) {
|
|
1329
|
+
const hct = Hct.fromInt(argb);
|
|
1330
|
+
colorsHct.push(hct);
|
|
1331
|
+
const hue = Math.floor(hct.hue);
|
|
1332
|
+
huePopulation[hue] += population;
|
|
1333
|
+
populationSum += population;
|
|
1334
|
+
}
|
|
1335
|
+
const hueExcitedProportions = new Array(360).fill(0);
|
|
1336
|
+
for (let hue = 0; hue < 360; hue++) {
|
|
1337
|
+
const proportion = huePopulation[hue] / populationSum;
|
|
1338
|
+
for (let i = hue - 14; i < hue + 16; i++) {
|
|
1339
|
+
const neighborHue = sanitizeDegreesInt(i);
|
|
1340
|
+
hueExcitedProportions[neighborHue] += proportion;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const scoredHct = new Array();
|
|
1344
|
+
for (const hct of colorsHct) {
|
|
1345
|
+
const hue = sanitizeDegreesInt(Math.round(hct.hue));
|
|
1346
|
+
const proportion = hueExcitedProportions[hue];
|
|
1347
|
+
if (filter && (hct.chroma < _Score.CUTOFF_CHROMA || proportion <= _Score.CUTOFF_EXCITED_PROPORTION)) {
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const proportionScore = proportion * 100 * _Score.WEIGHT_PROPORTION;
|
|
1351
|
+
const chromaWeight = hct.chroma < _Score.TARGET_CHROMA ? _Score.WEIGHT_CHROMA_BELOW : _Score.WEIGHT_CHROMA_ABOVE;
|
|
1352
|
+
const chromaScore = (hct.chroma - _Score.TARGET_CHROMA) * chromaWeight;
|
|
1353
|
+
const score = proportionScore + chromaScore;
|
|
1354
|
+
scoredHct.push({ hct, score });
|
|
1355
|
+
}
|
|
1356
|
+
scoredHct.sort(compare);
|
|
1357
|
+
const chosenColors = [];
|
|
1358
|
+
for (let differenceDegrees2 = 90; differenceDegrees2 >= 15; differenceDegrees2--) {
|
|
1359
|
+
chosenColors.length = 0;
|
|
1360
|
+
for (const { hct } of scoredHct) {
|
|
1361
|
+
const duplicateHue = chosenColors.find((chosenHct) => {
|
|
1362
|
+
return differenceDegrees(hct.hue, chosenHct.hue) < differenceDegrees2;
|
|
1363
|
+
});
|
|
1364
|
+
if (!duplicateHue) {
|
|
1365
|
+
chosenColors.push(hct);
|
|
1366
|
+
}
|
|
1367
|
+
if (chosenColors.length >= desired) break;
|
|
1368
|
+
}
|
|
1369
|
+
if (chosenColors.length >= desired) break;
|
|
1370
|
+
}
|
|
1371
|
+
const colors = [];
|
|
1372
|
+
if (chosenColors.length === 0) {
|
|
1373
|
+
colors.push(fallbackColorARGB);
|
|
1374
|
+
}
|
|
1375
|
+
for (const chosenHct of chosenColors) {
|
|
1376
|
+
colors.push(chosenHct.toInt());
|
|
1377
|
+
}
|
|
1378
|
+
return colors;
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
// src/color/extract-colors.ts
|
|
1383
|
+
var QUALITY_SETTINGS = {
|
|
1384
|
+
high: { maxPixels: 25e3, quantizeColors: 256 },
|
|
1385
|
+
low: { maxPixels: 5e3, quantizeColors: 64 },
|
|
1386
|
+
medium: { maxPixels: 1e4, quantizeColors: 128 }
|
|
1387
|
+
};
|
|
1388
|
+
function extractColors(imageData, options = {}) {
|
|
1389
|
+
const {
|
|
1390
|
+
filter = true,
|
|
1391
|
+
fixDislikedColors = false,
|
|
1392
|
+
maxColors = 5,
|
|
1393
|
+
quality = "medium",
|
|
1394
|
+
scoringEnabled = true
|
|
1395
|
+
} = options;
|
|
1396
|
+
const qualitySettings = QUALITY_SETTINGS[quality];
|
|
1397
|
+
let pixels = imageDataToPixels(imageData);
|
|
1398
|
+
pixels = samplePixels(pixels, qualitySettings.maxPixels);
|
|
1399
|
+
if (filter) {
|
|
1400
|
+
pixels = filterExtremeTones(pixels);
|
|
1401
|
+
}
|
|
1402
|
+
const quantized = QuantizerCelebi.quantize(
|
|
1403
|
+
pixels,
|
|
1404
|
+
qualitySettings.quantizeColors
|
|
1405
|
+
);
|
|
1406
|
+
let selectedColors;
|
|
1407
|
+
if (scoringEnabled) {
|
|
1408
|
+
selectedColors = Score.score(quantized, {
|
|
1409
|
+
desired: maxColors,
|
|
1410
|
+
filter
|
|
1411
|
+
});
|
|
1412
|
+
} else {
|
|
1413
|
+
const sorted = Array.from(quantized.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxColors).map((entry) => entry[0]);
|
|
1414
|
+
selectedColors = sorted;
|
|
1415
|
+
}
|
|
1416
|
+
const totalPopulation = Array.from(quantized.values()).reduce(
|
|
1417
|
+
(sum, pop) => sum + pop,
|
|
1418
|
+
0
|
|
1419
|
+
);
|
|
1420
|
+
return selectedColors.map((argb) => {
|
|
1421
|
+
let hct = Hct.fromInt(argb);
|
|
1422
|
+
if (fixDislikedColors && DislikeAnalyzer.isDisliked(hct)) {
|
|
1423
|
+
hct = DislikeAnalyzer.fixIfDisliked(hct);
|
|
1424
|
+
argb = hct.toInt();
|
|
1425
|
+
}
|
|
1426
|
+
const r = redFromArgb(argb);
|
|
1427
|
+
const g = greenFromArgb(argb);
|
|
1428
|
+
const b = blueFromArgb(argb);
|
|
1429
|
+
const population = quantized.get(argb) || 0;
|
|
1430
|
+
return {
|
|
1431
|
+
hct: { c: hct.chroma, h: hct.hue, t: hct.tone },
|
|
1432
|
+
hex: rgbToHex2(r, g, b),
|
|
1433
|
+
percentage: population / totalPopulation * 100,
|
|
1434
|
+
population,
|
|
1435
|
+
rgb: { b, g, r }
|
|
1436
|
+
};
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
function extractThemePalette(imageData) {
|
|
1440
|
+
const colors = extractColors(imageData, {
|
|
1441
|
+
filter: true,
|
|
1442
|
+
fixDislikedColors: true,
|
|
1443
|
+
// Always fix disliked colors for themes
|
|
1444
|
+
maxColors: 8,
|
|
1445
|
+
quality: "high",
|
|
1446
|
+
scoringEnabled: true
|
|
1447
|
+
});
|
|
1448
|
+
if (colors.length === 0) {
|
|
1449
|
+
throw new Error("No colors could be extracted from image");
|
|
1450
|
+
}
|
|
1451
|
+
const result = {
|
|
1452
|
+
primary: colors[0]
|
|
1453
|
+
};
|
|
1454
|
+
if (colors.length > 1) {
|
|
1455
|
+
const primaryHue = colors[0].hct.h;
|
|
1456
|
+
let maxHueDiff = 0;
|
|
1457
|
+
let secondaryIndex = 1;
|
|
1458
|
+
for (let i = 1; i < Math.min(colors.length, 4); i++) {
|
|
1459
|
+
const hueDiff = Math.abs(hueDifference(primaryHue, colors[i].hct.h));
|
|
1460
|
+
if (hueDiff > maxHueDiff) {
|
|
1461
|
+
maxHueDiff = hueDiff;
|
|
1462
|
+
secondaryIndex = i;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
result.secondary = colors[secondaryIndex];
|
|
1466
|
+
if (colors.length > 2) {
|
|
1467
|
+
const secondaryHue = colors[secondaryIndex].hct.h;
|
|
1468
|
+
let bestTertiaryIndex = -1;
|
|
1469
|
+
let bestScore = 0;
|
|
1470
|
+
for (let i = 1; i < colors.length; i++) {
|
|
1471
|
+
if (i === secondaryIndex) continue;
|
|
1472
|
+
const hue = colors[i].hct.h;
|
|
1473
|
+
const primaryDiff = Math.abs(hueDifference(primaryHue, hue));
|
|
1474
|
+
const secondaryDiff = Math.abs(hueDifference(secondaryHue, hue));
|
|
1475
|
+
const score = Math.min(primaryDiff, secondaryDiff);
|
|
1476
|
+
if (score > bestScore) {
|
|
1477
|
+
bestScore = score;
|
|
1478
|
+
bestTertiaryIndex = i;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (bestTertiaryIndex !== -1) {
|
|
1482
|
+
result.tertiary = colors[bestTertiaryIndex];
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const neutral = colors.find((c) => c.hct.c < 20);
|
|
1487
|
+
if (neutral) {
|
|
1488
|
+
result.neutral = neutral;
|
|
1489
|
+
}
|
|
1490
|
+
const errorColor = colors.find((c) => c.hct.h >= 350 || c.hct.h <= 40);
|
|
1491
|
+
if (errorColor) {
|
|
1492
|
+
result.error = errorColor;
|
|
1493
|
+
}
|
|
1494
|
+
return result;
|
|
1495
|
+
}
|
|
1496
|
+
function hueDifference(h1, h2) {
|
|
1497
|
+
const diff = Math.abs(h1 - h2);
|
|
1498
|
+
return diff > 180 ? 360 - diff : diff;
|
|
1499
|
+
}
|
|
1500
|
+
function rgbToHex2(r, g, b) {
|
|
1501
|
+
return "#" + [r, g, b].map((x) => Math.round(x).toString(16).padStart(2, "0")).join("");
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/color/material-theme.ts
|
|
1505
|
+
function generateMaterialTheme(sourceColor, options = {}) {
|
|
1506
|
+
const { fixDislikedColors = true } = options;
|
|
1507
|
+
let source = parseColor(sourceColor);
|
|
1508
|
+
if (!source) {
|
|
1509
|
+
throw new Error(`Invalid color format: ${sourceColor}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (fixDislikedColors) {
|
|
1512
|
+
const sourceHct = Hct.fromInt(rgbToArgb(source));
|
|
1513
|
+
if (DislikeAnalyzer.isDisliked(sourceHct)) {
|
|
1514
|
+
const fixedHct = DislikeAnalyzer.fixIfDisliked(sourceHct);
|
|
1515
|
+
const fixedArgb = fixedHct.toInt();
|
|
1516
|
+
source = {
|
|
1517
|
+
b: fixedArgb & 255,
|
|
1518
|
+
g: fixedArgb >> 8 & 255,
|
|
1519
|
+
r: fixedArgb >> 16 & 255
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
const corePalette = corePaletteFromRgb(source);
|
|
1524
|
+
const lightScheme = {
|
|
1525
|
+
background: rgbToHex(corePalette.neutral.tone(99)),
|
|
1526
|
+
error: rgbToHex(corePalette.error.tone(40)),
|
|
1527
|
+
errorContainer: rgbToHex(corePalette.error.tone(90)),
|
|
1528
|
+
onBackground: rgbToHex(corePalette.neutral.tone(10)),
|
|
1529
|
+
onError: rgbToHex(corePalette.error.tone(100)),
|
|
1530
|
+
onErrorContainer: rgbToHex(corePalette.error.tone(10)),
|
|
1531
|
+
onPrimary: rgbToHex(corePalette.primary.tone(100)),
|
|
1532
|
+
onPrimaryContainer: rgbToHex(corePalette.primary.tone(10)),
|
|
1533
|
+
onSecondary: rgbToHex(corePalette.secondary.tone(100)),
|
|
1534
|
+
onSecondaryContainer: rgbToHex(corePalette.secondary.tone(10)),
|
|
1535
|
+
onSurface: rgbToHex(corePalette.neutral.tone(10)),
|
|
1536
|
+
onSurfaceVariant: rgbToHex(corePalette.neutralVariant.tone(30)),
|
|
1537
|
+
onTertiary: rgbToHex(corePalette.tertiary.tone(100)),
|
|
1538
|
+
onTertiaryContainer: rgbToHex(corePalette.tertiary.tone(10)),
|
|
1539
|
+
outline: rgbToHex(corePalette.neutralVariant.tone(50)),
|
|
1540
|
+
primary: rgbToHex(corePalette.primary.tone(40)),
|
|
1541
|
+
primaryContainer: rgbToHex(corePalette.primary.tone(90)),
|
|
1542
|
+
secondary: rgbToHex(corePalette.secondary.tone(40)),
|
|
1543
|
+
secondaryContainer: rgbToHex(corePalette.secondary.tone(90)),
|
|
1544
|
+
surface: rgbToHex(corePalette.neutral.tone(99)),
|
|
1545
|
+
surfaceVariant: rgbToHex(corePalette.neutralVariant.tone(90)),
|
|
1546
|
+
tertiary: rgbToHex(corePalette.tertiary.tone(40)),
|
|
1547
|
+
tertiaryContainer: rgbToHex(corePalette.tertiary.tone(90))
|
|
1548
|
+
};
|
|
1549
|
+
const darkScheme = {
|
|
1550
|
+
background: rgbToHex(corePalette.neutral.tone(10)),
|
|
1551
|
+
error: rgbToHex(corePalette.error.tone(80)),
|
|
1552
|
+
errorContainer: rgbToHex(corePalette.error.tone(30)),
|
|
1553
|
+
onBackground: rgbToHex(corePalette.neutral.tone(90)),
|
|
1554
|
+
onError: rgbToHex(corePalette.error.tone(20)),
|
|
1555
|
+
onErrorContainer: rgbToHex(corePalette.error.tone(90)),
|
|
1556
|
+
onPrimary: rgbToHex(corePalette.primary.tone(20)),
|
|
1557
|
+
onPrimaryContainer: rgbToHex(corePalette.primary.tone(90)),
|
|
1558
|
+
onSecondary: rgbToHex(corePalette.secondary.tone(20)),
|
|
1559
|
+
onSecondaryContainer: rgbToHex(corePalette.secondary.tone(90)),
|
|
1560
|
+
onSurface: rgbToHex(corePalette.neutral.tone(90)),
|
|
1561
|
+
onSurfaceVariant: rgbToHex(corePalette.neutralVariant.tone(80)),
|
|
1562
|
+
onTertiary: rgbToHex(corePalette.tertiary.tone(20)),
|
|
1563
|
+
onTertiaryContainer: rgbToHex(corePalette.tertiary.tone(90)),
|
|
1564
|
+
outline: rgbToHex(corePalette.neutralVariant.tone(60)),
|
|
1565
|
+
primary: rgbToHex(corePalette.primary.tone(80)),
|
|
1566
|
+
primaryContainer: rgbToHex(corePalette.primary.tone(30)),
|
|
1567
|
+
secondary: rgbToHex(corePalette.secondary.tone(80)),
|
|
1568
|
+
secondaryContainer: rgbToHex(corePalette.secondary.tone(30)),
|
|
1569
|
+
surface: rgbToHex(corePalette.neutral.tone(10)),
|
|
1570
|
+
surfaceVariant: rgbToHex(corePalette.neutralVariant.tone(30)),
|
|
1571
|
+
tertiary: rgbToHex(corePalette.tertiary.tone(80)),
|
|
1572
|
+
tertiaryContainer: rgbToHex(corePalette.tertiary.tone(30))
|
|
1573
|
+
};
|
|
1574
|
+
return {
|
|
1575
|
+
schemes: {
|
|
1576
|
+
dark: darkScheme,
|
|
1577
|
+
light: lightScheme
|
|
1578
|
+
},
|
|
1579
|
+
sourceColor
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/tools/image-extraction.tools.ts
|
|
1584
|
+
var extractImageColorsTool = {
|
|
1585
|
+
description: "Extract dominant colors from an image. Input should be image data as an array of RGBA values.",
|
|
1586
|
+
execute: async (args, _context) => {
|
|
1587
|
+
const {
|
|
1588
|
+
format = "json",
|
|
1589
|
+
imageData,
|
|
1590
|
+
maxColors = 5,
|
|
1591
|
+
quality = "medium"
|
|
1592
|
+
} = args;
|
|
1593
|
+
try {
|
|
1594
|
+
const data = imageData.data instanceof Uint8ClampedArray ? imageData.data : new Uint8ClampedArray(imageData.data);
|
|
1595
|
+
const processedImageData = {
|
|
1596
|
+
data,
|
|
1597
|
+
height: imageData.height,
|
|
1598
|
+
width: imageData.width
|
|
1599
|
+
};
|
|
1600
|
+
const colors = extractColors(processedImageData, {
|
|
1601
|
+
filter: true,
|
|
1602
|
+
maxColors,
|
|
1603
|
+
quality,
|
|
1604
|
+
scoringEnabled: true
|
|
1605
|
+
});
|
|
1606
|
+
switch (format) {
|
|
1607
|
+
case "css":
|
|
1608
|
+
return formatAsCSS(colors);
|
|
1609
|
+
case "palette":
|
|
1610
|
+
return formatAsPalette(colors);
|
|
1611
|
+
case "json":
|
|
1612
|
+
default:
|
|
1613
|
+
return formatAsJSON(colors);
|
|
1614
|
+
}
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
return `Error extracting colors: ${error instanceof Error ? error.message : String(error)}`;
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
inputSchema: {
|
|
1620
|
+
properties: {
|
|
1621
|
+
format: {
|
|
1622
|
+
description: "Output format: json, css, or palette (default: json)",
|
|
1623
|
+
enum: ["json", "css", "palette"],
|
|
1624
|
+
type: "string"
|
|
1625
|
+
},
|
|
1626
|
+
imageData: {
|
|
1627
|
+
description: "Image data with RGBA values",
|
|
1628
|
+
properties: {
|
|
1629
|
+
data: {
|
|
1630
|
+
description: "Flat array of RGBA values (0-255)",
|
|
1631
|
+
items: { type: "number" },
|
|
1632
|
+
type: "array"
|
|
1633
|
+
},
|
|
1634
|
+
height: {
|
|
1635
|
+
description: "Image height in pixels",
|
|
1636
|
+
type: "number"
|
|
1637
|
+
},
|
|
1638
|
+
width: {
|
|
1639
|
+
description: "Image width in pixels",
|
|
1640
|
+
type: "number"
|
|
1641
|
+
}
|
|
1642
|
+
},
|
|
1643
|
+
required: ["data", "width", "height"],
|
|
1644
|
+
type: "object"
|
|
1645
|
+
},
|
|
1646
|
+
maxColors: {
|
|
1647
|
+
description: "Maximum number of colors to extract (default: 5)",
|
|
1648
|
+
maximum: 20,
|
|
1649
|
+
minimum: 1,
|
|
1650
|
+
type: "number"
|
|
1651
|
+
},
|
|
1652
|
+
quality: {
|
|
1653
|
+
description: "Extraction quality: low, medium, or high (default: medium)",
|
|
1654
|
+
enum: ["low", "medium", "high"],
|
|
1655
|
+
type: "string"
|
|
1656
|
+
}
|
|
1657
|
+
},
|
|
1658
|
+
required: ["imageData"],
|
|
1659
|
+
type: "object"
|
|
1660
|
+
},
|
|
1661
|
+
name: "extract_image_colors"
|
|
1662
|
+
};
|
|
1663
|
+
var generateThemeFromImageTool = {
|
|
1664
|
+
description: "Generate a complete Material Design 3 theme from an image",
|
|
1665
|
+
execute: async (args, _context) => {
|
|
1666
|
+
const {
|
|
1667
|
+
imageData,
|
|
1668
|
+
includeCustomColors = true,
|
|
1669
|
+
isDark = false
|
|
1670
|
+
} = args;
|
|
1671
|
+
try {
|
|
1672
|
+
const data = imageData.data instanceof Uint8ClampedArray ? imageData.data : new Uint8ClampedArray(imageData.data);
|
|
1673
|
+
const processedImageData = {
|
|
1674
|
+
data,
|
|
1675
|
+
height: imageData.height,
|
|
1676
|
+
width: imageData.width
|
|
1677
|
+
};
|
|
1678
|
+
const palette = extractThemePalette(processedImageData);
|
|
1679
|
+
const sourceColor = palette.primary.hex;
|
|
1680
|
+
const theme = generateMaterialTheme(sourceColor, { isDark });
|
|
1681
|
+
if (includeCustomColors && palette.secondary) {
|
|
1682
|
+
theme.customColors = {
|
|
1683
|
+
secondary: {
|
|
1684
|
+
color: palette.secondary.hex,
|
|
1685
|
+
hct: palette.secondary.hct,
|
|
1686
|
+
population: palette.secondary.population
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
if (palette.tertiary) {
|
|
1690
|
+
theme.customColors.tertiary = {
|
|
1691
|
+
color: palette.tertiary.hex,
|
|
1692
|
+
hct: palette.tertiary.hct,
|
|
1693
|
+
population: palette.tertiary.population
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
let output = `# Material Design 3 Theme from Image
|
|
1698
|
+
|
|
1699
|
+
`;
|
|
1700
|
+
output += `## Source Colors
|
|
1701
|
+
`;
|
|
1702
|
+
output += `- Primary: ${palette.primary.hex} (${palette.primary.percentage.toFixed(1)}%)
|
|
1703
|
+
`;
|
|
1704
|
+
if (palette.secondary) {
|
|
1705
|
+
output += `- Secondary: ${palette.secondary.hex} (${palette.secondary.percentage.toFixed(1)}%)
|
|
1706
|
+
`;
|
|
1707
|
+
}
|
|
1708
|
+
if (palette.tertiary) {
|
|
1709
|
+
output += `- Tertiary: ${palette.tertiary.hex} (${palette.tertiary.percentage.toFixed(1)}%)
|
|
1710
|
+
`;
|
|
1711
|
+
}
|
|
1712
|
+
output += `
|
|
1713
|
+
## Theme Colors (${isDark ? "Dark" : "Light"} Mode)
|
|
1714
|
+
|
|
1715
|
+
`;
|
|
1716
|
+
output += `### Primary
|
|
1717
|
+
`;
|
|
1718
|
+
output += `- primary: ${theme.schemes[isDark ? "dark" : "light"].primary}
|
|
1719
|
+
`;
|
|
1720
|
+
output += `- onPrimary: ${theme.schemes[isDark ? "dark" : "light"].onPrimary}
|
|
1721
|
+
`;
|
|
1722
|
+
output += `- primaryContainer: ${theme.schemes[isDark ? "dark" : "light"].primaryContainer}
|
|
1723
|
+
`;
|
|
1724
|
+
output += `- onPrimaryContainer: ${theme.schemes[isDark ? "dark" : "light"].onPrimaryContainer}
|
|
1725
|
+
`;
|
|
1726
|
+
output += `
|
|
1727
|
+
### Surface
|
|
1728
|
+
`;
|
|
1729
|
+
output += `- surface: ${theme.schemes[isDark ? "dark" : "light"].surface}
|
|
1730
|
+
`;
|
|
1731
|
+
output += `- onSurface: ${theme.schemes[isDark ? "dark" : "light"].onSurface}
|
|
1732
|
+
`;
|
|
1733
|
+
output += `- surfaceVariant: ${theme.schemes[isDark ? "dark" : "light"].surfaceVariant}
|
|
1734
|
+
`;
|
|
1735
|
+
output += `- onSurfaceVariant: ${theme.schemes[isDark ? "dark" : "light"].onSurfaceVariant}
|
|
1736
|
+
`;
|
|
1737
|
+
output += `
|
|
1738
|
+
### Background
|
|
1739
|
+
`;
|
|
1740
|
+
output += `- background: ${theme.schemes[isDark ? "dark" : "light"].background}
|
|
1741
|
+
`;
|
|
1742
|
+
output += `- onBackground: ${theme.schemes[isDark ? "dark" : "light"].onBackground}
|
|
1743
|
+
`;
|
|
1744
|
+
return output;
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
return `Error generating theme: ${error instanceof Error ? error.message : String(error)}`;
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
inputSchema: {
|
|
1750
|
+
properties: {
|
|
1751
|
+
imageData: {
|
|
1752
|
+
description: "Image data with RGBA values",
|
|
1753
|
+
properties: {
|
|
1754
|
+
data: {
|
|
1755
|
+
description: "Flat array of RGBA values (0-255)",
|
|
1756
|
+
items: { type: "number" },
|
|
1757
|
+
type: "array"
|
|
1758
|
+
},
|
|
1759
|
+
height: {
|
|
1760
|
+
description: "Image height in pixels",
|
|
1761
|
+
type: "number"
|
|
1762
|
+
},
|
|
1763
|
+
width: {
|
|
1764
|
+
description: "Image width in pixels",
|
|
1765
|
+
type: "number"
|
|
1766
|
+
}
|
|
1767
|
+
},
|
|
1768
|
+
required: ["data", "width", "height"],
|
|
1769
|
+
type: "object"
|
|
1770
|
+
},
|
|
1771
|
+
includeCustomColors: {
|
|
1772
|
+
description: "Include custom colors from image (default: true)",
|
|
1773
|
+
type: "boolean"
|
|
1774
|
+
},
|
|
1775
|
+
isDark: {
|
|
1776
|
+
description: "Generate dark theme (default: false for light theme)",
|
|
1777
|
+
type: "boolean"
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
required: ["imageData"],
|
|
1781
|
+
type: "object"
|
|
1782
|
+
},
|
|
1783
|
+
name: "generate_theme_from_image"
|
|
1784
|
+
};
|
|
1785
|
+
function formatAsCSS(colors) {
|
|
1786
|
+
let css = ":root {\n";
|
|
1787
|
+
colors.forEach((color, index) => {
|
|
1788
|
+
css += ` --extracted-color-${index + 1}: ${color.hex}; /* ${color.percentage.toFixed(1)}% */
|
|
1789
|
+
`;
|
|
1790
|
+
});
|
|
1791
|
+
css += "}\n\n";
|
|
1792
|
+
css += "/* Color Details */\n";
|
|
1793
|
+
colors.forEach((color, index) => {
|
|
1794
|
+
css += `/* Color ${index + 1}: ${color.hex}
|
|
1795
|
+
`;
|
|
1796
|
+
css += ` RGB: rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b})
|
|
1797
|
+
`;
|
|
1798
|
+
css += ` HCT: hct(${color.hct.h.toFixed(1)}, ${color.hct.c.toFixed(1)}, ${color.hct.t.toFixed(1)})
|
|
1799
|
+
`;
|
|
1800
|
+
css += ` Population: ${color.percentage.toFixed(1)}%
|
|
1801
|
+
`;
|
|
1802
|
+
css += `*/
|
|
1803
|
+
|
|
1804
|
+
`;
|
|
1805
|
+
});
|
|
1806
|
+
return css;
|
|
1807
|
+
}
|
|
1808
|
+
function formatAsJSON(colors) {
|
|
1809
|
+
return JSON.stringify(colors, null, 2);
|
|
1810
|
+
}
|
|
1811
|
+
function formatAsPalette(colors) {
|
|
1812
|
+
let output = "# Extracted Color Palette\n\n";
|
|
1813
|
+
colors.forEach((color, index) => {
|
|
1814
|
+
output += `## Color ${index + 1}
|
|
1815
|
+
`;
|
|
1816
|
+
output += `- Hex: ${color.hex}
|
|
1817
|
+
`;
|
|
1818
|
+
output += `- RGB: rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b})
|
|
1819
|
+
`;
|
|
1820
|
+
output += `- HCT: H:${color.hct.h.toFixed(1)}\xB0 C:${color.hct.c.toFixed(1)} T:${color.hct.t.toFixed(1)}
|
|
1821
|
+
`;
|
|
1822
|
+
output += `- Usage: ${color.percentage.toFixed(1)}%
|
|
1823
|
+
|
|
1824
|
+
`;
|
|
1825
|
+
});
|
|
1826
|
+
return output;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/tools/material-theme.tools.ts
|
|
1830
|
+
import { z as z6 } from "zod";
|
|
1831
|
+
var generateMaterialThemeTool = {
|
|
1832
|
+
description: "Generate a complete Material Design 3 color theme from a source color",
|
|
1833
|
+
execute: async (args) => {
|
|
1834
|
+
const source = parseColor(args.sourceColor);
|
|
1835
|
+
if (!source) {
|
|
1836
|
+
return `Invalid color format: ${args.sourceColor}`;
|
|
1837
|
+
}
|
|
1838
|
+
const corePalette = corePaletteFromRgb(source);
|
|
1839
|
+
const lightTheme = {
|
|
1840
|
+
background: rgbToHex(corePalette.neutral.tone(99)),
|
|
1841
|
+
error: rgbToHex(corePalette.error.tone(40)),
|
|
1842
|
+
errorContainer: rgbToHex(corePalette.error.tone(90)),
|
|
1843
|
+
onBackground: rgbToHex(corePalette.neutral.tone(10)),
|
|
1844
|
+
onError: rgbToHex(corePalette.error.tone(100)),
|
|
1845
|
+
onErrorContainer: rgbToHex(corePalette.error.tone(10)),
|
|
1846
|
+
onPrimary: rgbToHex(corePalette.primary.tone(100)),
|
|
1847
|
+
onPrimaryContainer: rgbToHex(corePalette.primary.tone(10)),
|
|
1848
|
+
onSecondary: rgbToHex(corePalette.secondary.tone(100)),
|
|
1849
|
+
onSecondaryContainer: rgbToHex(corePalette.secondary.tone(10)),
|
|
1850
|
+
onSurface: rgbToHex(corePalette.neutral.tone(10)),
|
|
1851
|
+
onSurfaceVariant: rgbToHex(corePalette.neutralVariant.tone(30)),
|
|
1852
|
+
onTertiary: rgbToHex(corePalette.tertiary.tone(100)),
|
|
1853
|
+
onTertiaryContainer: rgbToHex(corePalette.tertiary.tone(10)),
|
|
1854
|
+
outline: rgbToHex(corePalette.neutralVariant.tone(50)),
|
|
1855
|
+
primary: rgbToHex(corePalette.primary.tone(40)),
|
|
1856
|
+
primaryContainer: rgbToHex(corePalette.primary.tone(90)),
|
|
1857
|
+
secondary: rgbToHex(corePalette.secondary.tone(40)),
|
|
1858
|
+
secondaryContainer: rgbToHex(corePalette.secondary.tone(90)),
|
|
1859
|
+
surface: rgbToHex(corePalette.neutral.tone(99)),
|
|
1860
|
+
surfaceVariant: rgbToHex(corePalette.neutralVariant.tone(90)),
|
|
1861
|
+
tertiary: rgbToHex(corePalette.tertiary.tone(40)),
|
|
1862
|
+
tertiaryContainer: rgbToHex(corePalette.tertiary.tone(90))
|
|
1863
|
+
};
|
|
1864
|
+
const darkTheme = {
|
|
1865
|
+
background: rgbToHex(corePalette.neutral.tone(10)),
|
|
1866
|
+
error: rgbToHex(corePalette.error.tone(80)),
|
|
1867
|
+
errorContainer: rgbToHex(corePalette.error.tone(30)),
|
|
1868
|
+
onBackground: rgbToHex(corePalette.neutral.tone(90)),
|
|
1869
|
+
onError: rgbToHex(corePalette.error.tone(20)),
|
|
1870
|
+
onErrorContainer: rgbToHex(corePalette.error.tone(90)),
|
|
1871
|
+
onPrimary: rgbToHex(corePalette.primary.tone(20)),
|
|
1872
|
+
onPrimaryContainer: rgbToHex(corePalette.primary.tone(90)),
|
|
1873
|
+
onSecondary: rgbToHex(corePalette.secondary.tone(20)),
|
|
1874
|
+
onSecondaryContainer: rgbToHex(corePalette.secondary.tone(90)),
|
|
1875
|
+
onSurface: rgbToHex(corePalette.neutral.tone(90)),
|
|
1876
|
+
onSurfaceVariant: rgbToHex(corePalette.neutralVariant.tone(80)),
|
|
1877
|
+
onTertiary: rgbToHex(corePalette.tertiary.tone(20)),
|
|
1878
|
+
onTertiaryContainer: rgbToHex(corePalette.tertiary.tone(90)),
|
|
1879
|
+
outline: rgbToHex(corePalette.neutralVariant.tone(60)),
|
|
1880
|
+
primary: rgbToHex(corePalette.primary.tone(80)),
|
|
1881
|
+
primaryContainer: rgbToHex(corePalette.primary.tone(30)),
|
|
1882
|
+
secondary: rgbToHex(corePalette.secondary.tone(80)),
|
|
1883
|
+
secondaryContainer: rgbToHex(corePalette.secondary.tone(30)),
|
|
1884
|
+
surface: rgbToHex(corePalette.neutral.tone(10)),
|
|
1885
|
+
surfaceVariant: rgbToHex(corePalette.neutralVariant.tone(30)),
|
|
1886
|
+
tertiary: rgbToHex(corePalette.tertiary.tone(80)),
|
|
1887
|
+
tertiaryContainer: rgbToHex(corePalette.tertiary.tone(30))
|
|
1888
|
+
};
|
|
1889
|
+
let result = `Material Design 3 Theme
|
|
1890
|
+
Source Color: ${args.sourceColor}
|
|
1891
|
+
|
|
1892
|
+
LIGHT THEME:
|
|
1893
|
+
${Object.entries(lightTheme).map(([key, value]) => ` ${key}: ${value}`).join("\n")}
|
|
1894
|
+
|
|
1895
|
+
DARK THEME:
|
|
1896
|
+
${Object.entries(darkTheme).map(([key, value]) => ` ${key}: ${value}`).join("\n")}`;
|
|
1897
|
+
if (args.includeCustomColors) {
|
|
1898
|
+
result += `
|
|
1899
|
+
|
|
1900
|
+
TONAL PALETTES:
|
|
1901
|
+
Primary: ${[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100].map((t) => rgbToHex(corePalette.primary.tone(t))).join(", ")}
|
|
1902
|
+
Secondary: ${[40, 80].map((t) => rgbToHex(corePalette.secondary.tone(t))).join(", ")}
|
|
1903
|
+
Tertiary: ${[40, 80].map((t) => rgbToHex(corePalette.tertiary.tone(t))).join(", ")}`;
|
|
1904
|
+
}
|
|
1905
|
+
return result;
|
|
1906
|
+
},
|
|
1907
|
+
name: "generate_material_theme",
|
|
1908
|
+
parameters: z6.object({
|
|
1909
|
+
includeCustomColors: z6.boolean().optional().default(false).describe("Include custom color palettes"),
|
|
1910
|
+
sourceColor: z6.string().describe("Source color for theme generation")
|
|
1911
|
+
})
|
|
1912
|
+
};
|
|
1913
|
+
var harmonizeColorsTool = {
|
|
1914
|
+
description: "Harmonize colors to work better together using Material Design algorithms",
|
|
1915
|
+
execute: async (args) => {
|
|
1916
|
+
const colors = args.colors.map((c) => parseColor(c));
|
|
1917
|
+
if (colors.some((c) => c === null)) {
|
|
1918
|
+
return "One or more invalid color formats";
|
|
1919
|
+
}
|
|
1920
|
+
const validColors = colors;
|
|
1921
|
+
const method = args.method || "harmonize";
|
|
1922
|
+
const factor = args.factor || 0.5;
|
|
1923
|
+
const results = [];
|
|
1924
|
+
switch (method) {
|
|
1925
|
+
case "blend": {
|
|
1926
|
+
let result = validColors[0];
|
|
1927
|
+
for (let i = 1; i < validColors.length; i++) {
|
|
1928
|
+
result = blend(result, validColors[i], factor);
|
|
1929
|
+
}
|
|
1930
|
+
results.push(rgbToHex(result));
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
case "harmonize": {
|
|
1934
|
+
const source = validColors[0];
|
|
1935
|
+
results.push(rgbToHex(source));
|
|
1936
|
+
for (let i = 1; i < validColors.length; i++) {
|
|
1937
|
+
results.push(rgbToHex(harmonize(validColors[i], source, factor)));
|
|
1938
|
+
}
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
case "temperature": {
|
|
1942
|
+
const amount = (factor - 0.5) * 2;
|
|
1943
|
+
for (const color of validColors) {
|
|
1944
|
+
results.push(rgbToHex(adjustTemperature(color, amount)));
|
|
1945
|
+
}
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return `Harmonized Colors (${method}):
|
|
1950
|
+
Original: ${args.colors.join(", ")}
|
|
1951
|
+
Result: ${results.join(", ")}`;
|
|
1952
|
+
},
|
|
1953
|
+
name: "harmonize_colors",
|
|
1954
|
+
parameters: z6.object({
|
|
1955
|
+
colors: z6.array(z6.string()).min(2).max(10).describe("Array of colors to harmonize"),
|
|
1956
|
+
factor: z6.number().min(0).max(1).optional().default(0.5).describe("Harmonization strength (0-1)"),
|
|
1957
|
+
method: z6.enum(["blend", "harmonize", "temperature"]).optional().default("harmonize").describe("Harmonization method")
|
|
1958
|
+
})
|
|
1959
|
+
};
|
|
1960
|
+
var generateTonalPaletteTool = {
|
|
1961
|
+
description: "Generate a Material Design tonal palette from a color",
|
|
1962
|
+
execute: async (args) => {
|
|
1963
|
+
const rgb = parseColor(args.color);
|
|
1964
|
+
if (!rgb) {
|
|
1965
|
+
return `Invalid color format: ${args.color}`;
|
|
1966
|
+
}
|
|
1967
|
+
const palette = TonalPalette.fromRgb(rgb);
|
|
1968
|
+
const tones = args.tones || [
|
|
1969
|
+
0,
|
|
1970
|
+
10,
|
|
1971
|
+
20,
|
|
1972
|
+
30,
|
|
1973
|
+
40,
|
|
1974
|
+
50,
|
|
1975
|
+
60,
|
|
1976
|
+
70,
|
|
1977
|
+
80,
|
|
1978
|
+
90,
|
|
1979
|
+
95,
|
|
1980
|
+
99,
|
|
1981
|
+
100
|
|
1982
|
+
];
|
|
1983
|
+
const hct = rgbToHct(rgb);
|
|
1984
|
+
const colors = tones.map((tone) => ({
|
|
1985
|
+
hex: rgbToHex(palette.tone(tone)),
|
|
1986
|
+
tone
|
|
1987
|
+
}));
|
|
1988
|
+
return `Tonal Palette for ${args.color}
|
|
1989
|
+
HCT: h=${hct.h.toFixed(1)}\xB0, c=${hct.c.toFixed(1)}, t=${hct.t.toFixed(1)}
|
|
1990
|
+
|
|
1991
|
+
${colors.map(({ hex, tone }) => `Tone ${tone}: ${hex}`).join("\n")}`;
|
|
1992
|
+
},
|
|
1993
|
+
name: "generate_tonal_palette",
|
|
1994
|
+
parameters: z6.object({
|
|
1995
|
+
color: z6.string().describe("Base color for palette"),
|
|
1996
|
+
tones: z6.array(z6.number()).optional().describe("Custom tone values (default: Material standard tones)")
|
|
1997
|
+
})
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// src/tools/palette-generator.tool.ts
|
|
2001
|
+
import { z as z7 } from "zod";
|
|
2002
|
+
var paletteGeneratorTool = {
|
|
2003
|
+
description: "Generate a color palette from a base color",
|
|
2004
|
+
execute: async (args) => {
|
|
2005
|
+
const base = parseColor(args.baseColor);
|
|
2006
|
+
if (!base) {
|
|
2007
|
+
return `Invalid color format: ${args.baseColor}`;
|
|
2008
|
+
}
|
|
2009
|
+
const type = args.type || "monochromatic";
|
|
2010
|
+
const count = args.count || 5;
|
|
2011
|
+
const hsl = rgbToHsl(base);
|
|
2012
|
+
const palette = [];
|
|
2013
|
+
switch (type) {
|
|
2014
|
+
case "analogous": {
|
|
2015
|
+
const step = 30;
|
|
2016
|
+
for (let i = 0; i < count; i++) {
|
|
2017
|
+
const h = (hsl.h + (i - Math.floor(count / 2)) * step + 360) % 360;
|
|
2018
|
+
const color = parseColor(`hsl(${h}, ${hsl.s}%, ${hsl.l}%)`);
|
|
2019
|
+
if (color) palette.push(rgbToHex(color));
|
|
2020
|
+
}
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
case "complementary": {
|
|
2024
|
+
palette.push(rgbToHex(base));
|
|
2025
|
+
const complement = parseColor(
|
|
2026
|
+
`hsl(${(hsl.h + 180) % 360}, ${hsl.s}%, ${hsl.l}%)`
|
|
2027
|
+
);
|
|
2028
|
+
if (complement) palette.push(rgbToHex(complement));
|
|
2029
|
+
for (let i = 2; i < count; i++) {
|
|
2030
|
+
const l = hsl.l + (i % 2 === 0 ? 20 : -20);
|
|
2031
|
+
const h = i < count / 2 ? hsl.h : (hsl.h + 180) % 360;
|
|
2032
|
+
const color = parseColor(
|
|
2033
|
+
`hsl(${h}, ${hsl.s}%, ${Math.max(10, Math.min(90, l))}%)`
|
|
2034
|
+
);
|
|
2035
|
+
if (color) palette.push(rgbToHex(color));
|
|
2036
|
+
}
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
case "monochromatic": {
|
|
2040
|
+
const step = 80 / (count - 1);
|
|
2041
|
+
for (let i = 0; i < count; i++) {
|
|
2042
|
+
const l = 10 + i * step;
|
|
2043
|
+
const color = parseColor(`hsl(${hsl.h}, ${hsl.s}%, ${l}%)`);
|
|
2044
|
+
if (color) palette.push(rgbToHex(color));
|
|
2045
|
+
}
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
case "tetradic": {
|
|
2049
|
+
for (let i = 0; i < Math.min(4, count); i++) {
|
|
2050
|
+
const h = (hsl.h + i * 90) % 360;
|
|
2051
|
+
const color = parseColor(`hsl(${h}, ${hsl.s}%, ${hsl.l}%)`);
|
|
2052
|
+
if (color) palette.push(rgbToHex(color));
|
|
2053
|
+
}
|
|
2054
|
+
for (let i = 4; i < count; i++) {
|
|
2055
|
+
const baseIdx = i % 4;
|
|
2056
|
+
const h = (hsl.h + baseIdx * 90) % 360;
|
|
2057
|
+
const l = hsl.l + (i < 8 ? 15 : -15);
|
|
2058
|
+
const color = parseColor(
|
|
2059
|
+
`hsl(${h}, ${hsl.s}%, ${Math.max(10, Math.min(90, l))}%)`
|
|
2060
|
+
);
|
|
2061
|
+
if (color) palette.push(rgbToHex(color));
|
|
2062
|
+
}
|
|
2063
|
+
break;
|
|
2064
|
+
}
|
|
2065
|
+
case "triadic": {
|
|
2066
|
+
for (let i = 0; i < Math.min(3, count); i++) {
|
|
2067
|
+
const h = (hsl.h + i * 120) % 360;
|
|
2068
|
+
const color = parseColor(`hsl(${h}, ${hsl.s}%, ${hsl.l}%)`);
|
|
2069
|
+
if (color) palette.push(rgbToHex(color));
|
|
2070
|
+
}
|
|
2071
|
+
for (let i = 3; i < count; i++) {
|
|
2072
|
+
const baseIdx = i % 3;
|
|
2073
|
+
const h = (hsl.h + baseIdx * 120) % 360;
|
|
2074
|
+
const l = hsl.l + (i < 6 ? 20 : -20);
|
|
2075
|
+
const color = parseColor(
|
|
2076
|
+
`hsl(${h}, ${hsl.s}%, ${Math.max(10, Math.min(90, l))}%)`
|
|
2077
|
+
);
|
|
2078
|
+
if (color) palette.push(rgbToHex(color));
|
|
2079
|
+
}
|
|
2080
|
+
break;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return `Generated ${type} palette:
|
|
2084
|
+
${palette.map((color, i) => `${i + 1}. ${color}`).join("\n")}`;
|
|
2085
|
+
},
|
|
2086
|
+
name: "generate_palette",
|
|
2087
|
+
parameters: z7.object({
|
|
2088
|
+
baseColor: z7.string().describe("Base color for palette generation"),
|
|
2089
|
+
count: z7.number().min(3).max(10).default(5).describe("Number of colors to generate"),
|
|
2090
|
+
type: z7.enum([
|
|
2091
|
+
"monochromatic",
|
|
2092
|
+
"analogous",
|
|
2093
|
+
"complementary",
|
|
2094
|
+
"triadic",
|
|
2095
|
+
"tetradic"
|
|
2096
|
+
]).default("monochromatic").describe("Type of color palette")
|
|
2097
|
+
})
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
// src/tools/palette-with-locks.tool.ts
|
|
2101
|
+
import { z as z8 } from "zod";
|
|
2102
|
+
var paletteWithLocksTool = {
|
|
2103
|
+
description: "Generate a color palette while preserving specific locked colors",
|
|
2104
|
+
execute: async (args) => {
|
|
2105
|
+
const {
|
|
2106
|
+
colorSpace = "hsl",
|
|
2107
|
+
lockedColors,
|
|
2108
|
+
mode = "harmony",
|
|
2109
|
+
totalColors
|
|
2110
|
+
} = args;
|
|
2111
|
+
if (lockedColors.length >= totalColors) {
|
|
2112
|
+
return `Error: Number of locked colors (${lockedColors.length}) must be less than total colors (${totalColors})`;
|
|
2113
|
+
}
|
|
2114
|
+
const parsedLocked = [];
|
|
2115
|
+
for (const color of lockedColors) {
|
|
2116
|
+
const parsed = parseColor(color);
|
|
2117
|
+
if (!parsed) {
|
|
2118
|
+
return `Invalid color format: ${color}`;
|
|
2119
|
+
}
|
|
2120
|
+
parsedLocked.push(parsed);
|
|
2121
|
+
}
|
|
2122
|
+
const palette = [];
|
|
2123
|
+
const remainingSlots = totalColors - lockedColors.length;
|
|
2124
|
+
lockedColors.forEach((color) => palette.push(color));
|
|
2125
|
+
switch (mode) {
|
|
2126
|
+
case "contrast": {
|
|
2127
|
+
for (let i = 0; i < remainingSlots; i++) {
|
|
2128
|
+
let bestColor = null;
|
|
2129
|
+
let maxMinDistance = 0;
|
|
2130
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
2131
|
+
const candidate = {
|
|
2132
|
+
b: Math.floor(Math.random() * 256),
|
|
2133
|
+
g: Math.floor(Math.random() * 256),
|
|
2134
|
+
r: Math.floor(Math.random() * 256)
|
|
2135
|
+
};
|
|
2136
|
+
let minDistance = Infinity;
|
|
2137
|
+
for (const locked of parsedLocked) {
|
|
2138
|
+
const dist = colorDistance(candidate, locked, "deltaE2000");
|
|
2139
|
+
if (dist < minDistance) minDistance = dist;
|
|
2140
|
+
}
|
|
2141
|
+
if (minDistance > maxMinDistance) {
|
|
2142
|
+
maxMinDistance = minDistance;
|
|
2143
|
+
bestColor = candidate;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if (bestColor) {
|
|
2147
|
+
palette.push(rgbToHex(bestColor));
|
|
2148
|
+
parsedLocked.push(bestColor);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
case "gradient": {
|
|
2154
|
+
if (parsedLocked.length < 2) {
|
|
2155
|
+
return "Gradient mode requires at least 2 locked colors";
|
|
2156
|
+
}
|
|
2157
|
+
const steps = Math.floor(remainingSlots / (parsedLocked.length - 1));
|
|
2158
|
+
for (let i = 0; i < parsedLocked.length - 1; i++) {
|
|
2159
|
+
const start = parsedLocked[i];
|
|
2160
|
+
const end = parsedLocked[i + 1];
|
|
2161
|
+
for (let step = 1; step <= steps; step++) {
|
|
2162
|
+
const t = step / (steps + 1);
|
|
2163
|
+
if (colorSpace === "lab") {
|
|
2164
|
+
const startLab = rgbToLab(start);
|
|
2165
|
+
const endLab = rgbToLab(end);
|
|
2166
|
+
const interpolated = labToRgb({
|
|
2167
|
+
a: startLab.a + (endLab.a - startLab.a) * t,
|
|
2168
|
+
b: startLab.b + (endLab.b - startLab.b) * t,
|
|
2169
|
+
l: startLab.l + (endLab.l - startLab.l) * t
|
|
2170
|
+
});
|
|
2171
|
+
palette.push(rgbToHex(interpolated));
|
|
2172
|
+
} else {
|
|
2173
|
+
const startHsl = rgbToHsl(start);
|
|
2174
|
+
const endHsl = rgbToHsl(end);
|
|
2175
|
+
let hueDiff = endHsl.h - startHsl.h;
|
|
2176
|
+
if (hueDiff > 180) hueDiff -= 360;
|
|
2177
|
+
if (hueDiff < -180) hueDiff += 360;
|
|
2178
|
+
const interpolated = hslToRgb({
|
|
2179
|
+
h: (startHsl.h + hueDiff * t + 360) % 360,
|
|
2180
|
+
l: startHsl.l + (endHsl.l - startHsl.l) * t,
|
|
2181
|
+
s: startHsl.s + (endHsl.s - startHsl.s) * t
|
|
2182
|
+
});
|
|
2183
|
+
palette.push(rgbToHex(interpolated));
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
break;
|
|
2188
|
+
}
|
|
2189
|
+
case "harmony": {
|
|
2190
|
+
const baseHsl = rgbToHsl(parsedLocked[0]);
|
|
2191
|
+
const hueStep = 360 / totalColors;
|
|
2192
|
+
for (let i = 0; i < remainingSlots; i++) {
|
|
2193
|
+
let newHue = baseHsl.h;
|
|
2194
|
+
let attempts = 0;
|
|
2195
|
+
let bestColor = null;
|
|
2196
|
+
let maxMinDistance = 0;
|
|
2197
|
+
while (attempts < 36) {
|
|
2198
|
+
newHue = (baseHsl.h + attempts * 10) % 360;
|
|
2199
|
+
const candidate = hslToRgb({
|
|
2200
|
+
h: newHue,
|
|
2201
|
+
l: baseHsl.l + (Math.random() - 0.5) * 20,
|
|
2202
|
+
s: baseHsl.s + (Math.random() - 0.5) * 20
|
|
2203
|
+
});
|
|
2204
|
+
let minDistance = Infinity;
|
|
2205
|
+
for (const locked of parsedLocked) {
|
|
2206
|
+
const dist = colorDistance(candidate, locked, "deltaE2000");
|
|
2207
|
+
if (dist < minDistance) minDistance = dist;
|
|
2208
|
+
}
|
|
2209
|
+
if (minDistance > maxMinDistance && minDistance > 10) {
|
|
2210
|
+
maxMinDistance = minDistance;
|
|
2211
|
+
bestColor = candidate;
|
|
2212
|
+
}
|
|
2213
|
+
attempts++;
|
|
2214
|
+
}
|
|
2215
|
+
if (bestColor) {
|
|
2216
|
+
palette.push(rgbToHex(bestColor));
|
|
2217
|
+
parsedLocked.push(bestColor);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
const result = [];
|
|
2224
|
+
const lockedSet = new Set(lockedColors.map((c) => c.toLowerCase()));
|
|
2225
|
+
palette.forEach((color) => {
|
|
2226
|
+
if (lockedSet.has(color.toLowerCase())) {
|
|
2227
|
+
result.push(`${color} (locked)`);
|
|
2228
|
+
} else {
|
|
2229
|
+
result.push(color);
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
return `Generated palette with ${lockedColors.length} locked colors:
|
|
2233
|
+
${result.map((color, i) => `${i + 1}. ${color}`).join("\n")}
|
|
2234
|
+
|
|
2235
|
+
Mode: ${mode}
|
|
2236
|
+
Color space: ${colorSpace}
|
|
2237
|
+
Total colors: ${totalColors}`;
|
|
2238
|
+
},
|
|
2239
|
+
name: "generate_palette_with_locks",
|
|
2240
|
+
parameters: z8.object({
|
|
2241
|
+
colorSpace: z8.enum(["hsl", "lab"]).default("hsl").describe("Color space for interpolation (affects gradient smoothness)"),
|
|
2242
|
+
lockedColors: z8.array(z8.string()).min(1).describe("Colors that must be included in the palette"),
|
|
2243
|
+
mode: z8.enum(["harmony", "contrast", "gradient"]).default("harmony").describe(
|
|
2244
|
+
"Generation mode: harmony (similar), contrast (different), or gradient (smooth transition)"
|
|
2245
|
+
),
|
|
2246
|
+
totalColors: z8.number().min(2).max(20).describe("Total number of colors in the final palette")
|
|
2247
|
+
})
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
// src/tools/theme-matching.tools.ts
|
|
2251
|
+
import { z as z9 } from "zod";
|
|
2252
|
+
|
|
2253
|
+
// src/theme/matcher.ts
|
|
2254
|
+
var DEFAULT_WEIGHTS = {
|
|
2255
|
+
accessibility: 0.2,
|
|
2256
|
+
perceptual: 0.6,
|
|
2257
|
+
semantic: 0.2
|
|
2258
|
+
};
|
|
2259
|
+
var CONTEXT_MAX_DISTANCES = {
|
|
2260
|
+
accent: 15,
|
|
2261
|
+
// Moderate for brand consistency
|
|
2262
|
+
background: 15,
|
|
2263
|
+
// Moderate flexibility
|
|
2264
|
+
border: 20,
|
|
2265
|
+
// More flexibility
|
|
2266
|
+
decorative: 30,
|
|
2267
|
+
// Most flexible
|
|
2268
|
+
shadow: 25,
|
|
2269
|
+
// Even more flexibility
|
|
2270
|
+
text: 10
|
|
2271
|
+
// Strict for readability
|
|
2272
|
+
};
|
|
2273
|
+
function findBatchMatches(colors, themeVariables, options = {}) {
|
|
2274
|
+
const results = /* @__PURE__ */ new Map();
|
|
2275
|
+
for (const color of colors) {
|
|
2276
|
+
results.set(color, findClosestThemeColor(color, themeVariables, options));
|
|
2277
|
+
}
|
|
2278
|
+
return results;
|
|
2279
|
+
}
|
|
2280
|
+
function findClosestThemeColor(inputColor, themeVariables, options = {}) {
|
|
2281
|
+
let inputHct;
|
|
2282
|
+
try {
|
|
2283
|
+
const rgb = parseColorToRgb(inputColor);
|
|
2284
|
+
inputHct = rgbToHct(rgb);
|
|
2285
|
+
} catch {
|
|
2286
|
+
console.error(`Failed to parse input color: ${inputColor}`);
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
const allVariables = flattenThemeVariables(themeVariables);
|
|
2290
|
+
if (allVariables.length === 0) {
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
const candidates = allVariables.map((variable) => ({
|
|
2294
|
+
distance: calculateHctDistance(inputHct, variable.hct),
|
|
2295
|
+
score: 0,
|
|
2296
|
+
// Will be calculated next
|
|
2297
|
+
variable
|
|
2298
|
+
}));
|
|
2299
|
+
const maxDistance = options.maxDistance ?? (options.contextType ? CONTEXT_MAX_DISTANCES[options.contextType] : 30);
|
|
2300
|
+
const validCandidates = candidates.filter((c) => c.distance <= maxDistance);
|
|
2301
|
+
if (validCandidates.length === 0) {
|
|
2302
|
+
return null;
|
|
2303
|
+
}
|
|
2304
|
+
const weights = options.weights ?? DEFAULT_WEIGHTS;
|
|
2305
|
+
for (const candidate of validCandidates) {
|
|
2306
|
+
candidate.score = calculateMatchScore(
|
|
2307
|
+
inputHct,
|
|
2308
|
+
candidate.variable,
|
|
2309
|
+
candidate.distance,
|
|
2310
|
+
weights,
|
|
2311
|
+
options
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
validCandidates.sort((a, b) => b.score - a.score);
|
|
2315
|
+
const best = validCandidates[0];
|
|
2316
|
+
const alternatives = validCandidates.slice(1, 4).map((c) => ({
|
|
2317
|
+
alternatives: [],
|
|
2318
|
+
confidence: calculateConfidence(c.distance, c.score),
|
|
2319
|
+
distance: c.distance,
|
|
2320
|
+
semanticRole: c.variable.role,
|
|
2321
|
+
value: c.variable.value,
|
|
2322
|
+
variable: c.variable.name
|
|
2323
|
+
}));
|
|
2324
|
+
const accessibilityInfo = options.contextType === "text" || options.contextType === "background" ? calculateAccessibilityInfo(best.variable.value, themeVariables) : void 0;
|
|
2325
|
+
return {
|
|
2326
|
+
accessibilityInfo,
|
|
2327
|
+
alternatives,
|
|
2328
|
+
confidence: calculateConfidence(best.distance, best.score),
|
|
2329
|
+
distance: best.distance,
|
|
2330
|
+
semanticRole: best.variable.role,
|
|
2331
|
+
value: best.variable.value,
|
|
2332
|
+
variable: best.variable.name
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
function calculateAccessibilityInfo(color, themeVariables) {
|
|
2336
|
+
const rgb = parseColorToRgb(color);
|
|
2337
|
+
const backgrounds = findTypicalBackgrounds(themeVariables);
|
|
2338
|
+
const foregrounds = findTypicalForegrounds(themeVariables);
|
|
2339
|
+
let maxContrastBg = 0;
|
|
2340
|
+
let maxContrastFg = 0;
|
|
2341
|
+
for (const bg of backgrounds) {
|
|
2342
|
+
const bgRgb = parseColorToRgb(bg.value);
|
|
2343
|
+
const contrast = getContrastRatio(rgb, bgRgb);
|
|
2344
|
+
maxContrastBg = Math.max(maxContrastBg, contrast);
|
|
2345
|
+
}
|
|
2346
|
+
for (const fg of foregrounds) {
|
|
2347
|
+
const fgRgb = parseColorToRgb(fg.value);
|
|
2348
|
+
const contrast = getContrastRatio(rgb, fgRgb);
|
|
2349
|
+
maxContrastFg = Math.max(maxContrastFg, contrast);
|
|
2350
|
+
}
|
|
2351
|
+
return {
|
|
2352
|
+
contrastWithBackground: maxContrastBg,
|
|
2353
|
+
contrastWithForeground: maxContrastFg,
|
|
2354
|
+
meetsAA: maxContrastBg >= 4.5 || maxContrastFg >= 4.5,
|
|
2355
|
+
meetsAAA: maxContrastBg >= 7 || maxContrastFg >= 7
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function calculateConfidence(distance, score) {
|
|
2359
|
+
if (distance < 0.5) return 100;
|
|
2360
|
+
if (distance < 1) return 98;
|
|
2361
|
+
if (distance < 2) return 95;
|
|
2362
|
+
const distanceConfidence = Math.max(0, 100 - distance * 2);
|
|
2363
|
+
const scoreConfidence = score * 100;
|
|
2364
|
+
return Math.round((distanceConfidence + scoreConfidence) / 2);
|
|
2365
|
+
}
|
|
2366
|
+
function calculateContextSemanticScore(context, role) {
|
|
2367
|
+
if (!role) return 0.5;
|
|
2368
|
+
switch (context) {
|
|
2369
|
+
case "accent":
|
|
2370
|
+
return role === "primary" || role === "secondary" ? 1 : 0.5;
|
|
2371
|
+
case "background":
|
|
2372
|
+
return role === "surface" || role === "background" ? 1 : 0.3;
|
|
2373
|
+
case "border":
|
|
2374
|
+
return role === "outline" ? 1 : role === "neutral" ? 0.4 : 0.2;
|
|
2375
|
+
case "decorative":
|
|
2376
|
+
return 0.7;
|
|
2377
|
+
// Any role is acceptable
|
|
2378
|
+
case "shadow":
|
|
2379
|
+
return role === "shadow" ? 1 : 0.3;
|
|
2380
|
+
case "text":
|
|
2381
|
+
return role === "neutral" || role === "surface" ? 0.8 : 0.5;
|
|
2382
|
+
default:
|
|
2383
|
+
return 0.5;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function calculateHctDistance(color1, color2) {
|
|
2387
|
+
const hueWeight = 1;
|
|
2388
|
+
const chromaWeight = 1;
|
|
2389
|
+
const toneWeight = 2;
|
|
2390
|
+
let hueDiff = Math.abs(color1.h - color2.h);
|
|
2391
|
+
if (hueDiff > 180) {
|
|
2392
|
+
hueDiff = 360 - hueDiff;
|
|
2393
|
+
}
|
|
2394
|
+
hueDiff = hueDiff / 180;
|
|
2395
|
+
const chromaDiff = Math.abs(color1.c - color2.c) / 120;
|
|
2396
|
+
const toneDiff = Math.abs(color1.t - color2.t) / 100;
|
|
2397
|
+
return Math.sqrt(
|
|
2398
|
+
Math.pow(hueDiff * hueWeight, 2) + Math.pow(chromaDiff * chromaWeight, 2) + Math.pow(toneDiff * toneWeight, 2)
|
|
2399
|
+
) * 100;
|
|
2400
|
+
}
|
|
2401
|
+
function calculateMatchScore(inputHct, candidate, distance, weights, options) {
|
|
2402
|
+
const perceptualScore = Math.max(0, 100 - distance) / 100;
|
|
2403
|
+
let semanticScore = 0.5;
|
|
2404
|
+
if (options.preferredRole && candidate.role) {
|
|
2405
|
+
semanticScore = candidate.role === options.preferredRole ? 1 : 0.3;
|
|
2406
|
+
} else if (options.contextType) {
|
|
2407
|
+
semanticScore = calculateContextSemanticScore(
|
|
2408
|
+
options.contextType,
|
|
2409
|
+
candidate.role
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
let accessibilityScore = 0.5;
|
|
2413
|
+
if (options.contextType === "text") {
|
|
2414
|
+
accessibilityScore = candidate.tone < 30 || candidate.tone > 70 ? 1 : 0.3;
|
|
2415
|
+
} else if (options.contextType === "background") {
|
|
2416
|
+
accessibilityScore = candidate.tone >= 90 || candidate.tone <= 10 ? 1 : 0.5;
|
|
2417
|
+
}
|
|
2418
|
+
return perceptualScore * weights.perceptual + semanticScore * weights.semantic + accessibilityScore * weights.accessibility;
|
|
2419
|
+
}
|
|
2420
|
+
function findTypicalBackgrounds(theme) {
|
|
2421
|
+
const backgrounds = [];
|
|
2422
|
+
if (theme.surface) {
|
|
2423
|
+
backgrounds.push(
|
|
2424
|
+
...theme.surface.filter((v) => v.tone >= 90 || v.tone <= 10)
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
if (theme.background) {
|
|
2428
|
+
backgrounds.push(...theme.background);
|
|
2429
|
+
}
|
|
2430
|
+
if (theme.neutral) {
|
|
2431
|
+
backgrounds.push(
|
|
2432
|
+
...theme.neutral.filter((v) => v.tone >= 95 || v.tone <= 5)
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
return backgrounds;
|
|
2436
|
+
}
|
|
2437
|
+
function findTypicalForegrounds(theme) {
|
|
2438
|
+
const foregrounds = [];
|
|
2439
|
+
if (theme.neutral) {
|
|
2440
|
+
foregrounds.push(
|
|
2441
|
+
...theme.neutral.filter(
|
|
2442
|
+
(v) => v.tone >= 20 && v.tone <= 40 || v.tone >= 60 && v.tone <= 80
|
|
2443
|
+
)
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
if (theme.surface) {
|
|
2447
|
+
foregrounds.push(
|
|
2448
|
+
...theme.surface.filter((v) => v.tone >= 10 && v.tone <= 30)
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
return foregrounds;
|
|
2452
|
+
}
|
|
2453
|
+
function flattenThemeVariables(theme) {
|
|
2454
|
+
const variables = [];
|
|
2455
|
+
if (theme.primary) variables.push(...theme.primary);
|
|
2456
|
+
if (theme.secondary) variables.push(...theme.secondary);
|
|
2457
|
+
if (theme.tertiary) variables.push(...theme.tertiary);
|
|
2458
|
+
if (theme.error) variables.push(...theme.error);
|
|
2459
|
+
if (theme.neutral) variables.push(...theme.neutral);
|
|
2460
|
+
if (theme.surface) variables.push(...theme.surface);
|
|
2461
|
+
if (theme.background) variables.push(...theme.background);
|
|
2462
|
+
if (theme.outline) variables.push(...theme.outline);
|
|
2463
|
+
if (theme.shadow) variables.push(...theme.shadow);
|
|
2464
|
+
if (theme.custom) {
|
|
2465
|
+
for (const customVars of Object.values(theme.custom)) {
|
|
2466
|
+
variables.push(...customVars);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return variables;
|
|
2470
|
+
}
|
|
2471
|
+
function parseColorToRgb(color) {
|
|
2472
|
+
color = color.trim();
|
|
2473
|
+
if (color.startsWith("#")) {
|
|
2474
|
+
return hexToRgb(color);
|
|
2475
|
+
}
|
|
2476
|
+
if (color.startsWith("rgb")) {
|
|
2477
|
+
const match = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
2478
|
+
if (match) {
|
|
2479
|
+
return {
|
|
2480
|
+
b: parseInt(match[3], 10) / 255,
|
|
2481
|
+
g: parseInt(match[2], 10) / 255,
|
|
2482
|
+
r: parseInt(match[1], 10) / 255
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
throw new Error(`Unable to parse color: ${color}`);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/theme/parser.ts
|
|
2490
|
+
function parseThemeVariables(css) {
|
|
2491
|
+
const variables = [];
|
|
2492
|
+
const varRegex = /--([\w-]+):\s*([^;]+);/g;
|
|
2493
|
+
let match;
|
|
2494
|
+
while ((match = varRegex.exec(css)) !== null) {
|
|
2495
|
+
const [, name, value] = match;
|
|
2496
|
+
const fullName = `--${name}`;
|
|
2497
|
+
const trimmedValue = value.trim();
|
|
2498
|
+
if (isColorValue(trimmedValue)) {
|
|
2499
|
+
const colorValue = normalizeColorValue(trimmedValue);
|
|
2500
|
+
if (colorValue) {
|
|
2501
|
+
try {
|
|
2502
|
+
const rgb = parseColorToRgb2(colorValue);
|
|
2503
|
+
const hct = rgbToHct(rgb);
|
|
2504
|
+
variables.push({
|
|
2505
|
+
hct,
|
|
2506
|
+
name: fullName,
|
|
2507
|
+
role: detectSemanticRole(name),
|
|
2508
|
+
tone: hct.t,
|
|
2509
|
+
value: colorValue
|
|
2510
|
+
});
|
|
2511
|
+
} catch {
|
|
2512
|
+
console.warn(
|
|
2513
|
+
`Failed to parse color variable ${fullName}: ${trimmedValue}`
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return organizeByRole(variables);
|
|
2520
|
+
}
|
|
2521
|
+
function detectSemanticRole(name) {
|
|
2522
|
+
const lower = name.toLowerCase();
|
|
2523
|
+
if (lower.includes("primary")) return "primary";
|
|
2524
|
+
if (lower.includes("secondary")) return "secondary";
|
|
2525
|
+
if (lower.includes("tertiary")) return "tertiary";
|
|
2526
|
+
if (lower.includes("error") || lower.includes("danger")) return "error";
|
|
2527
|
+
if (lower.includes("neutral") || lower.includes("gray")) return "neutral";
|
|
2528
|
+
if (lower.includes("surface")) return "surface";
|
|
2529
|
+
if (lower.includes("background") || lower.includes("bg")) return "background";
|
|
2530
|
+
if (lower.includes("outline") || lower.includes("border")) return "outline";
|
|
2531
|
+
if (lower.includes("shadow")) return "shadow";
|
|
2532
|
+
return void 0;
|
|
2533
|
+
}
|
|
2534
|
+
function hslToRgb2(h, s, l) {
|
|
2535
|
+
let b, g, r;
|
|
2536
|
+
if (s === 0) {
|
|
2537
|
+
r = g = b = l;
|
|
2538
|
+
} else {
|
|
2539
|
+
const hue2rgb = (p2, q2, t) => {
|
|
2540
|
+
if (t < 0) t += 1;
|
|
2541
|
+
if (t > 1) t -= 1;
|
|
2542
|
+
if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
|
|
2543
|
+
if (t < 1 / 2) return q2;
|
|
2544
|
+
if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
|
|
2545
|
+
return p2;
|
|
2546
|
+
};
|
|
2547
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
2548
|
+
const p = 2 * l - q;
|
|
2549
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
|
2550
|
+
g = hue2rgb(p, q, h);
|
|
2551
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
|
2552
|
+
}
|
|
2553
|
+
return { b, g, r };
|
|
2554
|
+
}
|
|
2555
|
+
function isColorValue(value) {
|
|
2556
|
+
const trimmed = value.trim();
|
|
2557
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return true;
|
|
2558
|
+
if (/^rgba?\(/.test(trimmed)) return true;
|
|
2559
|
+
if (/^hsla?\(/.test(trimmed)) return true;
|
|
2560
|
+
if (/^[a-z]+$/i.test(trimmed)) {
|
|
2561
|
+
return isNamedColor(trimmed);
|
|
2562
|
+
}
|
|
2563
|
+
return false;
|
|
2564
|
+
}
|
|
2565
|
+
function isNamedColor(name) {
|
|
2566
|
+
const namedColors = [
|
|
2567
|
+
"black",
|
|
2568
|
+
"white",
|
|
2569
|
+
"red",
|
|
2570
|
+
"green",
|
|
2571
|
+
"blue",
|
|
2572
|
+
"yellow",
|
|
2573
|
+
"cyan",
|
|
2574
|
+
"magenta",
|
|
2575
|
+
"gray",
|
|
2576
|
+
"grey",
|
|
2577
|
+
"orange",
|
|
2578
|
+
"purple",
|
|
2579
|
+
"brown",
|
|
2580
|
+
"pink",
|
|
2581
|
+
"lime",
|
|
2582
|
+
"navy",
|
|
2583
|
+
"teal",
|
|
2584
|
+
"silver",
|
|
2585
|
+
"gold",
|
|
2586
|
+
"indigo",
|
|
2587
|
+
"violet",
|
|
2588
|
+
"turquoise",
|
|
2589
|
+
"coral"
|
|
2590
|
+
];
|
|
2591
|
+
return namedColors.includes(name.toLowerCase());
|
|
2592
|
+
}
|
|
2593
|
+
function namedColorToHex(name) {
|
|
2594
|
+
const colors = {
|
|
2595
|
+
black: "#000000",
|
|
2596
|
+
blue: "#0000ff",
|
|
2597
|
+
brown: "#a52a2a",
|
|
2598
|
+
coral: "#ff7f50",
|
|
2599
|
+
cyan: "#00ffff",
|
|
2600
|
+
gold: "#ffd700",
|
|
2601
|
+
gray: "#808080",
|
|
2602
|
+
green: "#008000",
|
|
2603
|
+
grey: "#808080",
|
|
2604
|
+
indigo: "#4b0082",
|
|
2605
|
+
lime: "#00ff00",
|
|
2606
|
+
magenta: "#ff00ff",
|
|
2607
|
+
navy: "#000080",
|
|
2608
|
+
orange: "#ffa500",
|
|
2609
|
+
pink: "#ffc0cb",
|
|
2610
|
+
purple: "#800080",
|
|
2611
|
+
red: "#ff0000",
|
|
2612
|
+
silver: "#c0c0c0",
|
|
2613
|
+
teal: "#008080",
|
|
2614
|
+
turquoise: "#40e0d0",
|
|
2615
|
+
violet: "#ee82ee",
|
|
2616
|
+
white: "#ffffff",
|
|
2617
|
+
yellow: "#ffff00"
|
|
2618
|
+
};
|
|
2619
|
+
return colors[name.toLowerCase()] || "#000000";
|
|
2620
|
+
}
|
|
2621
|
+
function normalizeColorValue(value) {
|
|
2622
|
+
const trimmed = value.trim();
|
|
2623
|
+
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) {
|
|
2624
|
+
return trimmed;
|
|
2625
|
+
}
|
|
2626
|
+
if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
|
|
2627
|
+
const [, r, g, b] = trimmed.match(/#(.)(.)(.)/);
|
|
2628
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
2629
|
+
}
|
|
2630
|
+
if (/^rgba?\(/.test(trimmed)) {
|
|
2631
|
+
return parseRgbString(trimmed);
|
|
2632
|
+
}
|
|
2633
|
+
if (/^hsla?\(/.test(trimmed)) {
|
|
2634
|
+
return parseHslString(trimmed);
|
|
2635
|
+
}
|
|
2636
|
+
if (isNamedColor(trimmed)) {
|
|
2637
|
+
return namedColorToHex(trimmed);
|
|
2638
|
+
}
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
function organizeByRole(variables) {
|
|
2642
|
+
const organized = {
|
|
2643
|
+
custom: {}
|
|
2644
|
+
};
|
|
2645
|
+
for (const variable of variables) {
|
|
2646
|
+
if (variable.role) {
|
|
2647
|
+
const role = variable.role;
|
|
2648
|
+
if (!organized[role]) {
|
|
2649
|
+
organized[role] = [];
|
|
2650
|
+
}
|
|
2651
|
+
organized[role].push(variable);
|
|
2652
|
+
} else {
|
|
2653
|
+
const colorPattern = variable.name.match(/--(?:color-)?([a-z]+)-\d+/i);
|
|
2654
|
+
const namedPattern = variable.name.match(/--([a-z]+)-[a-z]+/i);
|
|
2655
|
+
if (colorPattern) {
|
|
2656
|
+
const customRole = colorPattern[1].toLowerCase();
|
|
2657
|
+
if (!organized.custom[customRole]) {
|
|
2658
|
+
organized.custom[customRole] = [];
|
|
2659
|
+
}
|
|
2660
|
+
organized.custom[customRole].push(variable);
|
|
2661
|
+
} else if (namedPattern) {
|
|
2662
|
+
const customRole = namedPattern[1].toLowerCase();
|
|
2663
|
+
const knownPatterns = [
|
|
2664
|
+
"accent",
|
|
2665
|
+
"neutral",
|
|
2666
|
+
"success",
|
|
2667
|
+
"warning",
|
|
2668
|
+
"info"
|
|
2669
|
+
];
|
|
2670
|
+
if (knownPatterns.some((p) => customRole.includes(p))) {
|
|
2671
|
+
if (!organized.custom[customRole]) {
|
|
2672
|
+
organized.custom[customRole] = [];
|
|
2673
|
+
}
|
|
2674
|
+
organized.custom[customRole].push(variable);
|
|
2675
|
+
} else {
|
|
2676
|
+
if (!organized.custom["uncategorized"]) {
|
|
2677
|
+
organized.custom["uncategorized"] = [];
|
|
2678
|
+
}
|
|
2679
|
+
organized.custom["uncategorized"].push(variable);
|
|
2680
|
+
}
|
|
2681
|
+
} else {
|
|
2682
|
+
if (!organized.custom["uncategorized"]) {
|
|
2683
|
+
organized.custom["uncategorized"] = [];
|
|
2684
|
+
}
|
|
2685
|
+
organized.custom["uncategorized"].push(variable);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
for (const role of Object.keys(organized)) {
|
|
2690
|
+
if (Array.isArray(organized[role])) {
|
|
2691
|
+
organized[role].sort(
|
|
2692
|
+
(a, b) => a.tone - b.tone
|
|
2693
|
+
);
|
|
2694
|
+
} else if (role === "custom" && organized.custom) {
|
|
2695
|
+
for (const customRole of Object.keys(organized.custom)) {
|
|
2696
|
+
organized.custom[customRole].sort((a, b) => a.tone - b.tone);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return organized;
|
|
2701
|
+
}
|
|
2702
|
+
function parseColorToRgb2(color) {
|
|
2703
|
+
if (color.startsWith("#")) {
|
|
2704
|
+
return hexToRgb(color);
|
|
2705
|
+
}
|
|
2706
|
+
if (color.startsWith("rgb")) {
|
|
2707
|
+
const match = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
2708
|
+
if (match) {
|
|
2709
|
+
return {
|
|
2710
|
+
b: parseInt(match[3], 10) / 255,
|
|
2711
|
+
g: parseInt(match[2], 10) / 255,
|
|
2712
|
+
r: parseInt(match[1], 10) / 255
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
throw new Error(`Unable to parse color: ${color}`);
|
|
2717
|
+
}
|
|
2718
|
+
function parseHslString(hsl) {
|
|
2719
|
+
const match = hsl.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%/);
|
|
2720
|
+
if (match) {
|
|
2721
|
+
const h = parseInt(match[1], 10) / 360;
|
|
2722
|
+
const s = parseInt(match[2], 10) / 100;
|
|
2723
|
+
const l = parseInt(match[3], 10) / 100;
|
|
2724
|
+
const rgb = hslToRgb2(h, s, l);
|
|
2725
|
+
const r = Math.round(rgb.r * 255);
|
|
2726
|
+
const g = Math.round(rgb.g * 255);
|
|
2727
|
+
const b = Math.round(rgb.b * 255);
|
|
2728
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
2729
|
+
}
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
function parseRgbString(rgb) {
|
|
2733
|
+
const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
2734
|
+
if (match) {
|
|
2735
|
+
const r = parseInt(match[1], 10);
|
|
2736
|
+
const g = parseInt(match[2], 10);
|
|
2737
|
+
const b = parseInt(match[3], 10);
|
|
2738
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
2739
|
+
}
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// src/theme/refactor.ts
|
|
2744
|
+
var PROPERTY_CONTEXT_MAP = {
|
|
2745
|
+
background: "background",
|
|
2746
|
+
"background-color": "background",
|
|
2747
|
+
border: "border",
|
|
2748
|
+
"border-bottom-color": "border",
|
|
2749
|
+
"border-color": "border",
|
|
2750
|
+
"border-left-color": "border",
|
|
2751
|
+
"border-right-color": "border",
|
|
2752
|
+
"border-top-color": "border",
|
|
2753
|
+
"box-shadow": "shadow",
|
|
2754
|
+
color: "text",
|
|
2755
|
+
fill: "decorative",
|
|
2756
|
+
outline: "border",
|
|
2757
|
+
"outline-color": "border",
|
|
2758
|
+
"stop-color": "decorative",
|
|
2759
|
+
stroke: "border",
|
|
2760
|
+
"text-shadow": "shadow"
|
|
2761
|
+
};
|
|
2762
|
+
function generateRefactoringReport(result) {
|
|
2763
|
+
const lines = [
|
|
2764
|
+
"# CSS Refactoring Report",
|
|
2765
|
+
"",
|
|
2766
|
+
"## Summary",
|
|
2767
|
+
`- Total colors found: ${result.statistics.totalColors}`,
|
|
2768
|
+
`- Colors replaced: ${result.statistics.replacedColors}`,
|
|
2769
|
+
`- Replacement rate: ${Math.round(result.statistics.replacedColors / result.statistics.totalColors * 100)}%`,
|
|
2770
|
+
`- Average confidence: ${result.statistics.averageConfidence}%`,
|
|
2771
|
+
`- Accessibility issues: ${result.statistics.accessibilityIssues}`,
|
|
2772
|
+
"",
|
|
2773
|
+
"## Replacements"
|
|
2774
|
+
];
|
|
2775
|
+
const highConfidence = result.replacements.filter((r) => r.confidence >= 90);
|
|
2776
|
+
const mediumConfidence = result.replacements.filter(
|
|
2777
|
+
(r) => r.confidence >= 70 && r.confidence < 90
|
|
2778
|
+
);
|
|
2779
|
+
const lowConfidence = result.replacements.filter((r) => r.confidence < 70);
|
|
2780
|
+
if (highConfidence.length > 0) {
|
|
2781
|
+
lines.push("", "### High Confidence (\u226590%)");
|
|
2782
|
+
for (const replacement of highConfidence) {
|
|
2783
|
+
lines.push(
|
|
2784
|
+
`- Line ${replacement.line}: ${replacement.originalColor} \u2192 ${replacement.cssVariable} (${replacement.confidence}%)`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
if (mediumConfidence.length > 0) {
|
|
2789
|
+
lines.push("", "### Medium Confidence (70-89%)");
|
|
2790
|
+
for (const replacement of mediumConfidence) {
|
|
2791
|
+
lines.push(
|
|
2792
|
+
`- Line ${replacement.line}: ${replacement.originalColor} \u2192 ${replacement.cssVariable} (${replacement.confidence}%)`
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (lowConfidence.length > 0) {
|
|
2797
|
+
lines.push("", "### Low Confidence (<70%)");
|
|
2798
|
+
for (const replacement of lowConfidence) {
|
|
2799
|
+
lines.push(
|
|
2800
|
+
`- Line ${replacement.line}: ${replacement.originalColor} \u2192 ${replacement.cssVariable} (${replacement.confidence}%)`
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (result.warnings.length > 0) {
|
|
2805
|
+
lines.push("", "## Warnings");
|
|
2806
|
+
const warningsByType = result.warnings.reduce(
|
|
2807
|
+
(acc, warning) => {
|
|
2808
|
+
if (!acc[warning.type]) acc[warning.type] = [];
|
|
2809
|
+
acc[warning.type].push(warning);
|
|
2810
|
+
return acc;
|
|
2811
|
+
},
|
|
2812
|
+
{}
|
|
2813
|
+
);
|
|
2814
|
+
for (const [type, warnings] of Object.entries(warningsByType)) {
|
|
2815
|
+
lines.push("", `### ${formatWarningType(type)}`);
|
|
2816
|
+
for (const warning of warnings) {
|
|
2817
|
+
lines.push(`- ${warning.message}`);
|
|
2818
|
+
if (warning.suggestion) {
|
|
2819
|
+
lines.push(` Suggestion: ${warning.suggestion}`);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
return lines.join("\n");
|
|
2825
|
+
}
|
|
2826
|
+
function refactorCss(css, themeVariables, options = {}) {
|
|
2827
|
+
const {
|
|
2828
|
+
addComments = true,
|
|
2829
|
+
minConfidence = 70,
|
|
2830
|
+
preserveOriginal = true
|
|
2831
|
+
} = options;
|
|
2832
|
+
const replacements = [];
|
|
2833
|
+
const warnings = [];
|
|
2834
|
+
let refactoredCss = css;
|
|
2835
|
+
let totalColors = 0;
|
|
2836
|
+
let replacedColors = 0;
|
|
2837
|
+
let totalConfidence = 0;
|
|
2838
|
+
let accessibilityIssues = 0;
|
|
2839
|
+
const lines = css.split("\n");
|
|
2840
|
+
const refactoredLines = [];
|
|
2841
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
2842
|
+
const line = lines[lineIndex];
|
|
2843
|
+
let refactoredLine = line;
|
|
2844
|
+
const propertyMatch = line.match(/^\s*([a-z-]+):\s*/i);
|
|
2845
|
+
const property = propertyMatch ? propertyMatch[1].toLowerCase() : null;
|
|
2846
|
+
const context = property ? PROPERTY_CONTEXT_MAP[property] : void 0;
|
|
2847
|
+
const colorMatches = findColorsInLine(line);
|
|
2848
|
+
for (const colorMatch of colorMatches) {
|
|
2849
|
+
totalColors++;
|
|
2850
|
+
const { color, end, start } = colorMatch;
|
|
2851
|
+
const matchOptions = {
|
|
2852
|
+
contextType: context,
|
|
2853
|
+
weights: {
|
|
2854
|
+
accessibility: context === "text" || context === "background" ? 0.3 : 0.1,
|
|
2855
|
+
perceptual: 0.6,
|
|
2856
|
+
semantic: context ? 0.3 : 0.2
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
const match = findClosestThemeColor(color, themeVariables, matchOptions);
|
|
2860
|
+
if (match && match.confidence >= minConfidence) {
|
|
2861
|
+
replacedColors++;
|
|
2862
|
+
totalConfidence += match.confidence;
|
|
2863
|
+
const replacement = {
|
|
2864
|
+
column: start,
|
|
2865
|
+
confidence: match.confidence,
|
|
2866
|
+
context,
|
|
2867
|
+
cssVariable: match.variable,
|
|
2868
|
+
line: lineIndex + 1,
|
|
2869
|
+
originalColor: color,
|
|
2870
|
+
property: property || void 0
|
|
2871
|
+
};
|
|
2872
|
+
replacements.push(replacement);
|
|
2873
|
+
if (match.accessibilityInfo && !match.accessibilityInfo.meetsAA) {
|
|
2874
|
+
accessibilityIssues++;
|
|
2875
|
+
warnings.push({
|
|
2876
|
+
location: { column: start, line: lineIndex + 1 },
|
|
2877
|
+
message: `Color ${color} replaced with ${match.variable} may have accessibility issues`,
|
|
2878
|
+
type: "accessibility"
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
let replacementText = `var(${match.variable})`;
|
|
2882
|
+
if (preserveOriginal && addComments) {
|
|
2883
|
+
replacementText = `/* ${color} */ ${replacementText}`;
|
|
2884
|
+
}
|
|
2885
|
+
refactoredLine = refactoredLine.substring(0, start) + replacementText + refactoredLine.substring(end);
|
|
2886
|
+
} else if (match && match.confidence < minConfidence) {
|
|
2887
|
+
warnings.push({
|
|
2888
|
+
location: { column: start, line: lineIndex + 1 },
|
|
2889
|
+
message: `Low confidence match for ${color}: ${match.variable} (${match.confidence}%)`,
|
|
2890
|
+
suggestion: `Consider using ${match.variable} or defining a new theme variable`,
|
|
2891
|
+
type: "low-confidence"
|
|
2892
|
+
});
|
|
2893
|
+
} else {
|
|
2894
|
+
warnings.push({
|
|
2895
|
+
location: { column: start, line: lineIndex + 1 },
|
|
2896
|
+
message: `No suitable theme variable found for ${color}`,
|
|
2897
|
+
suggestion: "Consider adding this color to your theme",
|
|
2898
|
+
type: "no-match"
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
refactoredLines.push(refactoredLine);
|
|
2903
|
+
}
|
|
2904
|
+
if (addComments && replacedColors > 0) {
|
|
2905
|
+
const summaryComment = `/* CSS Refactored with Theme Variables
|
|
2906
|
+
* Total colors found: ${totalColors}
|
|
2907
|
+
* Colors replaced: ${replacedColors}
|
|
2908
|
+
* Average confidence: ${Math.round(totalConfidence / replacedColors)}%
|
|
2909
|
+
* Warnings: ${warnings.length}
|
|
2910
|
+
*/
|
|
2911
|
+
|
|
2912
|
+
`;
|
|
2913
|
+
refactoredCss = summaryComment + refactoredLines.join("\n");
|
|
2914
|
+
} else {
|
|
2915
|
+
refactoredCss = refactoredLines.join("\n");
|
|
2916
|
+
}
|
|
2917
|
+
return {
|
|
2918
|
+
original: css,
|
|
2919
|
+
refactored: refactoredCss,
|
|
2920
|
+
replacements,
|
|
2921
|
+
statistics: {
|
|
2922
|
+
accessibilityIssues,
|
|
2923
|
+
averageConfidence: replacedColors > 0 ? Math.round(totalConfidence / replacedColors) : 0,
|
|
2924
|
+
replacedColors,
|
|
2925
|
+
totalColors
|
|
2926
|
+
},
|
|
2927
|
+
warnings
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
function findColorsInLine(line) {
|
|
2931
|
+
const colors = [];
|
|
2932
|
+
let match;
|
|
2933
|
+
const hexRegex = /#[0-9a-fA-F]{3,8}/g;
|
|
2934
|
+
while ((match = hexRegex.exec(line)) !== null) {
|
|
2935
|
+
colors.push({
|
|
2936
|
+
color: match[0],
|
|
2937
|
+
end: match.index + match[0].length,
|
|
2938
|
+
start: match.index
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
const rgbRegex = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
|
|
2942
|
+
while ((match = rgbRegex.exec(line)) !== null) {
|
|
2943
|
+
colors.push({
|
|
2944
|
+
color: match[0],
|
|
2945
|
+
end: match.index + match[0].length,
|
|
2946
|
+
start: match.index
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
const hslRegex = /hsla?\([^)]+\)/g;
|
|
2950
|
+
while ((match = hslRegex.exec(line)) !== null) {
|
|
2951
|
+
colors.push({
|
|
2952
|
+
color: match[0],
|
|
2953
|
+
end: match.index + match[0].length,
|
|
2954
|
+
start: match.index
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
colors.sort((a, b) => a.start - b.start);
|
|
2958
|
+
const uniqueColors = [];
|
|
2959
|
+
let lastEnd = -1;
|
|
2960
|
+
for (const color of colors) {
|
|
2961
|
+
if (color.start >= lastEnd) {
|
|
2962
|
+
uniqueColors.push(color);
|
|
2963
|
+
lastEnd = color.end;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
return uniqueColors;
|
|
2967
|
+
}
|
|
2968
|
+
function formatWarningType(type) {
|
|
2969
|
+
const formats = {
|
|
2970
|
+
accessibility: "Accessibility Concerns",
|
|
2971
|
+
"low-confidence": "Low Confidence Matches",
|
|
2972
|
+
"no-match": "No Matches Found",
|
|
2973
|
+
"semantic-mismatch": "Semantic Mismatches"
|
|
2974
|
+
};
|
|
2975
|
+
return formats[type] || type;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// src/tools/theme-matching.tools.ts
|
|
2979
|
+
var matchThemeColorTool = {
|
|
2980
|
+
description: "Find the closest matching theme variable for a given color",
|
|
2981
|
+
execute: async (args) => {
|
|
2982
|
+
const themeVariables = parseThemeVariables(args.themeCSS);
|
|
2983
|
+
const match = findClosestThemeColor(args.color, themeVariables, {
|
|
2984
|
+
contextType: args.context
|
|
2985
|
+
});
|
|
2986
|
+
if (!match) {
|
|
2987
|
+
return "No matching theme variable found";
|
|
2988
|
+
}
|
|
2989
|
+
const minConfidence = args.minConfidence ?? 70;
|
|
2990
|
+
if (match.confidence < minConfidence) {
|
|
2991
|
+
return `Best match below confidence threshold:
|
|
2992
|
+
Variable: ${match.variable}
|
|
2993
|
+
Distance: ${match.distance.toFixed(2)}
|
|
2994
|
+
Confidence: ${match.confidence}%
|
|
2995
|
+
|
|
2996
|
+
Consider adding a new theme variable for this color.`;
|
|
2997
|
+
}
|
|
2998
|
+
let result = `Best Match:
|
|
2999
|
+
Variable: ${match.variable}
|
|
3000
|
+
Value: ${match.value}
|
|
3001
|
+
Distance: ${match.distance.toFixed(2)}
|
|
3002
|
+
Confidence: ${match.confidence}%`;
|
|
3003
|
+
if (match.semanticRole) {
|
|
3004
|
+
result += `
|
|
3005
|
+
Semantic Role: ${match.semanticRole}`;
|
|
3006
|
+
}
|
|
3007
|
+
if (match.accessibilityInfo) {
|
|
3008
|
+
result += `
|
|
3009
|
+
|
|
3010
|
+
Accessibility:
|
|
3011
|
+
Contrast with background: ${match.accessibilityInfo.contrastWithBackground.toFixed(2)}
|
|
3012
|
+
Contrast with foreground: ${match.accessibilityInfo.contrastWithForeground.toFixed(2)}
|
|
3013
|
+
Meets AA: ${match.accessibilityInfo.meetsAA ? "Yes" : "No"}
|
|
3014
|
+
Meets AAA: ${match.accessibilityInfo.meetsAAA ? "Yes" : "No"}`;
|
|
3015
|
+
}
|
|
3016
|
+
if (match.alternatives.length > 0) {
|
|
3017
|
+
result += `
|
|
3018
|
+
|
|
3019
|
+
Alternatives:`;
|
|
3020
|
+
for (const alt of match.alternatives) {
|
|
3021
|
+
result += `
|
|
3022
|
+
- ${alt.variable} (confidence: ${alt.confidence}%)`;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return result;
|
|
3026
|
+
},
|
|
3027
|
+
name: "match_theme_color",
|
|
3028
|
+
parameters: z9.object({
|
|
3029
|
+
color: z9.string().describe("Color to match (hex, rgb, hsl)"),
|
|
3030
|
+
context: z9.enum(["text", "background", "border", "shadow", "accent", "decorative"]).optional().describe("Usage context for better matching"),
|
|
3031
|
+
minConfidence: z9.number().min(0).max(100).optional().default(70).describe("Minimum confidence threshold (0-100)"),
|
|
3032
|
+
themeCSS: z9.string().describe("CSS containing theme variables")
|
|
3033
|
+
})
|
|
3034
|
+
};
|
|
3035
|
+
var refactorCssWithThemeTool = {
|
|
3036
|
+
description: "Refactor CSS to use theme variables instead of hardcoded colors",
|
|
3037
|
+
execute: async (args) => {
|
|
3038
|
+
const themeVariables = parseThemeVariables(args.themeCSS);
|
|
3039
|
+
const result = refactorCss(args.css, themeVariables, {
|
|
3040
|
+
addComments: args.preserveOriginal ?? true,
|
|
3041
|
+
minConfidence: args.minConfidence ?? 70,
|
|
3042
|
+
preserveOriginal: args.preserveOriginal ?? true
|
|
3043
|
+
});
|
|
3044
|
+
let output = `Refactoring Complete!
|
|
3045
|
+
|
|
3046
|
+
Statistics:
|
|
3047
|
+
- Total colors found: ${result.statistics.totalColors}
|
|
3048
|
+
- Colors replaced: ${result.statistics.replacedColors}
|
|
3049
|
+
- Average confidence: ${result.statistics.averageConfidence}%
|
|
3050
|
+
- Accessibility issues: ${result.statistics.accessibilityIssues}
|
|
3051
|
+
|
|
3052
|
+
Refactored CSS:
|
|
3053
|
+
----------------------------------------
|
|
3054
|
+
${result.refactored}
|
|
3055
|
+
----------------------------------------`;
|
|
3056
|
+
if (result.warnings.length > 0) {
|
|
3057
|
+
output += `
|
|
3058
|
+
|
|
3059
|
+
Warnings (${result.warnings.length}):`;
|
|
3060
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
3061
|
+
output += `
|
|
3062
|
+
- ${warning.message}`;
|
|
3063
|
+
if (warning.suggestion) {
|
|
3064
|
+
output += `
|
|
3065
|
+
Suggestion: ${warning.suggestion}`;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
if (result.warnings.length > 5) {
|
|
3069
|
+
output += `
|
|
3070
|
+
... and ${result.warnings.length - 5} more warnings`;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
if (args.generateReport) {
|
|
3074
|
+
output += `
|
|
3075
|
+
|
|
3076
|
+
${generateRefactoringReport(result)}`;
|
|
3077
|
+
}
|
|
3078
|
+
return output;
|
|
3079
|
+
},
|
|
3080
|
+
name: "refactor_css_with_theme",
|
|
3081
|
+
parameters: z9.object({
|
|
3082
|
+
css: z9.string().describe("CSS content to refactor"),
|
|
3083
|
+
generateReport: z9.boolean().optional().default(false).describe("Generate detailed refactoring report"),
|
|
3084
|
+
minConfidence: z9.number().min(0).max(100).optional().default(70).describe("Minimum confidence for replacements"),
|
|
3085
|
+
preserveOriginal: z9.boolean().optional().default(true).describe("Keep original values as comments"),
|
|
3086
|
+
themeCSS: z9.string().describe("CSS containing theme variables")
|
|
3087
|
+
})
|
|
3088
|
+
};
|
|
3089
|
+
var matchThemeColorsBatchTool = {
|
|
3090
|
+
description: "Find theme matches for multiple colors at once",
|
|
3091
|
+
execute: async (args) => {
|
|
3092
|
+
const themeVariables = parseThemeVariables(args.themeCSS);
|
|
3093
|
+
const matches = findBatchMatches(args.colors, themeVariables, {
|
|
3094
|
+
contextType: args.context
|
|
3095
|
+
});
|
|
3096
|
+
let result = `Theme Color Matches:
|
|
3097
|
+
`;
|
|
3098
|
+
let matchCount = 0;
|
|
3099
|
+
let totalConfidence = 0;
|
|
3100
|
+
for (const [color, match] of matches.entries()) {
|
|
3101
|
+
if (match) {
|
|
3102
|
+
matchCount++;
|
|
3103
|
+
totalConfidence += match.confidence;
|
|
3104
|
+
result += `
|
|
3105
|
+
${color} \u2192 ${match.variable} (${match.confidence}%)`;
|
|
3106
|
+
} else {
|
|
3107
|
+
result += `
|
|
3108
|
+
${color} \u2192 No match found`;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
result += `
|
|
3112
|
+
|
|
3113
|
+
Summary:
|
|
3114
|
+
- Matched: ${matchCount}/${args.colors.length}
|
|
3115
|
+
- Average confidence: ${matchCount > 0 ? Math.round(totalConfidence / matchCount) : 0}%`;
|
|
3116
|
+
return result;
|
|
3117
|
+
},
|
|
3118
|
+
name: "match_theme_colors_batch",
|
|
3119
|
+
parameters: z9.object({
|
|
3120
|
+
colors: z9.array(z9.string()).min(1).max(50).describe("Array of colors to match"),
|
|
3121
|
+
context: z9.enum(["text", "background", "border", "shadow", "accent", "decorative"]).optional().describe("Usage context for all colors"),
|
|
3122
|
+
themeCSS: z9.string().describe("CSS containing theme variables")
|
|
3123
|
+
})
|
|
3124
|
+
};
|
|
3125
|
+
var generateThemeCssTool = {
|
|
3126
|
+
description: "Generate CSS custom properties for a complete theme from a source color",
|
|
3127
|
+
execute: async (args) => {
|
|
3128
|
+
const source = parseColor(args.sourceColor);
|
|
3129
|
+
if (!source) {
|
|
3130
|
+
return `Invalid color format: ${args.sourceColor}`;
|
|
3131
|
+
}
|
|
3132
|
+
const corePalette = corePaletteFromRgb(source);
|
|
3133
|
+
const tones = args.includeTones || [
|
|
3134
|
+
0,
|
|
3135
|
+
10,
|
|
3136
|
+
20,
|
|
3137
|
+
30,
|
|
3138
|
+
40,
|
|
3139
|
+
50,
|
|
3140
|
+
60,
|
|
3141
|
+
70,
|
|
3142
|
+
80,
|
|
3143
|
+
90,
|
|
3144
|
+
95,
|
|
3145
|
+
99,
|
|
3146
|
+
100
|
|
3147
|
+
];
|
|
3148
|
+
const prefix = args.prefix || "color";
|
|
3149
|
+
let css = `:root {
|
|
3150
|
+
`;
|
|
3151
|
+
css += ` /* Primary Colors */
|
|
3152
|
+
`;
|
|
3153
|
+
for (const tone of tones) {
|
|
3154
|
+
const color = rgbToHex(corePalette.primary.tone(tone));
|
|
3155
|
+
css += ` --${prefix}-primary-${tone}: ${color};
|
|
3156
|
+
`;
|
|
3157
|
+
}
|
|
3158
|
+
css += `
|
|
3159
|
+
/* Secondary Colors */
|
|
3160
|
+
`;
|
|
3161
|
+
for (const tone of tones) {
|
|
3162
|
+
const color = rgbToHex(corePalette.secondary.tone(tone));
|
|
3163
|
+
css += ` --${prefix}-secondary-${tone}: ${color};
|
|
3164
|
+
`;
|
|
3165
|
+
}
|
|
3166
|
+
css += `
|
|
3167
|
+
/* Tertiary Colors */
|
|
3168
|
+
`;
|
|
3169
|
+
for (const tone of tones) {
|
|
3170
|
+
const color = rgbToHex(corePalette.tertiary.tone(tone));
|
|
3171
|
+
css += ` --${prefix}-tertiary-${tone}: ${color};
|
|
3172
|
+
`;
|
|
3173
|
+
}
|
|
3174
|
+
css += `
|
|
3175
|
+
/* Error Colors */
|
|
3176
|
+
`;
|
|
3177
|
+
for (const tone of tones) {
|
|
3178
|
+
const color = rgbToHex(corePalette.error.tone(tone));
|
|
3179
|
+
css += ` --${prefix}-error-${tone}: ${color};
|
|
3180
|
+
`;
|
|
3181
|
+
}
|
|
3182
|
+
css += `
|
|
3183
|
+
/* Neutral Colors */
|
|
3184
|
+
`;
|
|
3185
|
+
for (const tone of tones) {
|
|
3186
|
+
const color = rgbToHex(corePalette.neutral.tone(tone));
|
|
3187
|
+
css += ` --${prefix}-neutral-${tone}: ${color};
|
|
3188
|
+
`;
|
|
3189
|
+
}
|
|
3190
|
+
css += `
|
|
3191
|
+
/* Neutral Variant Colors */
|
|
3192
|
+
`;
|
|
3193
|
+
for (const tone of tones) {
|
|
3194
|
+
const color = rgbToHex(corePalette.neutralVariant.tone(tone));
|
|
3195
|
+
css += ` --${prefix}-neutral-variant-${tone}: ${color};
|
|
3196
|
+
`;
|
|
3197
|
+
}
|
|
3198
|
+
css += `}
|
|
3199
|
+
|
|
3200
|
+
`;
|
|
3201
|
+
css += `/* Semantic Color Mappings (Light Theme) */
|
|
3202
|
+
`;
|
|
3203
|
+
css += `:root {
|
|
3204
|
+
`;
|
|
3205
|
+
css += ` --${prefix}-primary: var(--${prefix}-primary-40);
|
|
3206
|
+
`;
|
|
3207
|
+
css += ` --${prefix}-on-primary: var(--${prefix}-primary-100);
|
|
3208
|
+
`;
|
|
3209
|
+
css += ` --${prefix}-primary-container: var(--${prefix}-primary-90);
|
|
3210
|
+
`;
|
|
3211
|
+
css += ` --${prefix}-on-primary-container: var(--${prefix}-primary-10);
|
|
3212
|
+
`;
|
|
3213
|
+
css += `
|
|
3214
|
+
`;
|
|
3215
|
+
css += ` --${prefix}-secondary: var(--${prefix}-secondary-40);
|
|
3216
|
+
`;
|
|
3217
|
+
css += ` --${prefix}-on-secondary: var(--${prefix}-secondary-100);
|
|
3218
|
+
`;
|
|
3219
|
+
css += ` --${prefix}-secondary-container: var(--${prefix}-secondary-90);
|
|
3220
|
+
`;
|
|
3221
|
+
css += ` --${prefix}-on-secondary-container: var(--${prefix}-secondary-10);
|
|
3222
|
+
`;
|
|
3223
|
+
css += `
|
|
3224
|
+
`;
|
|
3225
|
+
css += ` --${prefix}-background: var(--${prefix}-neutral-99);
|
|
3226
|
+
`;
|
|
3227
|
+
css += ` --${prefix}-on-background: var(--${prefix}-neutral-10);
|
|
3228
|
+
`;
|
|
3229
|
+
css += ` --${prefix}-surface: var(--${prefix}-neutral-99);
|
|
3230
|
+
`;
|
|
3231
|
+
css += ` --${prefix}-on-surface: var(--${prefix}-neutral-10);
|
|
3232
|
+
`;
|
|
3233
|
+
css += `}
|
|
3234
|
+
`;
|
|
3235
|
+
return css;
|
|
3236
|
+
},
|
|
3237
|
+
name: "generate_theme_css",
|
|
3238
|
+
parameters: z9.object({
|
|
3239
|
+
includeTones: z9.array(z9.number()).optional().describe(
|
|
3240
|
+
"Tone values to include (default: 0,10,20,30,40,50,60,70,80,90,95,99,100)"
|
|
3241
|
+
),
|
|
3242
|
+
prefix: z9.string().optional().default("color").describe(
|
|
3243
|
+
"Prefix for CSS variables (e.g., 'color' \u2192 --color-primary-50)"
|
|
3244
|
+
),
|
|
3245
|
+
sourceColor: z9.string().describe("Source color for theme generation")
|
|
3246
|
+
})
|
|
3247
|
+
};
|
|
3248
|
+
|
|
3249
|
+
// src/bin/server.ts
|
|
3250
|
+
var server = new CoolorsMcp({
|
|
3251
|
+
instructions: "Advanced color operations server with Material Design 3 support, CSS theme matching, image extraction, and accessibility compliance. Uses HCT color space for perceptually accurate operations.",
|
|
3252
|
+
name: "coolors-mcp",
|
|
3253
|
+
version: "1.0.0"
|
|
3254
|
+
});
|
|
3255
|
+
server.addTool(colorConversionTool);
|
|
3256
|
+
server.addTool(colorDistanceTool);
|
|
3257
|
+
server.addTool(contrastCheckerTool);
|
|
3258
|
+
server.addTool(paletteGeneratorTool);
|
|
3259
|
+
server.addTool(paletteWithLocksTool);
|
|
3260
|
+
server.addTool(gradientGeneratorTool);
|
|
3261
|
+
server.addTool(generateMaterialThemeTool);
|
|
3262
|
+
server.addTool(harmonizeColorsTool);
|
|
3263
|
+
server.addTool(generateTonalPaletteTool);
|
|
3264
|
+
server.addTool(matchThemeColorTool);
|
|
3265
|
+
server.addTool(refactorCssWithThemeTool);
|
|
3266
|
+
server.addTool(matchThemeColorsBatchTool);
|
|
3267
|
+
server.addTool(generateThemeCssTool);
|
|
3268
|
+
server.addTool(extractImageColorsTool);
|
|
3269
|
+
server.addTool(generateThemeFromImageTool);
|
|
3270
|
+
server.addTool(analyzeColorLikabilityTool);
|
|
3271
|
+
server.addTool(fixDislikedColorsBatchTool);
|
|
3272
|
+
server.start().catch((error) => {
|
|
3273
|
+
console.error("Failed to start server:", error);
|
|
3274
|
+
process.exit(1);
|
|
3275
|
+
});
|
|
3276
|
+
/**
|
|
3277
|
+
* @license
|
|
3278
|
+
* Copyright 2021 Google LLC
|
|
3279
|
+
*
|
|
3280
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3281
|
+
* you may not use this file except in compliance with the License.
|
|
3282
|
+
* You may obtain a copy of the License at
|
|
3283
|
+
*
|
|
3284
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
3285
|
+
*
|
|
3286
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
3287
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
3288
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
3289
|
+
* See the License for the specific language governing permissions and
|
|
3290
|
+
* limitations under the License.
|
|
3291
|
+
*/
|
|
3292
|
+
//# sourceMappingURL=server.js.map
|