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,66 @@
|
|
|
1
|
+
import { type cli, command, t } from 'clibuilder'
|
|
2
|
+
import { fullOption, orEmpty, result } from 'clibuilder/axi'
|
|
3
|
+
import { toCss, toHex } from '../../color/index.js'
|
|
4
|
+
import { semanticPalette, validate } from '../../palette/index.js'
|
|
5
|
+
import { GAMUTS, MODES, readColor, round } from '../shared.js'
|
|
6
|
+
|
|
7
|
+
const definition = command({
|
|
8
|
+
name: 'palette',
|
|
9
|
+
description:
|
|
10
|
+
'A semantic palette from one brand color: primary, status colors, surface and outline, each with a text color and its WCAG 2 ratio',
|
|
11
|
+
arguments: [{ name: 'brand', description: 'The brand color', type: t.string() }],
|
|
12
|
+
options: {
|
|
13
|
+
mode: { description: 'light or dark', type: t.enum([...MODES]), default: 'light' },
|
|
14
|
+
target: {
|
|
15
|
+
description: 'The WCAG 2 ratio every text pair must reach',
|
|
16
|
+
type: t.number({ min: 1, max: 21 }),
|
|
17
|
+
default: 4.5
|
|
18
|
+
},
|
|
19
|
+
gamut: { description: 'The gamut every color must fit', type: t.enum([...GAMUTS]), default: 'srgb' },
|
|
20
|
+
full: fullOption
|
|
21
|
+
},
|
|
22
|
+
run(args) {
|
|
23
|
+
const brand = readColor(args.brand, 'brand')
|
|
24
|
+
const made = semanticPalette({ brand, mode: args.mode, target: args.target, gamut: args.gamut })
|
|
25
|
+
const checked = validate(made, { target: args.target, gamut: args.gamut })
|
|
26
|
+
const roles = Object.entries(made.roles).map(([role, p]) => ({
|
|
27
|
+
role,
|
|
28
|
+
bg: toHex(p.bg),
|
|
29
|
+
fg: toHex(p.fg),
|
|
30
|
+
ratio: round(p.ratio),
|
|
31
|
+
target: p.target,
|
|
32
|
+
pass: p.ratio >= p.target
|
|
33
|
+
}))
|
|
34
|
+
const failing = roles.filter((r) => !r.pass).length
|
|
35
|
+
const full = args.full === true
|
|
36
|
+
return result(
|
|
37
|
+
{
|
|
38
|
+
brand: args.brand,
|
|
39
|
+
mode: args.mode,
|
|
40
|
+
contrast: made.meta.contrast,
|
|
41
|
+
passing: roles.length - failing,
|
|
42
|
+
failing,
|
|
43
|
+
roles,
|
|
44
|
+
issues: orEmpty(
|
|
45
|
+
checked.issues.map((i) => ({ kind: i.kind, path: i.path, message: i.message })),
|
|
46
|
+
'none: every pair meets its target and every color fits the gamut'
|
|
47
|
+
),
|
|
48
|
+
...(full
|
|
49
|
+
? {
|
|
50
|
+
primary: made.scales.primary.map((c) => toCss(c)),
|
|
51
|
+
neutral: made.scales.neutral.map((c) => toCss(c))
|
|
52
|
+
}
|
|
53
|
+
: {})
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
help: [
|
|
57
|
+
`Run \`color-map export '${args.brand}' --source palette --mode ${args.mode} --to css\` for CSS variables`,
|
|
58
|
+
`Run \`color-map palette '${args.brand}' --mode ${args.mode === 'light' ? 'dark' : 'light'}\` for the other mode`
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/** Typed as the plain command so its declaration does not name args-minus' internals. */
|
|
66
|
+
export const palette: cli.Command = definition
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type cli, command, t } from 'clibuilder'
|
|
2
|
+
import { CliError, exitCodes, fullOption, result } from 'clibuilder/axi'
|
|
3
|
+
import { type Color, contrast, inGamut, toCss, toHex } from '../../color/index.js'
|
|
4
|
+
import { createContrastScale, createScale, scaleFromBrand } from '../../scale/index.js'
|
|
5
|
+
import { GAMUTS, MODES, PRESETS, readColor, round } from '../shared.js'
|
|
6
|
+
|
|
7
|
+
const WHITE = '#ffffff'
|
|
8
|
+
const BLACK = '#000000'
|
|
9
|
+
|
|
10
|
+
function rows(colors: Color[], full: boolean) {
|
|
11
|
+
return colors.map((color, i) => ({
|
|
12
|
+
step: i + 1,
|
|
13
|
+
hex: toHex(color),
|
|
14
|
+
onWhite: round(contrast(color, WHITE)),
|
|
15
|
+
onBlack: round(contrast(color, BLACK)),
|
|
16
|
+
...(full ? { css: toCss(color), inSrgb: inGamut(color, 'srgb') } : {})
|
|
17
|
+
}))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const definition = command({
|
|
21
|
+
name: 'scale',
|
|
22
|
+
description: 'Build a color scale from a seed color: a preset, a Radix-style brand scale, or contrast targets',
|
|
23
|
+
arguments: [{ name: 'seed', description: 'The color the scale is built around', type: t.string() }],
|
|
24
|
+
options: {
|
|
25
|
+
preset: { description: 'The scale shape', type: t.enum([...PRESETS]), default: 'tailwind-11' },
|
|
26
|
+
steps: { description: 'Steps of an even scale; replaces --preset', type: t.int({ min: 2 }).optional() },
|
|
27
|
+
brand: { description: 'Build a 12-step UI scale where the seed is step 9 exactly', type: t.boolean().optional() },
|
|
28
|
+
mode: { description: 'light or dark, for --brand', type: t.enum([...MODES]), default: 'light' },
|
|
29
|
+
background: {
|
|
30
|
+
description: 'The background: page color for --brand, or what --targets are measured against',
|
|
31
|
+
type: t.string().optional()
|
|
32
|
+
},
|
|
33
|
+
targets: {
|
|
34
|
+
description: 'Contrast ratios, one per step, such as 3,4.5,7: builds a contrast scale',
|
|
35
|
+
type: t.array(t.number({ min: 1, max: 21 })).optional()
|
|
36
|
+
},
|
|
37
|
+
gamut: { description: 'The gamut every step must fit', type: t.enum([...GAMUTS]), default: 'srgb' },
|
|
38
|
+
full: fullOption
|
|
39
|
+
},
|
|
40
|
+
run(args) {
|
|
41
|
+
const full = args.full === true
|
|
42
|
+
const seed = readColor(args.seed, 'seed')
|
|
43
|
+
const background = args.background === undefined ? undefined : readColor(args.background, 'background')
|
|
44
|
+
if (args.targets) {
|
|
45
|
+
if (!background)
|
|
46
|
+
throw new CliError('--targets needs --background', {
|
|
47
|
+
code: 'missing-option',
|
|
48
|
+
exitCode: exitCodes.usage,
|
|
49
|
+
details: { option: 'background' },
|
|
50
|
+
help: `Run \`color-map scale '${args.seed}' --targets 3 --targets 4.5 --background '#ffffff'\``
|
|
51
|
+
})
|
|
52
|
+
const steps = createContrastScale({ seed, background, targets: args.targets })
|
|
53
|
+
const rows = steps.map((s) => ({
|
|
54
|
+
step: Number(s.name) + 1,
|
|
55
|
+
target: s.target,
|
|
56
|
+
hex: toHex(s.color),
|
|
57
|
+
ratio: round(s.contrast),
|
|
58
|
+
met: s.met,
|
|
59
|
+
...(full ? { css: toCss(s.color) } : {})
|
|
60
|
+
}))
|
|
61
|
+
return result({ kind: 'contrast', seed: args.seed, background: args.background, steps: rows })
|
|
62
|
+
}
|
|
63
|
+
const colors = args.brand
|
|
64
|
+
? scaleFromBrand(seed, { mode: args.mode, background, gamut: args.gamut })
|
|
65
|
+
: createScale({ seed, steps: args.steps ?? args.preset, gamut: args.gamut })
|
|
66
|
+
const kind = args.brand ? 'brand-12' : args.steps ? `even-${args.steps}` : args.preset
|
|
67
|
+
return result(
|
|
68
|
+
{ kind, seed: args.seed, steps: rows(colors, full) },
|
|
69
|
+
{ help: `Run \`color-map export '${args.seed}' --source scale --to tailwind\` to get it as a Tailwind v4 theme` }
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/** Typed as the plain command so its declaration does not name args-minus' internals. */
|
|
75
|
+
export const scale: cli.Command = definition
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type cli, command, t } from 'clibuilder'
|
|
2
|
+
import { CliError, document, exitCodes, result } from 'clibuilder/axi'
|
|
3
|
+
import { type SchemaSource, schemaOf } from './schemas.js'
|
|
4
|
+
|
|
5
|
+
/** The `schema` command, bound to the commands it describes. */
|
|
6
|
+
export function schemaCommand(sources: readonly SchemaSource[]): cli.Command {
|
|
7
|
+
const definition = command({
|
|
8
|
+
name: 'schema',
|
|
9
|
+
description: "Print the JSON Schema of a command's arguments and options",
|
|
10
|
+
arguments: [
|
|
11
|
+
{ name: 'command', description: 'The command to describe; leave out to list them', type: t.string().optional() }
|
|
12
|
+
],
|
|
13
|
+
run(args) {
|
|
14
|
+
if (args.command === undefined)
|
|
15
|
+
return result(
|
|
16
|
+
{ commands: sources.map((s) => s.name) },
|
|
17
|
+
{ help: 'Run `color-map schema <command>` for one schema' }
|
|
18
|
+
)
|
|
19
|
+
const source = sources.find((s) => s.name === args.command)
|
|
20
|
+
if (!source)
|
|
21
|
+
throw new CliError(`no command named "${args.command}"`, {
|
|
22
|
+
code: 'unknown-command',
|
|
23
|
+
exitCode: exitCodes.usage,
|
|
24
|
+
details: { command: args.command },
|
|
25
|
+
help: 'Run `color-map schema` to list the commands'
|
|
26
|
+
})
|
|
27
|
+
return document(`${JSON.stringify(schemaOf(source), null, 2)}\n`)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
return definition
|
|
31
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** The introspection data of a clibuilder `t` descriptor, as far as the schema needs it. */
|
|
2
|
+
interface Data {
|
|
3
|
+
kind: string
|
|
4
|
+
min?: number
|
|
5
|
+
max?: number
|
|
6
|
+
values?: readonly string[]
|
|
7
|
+
element?: Data
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The slice of a clibuilder command that a schema is read from. */
|
|
11
|
+
export interface SchemaSource {
|
|
12
|
+
name: string
|
|
13
|
+
description?: string | undefined
|
|
14
|
+
arguments?:
|
|
15
|
+
| readonly { name: string; description?: string | undefined; type?: { [k: string]: any } | undefined }[]
|
|
16
|
+
| undefined
|
|
17
|
+
options?: Record<
|
|
18
|
+
string,
|
|
19
|
+
{ description?: string | undefined; type?: { [k: string]: any } | undefined; default?: unknown } | undefined
|
|
20
|
+
>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Json = Record<string, unknown>
|
|
24
|
+
|
|
25
|
+
function ofType(data: Data): Json {
|
|
26
|
+
switch (data.kind) {
|
|
27
|
+
case 'string':
|
|
28
|
+
return { type: 'string' }
|
|
29
|
+
case 'number':
|
|
30
|
+
case 'int': {
|
|
31
|
+
const { min, max } = data
|
|
32
|
+
return {
|
|
33
|
+
type: data.kind === 'int' ? 'integer' : 'number',
|
|
34
|
+
...(min === undefined ? {} : { minimum: min }),
|
|
35
|
+
...(max === undefined ? {} : { maximum: max })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
case 'boolean':
|
|
39
|
+
return { type: 'boolean' }
|
|
40
|
+
case 'enum':
|
|
41
|
+
return { type: 'string', enum: [...(data.values as readonly string[])] }
|
|
42
|
+
case 'array':
|
|
43
|
+
return { type: 'array', items: ofType(data.element as Data) }
|
|
44
|
+
default:
|
|
45
|
+
// literal, custom and json are not used by this CLI's options
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function property(entry: NonNullable<SchemaSource['options']>[string], extra: Json = {}): Json {
|
|
51
|
+
const { description, type, default: value } = entry ?? {}
|
|
52
|
+
return {
|
|
53
|
+
...ofType((type ?? { kind: 'string' }) as Data),
|
|
54
|
+
...(description === undefined ? {} : { description }),
|
|
55
|
+
...(value === undefined ? {} : { default: value }),
|
|
56
|
+
...extra
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The JSON Schema (2020-12) of one command's arguments and options, read from
|
|
62
|
+
* the same descriptors the parser uses, so the two cannot drift. An argument
|
|
63
|
+
* or option with no default that is not optional is `required`.
|
|
64
|
+
*/
|
|
65
|
+
export function schemaOf(command: SchemaSource): Json {
|
|
66
|
+
const properties: Record<string, Json> = {}
|
|
67
|
+
const required: string[] = []
|
|
68
|
+
for (const arg of command.arguments ?? []) {
|
|
69
|
+
properties[arg.name] = property(arg, { 'x-kind': 'argument' })
|
|
70
|
+
if (!arg.type?.isOptional) required.push(arg.name)
|
|
71
|
+
}
|
|
72
|
+
for (const [name, option] of Object.entries(command.options ?? {})) {
|
|
73
|
+
properties[name] = property(option, { 'x-kind': 'option' })
|
|
74
|
+
if (option && !option.type?.isOptional && option.default === undefined) required.push(name)
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
78
|
+
$id: `https://unpkg.com/color-map/schemas/${command.name}.schema.json`,
|
|
79
|
+
title: `color-map ${command.name}`,
|
|
80
|
+
...(command.description === undefined ? {} : { description: command.description }),
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties,
|
|
83
|
+
required,
|
|
84
|
+
additionalProperties: false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CliError, exitCodes } from 'clibuilder/axi'
|
|
2
|
+
import { type Color, type ColorSpaceId, parseExpr, resolve, toCss, toHex } from '../color/index.js'
|
|
3
|
+
|
|
4
|
+
/** Every space `convert` can target, with the CSS names. */
|
|
5
|
+
export const SPACES = [
|
|
6
|
+
'srgb',
|
|
7
|
+
'srgb-linear',
|
|
8
|
+
'hsl',
|
|
9
|
+
'hwb',
|
|
10
|
+
'lab',
|
|
11
|
+
'lch',
|
|
12
|
+
'oklab',
|
|
13
|
+
'oklch',
|
|
14
|
+
'display-p3',
|
|
15
|
+
'display-p3-linear',
|
|
16
|
+
'a98-rgb',
|
|
17
|
+
'prophoto-rgb',
|
|
18
|
+
'rec2020',
|
|
19
|
+
'xyz-d50',
|
|
20
|
+
'xyz-d65'
|
|
21
|
+
] as const satisfies readonly ColorSpaceId[]
|
|
22
|
+
|
|
23
|
+
/** The bounded RGB spaces a color can be mapped into. */
|
|
24
|
+
export const GAMUTS = [
|
|
25
|
+
'srgb',
|
|
26
|
+
'srgb-linear',
|
|
27
|
+
'display-p3',
|
|
28
|
+
'display-p3-linear',
|
|
29
|
+
'a98-rgb',
|
|
30
|
+
'prophoto-rgb',
|
|
31
|
+
'rec2020'
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
export const PRESETS = ['tailwind-11', 'radix-12', 'tone-13', 'even'] as const
|
|
35
|
+
export const MODES = ['light', 'dark'] as const
|
|
36
|
+
export const HUE_METHODS = ['shorter', 'longer', 'increasing', 'decreasing'] as const
|
|
37
|
+
export const EXPORT_TARGETS = ['dtcg', 'css', 'tailwind'] as const
|
|
38
|
+
export const EXPORT_SOURCES = ['palette', 'scale'] as const
|
|
39
|
+
export const POLAR_SPACES = ['hsl', 'hwb', 'lch', 'oklch'] as const
|
|
40
|
+
|
|
41
|
+
/** Round to `digits` decimals; keeps TOON tables short. */
|
|
42
|
+
export const round = (n: number, digits = 2): number => Number(n.toFixed(digits))
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads a color from its CSS text: any Color 4 form, plus `color-mix()`,
|
|
46
|
+
* relative colors and `light-dark()` (resolved for the light scheme).
|
|
47
|
+
* Fails with a coded usage error that names the text.
|
|
48
|
+
*/
|
|
49
|
+
export function readColor(text: string, name = 'color'): Color {
|
|
50
|
+
try {
|
|
51
|
+
return resolve(parseExpr(text), { colorScheme: 'light' })
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
throw new CliError(`cannot read ${name} "${text}" as a CSS color`, {
|
|
54
|
+
code: 'invalid-color',
|
|
55
|
+
exitCode: exitCodes.usage,
|
|
56
|
+
details: { [name]: text, reason: (cause as Error).message },
|
|
57
|
+
help: 'Pass a CSS color such as "#3b82f6", "oklch(0.7 0.15 250)" or "color-mix(in oklab, red, blue)"'
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A color as the essential fields: CSS text and hex. `--full` adds the parts. */
|
|
63
|
+
export function describe(color: Color, full: boolean): Record<string, string | number | boolean | null> {
|
|
64
|
+
const base = { css: toCss(color), hex: toHex(color) }
|
|
65
|
+
if (!full) return base
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
space: color.colorSpace,
|
|
69
|
+
components: color.components.map((c) => (c === null ? 'none' : round(c, 5))).join(' '),
|
|
70
|
+
alpha: color.alpha === undefined ? 1 : color.alpha
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Writable } from 'node:stream'
|
|
2
|
+
import { createApp } from './cli.js'
|
|
3
|
+
|
|
4
|
+
/** Runs the CLI in process and returns what it wrote and how it exited. */
|
|
5
|
+
export async function run(...argv: string[]) {
|
|
6
|
+
let out = ''
|
|
7
|
+
let err = ''
|
|
8
|
+
const sink = (append: (s: string) => void) =>
|
|
9
|
+
new Writable({
|
|
10
|
+
decodeStrings: false,
|
|
11
|
+
write(chunk, _encoding, callback) {
|
|
12
|
+
append(String(chunk))
|
|
13
|
+
callback()
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
const code = await createApp({
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
streams: { stdout: sink((s) => (out += s)), stderr: sink((s) => (err += s)) }
|
|
19
|
+
}).parse(['node', 'color-map', ...argv])
|
|
20
|
+
return { out, err, code: typeof code === 'number' ? code : 0 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Runs with `--format json` and parses stdout. */
|
|
24
|
+
export async function runJson(...argv: string[]) {
|
|
25
|
+
const r = await run(...argv, '--format', 'json')
|
|
26
|
+
return { ...r, json: JSON.parse(r.out) as Record<string, any> }
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { a98ToXyz, xyzToA98 } from './matrices.js'
|
|
2
|
+
import { rgbSpace } from './rgb.js'
|
|
3
|
+
import { power } from './transfer.js'
|
|
4
|
+
|
|
5
|
+
const [toLinear, toGamma] = power(563 / 256)
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The CSS `a98-rgb` space (Adobe RGB 1998). Components are red, green and blue in 0 to 1, with a 563/256 power transfer curve.
|
|
9
|
+
* Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
|
|
10
|
+
*/
|
|
11
|
+
export const a98Rgb = rgbSpace('a98-rgb', toLinear, toGamma, a98ToXyz, xyzToA98)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Color } from './types.js'
|
|
2
|
+
|
|
3
|
+
const byte = (n: number): number => Math.min(255, Math.max(0, Math.round(n * 255)))
|
|
4
|
+
|
|
5
|
+
/** An sRGB color from a 32-bit ARGB integer, as Material Color Utilities uses. */
|
|
6
|
+
export function fromArgb(argb: number): Color {
|
|
7
|
+
const n = argb >>> 0
|
|
8
|
+
return {
|
|
9
|
+
colorSpace: 'srgb',
|
|
10
|
+
components: [((n >>> 16) & 255) / 255, ((n >>> 8) & 255) / 255, (n & 255) / 255],
|
|
11
|
+
alpha: (n >>> 24) / 255
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The unsigned 32-bit ARGB integer for an sRGB color. Convert other spaces to `srgb` first. */
|
|
16
|
+
export function toArgb(color: Color): number {
|
|
17
|
+
if (color.colorSpace !== 'srgb') throw new Error(`color-map: toArgb needs an srgb color, got "${color.colorSpace}"`)
|
|
18
|
+
const [r, g, b] = color.components
|
|
19
|
+
const a = color.alpha ?? 1
|
|
20
|
+
return ((byte(a) << 24) | (byte(r ?? 0) << 16) | (byte(g ?? 0) << 8) | byte(b ?? 0)) >>> 0
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { fail, UNITS } from './parse.js'
|
|
2
|
+
|
|
3
|
+
const TOKEN = /\s*(?:(\d*\.?\d+(?:e[+-]?\d+)?)([a-z%]*)|([a-z][a-z0-9-]*)|([-+*/(),]))/y
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Evaluates the `calc()` subset relative colors accept: numbers (angles become
|
|
7
|
+
* degrees), `+ - * /`, parentheses, `calc`, `min`, `max`, `clamp`, the
|
|
8
|
+
* constants `pi` and `e`, and the channel keywords in `vars`.
|
|
9
|
+
*/
|
|
10
|
+
export function evaluate(text: string, vars: Record<string, number>): number {
|
|
11
|
+
const tokens: string[] = []
|
|
12
|
+
TOKEN.lastIndex = 0
|
|
13
|
+
let end = 0
|
|
14
|
+
for (let m = TOKEN.exec(text); m; m = TOKEN.exec(text)) {
|
|
15
|
+
end = TOKEN.lastIndex
|
|
16
|
+
if (m[1] === undefined) tokens.push(m[3] ?? m[4])
|
|
17
|
+
else {
|
|
18
|
+
const unit = UNITS[m[2]]
|
|
19
|
+
if (unit === undefined) return fail(text, 'units other than angles are not supported in calc()', m[1] + m[2])
|
|
20
|
+
tokens.push(String(Number(m[1]) * unit))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (end !== text.trimEnd().length) return fail(text, 'invalid calc() expression')
|
|
24
|
+
let pos = 0
|
|
25
|
+
const next = (): string => tokens[pos++]
|
|
26
|
+
const eat = (t: string): boolean => (tokens[pos] === t ? ++pos > 0 : false)
|
|
27
|
+
const need = (t: string): void => {
|
|
28
|
+
if (!eat(t)) fail(text, `expected "${t}" in calc()`, tokens[pos])
|
|
29
|
+
}
|
|
30
|
+
function sum(): number {
|
|
31
|
+
let v = product()
|
|
32
|
+
for (;;) {
|
|
33
|
+
if (eat('+')) v += product()
|
|
34
|
+
else if (eat('-')) v -= product()
|
|
35
|
+
else return v
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function product(): number {
|
|
39
|
+
let v = unary()
|
|
40
|
+
for (;;) {
|
|
41
|
+
if (eat('*')) v *= unary()
|
|
42
|
+
else if (eat('/')) v /= unary()
|
|
43
|
+
else return v
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function unary(): number {
|
|
47
|
+
if (eat('-')) return -unary()
|
|
48
|
+
if (eat('+')) return unary()
|
|
49
|
+
return atom()
|
|
50
|
+
}
|
|
51
|
+
function args(): number[] {
|
|
52
|
+
const list = [sum()]
|
|
53
|
+
while (eat(',')) list.push(sum())
|
|
54
|
+
need(')')
|
|
55
|
+
return list
|
|
56
|
+
}
|
|
57
|
+
function atom(): number {
|
|
58
|
+
const t = next()
|
|
59
|
+
if (t === undefined) return fail(text, 'unfinished calc() expression')
|
|
60
|
+
if (/^[\d.]/.test(t)) return Number(t)
|
|
61
|
+
if (t === '(') {
|
|
62
|
+
const v = sum()
|
|
63
|
+
need(')')
|
|
64
|
+
return v
|
|
65
|
+
}
|
|
66
|
+
if (!/^[a-z]/.test(t)) return fail(text, 'unexpected token', t)
|
|
67
|
+
if (eat('(')) {
|
|
68
|
+
const list = args()
|
|
69
|
+
if (t === 'calc' && list.length === 1) return list[0]
|
|
70
|
+
if (t === 'min') return Math.min(...list)
|
|
71
|
+
if (t === 'max') return Math.max(...list)
|
|
72
|
+
if (t === 'clamp' && list.length === 3) return Math.min(list[2], Math.max(list[0], list[1]))
|
|
73
|
+
return fail(text, 'unknown function', t)
|
|
74
|
+
}
|
|
75
|
+
if (t === 'pi') return Math.PI
|
|
76
|
+
if (t === 'e') return Math.E
|
|
77
|
+
const v = vars[t]
|
|
78
|
+
if (typeof v === 'number') return v
|
|
79
|
+
return fail(text, 'unknown keyword', t)
|
|
80
|
+
}
|
|
81
|
+
const value = sum()
|
|
82
|
+
if (pos < tokens.length) return fail(text, 'unexpected token', tokens[pos])
|
|
83
|
+
return value
|
|
84
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ComponentsIn, componentsOver, convertWith } from './convert.js'
|
|
2
|
+
import { oklab } from './oklab.js'
|
|
3
|
+
import { componentsIn } from './spaces.js'
|
|
4
|
+
import type { Color, Space } from './types.js'
|
|
5
|
+
|
|
6
|
+
/** Euclidean distance in OKLab, the ΔEOK of CSS Color 4 gamut mapping. */
|
|
7
|
+
export function deltaEOk(a: Color, b: Color): number {
|
|
8
|
+
return deltaEOkWith(componentsIn, a, b)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** {@link deltaEOk} over the spaces `componentsIn` knows, which must include `oklab`. */
|
|
12
|
+
export function deltaEOkWith(componentsIn: ComponentsIn, a: Color, b: Color): number {
|
|
13
|
+
const [l1, a1, b1] = componentsIn(a, 'oklab') as number[]
|
|
14
|
+
const [l2, a2, b2] = componentsIn(b, 'oklab') as number[]
|
|
15
|
+
return Math.hypot(l1 - l2, a1 - a2, b1 - b2)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds {@link deltaEOk} over the spaces given, plus `oklab`, so a bundle
|
|
20
|
+
* contains only those. A color in a space not given throws, naming that space.
|
|
21
|
+
*/
|
|
22
|
+
export function createDeltaEOk(...spaces: Space[]): (a: Color, b: Color) => number {
|
|
23
|
+
const over = componentsOver([oklab, ...spaces])
|
|
24
|
+
return (a, b) => deltaEOkWith(over, a, b)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const same = (x: number | null | undefined, y: number | null | undefined, epsilon: number): boolean =>
|
|
28
|
+
x === y || (x != null && y != null && Math.abs(x - y) <= epsilon)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True when both colors are the same within `epsilon` per component. Colors in
|
|
32
|
+
* different spaces are compared in the first one's space. `none` equals only
|
|
33
|
+
* `none`, and a missing alpha equals 1.
|
|
34
|
+
*/
|
|
35
|
+
export function equals(a: Color, b: Color, epsilon = 1e-6): boolean {
|
|
36
|
+
return equalsWith(componentsIn, a, b, epsilon)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function equalsWith(componentsIn: ComponentsIn, a: Color, b: Color, epsilon: number): boolean {
|
|
40
|
+
const other = convertWith(componentsIn, b, a.colorSpace)
|
|
41
|
+
return (
|
|
42
|
+
same(a.alpha ?? 1, other.alpha ?? 1, epsilon) && a.components.every((c, i) => same(c, other.components[i], epsilon))
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds {@link equals} over the spaces given, so a bundle contains only those.
|
|
48
|
+
* It needs no space of its own: give the spaces of both colors.
|
|
49
|
+
*/
|
|
50
|
+
export function createEquals(...spaces: Space[]): (a: Color, b: Color, epsilon?: number) => boolean {
|
|
51
|
+
const over = componentsOver(spaces)
|
|
52
|
+
return (a, b, epsilon = 1e-6) => equalsWith(over, a, b, epsilon)
|
|
53
|
+
}
|