color-map 2.2.0 → 2.3.1

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 (366) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.codex-plugin/plugin.json +12 -0
  3. package/.cursor-plugin/plugin.json +12 -0
  4. package/CHANGELOG.md +100 -0
  5. package/README.md +71 -8
  6. package/bin/cli-CELmx3Y9.js +6414 -0
  7. package/bin/color-map.js +9 -0
  8. package/bin/index.js +1 -0
  9. package/cjs/color/index.d.ts +5 -0
  10. package/cjs/color/index.js +52 -0
  11. package/cjs/color-wNrkrhC7.js +374 -0
  12. package/cjs/color-wNrkrhC7.js.map +1 -0
  13. package/cjs/contrast-XjF5Rwui.js +293 -0
  14. package/cjs/contrast-XjF5Rwui.js.map +1 -0
  15. package/cjs/gamut-CG1J2aat.d.ts +158 -0
  16. package/cjs/gamut-CG1J2aat.d.ts.map +1 -0
  17. package/cjs/index-BcEqCPHz.d.ts +137 -0
  18. package/cjs/index-BcEqCPHz.d.ts.map +1 -0
  19. package/cjs/index-CGKCqRqD.d.ts +111 -0
  20. package/cjs/index-CGKCqRqD.d.ts.map +1 -0
  21. package/cjs/index-Cv3iUUYE.d.ts +171 -0
  22. package/cjs/index-Cv3iUUYE.d.ts.map +1 -0
  23. package/cjs/index-Dw43Hj4v.d.ts +41 -0
  24. package/cjs/index-Dw43Hj4v.d.ts.map +1 -0
  25. package/cjs/index-Dy8UARqO.d.ts +72 -0
  26. package/cjs/index-Dy8UARqO.d.ts.map +1 -0
  27. package/cjs/index.d.ts +26 -36
  28. package/cjs/index.d.ts.map +1 -1
  29. package/cjs/index.js +83 -62
  30. package/cjs/index.js.map +1 -1
  31. package/cjs/legacy/index.d.ts +2 -0
  32. package/cjs/legacy/index.js +75 -0
  33. package/cjs/legacy/index.js.map +1 -0
  34. package/cjs/mix-Cs4owSbW.js +158 -0
  35. package/cjs/mix-Cs4owSbW.js.map +1 -0
  36. package/cjs/mix-DGlKoUw4.d.ts +31 -0
  37. package/cjs/mix-DGlKoUw4.d.ts.map +1 -0
  38. package/cjs/palette/index.d.ts +3 -0
  39. package/cjs/palette/index.js +8 -0
  40. package/cjs/palette-DyFV_KoJ.js +332 -0
  41. package/cjs/palette-DyFV_KoJ.js.map +1 -0
  42. package/cjs/parse-8mt2OMKf.js +1508 -0
  43. package/cjs/parse-8mt2OMKf.js.map +1 -0
  44. package/cjs/scale/index.d.ts +2 -0
  45. package/cjs/scale/index.js +11 -0
  46. package/cjs/scale-Cq9pXrc1.js +318 -0
  47. package/cjs/scale-Cq9pXrc1.js.map +1 -0
  48. package/cjs/semantic-CrEIo8tS.d.ts +52 -0
  49. package/cjs/semantic-CrEIo8tS.d.ts.map +1 -0
  50. package/cjs/serialize-C69DAmoE.d.ts +25 -0
  51. package/cjs/serialize-C69DAmoE.d.ts.map +1 -0
  52. package/cjs/serialize-Ci2X0cPN.js +103 -0
  53. package/cjs/serialize-Ci2X0cPN.js.map +1 -0
  54. package/cjs/tokens/index.d.ts +2 -0
  55. package/cjs/tokens/index.js +6 -0
  56. package/cjs/tokens-CzXxaMgM.js +168 -0
  57. package/cjs/tokens-CzXxaMgM.js.map +1 -0
  58. package/cjs/tone-BAhu-QzQ.js +118 -0
  59. package/cjs/tone-BAhu-QzQ.js.map +1 -0
  60. package/dist/color-map.es5.js.map +1 -1
  61. package/esm/color/a98-rgb.d.ts +7 -0
  62. package/esm/color/a98-rgb.d.ts.map +1 -0
  63. package/esm/color/a98-rgb.js +13 -0
  64. package/esm/color/a98-rgb.js.map +1 -0
  65. package/esm/color/argb.d.ts +6 -0
  66. package/esm/color/argb.d.ts.map +1 -0
  67. package/esm/color/argb.js +25 -0
  68. package/esm/color/argb.js.map +1 -0
  69. package/esm/color/calc.js +82 -0
  70. package/esm/color/calc.js.map +1 -0
  71. package/esm/color/compare.d.ts +20 -0
  72. package/esm/color/compare.d.ts.map +1 -0
  73. package/esm/color/compare.js +46 -0
  74. package/esm/color/compare.js.map +1 -0
  75. package/esm/color/contrast.d.ts +107 -0
  76. package/esm/color/contrast.d.ts.map +1 -0
  77. package/esm/color/contrast.js +242 -0
  78. package/esm/color/contrast.js.map +1 -0
  79. package/esm/color/convert.d.ts +9 -0
  80. package/esm/color/convert.d.ts.map +1 -0
  81. package/esm/color/convert.js +45 -0
  82. package/esm/color/convert.js.map +1 -0
  83. package/esm/color/display-p3-linear.d.ts +7 -0
  84. package/esm/color/display-p3-linear.d.ts.map +1 -0
  85. package/esm/color/display-p3-linear.js +12 -0
  86. package/esm/color/display-p3-linear.js.map +1 -0
  87. package/esm/color/display-p3.d.ts +7 -0
  88. package/esm/color/display-p3.d.ts.map +1 -0
  89. package/esm/color/display-p3.js +12 -0
  90. package/esm/color/display-p3.js.map +1 -0
  91. package/esm/color/expr.d.ts +43 -0
  92. package/esm/color/expr.d.ts.map +1 -0
  93. package/esm/color/expr.js +160 -0
  94. package/esm/color/expr.js.map +1 -0
  95. package/esm/color/gamut.d.ts +31 -0
  96. package/esm/color/gamut.d.ts.map +1 -0
  97. package/esm/color/gamut.js +210 -0
  98. package/esm/color/gamut.js.map +1 -0
  99. package/esm/color/hsl.d.ts +7 -0
  100. package/esm/color/hsl.d.ts.map +1 -0
  101. package/esm/color/hsl.js +67 -0
  102. package/esm/color/hsl.js.map +1 -0
  103. package/esm/color/hwb.d.ts +7 -0
  104. package/esm/color/hwb.d.ts.map +1 -0
  105. package/esm/color/hwb.js +40 -0
  106. package/esm/color/hwb.js.map +1 -0
  107. package/esm/color/index.d.ts +28 -0
  108. package/esm/color/index.js +27 -0
  109. package/esm/color/lab.d.ts +7 -0
  110. package/esm/color/lab.d.ts.map +1 -0
  111. package/esm/color/lab.js +33 -0
  112. package/esm/color/lab.js.map +1 -0
  113. package/esm/color/lch.d.ts +7 -0
  114. package/esm/color/lch.d.ts.map +1 -0
  115. package/esm/color/lch.js +11 -0
  116. package/esm/color/lch.js.map +1 -0
  117. package/esm/color/math.js +32 -0
  118. package/esm/color/math.js.map +1 -0
  119. package/esm/color/matrices.js +180 -0
  120. package/esm/color/matrices.js.map +1 -0
  121. package/esm/color/mix.d.ts +30 -0
  122. package/esm/color/mix.d.ts.map +1 -0
  123. package/esm/color/mix.js +152 -0
  124. package/esm/color/mix.js.map +1 -0
  125. package/esm/color/named.js +11 -0
  126. package/esm/color/named.js.map +1 -0
  127. package/esm/color/oklab.d.ts +7 -0
  128. package/esm/color/oklab.d.ts.map +1 -0
  129. package/esm/color/oklab.js +21 -0
  130. package/esm/color/oklab.js.map +1 -0
  131. package/esm/color/oklch.d.ts +7 -0
  132. package/esm/color/oklch.d.ts.map +1 -0
  133. package/esm/color/oklch.js +11 -0
  134. package/esm/color/oklch.js.map +1 -0
  135. package/esm/color/parse.d.ts +9 -0
  136. package/esm/color/parse.d.ts.map +1 -0
  137. package/esm/color/parse.js +452 -0
  138. package/esm/color/parse.js.map +1 -0
  139. package/esm/color/polar.js +27 -0
  140. package/esm/color/polar.js.map +1 -0
  141. package/esm/color/prophoto-rgb.d.ts +7 -0
  142. package/esm/color/prophoto-rgb.d.ts.map +1 -0
  143. package/esm/color/prophoto-rgb.js +18 -0
  144. package/esm/color/prophoto-rgb.js.map +1 -0
  145. package/esm/color/rec2020.d.ts +7 -0
  146. package/esm/color/rec2020.d.ts.map +1 -0
  147. package/esm/color/rec2020.js +13 -0
  148. package/esm/color/rec2020.js.map +1 -0
  149. package/esm/color/resolve.d.ts +18 -0
  150. package/esm/color/resolve.d.ts.map +1 -0
  151. package/esm/color/resolve.js +98 -0
  152. package/esm/color/resolve.js.map +1 -0
  153. package/esm/color/rgb.js +20 -0
  154. package/esm/color/rgb.js.map +1 -0
  155. package/esm/color/serialize.d.ts +24 -0
  156. package/esm/color/serialize.d.ts.map +1 -0
  157. package/esm/color/serialize.js +88 -0
  158. package/esm/color/serialize.js.map +1 -0
  159. package/esm/color/spaces.d.ts +6 -0
  160. package/esm/color/spaces.d.ts.map +1 -0
  161. package/esm/color/spaces.js +42 -0
  162. package/esm/color/spaces.js.map +1 -0
  163. package/esm/color/srgb-linear.d.ts +7 -0
  164. package/esm/color/srgb-linear.d.ts.map +1 -0
  165. package/esm/color/srgb-linear.js +12 -0
  166. package/esm/color/srgb-linear.js.map +1 -0
  167. package/esm/color/srgb.d.ts +7 -0
  168. package/esm/color/srgb.d.ts.map +1 -0
  169. package/esm/color/srgb.js +12 -0
  170. package/esm/color/srgb.js.map +1 -0
  171. package/esm/color/transfer.js +11 -0
  172. package/esm/color/transfer.js.map +1 -0
  173. package/esm/color/types.d.ts +23 -0
  174. package/esm/color/types.d.ts.map +1 -0
  175. package/esm/color/unknown-name.js +16 -0
  176. package/esm/color/unknown-name.js.map +1 -0
  177. package/esm/color/xyz-d50.d.ts +7 -0
  178. package/esm/color/xyz-d50.d.ts.map +1 -0
  179. package/esm/color/xyz-d50.js +15 -0
  180. package/esm/color/xyz-d50.js.map +1 -0
  181. package/esm/color/xyz-d65.d.ts +4 -0
  182. package/esm/color/xyz-d65.d.ts.map +1 -0
  183. package/esm/color/xyz-d65.js +10 -0
  184. package/esm/color/xyz-d65.js.map +1 -0
  185. package/esm/index.d.ts +63 -6
  186. package/esm/index.d.ts.map +1 -0
  187. package/esm/index.js +54 -5
  188. package/esm/index.js.map +1 -0
  189. package/esm/legacy/createColors.d.ts +9 -0
  190. package/esm/legacy/createColors.d.ts.map +1 -0
  191. package/esm/{createColors.js → legacy/createColors.js} +7 -2
  192. package/esm/legacy/createColors.js.map +1 -0
  193. package/esm/legacy/createColorsFromMap.d.ts +9 -0
  194. package/esm/legacy/createColorsFromMap.d.ts.map +1 -0
  195. package/esm/{createColorsFromMap.js → legacy/createColorsFromMap.js} +4 -2
  196. package/esm/legacy/createColorsFromMap.js.map +1 -0
  197. package/esm/legacy/index.d.ts +6 -0
  198. package/esm/legacy/index.js +5 -0
  199. package/esm/legacy/interfaces.d.ts +19 -0
  200. package/esm/legacy/interfaces.d.ts.map +1 -0
  201. package/esm/{rgbHex.d.ts → legacy/rgbHex.d.ts} +1 -4
  202. package/esm/legacy/rgbHex.d.ts.map +1 -0
  203. package/esm/{rgbHex.js → legacy/rgbHex.js} +1 -2
  204. package/esm/legacy/rgbHex.js.map +1 -0
  205. package/esm/legacy/rgbaString.d.ts +4 -0
  206. package/esm/{rgbaString.d.ts.map → legacy/rgbaString.d.ts.map} +1 -1
  207. package/esm/legacy/rgbaString.js +8 -0
  208. package/esm/legacy/rgbaString.js.map +1 -0
  209. package/esm/palette/dark.d.ts +23 -0
  210. package/esm/palette/dark.d.ts.map +1 -0
  211. package/esm/palette/dark.js +54 -0
  212. package/esm/palette/dark.js.map +1 -0
  213. package/esm/palette/harmony.d.ts +56 -0
  214. package/esm/palette/harmony.d.ts.map +1 -0
  215. package/esm/palette/harmony.js +115 -0
  216. package/esm/palette/harmony.js.map +1 -0
  217. package/esm/palette/index.d.ts +5 -0
  218. package/esm/palette/index.js +5 -0
  219. package/esm/palette/semantic.d.ts +53 -0
  220. package/esm/palette/semantic.d.ts.map +1 -0
  221. package/esm/palette/semantic.js +96 -0
  222. package/esm/palette/semantic.js.map +1 -0
  223. package/esm/palette/validate.d.ts +39 -0
  224. package/esm/palette/validate.d.ts.map +1 -0
  225. package/esm/palette/validate.js +56 -0
  226. package/esm/palette/validate.js.map +1 -0
  227. package/esm/scale/brand.d.ts +21 -0
  228. package/esm/scale/brand.d.ts.map +1 -0
  229. package/esm/scale/brand.js +63 -0
  230. package/esm/scale/brand.js.map +1 -0
  231. package/esm/scale/contrast-scale.d.ts +33 -0
  232. package/esm/scale/contrast-scale.d.ts.map +1 -0
  233. package/esm/scale/contrast-scale.js +26 -0
  234. package/esm/scale/contrast-scale.js.map +1 -0
  235. package/esm/scale/gradient.d.ts +38 -0
  236. package/esm/scale/gradient.d.ts.map +1 -0
  237. package/esm/scale/gradient.js +73 -0
  238. package/esm/scale/gradient.js.map +1 -0
  239. package/esm/scale/index.d.ts +6 -0
  240. package/esm/scale/index.js +6 -0
  241. package/esm/scale/scale.d.ts +31 -0
  242. package/esm/scale/scale.d.ts.map +1 -0
  243. package/esm/scale/scale.js +152 -0
  244. package/esm/scale/scale.js.map +1 -0
  245. package/esm/scale/tone.d.ts +25 -0
  246. package/esm/scale/tone.d.ts.map +1 -0
  247. package/esm/scale/tone.js +96 -0
  248. package/esm/scale/tone.js.map +1 -0
  249. package/esm/tokens/css.d.ts +18 -0
  250. package/esm/tokens/css.d.ts.map +1 -0
  251. package/esm/tokens/css.js +23 -0
  252. package/esm/tokens/css.js.map +1 -0
  253. package/esm/tokens/dtcg.d.ts +33 -0
  254. package/esm/tokens/dtcg.d.ts.map +1 -0
  255. package/esm/tokens/dtcg.js +42 -0
  256. package/esm/tokens/dtcg.js.map +1 -0
  257. package/esm/tokens/index.d.ts +5 -0
  258. package/esm/tokens/index.js +4 -0
  259. package/esm/tokens/tailwind.d.ts +19 -0
  260. package/esm/tokens/tailwind.d.ts.map +1 -0
  261. package/esm/tokens/tailwind.js +19 -0
  262. package/esm/tokens/tailwind.js.map +1 -0
  263. package/esm/tokens/tokens.d.ts +11 -0
  264. package/esm/tokens/tokens.d.ts.map +1 -0
  265. package/esm/tokens/tokens.js +77 -0
  266. package/esm/tokens/tokens.js.map +1 -0
  267. package/llms.txt +48 -0
  268. package/package.json +277 -64
  269. package/plugin.json +18 -0
  270. package/schemas/contrast.schema.json +24 -0
  271. package/schemas/convert.schema.json +60 -0
  272. package/schemas/export.schema.json +106 -0
  273. package/schemas/find-contrast.schema.json +51 -0
  274. package/schemas/harmony.schema.json +58 -0
  275. package/schemas/mix.schema.json +72 -0
  276. package/schemas/palette.schema.json +56 -0
  277. package/schemas/scale.schema.json +86 -0
  278. package/skills/color-map/SKILL.md +38 -0
  279. package/src/cli/bin.ts +11 -0
  280. package/src/cli/cli.ts +40 -0
  281. package/src/cli/commands/contrast.ts +36 -0
  282. package/src/cli/commands/convert.ts +37 -0
  283. package/src/cli/commands/export.ts +102 -0
  284. package/src/cli/commands/find-contrast.ts +37 -0
  285. package/src/cli/commands/harmony.ts +30 -0
  286. package/src/cli/commands/mix.ts +29 -0
  287. package/src/cli/commands/palette.ts +66 -0
  288. package/src/cli/commands/scale.ts +75 -0
  289. package/src/cli/index.ts +3 -0
  290. package/src/cli/schema-command.ts +31 -0
  291. package/src/cli/schemas.ts +86 -0
  292. package/src/cli/shared.ts +72 -0
  293. package/src/cli/testing.ts +27 -0
  294. package/src/color/a98-rgb.ts +11 -0
  295. package/src/color/argb.ts +21 -0
  296. package/src/color/calc.ts +84 -0
  297. package/src/color/compare.ts +53 -0
  298. package/src/color/contrast.ts +277 -0
  299. package/src/color/convert.ts +43 -0
  300. package/src/color/display-p3-linear.ts +9 -0
  301. package/src/color/display-p3.ts +9 -0
  302. package/src/color/expr.ts +158 -0
  303. package/src/color/gamut.ts +215 -0
  304. package/src/color/hsl.ts +57 -0
  305. package/src/color/hwb.ts +26 -0
  306. package/src/color/index.ts +43 -0
  307. package/src/color/lab.ts +29 -0
  308. package/src/color/lch.ts +8 -0
  309. package/src/color/math.ts +30 -0
  310. package/src/color/matrices.ts +91 -0
  311. package/src/color/mix.ts +158 -0
  312. package/src/color/named.ts +13 -0
  313. package/src/color/oklab.ts +19 -0
  314. package/src/color/oklch.ts +8 -0
  315. package/src/color/parse.ts +355 -0
  316. package/src/color/polar.ts +17 -0
  317. package/src/color/prophoto-rgb.ts +18 -0
  318. package/src/color/rec2020.ts +11 -0
  319. package/src/color/resolve.ts +87 -0
  320. package/src/color/rgb.ts +20 -0
  321. package/src/color/serialize.ts +96 -0
  322. package/src/color/spaces.ts +42 -0
  323. package/src/color/srgb-linear.ts +9 -0
  324. package/src/color/srgb.ts +9 -0
  325. package/src/color/test-utils.ts +10 -0
  326. package/src/color/transfer.ts +14 -0
  327. package/src/color/types.ts +41 -0
  328. package/src/color/unknown-name.ts +15 -0
  329. package/src/color/xyz-d50.ts +13 -0
  330. package/src/color/xyz-d65.ts +8 -0
  331. package/src/index.ts +37 -0
  332. package/{ts → src/legacy}/createColors.ts +6 -0
  333. package/{ts → src/legacy}/createColorsFromMap.ts +3 -0
  334. package/{ts → src/legacy}/interfaces.ts +4 -0
  335. package/{ts → src/legacy}/rgbaString.ts +1 -0
  336. package/src/palette/dark.ts +55 -0
  337. package/src/palette/harmony.ts +107 -0
  338. package/src/palette/index.ts +18 -0
  339. package/src/palette/semantic.ts +120 -0
  340. package/src/palette/validate.ts +82 -0
  341. package/src/scale/brand.ts +50 -0
  342. package/src/scale/contrast-scale.ts +48 -0
  343. package/src/scale/gradient.ts +91 -0
  344. package/src/scale/index.ts +5 -0
  345. package/src/scale/scale.ts +127 -0
  346. package/src/scale/tone.ts +69 -0
  347. package/src/tokens/css.ts +32 -0
  348. package/src/tokens/dtcg.ts +62 -0
  349. package/src/tokens/index.ts +11 -0
  350. package/src/tokens/tailwind.ts +24 -0
  351. package/src/tokens/tokens.ts +82 -0
  352. package/esm/createColors.d.ts +0 -6
  353. package/esm/createColors.d.ts.map +0 -1
  354. package/esm/createColors.js.map +0 -1
  355. package/esm/createColorsFromMap.d.ts +0 -9
  356. package/esm/createColorsFromMap.d.ts.map +0 -1
  357. package/esm/createColorsFromMap.js.map +0 -1
  358. package/esm/interfaces.d.ts +0 -18
  359. package/esm/interfaces.d.ts.map +0 -1
  360. package/esm/rgbHex.d.ts.map +0 -1
  361. package/esm/rgbHex.js.map +0 -1
  362. package/esm/rgbaString.d.ts +0 -6
  363. package/esm/rgbaString.js +0 -8
  364. package/esm/rgbaString.js.map +0 -1
  365. /package/{ts → src/legacy}/index.ts +0 -0
  366. /package/{ts → src/legacy}/rgbHex.ts +0 -0
@@ -0,0 +1,277 @@
1
+ import { type ComponentsIn, componentsOver } from './convert.js'
2
+ import { toGamutWith } from './gamut.js'
3
+ import { oklab } from './oklab.js'
4
+ import { oklch } from './oklch.js'
5
+ import { toColor } from './parse.js'
6
+ import { componentsIn } from './spaces.js'
7
+ import { srgb } from './srgb.js'
8
+ import type { Color, Space } from './types.js'
9
+ import { unknownName } from './unknown-name.js'
10
+ import { xyzD65 } from './xyz-d65.js'
11
+
12
+ /**
13
+ * A contrast measure: `(fg, bg) => number`, plus what a caller needs to use it
14
+ * without knowing the algorithm. Any plain `(fg, bg) => number` also works
15
+ * wherever a `ContrastFn` is taken; the extra fields are optional.
16
+ */
17
+ export interface ContrastFn {
18
+ (fg: Color, bg: Color): number
19
+ /** A label for messages and output, for example `wcag2`. */
20
+ readonly algorithm?: string
21
+ /** Named targets, for example `{ AA: 4.5 }`, which `meets` accepts as a level. */
22
+ readonly thresholds?: Readonly<Record<string, number>>
23
+ /** `symmetric` when swapping fg and bg gives the same number; `signed` when the sign says which is lighter. */
24
+ readonly polarity?: 'symmetric' | 'signed'
25
+ }
26
+
27
+ /** The contrast algorithms built in. */
28
+ export type ContrastAlgorithm = 'wcag2'
29
+
30
+ /** Either a built-in name or a {@link ContrastFn}. */
31
+ export type ContrastOption = ContrastAlgorithm | ContrastFn
32
+
33
+ type ColorInput = Color | string
34
+
35
+ /** Relative luminance (WCAG 2): the Y of the color in XYZ-D65, with white at 1. */
36
+ function luminance(componentsIn: ComponentsIn, color: Color): number {
37
+ return Math.max(0, componentsIn(color, 'xyz-d65')[1] as number)
38
+ }
39
+
40
+ /** Lays a translucent color over `bg`, in `srgb`, the way a browser paints it. */
41
+ function flatten(componentsIn: ComponentsIn, fg: Color, bg: Color): Color {
42
+ const alpha = fg.alpha === null ? 0 : (fg.alpha ?? 1)
43
+ if (alpha >= 1) return fg
44
+ const f = componentsIn(fg, 'srgb')
45
+ const b = componentsIn(bg, 'srgb')
46
+ return { colorSpace: 'srgb', components: f.map((x, i) => (x as number) * alpha + (b[i] as number) * (1 - alpha)) }
47
+ }
48
+
49
+ /**
50
+ * WCAG 2 contrast ratio, 1 to 21. A translucent `fg` is laid over `bg` first;
51
+ * `bg` is taken as opaque.
52
+ */
53
+ export const wcag2: ContrastFn = /* @__PURE__ */ wcag2Over(componentsIn)
54
+
55
+ /** WCAG 2 over the spaces `componentsIn` knows, which must include `srgb` and `xyz-d65`. */
56
+ function wcag2Over(componentsIn: ComponentsIn): ContrastFn {
57
+ return Object.assign(
58
+ (fg: Color, bg: Color): number => {
59
+ const a = luminance(componentsIn, flatten(componentsIn, fg, bg)) + 0.05
60
+ const b = luminance(componentsIn, bg) + 0.05
61
+ return a > b ? a / b : b / a
62
+ },
63
+ {
64
+ algorithm: 'wcag2',
65
+ thresholds: { AA: 4.5, 'AA-large': 3, AAA: 7, 'AAA-large': 4.5, 'non-text': 3 },
66
+ polarity: 'symmetric'
67
+ } as const
68
+ )
69
+ }
70
+
71
+ /** The measure `algo` names: `wcag2` or a function; anything else is an unknown name. */
72
+ export function pick(algo: ContrastOption): ContrastFn {
73
+ return pickFrom(wcag2, algo)
74
+ }
75
+
76
+ /** {@link pick}, with `builtin` as what `wcag2` names. */
77
+ function pickFrom(builtin: ContrastFn, algo: ContrastOption): ContrastFn {
78
+ if (algo === 'wcag2') return builtin
79
+ if (typeof algo !== 'function') throw unknownName('contrast algorithm', algo, ['wcag2'])
80
+ return algo
81
+ }
82
+
83
+ /** What a narrow contrast helper works over: its spaces and its own `wcag2`. */
84
+ interface ContrastKit {
85
+ componentsIn: ComponentsIn
86
+ wcag2: ContrastFn
87
+ }
88
+
89
+ /** A {@link ContrastKit} over `needed` and `spaces`. */
90
+ function contrastKit(needed: Space[], spaces: Space[]): ContrastKit {
91
+ const over = componentsOver([...needed, ...spaces])
92
+ return { componentsIn: over, wcag2: wcag2Over(over) }
93
+ }
94
+
95
+ /**
96
+ * Contrast of `fg` on `bg`. Colors may be CSS strings. The default is WCAG 2;
97
+ * pass a {@link ContrastFn} to use another measure (see the APCA recipe).
98
+ */
99
+ export function contrast(fg: ColorInput, bg: ColorInput, algo: ContrastOption = 'wcag2'): number {
100
+ return pick(algo)(toColor(fg), toColor(bg))
101
+ }
102
+
103
+ /**
104
+ * Builds {@link contrast} over the spaces given, plus `srgb` and `xyz-d65`,
105
+ * which WCAG 2 works in, so a bundle contains only those. It takes color
106
+ * objects, not strings, so it does not carry `parse`. A color in a space not
107
+ * given throws, naming that space.
108
+ */
109
+ export function createContrast(...spaces: Space[]): (fg: Color, bg: Color, algo?: ContrastOption) => number {
110
+ const kit = contrastKit([srgb, xyzD65], spaces)
111
+ return (fg, bg, algo = 'wcag2') => pickFrom(kit.wcag2, algo)(fg, bg)
112
+ }
113
+
114
+ /**
115
+ * True when `fg` on `bg` reaches `level`: a number, or a name from the
116
+ * algorithm's thresholds (WCAG 2: `AA`, `AA-large`, `AAA`, `AAA-large`,
117
+ * `non-text`). A signed measure such as APCA is compared by magnitude.
118
+ */
119
+ export function meets(fg: ColorInput, bg: ColorInput, level: number | string, algo: ContrastOption = 'wcag2'): boolean {
120
+ const fn = pick(algo)
121
+ const target = levelOf(fn, level)
122
+ return Math.abs(fn(toColor(fg), toColor(bg))) >= target
123
+ }
124
+
125
+ /**
126
+ * Builds {@link meets} over the spaces given, plus `srgb` and `xyz-d65`, which
127
+ * WCAG 2 works in, so a bundle contains only those. It takes color objects,
128
+ * not strings, so it does not carry `parse`. A color in a space not given
129
+ * throws, naming that space.
130
+ */
131
+ export function createMeets(
132
+ ...spaces: Space[]
133
+ ): (fg: Color, bg: Color, level: number | string, algo?: ContrastOption) => boolean {
134
+ const kit = contrastKit([srgb, xyzD65], spaces)
135
+ return (fg, bg, level, algo = 'wcag2') => {
136
+ const fn = pickFrom(kit.wcag2, algo)
137
+ return Math.abs(fn(fg, bg)) >= levelOf(fn, level)
138
+ }
139
+ }
140
+
141
+ /** The contrast `level` stands for under `fn`: the number itself, or one of its thresholds by name. */
142
+ function levelOf(fn: ContrastFn, level: number | string): number {
143
+ const named = typeof level === 'number' ? level : fn.thresholds?.[level]
144
+ // A name that is not the algorithm's own (`constructor`) finds something that is not a number.
145
+ const target = typeof named === 'number' ? named : undefined
146
+ if (target === undefined) throw unknownName('contrast level', level, Object.keys(fn.thresholds ?? {}))
147
+ return target
148
+ }
149
+
150
+ /** Options for `findContrast` and the function `createFindContrast` returns. */
151
+ export interface FindContrastOptions {
152
+ /** Hue of the result, in OKLCH degrees. Default: the hue of `bg`. */
153
+ hue?: number
154
+ /** Chroma of the result, in OKLCH. Default 0. The result is mapped into the gamut. */
155
+ chroma?: number
156
+ /** Which way to look first from `bg`. Default: darker on a light `bg`, lighter on a dark one. */
157
+ direction?: 'lighter' | 'darker'
158
+ /** Default `wcag2`. */
159
+ contrast?: ContrastOption
160
+ }
161
+
162
+ /** What `findContrast` returns: the color it found and whether it reached the target. */
163
+ export interface FoundContrast {
164
+ /** The color, in `srgb` and inside the gamut. */
165
+ color: Color
166
+ /** The contrast it achieves against `bg`, as the algorithm reports it. */
167
+ contrast: number
168
+ /** False when no lightness reaches `target`; `color` is then the best one found. */
169
+ met: boolean
170
+ }
171
+
172
+ const STEPS = 32
173
+
174
+ /**
175
+ * Finds the color closest in lightness to `bg` (OKLCH L, at the given hue and
176
+ * chroma) that reaches `target` contrast against it, by binary search. It looks
177
+ * the preferred way first, then the other. The returned `contrast` is measured
178
+ * on the final, gamut-mapped color, and `met` says whether it reaches `target`.
179
+ */
180
+ export function findContrast(bg: ColorInput, target: number, options: FindContrastOptions = {}): FoundContrast {
181
+ return findContrastWith({ componentsIn, wcag2 }, toColor(bg), target, options)
182
+ }
183
+
184
+ /**
185
+ * Builds {@link findContrast} over the spaces given, plus `srgb`, `xyz-d65`,
186
+ * `oklch` and `oklab`, which WCAG 2 and gamut mapping work in, so a bundle
187
+ * contains only those. It takes a color object, not a string, so it does not
188
+ * carry `parse`. A color in a space not given throws, naming that space.
189
+ */
190
+ export function createFindContrast(
191
+ ...spaces: Space[]
192
+ ): (bg: Color, target: number, options?: FindContrastOptions) => FoundContrast {
193
+ const kit = contrastKit([srgb, xyzD65, oklch, oklab], spaces)
194
+ return (bg, target, options = {}) => findContrastWith(kit, bg, target, options)
195
+ }
196
+
197
+ function findContrastWith(
198
+ { componentsIn, wcag2 }: ContrastKit,
199
+ back: Color,
200
+ target: number,
201
+ options: FindContrastOptions
202
+ ): FoundContrast {
203
+ const fn = pickFrom(wcag2, options.contrast ?? 'wcag2')
204
+ const lch = componentsIn(back, 'oklch')
205
+ const l0 = lch[0] as number
206
+ const hue = options.hue ?? (lch[2] as number)
207
+ const chroma = options.chroma ?? 0
208
+ const at = (l: number): FoundContrast => {
209
+ const color = toGamutWith(componentsIn, { colorSpace: 'oklch', components: [l, chroma, hue] }, {})
210
+ const value = fn(color, back)
211
+ return { color, contrast: value, met: Math.abs(value) >= target }
212
+ }
213
+ const first = options.direction ?? (l0 > 0.5 ? 'darker' : 'lighter')
214
+ let best: FoundContrast | undefined
215
+ for (const side of [first, first === 'darker' ? 'lighter' : 'darker']) {
216
+ const far = side === 'darker' ? 0 : 1
217
+ const end = at(far)
218
+ if (!end.met) {
219
+ if (!best || Math.abs(end.contrast) > Math.abs(best.contrast)) best = end
220
+ continue
221
+ }
222
+ // `lo` is on the bg side and fails or is the bg itself; `hi` always passes.
223
+ let [lo, hi] = [l0, far]
224
+ let found = end
225
+ for (let i = 0; i < STEPS; i++) {
226
+ const mid = (lo + hi) / 2
227
+ const candidate = at(mid)
228
+ if (candidate.met) {
229
+ hi = mid
230
+ found = candidate
231
+ } else lo = mid
232
+ }
233
+ return found
234
+ }
235
+ return { ...(best as FoundContrast), met: false }
236
+ }
237
+
238
+ /** Options for `onColor` and the function `createOnColor` returns. */
239
+ export interface OnColorOptions {
240
+ /** The contrast the text needs. Default 4.5 for WCAG 2. */
241
+ target?: number
242
+ /** How much of the background's hue the dark text carries, as OKLCH chroma. Default 0.03. */
243
+ chroma?: number
244
+ /** Default `wcag2`. */
245
+ contrast?: ContrastOption
246
+ }
247
+
248
+ /**
249
+ * The text color for `bg`, by the Radix rule: white, unless white's contrast
250
+ * on `bg` is under `target`, then a near-black tinted with the hue of `bg`
251
+ * (OKLCH lightness 0.2). When neither reaches `target`, whichever contrasts more.
252
+ */
253
+ export function onColor(bg: ColorInput, options: OnColorOptions = {}): Color {
254
+ return onColorWith({ componentsIn, wcag2 }, toColor(bg), options)
255
+ }
256
+
257
+ /**
258
+ * Builds {@link onColor} over the spaces given, plus `srgb`, `xyz-d65`,
259
+ * `oklch` and `oklab`, which WCAG 2 and gamut mapping work in, so a bundle
260
+ * contains only those. It takes a color object, not a string, so it does not
261
+ * carry `parse`. A color in a space not given throws, naming that space.
262
+ */
263
+ export function createOnColor(...spaces: Space[]): (bg: Color, options?: OnColorOptions) => Color {
264
+ const kit = contrastKit([srgb, xyzD65, oklch, oklab], spaces)
265
+ return (bg, options = {}) => onColorWith(kit, bg, options)
266
+ }
267
+
268
+ function onColorWith({ componentsIn, wcag2 }: ContrastKit, back: Color, options: OnColorOptions): Color {
269
+ const fn = pickFrom(wcag2, options.contrast ?? 'wcag2')
270
+ const target = options.target ?? 4.5
271
+ const white: Color = { colorSpace: 'srgb', components: [1, 1, 1] }
272
+ const whiteScore = Math.abs(fn(white, back))
273
+ if (whiteScore >= target) return white
274
+ const hue = componentsIn(back, 'oklch')[2] as number
275
+ const dark = toGamutWith(componentsIn, { colorSpace: 'oklch', components: [0.2, options.chroma ?? 0.03, hue] }, {})
276
+ return Math.abs(fn(dark, back)) >= whiteScore ? dark : white
277
+ }
@@ -0,0 +1,43 @@
1
+ import type { Color, ColorSpaceId, Component, Space, Vec3 } from './types.js'
2
+ import { unknownName } from './unknown-name.js'
3
+
4
+ /** Converts bare components between two spaces, through the XYZ-D65 hub. */
5
+ export function convertComponents(from: Space, to: Space, c: Vec3): Vec3 {
6
+ return from === to ? c : to.fromXyz(from.toXyz(c))
7
+ }
8
+
9
+ /** The components of a color in another space: a copy in the same space, else `none` becomes 0. */
10
+ export type ComponentsIn = (color: Color, to: ColorSpaceId) => Component[]
11
+
12
+ /** {@link ComponentsIn} over the spaces given. */
13
+ export function componentsOver(spaces: Space[]): ComponentsIn {
14
+ const byId = new Map(spaces.map((s) => [s.id, s]))
15
+ const find = (id: ColorSpaceId): Space => {
16
+ const space = byId.get(id)
17
+ if (!space) throw unknownName('color space', id, [...byId.keys()])
18
+ return space
19
+ }
20
+ return (color, to) => {
21
+ if (color.colorSpace === to) return [...color.components]
22
+ const from = find(color.colorSpace)
23
+ const c = color.components
24
+ // Different ids, so different spaces: straight through the hub, `none` as 0.
25
+ return find(to).fromXyz(from.toXyz([c[0] ?? 0, c[1] ?? 0, c[2] ?? 0]))
26
+ }
27
+ }
28
+
29
+ /** `convert` over the spaces `componentsIn` knows: the color in `to`, keeping its alpha. */
30
+ export function convertWith(componentsIn: ComponentsIn, color: Color, to: ColorSpaceId): Color {
31
+ const result: Color = { colorSpace: to, components: componentsIn(color, to) }
32
+ if (color.alpha !== undefined) result.alpha = color.alpha
33
+ return result
34
+ }
35
+
36
+ /**
37
+ * Builds `convert` over just the spaces given, so a bundle contains only those.
38
+ * `none` components become 0 when the space changes.
39
+ */
40
+ export function createConverter(...spaces: Space[]): (color: Color, to: ColorSpaceId) => Color {
41
+ const componentsIn = componentsOver(spaces)
42
+ return (color, to) => convertWith(componentsIn, color, to)
43
+ }
@@ -0,0 +1,9 @@
1
+ import { p3ToXyz, xyzToP3 } from './matrices.js'
2
+ import { rgbSpace } from './rgb.js'
3
+ import { identity } from './transfer.js'
4
+
5
+ /**
6
+ * The CSS `display-p3-linear` space: Display P3 primaries without a transfer curve. Components are red, green and blue in 0 to 1.
7
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
8
+ */
9
+ export const displayP3Linear = rgbSpace('display-p3-linear', identity, identity, p3ToXyz, xyzToP3)
@@ -0,0 +1,9 @@
1
+ import { p3ToXyz, xyzToP3 } from './matrices.js'
2
+ import { rgbSpace } from './rgb.js'
3
+ import { srgbToGamma, srgbToLinear } from './transfer.js'
4
+
5
+ /**
6
+ * The CSS `display-p3` space. Components are red, green and blue in 0 to 1, with the sRGB transfer curve.
7
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
8
+ */
9
+ export const displayP3 = rgbSpace('display-p3', srgbToLinear, srgbToGamma, p3ToXyz, xyzToP3)
@@ -0,0 +1,158 @@
1
+ import type { HueMethod } from './mix.js'
2
+ import { COLOR_SPACES, FUNCTIONS, fail, parse, VALUES_NEEDING_RESOLVE } from './parse.js'
3
+ import type { Color, ColorSpaceId } from './types.js'
4
+
5
+ /** A color that needs a context to become concrete: see {@link resolve}. */
6
+ export type ColorExpr =
7
+ | Color
8
+ | { type: 'currentcolor' }
9
+ | { type: 'system'; name: string }
10
+ | { type: 'light-dark'; light: ColorExpr; dark: ColorExpr }
11
+ | { type: 'contrast-color'; color: ColorExpr }
12
+ | {
13
+ type: 'color-mix'
14
+ space: ColorSpaceId
15
+ hue?: HueMethod
16
+ /** Exactly two. `percent` is 0 to 100, absent when not written. */
17
+ items: { color: ColorExpr; percent?: number }[]
18
+ }
19
+ | {
20
+ /** `rgb(from red r g b)` and the like. The channels are kept as written, for `resolve` to evaluate. */
21
+ type: 'relative'
22
+ /** The function name: `rgb`, `hsl`, `hwb`, `lab`, `lch`, `oklab`, `oklch` or `color`. */
23
+ fn: string
24
+ space: ColorSpaceId
25
+ from: ColorExpr
26
+ channels: [string, string, string]
27
+ alpha?: string
28
+ }
29
+
30
+ const INTERPOLATION_SPACES: Record<string, ColorSpaceId> = {
31
+ __proto__: null as never,
32
+ ...COLOR_SPACES,
33
+ hsl: 'hsl',
34
+ hwb: 'hwb',
35
+ lab: 'lab',
36
+ lch: 'lch',
37
+ oklab: 'oklab',
38
+ oklch: 'oklch'
39
+ }
40
+ const HUE_METHODS = new Set(['shorter', 'longer', 'increasing', 'decreasing'])
41
+ const RELATIVE = new Set(['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'color'])
42
+ const PERCENT = /^[+-]?(?:\d+\.?\d*|\.\d+)%$/
43
+
44
+ /** Splits at top-level commas, or at top-level whitespace with `/` as its own item. */
45
+ function split(text: string, commas: boolean, input: string): string[] {
46
+ const out: string[] = []
47
+ let depth = 0
48
+ let current = ''
49
+ const flush = (): void => {
50
+ if (commas || current) out.push(current.trim())
51
+ current = ''
52
+ }
53
+ for (const ch of text) {
54
+ if (ch === '(') depth++
55
+ else if (ch === ')') depth--
56
+ if (depth === 0 && (commas ? ch === ',' : /\s|\//.test(ch))) {
57
+ flush()
58
+ if (ch === '/') out.push('/')
59
+ } else current += ch
60
+ }
61
+ if (depth !== 0) return fail(input, 'unbalanced parentheses')
62
+ flush()
63
+ return out
64
+ }
65
+
66
+ /** Splits `name(body)`, or returns undefined when the text is not one call that ends the text. */
67
+ function call(text: string): [string, string] | undefined {
68
+ const open = text.indexOf('(')
69
+ if (open < 1 || !text.endsWith(')') || !/^[a-z-]+$/.test(text.slice(0, open))) return undefined
70
+ let depth = 0
71
+ for (let i = open; i < text.length; i++) {
72
+ if (text[i] === '(') depth++
73
+ else if (text[i] === ')' && --depth === 0 && i < text.length - 1) return undefined
74
+ }
75
+ return [text.slice(0, open), text.slice(open + 1, -1)]
76
+ }
77
+
78
+ function colorMix(body: string, input: string): ColorExpr {
79
+ const [head, ...rest] = split(body, true, input)
80
+ const words = head.split(/\s+/)
81
+ if (words[0] !== 'in' || !INTERPOLATION_SPACES[words[1]])
82
+ return fail(input, 'color-mix() needs "in <space>"', words[1])
83
+ const space = INTERPOLATION_SPACES[words[1]]
84
+ let hue: HueMethod | undefined
85
+ if (words.length > 2) {
86
+ if (words.length !== 4 || words[3] !== 'hue' || !HUE_METHODS.has(words[2])) {
87
+ return fail(input, 'invalid hue interpolation', words.slice(2).join(' '))
88
+ }
89
+ hue = words[2] as HueMethod
90
+ }
91
+ if (rest.length !== 2) return fail(input, `color-mix() needs two colors, got ${rest.length}`)
92
+ const items = rest.map((text) => {
93
+ const parts = split(text, false, input)
94
+ const percents = parts.filter((p) => PERCENT.test(p))
95
+ if (parts.length - percents.length !== 1 || percents.length > 1)
96
+ return fail(input, 'invalid color-mix() item', text)
97
+ const percent = percents.length === 1 ? Number.parseFloat(percents[0]) : undefined
98
+ if (percent !== undefined && percent < 0) return fail(input, 'negative percentage', text)
99
+ const color = parseExpr(parts.find((p) => !PERCENT.test(p)) as string)
100
+ return percent === undefined ? { color } : { color, percent }
101
+ })
102
+ return hue === undefined ? { type: 'color-mix', space, items } : { type: 'color-mix', space, hue, items }
103
+ }
104
+
105
+ function relative(name: string, words: string[], input: string): ColorExpr {
106
+ const from = words[1] === undefined ? fail(input, 'missing origin color') : parseExpr(words[1])
107
+ let space: ColorSpaceId
108
+ let at = 2
109
+ const fn = name === 'rgba' ? 'rgb' : name === 'hsla' ? 'hsl' : name
110
+ if (fn === 'color') {
111
+ const id = COLOR_SPACES[words[2]]
112
+ if (!id) return fail(input, 'unknown color space', words[2])
113
+ space = id
114
+ at = 3
115
+ } else space = FUNCTIONS[fn].space
116
+ const channels = words.slice(at, at + 3)
117
+ const tail = words.slice(at + 3)
118
+ if (channels.length !== 3) return fail(input, `expected 3 components, got ${channels.length}`)
119
+ if (tail.length > 0 && (tail[0] !== '/' || tail.length !== 2))
120
+ return fail(input, 'expected "/ alpha" after the components')
121
+ const node: Extract<ColorExpr, { type: 'relative' }> = {
122
+ type: 'relative',
123
+ fn,
124
+ space,
125
+ from,
126
+ channels: channels as [string, string, string]
127
+ }
128
+ if (tail.length === 2) node.alpha = tail[1]
129
+ return node
130
+ }
131
+
132
+ /**
133
+ * Parses any CSS Color 4 or 5 color into a {@link ColorExpr}: everything
134
+ * `parse` takes, plus `currentcolor`, system colors, `light-dark()`,
135
+ * `color-mix()`, `contrast-color()` and relative colors such as
136
+ * `oklch(from #3b82f6 calc(l + 0.1) c h)`. A concrete color comes back as a
137
+ * plain {@link Color}; the rest are nodes that {@link resolve} turns into one.
138
+ */
139
+ export function parseExpr(input: string): ColorExpr {
140
+ const text = input.trim().toLowerCase()
141
+ if (text === 'currentcolor') return { type: 'currentcolor' }
142
+ if (VALUES_NEEDING_RESOLVE.test(text)) return { type: 'system', name: text }
143
+ const parts = call(text)
144
+ if (!parts) return parse(text)
145
+ const [name, body] = parts
146
+ if (name === 'color-mix') return colorMix(body, input)
147
+ if (name === 'light-dark') {
148
+ const list = split(body, true, input)
149
+ if (list.length !== 2) return fail(input, `light-dark() needs two colors, got ${list.length}`)
150
+ return { type: 'light-dark', light: parseExpr(list[0]), dark: parseExpr(list[1]) }
151
+ }
152
+ if (name === 'contrast-color') return { type: 'contrast-color', color: parseExpr(body) }
153
+ if (RELATIVE.has(name)) {
154
+ const words = split(body, false, input)
155
+ if (words[0] === 'from') return relative(name, words, input)
156
+ }
157
+ return parse(text)
158
+ }