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