@oxyhq/bloom 0.36.0 → 0.37.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 (235) hide show
  1. package/lib/commonjs/theme/build-theme.js +31 -43
  2. package/lib/commonjs/theme/build-theme.js.map +1 -1
  3. package/lib/commonjs/theme/color-engine/cam16.js +161 -0
  4. package/lib/commonjs/theme/color-engine/cam16.js.map +1 -0
  5. package/lib/commonjs/theme/color-engine/color-role.js +183 -0
  6. package/lib/commonjs/theme/color-engine/color-role.js.map +1 -0
  7. package/lib/commonjs/theme/color-engine/color-roles.js +416 -0
  8. package/lib/commonjs/theme/color-engine/color-roles.js.map +1 -0
  9. package/lib/commonjs/theme/color-engine/color-utils.js +157 -0
  10. package/lib/commonjs/theme/color-engine/color-utils.js.map +1 -0
  11. package/lib/commonjs/theme/color-engine/contrast-curve.js +32 -0
  12. package/lib/commonjs/theme/color-engine/contrast-curve.js.map +1 -0
  13. package/lib/commonjs/theme/color-engine/contrast.js +70 -0
  14. package/lib/commonjs/theme/color-engine/contrast.js.map +1 -0
  15. package/lib/commonjs/theme/color-engine/dislike-analyzer.js +26 -0
  16. package/lib/commonjs/theme/color-engine/dislike-analyzer.js.map +1 -0
  17. package/lib/commonjs/theme/color-engine/dynamic-scheme.js +70 -0
  18. package/lib/commonjs/theme/color-engine/dynamic-scheme.js.map +1 -0
  19. package/lib/commonjs/theme/color-engine/hct-solver.js +236 -0
  20. package/lib/commonjs/theme/color-engine/hct-solver.js.map +1 -0
  21. package/lib/commonjs/theme/color-engine/hct.js +67 -0
  22. package/lib/commonjs/theme/color-engine/hct.js.map +1 -0
  23. package/lib/commonjs/theme/color-engine/index.js +173 -0
  24. package/lib/commonjs/theme/color-engine/index.js.map +1 -0
  25. package/lib/commonjs/theme/color-engine/lab-point-provider.js +32 -0
  26. package/lib/commonjs/theme/color-engine/lab-point-provider.js.map +1 -0
  27. package/lib/commonjs/theme/color-engine/math-utils.js +77 -0
  28. package/lib/commonjs/theme/color-engine/math-utils.js.map +1 -0
  29. package/lib/commonjs/theme/color-engine/quantizer-map.js +24 -0
  30. package/lib/commonjs/theme/color-engine/quantizer-map.js.map +1 -0
  31. package/lib/commonjs/theme/color-engine/quantizer-wu.js +266 -0
  32. package/lib/commonjs/theme/color-engine/quantizer-wu.js.map +1 -0
  33. package/lib/commonjs/theme/color-engine/scheme-variants.js +102 -0
  34. package/lib/commonjs/theme/color-engine/scheme-variants.js.map +1 -0
  35. package/lib/commonjs/theme/color-engine/score.js +80 -0
  36. package/lib/commonjs/theme/color-engine/score.js.map +1 -0
  37. package/lib/commonjs/theme/color-engine/seed-from-image.js +73 -0
  38. package/lib/commonjs/theme/color-engine/seed-from-image.js.map +1 -0
  39. package/lib/commonjs/theme/color-engine/tonal-palette.js +95 -0
  40. package/lib/commonjs/theme/color-engine/tonal-palette.js.map +1 -0
  41. package/lib/commonjs/theme/color-engine/tone-delta-pair.js +26 -0
  42. package/lib/commonjs/theme/color-engine/tone-delta-pair.js.map +1 -0
  43. package/lib/commonjs/theme/color-engine/variant.js +26 -0
  44. package/lib/commonjs/theme/color-engine/variant.js.map +1 -0
  45. package/lib/commonjs/theme/color-engine/viewing-conditions.js +61 -0
  46. package/lib/commonjs/theme/color-engine/viewing-conditions.js.map +1 -0
  47. package/lib/commonjs/theme/color-presets.js +44 -572
  48. package/lib/commonjs/theme/color-presets.js.map +1 -1
  49. package/lib/commonjs/theme/preset-vars.js +64 -47
  50. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  51. package/lib/module/theme/build-theme.js +32 -44
  52. package/lib/module/theme/build-theme.js.map +1 -1
  53. package/lib/module/theme/color-engine/cam16.js +155 -0
  54. package/lib/module/theme/color-engine/cam16.js.map +1 -0
  55. package/lib/module/theme/color-engine/color-role.js +177 -0
  56. package/lib/module/theme/color-engine/color-role.js.map +1 -0
  57. package/lib/module/theme/color-engine/color-roles.js +411 -0
  58. package/lib/module/theme/color-engine/color-roles.js.map +1 -0
  59. package/lib/module/theme/color-engine/color-utils.js +135 -0
  60. package/lib/module/theme/color-engine/color-utils.js.map +1 -0
  61. package/lib/module/theme/color-engine/contrast-curve.js +26 -0
  62. package/lib/module/theme/color-engine/contrast-curve.js.map +1 -0
  63. package/lib/module/theme/color-engine/contrast.js +61 -0
  64. package/lib/module/theme/color-engine/contrast.js.map +1 -0
  65. package/lib/module/theme/color-engine/dislike-analyzer.js +20 -0
  66. package/lib/module/theme/color-engine/dislike-analyzer.js.map +1 -0
  67. package/lib/module/theme/color-engine/dynamic-scheme.js +65 -0
  68. package/lib/module/theme/color-engine/dynamic-scheme.js.map +1 -0
  69. package/lib/module/theme/color-engine/hct-solver.js +231 -0
  70. package/lib/module/theme/color-engine/hct-solver.js.map +1 -0
  71. package/lib/module/theme/color-engine/hct.js +61 -0
  72. package/lib/module/theme/color-engine/hct.js.map +1 -0
  73. package/lib/module/theme/color-engine/index.js +57 -0
  74. package/lib/module/theme/color-engine/index.js.map +1 -0
  75. package/lib/module/theme/color-engine/lab-point-provider.js +25 -0
  76. package/lib/module/theme/color-engine/lab-point-provider.js.map +1 -0
  77. package/lib/module/theme/color-engine/math-utils.js +65 -0
  78. package/lib/module/theme/color-engine/math-utils.js.map +1 -0
  79. package/lib/module/theme/color-engine/quantizer-map.js +20 -0
  80. package/lib/module/theme/color-engine/quantizer-map.js.map +1 -0
  81. package/lib/module/theme/color-engine/quantizer-wu.js +261 -0
  82. package/lib/module/theme/color-engine/quantizer-wu.js.map +1 -0
  83. package/lib/module/theme/color-engine/scheme-variants.js +93 -0
  84. package/lib/module/theme/color-engine/scheme-variants.js.map +1 -0
  85. package/lib/module/theme/color-engine/score.js +75 -0
  86. package/lib/module/theme/color-engine/score.js.map +1 -0
  87. package/lib/module/theme/color-engine/seed-from-image.js +67 -0
  88. package/lib/module/theme/color-engine/seed-from-image.js.map +1 -0
  89. package/lib/module/theme/color-engine/tonal-palette.js +89 -0
  90. package/lib/module/theme/color-engine/tonal-palette.js.map +1 -0
  91. package/lib/module/theme/color-engine/tone-delta-pair.js +21 -0
  92. package/lib/module/theme/color-engine/tone-delta-pair.js.map +1 -0
  93. package/lib/module/theme/color-engine/variant.js +22 -0
  94. package/lib/module/theme/color-engine/variant.js.map +1 -0
  95. package/lib/module/theme/color-engine/viewing-conditions.js +55 -0
  96. package/lib/module/theme/color-engine/viewing-conditions.js.map +1 -0
  97. package/lib/module/theme/color-presets.js +44 -572
  98. package/lib/module/theme/color-presets.js.map +1 -1
  99. package/lib/module/theme/preset-vars.js +64 -46
  100. package/lib/module/theme/preset-vars.js.map +1 -1
  101. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/theme/color-engine/cam16.d.ts +26 -0
  103. package/lib/typescript/commonjs/theme/color-engine/cam16.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/theme/color-engine/color-role.d.ts +36 -0
  105. package/lib/typescript/commonjs/theme/color-engine/color-role.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/theme/color-engine/color-roles.d.ts +58 -0
  107. package/lib/typescript/commonjs/theme/color-engine/color-roles.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/theme/color-engine/color-utils.d.ts +32 -0
  109. package/lib/typescript/commonjs/theme/color-engine/color-utils.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/theme/color-engine/contrast-curve.d.ts +9 -0
  111. package/lib/typescript/commonjs/theme/color-engine/contrast-curve.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/theme/color-engine/contrast.d.ts +12 -0
  113. package/lib/typescript/commonjs/theme/color-engine/contrast.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/theme/color-engine/dislike-analyzer.d.ts +11 -0
  115. package/lib/typescript/commonjs/theme/color-engine/dislike-analyzer.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/theme/color-engine/dynamic-scheme.d.ts +47 -0
  117. package/lib/typescript/commonjs/theme/color-engine/dynamic-scheme.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/theme/color-engine/hct-solver.d.ts +6 -0
  119. package/lib/typescript/commonjs/theme/color-engine/hct-solver.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/theme/color-engine/hct.d.ts +20 -0
  121. package/lib/typescript/commonjs/theme/color-engine/hct.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/theme/color-engine/index.d.ts +44 -0
  123. package/lib/typescript/commonjs/theme/color-engine/index.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/theme/color-engine/lab-point-provider.d.ts +6 -0
  125. package/lib/typescript/commonjs/theme/color-engine/lab-point-provider.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/theme/color-engine/math-utils.d.ts +46 -0
  127. package/lib/typescript/commonjs/theme/color-engine/math-utils.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/theme/color-engine/quantizer-map.d.ts +3 -0
  129. package/lib/typescript/commonjs/theme/color-engine/quantizer-map.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/theme/color-engine/quantizer-wu.d.ts +3 -0
  131. package/lib/typescript/commonjs/theme/color-engine/quantizer-wu.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts +23 -0
  133. package/lib/typescript/commonjs/theme/color-engine/scheme-variants.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/theme/color-engine/score.d.ts +8 -0
  135. package/lib/typescript/commonjs/theme/color-engine/score.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts +13 -0
  137. package/lib/typescript/commonjs/theme/color-engine/seed-from-image.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/theme/color-engine/tonal-palette.d.ts +24 -0
  139. package/lib/typescript/commonjs/theme/color-engine/tonal-palette.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/theme/color-engine/tone-delta-pair.d.ts +19 -0
  141. package/lib/typescript/commonjs/theme/color-engine/tone-delta-pair.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/theme/color-engine/variant.d.ts +19 -0
  143. package/lib/typescript/commonjs/theme/color-engine/variant.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/theme/color-engine/viewing-conditions.d.ts +18 -0
  145. package/lib/typescript/commonjs/theme/color-engine/viewing-conditions.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/theme/color-presets.d.ts +25 -20
  147. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/theme/preset-vars.d.ts +16 -12
  149. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  150. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  151. package/lib/typescript/module/theme/color-engine/cam16.d.ts +26 -0
  152. package/lib/typescript/module/theme/color-engine/cam16.d.ts.map +1 -0
  153. package/lib/typescript/module/theme/color-engine/color-role.d.ts +36 -0
  154. package/lib/typescript/module/theme/color-engine/color-role.d.ts.map +1 -0
  155. package/lib/typescript/module/theme/color-engine/color-roles.d.ts +58 -0
  156. package/lib/typescript/module/theme/color-engine/color-roles.d.ts.map +1 -0
  157. package/lib/typescript/module/theme/color-engine/color-utils.d.ts +32 -0
  158. package/lib/typescript/module/theme/color-engine/color-utils.d.ts.map +1 -0
  159. package/lib/typescript/module/theme/color-engine/contrast-curve.d.ts +9 -0
  160. package/lib/typescript/module/theme/color-engine/contrast-curve.d.ts.map +1 -0
  161. package/lib/typescript/module/theme/color-engine/contrast.d.ts +12 -0
  162. package/lib/typescript/module/theme/color-engine/contrast.d.ts.map +1 -0
  163. package/lib/typescript/module/theme/color-engine/dislike-analyzer.d.ts +11 -0
  164. package/lib/typescript/module/theme/color-engine/dislike-analyzer.d.ts.map +1 -0
  165. package/lib/typescript/module/theme/color-engine/dynamic-scheme.d.ts +47 -0
  166. package/lib/typescript/module/theme/color-engine/dynamic-scheme.d.ts.map +1 -0
  167. package/lib/typescript/module/theme/color-engine/hct-solver.d.ts +6 -0
  168. package/lib/typescript/module/theme/color-engine/hct-solver.d.ts.map +1 -0
  169. package/lib/typescript/module/theme/color-engine/hct.d.ts +20 -0
  170. package/lib/typescript/module/theme/color-engine/hct.d.ts.map +1 -0
  171. package/lib/typescript/module/theme/color-engine/index.d.ts +44 -0
  172. package/lib/typescript/module/theme/color-engine/index.d.ts.map +1 -0
  173. package/lib/typescript/module/theme/color-engine/lab-point-provider.d.ts +6 -0
  174. package/lib/typescript/module/theme/color-engine/lab-point-provider.d.ts.map +1 -0
  175. package/lib/typescript/module/theme/color-engine/math-utils.d.ts +46 -0
  176. package/lib/typescript/module/theme/color-engine/math-utils.d.ts.map +1 -0
  177. package/lib/typescript/module/theme/color-engine/quantizer-map.d.ts +3 -0
  178. package/lib/typescript/module/theme/color-engine/quantizer-map.d.ts.map +1 -0
  179. package/lib/typescript/module/theme/color-engine/quantizer-wu.d.ts +3 -0
  180. package/lib/typescript/module/theme/color-engine/quantizer-wu.d.ts.map +1 -0
  181. package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts +23 -0
  182. package/lib/typescript/module/theme/color-engine/scheme-variants.d.ts.map +1 -0
  183. package/lib/typescript/module/theme/color-engine/score.d.ts +8 -0
  184. package/lib/typescript/module/theme/color-engine/score.d.ts.map +1 -0
  185. package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts +13 -0
  186. package/lib/typescript/module/theme/color-engine/seed-from-image.d.ts.map +1 -0
  187. package/lib/typescript/module/theme/color-engine/tonal-palette.d.ts +24 -0
  188. package/lib/typescript/module/theme/color-engine/tonal-palette.d.ts.map +1 -0
  189. package/lib/typescript/module/theme/color-engine/tone-delta-pair.d.ts +19 -0
  190. package/lib/typescript/module/theme/color-engine/tone-delta-pair.d.ts.map +1 -0
  191. package/lib/typescript/module/theme/color-engine/variant.d.ts +19 -0
  192. package/lib/typescript/module/theme/color-engine/variant.d.ts.map +1 -0
  193. package/lib/typescript/module/theme/color-engine/viewing-conditions.d.ts +18 -0
  194. package/lib/typescript/module/theme/color-engine/viewing-conditions.d.ts.map +1 -0
  195. package/lib/typescript/module/theme/color-presets.d.ts +25 -20
  196. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  197. package/lib/typescript/module/theme/preset-vars.d.ts +16 -12
  198. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  199. package/package.json +1 -1
  200. package/src/__tests__/BloomThemeProvider.test.tsx +9 -4
  201. package/src/__tests__/native-root-vars.test.ts +8 -4
  202. package/src/__tests__/preset-vars.test.ts +66 -37
  203. package/src/__tests__/theme.test.ts +20 -12
  204. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +509 -509
  205. package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +613 -613
  206. package/src/theme/__tests__/color-engine-golden.test.ts +81 -0
  207. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  208. package/src/theme/__tests__/golden-tokens.test.ts +6 -7
  209. package/src/theme/__tests__/runtime-contract.test.ts +8 -8
  210. package/src/theme/__tests__/theme-colors-parity.test.ts +44 -88
  211. package/src/theme/build-theme.ts +32 -44
  212. package/src/theme/color-engine/cam16.ts +179 -0
  213. package/src/theme/color-engine/color-role.ts +217 -0
  214. package/src/theme/color-engine/color-roles.ts +435 -0
  215. package/src/theme/color-engine/color-utils.ts +157 -0
  216. package/src/theme/color-engine/contrast-curve.ts +25 -0
  217. package/src/theme/color-engine/contrast.ts +59 -0
  218. package/src/theme/color-engine/dislike-analyzer.ts +19 -0
  219. package/src/theme/color-engine/dynamic-scheme.ts +90 -0
  220. package/src/theme/color-engine/hct-solver.ts +263 -0
  221. package/src/theme/color-engine/hct.ts +69 -0
  222. package/src/theme/color-engine/index.ts +73 -0
  223. package/src/theme/color-engine/lab-point-provider.ts +26 -0
  224. package/src/theme/color-engine/math-utils.ts +69 -0
  225. package/src/theme/color-engine/quantizer-map.ts +17 -0
  226. package/src/theme/color-engine/quantizer-wu.ts +320 -0
  227. package/src/theme/color-engine/scheme-variants.ts +113 -0
  228. package/src/theme/color-engine/score.ts +82 -0
  229. package/src/theme/color-engine/seed-from-image.ts +64 -0
  230. package/src/theme/color-engine/tonal-palette.ts +94 -0
  231. package/src/theme/color-engine/tone-delta-pair.ts +21 -0
  232. package/src/theme/color-engine/variant.ts +18 -0
  233. package/src/theme/color-engine/viewing-conditions.ts +77 -0
  234. package/src/theme/color-presets.ts +54 -631
  235. package/src/theme/preset-vars.ts +65 -49
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Bloom colour engine — Wu colour quantizer.
3
+ *
4
+ * Faithful port of Material Color Utilities' `QuantizerWu` (Apache-2.0). Builds a
5
+ * 3-D histogram of the image in a 32³ RGB grid, computes cumulative moments, then
6
+ * greedily splits the colour cube along the axis of greatest variance until it
7
+ * has `maxColors` boxes — each box's mean colour is a representative. Fully
8
+ * DETERMINISTIC (no k-means/randomness), which is why Bloom uses it directly.
9
+ */
10
+ import { blueFromArgb, greenFromArgb, redFromArgb } from './color-utils';
11
+ import { countByColor } from './quantizer-map';
12
+
13
+ const INDEX_BITS = 5;
14
+ const SIDE_LENGTH = 33; // (1 << INDEX_BITS) + 1
15
+ const TOTAL_SIZE = 35937; // SIDE_LENGTH³
16
+
17
+ type Direction = 'red' | 'green' | 'blue';
18
+
19
+ interface Box {
20
+ r0: number;
21
+ r1: number;
22
+ g0: number;
23
+ g1: number;
24
+ b0: number;
25
+ b1: number;
26
+ vol: number;
27
+ }
28
+
29
+ const makeBox = (): Box => ({ r0: 0, r1: 0, g0: 0, g1: 0, b0: 0, b1: 0, vol: 0 });
30
+
31
+ /**
32
+ * Read a moment/weight cell. Every index here is produced by `getIndex` (or a
33
+ * bounded loop) and is always in range for the fixed-size grids, but under
34
+ * `noUncheckedIndexedAccess` the element type is `number | undefined`; this
35
+ * asserts the in-range invariant honestly instead of masking it.
36
+ */
37
+ function cell(grid: number[], index: number): number {
38
+ const value = grid[index];
39
+ if (value === undefined) throw new RangeError(`quantizer grid index ${index} out of range`);
40
+ return value;
41
+ }
42
+
43
+ /** Read a colour box by index (the box array is fully pre-populated). */
44
+ function boxAt(cubes: Box[], index: number): Box {
45
+ const box = cubes[index];
46
+ if (box === undefined) throw new RangeError(`quantizer box index ${index} out of range`);
47
+ return box;
48
+ }
49
+
50
+ function getIndex(r: number, g: number, b: number): number {
51
+ return (r << (INDEX_BITS * 2)) + (r << (INDEX_BITS + 1)) + r + (g << INDEX_BITS) + g + b;
52
+ }
53
+
54
+ /** Wu-quantize `pixels` into at most `maxColors` representative ARGB colours. */
55
+ export function quantizeWu(pixels: number[], maxColors: number): number[] {
56
+ const weights = new Array<number>(TOTAL_SIZE).fill(0);
57
+ const momentsR = new Array<number>(TOTAL_SIZE).fill(0);
58
+ const momentsG = new Array<number>(TOTAL_SIZE).fill(0);
59
+ const momentsB = new Array<number>(TOTAL_SIZE).fill(0);
60
+ const moments = new Array<number>(TOTAL_SIZE).fill(0);
61
+
62
+ // Histogram.
63
+ const counts = countByColor(pixels);
64
+ const bitsToRemove = 8 - INDEX_BITS;
65
+ for (const [pixel, count] of counts.entries()) {
66
+ const red = redFromArgb(pixel);
67
+ const green = greenFromArgb(pixel);
68
+ const blue = blueFromArgb(pixel);
69
+ const index = getIndex((red >> bitsToRemove) + 1, (green >> bitsToRemove) + 1, (blue >> bitsToRemove) + 1);
70
+ weights[index] = cell(weights, index) + count;
71
+ momentsR[index] = cell(momentsR, index) + count * red;
72
+ momentsG[index] = cell(momentsG, index) + count * green;
73
+ momentsB[index] = cell(momentsB, index) + count * blue;
74
+ moments[index] = cell(moments, index) + count * (red * red + green * green + blue * blue);
75
+ }
76
+
77
+ // Cumulative moments.
78
+ for (let r = 1; r < SIDE_LENGTH; r++) {
79
+ const area = new Array<number>(SIDE_LENGTH).fill(0);
80
+ const areaR = new Array<number>(SIDE_LENGTH).fill(0);
81
+ const areaG = new Array<number>(SIDE_LENGTH).fill(0);
82
+ const areaB = new Array<number>(SIDE_LENGTH).fill(0);
83
+ const area2 = new Array<number>(SIDE_LENGTH).fill(0.0);
84
+ for (let g = 1; g < SIDE_LENGTH; g++) {
85
+ let line = 0;
86
+ let lineR = 0;
87
+ let lineG = 0;
88
+ let lineB = 0;
89
+ let line2 = 0.0;
90
+ for (let b = 1; b < SIDE_LENGTH; b++) {
91
+ const index = getIndex(r, g, b);
92
+ line += cell(weights, index);
93
+ lineR += cell(momentsR, index);
94
+ lineG += cell(momentsG, index);
95
+ lineB += cell(momentsB, index);
96
+ line2 += cell(moments, index);
97
+ area[b] = cell(area, b) + line;
98
+ areaR[b] = cell(areaR, b) + lineR;
99
+ areaG[b] = cell(areaG, b) + lineG;
100
+ areaB[b] = cell(areaB, b) + lineB;
101
+ area2[b] = cell(area2, b) + line2;
102
+ const prev = getIndex(r - 1, g, b);
103
+ weights[index] = cell(weights, prev) + cell(area, b);
104
+ momentsR[index] = cell(momentsR, prev) + cell(areaR, b);
105
+ momentsG[index] = cell(momentsG, prev) + cell(areaG, b);
106
+ momentsB[index] = cell(momentsB, prev) + cell(areaB, b);
107
+ moments[index] = cell(moments, prev) + cell(area2, b);
108
+ }
109
+ }
110
+ }
111
+
112
+ const volume = (cube: Box, moment: number[]): number =>
113
+ cell(moment, getIndex(cube.r1, cube.g1, cube.b1)) -
114
+ cell(moment, getIndex(cube.r1, cube.g1, cube.b0)) -
115
+ cell(moment, getIndex(cube.r1, cube.g0, cube.b1)) +
116
+ cell(moment, getIndex(cube.r1, cube.g0, cube.b0)) -
117
+ cell(moment, getIndex(cube.r0, cube.g1, cube.b1)) +
118
+ cell(moment, getIndex(cube.r0, cube.g1, cube.b0)) +
119
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b1)) -
120
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b0));
121
+
122
+ const bottom = (cube: Box, direction: Direction, moment: number[]): number => {
123
+ switch (direction) {
124
+ case 'red':
125
+ return (
126
+ -cell(moment, getIndex(cube.r0, cube.g1, cube.b1)) +
127
+ cell(moment, getIndex(cube.r0, cube.g1, cube.b0)) +
128
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b1)) -
129
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b0))
130
+ );
131
+ case 'green':
132
+ return (
133
+ -cell(moment, getIndex(cube.r1, cube.g0, cube.b1)) +
134
+ cell(moment, getIndex(cube.r1, cube.g0, cube.b0)) +
135
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b1)) -
136
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b0))
137
+ );
138
+ case 'blue':
139
+ return (
140
+ -cell(moment, getIndex(cube.r1, cube.g1, cube.b0)) +
141
+ cell(moment, getIndex(cube.r1, cube.g0, cube.b0)) +
142
+ cell(moment, getIndex(cube.r0, cube.g1, cube.b0)) -
143
+ cell(moment, getIndex(cube.r0, cube.g0, cube.b0))
144
+ );
145
+ }
146
+ };
147
+
148
+ const top = (cube: Box, direction: Direction, position: number, moment: number[]): number => {
149
+ switch (direction) {
150
+ case 'red':
151
+ return (
152
+ cell(moment, getIndex(position, cube.g1, cube.b1)) -
153
+ cell(moment, getIndex(position, cube.g1, cube.b0)) -
154
+ cell(moment, getIndex(position, cube.g0, cube.b1)) +
155
+ cell(moment, getIndex(position, cube.g0, cube.b0))
156
+ );
157
+ case 'green':
158
+ return (
159
+ cell(moment, getIndex(cube.r1, position, cube.b1)) -
160
+ cell(moment, getIndex(cube.r1, position, cube.b0)) -
161
+ cell(moment, getIndex(cube.r0, position, cube.b1)) +
162
+ cell(moment, getIndex(cube.r0, position, cube.b0))
163
+ );
164
+ case 'blue':
165
+ return (
166
+ cell(moment, getIndex(cube.r1, cube.g1, position)) -
167
+ cell(moment, getIndex(cube.r1, cube.g0, position)) -
168
+ cell(moment, getIndex(cube.r0, cube.g1, position)) +
169
+ cell(moment, getIndex(cube.r0, cube.g0, position))
170
+ );
171
+ }
172
+ };
173
+
174
+ const variance = (cube: Box): number => {
175
+ const dr = volume(cube, momentsR);
176
+ const dg = volume(cube, momentsG);
177
+ const db = volume(cube, momentsB);
178
+ const xx =
179
+ cell(moments, getIndex(cube.r1, cube.g1, cube.b1)) -
180
+ cell(moments, getIndex(cube.r1, cube.g1, cube.b0)) -
181
+ cell(moments, getIndex(cube.r1, cube.g0, cube.b1)) +
182
+ cell(moments, getIndex(cube.r1, cube.g0, cube.b0)) -
183
+ cell(moments, getIndex(cube.r0, cube.g1, cube.b1)) +
184
+ cell(moments, getIndex(cube.r0, cube.g1, cube.b0)) +
185
+ cell(moments, getIndex(cube.r0, cube.g0, cube.b1)) -
186
+ cell(moments, getIndex(cube.r0, cube.g0, cube.b0));
187
+ const hypotenuse = dr * dr + dg * dg + db * db;
188
+ return xx - hypotenuse / volume(cube, weights);
189
+ };
190
+
191
+ const maximize = (
192
+ cube: Box,
193
+ direction: Direction,
194
+ first: number,
195
+ last: number,
196
+ wholeR: number,
197
+ wholeG: number,
198
+ wholeB: number,
199
+ wholeW: number,
200
+ ): { cutLocation: number; maximum: number } => {
201
+ const bottomR = bottom(cube, direction, momentsR);
202
+ const bottomG = bottom(cube, direction, momentsG);
203
+ const bottomB = bottom(cube, direction, momentsB);
204
+ const bottomW = bottom(cube, direction, weights);
205
+ let max = 0.0;
206
+ let cut = -1;
207
+ for (let i = first; i < last; i++) {
208
+ let halfR = bottomR + top(cube, direction, i, momentsR);
209
+ let halfG = bottomG + top(cube, direction, i, momentsG);
210
+ let halfB = bottomB + top(cube, direction, i, momentsB);
211
+ let halfW = bottomW + top(cube, direction, i, weights);
212
+ if (halfW === 0) continue;
213
+ let temp = (halfR * halfR + halfG * halfG + halfB * halfB) / halfW;
214
+ halfR = wholeR - halfR;
215
+ halfG = wholeG - halfG;
216
+ halfB = wholeB - halfB;
217
+ halfW = wholeW - halfW;
218
+ if (halfW === 0) continue;
219
+ temp += (halfR * halfR + halfG * halfG + halfB * halfB) / halfW;
220
+ if (temp > max) {
221
+ max = temp;
222
+ cut = i;
223
+ }
224
+ }
225
+ return { cutLocation: cut, maximum: max };
226
+ };
227
+
228
+ const cut = (one: Box, two: Box): boolean => {
229
+ const wholeR = volume(one, momentsR);
230
+ const wholeG = volume(one, momentsG);
231
+ const wholeB = volume(one, momentsB);
232
+ const wholeW = volume(one, weights);
233
+ const maxR = maximize(one, 'red', one.r0 + 1, one.r1, wholeR, wholeG, wholeB, wholeW);
234
+ const maxG = maximize(one, 'green', one.g0 + 1, one.g1, wholeR, wholeG, wholeB, wholeW);
235
+ const maxB = maximize(one, 'blue', one.b0 + 1, one.b1, wholeR, wholeG, wholeB, wholeW);
236
+ let direction: Direction;
237
+ if (maxR.maximum >= maxG.maximum && maxR.maximum >= maxB.maximum) {
238
+ if (maxR.cutLocation < 0) return false;
239
+ direction = 'red';
240
+ } else if (maxG.maximum >= maxR.maximum && maxG.maximum >= maxB.maximum) {
241
+ direction = 'green';
242
+ } else {
243
+ direction = 'blue';
244
+ }
245
+ two.r1 = one.r1;
246
+ two.g1 = one.g1;
247
+ two.b1 = one.b1;
248
+ switch (direction) {
249
+ case 'red':
250
+ one.r1 = maxR.cutLocation;
251
+ two.r0 = one.r1;
252
+ two.g0 = one.g0;
253
+ two.b0 = one.b0;
254
+ break;
255
+ case 'green':
256
+ one.g1 = maxG.cutLocation;
257
+ two.r0 = one.r0;
258
+ two.g0 = one.g1;
259
+ two.b0 = one.b0;
260
+ break;
261
+ case 'blue':
262
+ one.b1 = maxB.cutLocation;
263
+ two.r0 = one.r0;
264
+ two.g0 = one.g0;
265
+ two.b0 = one.b1;
266
+ break;
267
+ }
268
+ one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0);
269
+ two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0);
270
+ return true;
271
+ };
272
+
273
+ // Greedily split the colour cube.
274
+ const cubes: Box[] = Array.from({ length: maxColors }, makeBox);
275
+ const volumeVariance = new Array<number>(maxColors).fill(0.0);
276
+ const firstCube = boxAt(cubes, 0);
277
+ firstCube.r0 = 0;
278
+ firstCube.g0 = 0;
279
+ firstCube.b0 = 0;
280
+ firstCube.r1 = SIDE_LENGTH - 1;
281
+ firstCube.g1 = SIDE_LENGTH - 1;
282
+ firstCube.b1 = SIDE_LENGTH - 1;
283
+ let generatedColorCount = maxColors;
284
+ let next = 0;
285
+ for (let i = 1; i < maxColors; i++) {
286
+ if (cut(boxAt(cubes, next), boxAt(cubes, i))) {
287
+ volumeVariance[next] = boxAt(cubes, next).vol > 1 ? variance(boxAt(cubes, next)) : 0.0;
288
+ volumeVariance[i] = boxAt(cubes, i).vol > 1 ? variance(boxAt(cubes, i)) : 0.0;
289
+ } else {
290
+ volumeVariance[next] = 0.0;
291
+ i--;
292
+ }
293
+ next = 0;
294
+ let temp = cell(volumeVariance, 0);
295
+ for (let j = 1; j <= i; j++) {
296
+ if (cell(volumeVariance, j) > temp) {
297
+ temp = cell(volumeVariance, j);
298
+ next = j;
299
+ }
300
+ }
301
+ if (temp <= 0.0) {
302
+ generatedColorCount = i + 1;
303
+ break;
304
+ }
305
+ }
306
+
307
+ // Mean colour of each box.
308
+ const colors: number[] = [];
309
+ for (let i = 0; i < generatedColorCount; i++) {
310
+ const cube = boxAt(cubes, i);
311
+ const weight = volume(cube, weights);
312
+ if (weight > 0) {
313
+ const r = Math.round(volume(cube, momentsR) / weight);
314
+ const g = Math.round(volume(cube, momentsG) / weight);
315
+ const b = Math.round(volume(cube, momentsB) / weight);
316
+ colors.push(((255 << 24) | ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff)) >>> 0);
317
+ }
318
+ }
319
+ return colors;
320
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Bloom colour engine — scheme variants.
3
+ *
4
+ * How each variant derives its five tonal palettes from the seed HCT. Faithful
5
+ * port of Material Color Utilities' `Scheme*` families (Apache-2.0), expressed
6
+ * as plain builder functions (our style) rather than subclasses. Fidelity/
7
+ * Content are intentionally left out — they need the temperature-cache complement
8
+ * and aren't useful for Bloom's fixed brand seeds.
9
+ */
10
+ import { DynamicScheme } from './dynamic-scheme';
11
+ import type { Hct } from './hct';
12
+ import { sanitizeDegreesDouble } from './math-utils';
13
+ import { TonalPalette } from './tonal-palette';
14
+ import { Variant } from './variant';
15
+
16
+ export type SchemeVariant = 'vibrant' | 'expressive' | 'tonalSpot' | 'neutral';
17
+
18
+ const VIBRANT_HUES = [0.0, 41.0, 61.0, 101.0, 131.0, 181.0, 251.0, 301.0, 360.0];
19
+ const VIBRANT_SECONDARY_ROTATIONS = [18.0, 15.0, 10.0, 12.0, 15.0, 18.0, 15.0, 12.0, 12.0];
20
+ const VIBRANT_TERTIARY_ROTATIONS = [35.0, 30.0, 20.0, 25.0, 30.0, 35.0, 30.0, 25.0, 25.0];
21
+
22
+ const EXPRESSIVE_HUES = [0.0, 21.0, 51.0, 121.0, 151.0, 191.0, 271.0, 321.0, 360.0];
23
+ const EXPRESSIVE_SECONDARY_ROTATIONS = [45.0, 95.0, 45.0, 20.0, 45.0, 90.0, 45.0, 45.0, 45.0];
24
+ const EXPRESSIVE_TERTIARY_ROTATIONS = [120.0, 120.0, 20.0, 45.0, 20.0, 15.0, 20.0, 120.0, 120.0];
25
+
26
+ /** Maxed-out colourfulness at each position of the primary palette. */
27
+ export function schemeVibrant(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
28
+ return new DynamicScheme({
29
+ sourceColorArgb: source.toInt(),
30
+ variant: Variant.VIBRANT,
31
+ contrastLevel,
32
+ isDark,
33
+ primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 200.0),
34
+ secondaryPalette: TonalPalette.fromHueAndChroma(
35
+ DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_SECONDARY_ROTATIONS),
36
+ 24.0,
37
+ ),
38
+ tertiaryPalette: TonalPalette.fromHueAndChroma(
39
+ DynamicScheme.getRotatedHue(source, VIBRANT_HUES, VIBRANT_TERTIARY_ROTATIONS),
40
+ 32.0,
41
+ ),
42
+ neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 10.0),
43
+ neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
44
+ });
45
+ }
46
+
47
+ /** Playful, intentionally detached from the seed hue (primary rotated 240°). */
48
+ export function schemeExpressive(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
49
+ return new DynamicScheme({
50
+ sourceColorArgb: source.toInt(),
51
+ variant: Variant.EXPRESSIVE,
52
+ contrastLevel,
53
+ isDark,
54
+ primaryPalette: TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 240.0), 40.0),
55
+ secondaryPalette: TonalPalette.fromHueAndChroma(
56
+ DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_SECONDARY_ROTATIONS),
57
+ 24.0,
58
+ ),
59
+ tertiaryPalette: TonalPalette.fromHueAndChroma(
60
+ DynamicScheme.getRotatedHue(source, EXPRESSIVE_HUES, EXPRESSIVE_TERTIARY_ROTATIONS),
61
+ 32.0,
62
+ ),
63
+ neutralPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 8.0),
64
+ neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue + 15, 12.0),
65
+ });
66
+ }
67
+
68
+ /** The Material You default — a calm, balanced treatment of the seed. */
69
+ export function schemeTonalSpot(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
70
+ return new DynamicScheme({
71
+ sourceColorArgb: source.toInt(),
72
+ variant: Variant.TONAL_SPOT,
73
+ contrastLevel,
74
+ isDark,
75
+ primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 36.0),
76
+ secondaryPalette: TonalPalette.fromHueAndChroma(source.hue, 16.0),
77
+ tertiaryPalette: TonalPalette.fromHueAndChroma(sanitizeDegreesDouble(source.hue + 60.0), 24.0),
78
+ neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 6.0),
79
+ neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 8.0),
80
+ });
81
+ }
82
+
83
+ /** Near-greyscale — the seed hue is present only as a whisper. */
84
+ export function schemeNeutral(source: Hct, isDark: boolean, contrastLevel: number): DynamicScheme {
85
+ return new DynamicScheme({
86
+ sourceColorArgb: source.toInt(),
87
+ variant: Variant.NEUTRAL,
88
+ contrastLevel,
89
+ isDark,
90
+ primaryPalette: TonalPalette.fromHueAndChroma(source.hue, 12.0),
91
+ secondaryPalette: TonalPalette.fromHueAndChroma(source.hue, 8.0),
92
+ tertiaryPalette: TonalPalette.fromHueAndChroma(source.hue, 16.0),
93
+ neutralPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
94
+ neutralVariantPalette: TonalPalette.fromHueAndChroma(source.hue, 2.0),
95
+ });
96
+ }
97
+
98
+ const BUILDERS: Record<SchemeVariant, (s: Hct, d: boolean, c: number) => DynamicScheme> = {
99
+ vibrant: schemeVibrant,
100
+ expressive: schemeExpressive,
101
+ tonalSpot: schemeTonalSpot,
102
+ neutral: schemeNeutral,
103
+ };
104
+
105
+ /** Build a scheme by variant name. */
106
+ export function buildScheme(
107
+ variant: SchemeVariant,
108
+ source: Hct,
109
+ isDark: boolean,
110
+ contrastLevel = 0,
111
+ ): DynamicScheme {
112
+ return BUILDERS[variant](source, isDark, contrastLevel);
113
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Bloom colour engine — seed scoring.
3
+ *
4
+ * Faithful port of Material Color Utilities' `Score` (Apache-2.0). Given a map
5
+ * of quantized colours → population, ranks them for theme suitability — favouring
6
+ * colours whose hue is well-represented in the image AND chromatic enough to make
7
+ * a good accent — and de-duplicates by hue. Returns the best seed candidates.
8
+ */
9
+ import { Hct } from './hct';
10
+ import { differenceDegrees, sanitizeDegreesInt } from './math-utils';
11
+
12
+ const TARGET_CHROMA = 48.0;
13
+ const WEIGHT_PROPORTION = 0.7;
14
+ const WEIGHT_CHROMA_ABOVE = 0.3;
15
+ const WEIGHT_CHROMA_BELOW = 0.1;
16
+ const CUTOFF_CHROMA = 5.0;
17
+ const CUTOFF_EXCITED_PROPORTION = 0.01;
18
+
19
+ export interface ScoreOptions {
20
+ desired?: number;
21
+ fallbackColorARGB?: number;
22
+ filter?: boolean;
23
+ }
24
+
25
+ /** Rank quantized colours as theme seeds. Best first. */
26
+ export function score(colorsToPopulation: Map<number, number>, options: ScoreOptions = {}): number[] {
27
+ const desired = options.desired ?? 4;
28
+ const fallbackColorARGB = options.fallbackColorARGB ?? 0xff4285f4;
29
+ const filter = options.filter ?? true;
30
+
31
+ const colorsHct: Hct[] = [];
32
+ const huePopulation = new Array<number>(360).fill(0);
33
+ let populationSum = 0;
34
+ for (const [argb, population] of colorsToPopulation.entries()) {
35
+ const hct = Hct.fromInt(argb);
36
+ colorsHct.push(hct);
37
+ const hueIndex = Math.floor(hct.hue);
38
+ huePopulation[hueIndex] = (huePopulation[hueIndex] ?? 0) + population;
39
+ populationSum += population;
40
+ }
41
+
42
+ const hueExcitedProportions = new Array<number>(360).fill(0.0);
43
+ for (let hue = 0; hue < 360; hue++) {
44
+ const proportion = (huePopulation[hue] ?? 0) / populationSum;
45
+ for (let i = hue - 14; i < hue + 16; i++) {
46
+ const index = sanitizeDegreesInt(i);
47
+ hueExcitedProportions[index] = (hueExcitedProportions[index] ?? 0) + proportion;
48
+ }
49
+ }
50
+
51
+ const scoredHct: Array<{ hct: Hct; score: number }> = [];
52
+ for (const hct of colorsHct) {
53
+ const hue = sanitizeDegreesInt(Math.round(hct.hue));
54
+ const proportion = hueExcitedProportions[hue] ?? 0;
55
+ if (filter && (hct.chroma < CUTOFF_CHROMA || proportion <= CUTOFF_EXCITED_PROPORTION)) {
56
+ continue;
57
+ }
58
+ const proportionScore = proportion * 100.0 * WEIGHT_PROPORTION;
59
+ const chromaWeight = hct.chroma < TARGET_CHROMA ? WEIGHT_CHROMA_BELOW : WEIGHT_CHROMA_ABOVE;
60
+ const chromaScore = (hct.chroma - TARGET_CHROMA) * chromaWeight;
61
+ scoredHct.push({ hct, score: proportionScore + chromaScore });
62
+ }
63
+ scoredHct.sort((a, b) => (a.score > b.score ? -1 : a.score < b.score ? 1 : 0));
64
+
65
+ const chosenColors: Hct[] = [];
66
+ for (let differenceLimit = 90; differenceLimit >= 15; differenceLimit--) {
67
+ chosenColors.length = 0;
68
+ for (const { hct } of scoredHct) {
69
+ const duplicateHue = chosenColors.find(
70
+ (chosen) => differenceDegrees(hct.hue, chosen.hue) < differenceLimit,
71
+ );
72
+ if (!duplicateHue) chosenColors.push(hct);
73
+ if (chosenColors.length >= desired) break;
74
+ }
75
+ if (chosenColors.length >= desired) break;
76
+ }
77
+
78
+ const colors: number[] = [];
79
+ if (chosenColors.length === 0) colors.push(fallbackColorARGB);
80
+ for (const chosen of chosenColors) colors.push(chosen.toInt());
81
+ return colors;
82
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Bloom colour engine — a seed colour from an image.
3
+ *
4
+ * The "theme from your photo/wallpaper" entry point. Wu-quantizes the pixels to
5
+ * a small representative palette, tallies each pixel to its nearest
6
+ * representative (a deterministic single assignment in L*a*b* — Bloom's stand-in
7
+ * for the reference's stochastic k-means, so results are reproducible), then
8
+ * scores the palette for theme suitability. Feed the top result straight into
9
+ * `generateRoleColors({ seed })`.
10
+ */
11
+ import { hexFromArgb } from './index';
12
+ import { labDistance, labFromInt, type LabPoint } from './lab-point-provider';
13
+ import { quantizeWu } from './quantizer-wu';
14
+ import { countByColor } from './quantizer-map';
15
+ import { score, type ScoreOptions } from './score';
16
+
17
+ /**
18
+ * Quantize `pixels` to a representative palette with its populations. Uses Wu
19
+ * (deterministic) for the palette, then assigns every pixel to its nearest
20
+ * representative in L*a*b* — no randomness, so the same image always yields the
21
+ * same result.
22
+ */
23
+ export function quantizeImage(pixels: number[], maxColors = 128): Map<number, number> {
24
+ const representatives = quantizeWu(pixels, maxColors);
25
+ if (representatives.length === 0) return new Map();
26
+ const repPoints: LabPoint[] = representatives.map(labFromInt);
27
+
28
+ const populations = new Array<number>(representatives.length).fill(0);
29
+ for (const [pixel, count] of countByColor(pixels).entries()) {
30
+ const point = labFromInt(pixel);
31
+ let best = 0;
32
+ let bestDistance = Infinity;
33
+ for (let i = 0; i < repPoints.length; i++) {
34
+ const repPoint = repPoints[i];
35
+ if (repPoint === undefined) continue;
36
+ const d = labDistance(point, repPoint);
37
+ if (d < bestDistance) {
38
+ bestDistance = d;
39
+ best = i;
40
+ }
41
+ }
42
+ populations[best] = (populations[best] ?? 0) + count;
43
+ }
44
+
45
+ const result = new Map<number, number>();
46
+ for (let i = 0; i < representatives.length; i++) {
47
+ const representative = representatives[i];
48
+ const population = populations[i] ?? 0;
49
+ if (representative !== undefined && population > 0) result.set(representative, population);
50
+ }
51
+ return result;
52
+ }
53
+
54
+ /** Ranked seed candidates (best first) extracted from an image's pixels (ARGB). */
55
+ export function seedsFromImagePixels(pixels: number[], options?: ScoreOptions): number[] {
56
+ return score(quantizeImage(pixels), options);
57
+ }
58
+
59
+ /** The single best seed colour from an image, as `#rrggbb`. */
60
+ export function seedHexFromImagePixels(pixels: number[]): string {
61
+ const [top] = seedsFromImagePixels(pixels, { desired: 1 });
62
+ if (top === undefined) throw new Error('no seed colour could be extracted');
63
+ return hexFromArgb(top);
64
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Bloom colour engine — tonal palettes.
3
+ *
4
+ * Faithful port of Material Color Utilities' `TonalPalette` (Apache-2.0). A
5
+ * palette holds a fixed hue + chroma and yields a colour for any tone (0–100).
6
+ * Schemes build primary/secondary/tertiary/neutral palettes and read specific
7
+ * tones per role.
8
+ */
9
+ import { Hct } from './hct';
10
+
11
+ export class TonalPalette {
12
+ private readonly cache = new Map<number, number>();
13
+
14
+ private constructor(
15
+ readonly hue: number,
16
+ readonly chroma: number,
17
+ readonly keyColor: Hct,
18
+ ) {}
19
+
20
+ static fromInt(argb: number): TonalPalette {
21
+ return TonalPalette.fromHct(Hct.fromInt(argb));
22
+ }
23
+
24
+ static fromHct(hct: Hct): TonalPalette {
25
+ return new TonalPalette(hct.hue, hct.chroma, hct);
26
+ }
27
+
28
+ static fromHueAndChroma(hue: number, chroma: number): TonalPalette {
29
+ const keyColor = new KeyColor(hue, chroma).create();
30
+ return new TonalPalette(hue, chroma, keyColor);
31
+ }
32
+
33
+ /** ARGB of this palette at the given tone. */
34
+ tone(tone: number): number {
35
+ let argb = this.cache.get(tone);
36
+ if (argb === undefined) {
37
+ argb = Hct.from(this.hue, this.chroma, tone).toInt();
38
+ this.cache.set(tone, argb);
39
+ }
40
+ return argb;
41
+ }
42
+
43
+ /** HCT of this palette at the given tone. */
44
+ getHct(tone: number): Hct {
45
+ return Hct.fromInt(this.tone(tone));
46
+ }
47
+ }
48
+
49
+ /** The colour that represents a palette's hue + chroma (first from T50). */
50
+ class KeyColor {
51
+ private readonly chromaCache = new Map<number, number>();
52
+ private readonly maxChromaValue = 200.0;
53
+
54
+ constructor(
55
+ private readonly hue: number,
56
+ private readonly requestedChroma: number,
57
+ ) {}
58
+
59
+ create(): Hct {
60
+ const pivotTone = 50;
61
+ const toneStepSize = 1;
62
+ const epsilon = 0.01;
63
+ let lowerTone = 0;
64
+ let upperTone = 100;
65
+ while (lowerTone < upperTone) {
66
+ const midTone = Math.floor((lowerTone + upperTone) / 2);
67
+ const isAscending = this.maxChroma(midTone) < this.maxChroma(midTone + toneStepSize);
68
+ const sufficientChroma = this.maxChroma(midTone) >= this.requestedChroma - epsilon;
69
+ if (sufficientChroma) {
70
+ if (Math.abs(lowerTone - pivotTone) < Math.abs(upperTone - pivotTone)) {
71
+ upperTone = midTone;
72
+ } else {
73
+ if (lowerTone === midTone) {
74
+ return Hct.from(this.hue, this.requestedChroma, lowerTone);
75
+ }
76
+ lowerTone = midTone;
77
+ }
78
+ } else if (isAscending) {
79
+ lowerTone = midTone + toneStepSize;
80
+ } else {
81
+ upperTone = midTone;
82
+ }
83
+ }
84
+ return Hct.from(this.hue, this.requestedChroma, lowerTone);
85
+ }
86
+
87
+ private maxChroma(tone: number): number {
88
+ const cached = this.chromaCache.get(tone);
89
+ if (cached !== undefined) return cached;
90
+ const chroma = Hct.from(this.hue, this.maxChromaValue, tone).chroma;
91
+ this.chromaCache.set(tone, chroma);
92
+ return chroma;
93
+ }
94
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bloom colour engine — tone-delta pair.
3
+ *
4
+ * Faithful port of Material Color Utilities' `ToneDeltaPair` (Apache-2.0).
5
+ * Constrains two related roles to keep a minimum tonal distance in a given
6
+ * direction (e.g. a fixed colour must stay lighter than its dim variant), so
7
+ * paired roles never collapse into each other.
8
+ */
9
+ import type { ColorRole } from './color-role';
10
+
11
+ export type TonePolarity = 'darker' | 'lighter' | 'nearer' | 'farther';
12
+
13
+ export class ToneDeltaPair {
14
+ constructor(
15
+ readonly roleA: ColorRole,
16
+ readonly roleB: ColorRole,
17
+ readonly delta: number,
18
+ readonly polarity: TonePolarity,
19
+ readonly stayTogether: boolean,
20
+ ) {}
21
+ }