@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,435 @@
1
+ /**
2
+ * Bloom colour engine — the colour roles.
3
+ *
4
+ * Faithful port of Material Color Utilities' `MaterialDynamicColors` (Apache-2.0),
5
+ * renamed to Bloom's own `Roles`. Each role is a {@link ColorRole} recipe over
6
+ * the scheme's palettes: surfaces, on-surfaces, the accent trio + their
7
+ * containers, outlines and error. Resolving a role against a scheme yields the
8
+ * final colour. The 12 "*Fixed" roles the reference ships are omitted — Bloom's
9
+ * token contract doesn't use them.
10
+ */
11
+ import { ColorRole } from './color-role';
12
+ import { ContrastCurve } from './contrast-curve';
13
+ import type { DynamicScheme } from './dynamic-scheme';
14
+ import { fixIfDisliked } from './dislike-analyzer';
15
+ import { Hct } from './hct';
16
+ import { ToneDeltaPair } from './tone-delta-pair';
17
+ import { Variant } from './variant';
18
+
19
+ const isFidelity = (s: DynamicScheme): boolean =>
20
+ s.variant === Variant.FIDELITY || s.variant === Variant.CONTENT;
21
+ const isMonochrome = (s: DynamicScheme): boolean => s.variant === Variant.MONOCHROME;
22
+
23
+ /** For fidelity schemes: find the tone nearest a target chroma. */
24
+ function findDesiredChromaByTone(
25
+ hue: number,
26
+ chroma: number,
27
+ tone: number,
28
+ byDecreasingTone: boolean,
29
+ ): number {
30
+ let answer = tone;
31
+ let closestToChroma = Hct.from(hue, chroma, tone);
32
+ if (closestToChroma.chroma < chroma) {
33
+ let chromaPeak = closestToChroma.chroma;
34
+ while (closestToChroma.chroma < chroma) {
35
+ answer += byDecreasingTone ? -1.0 : 1.0;
36
+ const potentialSolution = Hct.from(hue, chroma, answer);
37
+ if (chromaPeak > potentialSolution.chroma) break;
38
+ if (Math.abs(potentialSolution.chroma - chroma) < 0.4) break;
39
+ const potentialDelta = Math.abs(potentialSolution.chroma - chroma);
40
+ const currentDelta = Math.abs(closestToChroma.chroma - chroma);
41
+ if (potentialDelta < currentDelta) closestToChroma = potentialSolution;
42
+ chromaPeak = Math.max(chromaPeak, potentialSolution.chroma);
43
+ }
44
+ }
45
+ return answer;
46
+ }
47
+
48
+ /** The most-elevated surface a scheme can show — the brightest/dimmest ground. */
49
+ const highestSurface = (s: DynamicScheme): ColorRole => (s.isDark ? surfaceBright : surfaceDim);
50
+
51
+ // ---- palette key colours ----
52
+ const primaryPaletteKeyColor = ColorRole.fromPalette({
53
+ name: 'primary_palette_key_color',
54
+ palette: (s) => s.primaryPalette,
55
+ tone: (s) => s.primaryPalette.keyColor.tone,
56
+ });
57
+ const secondaryPaletteKeyColor = ColorRole.fromPalette({
58
+ name: 'secondary_palette_key_color',
59
+ palette: (s) => s.secondaryPalette,
60
+ tone: (s) => s.secondaryPalette.keyColor.tone,
61
+ });
62
+ const tertiaryPaletteKeyColor = ColorRole.fromPalette({
63
+ name: 'tertiary_palette_key_color',
64
+ palette: (s) => s.tertiaryPalette,
65
+ tone: (s) => s.tertiaryPalette.keyColor.tone,
66
+ });
67
+ const neutralPaletteKeyColor = ColorRole.fromPalette({
68
+ name: 'neutral_palette_key_color',
69
+ palette: (s) => s.neutralPalette,
70
+ tone: (s) => s.neutralPalette.keyColor.tone,
71
+ });
72
+ const neutralVariantPaletteKeyColor = ColorRole.fromPalette({
73
+ name: 'neutral_variant_palette_key_color',
74
+ palette: (s) => s.neutralVariantPalette,
75
+ tone: (s) => s.neutralVariantPalette.keyColor.tone,
76
+ });
77
+
78
+ // ---- surfaces ----
79
+ const background = ColorRole.fromPalette({
80
+ name: 'background',
81
+ palette: (s) => s.neutralPalette,
82
+ tone: (s) => (s.isDark ? 6 : 98),
83
+ isBackground: true,
84
+ });
85
+ const onBackground = ColorRole.fromPalette({
86
+ name: 'on_background',
87
+ palette: (s) => s.neutralPalette,
88
+ tone: (s) => (s.isDark ? 90 : 10),
89
+ background: () => background,
90
+ contrastCurve: new ContrastCurve(3, 3, 4.5, 7),
91
+ });
92
+ const surface = ColorRole.fromPalette({
93
+ name: 'surface',
94
+ palette: (s) => s.neutralPalette,
95
+ tone: (s) => (s.isDark ? 6 : 98),
96
+ isBackground: true,
97
+ });
98
+ const surfaceDim = ColorRole.fromPalette({
99
+ name: 'surface_dim',
100
+ palette: (s) => s.neutralPalette,
101
+ tone: (s) => (s.isDark ? 6 : new ContrastCurve(87, 87, 80, 75).get(s.contrastLevel)),
102
+ isBackground: true,
103
+ });
104
+ const surfaceBright = ColorRole.fromPalette({
105
+ name: 'surface_bright',
106
+ palette: (s) => s.neutralPalette,
107
+ tone: (s) => (s.isDark ? new ContrastCurve(24, 24, 29, 34).get(s.contrastLevel) : 98),
108
+ isBackground: true,
109
+ });
110
+ const surfaceContainerLowest = ColorRole.fromPalette({
111
+ name: 'surface_container_lowest',
112
+ palette: (s) => s.neutralPalette,
113
+ tone: (s) => (s.isDark ? new ContrastCurve(4, 4, 2, 0).get(s.contrastLevel) : 100),
114
+ isBackground: true,
115
+ });
116
+ const surfaceContainerLow = ColorRole.fromPalette({
117
+ name: 'surface_container_low',
118
+ palette: (s) => s.neutralPalette,
119
+ tone: (s) =>
120
+ s.isDark
121
+ ? new ContrastCurve(10, 10, 11, 12).get(s.contrastLevel)
122
+ : new ContrastCurve(96, 96, 96, 95).get(s.contrastLevel),
123
+ isBackground: true,
124
+ });
125
+ const surfaceContainer = ColorRole.fromPalette({
126
+ name: 'surface_container',
127
+ palette: (s) => s.neutralPalette,
128
+ tone: (s) =>
129
+ s.isDark
130
+ ? new ContrastCurve(12, 12, 16, 20).get(s.contrastLevel)
131
+ : new ContrastCurve(94, 94, 92, 90).get(s.contrastLevel),
132
+ isBackground: true,
133
+ });
134
+ const surfaceContainerHigh = ColorRole.fromPalette({
135
+ name: 'surface_container_high',
136
+ palette: (s) => s.neutralPalette,
137
+ tone: (s) =>
138
+ s.isDark
139
+ ? new ContrastCurve(17, 17, 21, 25).get(s.contrastLevel)
140
+ : new ContrastCurve(92, 92, 88, 85).get(s.contrastLevel),
141
+ isBackground: true,
142
+ });
143
+ const surfaceContainerHighest = ColorRole.fromPalette({
144
+ name: 'surface_container_highest',
145
+ palette: (s) => s.neutralPalette,
146
+ tone: (s) =>
147
+ s.isDark
148
+ ? new ContrastCurve(22, 22, 26, 30).get(s.contrastLevel)
149
+ : new ContrastCurve(90, 90, 84, 80).get(s.contrastLevel),
150
+ isBackground: true,
151
+ });
152
+ const onSurface = ColorRole.fromPalette({
153
+ name: 'on_surface',
154
+ palette: (s) => s.neutralPalette,
155
+ tone: (s) => (s.isDark ? 90 : 10),
156
+ background: (s) => highestSurface(s),
157
+ contrastCurve: new ContrastCurve(4.5, 7, 11, 21),
158
+ });
159
+ const surfaceVariant = ColorRole.fromPalette({
160
+ name: 'surface_variant',
161
+ palette: (s) => s.neutralVariantPalette,
162
+ tone: (s) => (s.isDark ? 30 : 90),
163
+ isBackground: true,
164
+ });
165
+ const onSurfaceVariant = ColorRole.fromPalette({
166
+ name: 'on_surface_variant',
167
+ palette: (s) => s.neutralVariantPalette,
168
+ tone: (s) => (s.isDark ? 80 : 30),
169
+ background: (s) => highestSurface(s),
170
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 11),
171
+ });
172
+ const inverseSurface = ColorRole.fromPalette({
173
+ name: 'inverse_surface',
174
+ palette: (s) => s.neutralPalette,
175
+ tone: (s) => (s.isDark ? 90 : 20),
176
+ });
177
+ const inverseOnSurface = ColorRole.fromPalette({
178
+ name: 'inverse_on_surface',
179
+ palette: (s) => s.neutralPalette,
180
+ tone: (s) => (s.isDark ? 20 : 95),
181
+ background: () => inverseSurface,
182
+ contrastCurve: new ContrastCurve(4.5, 7, 11, 21),
183
+ });
184
+ const outline = ColorRole.fromPalette({
185
+ name: 'outline',
186
+ palette: (s) => s.neutralVariantPalette,
187
+ tone: (s) => (s.isDark ? 60 : 50),
188
+ background: (s) => highestSurface(s),
189
+ contrastCurve: new ContrastCurve(1.5, 3, 4.5, 7),
190
+ });
191
+ const outlineVariant = ColorRole.fromPalette({
192
+ name: 'outline_variant',
193
+ palette: (s) => s.neutralVariantPalette,
194
+ tone: (s) => (s.isDark ? 30 : 80),
195
+ background: (s) => highestSurface(s),
196
+ contrastCurve: new ContrastCurve(1, 1, 3, 4.5),
197
+ });
198
+ const shadow = ColorRole.fromPalette({
199
+ name: 'shadow',
200
+ palette: (s) => s.neutralPalette,
201
+ tone: () => 0,
202
+ });
203
+ const scrim = ColorRole.fromPalette({
204
+ name: 'scrim',
205
+ palette: (s) => s.neutralPalette,
206
+ tone: () => 0,
207
+ });
208
+ const surfaceTint = ColorRole.fromPalette({
209
+ name: 'surface_tint',
210
+ palette: (s) => s.primaryPalette,
211
+ tone: (s) => (s.isDark ? 80 : 40),
212
+ isBackground: true,
213
+ });
214
+
215
+ // ---- primary ----
216
+ const primary = ColorRole.fromPalette({
217
+ name: 'primary',
218
+ palette: (s) => s.primaryPalette,
219
+ tone: (s) => (isMonochrome(s) ? (s.isDark ? 100 : 0) : s.isDark ? 80 : 40),
220
+ isBackground: true,
221
+ background: (s) => highestSurface(s),
222
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 7),
223
+ toneDeltaPair: () => new ToneDeltaPair(primaryContainer, primary, 10, 'nearer', false),
224
+ });
225
+ const onPrimary = ColorRole.fromPalette({
226
+ name: 'on_primary',
227
+ palette: (s) => s.primaryPalette,
228
+ tone: (s) => (isMonochrome(s) ? (s.isDark ? 10 : 90) : s.isDark ? 20 : 100),
229
+ background: () => primary,
230
+ contrastCurve: new ContrastCurve(4.5, 7, 11, 21),
231
+ });
232
+ const primaryContainer = ColorRole.fromPalette({
233
+ name: 'primary_container',
234
+ palette: (s) => s.primaryPalette,
235
+ tone: (s) => {
236
+ if (isFidelity(s)) return s.sourceColorHct.tone;
237
+ if (isMonochrome(s)) return s.isDark ? 85 : 25;
238
+ return s.isDark ? 30 : 90;
239
+ },
240
+ isBackground: true,
241
+ background: (s) => highestSurface(s),
242
+ contrastCurve: new ContrastCurve(1, 1, 3, 4.5),
243
+ toneDeltaPair: () => new ToneDeltaPair(primaryContainer, primary, 10, 'nearer', false),
244
+ });
245
+ const onPrimaryContainer = ColorRole.fromPalette({
246
+ name: 'on_primary_container',
247
+ palette: (s) => s.primaryPalette,
248
+ tone: (s) => {
249
+ if (isFidelity(s)) return ColorRole.foregroundTone(primaryContainer.tone(s), 4.5);
250
+ if (isMonochrome(s)) return s.isDark ? 0 : 100;
251
+ return s.isDark ? 90 : 30;
252
+ },
253
+ background: () => primaryContainer,
254
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 11),
255
+ });
256
+ const inversePrimary = ColorRole.fromPalette({
257
+ name: 'inverse_primary',
258
+ palette: (s) => s.primaryPalette,
259
+ tone: (s) => (s.isDark ? 40 : 80),
260
+ background: () => inverseSurface,
261
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 7),
262
+ });
263
+
264
+ // ---- secondary ----
265
+ const secondary = ColorRole.fromPalette({
266
+ name: 'secondary',
267
+ palette: (s) => s.secondaryPalette,
268
+ tone: (s) => (s.isDark ? 80 : 40),
269
+ isBackground: true,
270
+ background: (s) => highestSurface(s),
271
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 7),
272
+ toneDeltaPair: () => new ToneDeltaPair(secondaryContainer, secondary, 10, 'nearer', false),
273
+ });
274
+ const onSecondary = ColorRole.fromPalette({
275
+ name: 'on_secondary',
276
+ palette: (s) => s.secondaryPalette,
277
+ tone: (s) => (isMonochrome(s) ? (s.isDark ? 10 : 100) : s.isDark ? 20 : 100),
278
+ background: () => secondary,
279
+ contrastCurve: new ContrastCurve(4.5, 7, 11, 21),
280
+ });
281
+ const secondaryContainer = ColorRole.fromPalette({
282
+ name: 'secondary_container',
283
+ palette: (s) => s.secondaryPalette,
284
+ tone: (s) => {
285
+ const initialTone = s.isDark ? 30 : 90;
286
+ if (isMonochrome(s)) return s.isDark ? 30 : 85;
287
+ if (!isFidelity(s)) return initialTone;
288
+ return findDesiredChromaByTone(
289
+ s.secondaryPalette.hue,
290
+ s.secondaryPalette.chroma,
291
+ initialTone,
292
+ !s.isDark,
293
+ );
294
+ },
295
+ isBackground: true,
296
+ background: (s) => highestSurface(s),
297
+ contrastCurve: new ContrastCurve(1, 1, 3, 4.5),
298
+ toneDeltaPair: () => new ToneDeltaPair(secondaryContainer, secondary, 10, 'nearer', false),
299
+ });
300
+ const onSecondaryContainer = ColorRole.fromPalette({
301
+ name: 'on_secondary_container',
302
+ palette: (s) => s.secondaryPalette,
303
+ tone: (s) => {
304
+ if (isMonochrome(s)) return s.isDark ? 90 : 10;
305
+ if (!isFidelity(s)) return s.isDark ? 90 : 30;
306
+ return ColorRole.foregroundTone(secondaryContainer.tone(s), 4.5);
307
+ },
308
+ background: () => secondaryContainer,
309
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 11),
310
+ });
311
+
312
+ // ---- tertiary ----
313
+ const tertiary = ColorRole.fromPalette({
314
+ name: 'tertiary',
315
+ palette: (s) => s.tertiaryPalette,
316
+ tone: (s) => (isMonochrome(s) ? (s.isDark ? 90 : 25) : s.isDark ? 80 : 40),
317
+ isBackground: true,
318
+ background: (s) => highestSurface(s),
319
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 7),
320
+ toneDeltaPair: () => new ToneDeltaPair(tertiaryContainer, tertiary, 10, 'nearer', false),
321
+ });
322
+ const onTertiary = ColorRole.fromPalette({
323
+ name: 'on_tertiary',
324
+ palette: (s) => s.tertiaryPalette,
325
+ tone: (s) => (isMonochrome(s) ? (s.isDark ? 10 : 90) : s.isDark ? 20 : 100),
326
+ background: () => tertiary,
327
+ contrastCurve: new ContrastCurve(4.5, 7, 11, 21),
328
+ });
329
+ const tertiaryContainer = ColorRole.fromPalette({
330
+ name: 'tertiary_container',
331
+ palette: (s) => s.tertiaryPalette,
332
+ tone: (s) => {
333
+ if (isMonochrome(s)) return s.isDark ? 60 : 49;
334
+ if (!isFidelity(s)) return s.isDark ? 30 : 90;
335
+ const proposedHct = s.tertiaryPalette.getHct(s.sourceColorHct.tone);
336
+ return fixIfDisliked(proposedHct).tone;
337
+ },
338
+ isBackground: true,
339
+ background: (s) => highestSurface(s),
340
+ contrastCurve: new ContrastCurve(1, 1, 3, 4.5),
341
+ toneDeltaPair: () => new ToneDeltaPair(tertiaryContainer, tertiary, 10, 'nearer', false),
342
+ });
343
+ const onTertiaryContainer = ColorRole.fromPalette({
344
+ name: 'on_tertiary_container',
345
+ palette: (s) => s.tertiaryPalette,
346
+ tone: (s) => {
347
+ if (isMonochrome(s)) return s.isDark ? 0 : 100;
348
+ if (!isFidelity(s)) return s.isDark ? 90 : 30;
349
+ return ColorRole.foregroundTone(tertiaryContainer.tone(s), 4.5);
350
+ },
351
+ background: () => tertiaryContainer,
352
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 11),
353
+ });
354
+
355
+ // ---- error ----
356
+ const error = ColorRole.fromPalette({
357
+ name: 'error',
358
+ palette: (s) => s.errorPalette,
359
+ tone: (s) => (s.isDark ? 80 : 40),
360
+ isBackground: true,
361
+ background: (s) => highestSurface(s),
362
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 7),
363
+ toneDeltaPair: () => new ToneDeltaPair(errorContainer, error, 10, 'nearer', false),
364
+ });
365
+ const onError = ColorRole.fromPalette({
366
+ name: 'on_error',
367
+ palette: (s) => s.errorPalette,
368
+ tone: (s) => (s.isDark ? 20 : 100),
369
+ background: () => error,
370
+ contrastCurve: new ContrastCurve(4.5, 7, 11, 21),
371
+ });
372
+ const errorContainer = ColorRole.fromPalette({
373
+ name: 'error_container',
374
+ palette: (s) => s.errorPalette,
375
+ tone: (s) => (s.isDark ? 30 : 90),
376
+ isBackground: true,
377
+ background: (s) => highestSurface(s),
378
+ contrastCurve: new ContrastCurve(1, 1, 3, 4.5),
379
+ toneDeltaPair: () => new ToneDeltaPair(errorContainer, error, 10, 'nearer', false),
380
+ });
381
+ const onErrorContainer = ColorRole.fromPalette({
382
+ name: 'on_error_container',
383
+ palette: (s) => s.errorPalette,
384
+ tone: (s) => (isMonochrome(s) ? (s.isDark ? 90 : 10) : s.isDark ? 90 : 30),
385
+ background: () => errorContainer,
386
+ contrastCurve: new ContrastCurve(3, 4.5, 7, 11),
387
+ });
388
+
389
+ /** The full set of Bloom colour roles, keyed by name. Resolve via `.getArgb(scheme)`. */
390
+ export const Roles = {
391
+ primaryPaletteKeyColor,
392
+ secondaryPaletteKeyColor,
393
+ tertiaryPaletteKeyColor,
394
+ neutralPaletteKeyColor,
395
+ neutralVariantPaletteKeyColor,
396
+ background,
397
+ onBackground,
398
+ surface,
399
+ surfaceDim,
400
+ surfaceBright,
401
+ surfaceContainerLowest,
402
+ surfaceContainerLow,
403
+ surfaceContainer,
404
+ surfaceContainerHigh,
405
+ surfaceContainerHighest,
406
+ onSurface,
407
+ surfaceVariant,
408
+ onSurfaceVariant,
409
+ inverseSurface,
410
+ inverseOnSurface,
411
+ outline,
412
+ outlineVariant,
413
+ shadow,
414
+ scrim,
415
+ surfaceTint,
416
+ primary,
417
+ onPrimary,
418
+ primaryContainer,
419
+ onPrimaryContainer,
420
+ inversePrimary,
421
+ secondary,
422
+ onSecondary,
423
+ secondaryContainer,
424
+ onSecondaryContainer,
425
+ tertiary,
426
+ onTertiary,
427
+ tertiaryContainer,
428
+ onTertiaryContainer,
429
+ error,
430
+ onError,
431
+ errorContainer,
432
+ onErrorContainer,
433
+ } as const;
434
+
435
+ export type RoleName = keyof typeof Roles;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Bloom colour engine — colour-science conversions (non-HCT).
3
+ *
4
+ * ARGB ⇄ XYZ ⇄ L*a*b*, sRGB (de)linearisation, and L*↔Y. Self-contained port of
5
+ * Material Color Utilities' `color_utils` (Apache-2.0) into Bloom's own code —
6
+ * no external colour dependency. Verified byte-for-byte by golden tests.
7
+ */
8
+ import { clampInt, matrixMultiply } from './math-utils';
9
+
10
+ type Matrix3 = [
11
+ [number, number, number],
12
+ [number, number, number],
13
+ [number, number, number],
14
+ ];
15
+
16
+ const SRGB_TO_XYZ: Matrix3 = [
17
+ [0.41233895, 0.35762064, 0.18051042],
18
+ [0.2126, 0.7152, 0.0722],
19
+ [0.01932141, 0.11916382, 0.95034478],
20
+ ];
21
+
22
+ const XYZ_TO_SRGB: Matrix3 = [
23
+ [3.2413774792388685, -1.5376652402851851, -0.49885366846268053],
24
+ [-0.9691452513005321, 1.8758853451067872, 0.04156585616912061],
25
+ [0.05562093689691305, -0.20395524564742123, 1.0571799111220335],
26
+ ];
27
+
28
+ const WHITE_POINT_D65: [number, number, number] = [95.047, 100.0, 108.883];
29
+
30
+ /** Pack 0–255 R/G/B into an opaque ARGB int. */
31
+ export function argbFromRgb(red: number, green: number, blue: number): number {
32
+ return ((255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255)) >>> 0;
33
+ }
34
+
35
+ /** Pack linear-RGB (0–100 per channel) into an opaque ARGB int. */
36
+ export function argbFromLinrgb(linrgb: [number, number, number]): number {
37
+ return argbFromRgb(delinearized(linrgb[0]), delinearized(linrgb[1]), delinearized(linrgb[2]));
38
+ }
39
+
40
+ export function alphaFromArgb(argb: number): number {
41
+ return (argb >> 24) & 255;
42
+ }
43
+ export function redFromArgb(argb: number): number {
44
+ return (argb >> 16) & 255;
45
+ }
46
+ export function greenFromArgb(argb: number): number {
47
+ return (argb >> 8) & 255;
48
+ }
49
+ export function blueFromArgb(argb: number): number {
50
+ return argb & 255;
51
+ }
52
+ export function isOpaque(argb: number): boolean {
53
+ return alphaFromArgb(argb) >= 255;
54
+ }
55
+
56
+ /** XYZ → ARGB. */
57
+ export function argbFromXyz(x: number, y: number, z: number): number {
58
+ const m = XYZ_TO_SRGB;
59
+ const linearR = m[0][0] * x + m[0][1] * y + m[0][2] * z;
60
+ const linearG = m[1][0] * x + m[1][1] * y + m[1][2] * z;
61
+ const linearB = m[2][0] * x + m[2][1] * y + m[2][2] * z;
62
+ return argbFromRgb(delinearized(linearR), delinearized(linearG), delinearized(linearB));
63
+ }
64
+
65
+ /** ARGB → XYZ. */
66
+ export function xyzFromArgb(argb: number): [number, number, number] {
67
+ const r = linearized(redFromArgb(argb));
68
+ const g = linearized(greenFromArgb(argb));
69
+ const b = linearized(blueFromArgb(argb));
70
+ return matrixMultiply([r, g, b], SRGB_TO_XYZ);
71
+ }
72
+
73
+ /** L*a*b* → ARGB. */
74
+ export function argbFromLab(l: number, a: number, b: number): number {
75
+ const wp = WHITE_POINT_D65;
76
+ const fy = (l + 16.0) / 116.0;
77
+ const fx = a / 500.0 + fy;
78
+ const fz = fy - b / 200.0;
79
+ const x = labInvf(fx) * wp[0];
80
+ const y = labInvf(fy) * wp[1];
81
+ const z = labInvf(fz) * wp[2];
82
+ return argbFromXyz(x, y, z);
83
+ }
84
+
85
+ /** ARGB → L*a*b*. */
86
+ export function labFromArgb(argb: number): [number, number, number] {
87
+ const linearR = linearized(redFromArgb(argb));
88
+ const linearG = linearized(greenFromArgb(argb));
89
+ const linearB = linearized(blueFromArgb(argb));
90
+ const m = SRGB_TO_XYZ;
91
+ const x = m[0][0] * linearR + m[0][1] * linearG + m[0][2] * linearB;
92
+ const y = m[1][0] * linearR + m[1][1] * linearG + m[1][2] * linearB;
93
+ const z = m[2][0] * linearR + m[2][1] * linearG + m[2][2] * linearB;
94
+ const wp = WHITE_POINT_D65;
95
+ const fx = labF(x / wp[0]);
96
+ const fy = labF(y / wp[1]);
97
+ const fz = labF(z / wp[2]);
98
+ return [116.0 * fy - 16, 500.0 * (fx - fy), 200.0 * (fy - fz)];
99
+ }
100
+
101
+ /** L* → ARGB (a grayscale color with that lightness). */
102
+ export function argbFromLstar(lstar: number): number {
103
+ const component = delinearized(yFromLstar(lstar));
104
+ return argbFromRgb(component, component, component);
105
+ }
106
+
107
+ /** ARGB → L*. */
108
+ export function lstarFromArgb(argb: number): number {
109
+ const y = xyzFromArgb(argb)[1];
110
+ return 116.0 * labF(y / 100.0) - 16.0;
111
+ }
112
+
113
+ /** L* → Y (XYZ luminance). */
114
+ export function yFromLstar(lstar: number): number {
115
+ return 100.0 * labInvf((lstar + 16.0) / 116.0);
116
+ }
117
+
118
+ /** Y (XYZ luminance) → L*. */
119
+ export function lstarFromY(y: number): number {
120
+ return labF(y / 100.0) * 116.0 - 16.0;
121
+ }
122
+
123
+ /** sRGB component (0–255) → linear (0–100). */
124
+ export function linearized(rgbComponent: number): number {
125
+ const normalized = rgbComponent / 255.0;
126
+ return normalized <= 0.040449936
127
+ ? (normalized / 12.92) * 100.0
128
+ : Math.pow((normalized + 0.055) / 1.055, 2.4) * 100.0;
129
+ }
130
+
131
+ /** Linear component (0–100) → sRGB (0–255). */
132
+ export function delinearized(rgbComponent: number): number {
133
+ const normalized = rgbComponent / 100.0;
134
+ const delin =
135
+ normalized <= 0.0031308
136
+ ? normalized * 12.92
137
+ : 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
138
+ return clampInt(0, 255, Math.round(delin * 255.0));
139
+ }
140
+
141
+ /** The D65 white point. */
142
+ export function whitePointD65(): [number, number, number] {
143
+ return WHITE_POINT_D65;
144
+ }
145
+
146
+ function labF(t: number): number {
147
+ const e = 216.0 / 24389.0;
148
+ const kappa = 24389.0 / 27.0;
149
+ return t > e ? Math.pow(t, 1.0 / 3.0) : (kappa * t + 16) / 116;
150
+ }
151
+
152
+ function labInvf(ft: number): number {
153
+ const e = 216.0 / 24389.0;
154
+ const kappa = 24389.0 / 27.0;
155
+ const ft3 = ft * ft * ft;
156
+ return ft3 > e ? ft3 : (116 * ft - 16) / kappa;
157
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Bloom colour engine — contrast curve.
3
+ *
4
+ * Faithful port of Material Color Utilities' `ContrastCurve` (Apache-2.0). Maps
5
+ * a scheme's contrast level (−1 low … 0 normal … 1 high) to the target WCAG
6
+ * ratio a role should hit, interpolating between four control points.
7
+ */
8
+ import { lerp } from './math-utils';
9
+
10
+ export class ContrastCurve {
11
+ constructor(
12
+ readonly low: number,
13
+ readonly normal: number,
14
+ readonly medium: number,
15
+ readonly high: number,
16
+ ) {}
17
+
18
+ get(contrastLevel: number): number {
19
+ if (contrastLevel <= -1.0) return this.low;
20
+ if (contrastLevel < 0.0) return lerp(this.low, this.normal, (contrastLevel - -1) / 1);
21
+ if (contrastLevel < 0.5) return lerp(this.normal, this.medium, (contrastLevel - 0) / 0.5);
22
+ if (contrastLevel < 1.0) return lerp(this.medium, this.high, (contrastLevel - 0.5) / 0.5);
23
+ return this.high;
24
+ }
25
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Bloom colour engine — contrast in tone space.
3
+ *
4
+ * Faithful port of Material Color Utilities' `Contrast` (Apache-2.0). Because
5
+ * HCT tone maps to L*, contrast ratios can be computed and *targeted* directly
6
+ * in tone: given a tone and a desired WCAG ratio, find the lighter/darker tone
7
+ * that hits it. Drives every role's on-colour and container legibility.
8
+ */
9
+ import { clampDouble } from './math-utils';
10
+ import { lstarFromY, yFromLstar } from './color-utils';
11
+
12
+ /** WCAG contrast ratio between two tones (L*). */
13
+ export function ratioOfTones(toneA: number, toneB: number): number {
14
+ toneA = clampDouble(0.0, 100.0, toneA);
15
+ toneB = clampDouble(0.0, 100.0, toneB);
16
+ return ratioOfYs(yFromLstar(toneA), yFromLstar(toneB));
17
+ }
18
+
19
+ export function ratioOfYs(y1: number, y2: number): number {
20
+ const lighter = y1 > y2 ? y1 : y2;
21
+ const darker = lighter === y2 ? y1 : y2;
22
+ return (lighter + 5.0) / (darker + 5.0);
23
+ }
24
+
25
+ /** Lightest tone that hits `ratio` against `tone`, or -1 if impossible. */
26
+ export function lighter(tone: number, ratio: number): number {
27
+ if (tone < 0.0 || tone > 100.0) return -1.0;
28
+ const darkY = yFromLstar(tone);
29
+ const lightY = ratio * (darkY + 5.0) - 5.0;
30
+ const realContrast = ratioOfYs(lightY, darkY);
31
+ const delta = Math.abs(realContrast - ratio);
32
+ if (realContrast < ratio && delta > 0.04) return -1;
33
+ const returnValue = lstarFromY(lightY) + 0.4;
34
+ return returnValue < 0 || returnValue > 100 ? -1 : returnValue;
35
+ }
36
+
37
+ /** Darkest tone that hits `ratio` against `tone`, or -1 if impossible. */
38
+ export function darker(tone: number, ratio: number): number {
39
+ if (tone < 0.0 || tone > 100.0) return -1.0;
40
+ const lightY = yFromLstar(tone);
41
+ const darkY = (lightY + 5.0) / ratio - 5.0;
42
+ const realContrast = ratioOfYs(lightY, darkY);
43
+ const delta = Math.abs(realContrast - ratio);
44
+ if (realContrast < ratio && delta > 0.04) return -1;
45
+ const returnValue = lstarFromY(darkY) - 0.4;
46
+ return returnValue < 0 || returnValue > 100 ? -1 : returnValue;
47
+ }
48
+
49
+ /** Like {@link lighter} but returns 100 instead of -1 when unattainable. */
50
+ export function lighterUnsafe(tone: number, ratio: number): number {
51
+ const lighterSafe = lighter(tone, ratio);
52
+ return lighterSafe < 0.0 ? 100.0 : lighterSafe;
53
+ }
54
+
55
+ /** Like {@link darker} but returns 0 instead of -1 when unattainable. */
56
+ export function darkerUnsafe(tone: number, ratio: number): number {
57
+ const darkerSafe = darker(tone, ratio);
58
+ return darkerSafe < 0.0 ? 0.0 : darkerSafe;
59
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bloom colour engine — dislike analyzer.
3
+ *
4
+ * Faithful port of Material Color Utilities' `DislikeAnalyzer` (Apache-2.0).
5
+ * Certain dark-yellow-green HCT colours read as "bile"; this nudges their tone
6
+ * up to 70 so generated palettes never land on a universally-disliked colour.
7
+ */
8
+ import { Hct } from './hct';
9
+
10
+ export function isDisliked(hct: Hct): boolean {
11
+ const huePasses = Math.round(hct.hue) >= 90.0 && Math.round(hct.hue) <= 111.0;
12
+ const chromaPasses = Math.round(hct.chroma) > 16.0;
13
+ const tonePasses = Math.round(hct.tone) < 65.0;
14
+ return huePasses && chromaPasses && tonePasses;
15
+ }
16
+
17
+ export function fixIfDisliked(hct: Hct): Hct {
18
+ return isDisliked(hct) ? Hct.from(hct.hue, hct.chroma, 70.0) : hct;
19
+ }