color-map 2.2.0 → 2.3.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 (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 +94 -0
  5. package/README.md +71 -8
  6. package/bin/cli-CNBftDBE.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 +29 -0
  268. package/package.json +276 -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 +2 -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,19 @@
1
+ import { multiply } from './math.js'
2
+ import { lmsToOklab, lmsToXyz, oklabToLms, xyzToLms } from './matrices.js'
3
+ import type { Space } from './types.js'
4
+
5
+ /**
6
+ * The CSS `oklab` space. Components are lightness in 0 to 1, then the a and b axes, which stay within about -0.4 to 0.4 for real colors.
7
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
8
+ */
9
+ export const oklab: Space = {
10
+ id: 'oklab',
11
+ toXyz: (c) => {
12
+ const v = multiply(oklabToLms, c[0], c[1], c[2])
13
+ return multiply(lmsToXyz, v[0] ** 3, v[1] ** 3, v[2] ** 3)
14
+ },
15
+ fromXyz: (xyz) => {
16
+ const v = multiply(xyzToLms, xyz[0], xyz[1], xyz[2])
17
+ return multiply(lmsToOklab, Math.cbrt(v[0]), Math.cbrt(v[1]), Math.cbrt(v[2]))
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ import { oklab } from './oklab.js'
2
+ import { polarSpace } from './polar.js'
3
+
4
+ /**
5
+ * The CSS `oklch` space, the polar form of `oklab`. Components are lightness in 0 to 1, chroma (about 0 to 0.4) and hue in degrees.
6
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
7
+ */
8
+ export const oklch = polarSpace('oklch', oklab)
@@ -0,0 +1,355 @@
1
+ import { namedColor } from './named.js'
2
+ import type { Color, ColorSpaceId, Component } from './types.js'
3
+
4
+ export const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/
5
+ // Tables keyed by text a caller wrote have no prototype (`__proto__: null`), so `constructor` or `toString` is an unknown name, not Object.prototype's.
6
+ export const UNITS: Record<string, number> = {
7
+ __proto__: null as never,
8
+ '': 1,
9
+ deg: 1,
10
+ grad: 0.9,
11
+ rad: 180 / Math.PI,
12
+ turn: 360
13
+ }
14
+
15
+ /** How one channel reads: what 100% means, and the allowed range. */
16
+ export interface Channel {
17
+ /** The value of 100%. Also the factor applied to a plain number, usually 1. */
18
+ pct: number
19
+ /** A plain number is divided by this (255 for sRGB channels). */
20
+ div?: number
21
+ min?: number
22
+ max?: number
23
+ hue?: boolean
24
+ }
25
+
26
+ export function fail(input: string, what: string, token?: string): never {
27
+ throw new Error(`color-map: cannot parse "${input}": ${what}${token === undefined ? '' : ` "${token}"`}`)
28
+ }
29
+
30
+ export function channel(token: string, spec: Channel, input: string): Component {
31
+ if (token === 'none') return null
32
+ const m = NUMBER.exec(token)
33
+ if (!m) return fail(input, 'invalid value', token)
34
+ const n = Number(m[0])
35
+ const rest = token.slice(m[0].length)
36
+ let value: number
37
+ if (spec.hue) {
38
+ const unit = UNITS[rest]
39
+ if (unit === undefined) return fail(input, 'invalid angle', token)
40
+ return n * unit
41
+ }
42
+ if (rest === '%') value = (n * spec.pct) / 100
43
+ else if (rest === '') value = n / (spec.div ?? 1)
44
+ else return fail(input, 'invalid value', token)
45
+ return Math.min(spec.max ?? Number.POSITIVE_INFINITY, Math.max(spec.min ?? Number.NEGATIVE_INFINITY, value))
46
+ }
47
+
48
+ export const SRGB: Channel = { pct: 1, div: 255 }
49
+ const PCT100: Channel = { pct: 100, min: 0 }
50
+ export const UNIT: Channel = { pct: 1 }
51
+ export const ALPHA: Channel = { pct: 1, min: 0, max: 1 }
52
+ const HUE: Channel = { pct: 0, hue: true }
53
+
54
+ export const FUNCTIONS: Record<string, { space: ColorSpaceId; channels: [Channel, Channel, Channel] }> = {
55
+ __proto__: null as never,
56
+ rgb: { space: 'srgb', channels: [SRGB, SRGB, SRGB] },
57
+ hsl: { space: 'hsl', channels: [HUE, PCT100, PCT100] },
58
+ hwb: { space: 'hwb', channels: [HUE, PCT100, PCT100] },
59
+ lab: { space: 'lab', channels: [{ pct: 100, min: 0, max: 100 }, { pct: 125 }, { pct: 125 }] },
60
+ lch: { space: 'lch', channels: [{ pct: 100, min: 0, max: 100 }, { pct: 150, min: 0 }, HUE] },
61
+ oklab: { space: 'oklab', channels: [{ pct: 1, min: 0, max: 1 }, { pct: 0.4 }, { pct: 0.4 }] },
62
+ oklch: { space: 'oklch', channels: [{ pct: 1, min: 0, max: 1 }, { pct: 0.4, min: 0 }, HUE] }
63
+ }
64
+
65
+ /** The spaces `color()` names, by the name it uses: their own, and `xyz` for `xyz-d65`. */
66
+ export const COLOR_SPACES: Record<string, ColorSpaceId> = { __proto__: null as never, xyz: 'xyz-d65' }
67
+ for (const id of [
68
+ 'srgb',
69
+ 'srgb-linear',
70
+ 'display-p3',
71
+ 'display-p3-linear',
72
+ 'a98-rgb',
73
+ 'prophoto-rgb',
74
+ 'rec2020',
75
+ 'xyz-d50',
76
+ 'xyz-d65'
77
+ ] as const)
78
+ COLOR_SPACES[id] = id
79
+
80
+ /** `currentcolor` and the CSS Color 4 system colors (canvas, canvastext, linktext, buttonface, ...). */
81
+ export const VALUES_NEEDING_RESOLVE =
82
+ /^(?:currentcolor|(?:canvas|field|highlight|selecteditem|mark|accentcolor)(?:text)?|(?:link|visited|active|button|gray)text|button(?:face|border))$/
83
+
84
+ function hex(input: string): Color {
85
+ const digits = input.slice(1)
86
+ if (!/^(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/.test(digits)) return fail(input, 'invalid hex color')
87
+ const full = digits.length < 5 ? [...digits].map((d) => d + d).join('') : digits
88
+ const byte = (i: number): number => Number.parseInt(full.slice(i * 2, i * 2 + 2), 16) / 255
89
+ const color: Color = { colorSpace: 'srgb', components: [byte(0), byte(1), byte(2)] }
90
+ if (full.length === 8) color.alpha = byte(3)
91
+ return color
92
+ }
93
+
94
+ const ALIASES: Record<string, string> = { __proto__: null as never, rgba: 'rgb', hsla: 'hsl' }
95
+
96
+ function functional(rawName: string, body: string, input: string): Color {
97
+ const name = ALIASES[rawName] ?? rawName
98
+ const [main, alphaToken, ...extra] = body.split('/').map((s) => s.trim())
99
+ if (extra.length > 0) return fail(input, 'more than one "/"')
100
+ const legacy = main.includes(',')
101
+ const tokens = (legacy ? main.split(',') : main.split(/\s+/)).map((s) => s.trim()).filter(Boolean)
102
+ let alpha = alphaToken
103
+ if (legacy) {
104
+ if (name !== 'rgb' && name !== 'hsl') return fail(input, 'the comma syntax is only for rgb() and hsl()')
105
+ if (alpha !== undefined) return fail(input, 'cannot mix "," and "/"')
106
+ if (tokens.length === 4) alpha = tokens.pop() as string
107
+ if (tokens.includes('none')) return fail(input, '"none" is not allowed in the comma syntax')
108
+ }
109
+ let space: ColorSpaceId
110
+ let channels: [Channel, Channel, Channel]
111
+ if (name === 'color') {
112
+ const id = COLOR_SPACES[tokens.shift() ?? '']
113
+ if (!id) return fail(input, 'unknown color space', main.split(/\s+/)[0])
114
+ space = id
115
+ channels = [UNIT, UNIT, UNIT]
116
+ } else {
117
+ const fn = FUNCTIONS[name]
118
+ if (!fn) return fail(input, 'unknown function', rawName)
119
+ ;({ space, channels } = fn)
120
+ }
121
+ if (tokens.length !== 3) return fail(input, `expected 3 components, got ${tokens.length}`)
122
+ if (legacy) {
123
+ const percents = tokens.map((t) => t.endsWith('%'))
124
+ const ok = space === 'hsl' ? !percents[0] && percents[1] && percents[2] : percents.every((p) => p === percents[0])
125
+ if (!ok) return fail(input, 'mixed numbers and percentages', tokens.join(', '))
126
+ }
127
+ const color: Color = {
128
+ colorSpace: space,
129
+ components: tokens.map((t, i) => channel(t, channels[i], input))
130
+ }
131
+ if (alpha !== undefined) color.alpha = channel(alpha, ALPHA, input)
132
+ return color
133
+ }
134
+
135
+ /**
136
+ * Parses a CSS Color 4 color: hex, named colors, `transparent`, `rgb()`,
137
+ * `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()` and `color()`.
138
+ * `none` becomes `null`. Values that need a context (`currentcolor`, system
139
+ * colors) throw, because nothing here can resolve them.
140
+ */
141
+ export function parse(input: string): Color {
142
+ const text = input.trim().toLowerCase()
143
+ return scan(text) ?? parseGeneric(input, text)
144
+ }
145
+
146
+ /**
147
+ * The regex-based parser: every accepted form, and every error message. `parse`
148
+ * tries {@link scan} first, which takes only the plain valid forms and leaves
149
+ * the rest, including every error, to this.
150
+ */
151
+ export function parseGeneric(input: string, text: string): Color {
152
+ if (text.startsWith('#')) return hex(text)
153
+ const fn = /^([a-z]+)\((.*)\)$/s.exec(text)
154
+ if (fn) return functional(fn[1], fn[2], input)
155
+ if (text === 'transparent') return { colorSpace: 'srgb', components: [0, 0, 0], alpha: 0 }
156
+ const named = namedColor(text)
157
+ if (named !== undefined) {
158
+ return { colorSpace: 'srgb', components: [(named >> 16) / 255, ((named >> 8) & 255) / 255, (named & 255) / 255] }
159
+ }
160
+ if (VALUES_NEEDING_RESOLVE.test(text)) return fail(input, 'needs a context to resolve', text)
161
+ return fail(input, 'not a color')
162
+ }
163
+
164
+ /** A color as given, or parsed with {@link parse} when it is a string. */
165
+ export const toColor = (c: Color | string): Color => (typeof c === 'string' ? parse(c) : c)
166
+
167
+ // --- The scanner: a char-code fast path for hex and the functional forms ---
168
+
169
+ const isSpace = (c: number): boolean => c === 32 || c === 9 || c === 10 || c === 13 || c === 12
170
+ const isDigit = (c: number): boolean => c >= 48 && c <= 57
171
+ const hexValue = (c: number): number => (c >= 48 && c <= 57 ? c - 48 : c >= 97 && c <= 102 ? c - 87 : -1)
172
+
173
+ // Scanner state, shared across calls: `parse` is synchronous and does not re-enter.
174
+ let at = 0
175
+ let percent = false
176
+
177
+ function scanHex(text: string): Color | undefined {
178
+ const len = text.length
179
+ if (len !== 4 && len !== 5 && len !== 7 && len !== 9) return undefined
180
+ const short = len < 6
181
+ const count = short ? len - 1 : (len - 1) / 2
182
+ const bytes: number[] = []
183
+ for (let i = 0; i < count; i++) {
184
+ let value: number
185
+ if (short) {
186
+ const d = hexValue(text.charCodeAt(1 + i))
187
+ value = d * 17
188
+ if (d < 0) return undefined
189
+ } else {
190
+ const hi = hexValue(text.charCodeAt(1 + i * 2))
191
+ const lo = hexValue(text.charCodeAt(2 + i * 2))
192
+ if (hi < 0 || lo < 0) return undefined
193
+ value = hi * 16 + lo
194
+ }
195
+ bytes.push(value / 255)
196
+ }
197
+ const color: Color = { colorSpace: 'srgb', components: [bytes[0], bytes[1], bytes[2]] }
198
+ if (count === 4) color.alpha = bytes[3]
199
+ return color
200
+ }
201
+
202
+ /** Skips whitespace from `at` up to `end`. */
203
+ function skip(text: string, end: number): void {
204
+ while (at < end && isSpace(text.charCodeAt(at))) at++
205
+ }
206
+
207
+ /** The end of the token that starts at `at`: it runs to whitespace, a comma, a slash or `end`. */
208
+ function tokenEnd(text: string, end: number): number {
209
+ let j = at
210
+ while (j < end) {
211
+ const c = text.charCodeAt(j)
212
+ if (isSpace(c) || c === 44 || c === 47) break
213
+ j++
214
+ }
215
+ return j
216
+ }
217
+
218
+ /**
219
+ * Reads the token at `at` as {@link channel} would, or returns `undefined` when
220
+ * it is anything but a plain valid one. Leaves `at` after the token.
221
+ */
222
+ function scanChannel(text: string, end: number, spec: Channel): Component | undefined {
223
+ const s = at
224
+ const e = tokenEnd(text, end)
225
+ at = e
226
+ if (e === s) return undefined
227
+ if (e - s === 4 && text.startsWith('none', s)) return null
228
+ let j = s
229
+ let c = text.charCodeAt(j)
230
+ if (c === 43 || c === 45) c = text.charCodeAt(++j)
231
+ let digits = 0
232
+ while (isDigit(c)) {
233
+ c = text.charCodeAt(++j)
234
+ digits++
235
+ }
236
+ if (c === 46) {
237
+ c = text.charCodeAt(++j)
238
+ while (isDigit(c)) {
239
+ c = text.charCodeAt(++j)
240
+ digits++
241
+ }
242
+ }
243
+ if (digits === 0) return undefined
244
+ if (c === 101) {
245
+ let k = j + 1
246
+ let x = text.charCodeAt(k)
247
+ if (x === 43 || x === 45) x = text.charCodeAt(++k)
248
+ if (isDigit(x)) {
249
+ while (isDigit(text.charCodeAt(k))) k++
250
+ j = k
251
+ }
252
+ }
253
+ const n = Number(text.slice(s, j))
254
+ if (spec.hue) {
255
+ const unit = j === e ? 1 : UNITS[text.slice(j, e)]
256
+ return unit === undefined ? undefined : n * unit
257
+ }
258
+ let value: number
259
+ if (j === e) value = n / (spec.div ?? 1)
260
+ else if (e - j === 1 && text.charCodeAt(j) === 37) {
261
+ percent = true
262
+ value = (n * spec.pct) / 100
263
+ } else return undefined
264
+ return Math.min(spec.max ?? Number.POSITIVE_INFINITY, Math.max(spec.min ?? Number.NEGATIVE_INFINITY, value))
265
+ }
266
+
267
+ /**
268
+ * Parses the plain valid forms (hex, and the comma or space forms of the
269
+ * functions) by char code, with no regex, split or per-token array. Anything
270
+ * else returns `undefined`, so that {@link parseGeneric} handles it and says
271
+ * why it is wrong.
272
+ */
273
+ function scan(text: string): Color | undefined {
274
+ if (text.charCodeAt(0) === 35) return scanHex(text)
275
+ const end = text.length - 1
276
+ if (text.charCodeAt(end) !== 41) return undefined
277
+ let open = 0
278
+ while (open < end) {
279
+ const c = text.charCodeAt(open)
280
+ if (c < 97 || c > 122) break
281
+ open++
282
+ }
283
+ if (open === 0 || text.charCodeAt(open) !== 40) return undefined
284
+ const rawName = text.slice(0, open)
285
+ const name = ALIASES[rawName] ?? rawName
286
+ let space: ColorSpaceId
287
+ let channels: [Channel, Channel, Channel]
288
+ at = open + 1
289
+ skip(text, end)
290
+ if (name === 'color') {
291
+ const s = at
292
+ while (at < end && !isSpace(text.charCodeAt(at))) at++
293
+ const id = COLOR_SPACES[text.slice(s, at)]
294
+ if (!id) return undefined
295
+ space = id
296
+ channels = [UNIT, UNIT, UNIT]
297
+ skip(text, end)
298
+ } else {
299
+ const fn = FUNCTIONS[name]
300
+ if (!fn) return undefined
301
+ ;({ space, channels } = fn)
302
+ }
303
+ percent = false
304
+ const c0 = scanChannel(text, end, channels[0])
305
+ if (c0 === undefined) return undefined
306
+ const p0 = percent
307
+ skip(text, end)
308
+ const comma = text.charCodeAt(at) === 44
309
+ if (comma && name !== 'rgb' && name !== 'hsl') return undefined
310
+ let alpha: Component | undefined
311
+ let c1: Component | undefined
312
+ let c2: Component | undefined
313
+ let p1 = false
314
+ let p2 = false
315
+ if (comma) {
316
+ at++
317
+ skip(text, end)
318
+ percent = false
319
+ c1 = scanChannel(text, end, channels[1])
320
+ p1 = percent
321
+ skip(text, end)
322
+ if (text.charCodeAt(at++) !== 44) return undefined
323
+ skip(text, end)
324
+ percent = false
325
+ c2 = scanChannel(text, end, channels[2])
326
+ p2 = percent
327
+ skip(text, end)
328
+ if (c0 === null || c1 === undefined || c1 === null || c2 === undefined || c2 === null) return undefined
329
+ const ok = space === 'hsl' ? !p0 && p1 && p2 : p0 === p1 && p1 === p2
330
+ if (!ok) return undefined
331
+ if (at < end) {
332
+ if (text.charCodeAt(at++) !== 44) return undefined
333
+ skip(text, end)
334
+ alpha = scanChannel(text, end, ALPHA)
335
+ if (alpha === undefined || alpha === null) return undefined
336
+ skip(text, end)
337
+ }
338
+ } else {
339
+ c1 = scanChannel(text, end, channels[1])
340
+ skip(text, end)
341
+ c2 = scanChannel(text, end, channels[2])
342
+ skip(text, end)
343
+ if (at < end) {
344
+ if (text.charCodeAt(at++) !== 47) return undefined
345
+ skip(text, end)
346
+ alpha = scanChannel(text, end, ALPHA)
347
+ if (alpha === undefined) return undefined
348
+ skip(text, end)
349
+ }
350
+ }
351
+ if (at !== end || c1 === undefined || c2 === undefined) return undefined
352
+ const color: Color = { colorSpace: space, components: [c0, c1, c2] }
353
+ if (alpha !== undefined) color.alpha = alpha
354
+ return color
355
+ }
@@ -0,0 +1,17 @@
1
+ import { normalizeHue } from './math.js'
2
+ import type { Space } from './types.js'
3
+
4
+ /** Wraps a rectangular Lab-like space as its polar (LCH-like) twin. */
5
+ export function polarSpace(id: 'lch' | 'oklch', base: Space): Space {
6
+ return {
7
+ id,
8
+ toXyz: ([l, c, h]) => {
9
+ const r = (h * Math.PI) / 180
10
+ return base.toXyz([l, c * Math.cos(r), c * Math.sin(r)])
11
+ },
12
+ fromXyz: (xyz) => {
13
+ const [l, a, b] = base.fromXyz(xyz)
14
+ return [l, Math.hypot(a, b), normalizeHue((Math.atan2(b, a) * 180) / Math.PI)]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ import { prophotoToXyzD50, xyzD50ToProphoto } from './matrices.js'
2
+ import { rgbSpace } from './rgb.js'
3
+ import { prophotoToGamma, prophotoToLinear } from './transfer.js'
4
+ import type { Space } from './types.js'
5
+ import { xyzD50 } from './xyz-d50.js'
6
+
7
+ // ProPhoto's matrices lead to XYZ-D50, so it goes through that space on the way to the hub.
8
+ const d50 = rgbSpace('prophoto-rgb', prophotoToLinear, prophotoToGamma, prophotoToXyzD50, xyzD50ToProphoto)
9
+
10
+ /**
11
+ * The CSS `prophoto-rgb` space. Components are red, green and blue in 0 to 1. It is defined against a D50 white and converted through `xyzD50`.
12
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
13
+ */
14
+ export const prophotoRgb: Space = {
15
+ id: 'prophoto-rgb',
16
+ toXyz: (c) => xyzD50.toXyz(d50.toXyz(c)),
17
+ fromXyz: (xyz) => d50.fromXyz(xyzD50.fromXyz(xyz))
18
+ }
@@ -0,0 +1,11 @@
1
+ import { rec2020ToXyz, xyzToRec2020 } from './matrices.js'
2
+ import { rgbSpace } from './rgb.js'
3
+ import { power } from './transfer.js'
4
+
5
+ const [toLinear, toGamma] = power(2.4)
6
+
7
+ /**
8
+ * The CSS `rec2020` space. Components are red, green and blue in 0 to 1, with a 2.4 power transfer curve.
9
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
10
+ */
11
+ export const rec2020 = rgbSpace('rec2020', toLinear, toGamma, rec2020ToXyz, xyzToRec2020)
@@ -0,0 +1,87 @@
1
+ import { evaluate } from './calc.js'
2
+ import { wcag2 } from './contrast.js'
3
+ import { type ColorExpr, parseExpr } from './expr.js'
4
+ import { mix } from './mix.js'
5
+ import { ALPHA, channel, FUNCTIONS, fail, UNIT } from './parse.js'
6
+ import { componentsIn } from './spaces.js'
7
+ import type { Color, Component } from './types.js'
8
+
9
+ /** What `resolve` needs from its surroundings to settle `currentcolor`, `light-dark()` and system colors. */
10
+ export interface ResolveContext {
11
+ /** What `currentcolor` means. */
12
+ currentColor?: string | ColorExpr
13
+ /** Which side `light-dark()` takes. Default `light`. */
14
+ colorScheme?: 'light' | 'dark'
15
+ /** What each system color (`canvas`, `buttontext`, ...) means, by lowercase name. */
16
+ systemColors?: Record<string, string | ColorExpr>
17
+ }
18
+
19
+ /** The names a relative color's channel keywords have, by space, one letter each. The RGB spaces use `rgb`. */
20
+ const KEYWORDS: Record<string, string> = {
21
+ __proto__: null as never,
22
+ hsl: 'hsl',
23
+ hwb: 'hwb',
24
+ lab: 'lab',
25
+ lch: 'lch',
26
+ oklab: 'lab',
27
+ oklch: 'lch',
28
+ 'xyz-d50': 'xyz',
29
+ 'xyz-d65': 'xyz'
30
+ }
31
+ const SIMPLE = /^(?:none|[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?[a-z%]*)$/
32
+
33
+ const WHITE: Color = { colorSpace: 'srgb', components: [1, 1, 1] }
34
+ const BLACK: Color = { colorSpace: 'srgb', components: [0, 0, 0] }
35
+
36
+ /** WCAG 2 choice between white and black: whichever contrasts more with the color. */
37
+ function contrastColor(color: Color): Color {
38
+ return wcag2(WHITE, color) >= wcag2(BLACK, color) ? WHITE : BLACK
39
+ }
40
+
41
+ function resolveRelative(node: Extract<ColorExpr, { type: 'relative' }>, context: ResolveContext): Color {
42
+ const origin = resolve(node.from, context)
43
+ const c = componentsIn(origin, node.space)
44
+ const scale = node.fn === 'rgb' ? 255 : 1
45
+ const names = KEYWORDS[node.space] ?? 'rgb'
46
+ const vars: Record<string, number> = { alpha: origin.alpha === undefined ? 1 : (origin.alpha ?? 0) }
47
+ for (let i = 0; i < 3; i++) vars[names[i]] = (c[i] ?? 0) * scale
48
+ const specs = node.fn === 'color' ? [UNIT, UNIT, UNIT] : FUNCTIONS[node.fn].channels
49
+ const value = (token: string, spec: typeof ALPHA): Component =>
50
+ channel(SIMPLE.test(token) ? token : String(evaluate(token, vars)), spec, token)
51
+ const result: Color = { colorSpace: node.space, components: node.channels.map((t, i) => value(t, specs[i])) }
52
+ const alpha = node.alpha === undefined ? origin.alpha : value(node.alpha, ALPHA)
53
+ if (alpha !== undefined) result.alpha = alpha
54
+ return result
55
+ }
56
+
57
+ function resolveMix(node: Extract<ColorExpr, { type: 'color-mix' }>, context: ResolveContext): Color {
58
+ const [first, second] = node.items
59
+ const p1 = first.percent ?? (second.percent === undefined ? 50 : 100 - second.percent)
60
+ const p2 = second.percent ?? 100 - p1
61
+ const sum = p1 + p2
62
+ if (sum <= 0) return fail('color-mix()', 'the percentages must add up to more than 0')
63
+ const options = node.hue === undefined ? { in: node.space } : { in: node.space, hue: node.hue }
64
+ const result = mix(resolve(first.color, context), resolve(second.color, context), p2 / sum, options)
65
+ // Percentages adding up to less than 100% make the result translucent.
66
+ if (sum < 100 && result.alpha !== null) result.alpha = (result.alpha ?? 1) * (sum / 100)
67
+ return result
68
+ }
69
+
70
+ /**
71
+ * Turns a color, a CSS string or a {@link ColorExpr} into a concrete {@link Color}.
72
+ * `currentcolor`, system colors and `light-dark()` read `context`; nothing is
73
+ * guessed, so a missing entry throws and names the value.
74
+ */
75
+ export function resolve(input: string | ColorExpr, context: ResolveContext = {}): Color {
76
+ const expr = typeof input === 'string' ? parseExpr(input) : input
77
+ if ('colorSpace' in expr) return expr
78
+ if (expr.type === 'light-dark') return resolve(context.colorScheme === 'dark' ? expr.dark : expr.light, context)
79
+ if (expr.type === 'color-mix') return resolveMix(expr, context)
80
+ if (expr.type === 'relative') return resolveRelative(expr, context)
81
+ if (expr.type === 'contrast-color') return contrastColor(resolve(expr.color, context))
82
+ const name = expr.type === 'currentcolor' ? 'currentcolor' : expr.name
83
+ const value = expr.type === 'currentcolor' ? context.currentColor : context.systemColors?.[name]
84
+ if (value === undefined)
85
+ return fail(name, `needs ${expr.type === 'currentcolor' ? 'currentColor' : `systemColors.${name}`} to resolve`)
86
+ return resolve(value, { ...context, currentColor: undefined })
87
+ }
@@ -0,0 +1,20 @@
1
+ import { type Matrix, multiply } from './math.js'
2
+ import type { ColorSpaceId, Space } from './types.js'
3
+
4
+ /** An RGB space: a transfer function, then a matrix to XYZ-D65. */
5
+ export function rgbSpace(
6
+ id: ColorSpaceId,
7
+ toLinear: (n: number) => number,
8
+ toGamma: (n: number) => number,
9
+ toXyz: Matrix,
10
+ fromXyz: Matrix
11
+ ): Space {
12
+ return {
13
+ id,
14
+ toXyz: (c) => multiply(toXyz, toLinear(c[0]), toLinear(c[1]), toLinear(c[2])),
15
+ fromXyz: (xyz) => {
16
+ const v = multiply(fromXyz, xyz[0], xyz[1], xyz[2])
17
+ return [toGamma(v[0]), toGamma(v[1]), toGamma(v[2])]
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,96 @@
1
+ import { type ComponentsIn, componentsOver } from './convert.js'
2
+ import { componentsIn } from './spaces.js'
3
+ import { srgb } from './srgb.js'
4
+ import type { Color, ColorSpaceId, Component, Space } from './types.js'
5
+
6
+ /** Options for `toCss`. */
7
+ export interface ToCssOptions {
8
+ /** Decimal places kept in every number. Default 5. */
9
+ precision?: number
10
+ }
11
+
12
+ const POWERS = Array.from({ length: 23 }, (_, i) => Number(`1e${i}`))
13
+
14
+ /**
15
+ * `Number(n.toFixed(precision))`, without building the string. `toFixed` rounds
16
+ * the exact value of `n` half up (away from zero), and the number it then reads
17
+ * is `k / 10^precision` for that integer `k`, which one division gives exactly.
18
+ * `k` comes from `t = |n| * 10^precision`. The product is rounded, but a tie
19
+ * (`t` ending in .5) is a double, and rounding is monotonic, so any `t` that
20
+ * is not exactly a tie lies on the same side of it as the exact product: only
21
+ * `t` ending in .5 is left to `toFixed`. So are NaN, the infinities, products
22
+ * of 1e9 and up, and precisions that `POWERS` lacks.
23
+ */
24
+ export function roundTo(n: number, precision: number): number {
25
+ const scale = POWERS[precision]
26
+ const t = Math.abs(n) * scale
27
+ const whole = Math.floor(t)
28
+ const rest = t - whole
29
+ if (t < 1e9 && rest !== 0.5) {
30
+ const r = (rest > 0.5 ? whole + 1 : whole) / scale
31
+ return n < 0 ? -r : r
32
+ }
33
+ return Number(n.toFixed(precision))
34
+ }
35
+
36
+ /** Prints a number with at most `precision` decimals, without `-0` or trailing zeros. */
37
+ function num(n: number, precision: number): string {
38
+ return String(roundTo(n, precision) + 0)
39
+ }
40
+
41
+ const part = (c: Component, precision: number, scale = 1, suffix = ''): string =>
42
+ c === null ? 'none' : num(c * scale, precision) + suffix
43
+
44
+ const RECTANGULAR = new Set<ColorSpaceId>(['hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch'])
45
+
46
+ /**
47
+ * Serializes a color as CSS. `srgb` and `hsl` print in the comma syntax
48
+ * (`rgb(255, 0, 0)`, `rgba(...)`) unless a component is `none`; the other
49
+ * spaces print in the space syntax (`oklch(0.7 0.1 200 / 0.5)`,
50
+ * `color(display-p3 1 0 0)`). Out-of-gamut values are kept, not clipped.
51
+ */
52
+ export function toCss(color: Color, options: ToCssOptions = {}): string {
53
+ const { precision = 5 } = options
54
+ const id = color.colorSpace
55
+ const [c0, c1, c2] = color.components
56
+ const p = (c: Component, scale?: number, suffix?: string): string => part(c, precision, scale, suffix)
57
+ const hasAlpha = color.alpha !== undefined && color.alpha !== 1
58
+ const a = hasAlpha ? p(color.alpha as Component) : undefined
59
+ const percent = id === 'hsl' || id === 'hwb' ? '%' : ''
60
+ const scale = id === 'srgb' ? 255 : 1
61
+ const args = [p(c0, scale), p(c1, scale, percent), p(c2, scale, percent)]
62
+ if ((id === 'srgb' || id === 'hsl') && color.alpha !== null && !color.components.includes(null)) {
63
+ const name = id === 'srgb' ? 'rgb' : 'hsl'
64
+ return a === undefined ? `${name}(${args.join(', ')})` : `${name}a(${args.join(', ')}, ${a})`
65
+ }
66
+ const head = id === 'srgb' ? 'rgb(' : RECTANGULAR.has(id) ? `${id}(` : `color(${id} `
67
+ return `${head}${args.join(' ')}${a === undefined ? '' : ` / ${a}`})`
68
+ }
69
+
70
+ const hexByte = (n: number): string =>
71
+ Math.min(255, Math.max(0, Math.round(n * 255)))
72
+ .toString(16)
73
+ .padStart(2, '0')
74
+
75
+ /**
76
+ * The hex form of a color, `#rrggbb` or `#rrggbbaa` when it is not opaque.
77
+ * Colors outside sRGB are converted and clipped, so map them with `toGamut` first.
78
+ */
79
+ export function toHex(color: Color): string {
80
+ return hexWith(componentsIn, color)
81
+ }
82
+
83
+ function hexWith(componentsIn: ComponentsIn, color: Color): string {
84
+ const [r, g, b] = componentsIn(color, 'srgb') as number[]
85
+ const alpha = color.alpha ?? 1
86
+ return `#${hexByte(r)}${hexByte(g)}${hexByte(b)}${alpha === 1 ? '' : hexByte(alpha)}`
87
+ }
88
+
89
+ /**
90
+ * Builds {@link toHex} over the spaces given, plus `srgb`, so a bundle contains
91
+ * only those. A color in a space not given throws, naming that space.
92
+ */
93
+ export function createToHex(...spaces: Space[]): (color: Color) => string {
94
+ const over = componentsOver([srgb, ...spaces])
95
+ return (color) => hexWith(over, color)
96
+ }
@@ -0,0 +1,42 @@
1
+ import { a98Rgb } from './a98-rgb.js'
2
+ import { componentsOver, createConverter } from './convert.js'
3
+ import { displayP3 } from './display-p3.js'
4
+ import { displayP3Linear } from './display-p3-linear.js'
5
+ import { hsl } from './hsl.js'
6
+ import { hwb } from './hwb.js'
7
+ import { lab } from './lab.js'
8
+ import { lch } from './lch.js'
9
+ import { oklab } from './oklab.js'
10
+ import { oklch } from './oklch.js'
11
+ import { prophotoRgb } from './prophoto-rgb.js'
12
+ import { rec2020 } from './rec2020.js'
13
+ import { srgb } from './srgb.js'
14
+ import { srgbLinear } from './srgb-linear.js'
15
+ import type { ColorSpaceId, Space } from './types.js'
16
+ import { xyzD50 } from './xyz-d50.js'
17
+ import { xyzD65 } from './xyz-d65.js'
18
+
19
+ /** Every space. Importing this pulls them all in; import single spaces to avoid that. */
20
+ export const spaces: Record<ColorSpaceId, Space> = {
21
+ srgb,
22
+ 'srgb-linear': srgbLinear,
23
+ hsl,
24
+ hwb,
25
+ lab,
26
+ lch,
27
+ oklab,
28
+ oklch,
29
+ 'display-p3': displayP3,
30
+ 'display-p3-linear': displayP3Linear,
31
+ 'a98-rgb': a98Rgb,
32
+ 'prophoto-rgb': prophotoRgb,
33
+ rec2020,
34
+ 'xyz-d50': xyzD50,
35
+ 'xyz-d65': xyzD65
36
+ }
37
+
38
+ /** A color's components in any of the 15 spaces, as `convert` gives them, without the color object. */
39
+ export const componentsIn = componentsOver(Object.values(spaces))
40
+
41
+ /** Converts a color to any of the 15 spaces. */
42
+ export const convert = createConverter(...Object.values(spaces))