color-map 2.1.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 -11
- 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 +27 -5
- package/cjs/index.d.ts.map +1 -0
- package/cjs/index.js +89 -20
- 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/package.json +1 -1
- 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 +1 -1
- 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 -5
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +53 -4
- package/esm/index.js.map +1 -1
- package/esm/legacy/createColors.d.ts +9 -0
- package/esm/legacy/createColors.d.ts.map +1 -0
- package/esm/legacy/createColors.js +32 -0
- 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/legacy/createColorsFromMap.js +24 -0
- 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} +2 -1
- package/esm/legacy/rgbHex.d.ts.map +1 -0
- package/esm/legacy/rgbHex.js +17 -0
- package/esm/legacy/rgbHex.js.map +1 -0
- package/esm/legacy/rgbaString.d.ts +4 -0
- package/esm/legacy/rgbaString.d.ts.map +1 -0
- 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 +240 -56
- 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/src/legacy/createColors.ts +36 -0
- package/src/legacy/createColorsFromMap.ts +29 -0
- package/src/legacy/interfaces.ts +19 -0
- package/{ts → src/legacy}/rgbHex.ts +7 -7
- package/src/legacy/rgbaString.ts +6 -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/cjs/createColors.d.ts +0 -2
- package/cjs/createColors.js +0 -40
- package/cjs/createColors.js.map +0 -1
- package/cjs/createColors.spec.d.ts +0 -1
- package/cjs/createColors.spec.js +0 -176
- package/cjs/createColors.spec.js.map +0 -1
- package/cjs/createColorsFromMap.d.ts +0 -5
- package/cjs/createColorsFromMap.js +0 -25
- package/cjs/createColorsFromMap.js.map +0 -1
- package/cjs/createColorsFromMap.spec.d.ts +0 -1
- package/cjs/createColorsFromMap.spec.js +0 -11
- package/cjs/createColorsFromMap.spec.js.map +0 -1
- package/cjs/interfaces.d.ts +0 -14
- package/cjs/interfaces.js +0 -3
- package/cjs/interfaces.js.map +0 -1
- package/cjs/rgbHex.d.ts +0 -6
- package/cjs/rgbHex.js +0 -19
- package/cjs/rgbHex.js.map +0 -1
- package/cjs/rgbHex.spec.d.ts +0 -1
- package/cjs/rgbHex.spec.js +0 -16
- package/cjs/rgbHex.spec.js.map +0 -1
- package/cjs/rgbaString.d.ts +0 -2
- package/cjs/rgbaString.js +0 -7
- package/cjs/rgbaString.js.map +0 -1
- package/cjs/rgbaString.spec.d.ts +0 -1
- package/cjs/rgbaString.spec.js +0 -10
- package/cjs/rgbaString.spec.js.map +0 -1
- package/esm/createColors.d.ts +0 -2
- package/esm/createColors.js +0 -27
- package/esm/createColors.js.map +0 -1
- package/esm/createColors.spec.d.ts +0 -1
- package/esm/createColors.spec.js +0 -171
- package/esm/createColors.spec.js.map +0 -1
- package/esm/createColorsFromMap.d.ts +0 -5
- package/esm/createColorsFromMap.js +0 -22
- package/esm/createColorsFromMap.js.map +0 -1
- package/esm/createColorsFromMap.spec.d.ts +0 -1
- package/esm/createColorsFromMap.spec.js +0 -9
- package/esm/createColorsFromMap.spec.js.map +0 -1
- package/esm/interfaces.d.ts +0 -14
- package/esm/interfaces.js +0 -2
- package/esm/interfaces.js.map +0 -1
- package/esm/rgbHex.js +0 -16
- package/esm/rgbHex.js.map +0 -1
- package/esm/rgbHex.spec.d.ts +0 -1
- package/esm/rgbHex.spec.js +0 -14
- package/esm/rgbHex.spec.js.map +0 -1
- package/esm/rgbaString.d.ts +0 -2
- package/esm/rgbaString.js +0 -4
- package/esm/rgbaString.js.map +0 -1
- package/esm/rgbaString.spec.d.ts +0 -1
- package/esm/rgbaString.spec.js +0 -8
- package/esm/rgbaString.spec.js.map +0 -1
- package/ts/createColors.ts +0 -35
- package/ts/createColorsFromMap.ts +0 -26
- package/ts/interfaces.ts +0 -15
- package/ts/rgbaString.ts +0 -5
- package/{ts → src/legacy}/index.ts +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { srgbToXyz, xyzToSrgb } from './matrices.js'
|
|
2
|
+
import { rgbSpace } from './rgb.js'
|
|
3
|
+
import { identity } from './transfer.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The CSS `srgb-linear` space: sRGB 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 srgbLinear = rgbSpace('srgb-linear', identity, identity, srgbToXyz, xyzToSrgb)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { srgbToXyz, xyzToSrgb } from './matrices.js'
|
|
2
|
+
import { rgbSpace } from './rgb.js'
|
|
3
|
+
import { srgbToGamma, srgbToLinear } from './transfer.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The CSS `srgb` 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 srgb = rgbSpace('srgb', srgbToLinear, srgbToGamma, srgbToXyz, xyzToSrgb)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { signed } from './math.js'
|
|
2
|
+
|
|
3
|
+
export const identity = (n: number): number => n
|
|
4
|
+
|
|
5
|
+
export const srgbToLinear = signed((a) => (a <= 0.04045 ? a / 12.92 : ((a + 0.055) / 1.055) ** 2.4))
|
|
6
|
+
export const srgbToGamma = signed((a) => (a > 0.0031308 ? 1.055 * a ** (1 / 2.4) - 0.055 : 12.92 * a))
|
|
7
|
+
|
|
8
|
+
export const power = (gamma: number): [(n: number) => number, (n: number) => number] => [
|
|
9
|
+
signed((a) => a ** gamma),
|
|
10
|
+
signed((a) => a ** (1 / gamma))
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export const prophotoToLinear = signed((a) => (a <= 16 / 512 ? a / 16 : a ** 1.8))
|
|
14
|
+
export const prophotoToGamma = signed((a) => (a >= 1 / 512 ? a ** (1 / 1.8) : 16 * a))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The spaces a {@link Color} can be in. Names are the CSS ones. */
|
|
2
|
+
export type ColorSpaceId =
|
|
3
|
+
| 'srgb'
|
|
4
|
+
| 'srgb-linear'
|
|
5
|
+
| 'hsl'
|
|
6
|
+
| 'hwb'
|
|
7
|
+
| 'lab'
|
|
8
|
+
| 'lch'
|
|
9
|
+
| 'oklab'
|
|
10
|
+
| 'oklch'
|
|
11
|
+
| 'display-p3'
|
|
12
|
+
| 'display-p3-linear'
|
|
13
|
+
| 'a98-rgb'
|
|
14
|
+
| 'prophoto-rgb'
|
|
15
|
+
| 'rec2020'
|
|
16
|
+
| 'xyz-d50'
|
|
17
|
+
| 'xyz-d65'
|
|
18
|
+
|
|
19
|
+
/** A component value. `null` stands for the CSS keyword `none`. */
|
|
20
|
+
export type Component = number | null
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A color as plain JSON. Component ranges follow CSS: `srgb` is 0 to 1, `hsl`
|
|
24
|
+
* and `hwb` take hue in degrees and the rest in 0 to 100, `lab` and `lch` take
|
|
25
|
+
* L in 0 to 100, `oklab` and `oklch` take L in 0 to 1. A missing `alpha` is 1.
|
|
26
|
+
*/
|
|
27
|
+
export interface Color {
|
|
28
|
+
colorSpace: ColorSpaceId
|
|
29
|
+
components: Component[]
|
|
30
|
+
alpha?: Component
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Three numbers: the components of a color without alpha, or a point in XYZ. */
|
|
34
|
+
export type Vec3 = [number, number, number]
|
|
35
|
+
|
|
36
|
+
/** A color space, defined by its conversion to and from the XYZ-D65 hub. */
|
|
37
|
+
export interface Space {
|
|
38
|
+
id: ColorSpaceId
|
|
39
|
+
toXyz(c: Vec3): Vec3
|
|
40
|
+
fromXyz(xyz: Vec3): Vec3
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error for a name a caller wrote that the library does not know: it names
|
|
3
|
+
* the value and lists the valid ones, in the style of `unknown color space`.
|
|
4
|
+
*/
|
|
5
|
+
export function unknownName(what: string, value: unknown, valid: readonly string[]): Error {
|
|
6
|
+
return new Error(
|
|
7
|
+
`color-map: unknown ${what} "${String(value)}"${valid.length ? `; use one of ${valid.join(', ')}` : ''}`
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** `mode` itself when it is `light` or `dark`; any other value is an unknown mode. */
|
|
12
|
+
export function checkMode<T>(mode: T): T {
|
|
13
|
+
if (mode !== 'light' && mode !== 'dark') throw unknownName('mode', mode, ['light', 'dark'])
|
|
14
|
+
return mode
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { multiply } from './math.js'
|
|
2
|
+
import { d50ToD65, d65ToD50 } from './matrices.js'
|
|
3
|
+
import type { Space } from './types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The CSS `xyz-d50` space (CIE XYZ, D50 white). Components are X, Y and Z, where Y is 1 for white.
|
|
7
|
+
* Pass it to `createConverter` and to the narrow helpers such as `createMix` and `createToGamut`.
|
|
8
|
+
*/
|
|
9
|
+
export const xyzD50: Space = {
|
|
10
|
+
id: 'xyz-d50',
|
|
11
|
+
toXyz: (c) => multiply(d50ToD65, c[0], c[1], c[2]),
|
|
12
|
+
fromXyz: (xyz) => multiply(d65ToD50, xyz[0], xyz[1], xyz[2])
|
|
13
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The root re-exports the four subpaths, which stay the stable contract. The
|
|
2
|
+
// legacy names stay here through 2.x, deprecated in favor of `color-map/legacy`,
|
|
3
|
+
// and leave the root in 3.0. They are aliases rather than `export *` so that the
|
|
4
|
+
// deprecation shows only at the root and not in `color-map/legacy`.
|
|
5
|
+
import {
|
|
6
|
+
type Alpha as LegacyAlpha,
|
|
7
|
+
type ColorMap as LegacyColorMap,
|
|
8
|
+
type RGB as LegacyRGB,
|
|
9
|
+
type RGBA as LegacyRGBA,
|
|
10
|
+
createColors as legacyCreateColors,
|
|
11
|
+
createColorsFromMap as legacyCreateColorsFromMap,
|
|
12
|
+
rgbaString as legacyRgbaString,
|
|
13
|
+
rgbHex as legacyRgbHex
|
|
14
|
+
} from './legacy/index.js'
|
|
15
|
+
|
|
16
|
+
export * from './color/index.js'
|
|
17
|
+
export * from './palette/index.js'
|
|
18
|
+
export * from './scale/index.js'
|
|
19
|
+
export * from './tokens/index.js'
|
|
20
|
+
|
|
21
|
+
/** @deprecated Import it from `color-map/legacy`. The root stops exporting it in 3.0. */
|
|
22
|
+
export const createColors = legacyCreateColors
|
|
23
|
+
/** @deprecated Use `createGradient` from `color-map`, or import it from `color-map/legacy`. The root stops exporting it in 3.0. */
|
|
24
|
+
export const createColorsFromMap = legacyCreateColorsFromMap
|
|
25
|
+
/** @deprecated Import it from `color-map/legacy`, or use `toHex` from `color-map`. The root stops exporting it in 3.0. */
|
|
26
|
+
export const rgbHex = legacyRgbHex
|
|
27
|
+
/** @deprecated Import it from `color-map/legacy`, or use `toCss` from `color-map`. The root stops exporting it in 3.0. */
|
|
28
|
+
export const rgbaString = legacyRgbaString
|
|
29
|
+
|
|
30
|
+
/** @deprecated Import it from `color-map/legacy`. The root stops exporting it in 3.0. */
|
|
31
|
+
export type Alpha = LegacyAlpha
|
|
32
|
+
/** @deprecated Import it from `color-map/legacy`. The root stops exporting it in 3.0. */
|
|
33
|
+
export type ColorMap = LegacyColorMap
|
|
34
|
+
/** @deprecated Import it from `color-map/legacy`. The root stops exporting it in 3.0. */
|
|
35
|
+
export type RGB = LegacyRGB
|
|
36
|
+
/** @deprecated Import it from `color-map/legacy`. The root stops exporting it in 3.0. */
|
|
37
|
+
export type RGBA = LegacyRGBA
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Alpha, RGB, RGBA } from './interfaces.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates `shades` colors that step evenly from `from` to `to`. Red, green and
|
|
5
|
+
* blue are rounded to whole numbers. Without `alpha`, the alpha of each end
|
|
6
|
+
* comes from its own fourth value, then from the other end, then is 1.
|
|
7
|
+
* Every result is an `RGBA`.
|
|
8
|
+
*/
|
|
9
|
+
export function createColors(
|
|
10
|
+
from: RGB | RGBA,
|
|
11
|
+
to: RGB | RGBA,
|
|
12
|
+
shades: number,
|
|
13
|
+
alpha: Alpha = [
|
|
14
|
+
from.length === 4 ? from[3] : to.length === 4 ? to[3] : 1,
|
|
15
|
+
to.length === 4 ? to[3] : from.length === 4 ? from[3] : 1
|
|
16
|
+
]
|
|
17
|
+
): RGBA[] {
|
|
18
|
+
const start = [...from]
|
|
19
|
+
const diff = [to[0] - from[0], to[1] - from[1], to[2] - from[2], alpha[1] - alpha[0]]
|
|
20
|
+
|
|
21
|
+
if (start.length === 3) start.push(alpha[0])
|
|
22
|
+
|
|
23
|
+
const inc = 1 / Math.max(shades - 1, 1)
|
|
24
|
+
|
|
25
|
+
const rgba: any[] = []
|
|
26
|
+
for (let i = 0; i < shades; i++) {
|
|
27
|
+
const color = [
|
|
28
|
+
Math.round(start[0] + i * diff[0] * inc),
|
|
29
|
+
Math.round(start[1] + i * diff[1] * inc),
|
|
30
|
+
Math.round(start[2] + i * diff[2] * inc),
|
|
31
|
+
start[3] + i * diff[3] * inc
|
|
32
|
+
]
|
|
33
|
+
rgba.push(color)
|
|
34
|
+
}
|
|
35
|
+
return rgba
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createColors } from './createColors.js'
|
|
2
|
+
import type { Alpha, ColorMap, RGBA } from './interfaces.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create colors with specified color map.
|
|
6
|
+
*
|
|
7
|
+
* @deprecated Use `createGradient` from `color-map`. Each of its stops
|
|
8
|
+
* carries its own alpha, and `{ in: 'srgb' }` comes close to these results.
|
|
9
|
+
*/
|
|
10
|
+
export function createColorsFromMap(colormap: ColorMap, shades: number, alpha?: Alpha): RGBA[] {
|
|
11
|
+
if (shades < colormap.length) {
|
|
12
|
+
throw new Error(`Requires at least ${colormap.length} shades.`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const result: RGBA[] = []
|
|
16
|
+
const steps: number[] = []
|
|
17
|
+
for (let i = 0; i < colormap.length; i++) {
|
|
18
|
+
steps.push(Math.round(colormap[i].index * shades))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < colormap.length - 1; i++) {
|
|
22
|
+
const n = steps[i + 1] - steps[i]
|
|
23
|
+
const from = colormap[i].rgb
|
|
24
|
+
const to = colormap[i + 1].rgb
|
|
25
|
+
result.push(...createColors(from, to, n, alpha))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Red, green and blue as whole numbers in 0 to 255. */
|
|
2
|
+
export type RGB = [number, number, number]
|
|
3
|
+
/** Red, green and blue as whole numbers in 0 to 255, then alpha in 0 to 1. */
|
|
4
|
+
export type RGBA = [number, number, number, number]
|
|
5
|
+
/** The alpha at the start and at the end of a run of colors, each in 0 to 1. */
|
|
6
|
+
export type Alpha = [number, number]
|
|
7
|
+
|
|
8
|
+
/** The segments of a gradient, in order, for `createColorsFromMap`. Each has an `index` and an `rgb`. */
|
|
9
|
+
export type ColorMap = {
|
|
10
|
+
/**
|
|
11
|
+
* Defines how fast or slow the `rgb` values will change from one segment to the next.
|
|
12
|
+
* i.e. the steepness of the gradient between two segments.
|
|
13
|
+
*/
|
|
14
|
+
index: number
|
|
15
|
+
/**
|
|
16
|
+
* RGB or RGBA. if RGBA, this alpha will be used instead of the alpha specified in the call.
|
|
17
|
+
*/
|
|
18
|
+
rgb: RGB | RGBA
|
|
19
|
+
}[]
|
|
@@ -5,14 +5,14 @@ import type { RGB, RGBA } from './interfaces.js'
|
|
|
5
5
|
* JavaScript note: no check for array length, use it properly.
|
|
6
6
|
*/
|
|
7
7
|
export function rgbHex(rgb: RGB | RGBA) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
let hex = '#'
|
|
9
|
+
for (let i = 0; i < 3; i++) {
|
|
10
|
+
hex += d2h(rgb[i])
|
|
11
|
+
}
|
|
12
|
+
return hex
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function d2h(d: number) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const s = (+d).toString(16)
|
|
17
|
+
return s.length < 2 ? '0' + s : s
|
|
18
18
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type GamutId, toGamut } from '../color/gamut.js'
|
|
2
|
+
import { componentsIn } from '../color/spaces.js'
|
|
3
|
+
import type { Color } from '../color/types.js'
|
|
4
|
+
import { unknownName } from '../color/unknown-name.js'
|
|
5
|
+
import { lstar, toneColor } from '../scale/tone.js'
|
|
6
|
+
|
|
7
|
+
/** How `darkVariant` builds a dark scale from a light one: `mirror`, `tone-swap` or `lightness-invert`. */
|
|
8
|
+
export type DarkStrategy = 'mirror' | 'tone-swap' | 'lightness-invert'
|
|
9
|
+
|
|
10
|
+
const STRATEGIES: readonly string[] = ['mirror', 'tone-swap', 'lightness-invert']
|
|
11
|
+
|
|
12
|
+
/** Options for `darkVariant`. */
|
|
13
|
+
export interface DarkVariantOptions {
|
|
14
|
+
/** The gamut every color must fit. Default `srgb`. */
|
|
15
|
+
gamut?: GamutId
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A dark version of a light scale (ordered light to dark), with the same
|
|
20
|
+
* number of steps.
|
|
21
|
+
* - `mirror`: step n takes the color of step N+1-n, so the scale runs dark to
|
|
22
|
+
* light and step 1 is the old last step. Colors are unchanged.
|
|
23
|
+
* - `tone-swap`: each color keeps its OKLCH hue and chroma and takes the CIE
|
|
24
|
+
* L* tone 100 minus its own, as Material swaps tone 40 for 80. Contrast
|
|
25
|
+
* between two steps is kept roughly the same.
|
|
26
|
+
* - `lightness-invert`: OKLCH lightness is reflected within the scale's own
|
|
27
|
+
* range, keeping step order, with chroma eased by 10% to curb vibration on
|
|
28
|
+
* dark backgrounds.
|
|
29
|
+
*/
|
|
30
|
+
export function darkVariant(
|
|
31
|
+
scale: readonly Color[],
|
|
32
|
+
strategy: DarkStrategy = 'tone-swap',
|
|
33
|
+
options: DarkVariantOptions = {}
|
|
34
|
+
): Color[] {
|
|
35
|
+
const { gamut = 'srgb' } = options
|
|
36
|
+
if (!STRATEGIES.includes(strategy)) throw unknownName('dark variant strategy', strategy, STRATEGIES)
|
|
37
|
+
if (strategy === 'mirror') return [...scale].reverse()
|
|
38
|
+
if (strategy === 'tone-swap') {
|
|
39
|
+
return scale.map((c) => {
|
|
40
|
+
const [, chroma, hue] = componentsIn(c, 'oklch') as [number, number, number | null]
|
|
41
|
+
return withAlpha(toneColor(100 - lstar(c), chroma, hue ?? 0, gamut), c)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
const oklch = scale.map((c) => componentsIn(c, 'oklch') as [number, number, number | null])
|
|
45
|
+
const ls = oklch.map((c) => c[0])
|
|
46
|
+
const sum = Math.min(...ls) + Math.max(...ls)
|
|
47
|
+
return scale.map((c, i) => {
|
|
48
|
+
const [l, chroma, hue] = oklch[i] as [number, number, number | null]
|
|
49
|
+
return withAlpha(toGamut({ colorSpace: 'oklch', components: [sum - l, chroma * 0.9, hue] }, { gamut }), c)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function withAlpha(color: Color, from: Color): Color {
|
|
54
|
+
return from.alpha === undefined ? color : { ...color, alpha: from.alpha }
|
|
55
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type GamutId, toGamut } from '../color/gamut.js'
|
|
2
|
+
import { normalizeHue } from '../color/math.js'
|
|
3
|
+
import { toColor } from '../color/parse.js'
|
|
4
|
+
import { convert } from '../color/spaces.js'
|
|
5
|
+
import type { Color } from '../color/types.js'
|
|
6
|
+
import { unknownName } from '../color/unknown-name.js'
|
|
7
|
+
|
|
8
|
+
type ColorInput = Color | string
|
|
9
|
+
|
|
10
|
+
/** The hue rotations, in degrees, that each scheme adds to the base color. */
|
|
11
|
+
const SCHEMES = {
|
|
12
|
+
__proto__: null,
|
|
13
|
+
complementary: [0, 180],
|
|
14
|
+
split: [0, 150, 210],
|
|
15
|
+
analogous: [-30, 0, 30],
|
|
16
|
+
triadic: [0, 120, 240],
|
|
17
|
+
tetradic: [0, 90, 180, 270]
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
/** A harmony scheme `harmony` accepts: `complementary`, `split`, `analogous`, `triadic` or `tetradic`. */
|
|
21
|
+
export type HarmonyScheme = keyof typeof SCHEMES
|
|
22
|
+
|
|
23
|
+
/** Spaces with a hue channel that a scheme can rotate in. */
|
|
24
|
+
export type HarmonySpace = 'oklch' | 'lch' | 'hsl' | 'hwb'
|
|
25
|
+
|
|
26
|
+
/** Options for `harmony`. */
|
|
27
|
+
export interface HarmonyOptions {
|
|
28
|
+
/** The space whose hue is rotated. Default `oklch`, which keeps perceived lightness steady. */
|
|
29
|
+
space?: HarmonySpace
|
|
30
|
+
/** The gamut every color must fit. Default `srgb`. */
|
|
31
|
+
gamut?: GamutId
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const HUE_SPACES: readonly string[] = ['oklch', 'lch', 'hsl', 'hwb']
|
|
35
|
+
|
|
36
|
+
const hueIndex = (space: HarmonySpace): number => (space === 'hsl' || space === 'hwb' ? 0 : 2)
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The colors of a harmony: `color` rotated around the hue wheel of `space`,
|
|
40
|
+
* with lightness and chroma held, in the order of the scheme's angles
|
|
41
|
+
* (`complementary` 0 and 180; `split` 0, 150, 210; `analogous` -30, 0, 30;
|
|
42
|
+
* `triadic` 0, 120, 240; `tetradic` 0, 90, 180, 270). The color itself is the
|
|
43
|
+
* entry at angle 0, returned as given. Rotated colors are mapped into `gamut`.
|
|
44
|
+
* The same angle looks different in `hsl` and `oklch`, so name the space.
|
|
45
|
+
*/
|
|
46
|
+
export function harmony(color: ColorInput, scheme: HarmonyScheme, options: HarmonyOptions = {}): Color[] {
|
|
47
|
+
const { space = 'oklch', gamut = 'srgb' } = options
|
|
48
|
+
const angles = SCHEMES[scheme]
|
|
49
|
+
if (!angles) throw unknownName('harmony scheme', scheme, Object.keys(SCHEMES))
|
|
50
|
+
if (!HUE_SPACES.includes(space)) throw unknownName('harmony space', space, HUE_SPACES)
|
|
51
|
+
const base = toColor(color)
|
|
52
|
+
const inSpace = convert(base, space)
|
|
53
|
+
const at = hueIndex(space)
|
|
54
|
+
const hue = (inSpace.components[at] as number | null) ?? 0
|
|
55
|
+
return angles.map((angle) => {
|
|
56
|
+
if (angle === 0) return base
|
|
57
|
+
const components = [...inSpace.components]
|
|
58
|
+
components[at] = normalizeHue(hue + angle)
|
|
59
|
+
return toGamut({ ...inSpace, components }, { gamut })
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Options for `harmonize`. */
|
|
64
|
+
export interface HarmonizeOptions {
|
|
65
|
+
/** The most the hue may move, in degrees. Default 15. */
|
|
66
|
+
max?: number
|
|
67
|
+
/** The share of the hue difference to move. Default 0.5. */
|
|
68
|
+
factor?: number
|
|
69
|
+
/** The gamut the result must fit. Default `srgb`. */
|
|
70
|
+
gamut?: GamutId
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Nudges `color`'s hue toward `toward`, as Material's `Blend.harmonize` does:
|
|
75
|
+
* by half the hue difference, but at most 15 degrees, in OKLCH, keeping
|
|
76
|
+
* lightness and chroma. Use it to make a status color feel at home with a
|
|
77
|
+
* brand color without changing its meaning.
|
|
78
|
+
*/
|
|
79
|
+
export function harmonize(color: ColorInput, toward: ColorInput, options: HarmonizeOptions = {}): Color {
|
|
80
|
+
const { max = 15, factor = 0.5, gamut = 'srgb' } = options
|
|
81
|
+
const from = convert(toColor(color), 'oklch')
|
|
82
|
+
const to = convert(toColor(toward), 'oklch')
|
|
83
|
+
const h0 = (from.components[2] as number | null) ?? 0
|
|
84
|
+
const h1 = (to.components[2] as number | null) ?? h0
|
|
85
|
+
// Signed shortest way round, -180 to 180.
|
|
86
|
+
const diff = normalizeHue(h1 - h0 + 180) - 180
|
|
87
|
+
const rotation = Math.min(Math.abs(diff) * factor, max) * Math.sign(diff)
|
|
88
|
+
const components = [...from.components]
|
|
89
|
+
components[2] = normalizeHue(h0 + rotation)
|
|
90
|
+
return toGamut({ ...from, components }, { gamut })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The opposite of a color. `lightness` flips OKLCH lightness (L becomes 1 - L)
|
|
95
|
+
* and keeps hue and chroma, so light turns dark without changing its hue;
|
|
96
|
+
* `rgb` inverts each sRGB channel, which also turns the hue to its complement.
|
|
97
|
+
* Alpha is kept. The result is mapped into sRGB.
|
|
98
|
+
*/
|
|
99
|
+
export function inverse(color: ColorInput, mode: 'lightness' | 'rgb' = 'lightness'): Color {
|
|
100
|
+
const base = toColor(color)
|
|
101
|
+
if (mode === 'rgb') {
|
|
102
|
+
const c = convert(base, 'srgb')
|
|
103
|
+
return toGamut({ ...c, components: c.components.map((x) => 1 - (x as number)) })
|
|
104
|
+
}
|
|
105
|
+
const c = convert(base, 'oklch')
|
|
106
|
+
return toGamut({ ...c, components: [1 - (c.components[0] as number), c.components[1], c.components[2]] })
|
|
107
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { type DarkStrategy, type DarkVariantOptions, darkVariant } from './dark.js'
|
|
2
|
+
export {
|
|
3
|
+
type HarmonizeOptions,
|
|
4
|
+
type HarmonyOptions,
|
|
5
|
+
type HarmonyScheme,
|
|
6
|
+
type HarmonySpace,
|
|
7
|
+
harmonize,
|
|
8
|
+
harmony,
|
|
9
|
+
inverse
|
|
10
|
+
} from './harmony.js'
|
|
11
|
+
export {
|
|
12
|
+
type RolePair,
|
|
13
|
+
type SemanticPalette,
|
|
14
|
+
type SemanticPaletteOptions,
|
|
15
|
+
type SemanticRole,
|
|
16
|
+
semanticPalette
|
|
17
|
+
} from './semantic.js'
|
|
18
|
+
export { type PaletteIssue, type PaletteLike, type ValidateOptions, type ValidateResult, validate } from './validate.js'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type ContrastOption, findContrast, pick } from '../color/contrast.js'
|
|
2
|
+
import type { GamutId } from '../color/gamut.js'
|
|
3
|
+
import { toColor } from '../color/parse.js'
|
|
4
|
+
import { componentsIn } from '../color/spaces.js'
|
|
5
|
+
import type { Color } from '../color/types.js'
|
|
6
|
+
import { checkMode } from '../color/unknown-name.js'
|
|
7
|
+
import { tonalPalette, toneColor } from '../scale/tone.js'
|
|
8
|
+
import { harmonize } from './harmony.js'
|
|
9
|
+
|
|
10
|
+
/** A background, the foreground drawn on it, and the contrast they reach. */
|
|
11
|
+
export interface RolePair {
|
|
12
|
+
bg: Color
|
|
13
|
+
fg: Color
|
|
14
|
+
/** The contrast of `fg` on `bg`, as the algorithm reports it (absolute value). */
|
|
15
|
+
ratio: number
|
|
16
|
+
/** The contrast the pair was built to reach. */
|
|
17
|
+
target: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Options for `semanticPalette`. Only `brand` is required. */
|
|
21
|
+
export interface SemanticPaletteOptions {
|
|
22
|
+
/** The brand color; it sets the hue of the primary, the neutrals and the harmonized status colors. */
|
|
23
|
+
brand: Color | string
|
|
24
|
+
/** Default `light`. */
|
|
25
|
+
mode?: 'light' | 'dark'
|
|
26
|
+
/** The contrast measure. Default `wcag2`. */
|
|
27
|
+
contrast?: ContrastOption
|
|
28
|
+
/** The contrast text pairs must reach. Default 4.5, which fits WCAG 2 (pass about 60 for APCA Lc). */
|
|
29
|
+
target?: number
|
|
30
|
+
/** The gamut every color must fit. Default `srgb`. */
|
|
31
|
+
gamut?: GamutId
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A role in a {@link SemanticPalette}. Each role holds a {@link RolePair}. */
|
|
35
|
+
export type SemanticRole =
|
|
36
|
+
| 'primary'
|
|
37
|
+
| 'primaryContainer'
|
|
38
|
+
| 'success'
|
|
39
|
+
| 'warning'
|
|
40
|
+
| 'danger'
|
|
41
|
+
| 'info'
|
|
42
|
+
| 'surface'
|
|
43
|
+
| 'surfaceVariant'
|
|
44
|
+
| 'outline'
|
|
45
|
+
|
|
46
|
+
/** What `semanticPalette` returns: the settings it used, the tonal scales it built from, and a pair for each role. */
|
|
47
|
+
export interface SemanticPalette {
|
|
48
|
+
meta: { mode: 'light' | 'dark'; contrast: string; target: number; gamut: GamutId }
|
|
49
|
+
/** Material's 13 tones, dark to light, in the brand's hue (`primary`) and a faint tint of it (`neutral`). */
|
|
50
|
+
scales: { primary: Color[]; neutral: Color[] }
|
|
51
|
+
roles: Record<SemanticRole, RolePair>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Tones (light, dark) per role: Material's 2021 assignments (see the research notes, B-palettes).
|
|
55
|
+
const BG_TONE = { primary: [40, 80], primaryContainer: [90, 30], surface: [98, 6], surfaceVariant: [90, 30] } as const
|
|
56
|
+
const FG_TONE = { primary: [100, 20], primaryContainer: [10, 90], surface: [10, 90], surfaceVariant: [30, 80] } as const
|
|
57
|
+
|
|
58
|
+
// OKLCH hues of the status colors, before they are harmonized toward the brand.
|
|
59
|
+
const STATUS = { success: 145, warning: 85, danger: 25, info: 240 } as const
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A role-based palette from one brand color: primary and its container, four
|
|
63
|
+
* status colors (success, warning, danger, info) harmonized toward the brand's
|
|
64
|
+
* hue, a surface and a variant tinted by the brand hue, and an outline. Every
|
|
65
|
+
* pair is built from L* tones, so a 50-tone gap or more reaches about 4.5 on
|
|
66
|
+
* WCAG 2 (the worst pair, 100 on 50, gives 4.48). Each pair records the ratio
|
|
67
|
+
* it reached; a foreground that falls short of `target` is replaced by the
|
|
68
|
+
* closest one that meets it, and `validate` reports any that cannot.
|
|
69
|
+
*/
|
|
70
|
+
export function semanticPalette(options: SemanticPaletteOptions): SemanticPalette {
|
|
71
|
+
const { mode = 'light', contrast = 'wcag2', target = 4.5, gamut = 'srgb' } = options
|
|
72
|
+
const fn = pick(contrast)
|
|
73
|
+
const dark = checkMode(mode) === 'dark'
|
|
74
|
+
const i = dark ? 1 : 0
|
|
75
|
+
const brand = toColor(options.brand)
|
|
76
|
+
const [, c, h] = componentsIn(brand, 'oklch') as [number, number, number | null]
|
|
77
|
+
const hue = h ?? 0
|
|
78
|
+
const chroma = Math.max(c, 0.04)
|
|
79
|
+
const tint = 0.015
|
|
80
|
+
|
|
81
|
+
const pair = (bg: Color, fgTone: number, fgChroma: number, need = target): RolePair => {
|
|
82
|
+
let fg = toneColor(fgTone, fgChroma, hue, gamut)
|
|
83
|
+
let ratio = Math.abs(fn(fg, bg))
|
|
84
|
+
if (ratio < need) {
|
|
85
|
+
const found = findContrast(bg, need, { contrast: fn, chroma: fgChroma, hue })
|
|
86
|
+
fg = found.color
|
|
87
|
+
ratio = Math.abs(found.contrast)
|
|
88
|
+
}
|
|
89
|
+
return { bg, fg, ratio, target: need }
|
|
90
|
+
}
|
|
91
|
+
const status = (name: keyof typeof STATUS): RolePair => {
|
|
92
|
+
const base = toneColor(BG_TONE.primary[i], 0.14, STATUS[name], gamut)
|
|
93
|
+
return pair(harmonize(base, brand, { gamut }), FG_TONE.primary[i], 0.02)
|
|
94
|
+
}
|
|
95
|
+
const surface = toneColor(BG_TONE.surface[i], tint, hue, gamut)
|
|
96
|
+
const outline = pair(surface, dark ? 60 : 50, tint, 3)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
meta: { mode, contrast: fn.algorithm ?? 'custom', target, gamut },
|
|
100
|
+
scales: {
|
|
101
|
+
primary: tonalPalette(hue, chroma, undefined, { gamut }),
|
|
102
|
+
neutral: tonalPalette(hue, tint, undefined, { gamut })
|
|
103
|
+
},
|
|
104
|
+
roles: {
|
|
105
|
+
primary: pair(toneColor(BG_TONE.primary[i], chroma, hue, gamut), FG_TONE.primary[i], 0.02),
|
|
106
|
+
primaryContainer: pair(
|
|
107
|
+
toneColor(BG_TONE.primaryContainer[i], chroma, hue, gamut),
|
|
108
|
+
FG_TONE.primaryContainer[i],
|
|
109
|
+
0.02
|
|
110
|
+
),
|
|
111
|
+
success: status('success'),
|
|
112
|
+
warning: status('warning'),
|
|
113
|
+
danger: status('danger'),
|
|
114
|
+
info: status('info'),
|
|
115
|
+
surface: pair(surface, FG_TONE.surface[i], tint),
|
|
116
|
+
surfaceVariant: pair(toneColor(BG_TONE.surfaceVariant[i], tint, hue, gamut), FG_TONE.surfaceVariant[i], tint),
|
|
117
|
+
outline
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type ContrastOption, pick } from '../color/contrast.js'
|
|
2
|
+
import { type GamutId, inGamut } from '../color/gamut.js'
|
|
3
|
+
import { componentsIn } from '../color/spaces.js'
|
|
4
|
+
import type { Color } from '../color/types.js'
|
|
5
|
+
import type { RolePair } from './semantic.js'
|
|
6
|
+
|
|
7
|
+
/** What `validate` accepts: a {@link SemanticPalette}, or any object with role pairs and/or scales. */
|
|
8
|
+
export interface PaletteLike {
|
|
9
|
+
roles?: Record<string, RolePair>
|
|
10
|
+
scales?: Record<string, readonly Color[]>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Options for `validate`. */
|
|
14
|
+
export interface ValidateOptions {
|
|
15
|
+
/** The measure used to re-check each pair. Default `wcag2`. */
|
|
16
|
+
contrast?: ContrastOption
|
|
17
|
+
/** The target for a pair that carries none. Default 4.5. */
|
|
18
|
+
target?: number
|
|
19
|
+
/** The gamut colors must fit; a color outside it was clipped or never mapped. Default `srgb`. */
|
|
20
|
+
gamut?: GamutId
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One problem `validate` found in a palette. */
|
|
24
|
+
export interface PaletteIssue {
|
|
25
|
+
kind: 'contrast' | 'lightness' | 'gamut'
|
|
26
|
+
/** Where: `roles.danger`, `scales.primary[3]`. */
|
|
27
|
+
path: string
|
|
28
|
+
message: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What `validate` returns: `ok` is true when `issues` is empty. */
|
|
32
|
+
export interface ValidateResult {
|
|
33
|
+
ok: boolean
|
|
34
|
+
issues: PaletteIssue[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks a palette and lists what is wrong, so an agent can fix it:
|
|
39
|
+
* - `contrast`: a role pair whose measured contrast is under its target;
|
|
40
|
+
* - `lightness`: a scale whose OKLCH lightness does not change steadily in one
|
|
41
|
+
* direction from one step to the next;
|
|
42
|
+
* - `gamut`: a color outside the gamut (it would be clipped on display).
|
|
43
|
+
*/
|
|
44
|
+
export function validate(palette: PaletteLike, options: ValidateOptions = {}): ValidateResult {
|
|
45
|
+
const { contrast = 'wcag2', target = 4.5, gamut = 'srgb' } = options
|
|
46
|
+
const fn = pick(contrast)
|
|
47
|
+
const issues: PaletteIssue[] = []
|
|
48
|
+
const outside = (path: string, color: Color) => {
|
|
49
|
+
if (!inGamut(color, gamut)) issues.push({ kind: 'gamut', path, message: `${path} is outside ${gamut}` })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [name, p] of Object.entries(palette.roles ?? {})) {
|
|
53
|
+
const need = p.target ?? target
|
|
54
|
+
const ratio = Math.abs(fn(p.fg, p.bg))
|
|
55
|
+
if (ratio < need)
|
|
56
|
+
issues.push({
|
|
57
|
+
kind: 'contrast',
|
|
58
|
+
path: `roles.${name}`,
|
|
59
|
+
message: `roles.${name} reaches ${ratio.toFixed(2)}, needs ${need}`
|
|
60
|
+
})
|
|
61
|
+
outside(`roles.${name}.bg`, p.bg)
|
|
62
|
+
outside(`roles.${name}.fg`, p.fg)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [name, scale] of Object.entries(palette.scales ?? {})) {
|
|
66
|
+
const ls = scale.map((c) => componentsIn(c, 'oklch')[0] as number)
|
|
67
|
+
const steps = ls.slice(1).map((l, k) => l - (ls[k] as number))
|
|
68
|
+
const direction = Math.sign(steps.find((d) => d !== 0) ?? 0)
|
|
69
|
+
steps.forEach((d, k) => {
|
|
70
|
+
if (Math.sign(d) !== direction || Math.abs(d) < 1e-9)
|
|
71
|
+
issues.push({
|
|
72
|
+
kind: 'lightness',
|
|
73
|
+
path: `scales.${name}[${k + 1}]`,
|
|
74
|
+
message: `scales.${name} lightness does not change steadily at step ${k + 1}`
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
scale.forEach((c, k) => {
|
|
78
|
+
outside(`scales.${name}[${k}]`, c)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
return { ok: issues.length === 0, issues }
|
|
82
|
+
}
|