@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,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference color values from official sources for testing
|
|
3
|
+
* These values are from:
|
|
4
|
+
* - Material Design color system
|
|
5
|
+
* - CSS named colors (W3C standard)
|
|
6
|
+
* - WCAG contrast ratio examples
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { hexToRgb, rgbToHex, rgbToHsl } from "../conversions";
|
|
12
|
+
import { rgbToHct } from "../hct";
|
|
13
|
+
import { Hct } from "../hct/hct-class";
|
|
14
|
+
import * as utils from "../utils/color_utils";
|
|
15
|
+
|
|
16
|
+
// Helper function to calculate WCAG contrast ratio
|
|
17
|
+
function calculateContrast(
|
|
18
|
+
rgb1: { b: number; g: number; r: number },
|
|
19
|
+
rgb2: { b: number; g: number; r: number },
|
|
20
|
+
): number {
|
|
21
|
+
// Calculate relative luminance
|
|
22
|
+
function getLuminance(rgb: { b: number; g: number; r: number }): number {
|
|
23
|
+
const { b, g, r } = rgb;
|
|
24
|
+
const sRGB = [r, g, b].map((val) => {
|
|
25
|
+
if (val <= 0.03928) {
|
|
26
|
+
return val / 12.92;
|
|
27
|
+
}
|
|
28
|
+
return Math.pow((val + 0.055) / 1.055, 2.4);
|
|
29
|
+
});
|
|
30
|
+
return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const l1 = getLuminance(rgb1);
|
|
34
|
+
const l2 = getLuminance(rgb2);
|
|
35
|
+
const lighter = Math.max(l1, l2);
|
|
36
|
+
const darker = Math.min(l1, l2);
|
|
37
|
+
|
|
38
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("Reference Color Values", () => {
|
|
42
|
+
describe("Material Design Colors", () => {
|
|
43
|
+
it("should correctly convert Material Design Blue 500", () => {
|
|
44
|
+
const blue500 = "#2196F3";
|
|
45
|
+
const rgb = hexToRgb(blue500);
|
|
46
|
+
|
|
47
|
+
// Official RGB values from Material Design
|
|
48
|
+
expect(rgb.r).toBe(33);
|
|
49
|
+
expect(rgb.g).toBe(150);
|
|
50
|
+
expect(rgb.b).toBe(243);
|
|
51
|
+
|
|
52
|
+
// Convert to HCT
|
|
53
|
+
const hct = rgbToHct(rgb);
|
|
54
|
+
expect(hct.h).toBeCloseTo(272, -1); // Blue hue in HCT (different from HSL)
|
|
55
|
+
expect(hct.c).toBeGreaterThan(50); // High chroma for vibrant blue
|
|
56
|
+
expect(hct.t).toBeCloseTo(58, -1); // Medium tone
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle Material Design color tones correctly", () => {
|
|
60
|
+
// Material Design 3 standard tones
|
|
61
|
+
const standardTones = [
|
|
62
|
+
0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Create a tonal palette from a primary color
|
|
66
|
+
const primaryHue = 260; // Purple
|
|
67
|
+
const primaryChroma = 48;
|
|
68
|
+
|
|
69
|
+
for (const tone of standardTones) {
|
|
70
|
+
const hct = Hct.from(primaryHue, primaryChroma, tone);
|
|
71
|
+
// Allow some variation in extreme tones due to color space limitations
|
|
72
|
+
if (tone === 0 || tone === 100) {
|
|
73
|
+
expect(hct.tone).toBeCloseTo(tone, -1);
|
|
74
|
+
} else if (tone <= 40) {
|
|
75
|
+
// Lower tones have more variation due to gamut limitations
|
|
76
|
+
expect(Math.abs(hct.tone - tone)).toBeLessThan(10);
|
|
77
|
+
} else {
|
|
78
|
+
expect(hct.tone).toBeCloseTo(tone, -1); // Within 10 units
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Verify tone creates proper contrast
|
|
82
|
+
if (tone <= 50) {
|
|
83
|
+
// Dark tones should have good contrast with white
|
|
84
|
+
const argb = hct.toInt();
|
|
85
|
+
const rgb = {
|
|
86
|
+
b: utils.blueFromArgb(argb) / 255,
|
|
87
|
+
g: utils.greenFromArgb(argb) / 255,
|
|
88
|
+
r: utils.redFromArgb(argb) / 255,
|
|
89
|
+
};
|
|
90
|
+
const contrastWithWhite = calculateContrast(rgb, {
|
|
91
|
+
b: 1,
|
|
92
|
+
g: 1,
|
|
93
|
+
r: 1,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (tone <= 40) {
|
|
97
|
+
expect(contrastWithWhite).toBeGreaterThan(3); // WCAG AA for large text
|
|
98
|
+
}
|
|
99
|
+
if (tone <= 30) {
|
|
100
|
+
expect(contrastWithWhite).toBeGreaterThan(4.5); // WCAG AA for normal text
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("CSS Named Colors", () => {
|
|
108
|
+
const namedColors = [
|
|
109
|
+
{ hex: "#ff6347", name: "tomato", rgb: { b: 71, g: 99, r: 255 } },
|
|
110
|
+
{ hex: "#1e90ff", name: "dodgerblue", rgb: { b: 255, g: 144, r: 30 } },
|
|
111
|
+
{ hex: "#663399", name: "rebeccapurple", rgb: { b: 153, g: 51, r: 102 } },
|
|
112
|
+
{ hex: "#ff7f50", name: "coral", rgb: { b: 80, g: 127, r: 255 } },
|
|
113
|
+
{ hex: "#ffd700", name: "gold", rgb: { b: 0, g: 215, r: 255 } },
|
|
114
|
+
{ hex: "#dc143c", name: "crimson", rgb: { b: 60, g: 20, r: 220 } },
|
|
115
|
+
{
|
|
116
|
+
hex: "#3cb371",
|
|
117
|
+
name: "mediumseagreen",
|
|
118
|
+
rgb: { b: 113, g: 179, r: 60 },
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
namedColors.forEach(({ hex, name, rgb: expectedRgb }) => {
|
|
123
|
+
it(`should correctly convert CSS named color: ${name}`, () => {
|
|
124
|
+
const rgb = hexToRgb(hex);
|
|
125
|
+
|
|
126
|
+
// Verify RGB values
|
|
127
|
+
expect(rgb.r).toBe(expectedRgb.r);
|
|
128
|
+
expect(rgb.g).toBe(expectedRgb.g);
|
|
129
|
+
expect(rgb.b).toBe(expectedRgb.b);
|
|
130
|
+
|
|
131
|
+
// Verify round-trip conversion
|
|
132
|
+
const hexBack = rgbToHex(rgb);
|
|
133
|
+
expect(hexBack.toLowerCase()).toBe(hex.toLowerCase());
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should convert rebeccapurple to correct HCT values", () => {
|
|
138
|
+
// rebeccapurple was added to CSS to honor Eric Meyer
|
|
139
|
+
const rgb = hexToRgb("#663399");
|
|
140
|
+
const hct = rgbToHct(rgb);
|
|
141
|
+
|
|
142
|
+
expect(hct.h).toBeCloseTo(309, -1); // Purple hue in HCT
|
|
143
|
+
expect(hct.c).toBeGreaterThan(40); // Moderate chroma
|
|
144
|
+
expect(hct.t).toBeCloseTo(32, -1); // Dark tone
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("WCAG Contrast Ratios", () => {
|
|
149
|
+
it("should verify maximum contrast ratio (black on white)", () => {
|
|
150
|
+
const black = { b: 0, g: 0, r: 0 };
|
|
151
|
+
const white = { b: 1, g: 1, r: 1 };
|
|
152
|
+
|
|
153
|
+
const contrast = calculateContrast(black, white);
|
|
154
|
+
expect(contrast).toBeCloseTo(21, 0); // Maximum possible contrast
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should verify WCAG AA compliance examples", () => {
|
|
158
|
+
// #777777 on white achieves exactly 4.5:1
|
|
159
|
+
const gray777 = hexToRgb("#777777");
|
|
160
|
+
const white = { b: 1, g: 1, r: 1 };
|
|
161
|
+
|
|
162
|
+
const contrast = calculateContrast(
|
|
163
|
+
{ b: gray777.b / 255, g: gray777.g / 255, r: gray777.r / 255 },
|
|
164
|
+
white,
|
|
165
|
+
);
|
|
166
|
+
expect(contrast).toBeCloseTo(4.5, 1); // WCAG AA minimum
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should verify WCAG AAA compliance examples", () => {
|
|
170
|
+
// #595959 on white achieves approximately 7:1
|
|
171
|
+
const gray595 = hexToRgb("#595959");
|
|
172
|
+
const white = { b: 1, g: 1, r: 1 };
|
|
173
|
+
|
|
174
|
+
const contrast = calculateContrast(
|
|
175
|
+
{ b: gray595.b / 255, g: gray595.g / 255, r: gray595.r / 255 },
|
|
176
|
+
white,
|
|
177
|
+
);
|
|
178
|
+
expect(contrast).toBeCloseTo(7, 1); // WCAG AAA minimum
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should verify specific color contrasts on white", () => {
|
|
182
|
+
const white = { b: 1, g: 1, r: 1 };
|
|
183
|
+
|
|
184
|
+
// Pure red on white
|
|
185
|
+
const red = { b: 0, g: 0, r: 1 };
|
|
186
|
+
const redContrast = calculateContrast(red, white);
|
|
187
|
+
expect(redContrast).toBeCloseTo(4, 0.5); // ~4:1
|
|
188
|
+
|
|
189
|
+
// Pure green on white (poor contrast)
|
|
190
|
+
const green = { b: 0, g: 1, r: 0 };
|
|
191
|
+
const greenContrast = calculateContrast(green, white);
|
|
192
|
+
expect(greenContrast).toBeLessThan(2); // ~1.4:1, fails WCAG
|
|
193
|
+
|
|
194
|
+
// Pure blue on white
|
|
195
|
+
const blue = { b: 1, g: 0, r: 0 };
|
|
196
|
+
const blueContrast = calculateContrast(blue, white);
|
|
197
|
+
expect(blueContrast).toBeCloseTo(8.6, 0.5); // ~8.6:1
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("HCT Color Space Properties", () => {
|
|
202
|
+
it("should maintain tone-based contrast guarantees", () => {
|
|
203
|
+
// HCT tone differences guarantee specific contrast ratios
|
|
204
|
+
const hue = 180;
|
|
205
|
+
const chroma = 40;
|
|
206
|
+
|
|
207
|
+
// Tone 40 vs white should give ~3:1 contrast
|
|
208
|
+
const tone40 = Hct.from(hue, chroma, 40);
|
|
209
|
+
const argb40 = tone40.toInt();
|
|
210
|
+
const rgb40 = {
|
|
211
|
+
b: utils.blueFromArgb(argb40) / 255,
|
|
212
|
+
g: utils.greenFromArgb(argb40) / 255,
|
|
213
|
+
r: utils.redFromArgb(argb40) / 255,
|
|
214
|
+
};
|
|
215
|
+
const contrast40 = calculateContrast(rgb40, { b: 1, g: 1, r: 1 });
|
|
216
|
+
expect(contrast40).toBeGreaterThan(2.5); // Close to 3:1
|
|
217
|
+
|
|
218
|
+
// Tone 50 vs white should give ~4.5:1 contrast
|
|
219
|
+
const tone50 = Hct.from(hue, chroma, 50);
|
|
220
|
+
const argb50 = tone50.toInt();
|
|
221
|
+
const rgb50 = {
|
|
222
|
+
b: utils.blueFromArgb(argb50) / 255,
|
|
223
|
+
g: utils.greenFromArgb(argb50) / 255,
|
|
224
|
+
r: utils.redFromArgb(argb50) / 255,
|
|
225
|
+
};
|
|
226
|
+
const contrast50 = calculateContrast(rgb50, { b: 1, g: 1, r: 1 });
|
|
227
|
+
expect(contrast50).toBeGreaterThan(4); // Close to 4.5:1
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should handle HCT chroma clamping for impossible colors", () => {
|
|
231
|
+
// Very high chroma at extreme tones is impossible
|
|
232
|
+
// Very high chroma at extreme tones gets adjusted
|
|
233
|
+
const hct1 = Hct.from(0, 200, 95); // High tone, high chroma
|
|
234
|
+
expect(hct1.chroma).toBeLessThan(120); // Chroma gets clamped
|
|
235
|
+
// Tone might be adjusted when chroma is impossible
|
|
236
|
+
expect(hct1.tone).toBeGreaterThan(50); // Tone adjusted
|
|
237
|
+
|
|
238
|
+
const hct2 = Hct.from(180, 200, 5); // Low tone, high chroma
|
|
239
|
+
expect(hct2.chroma).toBeLessThan(120); // Chroma gets clamped
|
|
240
|
+
// Tone might be adjusted when chroma is impossible
|
|
241
|
+
expect(hct2.tone).toBeLessThan(50); // Tone adjusted
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("Color Space Conversions", () => {
|
|
246
|
+
it("should accurately convert between color spaces", () => {
|
|
247
|
+
// Test with Material Design Indigo 500
|
|
248
|
+
const indigo500 = "#3F51B5";
|
|
249
|
+
const rgb = hexToRgb(indigo500);
|
|
250
|
+
|
|
251
|
+
// To HSL
|
|
252
|
+
const hsl = rgbToHsl(rgb);
|
|
253
|
+
expect(hsl.h).toBeCloseTo(231, -1); // Blue-purple hue
|
|
254
|
+
expect(hsl.s).toBeCloseTo(48, -1); // Moderate saturation (percentage)
|
|
255
|
+
expect(hsl.l).toBeCloseTo(48, -1); // Medium lightness (percentage)
|
|
256
|
+
|
|
257
|
+
// To HCT
|
|
258
|
+
const hct = rgbToHct(rgb);
|
|
259
|
+
expect(hct.h).toBeCloseTo(295, -1); // Blue-purple in HCT
|
|
260
|
+
expect(hct.c).toBeGreaterThan(30); // Moderate chroma
|
|
261
|
+
expect(hct.t).toBeCloseTo(40, -1); // Medium-dark tone
|
|
262
|
+
|
|
263
|
+
// Round-trip test
|
|
264
|
+
const hctObj = Hct.from(hct.h, hct.c, hct.t);
|
|
265
|
+
const argb = hctObj.toInt();
|
|
266
|
+
const rgbBack = {
|
|
267
|
+
b: utils.blueFromArgb(argb),
|
|
268
|
+
g: utils.greenFromArgb(argb),
|
|
269
|
+
r: utils.redFromArgb(argb),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Allow some tolerance due to color space conversions
|
|
273
|
+
expect(rgbBack.r).toBeCloseTo(rgb.r, -1);
|
|
274
|
+
expect(rgbBack.g).toBeCloseTo(rgb.g, -1);
|
|
275
|
+
expect(rgbBack.b).toBeCloseTo(rgb.b, -1);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip conversion tests
|
|
3
|
+
* Inspired by chromaticity-color-utilities tests
|
|
4
|
+
* Tests that converting from one color space to another and back preserves values
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
hexToRgb,
|
|
11
|
+
hslToRgb,
|
|
12
|
+
hsvToRgb,
|
|
13
|
+
labToRgb,
|
|
14
|
+
rgbToHex,
|
|
15
|
+
rgbToHsl,
|
|
16
|
+
rgbToHsv,
|
|
17
|
+
rgbToLab,
|
|
18
|
+
rgbToXyz,
|
|
19
|
+
xyzToRgb,
|
|
20
|
+
} from "../conversions";
|
|
21
|
+
|
|
22
|
+
describe("Round-trip Conversions", () => {
|
|
23
|
+
// Test a range of colors with step to avoid too many tests
|
|
24
|
+
const testColors = [
|
|
25
|
+
{ b: 0, g: 0, r: 0 }, // Black
|
|
26
|
+
{ b: 255, g: 255, r: 255 }, // White
|
|
27
|
+
{ b: 0, g: 0, r: 255 }, // Red
|
|
28
|
+
{ b: 0, g: 255, r: 0 }, // Green
|
|
29
|
+
{ b: 255, g: 0, r: 0 }, // Blue
|
|
30
|
+
{ b: 255, g: 0, r: 255 }, // Magenta
|
|
31
|
+
{ b: 0, g: 255, r: 255 }, // Yellow
|
|
32
|
+
{ b: 255, g: 255, r: 0 }, // Cyan
|
|
33
|
+
{ b: 128, g: 128, r: 128 }, // Gray
|
|
34
|
+
{ b: 0, g: 128, r: 255 }, // Orange
|
|
35
|
+
{ b: 128, g: 0, r: 128 }, // Purple
|
|
36
|
+
{ b: 208, g: 224, r: 64 }, // Turquoise
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Additional random colors for broader testing
|
|
40
|
+
for (let i = 0; i < 20; i++) {
|
|
41
|
+
testColors.push({
|
|
42
|
+
b: Math.floor(Math.random() * 256),
|
|
43
|
+
g: Math.floor(Math.random() * 256),
|
|
44
|
+
r: Math.floor(Math.random() * 256),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("RGB ↔ HSL", () => {
|
|
49
|
+
testColors.forEach((color, index) => {
|
|
50
|
+
it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
|
|
51
|
+
const hsl = rgbToHsl(color);
|
|
52
|
+
const rgbBack = hslToRgb(hsl);
|
|
53
|
+
|
|
54
|
+
expect(rgbBack.r).toBeCloseTo(color.r, -1); // Within 10
|
|
55
|
+
expect(rgbBack.g).toBeCloseTo(color.g, -1);
|
|
56
|
+
expect(rgbBack.b).toBeCloseTo(color.b, -1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("RGB ↔ HSV", () => {
|
|
62
|
+
testColors.forEach((color, index) => {
|
|
63
|
+
it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
|
|
64
|
+
const hsv = rgbToHsv(color);
|
|
65
|
+
const rgbBack = hsvToRgb(hsv);
|
|
66
|
+
|
|
67
|
+
expect(rgbBack.r).toBeCloseTo(color.r, -1);
|
|
68
|
+
expect(rgbBack.g).toBeCloseTo(color.g, -1);
|
|
69
|
+
expect(rgbBack.b).toBeCloseTo(color.b, -1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("RGB ↔ LAB", () => {
|
|
75
|
+
testColors.forEach((color, index) => {
|
|
76
|
+
it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
|
|
77
|
+
const lab = rgbToLab(color);
|
|
78
|
+
const rgbBack = labToRgb(lab);
|
|
79
|
+
|
|
80
|
+
// LAB conversions have more variance
|
|
81
|
+
expect(rgbBack.r).toBeCloseTo(color.r, -1.5); // Within 32
|
|
82
|
+
expect(rgbBack.g).toBeCloseTo(color.g, -1.5);
|
|
83
|
+
expect(rgbBack.b).toBeCloseTo(color.b, -1.5);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("RGB ↔ XYZ", () => {
|
|
89
|
+
testColors.forEach((color, index) => {
|
|
90
|
+
it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
|
|
91
|
+
const xyz = rgbToXyz(color);
|
|
92
|
+
const rgbBack = xyzToRgb(xyz);
|
|
93
|
+
|
|
94
|
+
expect(rgbBack.r).toBeCloseTo(color.r, -1);
|
|
95
|
+
expect(rgbBack.g).toBeCloseTo(color.g, -1);
|
|
96
|
+
expect(rgbBack.b).toBeCloseTo(color.b, -1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("RGB ↔ HEX", () => {
|
|
102
|
+
testColors.forEach((color, index) => {
|
|
103
|
+
it(`should preserve color ${index}: rgb(${color.r}, ${color.g}, ${color.b})`, () => {
|
|
104
|
+
const hex = rgbToHex(color);
|
|
105
|
+
const rgbBack = hexToRgb(hex);
|
|
106
|
+
|
|
107
|
+
// HEX should be exact
|
|
108
|
+
expect(rgbBack.r).toBe(color.r);
|
|
109
|
+
expect(rgbBack.g).toBe(color.g);
|
|
110
|
+
expect(rgbBack.b).toBe(color.b);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Reference colors from chromaticity", () => {
|
|
116
|
+
it("should convert magenta correctly in LAB", () => {
|
|
117
|
+
const magenta = { b: 255, g: 0, r: 255 };
|
|
118
|
+
const lab = rgbToLab(magenta);
|
|
119
|
+
|
|
120
|
+
// Chromaticity reference: L=60, a=98, b=-61
|
|
121
|
+
expect(lab.l).toBeCloseTo(60, -1);
|
|
122
|
+
expect(lab.a).toBeCloseTo(98, -1);
|
|
123
|
+
expect(lab.b).toBeCloseTo(-61, -1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("Grayscale preservation", () => {
|
|
128
|
+
it("should preserve grayscale in all color spaces", () => {
|
|
129
|
+
const grays = [
|
|
130
|
+
{ b: 0, g: 0, r: 0 },
|
|
131
|
+
{ b: 64, g: 64, r: 64 },
|
|
132
|
+
{ b: 128, g: 128, r: 128 },
|
|
133
|
+
{ b: 192, g: 192, r: 192 },
|
|
134
|
+
{ b: 255, g: 255, r: 255 },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
grays.forEach((gray) => {
|
|
138
|
+
// HSL
|
|
139
|
+
const hsl = rgbToHsl(gray);
|
|
140
|
+
expect(hsl.s).toBe(0); // No saturation for grays
|
|
141
|
+
|
|
142
|
+
// HSV
|
|
143
|
+
const hsv = rgbToHsv(gray);
|
|
144
|
+
expect(hsv.s).toBe(0); // No saturation for grays
|
|
145
|
+
|
|
146
|
+
// LAB
|
|
147
|
+
const lab = rgbToLab(gray);
|
|
148
|
+
expect(Math.abs(lab.a)).toBeLessThan(1); // Near zero
|
|
149
|
+
expect(Math.abs(lab.b)).toBeLessThan(1); // Near zero
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Primary colors", () => {
|
|
155
|
+
it("should handle pure red correctly", () => {
|
|
156
|
+
const red = { b: 0, g: 0, r: 255 };
|
|
157
|
+
|
|
158
|
+
const hsl = rgbToHsl(red);
|
|
159
|
+
expect(hsl.h).toBe(0);
|
|
160
|
+
expect(hsl.s).toBe(100);
|
|
161
|
+
expect(hsl.l).toBe(50);
|
|
162
|
+
|
|
163
|
+
const hsv = rgbToHsv(red);
|
|
164
|
+
expect(hsv.h).toBe(0);
|
|
165
|
+
expect(hsv.s).toBe(100);
|
|
166
|
+
expect(hsv.v).toBe(100);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should handle pure green correctly", () => {
|
|
170
|
+
const green = { b: 0, g: 255, r: 0 };
|
|
171
|
+
|
|
172
|
+
const hsl = rgbToHsl(green);
|
|
173
|
+
expect(hsl.h).toBe(120);
|
|
174
|
+
expect(hsl.s).toBe(100);
|
|
175
|
+
expect(hsl.l).toBe(50);
|
|
176
|
+
|
|
177
|
+
const hsv = rgbToHsv(green);
|
|
178
|
+
expect(hsv.h).toBe(120);
|
|
179
|
+
expect(hsv.s).toBe(100);
|
|
180
|
+
expect(hsv.v).toBe(100);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle pure blue correctly", () => {
|
|
184
|
+
const blue = { b: 255, g: 0, r: 0 };
|
|
185
|
+
|
|
186
|
+
const hsl = rgbToHsl(blue);
|
|
187
|
+
expect(hsl.h).toBe(240);
|
|
188
|
+
expect(hsl.s).toBe(100);
|
|
189
|
+
expect(hsl.l).toBe(50);
|
|
190
|
+
|
|
191
|
+
const hsv = rgbToHsv(blue);
|
|
192
|
+
expect(hsv.h).toBe(240);
|
|
193
|
+
expect(hsv.s).toBe(100);
|
|
194
|
+
expect(hsv.v).toBe(100);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|