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