@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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// This file is automatically generated. Do not modify it.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Utility methods for mathematical operations.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Clamps an integer between two floating-point numbers.
|
|
26
|
+
*
|
|
27
|
+
* @return input when min <= input <= max, and either min or max
|
|
28
|
+
* otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function clampDouble(min: number, max: number, input: number): number {
|
|
31
|
+
if (input < min) {
|
|
32
|
+
return min;
|
|
33
|
+
} else if (input > max) {
|
|
34
|
+
return max;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return input;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clamps an integer between two integers.
|
|
42
|
+
*
|
|
43
|
+
* @return input when min <= input <= max, and either min or max
|
|
44
|
+
* otherwise.
|
|
45
|
+
*/
|
|
46
|
+
export function clampInt(min: number, max: number, input: number): number {
|
|
47
|
+
if (input < min) {
|
|
48
|
+
return min;
|
|
49
|
+
} else if (input > max) {
|
|
50
|
+
return max;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return input;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Distance of two points on a circle, represented using degrees.
|
|
58
|
+
*/
|
|
59
|
+
export function differenceDegrees(a: number, b: number): number {
|
|
60
|
+
return 180.0 - Math.abs(Math.abs(a - b) - 180.0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The linear interpolation function.
|
|
65
|
+
*
|
|
66
|
+
* @return start if amount = 0 and stop if amount = 1
|
|
67
|
+
*/
|
|
68
|
+
export function lerp(start: number, stop: number, amount: number): number {
|
|
69
|
+
return (1.0 - amount) * start + amount * stop;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Multiplies a 1x3 row vector with a 3x3 matrix.
|
|
74
|
+
*/
|
|
75
|
+
export function matrixMultiply(row: number[], matrix: number[][]): number[] {
|
|
76
|
+
const a =
|
|
77
|
+
row[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
|
|
78
|
+
const b =
|
|
79
|
+
row[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
|
|
80
|
+
const c =
|
|
81
|
+
row[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
|
|
82
|
+
return [a, b, c];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sign of direction change needed to travel from one angle to
|
|
87
|
+
* another.
|
|
88
|
+
*
|
|
89
|
+
* For angles that are 180 degrees apart from each other, both
|
|
90
|
+
* directions have the same travel distance, so either direction is
|
|
91
|
+
* shortest. The value 1.0 is returned in this case.
|
|
92
|
+
*
|
|
93
|
+
* @param from The angle travel starts from, in degrees.
|
|
94
|
+
* @param to The angle travel ends at, in degrees.
|
|
95
|
+
* @return -1 if decreasing from leads to the shortest travel
|
|
96
|
+
* distance, 1 if increasing from leads to the shortest travel
|
|
97
|
+
* distance.
|
|
98
|
+
*/
|
|
99
|
+
export function rotationDirection(from: number, to: number): number {
|
|
100
|
+
const increasingDifference = sanitizeDegreesDouble(to - from);
|
|
101
|
+
return increasingDifference <= 180.0 ? 1.0 : -1.0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sanitizes a degree measure as a floating-point number.
|
|
106
|
+
*
|
|
107
|
+
* @return a degree measure between 0.0 (inclusive) and 360.0
|
|
108
|
+
* (exclusive).
|
|
109
|
+
*/
|
|
110
|
+
export function sanitizeDegreesDouble(degrees: number): number {
|
|
111
|
+
degrees = degrees % 360.0;
|
|
112
|
+
if (degrees < 0) {
|
|
113
|
+
degrees = degrees + 360.0;
|
|
114
|
+
}
|
|
115
|
+
return degrees;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sanitizes a degree measure as an integer.
|
|
120
|
+
*
|
|
121
|
+
* @return a degree measure between 0 (inclusive) and 360
|
|
122
|
+
* (exclusive).
|
|
123
|
+
*/
|
|
124
|
+
export function sanitizeDegreesInt(degrees: number): number {
|
|
125
|
+
degrees = degrees % 360;
|
|
126
|
+
if (degrees < 0) {
|
|
127
|
+
degrees = degrees + 360;
|
|
128
|
+
}
|
|
129
|
+
return degrees;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The signum function.
|
|
134
|
+
*
|
|
135
|
+
* @return 1 if num > 0, -1 if num < 0, and 0 if num = 0
|
|
136
|
+
*/
|
|
137
|
+
export function signum(num: number): number {
|
|
138
|
+
if (num < 0) {
|
|
139
|
+
return -1;
|
|
140
|
+
} else if (num === 0) {
|
|
141
|
+
return 0;
|
|
142
|
+
} else {
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for color utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { RGB } from "./types";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
clampHsl,
|
|
11
|
+
clampHsv,
|
|
12
|
+
clampRgb,
|
|
13
|
+
darken,
|
|
14
|
+
desaturate,
|
|
15
|
+
formatColor,
|
|
16
|
+
formatHsl,
|
|
17
|
+
formatRgb,
|
|
18
|
+
getComplementary,
|
|
19
|
+
getContrastRatio,
|
|
20
|
+
getLuminance,
|
|
21
|
+
invertColor,
|
|
22
|
+
isDark,
|
|
23
|
+
isLight,
|
|
24
|
+
isValidHex,
|
|
25
|
+
isValidHsl,
|
|
26
|
+
isValidHsv,
|
|
27
|
+
isValidRgb,
|
|
28
|
+
lighten,
|
|
29
|
+
meetsContrastAA,
|
|
30
|
+
meetsContrastAAA,
|
|
31
|
+
mixColors,
|
|
32
|
+
randomColor,
|
|
33
|
+
saturate,
|
|
34
|
+
toGrayscale,
|
|
35
|
+
} from "./utils";
|
|
36
|
+
|
|
37
|
+
describe("Color Utils", () => {
|
|
38
|
+
describe("Validation functions", () => {
|
|
39
|
+
describe("isValidRgb", () => {
|
|
40
|
+
it("should validate RGB colors correctly", () => {
|
|
41
|
+
expect(isValidRgb({ b: 0, g: 0, r: 0 })).toBe(true);
|
|
42
|
+
expect(isValidRgb({ b: 255, g: 255, r: 255 })).toBe(true);
|
|
43
|
+
expect(isValidRgb({ b: 128, g: 128, r: 128 })).toBe(true);
|
|
44
|
+
|
|
45
|
+
expect(isValidRgb({ b: 0, g: 0, r: -1 })).toBe(false);
|
|
46
|
+
expect(isValidRgb({ b: 0, g: 0, r: 256 })).toBe(false);
|
|
47
|
+
expect(isValidRgb({ b: 0, g: -1, r: 0 })).toBe(false);
|
|
48
|
+
expect(isValidRgb({ b: 256, g: 0, r: 0 })).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("isValidHsl", () => {
|
|
53
|
+
it("should validate HSL colors correctly", () => {
|
|
54
|
+
expect(isValidHsl({ h: 0, l: 0, s: 0 })).toBe(true);
|
|
55
|
+
expect(isValidHsl({ h: 360, l: 100, s: 100 })).toBe(true);
|
|
56
|
+
expect(isValidHsl({ h: 180, l: 50, s: 50 })).toBe(true);
|
|
57
|
+
|
|
58
|
+
expect(isValidHsl({ h: -1, l: 0, s: 0 })).toBe(false);
|
|
59
|
+
expect(isValidHsl({ h: 361, l: 0, s: 0 })).toBe(false);
|
|
60
|
+
expect(isValidHsl({ h: 0, l: 0, s: -1 })).toBe(false);
|
|
61
|
+
expect(isValidHsl({ h: 0, l: 0, s: 101 })).toBe(false);
|
|
62
|
+
expect(isValidHsl({ h: 0, l: -1, s: 0 })).toBe(false);
|
|
63
|
+
expect(isValidHsl({ h: 0, l: 101, s: 0 })).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("isValidHsv", () => {
|
|
68
|
+
it("should validate HSV colors correctly", () => {
|
|
69
|
+
expect(isValidHsv({ h: 0, s: 0, v: 0 })).toBe(true);
|
|
70
|
+
expect(isValidHsv({ h: 360, s: 100, v: 100 })).toBe(true);
|
|
71
|
+
expect(isValidHsv({ h: 180, s: 50, v: 50 })).toBe(true);
|
|
72
|
+
|
|
73
|
+
expect(isValidHsv({ h: -1, s: 0, v: 0 })).toBe(false);
|
|
74
|
+
expect(isValidHsv({ h: 361, s: 0, v: 0 })).toBe(false);
|
|
75
|
+
expect(isValidHsv({ h: 0, s: -1, v: 0 })).toBe(false);
|
|
76
|
+
expect(isValidHsv({ h: 0, s: 101, v: 0 })).toBe(false);
|
|
77
|
+
expect(isValidHsv({ h: 0, s: 0, v: -1 })).toBe(false);
|
|
78
|
+
expect(isValidHsv({ h: 0, s: 0, v: 101 })).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("isValidHex", () => {
|
|
83
|
+
it("should validate hex colors correctly", () => {
|
|
84
|
+
expect(isValidHex("#fff")).toBe(true);
|
|
85
|
+
expect(isValidHex("#ffffff")).toBe(true);
|
|
86
|
+
expect(isValidHex("#000")).toBe(true);
|
|
87
|
+
expect(isValidHex("#000000")).toBe(true);
|
|
88
|
+
expect(isValidHex("fff")).toBe(true);
|
|
89
|
+
expect(isValidHex("ffffff")).toBe(true);
|
|
90
|
+
|
|
91
|
+
expect(isValidHex("#ff")).toBe(false);
|
|
92
|
+
expect(isValidHex("#ffff")).toBe(false);
|
|
93
|
+
expect(isValidHex("#fffff")).toBe(false);
|
|
94
|
+
expect(isValidHex("#fffffff")).toBe(false);
|
|
95
|
+
expect(isValidHex("#ggg")).toBe(false);
|
|
96
|
+
expect(isValidHex("xyz")).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("Clamping functions", () => {
|
|
102
|
+
describe("clampRgb", () => {
|
|
103
|
+
it("should clamp RGB values to valid range", () => {
|
|
104
|
+
expect(clampRgb({ b: 128, g: 300, r: -10 })).toEqual({
|
|
105
|
+
b: 128,
|
|
106
|
+
g: 255,
|
|
107
|
+
r: 0,
|
|
108
|
+
});
|
|
109
|
+
expect(clampRgb({ b: 0, g: 0, r: 0 })).toEqual({ b: 0, g: 0, r: 0 });
|
|
110
|
+
expect(clampRgb({ b: 255, g: 255, r: 255 })).toEqual({
|
|
111
|
+
b: 255,
|
|
112
|
+
g: 255,
|
|
113
|
+
r: 255,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("clampHsl", () => {
|
|
119
|
+
it("should clamp HSL values to valid range", () => {
|
|
120
|
+
expect(clampHsl({ h: -10, l: 50, s: 150 })).toEqual({
|
|
121
|
+
h: 0,
|
|
122
|
+
l: 50,
|
|
123
|
+
s: 100,
|
|
124
|
+
});
|
|
125
|
+
expect(clampHsl({ h: 400, l: 150, s: -10 })).toEqual({
|
|
126
|
+
h: 360,
|
|
127
|
+
l: 100,
|
|
128
|
+
s: 0,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("clampHsv", () => {
|
|
134
|
+
it("should clamp HSV values to valid range", () => {
|
|
135
|
+
expect(clampHsv({ h: -10, s: 150, v: 50 })).toEqual({
|
|
136
|
+
h: 0,
|
|
137
|
+
s: 100,
|
|
138
|
+
v: 50,
|
|
139
|
+
});
|
|
140
|
+
expect(clampHsv({ h: 400, s: -10, v: 150 })).toEqual({
|
|
141
|
+
h: 360,
|
|
142
|
+
s: 0,
|
|
143
|
+
v: 100,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("Luminance and contrast", () => {
|
|
150
|
+
describe("getLuminance", () => {
|
|
151
|
+
it("should calculate luminance correctly", () => {
|
|
152
|
+
expect(getLuminance({ b: 0, g: 0, r: 0 })).toBeCloseTo(0, 3);
|
|
153
|
+
expect(getLuminance({ b: 255, g: 255, r: 255 })).toBeCloseTo(1, 3);
|
|
154
|
+
|
|
155
|
+
// Red has lower luminance than green
|
|
156
|
+
const redLum = getLuminance({ b: 0, g: 0, r: 255 });
|
|
157
|
+
const greenLum = getLuminance({ b: 0, g: 255, r: 0 });
|
|
158
|
+
expect(redLum).toBeLessThan(greenLum);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("getContrastRatio", () => {
|
|
163
|
+
it("should calculate contrast ratio correctly", () => {
|
|
164
|
+
const black: RGB = { b: 0, g: 0, r: 0 };
|
|
165
|
+
const white: RGB = { b: 255, g: 255, r: 255 };
|
|
166
|
+
|
|
167
|
+
expect(getContrastRatio(black, white)).toBeCloseTo(21, 0);
|
|
168
|
+
expect(getContrastRatio(white, black)).toBeCloseTo(21, 0);
|
|
169
|
+
expect(getContrastRatio(black, black)).toBeCloseTo(1, 0);
|
|
170
|
+
expect(getContrastRatio(white, white)).toBeCloseTo(1, 0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("meetsContrastAA", () => {
|
|
175
|
+
it("should check AA contrast requirements", () => {
|
|
176
|
+
const black: RGB = { b: 0, g: 0, r: 0 };
|
|
177
|
+
const white: RGB = { b: 255, g: 255, r: 255 };
|
|
178
|
+
const gray: RGB = { b: 128, g: 128, r: 128 };
|
|
179
|
+
|
|
180
|
+
expect(meetsContrastAA(black, white)).toBe(true);
|
|
181
|
+
expect(meetsContrastAA(black, gray)).toBe(true);
|
|
182
|
+
expect(meetsContrastAA(white, gray)).toBe(false);
|
|
183
|
+
|
|
184
|
+
// Large text has lower requirements
|
|
185
|
+
expect(meetsContrastAA(white, gray, true)).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("meetsContrastAAA", () => {
|
|
190
|
+
it("should check AAA contrast requirements", () => {
|
|
191
|
+
const black: RGB = { b: 0, g: 0, r: 0 };
|
|
192
|
+
const white: RGB = { b: 255, g: 255, r: 255 };
|
|
193
|
+
const darkGray: RGB = { b: 85, g: 85, r: 85 }; // Adjusted for better contrast
|
|
194
|
+
|
|
195
|
+
expect(meetsContrastAAA(black, white)).toBe(true);
|
|
196
|
+
expect(meetsContrastAAA(black, darkGray)).toBe(false);
|
|
197
|
+
|
|
198
|
+
// Large text has lower requirements - use lighter gray for AAA large text
|
|
199
|
+
const mediumGray: RGB = { b: 118, g: 118, r: 118 };
|
|
200
|
+
expect(meetsContrastAAA(black, mediumGray, true)).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("Formatting functions", () => {
|
|
206
|
+
describe("formatRgb", () => {
|
|
207
|
+
it("should format RGB to string", () => {
|
|
208
|
+
expect(formatRgb({ b: 0, g: 0, r: 255 })).toBe("rgb(255, 0, 0)");
|
|
209
|
+
expect(formatRgb({ b: 128, g: 128, r: 128 }, 0.5)).toBe(
|
|
210
|
+
"rgba(128, 128, 128, 0.5)",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("formatHsl", () => {
|
|
216
|
+
it("should format HSL to string", () => {
|
|
217
|
+
expect(formatHsl({ h: 0, l: 50, s: 100 })).toBe("hsl(0, 100%, 50%)");
|
|
218
|
+
expect(formatHsl({ h: 180, l: 50, s: 50 }, 0.5)).toBe(
|
|
219
|
+
"hsla(180, 50%, 50%, 0.5)",
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("formatColor", () => {
|
|
225
|
+
it("should format color to various formats", () => {
|
|
226
|
+
const red: RGB = { b: 0, g: 0, r: 255 };
|
|
227
|
+
|
|
228
|
+
expect(formatColor(red, "hex")).toBe("#ff0000");
|
|
229
|
+
expect(formatColor(red, "rgb")).toBe("rgb(255, 0, 0)");
|
|
230
|
+
expect(formatColor(red, "hsl")).toBe("hsl(0, 100%, 50%)");
|
|
231
|
+
expect(formatColor(red, "hsv")).toBe("hsv(0, 100%, 100%)");
|
|
232
|
+
expect(formatColor(red)).toBe("#ff0000"); // Default to hex
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("Color manipulation", () => {
|
|
238
|
+
describe("mixColors", () => {
|
|
239
|
+
it("should mix colors correctly", () => {
|
|
240
|
+
const red: RGB = { b: 0, g: 0, r: 255 };
|
|
241
|
+
const blue: RGB = { b: 255, g: 0, r: 0 };
|
|
242
|
+
|
|
243
|
+
const purple = mixColors(red, blue, 0.5);
|
|
244
|
+
expect(purple.r).toBeCloseTo(128, 0);
|
|
245
|
+
expect(purple.g).toBe(0);
|
|
246
|
+
expect(purple.b).toBeCloseTo(128, 0);
|
|
247
|
+
|
|
248
|
+
// Full weight to first color
|
|
249
|
+
expect(mixColors(red, blue, 1)).toEqual(red);
|
|
250
|
+
|
|
251
|
+
// Full weight to second color
|
|
252
|
+
expect(mixColors(red, blue, 0)).toEqual(blue);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("lighten", () => {
|
|
257
|
+
it("should lighten colors", () => {
|
|
258
|
+
const color: RGB = { b: 0, g: 0, r: 128 };
|
|
259
|
+
const lighter = lighten(color, 20);
|
|
260
|
+
|
|
261
|
+
expect(lighter.r).toBeGreaterThan(color.r);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("darken", () => {
|
|
266
|
+
it("should darken colors", () => {
|
|
267
|
+
const color: RGB = { b: 128, g: 128, r: 255 };
|
|
268
|
+
const darker = darken(color, 20);
|
|
269
|
+
|
|
270
|
+
expect(darker.r).toBeLessThanOrEqual(color.r);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("saturate", () => {
|
|
275
|
+
it("should increase saturation", () => {
|
|
276
|
+
const color: RGB = { b: 100, g: 100, r: 200 };
|
|
277
|
+
const saturated = saturate(color, 20);
|
|
278
|
+
|
|
279
|
+
// More saturated colors have greater difference between channels
|
|
280
|
+
const originalDiff =
|
|
281
|
+
Math.max(color.r, color.g, color.b) -
|
|
282
|
+
Math.min(color.r, color.g, color.b);
|
|
283
|
+
const saturatedDiff =
|
|
284
|
+
Math.max(saturated.r, saturated.g, saturated.b) -
|
|
285
|
+
Math.min(saturated.r, saturated.g, saturated.b);
|
|
286
|
+
|
|
287
|
+
expect(saturatedDiff).toBeGreaterThanOrEqual(originalDiff);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("desaturate", () => {
|
|
292
|
+
it("should decrease saturation", () => {
|
|
293
|
+
const color: RGB = { b: 0, g: 0, r: 255 };
|
|
294
|
+
const desaturated = desaturate(color, 50);
|
|
295
|
+
|
|
296
|
+
// Less saturated colors have less difference between channels
|
|
297
|
+
const originalDiff =
|
|
298
|
+
Math.max(color.r, color.g, color.b) -
|
|
299
|
+
Math.min(color.r, color.g, color.b);
|
|
300
|
+
const desaturatedDiff =
|
|
301
|
+
Math.max(desaturated.r, desaturated.g, desaturated.b) -
|
|
302
|
+
Math.min(desaturated.r, desaturated.g, desaturated.b);
|
|
303
|
+
|
|
304
|
+
expect(desaturatedDiff).toBeLessThan(originalDiff);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("toGrayscale", () => {
|
|
309
|
+
it("should convert to grayscale", () => {
|
|
310
|
+
const red: RGB = { b: 0, g: 0, r: 255 };
|
|
311
|
+
const gray = toGrayscale(red);
|
|
312
|
+
|
|
313
|
+
expect(gray.r).toBe(gray.g);
|
|
314
|
+
expect(gray.g).toBe(gray.b);
|
|
315
|
+
expect(gray.r).toBeCloseTo(76, 0); // Weighted average
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("invertColor", () => {
|
|
320
|
+
it("should invert colors", () => {
|
|
321
|
+
expect(invertColor({ b: 128, g: 0, r: 255 })).toEqual({
|
|
322
|
+
b: 127,
|
|
323
|
+
g: 255,
|
|
324
|
+
r: 0,
|
|
325
|
+
});
|
|
326
|
+
expect(invertColor({ b: 0, g: 0, r: 0 })).toEqual({
|
|
327
|
+
b: 255,
|
|
328
|
+
g: 255,
|
|
329
|
+
r: 255,
|
|
330
|
+
});
|
|
331
|
+
expect(invertColor({ b: 255, g: 255, r: 255 })).toEqual({
|
|
332
|
+
b: 0,
|
|
333
|
+
g: 0,
|
|
334
|
+
r: 0,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("getComplementary", () => {
|
|
340
|
+
it("should get complementary color", () => {
|
|
341
|
+
const red: RGB = { b: 0, g: 0, r: 255 };
|
|
342
|
+
const complementary = getComplementary(red);
|
|
343
|
+
|
|
344
|
+
// Complementary of red should be cyan
|
|
345
|
+
expect(complementary.r).toBe(0);
|
|
346
|
+
expect(complementary.g).toBe(255);
|
|
347
|
+
expect(complementary.b).toBe(255);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("Random and detection", () => {
|
|
353
|
+
describe("randomColor", () => {
|
|
354
|
+
it("should generate valid random colors", () => {
|
|
355
|
+
for (let i = 0; i < 10; i++) {
|
|
356
|
+
const color = randomColor();
|
|
357
|
+
expect(isValidRgb(color)).toBe(true);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should generate different colors", () => {
|
|
362
|
+
const colors = new Set();
|
|
363
|
+
for (let i = 0; i < 100; i++) {
|
|
364
|
+
const color = randomColor();
|
|
365
|
+
colors.add(`${color.r},${color.g},${color.b}`);
|
|
366
|
+
}
|
|
367
|
+
// Should generate at least some different colors
|
|
368
|
+
expect(colors.size).toBeGreaterThan(50);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("isDark", () => {
|
|
373
|
+
it("should identify dark colors", () => {
|
|
374
|
+
expect(isDark({ b: 0, g: 0, r: 0 })).toBe(true);
|
|
375
|
+
expect(isDark({ b: 50, g: 50, r: 50 })).toBe(true);
|
|
376
|
+
expect(isDark({ b: 255, g: 255, r: 255 })).toBe(false);
|
|
377
|
+
expect(isDark({ b: 200, g: 200, r: 200 })).toBe(false);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should use custom threshold", () => {
|
|
381
|
+
const gray: RGB = { b: 128, g: 128, r: 128 };
|
|
382
|
+
// Gray at 128 has luminance ~0.22
|
|
383
|
+
expect(isDark(gray, 0.2)).toBe(false);
|
|
384
|
+
expect(isDark(gray, 0.25)).toBe(true); // Use lower threshold
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("isLight", () => {
|
|
389
|
+
it("should identify light colors", () => {
|
|
390
|
+
expect(isLight({ b: 255, g: 255, r: 255 })).toBe(true);
|
|
391
|
+
expect(isLight({ b: 200, g: 200, r: 200 })).toBe(true);
|
|
392
|
+
expect(isLight({ b: 0, g: 0, r: 0 })).toBe(false);
|
|
393
|
+
expect(isLight({ b: 50, g: 50, r: 50 })).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should use custom threshold", () => {
|
|
397
|
+
const gray: RGB = { b: 128, g: 128, r: 128 };
|
|
398
|
+
expect(isLight(gray, 0.2)).toBe(true); // Use lower threshold
|
|
399
|
+
expect(isLight(gray, 0.25)).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|