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,215 @@
1
+ import { deltaEOkWith } from './compare.js'
2
+ import { type ComponentsIn, componentsOver, convertWith } from './convert.js'
3
+ import { type Matrix, product } from './math.js'
4
+ import { lmsToOklab, lmsToXyz, oklabToLms, srgbToXyz, xyzToLms, xyzToSrgb } from './matrices.js'
5
+ import { oklab } from './oklab.js'
6
+ import { oklch } from './oklch.js'
7
+ import { componentsIn } from './spaces.js'
8
+ import { srgb } from './srgb.js'
9
+ import { srgbToGamma } from './transfer.js'
10
+ import type { Color, ColorSpaceId, Component, Space, Vec3 } from './types.js'
11
+
12
+ /** The bounded RGB spaces a color can be tested or mapped against. */
13
+ export type GamutId =
14
+ | 'srgb'
15
+ | 'srgb-linear'
16
+ | 'display-p3'
17
+ | 'display-p3-linear'
18
+ | 'a98-rgb'
19
+ | 'prophoto-rgb'
20
+ | 'rec2020'
21
+
22
+ const within = (components: Component[], epsilon: number): boolean =>
23
+ components.every((c) => (c as number) >= -epsilon && (c as number) <= 1 + epsilon)
24
+
25
+ /** True when every channel of `color`, seen in `gamut`, is within 0 to 1 (± `epsilon`). */
26
+ export function inGamut(color: Color, gamut: GamutId = 'srgb', epsilon = 1e-6): boolean {
27
+ return within(componentsIn(color, gamut), epsilon)
28
+ }
29
+
30
+ /**
31
+ * Builds {@link inGamut} over the spaces given, plus `srgb`, so a bundle
32
+ * contains only those. Give the gamut too when it is not `srgb`. A color or
33
+ * gamut in a space not given throws, naming that space.
34
+ */
35
+ export function createInGamut(...spaces: Space[]): (color: Color, gamut?: GamutId, epsilon?: number) => boolean {
36
+ const over = componentsOver([srgb, ...spaces])
37
+ return (color, gamut = 'srgb', epsilon = 1e-6) => within(over(color, gamut), epsilon)
38
+ }
39
+
40
+ /** Options for `toGamut` and the function `createToGamut` returns. */
41
+ export interface ToGamutOptions {
42
+ /** The gamut to fit into. Default `srgb`. */
43
+ gamut?: GamutId
44
+ /** `css` is the CSS Color 4 OKLCH chroma reduction. `clip` clamps each channel. Default `css`. */
45
+ method?: 'css' | 'clip'
46
+ }
47
+
48
+ /** `color` with `components`, which are in `gamut`, clamped to 0 to 1: what `convert` then a clamp gives. */
49
+ function clipped(color: Color, gamut: GamutId, components: Component[]): Color {
50
+ return withAlpha(
51
+ { colorSpace: gamut, components: components.map((c) => Math.min(1, Math.max(0, c as number))) },
52
+ color
53
+ )
54
+ }
55
+
56
+ /**
57
+ * Maps a color into a gamut and returns it in its original space. `css` follows
58
+ * CSS Color 4 section 13.2: reduce OKLCH chroma until the clipped color is
59
+ * within a just-noticeable ΔEOK of 0.02.
60
+ */
61
+ export function toGamut(color: Color, options: ToGamutOptions = {}): Color {
62
+ return toGamutWith(componentsIn, color, options)
63
+ }
64
+
65
+ /**
66
+ * Builds {@link toGamut} over the spaces given, plus `srgb`, `oklch` and
67
+ * `oklab`, which the CSS method works in. Give the gamut too when it is not
68
+ * `srgb`. A color or gamut in a space not given throws, naming that space.
69
+ */
70
+ export function createToGamut(...spaces: Space[]): (color: Color, options?: ToGamutOptions) => Color {
71
+ const over = componentsOver([srgb, oklch, oklab, ...spaces])
72
+ return (color, options = {}) => toGamutWith(over, color, options)
73
+ }
74
+
75
+ /** {@link toGamut} over the spaces `componentsIn` knows, which must include `oklch` and `oklab`. */
76
+ export function toGamutWith(componentsIn: ComponentsIn, color: Color, options: ToGamutOptions): Color {
77
+ const { gamut = 'srgb', method = 'css' } = options
78
+ const deltaEOk = (a: Color, b: Color): number => deltaEOkWith(componentsIn, a, b)
79
+ const back = (c: Color): Color => convertWith(componentsIn, c, color.colorSpace as ColorSpaceId)
80
+ // Each candidate is converted to the gamut once, then tested and clipped from that.
81
+ const fit = componentsIn(color, gamut)
82
+ if (within(fit, 1e-6)) return color
83
+ if (method === 'clip') return back(clipped(color, gamut, fit))
84
+
85
+ const origin = convertWith(componentsIn, color, 'oklch')
86
+ const [lightness, chroma, hue] = origin.components as [number, number, Component]
87
+ if (lightness >= 1) return back(withAlpha({ colorSpace: gamut, components: [1, 1, 1] }, color))
88
+ if (lightness <= 0) return back(withAlpha({ colorSpace: gamut, components: [0, 0, 0] }, color))
89
+
90
+ const JND = 0.02
91
+ const EPSILON = 0.0001
92
+ if (gamut === 'srgb') return back(reduceChromaInSrgb(origin, lightness, chroma, hue))
93
+ let min = 0
94
+ let max = chroma
95
+ let minInGamut = true
96
+ let clip = clipped(origin, gamut, componentsIn(origin, gamut))
97
+ if (deltaEOk(clip, origin) < JND) return back(clip)
98
+ while (max - min > EPSILON) {
99
+ const mid = (min + max) / 2
100
+ const current = withAlpha({ colorSpace: 'oklch', components: [lightness, mid, hue] }, origin)
101
+ const inGamutSpace = componentsIn(current, gamut)
102
+ if (minInGamut && within(inGamutSpace, 1e-6)) {
103
+ min = mid
104
+ continue
105
+ }
106
+ clip = clipped(current, gamut, inGamutSpace)
107
+ const e = deltaEOk(clip, current)
108
+ if (e < JND) {
109
+ if (JND - e < EPSILON) return back(clip)
110
+ minInGamut = false
111
+ min = mid
112
+ } else {
113
+ max = mid
114
+ }
115
+ }
116
+ // The search ended by narrowing to EPSILON rather than by an early return. A
117
+ // sweep of 88,000 colors never gets here, but the algorithm allows it.
118
+ /* v8 ignore next */
119
+ return back(clip)
120
+ }
121
+
122
+ function withAlpha(c: Color, source: Color): Color {
123
+ if (source.alpha !== undefined) c.alpha = source.alpha
124
+ return c
125
+ }
126
+
127
+ let lmsToLinear: Matrix | undefined
128
+ let linearToLms: Matrix | undefined
129
+ // The gamma-space tolerance of 1e-6 on each side, as a linear one: the transfer curve is monotone.
130
+ const LOW = -1e-6 / 12.92
131
+ const HIGH = ((1 + 1e-6 + 0.055) / 1.055) ** 2.4
132
+
133
+ // A clipped channel of exactly 0 or 1 stays exactly that, as clipping after the curve gives.
134
+ const encode = (n: number): number => (n === 0 || n === 1 ? n : srgbToGamma(n))
135
+ const clamp01 = (n: number): number => Math.min(1, Math.max(0, n))
136
+
137
+ /**
138
+ * The CSS Color 4 section 13.2 search of `toGamut` for the `srgb` gamut, done in
139
+ * linear sRGB so the loop has no hub conversion, `**` or trigonometry. Clipping
140
+ * to 0 to 1 commutes with the monotone transfer curve, so it finds what the
141
+ * generic path finds, up to rounding. Returns the mapped color in `srgb`.
142
+ */
143
+ function reduceChromaInSrgb(origin: Color, lightness: number, chroma: number, hue: Component): Color {
144
+ // Composed on first use, so a bundle without this path does not pay for them at load.
145
+ const m = (lmsToLinear ??= product(xyzToSrgb, lmsToXyz))
146
+ const n = (linearToLms ??= product(xyzToLms, srgbToXyz))
147
+ const o = oklabToLms
148
+ const k = lmsToOklab
149
+ // A `none` hue or chroma reads as 0, as it does in a conversion. A `none` lightness never gets here.
150
+ const l = lightness
151
+ const radians = ((hue ?? 0) * Math.PI) / 180
152
+ const cos = Math.cos(radians)
153
+ const sin = Math.sin(radians)
154
+ let r = 0
155
+ let g = 0
156
+ let b = 0
157
+ let inside = false
158
+ /** Puts the candidate of chroma `c` in linear sRGB into r, g, b (unclipped), and says if it is within the gamut. */
159
+ const place = (c: number): void => {
160
+ const a = c * cos
161
+ const bb = c * sin
162
+ const x = l + o[1] * a + o[2] * bb
163
+ const y = l + o[4] * a + o[5] * bb
164
+ const z = l + o[7] * a + o[8] * bb
165
+ const x3 = x * x * x
166
+ const y3 = y * y * y
167
+ const z3 = z * z * z
168
+ r = m[0] * x3 + m[1] * y3 + m[2] * z3
169
+ g = m[3] * x3 + m[4] * y3 + m[5] * z3
170
+ b = m[6] * x3 + m[7] * y3 + m[8] * z3
171
+ inside = r >= LOW && r <= HIGH && g >= LOW && g <= HIGH && b >= LOW && b <= HIGH
172
+ }
173
+ /** Clips r, g, b and returns its ΔEOK to the candidate of chroma `c`. */
174
+ const clipAndMeasure = (c: number): number => {
175
+ r = clamp01(r)
176
+ g = clamp01(g)
177
+ b = clamp01(b)
178
+ const x = Math.cbrt(n[0] * r + n[1] * g + n[2] * b)
179
+ const y = Math.cbrt(n[3] * r + n[4] * g + n[5] * b)
180
+ const z = Math.cbrt(n[6] * r + n[7] * g + n[8] * b)
181
+ const dl = k[0] * x + k[1] * y + k[2] * z - l
182
+ const da = k[3] * x + k[4] * y + k[5] * z - c * cos
183
+ const db = k[6] * x + k[7] * y + k[8] * z - c * sin
184
+ return Math.sqrt(dl * dl + da * da + db * db)
185
+ }
186
+ const JND = 0.02
187
+ const EPSILON = 0.0001
188
+ let min = 0
189
+ let max = chroma
190
+ let minInGamut = true
191
+ place(chroma ?? 0)
192
+ let clip: Vec3
193
+ let e = clipAndMeasure(chroma ?? 0)
194
+ clip = [r, g, b]
195
+ if (e >= JND) {
196
+ while (max - min > EPSILON) {
197
+ const mid = (min + max) / 2
198
+ place(mid)
199
+ if (minInGamut && inside) {
200
+ min = mid
201
+ continue
202
+ }
203
+ e = clipAndMeasure(mid)
204
+ clip = [r, g, b]
205
+ if (e < JND) {
206
+ if (JND - e < EPSILON) break
207
+ minInGamut = false
208
+ min = mid
209
+ } else {
210
+ max = mid
211
+ }
212
+ }
213
+ }
214
+ return withAlpha({ colorSpace: 'srgb', components: clip.map(encode) }, origin)
215
+ }
@@ -0,0 +1,57 @@
1
+ import { normalizeHue } from './math.js'
2
+ import { srgb } from './srgb.js'
3
+ import type { Space, Vec3 } from './types.js'
4
+
5
+ function channel(t1: number, t2: number, sextant: number): number {
6
+ const h = (sextant + 6) % 6
7
+ if (h < 1) return (t2 - t1) * h + t1
8
+ if (h < 3) return t2
9
+ if (h < 4) return (t2 - t1) * (4 - h) + t1
10
+ return t1
11
+ }
12
+
13
+ /** CSS Color 4 hslToRgb. Hue in degrees, s and l in 0 to 1. */
14
+ export function hslToSrgb(h: number, s: number, l: number): Vec3 {
15
+ const t2 = l <= 0.5 ? l * (s + 1) : l + s - l * s
16
+ const t1 = l * 2 - t2
17
+ const hue = normalizeHue(h) / 60
18
+ return [channel(t1, t2, hue + 2), channel(t1, t2, hue), channel(t1, t2, hue - 2)]
19
+ }
20
+
21
+ /**
22
+ * CSS Color 4 rgbToHsl. Returns hue in degrees, s and l in 0 to 1. For
23
+ * out-of-gamut input the saturation can come out negative; it is made positive
24
+ * and the hue turned by 180 degrees, unless `flipNegative` is false.
25
+ */
26
+ export function srgbToHsl([r, g, b]: Vec3, flipNegative = true): Vec3 {
27
+ const max = Math.max(r, g, b)
28
+ const min = Math.min(r, g, b)
29
+ const l = (min + max) / 2
30
+ const d = max - min
31
+ let h = 0
32
+ let s = 0
33
+ // A spread of rounding noise is a gray: a round trip leaves white at 1 - 1e-16.
34
+ if (d > 1e-9) {
35
+ s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l)
36
+ h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4
37
+ h *= 60
38
+ }
39
+ if (s < 0) {
40
+ if (flipNegative) h += 180
41
+ s = -s
42
+ }
43
+ return [normalizeHue(h), s, l]
44
+ }
45
+
46
+ /**
47
+ * The CSS `hsl` space. Components are hue in degrees, then saturation and lightness in 0 to 100.
48
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
49
+ */
50
+ export const hsl: Space = {
51
+ id: 'hsl',
52
+ toXyz: ([h, s, l]) => srgb.toXyz(hslToSrgb(h, s / 100, l / 100)),
53
+ fromXyz: (xyz) => {
54
+ const [h, s, l] = srgbToHsl(srgb.fromXyz(xyz))
55
+ return [h, s * 100, l * 100]
56
+ }
57
+ }
@@ -0,0 +1,26 @@
1
+ import { hslToSrgb, srgbToHsl } from './hsl.js'
2
+ import { srgb } from './srgb.js'
3
+ import type { Space } from './types.js'
4
+
5
+ /**
6
+ * The CSS `hwb` space. Components are hue in degrees, then whiteness and blackness in 0 to 100.
7
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
8
+ */
9
+ export const hwb: Space = {
10
+ id: 'hwb',
11
+ toXyz: ([h, w, b]) => {
12
+ const white = w / 100
13
+ const black = b / 100
14
+ if (white + black >= 1) {
15
+ const gray = white / (white + black)
16
+ return srgb.toXyz([gray, gray, gray])
17
+ }
18
+ const k = 1 - white - black
19
+ const [red, green, blue] = hslToSrgb(h, 1, 0.5)
20
+ return srgb.toXyz([red * k + white, green * k + white, blue * k + white])
21
+ },
22
+ fromXyz: (xyz) => {
23
+ const rgb = srgb.fromXyz(xyz)
24
+ return [srgbToHsl(rgb, false)[0], Math.min(...rgb) * 100, (1 - Math.max(...rgb)) * 100]
25
+ }
26
+ }
@@ -0,0 +1,43 @@
1
+ export { a98Rgb } from './a98-rgb.js'
2
+ export { fromArgb, toArgb } from './argb.js'
3
+ export { createDeltaEOk, createEquals, deltaEOk, equals } from './compare.js'
4
+ export {
5
+ type ContrastAlgorithm,
6
+ type ContrastFn,
7
+ type ContrastOption,
8
+ contrast,
9
+ createContrast,
10
+ createFindContrast,
11
+ createMeets,
12
+ createOnColor,
13
+ type FindContrastOptions,
14
+ type FoundContrast,
15
+ findContrast,
16
+ meets,
17
+ type OnColorOptions,
18
+ onColor,
19
+ wcag2
20
+ } from './contrast.js'
21
+ export { convertComponents, createConverter } from './convert.js'
22
+ export { displayP3 } from './display-p3.js'
23
+ export { displayP3Linear } from './display-p3-linear.js'
24
+ export { type ColorExpr, parseExpr } from './expr.js'
25
+ export { createInGamut, createToGamut, type GamutId, inGamut, type ToGamutOptions, toGamut } from './gamut.js'
26
+ export { hsl } from './hsl.js'
27
+ export { hwb } from './hwb.js'
28
+ export { lab } from './lab.js'
29
+ export { lch } from './lch.js'
30
+ export { createMix, type HueMethod, type MixOptions, mix } from './mix.js'
31
+ export { oklab } from './oklab.js'
32
+ export { oklch } from './oklch.js'
33
+ export { parse } from './parse.js'
34
+ export { prophotoRgb } from './prophoto-rgb.js'
35
+ export { rec2020 } from './rec2020.js'
36
+ export { type ResolveContext, resolve } from './resolve.js'
37
+ export { createToHex, type ToCssOptions, toCss, toHex } from './serialize.js'
38
+ export { convert, spaces } from './spaces.js'
39
+ export { srgb } from './srgb.js'
40
+ export { srgbLinear } from './srgb-linear.js'
41
+ export type { Color, ColorSpaceId, Component, Space, Vec3 } from './types.js'
42
+ export { xyzD50 } from './xyz-d50.js'
43
+ export { xyzD65 } from './xyz-d65.js'
@@ -0,0 +1,29 @@
1
+ import { D50, multiply } from './math.js'
2
+ import { d50ToD65, d65ToD50 } from './matrices.js'
3
+ import type { Space } from './types.js'
4
+
5
+ const EPSILON = 216 / 24389
6
+ const KAPPA = 24389 / 27
7
+
8
+ const finv = (f: number): number => (f ** 3 > EPSILON ? f ** 3 : (116 * f - 16) / KAPPA)
9
+ const forward = (v: number): number => (v > EPSILON ? Math.cbrt(v) : (KAPPA * v + 16) / 116)
10
+
11
+ /**
12
+ * The CSS `lab` space (CIE Lab, D50 white). Components are lightness in 0 to 100, then the a and b axes, which are not bounded but stay near -125 to 125 for real colors.
13
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
14
+ */
15
+ export const lab: Space = {
16
+ id: 'lab',
17
+ toXyz: ([l, a, b]) => {
18
+ const fy = (l + 16) / 116
19
+ const y = l > KAPPA * EPSILON ? fy ** 3 : l / KAPPA
20
+ const x = finv(fy + a / 500)
21
+ const z = finv(fy - b / 200)
22
+ return multiply(d50ToD65, x * D50[0], y * D50[1], z * D50[2])
23
+ },
24
+ fromXyz: (xyz) => {
25
+ const [x, y, z] = multiply(d65ToD50, xyz[0], xyz[1], xyz[2])
26
+ const fy = forward(y / D50[1])
27
+ return [116 * fy - 16, 500 * (forward(x / D50[0]) - fy), 200 * (fy - forward(z / D50[2]))]
28
+ }
29
+ }
@@ -0,0 +1,8 @@
1
+ import { lab } from './lab.js'
2
+ import { polarSpace } from './polar.js'
3
+
4
+ /**
5
+ * The CSS `lch` space, the polar form of `lab`. Components are lightness in 0 to 100, chroma (not bounded, about 0 to 150) and hue in degrees.
6
+ * Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
7
+ */
8
+ export const lch = polarSpace('lch', lab)
@@ -0,0 +1,30 @@
1
+ import type { Vec3 } from './types.js'
2
+
3
+ /** A 3x3 matrix, flat, row by row. */
4
+ export type Matrix = readonly [number, number, number, number, number, number, number, number, number]
5
+
6
+ /** `m` times the column vector (x, y, z). Takes numbers rather than a Vec3, so callers need no array. */
7
+ export function multiply(m: Matrix, x: number, y: number, z: number): Vec3 {
8
+ return [m[0] * x + m[1] * y + m[2] * z, m[3] * x + m[4] * y + m[5] * z, m[6] * x + m[7] * y + m[8] * z]
9
+ }
10
+
11
+ /** Keeps the sign, so out-of-gamut values survive the transfer function. */
12
+ export function signed(f: (abs: number) => number): (n: number) => number {
13
+ return (n) => (n < 0 ? -f(-n) : f(n))
14
+ }
15
+
16
+ export function normalizeHue(h: number): number {
17
+ return ((h % 360) + 360) % 360
18
+ }
19
+
20
+ export const D50: Vec3 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585]
21
+
22
+ /** The product of 3x3 matrices, left to right: `product(a, b)` applies `b` first. */
23
+ export function product(...ms: Matrix[]): Matrix {
24
+ return ms.reduce((a, b) => {
25
+ const out: number[] = []
26
+ for (let i = 0; i < 3; i++)
27
+ for (let j = 0; j < 3; j++) out.push(a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j])
28
+ return out as unknown as Matrix
29
+ })
30
+ }
@@ -0,0 +1,91 @@
1
+ // biome-ignore-all format: each matrix is written as its three rows
2
+ // CSS Color 4, section 19 (sample code for color conversions). Each matrix is
3
+ // flat, row by row.
4
+ import type { Matrix } from './math.js'
5
+
6
+ export const srgbToXyz: Matrix = [
7
+ 506752 / 1228815, 87881 / 245763, 12673 / 70218,
8
+ 87098 / 409605, 175762 / 245763, 12673 / 175545,
9
+ 7918 / 409605, 87881 / 737289, 1001167 / 1053270
10
+ ]
11
+ export const xyzToSrgb: Matrix = [
12
+ 12831 / 3959, -329 / 214, -1974 / 3959,
13
+ -851781 / 878810, 1648619 / 878810, 36519 / 878810,
14
+ 705 / 12673, -2585 / 12673, 705 / 667
15
+ ]
16
+
17
+ export const p3ToXyz: Matrix = [
18
+ 608311 / 1250200, 189793 / 714400, 198249 / 1000160,
19
+ 35783 / 156275, 247089 / 357200, 198249 / 2500400,
20
+ 0, 32229 / 714400, 5220557 / 5000800
21
+ ]
22
+ export const xyzToP3: Matrix = [
23
+ 446124 / 178915, -333277 / 357830, -72051 / 178915,
24
+ -14852 / 17905, 63121 / 35810, 423 / 17905,
25
+ 11844 / 330415, -50337 / 660830, 316169 / 330415
26
+ ]
27
+
28
+ export const a98ToXyz: Matrix = [
29
+ 573536 / 994567, 263643 / 1420810, 187206 / 994567,
30
+ 591459 / 1989134, 6239551 / 9945670, 374412 / 4972835,
31
+ 53769 / 1989134, 351524 / 4972835, 4929758 / 4972835
32
+ ]
33
+ export const xyzToA98: Matrix = [
34
+ 1829569 / 896150, -506331 / 896150, -308931 / 896150,
35
+ -851781 / 878810, 1648619 / 878810, 36519 / 878810,
36
+ 16779 / 1248040, -147721 / 1248040, 1266979 / 1248040
37
+ ]
38
+
39
+ export const prophotoToXyzD50: Matrix = [
40
+ 0.7977666449006423, 0.13518129740053308, 0.0313477341283922,
41
+ 0.2880748288194013, 0.711835234241873, 0.00008993693872564,
42
+ 0, 0, 0.8251046025104602
43
+ ]
44
+ export const xyzD50ToProphoto: Matrix = [
45
+ 1.3457868816471583, -0.25557208737979464, -0.05110186497554526,
46
+ -0.5446307051249019, 1.5082477428451468, 0.02052744743642139,
47
+ 0, 0, 1.2119675456389452
48
+ ]
49
+
50
+ export const rec2020ToXyz: Matrix = [
51
+ 63426534 / 99577255, 20160776 / 139408157, 47086771 / 278816314,
52
+ 26158966 / 99577255, 472592308 / 697040785, 8267143 / 139408157,
53
+ 0, 19567812 / 697040785, 295819943 / 278816314
54
+ ]
55
+ export const xyzToRec2020: Matrix = [
56
+ 30757411 / 17917100, -6372589 / 17917100, -4539589 / 17917100,
57
+ -19765991 / 29648200, 47925759 / 29648200, 467509 / 29648200,
58
+ 792561 / 44930125, -1921689 / 44930125, 42328811 / 44930125
59
+ ]
60
+
61
+ export const d65ToD50: Matrix = [
62
+ 1.0479297925449969, 0.022946870601609652, -0.05019226628920524,
63
+ 0.02962780877005599, 0.9904344267538799, -0.017073799063418826,
64
+ -0.009243040646204504, 0.015055191490298152, 0.7518742814281371
65
+ ]
66
+ export const d50ToD65: Matrix = [
67
+ 0.955473421488075, -0.02309845494876471, 0.06325924320057072,
68
+ -0.0283697093338637, 1.0099953980813041, 0.021041441191917323,
69
+ 0.012314014864481998, -0.020507649298898964, 1.330365926242124
70
+ ]
71
+
72
+ export const xyzToLms: Matrix = [
73
+ 0.819022437996703, 0.3619062600528904, -0.1288737815209879,
74
+ 0.0329836539323885, 0.9292868615863434, 0.0361446663506424,
75
+ 0.0481771893596242, 0.2642395317527308, 0.6335478284694309
76
+ ]
77
+ export const lmsToOklab: Matrix = [
78
+ 0.210454268309314, 0.7936177747023054, -0.0040720430116193,
79
+ 1.9779985324311684, -2.42859224204858, 0.450593709617411,
80
+ 0.0259040424655478, 0.7827717124575296, -0.8086757549230774
81
+ ]
82
+ export const lmsToXyz: Matrix = [
83
+ 1.2268798758459243, -0.5578149944602171, 0.2813910456659647,
84
+ -0.0405757452148008, 1.112286803280317, -0.0717110580655164,
85
+ -0.0763729366746601, -0.4214933324022432, 1.5869240198367816
86
+ ]
87
+ export const oklabToLms: Matrix = [
88
+ 1, 0.3963377773761749, 0.2158037573099136,
89
+ 1, -0.1055613458156586, -0.0638541728258133,
90
+ 1, -0.0894841775298119, -1.2914855480194092
91
+ ]
@@ -0,0 +1,158 @@
1
+ import { type ComponentsIn, componentsOver, convertWith } from './convert.js'
2
+ import { normalizeHue } from './math.js'
3
+ import { oklab } from './oklab.js'
4
+ import { toColor } from './parse.js'
5
+ import { componentsIn } from './spaces.js'
6
+ import type { Color, ColorSpaceId, Component, Space } from './types.js'
7
+ import { unknownName } from './unknown-name.js'
8
+
9
+ /** How `mix` travels around the hue circle (CSS Color 4 section 12.4). */
10
+ export type HueMethod = 'shorter' | 'longer' | 'increasing' | 'decreasing'
11
+
12
+ /** Options for `mix` and the function `createMix` returns. */
13
+ export interface MixOptions {
14
+ /** The space to interpolate in. Default `oklab`, which is what `color-mix()` uses. */
15
+ in?: ColorSpaceId
16
+ /** How hues are interpolated, in the polar spaces. Default `shorter`. */
17
+ hue?: HueMethod
18
+ }
19
+
20
+ /** Index of the hue component, for the polar spaces. */
21
+ const HUE_AT: Record<string, number> = { __proto__: null as never, hsl: 0, hwb: 0, lch: 2, oklch: 2 }
22
+
23
+ /**
24
+ * Which components are "analogous" (CSS Color 4 section 4.4): a `none` in one
25
+ * space carries to the analogous component of another. One letter per
26
+ * component; the RGB spaces share `RGB` and the XYZ ones `XYZ`.
27
+ */
28
+ const ANALOGOUS: Record<string, string> = {
29
+ __proto__: null as never,
30
+ hsl: 'hcl',
31
+ hwb: 'hwk',
32
+ lab: 'lab',
33
+ lch: 'lch',
34
+ oklab: 'lab',
35
+ oklch: 'lch',
36
+ 'xyz-d50': 'XYZ',
37
+ 'xyz-d65': 'XYZ'
38
+ }
39
+ const analogous = (id: ColorSpaceId): string => ANALOGOUS[id] ?? 'RGB'
40
+
41
+ /** Converts, keeping `none` on components that have an analogous one in the source. */
42
+ function convertKeepingNone(componentsIn: ComponentsIn, color: Color, space: ColorSpaceId): Color {
43
+ const out = convertWith(componentsIn, color, space)
44
+ // Nothing to carry when no component is `none`, the common case.
45
+ if (color.colorSpace !== space && color.components.includes(null)) {
46
+ const from = analogous(color.colorSpace)
47
+ const to = analogous(space)
48
+ out.components = out.components.map((c, i) => (color.components[from.indexOf(to[i])] === null ? null : c))
49
+ }
50
+ return out
51
+ }
52
+
53
+ /**
54
+ * Chroma below which a converted hue is powerless and so `none`, per polar
55
+ * space. Follows colorjs.io, which treats a hue as powerless when both Lab
56
+ * axes are under 1/100000 of their reference extent: 0.8 for Oklab (a, b in
57
+ * -0.4 to 0.4) gives 8e-6, 250 for Lab (-125 to 125) gives 0.0025.
58
+ */
59
+ const POWERLESS: Record<string, number> = { __proto__: null as never, oklch: 8e-6, lch: 0.0025, hsl: 1e-4 }
60
+
61
+ /**
62
+ * Converts for interpolation. A hue that conversion produced for a gray
63
+ * becomes `none`, so it takes the other color's hue; a hue the author wrote
64
+ * (the color already in `space`) stays as written.
65
+ */
66
+ function prepare(componentsIn: ComponentsIn, color: Color, space: ColorSpaceId): Color {
67
+ const out = convertKeepingNone(componentsIn, color, space)
68
+ // A space with no powerless entry (and not `hwb`) has no hue to lose.
69
+ if (color.colorSpace === space || (space !== 'hwb' && POWERLESS[space] === undefined)) return out
70
+ const [, x, y] = out.components
71
+ const epsilon = POWERLESS[space]
72
+ const gray =
73
+ typeof x === 'number' &&
74
+ (space === 'hwb' ? typeof y === 'number' && x + y > 100 - 1e-4 : epsilon !== undefined && x < epsilon)
75
+ if (gray) out.components[HUE_AT[space] as number] = null
76
+ return out
77
+ }
78
+
79
+ const HUE_METHODS = ['shorter', 'longer', 'increasing', 'decreasing']
80
+
81
+ /** Fixes up two hues so they interpolate by `method` (section 12.4). */
82
+ function fixHues(h1: number, h2: number, method: HueMethod): [number, number] {
83
+ let a = normalizeHue(h1)
84
+ let b = normalizeHue(h2)
85
+ const d = b - a
86
+ if (method === 'shorter') {
87
+ if (d > 180) a += 360
88
+ else if (d < -180) b += 360
89
+ } else if (method === 'longer') {
90
+ if (d > 0 && d < 180) a += 360
91
+ else if (d > -180 && d <= 0) b += 360
92
+ } else if (method === 'increasing') {
93
+ if (b < a) b += 360
94
+ } else if (method !== 'decreasing') throw unknownName('hue method', method, HUE_METHODS)
95
+ else if (a < b) a += 360
96
+ return [a, b]
97
+ }
98
+
99
+ /**
100
+ * Interpolates two colors the way CSS does (Color 4 section 13): in the space
101
+ * `in`, with premultiplied alpha, `none` taking the other color's value, and
102
+ * the chosen hue method. `t` is the weight of `b`, 0 to 1 (outside it
103
+ * extrapolates). Strings are parsed with `parse`.
104
+ *
105
+ * A hue that conversion makes powerless (chroma under 8e-6 in `oklch`, 0.0025
106
+ * in `lch`, saturation under 1e-4 in `hsl`, whiteness plus blackness filling `hwb`; for example white) becomes `none`, so it takes the other color's
107
+ * hue. A hue you wrote in the target space stays as written.
108
+ */
109
+ export function mix(a: Color | string, b: Color | string, t = 0.5, options: MixOptions = {}): Color {
110
+ return mixWith(componentsIn, toColor(a), toColor(b), t, options)
111
+ }
112
+
113
+ /**
114
+ * Builds {@link mix} over the spaces given, plus `oklab`, the default `in`, so
115
+ * a bundle contains only those. Give the `in` space too when it is not
116
+ * `oklab`. It takes color objects, not strings, so it does not carry `parse`;
117
+ * pass strings through `parse` first. A color in a space not given throws,
118
+ * naming that space.
119
+ */
120
+ export function createMix(...spaces: Space[]): (a: Color, b: Color, t?: number, options?: MixOptions) => Color {
121
+ const over = componentsOver([oklab, ...spaces])
122
+ return (a, b, t = 0.5, options = {}) => mixWith(over, a, b, t, options)
123
+ }
124
+
125
+ /** {@link mix} of two colors over the spaces `componentsIn` knows. */
126
+ function mixWith(componentsIn: ComponentsIn, a: Color, b: Color, t: number, options: MixOptions): Color {
127
+ const space = options.in ?? 'oklab'
128
+ const c1 = prepare(componentsIn, a, space)
129
+ const c2 = prepare(componentsIn, b, space)
130
+ // A `none` takes the other side's value; both `none` stays `none`.
131
+ const alpha1 = c1.alpha === undefined ? 1 : c1.alpha
132
+ const alpha2 = c2.alpha === undefined ? 1 : c2.alpha
133
+ const a1 = alpha1 ?? alpha2
134
+ const p1 = a1 ?? 1
135
+ const p2 = alpha2 ?? alpha1 ?? 1
136
+ const alpha = p1 * (1 - t) + p2 * t
137
+ const hueAt = HUE_AT[space]
138
+ const method = options.hue ?? 'shorter'
139
+ const components: Component[] = []
140
+ for (let i = 0; i < c1.components.length; i++) {
141
+ const x = c1.components[i] as Component
142
+ const y = c2.components[i] as Component
143
+ const u = x ?? y
144
+ const v = y ?? x
145
+ if (u === null || v === null) {
146
+ components.push(null)
147
+ } else if (i === hueAt) {
148
+ const [h1, h2] = fixHues(u, v, method)
149
+ components.push(normalizeHue(h1 * (1 - t) + h2 * t))
150
+ } else {
151
+ const premultiplied = u * p1 * (1 - t) + v * p2 * t
152
+ components.push(alpha === 0 ? premultiplied : premultiplied / alpha)
153
+ }
154
+ }
155
+ const result: Color = { colorSpace: space, components }
156
+ if (a1 === null || alpha !== 1) result.alpha = a1 === null ? null : alpha
157
+ return result
158
+ }
@@ -0,0 +1,13 @@
1
+ // The 148 CSS named colors (CSS Color 4 section 6.1), as `name:rrggbb` pairs.
2
+ const DATA =
3
+ 'aliceblue:f0f8ff,antiquewhite:faebd7,aqua:00ffff,aquamarine:7fffd4,azure:f0ffff,beige:f5f5dc,bisque:ffe4c4,black:000000,blanchedalmond:ffebcd,blue:0000ff,blueviolet:8a2be2,brown:a52a2a,burlywood:deb887,cadetblue:5f9ea0,chartreuse:7fff00,chocolate:d2691e,coral:ff7f50,cornflowerblue:6495ed,cornsilk:fff8dc,crimson:dc143c,cyan:00ffff,darkblue:00008b,darkcyan:008b8b,darkgoldenrod:b8860b,darkgray:a9a9a9,darkgreen:006400,darkgrey:a9a9a9,darkkhaki:bdb76b,darkmagenta:8b008b,darkolivegreen:556b2f,darkorange:ff8c00,darkorchid:9932cc,darkred:8b0000,darksalmon:e9967a,darkseagreen:8fbc8f,darkslateblue:483d8b,darkslategray:2f4f4f,darkslategrey:2f4f4f,darkturquoise:00ced1,darkviolet:9400d3,deeppink:ff1493,deepskyblue:00bfff,dimgray:696969,dimgrey:696969,dodgerblue:1e90ff,firebrick:b22222,floralwhite:fffaf0,forestgreen:228b22,fuchsia:ff00ff,gainsboro:dcdcdc,ghostwhite:f8f8ff,gold:ffd700,goldenrod:daa520,gray:808080,green:008000,greenyellow:adff2f,grey:808080,honeydew:f0fff0,hotpink:ff69b4,indianred:cd5c5c,indigo:4b0082,ivory:fffff0,khaki:f0e68c,lavender:e6e6fa,lavenderblush:fff0f5,lawngreen:7cfc00,lemonchiffon:fffacd,lightblue:add8e6,lightcoral:f08080,lightcyan:e0ffff,lightgoldenrodyellow:fafad2,lightgray:d3d3d3,lightgreen:90ee90,lightgrey:d3d3d3,lightpink:ffb6c1,lightsalmon:ffa07a,lightseagreen:20b2aa,lightskyblue:87cefa,lightslategray:778899,lightslategrey:778899,lightsteelblue:b0c4de,lightyellow:ffffe0,lime:00ff00,limegreen:32cd32,linen:faf0e6,magenta:ff00ff,maroon:800000,mediumaquamarine:66cdaa,mediumblue:0000cd,mediumorchid:ba55d3,mediumpurple:9370db,mediumseagreen:3cb371,mediumslateblue:7b68ee,mediumspringgreen:00fa9a,mediumturquoise:48d1cc,mediumvioletred:c71585,midnightblue:191970,mintcream:f5fffa,mistyrose:ffe4e1,moccasin:ffe4b5,navajowhite:ffdead,navy:000080,oldlace:fdf5e6,olive:808000,olivedrab:6b8e23,orange:ffa500,orangered:ff4500,orchid:da70d6,palegoldenrod:eee8aa,palegreen:98fb98,paleturquoise:afeeee,palevioletred:db7093,papayawhip:ffefd5,peachpuff:ffdab9,peru:cd853f,pink:ffc0cb,plum:dda0dd,powderblue:b0e0e6,purple:800080,rebeccapurple:663399,red:ff0000,rosybrown:bc8f8f,royalblue:4169e1,saddlebrown:8b4513,salmon:fa8072,sandybrown:f4a460,seagreen:2e8b57,seashell:fff5ee,sienna:a0522d,silver:c0c0c0,skyblue:87ceeb,slateblue:6a5acd,slategray:708090,slategrey:708090,snow:fffafa,springgreen:00ff7f,steelblue:4682b4,tan:d2b48c,teal:008080,thistle:d8bfd8,tomato:ff6347,turquoise:40e0d0,violet:ee82ee,wheat:f5deb3,white:ffffff,whitesmoke:f5f5f5,yellow:ffff00,yellowgreen:9acd32'
4
+
5
+ let table: Map<string, number> | undefined
6
+
7
+ /** The 0xRRGGBB value of a CSS named color, or undefined. Built on first use. */
8
+ export function namedColor(name: string): number | undefined {
9
+ table ??= new Map(
10
+ DATA.split(',').map((p) => [p.slice(0, p.indexOf(':')), Number.parseInt(p.slice(p.indexOf(':') + 1), 16)])
11
+ )
12
+ return table.get(name)
13
+ }