@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,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Color Matcher
|
|
3
|
+
* Finds closest theme colors using HCT perceptual distance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HCT } from "../color/hct/types.js";
|
|
7
|
+
import type {
|
|
8
|
+
ColorContext,
|
|
9
|
+
MatchingOptions,
|
|
10
|
+
SemanticRole,
|
|
11
|
+
ThemeMatch,
|
|
12
|
+
ThemeVariable,
|
|
13
|
+
ThemeVariables,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
import { hexToRgb } from "../color/conversions.js";
|
|
17
|
+
import { rgbToHct } from "../color/hct/hct-solver.js";
|
|
18
|
+
import { getContrastRatio } from "../color/utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default matching weights
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_WEIGHTS = {
|
|
24
|
+
accessibility: 0.2,
|
|
25
|
+
perceptual: 0.6,
|
|
26
|
+
semantic: 0.2,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maximum acceptable distances for different contexts
|
|
31
|
+
*/
|
|
32
|
+
const CONTEXT_MAX_DISTANCES: Record<ColorContext, number> = {
|
|
33
|
+
accent: 15, // Moderate for brand consistency
|
|
34
|
+
background: 15, // Moderate flexibility
|
|
35
|
+
border: 20, // More flexibility
|
|
36
|
+
decorative: 30, // Most flexible
|
|
37
|
+
shadow: 25, // Even more flexibility
|
|
38
|
+
text: 10, // Strict for readability
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find matches for multiple colors
|
|
43
|
+
*/
|
|
44
|
+
export function findBatchMatches(
|
|
45
|
+
colors: string[],
|
|
46
|
+
themeVariables: ThemeVariables,
|
|
47
|
+
options: MatchingOptions = {},
|
|
48
|
+
): Map<string, null | ThemeMatch> {
|
|
49
|
+
const results = new Map<string, null | ThemeMatch>();
|
|
50
|
+
|
|
51
|
+
for (const color of colors) {
|
|
52
|
+
results.set(color, findClosestThemeColor(color, themeVariables, options));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find the closest theme color for a given input color
|
|
60
|
+
*/
|
|
61
|
+
export function findClosestThemeColor(
|
|
62
|
+
inputColor: string,
|
|
63
|
+
themeVariables: ThemeVariables,
|
|
64
|
+
options: MatchingOptions = {},
|
|
65
|
+
): null | ThemeMatch {
|
|
66
|
+
// Parse input color to HCT
|
|
67
|
+
let inputHct: HCT;
|
|
68
|
+
try {
|
|
69
|
+
const rgb = parseColorToRgb(inputColor);
|
|
70
|
+
inputHct = rgbToHct(rgb);
|
|
71
|
+
} catch {
|
|
72
|
+
console.error(`Failed to parse input color: ${inputColor}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Flatten theme variables
|
|
77
|
+
const allVariables = flattenThemeVariables(themeVariables);
|
|
78
|
+
if (allVariables.length === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Calculate distances and scores
|
|
83
|
+
const candidates = allVariables.map((variable) => ({
|
|
84
|
+
distance: calculateHctDistance(inputHct, variable.hct),
|
|
85
|
+
score: 0, // Will be calculated next
|
|
86
|
+
variable,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// Apply context-based filtering
|
|
90
|
+
const maxDistance =
|
|
91
|
+
options.maxDistance ??
|
|
92
|
+
(options.contextType ? CONTEXT_MAX_DISTANCES[options.contextType] : 30);
|
|
93
|
+
|
|
94
|
+
const validCandidates = candidates.filter((c) => c.distance <= maxDistance);
|
|
95
|
+
if (validCandidates.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate multi-factor scores
|
|
100
|
+
const weights = options.weights ?? DEFAULT_WEIGHTS;
|
|
101
|
+
for (const candidate of validCandidates) {
|
|
102
|
+
candidate.score = calculateMatchScore(
|
|
103
|
+
inputHct,
|
|
104
|
+
candidate.variable,
|
|
105
|
+
candidate.distance,
|
|
106
|
+
weights,
|
|
107
|
+
options,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Sort by score (higher is better)
|
|
112
|
+
validCandidates.sort((a, b) => b.score - a.score);
|
|
113
|
+
|
|
114
|
+
// Get the best match and alternatives
|
|
115
|
+
const best = validCandidates[0];
|
|
116
|
+
const alternatives = validCandidates.slice(1, 4).map((c) => ({
|
|
117
|
+
alternatives: [],
|
|
118
|
+
confidence: calculateConfidence(c.distance, c.score),
|
|
119
|
+
distance: c.distance,
|
|
120
|
+
semanticRole: c.variable.role,
|
|
121
|
+
value: c.variable.value,
|
|
122
|
+
variable: c.variable.name,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
// Calculate accessibility info if needed
|
|
126
|
+
const accessibilityInfo =
|
|
127
|
+
options.contextType === "text" || options.contextType === "background"
|
|
128
|
+
? calculateAccessibilityInfo(best.variable.value, themeVariables)
|
|
129
|
+
: undefined;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
accessibilityInfo,
|
|
133
|
+
alternatives,
|
|
134
|
+
confidence: calculateConfidence(best.distance, best.score),
|
|
135
|
+
distance: best.distance,
|
|
136
|
+
semanticRole: best.variable.role,
|
|
137
|
+
value: best.variable.value,
|
|
138
|
+
variable: best.variable.name,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculate accessibility information
|
|
144
|
+
*/
|
|
145
|
+
function calculateAccessibilityInfo(
|
|
146
|
+
color: string,
|
|
147
|
+
themeVariables: ThemeVariables,
|
|
148
|
+
): ThemeMatch["accessibilityInfo"] {
|
|
149
|
+
const rgb = parseColorToRgb(color);
|
|
150
|
+
|
|
151
|
+
// Find typical background and foreground colors
|
|
152
|
+
const backgrounds = findTypicalBackgrounds(themeVariables);
|
|
153
|
+
const foregrounds = findTypicalForegrounds(themeVariables);
|
|
154
|
+
|
|
155
|
+
let maxContrastBg = 0;
|
|
156
|
+
let maxContrastFg = 0;
|
|
157
|
+
|
|
158
|
+
for (const bg of backgrounds) {
|
|
159
|
+
const bgRgb = parseColorToRgb(bg.value);
|
|
160
|
+
const contrast = getContrastRatio(rgb, bgRgb);
|
|
161
|
+
maxContrastBg = Math.max(maxContrastBg, contrast);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const fg of foregrounds) {
|
|
165
|
+
const fgRgb = parseColorToRgb(fg.value);
|
|
166
|
+
const contrast = getContrastRatio(rgb, fgRgb);
|
|
167
|
+
maxContrastFg = Math.max(maxContrastFg, contrast);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
contrastWithBackground: maxContrastBg,
|
|
172
|
+
contrastWithForeground: maxContrastFg,
|
|
173
|
+
meetsAA: maxContrastBg >= 4.5 || maxContrastFg >= 4.5,
|
|
174
|
+
meetsAAA: maxContrastBg >= 7.0 || maxContrastFg >= 7.0,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Calculate confidence score (0-100)
|
|
180
|
+
*/
|
|
181
|
+
function calculateConfidence(distance: number, score: number): number {
|
|
182
|
+
// For very small distances (exact or near-exact matches), return high confidence
|
|
183
|
+
if (distance < 0.5) return 100;
|
|
184
|
+
if (distance < 1) return 98;
|
|
185
|
+
if (distance < 2) return 95;
|
|
186
|
+
|
|
187
|
+
// Combine distance and score for confidence
|
|
188
|
+
const distanceConfidence = Math.max(0, 100 - distance * 2);
|
|
189
|
+
const scoreConfidence = score * 100;
|
|
190
|
+
return Math.round((distanceConfidence + scoreConfidence) / 2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Calculate semantic score based on context
|
|
195
|
+
*/
|
|
196
|
+
function calculateContextSemanticScore(
|
|
197
|
+
context: ColorContext,
|
|
198
|
+
role: SemanticRole | undefined,
|
|
199
|
+
): number {
|
|
200
|
+
if (!role) return 0.5;
|
|
201
|
+
|
|
202
|
+
switch (context) {
|
|
203
|
+
case "accent":
|
|
204
|
+
return role === "primary" || role === "secondary" ? 1.0 : 0.5;
|
|
205
|
+
case "background":
|
|
206
|
+
return role === "surface" || role === "background" ? 1.0 : 0.3;
|
|
207
|
+
case "border":
|
|
208
|
+
// Strongly prefer outline role for border context
|
|
209
|
+
return role === "outline" ? 1.0 : role === "neutral" ? 0.4 : 0.2;
|
|
210
|
+
case "decorative":
|
|
211
|
+
return 0.7; // Any role is acceptable
|
|
212
|
+
case "shadow":
|
|
213
|
+
return role === "shadow" ? 1.0 : 0.3;
|
|
214
|
+
case "text":
|
|
215
|
+
return role === "neutral" || role === "surface" ? 0.8 : 0.5;
|
|
216
|
+
default:
|
|
217
|
+
return 0.5;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Calculate HCT perceptual distance
|
|
223
|
+
*/
|
|
224
|
+
function calculateHctDistance(color1: HCT, color2: HCT): number {
|
|
225
|
+
// Weight factors for HCT components
|
|
226
|
+
const hueWeight = 1.0;
|
|
227
|
+
const chromaWeight = 1.0;
|
|
228
|
+
const toneWeight = 2.0; // Tone is most important for perception
|
|
229
|
+
|
|
230
|
+
// Calculate hue difference (circular)
|
|
231
|
+
let hueDiff = Math.abs(color1.h - color2.h);
|
|
232
|
+
if (hueDiff > 180) {
|
|
233
|
+
hueDiff = 360 - hueDiff;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Normalize hue difference (0-1)
|
|
237
|
+
hueDiff = hueDiff / 180;
|
|
238
|
+
|
|
239
|
+
// Calculate chroma difference (normalize by max chroma ~120)
|
|
240
|
+
const chromaDiff = Math.abs(color1.c - color2.c) / 120;
|
|
241
|
+
|
|
242
|
+
// Calculate tone difference (0-100)
|
|
243
|
+
const toneDiff = Math.abs(color1.t - color2.t) / 100;
|
|
244
|
+
|
|
245
|
+
// Weighted Euclidean distance
|
|
246
|
+
return (
|
|
247
|
+
Math.sqrt(
|
|
248
|
+
Math.pow(hueDiff * hueWeight, 2) +
|
|
249
|
+
Math.pow(chromaDiff * chromaWeight, 2) +
|
|
250
|
+
Math.pow(toneDiff * toneWeight, 2),
|
|
251
|
+
) * 100
|
|
252
|
+
); // Scale to 0-100 range
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Calculate multi-factor match score
|
|
257
|
+
*/
|
|
258
|
+
function calculateMatchScore(
|
|
259
|
+
inputHct: HCT,
|
|
260
|
+
candidate: ThemeVariable,
|
|
261
|
+
distance: number,
|
|
262
|
+
weights: { accessibility: number; perceptual: number; semantic: number },
|
|
263
|
+
options: MatchingOptions,
|
|
264
|
+
): number {
|
|
265
|
+
// Perceptual score (inverse of distance)
|
|
266
|
+
const perceptualScore = Math.max(0, 100 - distance) / 100;
|
|
267
|
+
|
|
268
|
+
// Semantic score
|
|
269
|
+
let semanticScore = 0.5; // Neutral if no preference
|
|
270
|
+
if (options.preferredRole && candidate.role) {
|
|
271
|
+
semanticScore = candidate.role === options.preferredRole ? 1.0 : 0.3;
|
|
272
|
+
} else if (options.contextType) {
|
|
273
|
+
semanticScore = calculateContextSemanticScore(
|
|
274
|
+
options.contextType,
|
|
275
|
+
candidate.role,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Accessibility score (based on tone appropriateness)
|
|
280
|
+
let accessibilityScore = 0.5; // Neutral default
|
|
281
|
+
if (options.contextType === "text") {
|
|
282
|
+
// Prefer high contrast (very light or very dark)
|
|
283
|
+
accessibilityScore = candidate.tone < 30 || candidate.tone > 70 ? 1.0 : 0.3;
|
|
284
|
+
} else if (options.contextType === "background") {
|
|
285
|
+
// Prefer mid-tones for backgrounds
|
|
286
|
+
accessibilityScore =
|
|
287
|
+
candidate.tone >= 90 || candidate.tone <= 10 ? 1.0 : 0.5;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Calculate weighted score
|
|
291
|
+
return (
|
|
292
|
+
perceptualScore * weights.perceptual +
|
|
293
|
+
semanticScore * weights.semantic +
|
|
294
|
+
accessibilityScore * weights.accessibility
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Find typical background colors from theme
|
|
300
|
+
*/
|
|
301
|
+
function findTypicalBackgrounds(theme: ThemeVariables): ThemeVariable[] {
|
|
302
|
+
const backgrounds: ThemeVariable[] = [];
|
|
303
|
+
|
|
304
|
+
if (theme.surface) {
|
|
305
|
+
backgrounds.push(
|
|
306
|
+
...theme.surface.filter((v) => v.tone >= 90 || v.tone <= 10),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (theme.background) {
|
|
310
|
+
backgrounds.push(...theme.background);
|
|
311
|
+
}
|
|
312
|
+
if (theme.neutral) {
|
|
313
|
+
backgrounds.push(
|
|
314
|
+
...theme.neutral.filter((v) => v.tone >= 95 || v.tone <= 5),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return backgrounds;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Find typical foreground colors from theme
|
|
323
|
+
*/
|
|
324
|
+
function findTypicalForegrounds(theme: ThemeVariables): ThemeVariable[] {
|
|
325
|
+
const foregrounds: ThemeVariable[] = [];
|
|
326
|
+
|
|
327
|
+
if (theme.neutral) {
|
|
328
|
+
foregrounds.push(
|
|
329
|
+
...theme.neutral.filter(
|
|
330
|
+
(v) => (v.tone >= 20 && v.tone <= 40) || (v.tone >= 60 && v.tone <= 80),
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (theme.surface) {
|
|
335
|
+
foregrounds.push(
|
|
336
|
+
...theme.surface.filter((v) => v.tone >= 10 && v.tone <= 30),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return foregrounds;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Flatten theme variables into a single array
|
|
345
|
+
*/
|
|
346
|
+
function flattenThemeVariables(theme: ThemeVariables): ThemeVariable[] {
|
|
347
|
+
const variables: ThemeVariable[] = [];
|
|
348
|
+
|
|
349
|
+
// Add standard roles
|
|
350
|
+
if (theme.primary) variables.push(...theme.primary);
|
|
351
|
+
if (theme.secondary) variables.push(...theme.secondary);
|
|
352
|
+
if (theme.tertiary) variables.push(...theme.tertiary);
|
|
353
|
+
if (theme.error) variables.push(...theme.error);
|
|
354
|
+
if (theme.neutral) variables.push(...theme.neutral);
|
|
355
|
+
if (theme.surface) variables.push(...theme.surface);
|
|
356
|
+
if (theme.background) variables.push(...theme.background);
|
|
357
|
+
if (theme.outline) variables.push(...theme.outline);
|
|
358
|
+
if (theme.shadow) variables.push(...theme.shadow);
|
|
359
|
+
|
|
360
|
+
// Add custom roles
|
|
361
|
+
if (theme.custom) {
|
|
362
|
+
for (const customVars of Object.values(theme.custom)) {
|
|
363
|
+
variables.push(...customVars);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return variables;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Parse color string to RGB
|
|
372
|
+
*/
|
|
373
|
+
function parseColorToRgb(color: string): { b: number; g: number; r: number } {
|
|
374
|
+
// Remove whitespace
|
|
375
|
+
color = color.trim();
|
|
376
|
+
|
|
377
|
+
// Hex color
|
|
378
|
+
if (color.startsWith("#")) {
|
|
379
|
+
return hexToRgb(color);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// RGB/RGBA
|
|
383
|
+
if (color.startsWith("rgb")) {
|
|
384
|
+
const match = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
385
|
+
if (match) {
|
|
386
|
+
return {
|
|
387
|
+
b: parseInt(match[3], 10) / 255,
|
|
388
|
+
g: parseInt(match[2], 10) / 255,
|
|
389
|
+
r: parseInt(match[1], 10) / 255,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
throw new Error(`Unable to parse color: ${color}`);
|
|
395
|
+
}
|