@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.
Files changed (197) hide show
  1. package/.claude/settings.local.json +39 -0
  2. package/.env +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
  5. package/.github/pull_request_template.md +97 -0
  6. package/.github/workflows/ci.yml +127 -0
  7. package/.github/workflows/deploy-docs.yml +56 -0
  8. package/.github/workflows/release.yml +99 -0
  9. package/.mcp.json +12 -0
  10. package/.prettierignore +1 -0
  11. package/CLAUDE.md +201 -0
  12. package/DOCUMENTATION.md +274 -0
  13. package/GEMINI.md +54 -0
  14. package/LICENSE +21 -0
  15. package/README.md +401 -0
  16. package/demo/content_based_color.png +0 -0
  17. package/demo/music-player.html +621 -0
  18. package/demo/podcast-player.html +903 -0
  19. package/dist/bin/coolors-mcp.d.ts +1 -0
  20. package/dist/bin/coolors-mcp.js +154 -0
  21. package/dist/bin/coolors-mcp.js.map +1 -0
  22. package/dist/bin/server.d.ts +1 -0
  23. package/dist/bin/server.js +3292 -0
  24. package/dist/bin/server.js.map +1 -0
  25. package/dist/chunk-IQ7NN26V.js +114 -0
  26. package/dist/chunk-IQ7NN26V.js.map +1 -0
  27. package/dist/chunk-P3ARRKLS.js +1214 -0
  28. package/dist/chunk-P3ARRKLS.js.map +1 -0
  29. package/dist/color/index.d.ts +716 -0
  30. package/dist/color/index.js +153 -0
  31. package/dist/color/index.js.map +1 -0
  32. package/dist/coolors-mcp.d.ts +136 -0
  33. package/dist/coolors-mcp.js +7 -0
  34. package/dist/coolors-mcp.js.map +1 -0
  35. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  36. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  37. package/docs/.vitepress/cache/deps/_metadata.json +127 -0
  38. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  39. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  40. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
  41. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
  42. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
  43. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
  44. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  45. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  46. package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
  47. package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
  48. package/docs/.vitepress/cache/deps/dayjs.js +285 -0
  49. package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
  50. package/docs/.vitepress/cache/deps/debug.js +468 -0
  51. package/docs/.vitepress/cache/deps/debug.js.map +7 -0
  52. package/docs/.vitepress/cache/deps/package.json +3 -0
  53. package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
  54. package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
  55. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
  56. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
  57. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
  58. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
  59. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
  60. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
  61. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
  62. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
  63. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
  64. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
  65. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
  66. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
  67. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  68. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  69. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  70. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  71. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
  72. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  73. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  74. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  75. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
  76. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  77. package/docs/.vitepress/cache/deps/vue.js +344 -0
  78. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  79. package/docs/.vitepress/components/ClientGrid.vue +125 -0
  80. package/docs/.vitepress/components/CodeBlock.vue +231 -0
  81. package/docs/.vitepress/components/ConfigModal.vue +477 -0
  82. package/docs/.vitepress/components/DiagramModal.vue +528 -0
  83. package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
  84. package/docs/.vitepress/config.js +162 -0
  85. package/docs/.vitepress/theme/FundingLayout.vue +251 -0
  86. package/docs/.vitepress/theme/Layout.vue +134 -0
  87. package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
  88. package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
  89. package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
  90. package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
  91. package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
  92. package/docs/.vitepress/theme/custom-app.css +339 -0
  93. package/docs/.vitepress/theme/custom.css +699 -0
  94. package/docs/.vitepress/theme/index.js +25 -0
  95. package/docs/README.md +198 -0
  96. package/docs/concepts/accessibility.md +473 -0
  97. package/docs/concepts/color-spaces.md +222 -0
  98. package/docs/concepts/distance-metrics.md +384 -0
  99. package/docs/concepts/hct.md +261 -0
  100. package/docs/concepts/image-analysis.md +396 -0
  101. package/docs/concepts/material-design.md +306 -0
  102. package/docs/concepts/theme-matching.md +399 -0
  103. package/docs/examples/basic-colors.md +490 -0
  104. package/docs/examples/creating-themes.md +898 -0
  105. package/docs/examples/css-refactoring.md +824 -0
  106. package/docs/examples/image-extraction.md +882 -0
  107. package/docs/getting-started.md +366 -0
  108. package/docs/index.md +190 -0
  109. package/docs/installation.md +157 -0
  110. package/docs/tools/README.md +234 -0
  111. package/docs/tools/accessibility.md +614 -0
  112. package/docs/tools/color-operations.md +374 -0
  113. package/docs/tools/image-extraction.md +624 -0
  114. package/docs/tools/material-design.md +347 -0
  115. package/docs/tools/theme-matching.md +552 -0
  116. package/eslint.config.ts +14 -0
  117. package/examples/theme-matching.md +113 -0
  118. package/jsr.json +7 -0
  119. package/mcp-config.json +8 -0
  120. package/note.md +35 -0
  121. package/package.json +122 -0
  122. package/research_results.md +53 -0
  123. package/src/bin/coolors-mcp.ts +194 -0
  124. package/src/bin/server.ts +61 -0
  125. package/src/color/__tests__/conversions-argb.test.ts +198 -0
  126. package/src/color/__tests__/extract-colors.test.ts +360 -0
  127. package/src/color/__tests__/image-utils.test.ts +242 -0
  128. package/src/color/__tests__/reference-colors.test.ts +278 -0
  129. package/src/color/__tests__/round-trip.test.ts +197 -0
  130. package/src/color/conversions.test.ts +402 -0
  131. package/src/color/conversions.ts +393 -0
  132. package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
  133. package/src/color/dislike/dislike-analyzer.ts +114 -0
  134. package/src/color/extract-colors.ts +228 -0
  135. package/src/color/hct/__tests__/hct-class.test.ts +232 -0
  136. package/src/color/hct/harmonization.ts +204 -0
  137. package/src/color/hct/hct-class.ts +109 -0
  138. package/src/color/hct/hct-solver.ts +168 -0
  139. package/src/color/hct/index.ts +39 -0
  140. package/src/color/hct/tonal-palette.ts +211 -0
  141. package/src/color/hct/types.ts +88 -0
  142. package/src/color/image-utils.ts +79 -0
  143. package/src/color/index.ts +87 -0
  144. package/src/color/material-theme.ts +157 -0
  145. package/src/color/metrics.test.ts +276 -0
  146. package/src/color/metrics.ts +281 -0
  147. package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
  148. package/src/color/quantize/lab_point_provider.ts +55 -0
  149. package/src/color/quantize/point_provider.ts +27 -0
  150. package/src/color/quantize/quantizer_celebi.ts +51 -0
  151. package/src/color/quantize/quantizer_celebi_test.ts +71 -0
  152. package/src/color/quantize/quantizer_map.ts +47 -0
  153. package/src/color/quantize/quantizer_wsmeans.ts +232 -0
  154. package/src/color/quantize/quantizer_wu.ts +472 -0
  155. package/src/color/score/__tests__/score.test.ts +224 -0
  156. package/src/color/score/score.ts +175 -0
  157. package/src/color/types.ts +151 -0
  158. package/src/color/utils/color_utils.ts +292 -0
  159. package/src/color/utils/math_utils.ts +145 -0
  160. package/src/color/utils.test.ts +403 -0
  161. package/src/color/utils.ts +315 -0
  162. package/src/constants.ts +5 -0
  163. package/src/coolors-mcp.ts +37 -0
  164. package/src/examples/addition.ts +333 -0
  165. package/src/examples/color-demo.ts +125 -0
  166. package/src/examples/custom-logger.ts +201 -0
  167. package/src/examples/oauth-server.ts +113 -0
  168. package/src/examples/session-context.ts +269 -0
  169. package/src/session.ts +116 -0
  170. package/src/theme/__tests__/matcher.test.ts +180 -0
  171. package/src/theme/__tests__/parser.test.ts +148 -0
  172. package/src/theme/__tests__/refactor.test.ts +224 -0
  173. package/src/theme/index.ts +34 -0
  174. package/src/theme/matcher.ts +395 -0
  175. package/src/theme/parser.ts +392 -0
  176. package/src/theme/refactor.ts +360 -0
  177. package/src/theme/types.ts +152 -0
  178. package/src/tools/__tests__/gradient-generator.test.ts +206 -0
  179. package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
  180. package/src/tools/color-conversion.tool.ts +54 -0
  181. package/src/tools/color-distance.tool.ts +41 -0
  182. package/src/tools/colors.ts +31 -0
  183. package/src/tools/contrast-checker.tool.ts +37 -0
  184. package/src/tools/dislike-analyzer.tool.ts +247 -0
  185. package/src/tools/gradient-generator.tool.ts +250 -0
  186. package/src/tools/image-extraction.tools.ts +289 -0
  187. package/src/tools/index.ts +39 -0
  188. package/src/tools/material-theme.tools.ts +250 -0
  189. package/src/tools/palette-generator.tool.ts +135 -0
  190. package/src/tools/palette-with-locks.tool.ts +221 -0
  191. package/src/tools/registry.ts +142 -0
  192. package/src/tools/simple-tools.ts +37 -0
  193. package/src/tools/theme-matching.tools.ts +334 -0
  194. package/src/types.ts +182 -0
  195. package/src/utils.ts +22 -0
  196. package/tsconfig.json +8 -0
  197. package/vitest.config.js +15 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Tests for Celebi quantizer
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import * as utils from "../../utils/color_utils";
8
+ import { QuantizerCelebi } from "../quantizer_celebi";
9
+
10
+ describe("QuantizerCelebi", () => {
11
+ it("should quantize a single color", () => {
12
+ const pixels = [
13
+ utils.argbFromRgb(255, 0, 0),
14
+ utils.argbFromRgb(255, 0, 0),
15
+ utils.argbFromRgb(255, 0, 0),
16
+ ];
17
+
18
+ const result = QuantizerCelebi.quantize(pixels, 1);
19
+
20
+ expect(result.size).toBe(1);
21
+ // Should have red color
22
+ const colors = Array.from(result.keys());
23
+ const rgb = {
24
+ b: utils.blueFromArgb(colors[0]),
25
+ g: utils.greenFromArgb(colors[0]),
26
+ r: utils.redFromArgb(colors[0]),
27
+ };
28
+
29
+ // Should be close to red
30
+ expect(rgb.r).toBeGreaterThan(250);
31
+ expect(rgb.g).toBeLessThan(5);
32
+ expect(rgb.b).toBeLessThan(5);
33
+
34
+ // Population should be 3
35
+ expect(result.get(colors[0])).toBe(3);
36
+ });
37
+
38
+ it("should quantize multiple distinct colors", () => {
39
+ const pixels = [
40
+ utils.argbFromRgb(255, 0, 0), // Red
41
+ utils.argbFromRgb(0, 255, 0), // Green
42
+ utils.argbFromRgb(0, 0, 255), // Blue
43
+ utils.argbFromRgb(255, 255, 0), // Yellow
44
+ ];
45
+
46
+ const result = QuantizerCelebi.quantize(pixels, 4);
47
+
48
+ // Should preserve distinct colors when maxColors is sufficient
49
+ expect(result.size).toBeLessThanOrEqual(4);
50
+ expect(result.size).toBeGreaterThan(0);
51
+
52
+ // Check total population
53
+ let totalPopulation = 0;
54
+ for (const pop of result.values()) {
55
+ totalPopulation += pop;
56
+ }
57
+ expect(totalPopulation).toBe(4);
58
+ });
59
+
60
+ it("should reduce colors when maxColors is limited", () => {
61
+ const pixels = [
62
+ utils.argbFromRgb(255, 0, 0),
63
+ utils.argbFromRgb(254, 0, 0), // Very similar to red
64
+ utils.argbFromRgb(0, 255, 0),
65
+ utils.argbFromRgb(0, 254, 0), // Very similar to green
66
+ utils.argbFromRgb(0, 0, 255),
67
+ utils.argbFromRgb(0, 0, 254), // Very similar to blue
68
+ ];
69
+
70
+ const result = QuantizerCelebi.quantize(pixels, 3);
71
+
72
+ // Should reduce to approximately 3 colors
73
+ expect(result.size).toBeLessThanOrEqual(3);
74
+ expect(result.size).toBeGreaterThan(0);
75
+ });
76
+
77
+ it("should handle grayscale colors", () => {
78
+ const pixels = [
79
+ utils.argbFromRgb(0, 0, 0), // Black
80
+ utils.argbFromRgb(128, 128, 128), // Gray
81
+ utils.argbFromRgb(255, 255, 255), // White
82
+ ];
83
+
84
+ const result = QuantizerCelebi.quantize(pixels, 3);
85
+
86
+ expect(result.size).toBeLessThanOrEqual(3);
87
+ expect(result.size).toBeGreaterThan(0);
88
+
89
+ // Total population should be preserved
90
+ let totalPopulation = 0;
91
+ for (const pop of result.values()) {
92
+ totalPopulation += pop;
93
+ }
94
+ expect(totalPopulation).toBe(3);
95
+ });
96
+
97
+ it("should handle empty pixel array", () => {
98
+ const result = QuantizerCelebi.quantize([], 10);
99
+
100
+ expect(result.size).toBe(0);
101
+ });
102
+
103
+ it("should handle single pixel", () => {
104
+ const pixel = utils.argbFromRgb(123, 45, 67);
105
+ const result = QuantizerCelebi.quantize([pixel], 10);
106
+
107
+ expect(result.size).toBe(1);
108
+ expect(result.has(pixel)).toBe(true);
109
+ expect(result.get(pixel)).toBe(1);
110
+ });
111
+
112
+ it("should handle maxColors of 1", () => {
113
+ const pixels = [
114
+ utils.argbFromRgb(255, 0, 0),
115
+ utils.argbFromRgb(0, 255, 0),
116
+ utils.argbFromRgb(0, 0, 255),
117
+ ];
118
+
119
+ const result = QuantizerCelebi.quantize(pixels, 1);
120
+
121
+ expect(result.size).toBe(1);
122
+
123
+ // Population should be total
124
+ const population = Array.from(result.values())[0];
125
+ expect(population).toBe(3);
126
+ });
127
+
128
+ it("should handle large number of similar colors", () => {
129
+ const pixels: number[] = [];
130
+
131
+ // Add many similar shades of blue
132
+ for (let i = 0; i < 100; i++) {
133
+ const variation = Math.floor(i / 10);
134
+ pixels.push(utils.argbFromRgb(0, 0, 200 + variation));
135
+ }
136
+
137
+ const result = QuantizerCelebi.quantize(pixels, 5);
138
+
139
+ // Should reduce to fewer colors
140
+ expect(result.size).toBeLessThanOrEqual(5);
141
+ expect(result.size).toBeGreaterThan(0);
142
+
143
+ // Total population should be preserved
144
+ let totalPopulation = 0;
145
+ for (const pop of result.values()) {
146
+ totalPopulation += pop;
147
+ }
148
+ expect(totalPopulation).toBe(100);
149
+ });
150
+
151
+ it("should produce consistent results for same input", () => {
152
+ const pixels = [
153
+ utils.argbFromRgb(100, 150, 200),
154
+ utils.argbFromRgb(150, 100, 200),
155
+ utils.argbFromRgb(200, 100, 150),
156
+ ];
157
+
158
+ const result1 = QuantizerCelebi.quantize(pixels, 2);
159
+ const result2 = QuantizerCelebi.quantize(pixels, 2);
160
+
161
+ // Results should be deterministic
162
+ expect(result1.size).toBe(result2.size);
163
+
164
+ // Check that the same colors are produced
165
+ const colors1 = Array.from(result1.keys()).sort();
166
+ const colors2 = Array.from(result2.keys()).sort();
167
+
168
+ expect(colors1).toEqual(colors2);
169
+ });
170
+
171
+ it("should handle gradient properly", () => {
172
+ const pixels: number[] = [];
173
+
174
+ // Create a gradient from red to blue
175
+ for (let i = 0; i < 100; i++) {
176
+ const r = Math.floor(255 * (1 - i / 100));
177
+ const b = Math.floor(255 * (i / 100));
178
+ pixels.push(utils.argbFromRgb(r, 0, b));
179
+ }
180
+
181
+ const result = QuantizerCelebi.quantize(pixels, 10);
182
+
183
+ // Should extract representative colors from gradient
184
+ expect(result.size).toBeLessThanOrEqual(10);
185
+ expect(result.size).toBeGreaterThan(1); // Should find multiple colors
186
+
187
+ // Check that we have both reddish and bluish colors
188
+ const colors = Array.from(result.keys());
189
+ const hasReddish = colors.some((c) => utils.redFromArgb(c) > 200);
190
+ const hasBluish = colors.some((c) => utils.blueFromArgb(c) > 200);
191
+
192
+ expect(hasReddish).toBe(true);
193
+ expect(hasBluish).toBe(true);
194
+ });
195
+ });
@@ -0,0 +1,55 @@
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
+ import * as utils from "../utils/color_utils.js";
19
+ import { PointProvider } from "./point_provider.js";
20
+
21
+ /**
22
+ * Provides conversions needed for K-Means quantization. Converting input to
23
+ * points, and converting the final state of the K-Means algorithm to colors.
24
+ */
25
+ export class LabPointProvider implements PointProvider {
26
+ /**
27
+ * Standard CIE 1976 delta E formula also takes the square root, unneeded
28
+ * here. This method is used by quantization algorithms to compare distance,
29
+ * and the relative ordering is the same, with or without a square root.
30
+ *
31
+ * This relatively minor optimization is helpful because this method is
32
+ * called at least once for each pixel in an image.
33
+ */
34
+ distance(from: number[], to: number[]): number {
35
+ const dL = from[0] - to[0];
36
+ const dA = from[1] - to[1];
37
+ const dB = from[2] - to[2];
38
+ return dL * dL + dA * dA + dB * dB;
39
+ }
40
+
41
+ /**
42
+ * Convert a color represented in ARGB to a 3-element array of L*a*b*
43
+ * coordinates of the color.
44
+ */
45
+ fromInt(argb: number): number[] {
46
+ return utils.labFromArgb(argb);
47
+ }
48
+
49
+ /**
50
+ * Convert a 3-element array to a color represented in ARGB.
51
+ */
52
+ toInt(point: number[]): number {
53
+ return utils.argbFromLab(point[0], point[1], point[2]);
54
+ }
55
+ }
@@ -0,0 +1,27 @@
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
+ /**
19
+ * An interface to allow use of different color spaces by
20
+ * quantizers.
21
+ */
22
+
23
+ export declare interface PointProvider {
24
+ distance(from: number[], to: number[]): number;
25
+ fromInt(argb: number): number[];
26
+ toInt(point: number[]): number;
27
+ }
@@ -0,0 +1,51 @@
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
+ import { QuantizerWsmeans } from "./quantizer_wsmeans.js";
19
+ import { QuantizerWu } from "./quantizer_wu.js";
20
+
21
+ /**
22
+ * An image quantizer that improves on the quality of a standard K-Means
23
+ * algorithm by setting the K-Means initial state to the output of a Wu
24
+ * quantizer, instead of random centroids. Improves on speed by several
25
+ * optimizations, as implemented in Wsmeans, or Weighted Square Means, K-Means
26
+ * with those optimizations.
27
+ *
28
+ * This algorithm was designed by M. Emre Celebi, and was found in their 2011
29
+ * paper, Improving the Performance of K-Means for Color Quantization.
30
+ * https://arxiv.org/abs/1101.0395
31
+ */
32
+ // material_color_utilities is designed to have a consistent API across
33
+ // platforms and modular components that can be moved around easily. Using a
34
+ // class as a namespace facilitates this.
35
+ //
36
+ // tslint:disable-next-line:class-as-namespace
37
+ export class QuantizerCelebi {
38
+ /**
39
+ * @param pixels Colors in ARGB format.
40
+ * @param maxColors The number of colors to divide the image into. A lower
41
+ * number of colors may be returned.
42
+ * @return Map with keys of colors in ARGB format, and values of number of
43
+ * pixels in the original image that correspond to the color in the
44
+ * quantized image.
45
+ */
46
+ static quantize(pixels: number[], maxColors: number): Map<number, number> {
47
+ const wu = new QuantizerWu();
48
+ const wuResult = wu.quantize(pixels, maxColors);
49
+ return QuantizerWsmeans.quantize(pixels, wuResult, maxColors);
50
+ }
51
+ }
@@ -0,0 +1,71 @@
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
+ import "jasmine";
19
+
20
+ import { QuantizerCelebi } from "./quantizer_celebi.js";
21
+
22
+ const RED = 0xffff0000;
23
+ const GREEN = 0xff00ff00;
24
+ const BLUE = 0xff0000ff;
25
+
26
+ describe("QuantizerCelebi", () => {
27
+ it("1R", () => {
28
+ const answer = QuantizerCelebi.quantize([RED], 128);
29
+ expect(answer.size).toBe(1);
30
+ expect(answer.get(RED)).toBe(1);
31
+ });
32
+
33
+ it("1G", () => {
34
+ const answer = QuantizerCelebi.quantize([GREEN], 128);
35
+ expect(answer.size).toBe(1);
36
+ expect(answer.get(GREEN)).toBe(1);
37
+ });
38
+
39
+ it("1B", () => {
40
+ const answer = QuantizerCelebi.quantize([BLUE], 128);
41
+ expect(answer.size).toBe(1);
42
+ expect(answer.get(BLUE)).toBe(1);
43
+ });
44
+
45
+ it("5B", () => {
46
+ const answer = QuantizerCelebi.quantize(
47
+ [BLUE, BLUE, BLUE, BLUE, BLUE],
48
+ 128,
49
+ );
50
+ expect(answer.size).toBe(1);
51
+ expect(answer.get(BLUE)).toBe(5);
52
+ });
53
+
54
+ it("2R 3G", () => {
55
+ const answer = QuantizerCelebi.quantize(
56
+ [RED, RED, GREEN, GREEN, GREEN],
57
+ 128,
58
+ );
59
+ expect(answer.size).toBe(2);
60
+ expect(answer.get(RED)).toBe(2);
61
+ expect(answer.get(GREEN)).toBe(3);
62
+ });
63
+
64
+ it("1R 1G 1B", () => {
65
+ const answer = QuantizerCelebi.quantize([RED, GREEN, BLUE], 128);
66
+ expect(answer.size).toBe(3);
67
+ expect(answer.get(RED)).toBe(1);
68
+ expect(answer.get(GREEN)).toBe(1);
69
+ expect(answer.get(BLUE)).toBe(1);
70
+ });
71
+ });
@@ -0,0 +1,47 @@
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
+ import * as utils from "../utils/color_utils.js";
19
+
20
+ /**
21
+ * Quantizes an image into a map, with keys of ARGB colors, and values of the
22
+ * number of times that color appears in the image.
23
+ */
24
+ // material_color_utilities is designed to have a consistent API across
25
+ // platforms and modular components that can be moved around easily. Using a
26
+ // class as a namespace facilitates this.
27
+ //
28
+ // tslint:disable-next-line:class-as-namespace
29
+ export class QuantizerMap {
30
+ /**
31
+ * @param pixels Colors in ARGB format.
32
+ * @return A Map with keys of ARGB colors, and values of the number of times
33
+ * the color appears in the image.
34
+ */
35
+ static quantize(pixels: number[]): Map<number, number> {
36
+ const countByColor = new Map<number, number>();
37
+ for (let i = 0; i < pixels.length; i++) {
38
+ const pixel = pixels[i];
39
+ const alpha = utils.alphaFromArgb(pixel);
40
+ if (alpha < 255) {
41
+ continue;
42
+ }
43
+ countByColor.set(pixel, (countByColor.get(pixel) ?? 0) + 1);
44
+ }
45
+ return countByColor;
46
+ }
47
+ }
@@ -0,0 +1,232 @@
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
+ import { LabPointProvider } from "./lab_point_provider.js";
19
+
20
+ const MAX_ITERATIONS = 10;
21
+ const MIN_MOVEMENT_DISTANCE = 3.0;
22
+
23
+ /**
24
+ * A wrapper for maintaining a table of distances between K-Means clusters.
25
+ */
26
+ class DistanceAndIndex {
27
+ distance: number = -1;
28
+ index: number = -1;
29
+ }
30
+
31
+ /**
32
+ * An image quantizer that improves on the speed of a standard K-Means algorithm
33
+ * by implementing several optimizations, including deduping identical pixels
34
+ * and a triangle inequality rule that reduces the number of comparisons needed
35
+ * to identify which cluster a point should be moved to.
36
+ *
37
+ * Wsmeans stands for Weighted Square Means.
38
+ *
39
+ * This algorithm was designed by M. Emre Celebi, and was found in their 2011
40
+ * paper, Improving the Performance of K-Means for Color Quantization.
41
+ * https://arxiv.org/abs/1101.0395
42
+ */
43
+ // material_color_utilities is designed to have a consistent API across
44
+ // platforms and modular components that can be moved around easily. Using a
45
+ // class as a namespace facilitates this.
46
+ //
47
+ // tslint:disable-next-line:class-as-namespace
48
+ export class QuantizerWsmeans {
49
+ /**
50
+ * @param inputPixels Colors in ARGB format.
51
+ * @param startingClusters Defines the initial state of the quantizer. Passing
52
+ * an empty array is fine, the implementation will create its own initial
53
+ * state that leads to reproducible results for the same inputs.
54
+ * Passing an array that is the result of Wu quantization leads to higher
55
+ * quality results.
56
+ * @param maxColors The number of colors to divide the image into. A lower
57
+ * number of colors may be returned.
58
+ * @return Colors in ARGB format.
59
+ */
60
+ static quantize(
61
+ inputPixels: number[],
62
+ startingClusters: number[],
63
+ maxColors: number,
64
+ ): Map<number, number> {
65
+ const pixelToCount = new Map<number, number>();
66
+ const points = new Array<number[]>();
67
+ const pixels = new Array<number>();
68
+ const pointProvider = new LabPointProvider();
69
+ let pointCount = 0;
70
+ for (let i = 0; i < inputPixels.length; i++) {
71
+ const inputPixel = inputPixels[i];
72
+ const pixelCount = pixelToCount.get(inputPixel);
73
+ if (pixelCount === undefined) {
74
+ pointCount++;
75
+ points.push(pointProvider.fromInt(inputPixel));
76
+ pixels.push(inputPixel);
77
+ pixelToCount.set(inputPixel, 1);
78
+ } else {
79
+ pixelToCount.set(inputPixel, pixelCount + 1);
80
+ }
81
+ }
82
+
83
+ const counts = new Array<number>();
84
+ for (let i = 0; i < pointCount; i++) {
85
+ const pixel = pixels[i];
86
+ const count = pixelToCount.get(pixel);
87
+ if (count !== undefined) {
88
+ counts[i] = count;
89
+ }
90
+ }
91
+
92
+ let clusterCount = Math.min(maxColors, pointCount);
93
+ if (startingClusters.length > 0) {
94
+ clusterCount = Math.min(clusterCount, startingClusters.length);
95
+ }
96
+
97
+ const clusters = new Array<number[]>();
98
+ for (let i = 0; i < startingClusters.length; i++) {
99
+ clusters.push(pointProvider.fromInt(startingClusters[i]));
100
+ }
101
+ const additionalClustersNeeded = clusterCount - clusters.length;
102
+ if (startingClusters.length === 0 && additionalClustersNeeded > 0) {
103
+ for (let i = 0; i < additionalClustersNeeded; i++) {
104
+ const l = Math.random() * 100.0;
105
+ const a = Math.random() * (100.0 - -100.0 + 1) + -100;
106
+ const b = Math.random() * (100.0 - -100.0 + 1) + -100;
107
+
108
+ clusters.push([l, a, b]);
109
+ }
110
+ }
111
+
112
+ const clusterIndices = new Array<number>();
113
+ for (let i = 0; i < pointCount; i++) {
114
+ clusterIndices.push(Math.floor(Math.random() * clusterCount));
115
+ }
116
+
117
+ const indexMatrix = new Array<number[]>();
118
+ for (let i = 0; i < clusterCount; i++) {
119
+ indexMatrix.push(new Array<number>());
120
+ for (let j = 0; j < clusterCount; j++) {
121
+ indexMatrix[i].push(0);
122
+ }
123
+ }
124
+
125
+ const distanceToIndexMatrix = new Array<DistanceAndIndex[]>();
126
+ for (let i = 0; i < clusterCount; i++) {
127
+ distanceToIndexMatrix.push(new Array<DistanceAndIndex>());
128
+ for (let j = 0; j < clusterCount; j++) {
129
+ distanceToIndexMatrix[i].push(new DistanceAndIndex());
130
+ }
131
+ }
132
+
133
+ const pixelCountSums = new Array<number>();
134
+ for (let i = 0; i < clusterCount; i++) {
135
+ pixelCountSums.push(0);
136
+ }
137
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
138
+ for (let i = 0; i < clusterCount; i++) {
139
+ for (let j = i + 1; j < clusterCount; j++) {
140
+ const distance = pointProvider.distance(clusters[i], clusters[j]);
141
+ distanceToIndexMatrix[j][i].distance = distance;
142
+ distanceToIndexMatrix[j][i].index = i;
143
+ distanceToIndexMatrix[i][j].distance = distance;
144
+ distanceToIndexMatrix[i][j].index = j;
145
+ }
146
+ distanceToIndexMatrix[i].sort();
147
+ for (let j = 0; j < clusterCount; j++) {
148
+ indexMatrix[i][j] = distanceToIndexMatrix[i][j].index;
149
+ }
150
+ }
151
+
152
+ let pointsMoved = 0;
153
+ for (let i = 0; i < pointCount; i++) {
154
+ const point = points[i];
155
+ const previousClusterIndex = clusterIndices[i];
156
+ const previousCluster = clusters[previousClusterIndex];
157
+ const previousDistance = pointProvider.distance(point, previousCluster);
158
+ let minimumDistance = previousDistance;
159
+ let newClusterIndex = -1;
160
+ for (let j = 0; j < clusterCount; j++) {
161
+ if (
162
+ distanceToIndexMatrix[previousClusterIndex][j].distance >=
163
+ 4 * previousDistance
164
+ ) {
165
+ continue;
166
+ }
167
+ const distance = pointProvider.distance(point, clusters[j]);
168
+ if (distance < minimumDistance) {
169
+ minimumDistance = distance;
170
+ newClusterIndex = j;
171
+ }
172
+ }
173
+ if (newClusterIndex !== -1) {
174
+ const distanceChange = Math.abs(
175
+ Math.sqrt(minimumDistance) - Math.sqrt(previousDistance),
176
+ );
177
+ if (distanceChange > MIN_MOVEMENT_DISTANCE) {
178
+ pointsMoved++;
179
+ clusterIndices[i] = newClusterIndex;
180
+ }
181
+ }
182
+ }
183
+
184
+ if (pointsMoved === 0 && iteration !== 0) {
185
+ break;
186
+ }
187
+
188
+ const componentASums = new Array<number>(clusterCount).fill(0);
189
+ const componentBSums = new Array<number>(clusterCount).fill(0);
190
+ const componentCSums = new Array<number>(clusterCount).fill(0);
191
+
192
+ for (let i = 0; i < clusterCount; i++) {
193
+ pixelCountSums[i] = 0;
194
+ }
195
+ for (let i = 0; i < pointCount; i++) {
196
+ const clusterIndex = clusterIndices[i];
197
+ const point = points[i];
198
+ const count = counts[i];
199
+ pixelCountSums[clusterIndex] += count;
200
+ componentASums[clusterIndex] += point[0] * count;
201
+ componentBSums[clusterIndex] += point[1] * count;
202
+ componentCSums[clusterIndex] += point[2] * count;
203
+ }
204
+
205
+ for (let i = 0; i < clusterCount; i++) {
206
+ const count = pixelCountSums[i];
207
+ if (count === 0) {
208
+ clusters[i] = [0.0, 0.0, 0.0];
209
+ continue;
210
+ }
211
+ const a = componentASums[i] / count;
212
+ const b = componentBSums[i] / count;
213
+ const c = componentCSums[i] / count;
214
+ clusters[i] = [a, b, c];
215
+ }
216
+ }
217
+
218
+ const argbToPopulation = new Map<number, number>();
219
+ for (let i = 0; i < clusterCount; i++) {
220
+ const count = pixelCountSums[i];
221
+ if (count === 0) {
222
+ continue;
223
+ }
224
+
225
+ const possibleNewCluster = pointProvider.toInt(clusters[i]);
226
+ // Accumulate population counts when multiple clusters converge to same color
227
+ const existingCount = argbToPopulation.get(possibleNewCluster) || 0;
228
+ argbToPopulation.set(possibleNewCluster, existingCount + count);
229
+ }
230
+ return argbToPopulation;
231
+ }
232
+ }