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,96 @@
|
|
|
1
|
+
import { findContrast, pick } from "../color/contrast.js";
|
|
2
|
+
import { toColor } from "../color/parse.js";
|
|
3
|
+
import { componentsIn } from "../color/spaces.js";
|
|
4
|
+
import { checkMode } from "../color/unknown-name.js";
|
|
5
|
+
import { tonalPalette, toneColor } from "../scale/tone.js";
|
|
6
|
+
import { harmonize } from "./harmony.js";
|
|
7
|
+
// src/palette/semantic.ts
|
|
8
|
+
const BG_TONE = {
|
|
9
|
+
primary: [40, 80],
|
|
10
|
+
primaryContainer: [90, 30],
|
|
11
|
+
surface: [98, 6],
|
|
12
|
+
surfaceVariant: [90, 30]
|
|
13
|
+
};
|
|
14
|
+
const FG_TONE = {
|
|
15
|
+
primary: [100, 20],
|
|
16
|
+
primaryContainer: [10, 90],
|
|
17
|
+
surface: [10, 90],
|
|
18
|
+
surfaceVariant: [30, 80]
|
|
19
|
+
};
|
|
20
|
+
const STATUS = {
|
|
21
|
+
success: 145,
|
|
22
|
+
warning: 85,
|
|
23
|
+
danger: 25,
|
|
24
|
+
info: 240
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A role-based palette from one brand color: primary and its container, four
|
|
28
|
+
* status colors (success, warning, danger, info) harmonized toward the brand's
|
|
29
|
+
* hue, a surface and a variant tinted by the brand hue, and an outline. Every
|
|
30
|
+
* pair is built from L* tones, so a 50-tone gap or more reaches about 4.5 on
|
|
31
|
+
* WCAG 2 (the worst pair, 100 on 50, gives 4.48). Each pair records the ratio
|
|
32
|
+
* it reached; a foreground that falls short of `target` is replaced by the
|
|
33
|
+
* closest one that meets it, and `validate` reports any that cannot.
|
|
34
|
+
*/
|
|
35
|
+
function semanticPalette(options) {
|
|
36
|
+
const { mode = "light", contrast = "wcag2", target = 4.5, gamut = "srgb" } = options;
|
|
37
|
+
const fn = pick(contrast);
|
|
38
|
+
const dark = checkMode(mode) === "dark";
|
|
39
|
+
const i = dark ? 1 : 0;
|
|
40
|
+
const brand = toColor(options.brand);
|
|
41
|
+
const [, c, h] = componentsIn(brand, "oklch");
|
|
42
|
+
const hue = h ?? 0;
|
|
43
|
+
const chroma = Math.max(c, .04);
|
|
44
|
+
const tint = .015;
|
|
45
|
+
const pair = (bg, fgTone, fgChroma, need = target) => {
|
|
46
|
+
let fg = toneColor(fgTone, fgChroma, hue, gamut);
|
|
47
|
+
let ratio = Math.abs(fn(fg, bg));
|
|
48
|
+
if (ratio < need) {
|
|
49
|
+
const found = findContrast(bg, need, {
|
|
50
|
+
contrast: fn,
|
|
51
|
+
chroma: fgChroma,
|
|
52
|
+
hue
|
|
53
|
+
});
|
|
54
|
+
fg = found.color;
|
|
55
|
+
ratio = Math.abs(found.contrast);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
bg,
|
|
59
|
+
fg,
|
|
60
|
+
ratio,
|
|
61
|
+
target: need
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const status = (name) => {
|
|
65
|
+
const base = toneColor(BG_TONE.primary[i], .14, STATUS[name], gamut);
|
|
66
|
+
return pair(harmonize(base, brand, { gamut }), FG_TONE.primary[i], .02);
|
|
67
|
+
};
|
|
68
|
+
const surface = toneColor(BG_TONE.surface[i], tint, hue, gamut);
|
|
69
|
+
const outline = pair(surface, dark ? 60 : 50, tint, 3);
|
|
70
|
+
return {
|
|
71
|
+
meta: {
|
|
72
|
+
mode,
|
|
73
|
+
contrast: fn.algorithm ?? "custom",
|
|
74
|
+
target,
|
|
75
|
+
gamut
|
|
76
|
+
},
|
|
77
|
+
scales: {
|
|
78
|
+
primary: tonalPalette(hue, chroma, void 0, { gamut }),
|
|
79
|
+
neutral: tonalPalette(hue, tint, void 0, { gamut })
|
|
80
|
+
},
|
|
81
|
+
roles: {
|
|
82
|
+
primary: pair(toneColor(BG_TONE.primary[i], chroma, hue, gamut), FG_TONE.primary[i], .02),
|
|
83
|
+
primaryContainer: pair(toneColor(BG_TONE.primaryContainer[i], chroma, hue, gamut), FG_TONE.primaryContainer[i], .02),
|
|
84
|
+
success: status("success"),
|
|
85
|
+
warning: status("warning"),
|
|
86
|
+
danger: status("danger"),
|
|
87
|
+
info: status("info"),
|
|
88
|
+
surface: pair(surface, FG_TONE.surface[i], tint),
|
|
89
|
+
surfaceVariant: pair(toneColor(BG_TONE.surfaceVariant[i], tint, hue, gamut), FG_TONE.surfaceVariant[i], tint),
|
|
90
|
+
outline
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export { semanticPalette };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=semantic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic.js","names":[],"sources":["../../src/palette/semantic.ts"],"sourcesContent":["import { type ContrastOption, findContrast, pick } from '../color/contrast.js'\nimport type { GamutId } from '../color/gamut.js'\nimport { toColor } from '../color/parse.js'\nimport { componentsIn } from '../color/spaces.js'\nimport type { Color } from '../color/types.js'\nimport { checkMode } from '../color/unknown-name.js'\nimport { tonalPalette, toneColor } from '../scale/tone.js'\nimport { harmonize } from './harmony.js'\n\n/** A background, the foreground drawn on it, and the contrast they reach. */\nexport interface RolePair {\n\tbg: Color\n\tfg: Color\n\t/** The contrast of `fg` on `bg`, as the algorithm reports it (absolute value). */\n\tratio: number\n\t/** The contrast the pair was built to reach. */\n\ttarget: number\n}\n\n/** Options for `semanticPalette`. Only `brand` is required. */\nexport interface SemanticPaletteOptions {\n\t/** The brand color; it sets the hue of the primary, the neutrals and the harmonized status colors. */\n\tbrand: Color | string\n\t/** Default `light`. */\n\tmode?: 'light' | 'dark'\n\t/** The contrast measure. Default `wcag2`. */\n\tcontrast?: ContrastOption\n\t/** The contrast text pairs must reach. Default 4.5, which fits WCAG 2 (pass about 60 for APCA Lc). */\n\ttarget?: number\n\t/** The gamut every color must fit. Default `srgb`. */\n\tgamut?: GamutId\n}\n\n/** A role in a {@link SemanticPalette}. Each role holds a {@link RolePair}. */\nexport type SemanticRole =\n\t| 'primary'\n\t| 'primaryContainer'\n\t| 'success'\n\t| 'warning'\n\t| 'danger'\n\t| 'info'\n\t| 'surface'\n\t| 'surfaceVariant'\n\t| 'outline'\n\n/** What `semanticPalette` returns: the settings it used, the tonal scales it built from, and a pair for each role. */\nexport interface SemanticPalette {\n\tmeta: { mode: 'light' | 'dark'; contrast: string; target: number; gamut: GamutId }\n\t/** Material's 13 tones, dark to light, in the brand's hue (`primary`) and a faint tint of it (`neutral`). */\n\tscales: { primary: Color[]; neutral: Color[] }\n\troles: Record<SemanticRole, RolePair>\n}\n\n// Tones (light, dark) per role: Material's 2021 assignments (see the research notes, B-palettes).\nconst BG_TONE = { primary: [40, 80], primaryContainer: [90, 30], surface: [98, 6], surfaceVariant: [90, 30] } as const\nconst FG_TONE = { primary: [100, 20], primaryContainer: [10, 90], surface: [10, 90], surfaceVariant: [30, 80] } as const\n\n// OKLCH hues of the status colors, before they are harmonized toward the brand.\nconst STATUS = { success: 145, warning: 85, danger: 25, info: 240 } as const\n\n/**\n * A role-based palette from one brand color: primary and its container, four\n * status colors (success, warning, danger, info) harmonized toward the brand's\n * hue, a surface and a variant tinted by the brand hue, and an outline. Every\n * pair is built from L* tones, so a 50-tone gap or more reaches about 4.5 on\n * WCAG 2 (the worst pair, 100 on 50, gives 4.48). Each pair records the ratio\n * it reached; a foreground that falls short of `target` is replaced by the\n * closest one that meets it, and `validate` reports any that cannot.\n */\nexport function semanticPalette(options: SemanticPaletteOptions): SemanticPalette {\n\tconst { mode = 'light', contrast = 'wcag2', target = 4.5, gamut = 'srgb' } = options\n\tconst fn = pick(contrast)\n\tconst dark = checkMode(mode) === 'dark'\n\tconst i = dark ? 1 : 0\n\tconst brand = toColor(options.brand)\n\tconst [, c, h] = componentsIn(brand, 'oklch') as [number, number, number | null]\n\tconst hue = h ?? 0\n\tconst chroma = Math.max(c, 0.04)\n\tconst tint = 0.015\n\n\tconst pair = (bg: Color, fgTone: number, fgChroma: number, need = target): RolePair => {\n\t\tlet fg = toneColor(fgTone, fgChroma, hue, gamut)\n\t\tlet ratio = Math.abs(fn(fg, bg))\n\t\tif (ratio < need) {\n\t\t\tconst found = findContrast(bg, need, { contrast: fn, chroma: fgChroma, hue })\n\t\t\tfg = found.color\n\t\t\tratio = Math.abs(found.contrast)\n\t\t}\n\t\treturn { bg, fg, ratio, target: need }\n\t}\n\tconst status = (name: keyof typeof STATUS): RolePair => {\n\t\tconst base = toneColor(BG_TONE.primary[i], 0.14, STATUS[name], gamut)\n\t\treturn pair(harmonize(base, brand, { gamut }), FG_TONE.primary[i], 0.02)\n\t}\n\tconst surface = toneColor(BG_TONE.surface[i], tint, hue, gamut)\n\tconst outline = pair(surface, dark ? 60 : 50, tint, 3)\n\n\treturn {\n\t\tmeta: { mode, contrast: fn.algorithm ?? 'custom', target, gamut },\n\t\tscales: {\n\t\t\tprimary: tonalPalette(hue, chroma, undefined, { gamut }),\n\t\t\tneutral: tonalPalette(hue, tint, undefined, { gamut })\n\t\t},\n\t\troles: {\n\t\t\tprimary: pair(toneColor(BG_TONE.primary[i], chroma, hue, gamut), FG_TONE.primary[i], 0.02),\n\t\t\tprimaryContainer: pair(\n\t\t\t\ttoneColor(BG_TONE.primaryContainer[i], chroma, hue, gamut),\n\t\t\t\tFG_TONE.primaryContainer[i],\n\t\t\t\t0.02\n\t\t\t),\n\t\t\tsuccess: status('success'),\n\t\t\twarning: status('warning'),\n\t\t\tdanger: status('danger'),\n\t\t\tinfo: status('info'),\n\t\t\tsurface: pair(surface, FG_TONE.surface[i], tint),\n\t\t\tsurfaceVariant: pair(toneColor(BG_TONE.surfaceVariant[i], tint, hue, gamut), FG_TONE.surfaceVariant[i], tint),\n\t\t\toutline\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;AAsDA,MAAM,UAAU;CAAE,SAAS,CAAC,IAAI,EAAE;CAAG,kBAAkB,CAAC,IAAI,EAAE;CAAG,SAAS,CAAC,IAAI,CAAC;CAAG,gBAAgB,CAAC,IAAI,EAAE;AAAE;AAC5G,MAAM,UAAU;CAAE,SAAS,CAAC,KAAK,EAAE;CAAG,kBAAkB,CAAC,IAAI,EAAE;CAAG,SAAS,CAAC,IAAI,EAAE;CAAG,gBAAgB,CAAC,IAAI,EAAE;AAAE;AAG9G,MAAM,SAAS;CAAE,SAAS;CAAK,SAAS;CAAI,QAAQ;CAAI,MAAM;AAAI;;;;;;;;;;AAWlE,SAAgB,gBAAgB,SAAkD;CACjF,MAAM,EAAE,OAAO,SAAS,WAAW,SAAS,SAAS,KAAK,QAAQ,WAAW;CAC7E,MAAM,KAAK,KAAK,QAAQ;CACxB,MAAM,OAAO,UAAU,IAAI,MAAM;CACjC,MAAM,IAAI,OAAO,IAAI;CACrB,MAAM,QAAQ,QAAQ,QAAQ,KAAK;CACnC,MAAM,GAAG,GAAG,KAAK,aAAa,OAAO,OAAO;CAC5C,MAAM,MAAM,KAAK;CACjB,MAAM,SAAS,KAAK,IAAI,GAAG,GAAI;CAC/B,MAAM,OAAO;CAEb,MAAM,QAAQ,IAAW,QAAgB,UAAkB,OAAO,WAAqB;EACtF,IAAI,KAAK,UAAU,QAAQ,UAAU,KAAK,KAAK;EAC/C,IAAI,QAAQ,KAAK,IAAI,GAAG,IAAI,EAAE,CAAC;EAC/B,IAAI,QAAQ,MAAM;GACjB,MAAM,QAAQ,aAAa,IAAI,MAAM;IAAE,UAAU;IAAI,QAAQ;IAAU;GAAI,CAAC;GAC5E,KAAK,MAAM;GACX,QAAQ,KAAK,IAAI,MAAM,QAAQ;EAChC;EACA,OAAO;GAAE;GAAI;GAAI;GAAO,QAAQ;EAAK;CACtC;CACA,MAAM,UAAU,SAAwC;EACvD,MAAM,OAAO,UAAU,QAAQ,QAAQ,IAAI,KAAM,OAAO,OAAO,KAAK;EACpE,OAAO,KAAK,UAAU,MAAM,OAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,QAAQ,IAAI,GAAI;CACxE;CACA,MAAM,UAAU,UAAU,QAAQ,QAAQ,IAAI,MAAM,KAAK,KAAK;CAC9D,MAAM,UAAU,KAAK,SAAS,OAAO,KAAK,IAAI,MAAM,CAAC;CAErD,OAAO;EACN,MAAM;GAAE;GAAM,UAAU,GAAG,aAAa;GAAU;GAAQ;EAAM;EAChE,QAAQ;GACP,SAAS,aAAa,KAAK,QAAQ,KAAA,GAAW,EAAE,MAAM,CAAC;GACvD,SAAS,aAAa,KAAK,MAAM,KAAA,GAAW,EAAE,MAAM,CAAC;EACtD;EACA,OAAO;GACN,SAAS,KAAK,UAAU,QAAQ,QAAQ,IAAI,QAAQ,KAAK,KAAK,GAAG,QAAQ,QAAQ,IAAI,GAAI;GACzF,kBAAkB,KACjB,UAAU,QAAQ,iBAAiB,IAAI,QAAQ,KAAK,KAAK,GACzD,QAAQ,iBAAiB,IACzB,GACD;GACA,SAAS,OAAO,SAAS;GACzB,SAAS,OAAO,SAAS;GACzB,QAAQ,OAAO,QAAQ;GACvB,MAAM,OAAO,MAAM;GACnB,SAAS,KAAK,SAAS,QAAQ,QAAQ,IAAI,IAAI;GAC/C,gBAAgB,KAAK,UAAU,QAAQ,eAAe,IAAI,MAAM,KAAK,KAAK,GAAG,QAAQ,eAAe,IAAI,IAAI;GAC5G;EACD;CACD;AACD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ContrastOption } from "../color/contrast.js";
|
|
2
|
+
import { GamutId } from "../color/gamut.js";
|
|
3
|
+
import { Color } from "../color/types.js";
|
|
4
|
+
import { RolePair } from "./semantic.js";
|
|
5
|
+
/** What `validate` accepts: a {@link SemanticPalette}, or any object with role pairs and/or scales. */
|
|
6
|
+
export interface PaletteLike {
|
|
7
|
+
roles?: Record<string, RolePair>;
|
|
8
|
+
scales?: Record<string, readonly Color[]>;
|
|
9
|
+
}
|
|
10
|
+
/** Options for `validate`. */
|
|
11
|
+
export interface ValidateOptions {
|
|
12
|
+
/** The measure used to re-check each pair. Default `wcag2`. */
|
|
13
|
+
contrast?: ContrastOption;
|
|
14
|
+
/** The target for a pair that carries none. Default 4.5. */
|
|
15
|
+
target?: number;
|
|
16
|
+
/** The gamut colors must fit; a color outside it was clipped or never mapped. Default `srgb`. */
|
|
17
|
+
gamut?: GamutId;
|
|
18
|
+
}
|
|
19
|
+
/** One problem `validate` found in a palette. */
|
|
20
|
+
export interface PaletteIssue {
|
|
21
|
+
kind: 'contrast' | 'lightness' | 'gamut';
|
|
22
|
+
/** Where: `roles.danger`, `scales.primary[3]`. */
|
|
23
|
+
path: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
/** What `validate` returns: `ok` is true when `issues` is empty. */
|
|
27
|
+
export interface ValidateResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
issues: PaletteIssue[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Checks a palette and lists what is wrong, so an agent can fix it:
|
|
33
|
+
* - `contrast`: a role pair whose measured contrast is under its target;
|
|
34
|
+
* - `lightness`: a scale whose OKLCH lightness does not change steadily in one
|
|
35
|
+
* direction from one step to the next;
|
|
36
|
+
* - `gamut`: a color outside the gamut (it would be clipped on display).
|
|
37
|
+
*/
|
|
38
|
+
export declare function validate(palette: PaletteLike, options?: ValidateOptions): ValidateResult;
|
|
39
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","names":[],"sources":["../../src/palette/validate.ts"],"mappings":";;;;;iBAOiB;EAChB,QAAQ,eAAe;EACvB,SAAS,wBAAwB;;;iBAIjB;;EAEhB,WAAW;;EAEX;;EAEA,QAAQ;;;iBAIQ;EAChB;;EAEA;EACA;;;iBAIgB;EAChB;EACA,QAAQ;;;;;;;;;wBAUO,SAAS,SAAS,aAAa,UAAS,kBAAuB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { pick } from "../color/contrast.js";
|
|
2
|
+
import { inGamut } from "../color/gamut.js";
|
|
3
|
+
import { componentsIn } from "../color/spaces.js";
|
|
4
|
+
// src/palette/validate.ts
|
|
5
|
+
/**
|
|
6
|
+
* Checks a palette and lists what is wrong, so an agent can fix it:
|
|
7
|
+
* - `contrast`: a role pair whose measured contrast is under its target;
|
|
8
|
+
* - `lightness`: a scale whose OKLCH lightness does not change steadily in one
|
|
9
|
+
* direction from one step to the next;
|
|
10
|
+
* - `gamut`: a color outside the gamut (it would be clipped on display).
|
|
11
|
+
*/
|
|
12
|
+
function validate(palette, options = {}) {
|
|
13
|
+
const { contrast = "wcag2", target = 4.5, gamut = "srgb" } = options;
|
|
14
|
+
const fn = pick(contrast);
|
|
15
|
+
const issues = [];
|
|
16
|
+
const outside = (path, color) => {
|
|
17
|
+
if (!inGamut(color, gamut)) issues.push({
|
|
18
|
+
kind: "gamut",
|
|
19
|
+
path,
|
|
20
|
+
message: `${path} is outside ${gamut}`
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
for (const [name, p] of Object.entries(palette.roles ?? {})) {
|
|
24
|
+
const need = p.target ?? target;
|
|
25
|
+
const ratio = Math.abs(fn(p.fg, p.bg));
|
|
26
|
+
if (ratio < need) issues.push({
|
|
27
|
+
kind: "contrast",
|
|
28
|
+
path: `roles.${name}`,
|
|
29
|
+
message: `roles.${name} reaches ${ratio.toFixed(2)}, needs ${need}`
|
|
30
|
+
});
|
|
31
|
+
outside(`roles.${name}.bg`, p.bg);
|
|
32
|
+
outside(`roles.${name}.fg`, p.fg);
|
|
33
|
+
}
|
|
34
|
+
for (const [name, scale] of Object.entries(palette.scales ?? {})) {
|
|
35
|
+
const ls = scale.map((c) => componentsIn(c, "oklch")[0]);
|
|
36
|
+
const steps = ls.slice(1).map((l, k) => l - ls[k]);
|
|
37
|
+
const direction = Math.sign(steps.find((d) => d !== 0) ?? 0);
|
|
38
|
+
steps.forEach((d, k) => {
|
|
39
|
+
if (Math.sign(d) !== direction || Math.abs(d) < 1e-9) issues.push({
|
|
40
|
+
kind: "lightness",
|
|
41
|
+
path: `scales.${name}[${k + 1}]`,
|
|
42
|
+
message: `scales.${name} lightness does not change steadily at step ${k + 1}`
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
scale.forEach((c, k) => {
|
|
46
|
+
outside(`scales.${name}[${k}]`, c);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok: issues.length === 0,
|
|
51
|
+
issues
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export { validate };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","names":[],"sources":["../../src/palette/validate.ts"],"sourcesContent":["import { type ContrastOption, pick } from '../color/contrast.js'\nimport { type GamutId, inGamut } from '../color/gamut.js'\nimport { componentsIn } from '../color/spaces.js'\nimport type { Color } from '../color/types.js'\nimport type { RolePair } from './semantic.js'\n\n/** What `validate` accepts: a {@link SemanticPalette}, or any object with role pairs and/or scales. */\nexport interface PaletteLike {\n\troles?: Record<string, RolePair>\n\tscales?: Record<string, readonly Color[]>\n}\n\n/** Options for `validate`. */\nexport interface ValidateOptions {\n\t/** The measure used to re-check each pair. Default `wcag2`. */\n\tcontrast?: ContrastOption\n\t/** The target for a pair that carries none. Default 4.5. */\n\ttarget?: number\n\t/** The gamut colors must fit; a color outside it was clipped or never mapped. Default `srgb`. */\n\tgamut?: GamutId\n}\n\n/** One problem `validate` found in a palette. */\nexport interface PaletteIssue {\n\tkind: 'contrast' | 'lightness' | 'gamut'\n\t/** Where: `roles.danger`, `scales.primary[3]`. */\n\tpath: string\n\tmessage: string\n}\n\n/** What `validate` returns: `ok` is true when `issues` is empty. */\nexport interface ValidateResult {\n\tok: boolean\n\tissues: PaletteIssue[]\n}\n\n/**\n * Checks a palette and lists what is wrong, so an agent can fix it:\n * - `contrast`: a role pair whose measured contrast is under its target;\n * - `lightness`: a scale whose OKLCH lightness does not change steadily in one\n * direction from one step to the next;\n * - `gamut`: a color outside the gamut (it would be clipped on display).\n */\nexport function validate(palette: PaletteLike, options: ValidateOptions = {}): ValidateResult {\n\tconst { contrast = 'wcag2', target = 4.5, gamut = 'srgb' } = options\n\tconst fn = pick(contrast)\n\tconst issues: PaletteIssue[] = []\n\tconst outside = (path: string, color: Color) => {\n\t\tif (!inGamut(color, gamut)) issues.push({ kind: 'gamut', path, message: `${path} is outside ${gamut}` })\n\t}\n\n\tfor (const [name, p] of Object.entries(palette.roles ?? {})) {\n\t\tconst need = p.target ?? target\n\t\tconst ratio = Math.abs(fn(p.fg, p.bg))\n\t\tif (ratio < need)\n\t\t\tissues.push({\n\t\t\t\tkind: 'contrast',\n\t\t\t\tpath: `roles.${name}`,\n\t\t\t\tmessage: `roles.${name} reaches ${ratio.toFixed(2)}, needs ${need}`\n\t\t\t})\n\t\toutside(`roles.${name}.bg`, p.bg)\n\t\toutside(`roles.${name}.fg`, p.fg)\n\t}\n\n\tfor (const [name, scale] of Object.entries(palette.scales ?? {})) {\n\t\tconst ls = scale.map((c) => componentsIn(c, 'oklch')[0] as number)\n\t\tconst steps = ls.slice(1).map((l, k) => l - (ls[k] as number))\n\t\tconst direction = Math.sign(steps.find((d) => d !== 0) ?? 0)\n\t\tsteps.forEach((d, k) => {\n\t\t\tif (Math.sign(d) !== direction || Math.abs(d) < 1e-9)\n\t\t\t\tissues.push({\n\t\t\t\t\tkind: 'lightness',\n\t\t\t\t\tpath: `scales.${name}[${k + 1}]`,\n\t\t\t\t\tmessage: `scales.${name} lightness does not change steadily at step ${k + 1}`\n\t\t\t\t})\n\t\t})\n\t\tscale.forEach((c, k) => {\n\t\t\toutside(`scales.${name}[${k}]`, c)\n\t\t})\n\t}\n\treturn { ok: issues.length === 0, issues }\n}\n"],"mappings":";;;;;;;;;;;AA2CA,SAAgB,SAAS,SAAsB,UAA2B,CAAC,GAAmB;CAC7F,MAAM,EAAE,WAAW,SAAS,SAAS,KAAK,QAAQ,WAAW;CAC7D,MAAM,KAAK,KAAK,QAAQ;CACxB,MAAM,SAAyB,CAAC;CAChC,MAAM,WAAW,MAAc,UAAiB;EAC/C,IAAI,CAAC,QAAQ,OAAO,KAAK,GAAG,OAAO,KAAK;GAAE,MAAM;GAAS;GAAM,SAAS,GAAG,KAAK,cAAc;EAAQ,CAAC;CACxG;CAEA,KAAK,MAAM,CAAC,MAAM,MAAM,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,GAAG;EAC5D,MAAM,OAAO,EAAE,UAAU;EACzB,MAAM,QAAQ,KAAK,IAAI,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC;EACrC,IAAI,QAAQ,MACX,OAAO,KAAK;GACX,MAAM;GACN,MAAM,SAAS;GACf,SAAS,SAAS,KAAK,WAAW,MAAM,QAAQ,CAAC,EAAE,UAAU;EAC9D,CAAC;EACF,QAAQ,SAAS,KAAK,MAAM,EAAE,EAAE;EAChC,QAAQ,SAAS,KAAK,MAAM,EAAE,EAAE;CACjC;CAEA,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,UAAU,CAAC,CAAC,GAAG;EACjE,MAAM,KAAK,MAAM,KAAK,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,EAAY;EACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,IAAK,GAAG,EAAa;EAC7D,MAAM,YAAY,KAAK,KAAK,MAAM,MAAM,MAAM,MAAM,CAAC,KAAK,CAAC;EAC3D,MAAM,SAAS,GAAG,MAAM;GACvB,IAAI,KAAK,KAAK,CAAC,MAAM,aAAa,KAAK,IAAI,CAAC,IAAI,MAC/C,OAAO,KAAK;IACX,MAAM;IACN,MAAM,UAAU,KAAK,GAAG,IAAI,EAAE;IAC9B,SAAS,UAAU,KAAK,8CAA8C,IAAI;GAC3E,CAAC;EACH,CAAC;EACD,MAAM,SAAS,GAAG,MAAM;GACvB,QAAQ,UAAU,KAAK,GAAG,EAAE,IAAI,CAAC;EAClC,CAAC;CACF;CACA,OAAO;EAAE,IAAI,OAAO,WAAW;EAAG;CAAO;AAC1C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GamutId } from "../color/gamut.js";
|
|
2
|
+
import { Color } from "../color/types.js";
|
|
3
|
+
/** Options for `scaleFromBrand`. */
|
|
4
|
+
export interface BrandScaleOptions {
|
|
5
|
+
/** `light` runs from a near-white step 1 to dark text; `dark` the other way. Default `light`. */
|
|
6
|
+
mode?: 'light' | 'dark';
|
|
7
|
+
/** The page background, whose lightness step 1 takes. Default near-white, or near-black in `dark`. */
|
|
8
|
+
background?: Color | string;
|
|
9
|
+
/** The gamut steps other than 9 must fit. Default `srgb`. */
|
|
10
|
+
gamut?: GamutId;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A 12-step UI scale from one brand color, after Radix's palette generator:
|
|
14
|
+
* the brand color is step 9 exactly (returned as given, never remapped), steps
|
|
15
|
+
* 1 to 8 run from the background to it, step 10 is its hover state, and steps
|
|
16
|
+
* 11 and 12 are low- and high-contrast text. Hue is the brand's throughout.
|
|
17
|
+
* Radix's generator also borrows the shape of the nearest of its hand-tuned
|
|
18
|
+
* scales; this one uses a single fixed shape instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function scaleFromBrand(brand: Color | string, options?: BrandScaleOptions): Color[];
|
|
21
|
+
//# sourceMappingURL=brand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.d.ts","names":[],"sources":["../../src/scale/brand.ts"],"mappings":";;;iBAQiB;;EAEhB;;EAEA,aAAa;;EAEb,QAAQ;;;;;;;;;;wBAgBO,eAAe,OAAO,gBAAgB,UAAS,oBAAyB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { toGamut } from "../color/gamut.js";
|
|
2
|
+
import { toColor } from "../color/parse.js";
|
|
3
|
+
import { componentsIn } from "../color/spaces.js";
|
|
4
|
+
import { checkMode } from "../color/unknown-name.js";
|
|
5
|
+
// src/scale/brand.ts
|
|
6
|
+
const LIGHT_PROGRESS = [
|
|
7
|
+
0,
|
|
8
|
+
.0376,
|
|
9
|
+
.1,
|
|
10
|
+
.1796,
|
|
11
|
+
.2606,
|
|
12
|
+
.3466,
|
|
13
|
+
.4656,
|
|
14
|
+
.6312,
|
|
15
|
+
1
|
|
16
|
+
];
|
|
17
|
+
const CHROMA = [
|
|
18
|
+
.04,
|
|
19
|
+
.08,
|
|
20
|
+
.2,
|
|
21
|
+
.32,
|
|
22
|
+
.43,
|
|
23
|
+
.52,
|
|
24
|
+
.6,
|
|
25
|
+
.75
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* A 12-step UI scale from one brand color, after Radix's palette generator:
|
|
29
|
+
* the brand color is step 9 exactly (returned as given, never remapped), steps
|
|
30
|
+
* 1 to 8 run from the background to it, step 10 is its hover state, and steps
|
|
31
|
+
* 11 and 12 are low- and high-contrast text. Hue is the brand's throughout.
|
|
32
|
+
* Radix's generator also borrows the shape of the nearest of its hand-tuned
|
|
33
|
+
* scales; this one uses a single fixed shape instead.
|
|
34
|
+
*/
|
|
35
|
+
function scaleFromBrand(brand, options = {}) {
|
|
36
|
+
const { mode = "light", gamut = "srgb" } = options;
|
|
37
|
+
const dark = checkMode(mode) === "dark";
|
|
38
|
+
const exact = toColor(brand);
|
|
39
|
+
const [l9, c9, h9] = componentsIn(exact, "oklch");
|
|
40
|
+
const hue = h9 ?? 0;
|
|
41
|
+
const bg = options.background;
|
|
42
|
+
const l1 = bg ? componentsIn(toColor(bg), "oklch")[0] : dark ? .18 : .992;
|
|
43
|
+
const step = (l, c) => toGamut({
|
|
44
|
+
colorSpace: "oklch",
|
|
45
|
+
components: [
|
|
46
|
+
l,
|
|
47
|
+
c,
|
|
48
|
+
hue
|
|
49
|
+
]
|
|
50
|
+
}, { gamut });
|
|
51
|
+
const out = [];
|
|
52
|
+
for (let i = 0; i < 8; i++) out.push(step(l1 + (l9 - l1) * LIGHT_PROGRESS[i], c9 * CHROMA[i]));
|
|
53
|
+
out.push(exact);
|
|
54
|
+
const hover = .03 / (l9 + .1);
|
|
55
|
+
out.push(step(dark ? l9 + hover : l9 - hover, c9 * .93));
|
|
56
|
+
const c8 = c9 * CHROMA[7];
|
|
57
|
+
out.push(step(dark ? .78 : .5, Math.min(c9 * .9, Math.max(c8, c9))));
|
|
58
|
+
out.push(step(dark ? .93 : .3, Math.min(c9 * .35, Math.max(c8, c9))));
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
export { scaleFromBrand };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.js","names":[],"sources":["../../src/scale/brand.ts"],"sourcesContent":["import type { GamutId } from '../color/gamut.js'\nimport { toGamut } from '../color/gamut.js'\nimport { toColor } from '../color/parse.js'\nimport { componentsIn } from '../color/spaces.js'\nimport type { Color } from '../color/types.js'\nimport { checkMode } from '../color/unknown-name.js'\n\n/** Options for `scaleFromBrand`. */\nexport interface BrandScaleOptions {\n\t/** `light` runs from a near-white step 1 to dark text; `dark` the other way. Default `light`. */\n\tmode?: 'light' | 'dark'\n\t/** The page background, whose lightness step 1 takes. Default near-white, or near-black in `dark`. */\n\tbackground?: Color | string\n\t/** The gamut steps other than 9 must fit. Default `srgb`. */\n\tgamut?: GamutId\n}\n\n// Radix-like lightness shapes, steps 1 to 9, as fractions of the way from\n// step 1 to step 9; chroma of steps 1 to 8 as multiples of the brand's.\nconst LIGHT_PROGRESS = [0, 0.0376, 0.1, 0.1796, 0.2606, 0.3466, 0.4656, 0.6312, 1]\nconst CHROMA = [0.04, 0.08, 0.2, 0.32, 0.43, 0.52, 0.6, 0.75]\n\n/**\n * A 12-step UI scale from one brand color, after Radix's palette generator:\n * the brand color is step 9 exactly (returned as given, never remapped), steps\n * 1 to 8 run from the background to it, step 10 is its hover state, and steps\n * 11 and 12 are low- and high-contrast text. Hue is the brand's throughout.\n * Radix's generator also borrows the shape of the nearest of its hand-tuned\n * scales; this one uses a single fixed shape instead.\n */\nexport function scaleFromBrand(brand: Color | string, options: BrandScaleOptions = {}): Color[] {\n\tconst { mode = 'light', gamut = 'srgb' } = options\n\tconst dark = checkMode(mode) === 'dark'\n\tconst exact = toColor(brand)\n\tconst [l9, c9, h9] = componentsIn(exact, 'oklch') as [number, number, number | null]\n\tconst hue = h9 ?? 0\n\tconst bg = options.background\n\tconst l1 = bg ? (componentsIn(toColor(bg), 'oklch')[0] as number) : dark ? 0.18 : 0.992\n\tconst step = (l: number, c: number): Color => toGamut({ colorSpace: 'oklch', components: [l, c, hue] }, { gamut })\n\n\tconst out: Color[] = []\n\tfor (let i = 0; i < 8; i++) out.push(step(l1 + (l9 - l1) * (LIGHT_PROGRESS[i] as number), c9 * (CHROMA[i] as number)))\n\tout.push(exact)\n\tconst hover = 0.03 / (l9 + 0.1)\n\tout.push(step(dark ? l9 + hover : l9 - hover, c9 * 0.93))\n\tconst c8 = c9 * (CHROMA[7] as number)\n\tout.push(step(dark ? 0.78 : 0.5, Math.min(c9 * 0.9, Math.max(c8, c9))))\n\tout.push(step(dark ? 0.93 : 0.3, Math.min(c9 * 0.35, Math.max(c8, c9))))\n\treturn out\n}\n"],"mappings":";;;;;AAmBA,MAAM,iBAAiB;CAAC;CAAG;CAAQ;CAAK;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;AAAC;AACjF,MAAM,SAAS;CAAC;CAAM;CAAM;CAAK;CAAM;CAAM;CAAM;CAAK;AAAI;;;;;;;;;AAU5D,SAAgB,eAAe,OAAuB,UAA6B,CAAC,GAAY;CAC/F,MAAM,EAAE,OAAO,SAAS,QAAQ,WAAW;CAC3C,MAAM,OAAO,UAAU,IAAI,MAAM;CACjC,MAAM,QAAQ,QAAQ,KAAK;CAC3B,MAAM,CAAC,IAAI,IAAI,MAAM,aAAa,OAAO,OAAO;CAChD,MAAM,MAAM,MAAM;CAClB,MAAM,KAAK,QAAQ;CACnB,MAAM,KAAK,KAAM,aAAa,QAAQ,EAAE,GAAG,OAAO,CAAC,CAAC,KAAgB,OAAO,MAAO;CAClF,MAAM,QAAQ,GAAW,MAAqB,QAAQ;EAAE,YAAY;EAAS,YAAY;GAAC;GAAG;GAAG;EAAG;CAAE,GAAG,EAAE,MAAM,CAAC;CAEjH,MAAM,MAAe,CAAC;CACtB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAM,KAAK,MAAO,eAAe,IAAe,KAAM,OAAO,EAAa,CAAC;CACrH,IAAI,KAAK,KAAK;CACd,MAAM,QAAQ,OAAQ,KAAK;CAC3B,IAAI,KAAK,KAAK,OAAO,KAAK,QAAQ,KAAK,OAAO,KAAK,GAAI,CAAC;CACxD,MAAM,KAAK,KAAM,OAAO;CACxB,IAAI,KAAK,KAAK,OAAO,MAAO,IAAK,KAAK,IAAI,KAAK,IAAK,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;CACtE,IAAI,KAAK,KAAK,OAAO,MAAO,IAAK,KAAK,IAAI,KAAK,KAAM,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;CACvE,OAAO;AACR"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ContrastOption } from "../color/contrast.js";
|
|
2
|
+
import { Color } from "../color/types.js";
|
|
3
|
+
/** Options for `createContrastScale`. */
|
|
4
|
+
export interface ContrastScaleOptions {
|
|
5
|
+
/** The color the scale is built around: its hue and chroma carry to every step. */
|
|
6
|
+
seed: Color | string;
|
|
7
|
+
/** What every step is measured against. */
|
|
8
|
+
background: Color | string;
|
|
9
|
+
/** Target contrast per step, in order, or a record of named targets (`{ text: 7, border: 3 }`). */
|
|
10
|
+
targets: number[] | Record<string, number>;
|
|
11
|
+
/** Default `wcag2`. */
|
|
12
|
+
contrast?: ContrastOption;
|
|
13
|
+
/** Which way to look first from the background. Default: darker on a light background, lighter on a dark one. */
|
|
14
|
+
direction?: 'lighter' | 'darker';
|
|
15
|
+
}
|
|
16
|
+
/** One step of a contrast scale: the target it was given and the color and contrast it got. */
|
|
17
|
+
export interface ContrastStep {
|
|
18
|
+
/** The key in a record of targets; otherwise the step's index as a string. */
|
|
19
|
+
name: string;
|
|
20
|
+
target: number;
|
|
21
|
+
color: Color;
|
|
22
|
+
/** The contrast the color achieves, measured on the color itself. */
|
|
23
|
+
contrast: number;
|
|
24
|
+
/** False when no lightness reaches `target`; `color` is then the best one found. */
|
|
25
|
+
met: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A Leonardo-style scale: each step is the color nearest the background's
|
|
29
|
+
* lightness, at the seed's hue and chroma, that reaches its target contrast.
|
|
30
|
+
* Contrast is the same across hues, which a fixed-lightness scale cannot give.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createContrastScale(options: ContrastScaleOptions): ContrastStep[];
|
|
33
|
+
//# sourceMappingURL=contrast-scale.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrast-scale.d.ts","names":[],"sources":["../../src/scale/contrast-scale.ts"],"mappings":";;;iBAMiB;;EAEhB,MAAM;;EAEN,YAAY;;EAEZ,oBAAoB;;EAEpB,WAAW;;EAEX;;;iBAIgB;;EAEhB;EACA;EACA,OAAO;;EAEP;;EAEA;;;;;;;wBAQe,oBAAoB,SAAS,uBAAuB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { findContrast } from "../color/contrast.js";
|
|
2
|
+
import { toColor } from "../color/parse.js";
|
|
3
|
+
import { componentsIn } from "../color/spaces.js";
|
|
4
|
+
// src/scale/contrast-scale.ts
|
|
5
|
+
/**
|
|
6
|
+
* A Leonardo-style scale: each step is the color nearest the background's
|
|
7
|
+
* lightness, at the seed's hue and chroma, that reaches its target contrast.
|
|
8
|
+
* Contrast is the same across hues, which a fixed-lightness scale cannot give.
|
|
9
|
+
*/
|
|
10
|
+
function createContrastScale(options) {
|
|
11
|
+
const { seed, background, targets, contrast, direction } = options;
|
|
12
|
+
const [, chroma, hue] = componentsIn(toColor(seed), "oklch");
|
|
13
|
+
return (Array.isArray(targets) ? targets.map((t, i) => [String(i), t]) : Object.entries(targets)).map(([name, target]) => ({
|
|
14
|
+
name,
|
|
15
|
+
target,
|
|
16
|
+
...findContrast(background, target, {
|
|
17
|
+
hue: hue ?? 0,
|
|
18
|
+
chroma,
|
|
19
|
+
contrast,
|
|
20
|
+
direction
|
|
21
|
+
})
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export { createContrastScale };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=contrast-scale.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrast-scale.js","names":[],"sources":["../../src/scale/contrast-scale.ts"],"sourcesContent":["import { type ContrastOption, findContrast } from '../color/contrast.js'\nimport { toColor } from '../color/parse.js'\nimport { componentsIn } from '../color/spaces.js'\nimport type { Color } from '../color/types.js'\n\n/** Options for `createContrastScale`. */\nexport interface ContrastScaleOptions {\n\t/** The color the scale is built around: its hue and chroma carry to every step. */\n\tseed: Color | string\n\t/** What every step is measured against. */\n\tbackground: Color | string\n\t/** Target contrast per step, in order, or a record of named targets (`{ text: 7, border: 3 }`). */\n\ttargets: number[] | Record<string, number>\n\t/** Default `wcag2`. */\n\tcontrast?: ContrastOption\n\t/** Which way to look first from the background. Default: darker on a light background, lighter on a dark one. */\n\tdirection?: 'lighter' | 'darker'\n}\n\n/** One step of a contrast scale: the target it was given and the color and contrast it got. */\nexport interface ContrastStep {\n\t/** The key in a record of targets; otherwise the step's index as a string. */\n\tname: string\n\ttarget: number\n\tcolor: Color\n\t/** The contrast the color achieves, measured on the color itself. */\n\tcontrast: number\n\t/** False when no lightness reaches `target`; `color` is then the best one found. */\n\tmet: boolean\n}\n\n/**\n * A Leonardo-style scale: each step is the color nearest the background's\n * lightness, at the seed's hue and chroma, that reaches its target contrast.\n * Contrast is the same across hues, which a fixed-lightness scale cannot give.\n */\nexport function createContrastScale(options: ContrastScaleOptions): ContrastStep[] {\n\tconst { seed, background, targets, contrast, direction } = options\n\tconst [, chroma, hue] = componentsIn(toColor(seed), 'oklch') as [number, number, number | null]\n\tconst entries: [string, number][] = Array.isArray(targets)\n\t\t? targets.map((t, i) => [String(i), t])\n\t\t: Object.entries(targets)\n\treturn entries.map(([name, target]) => ({\n\t\tname,\n\t\ttarget,\n\t\t...findContrast(background, target, { hue: hue ?? 0, chroma, contrast, direction })\n\t}))\n}\n"],"mappings":";;;;;;;;;AAoCA,SAAgB,oBAAoB,SAA+C;CAClF,MAAM,EAAE,MAAM,YAAY,SAAS,UAAU,cAAc;CAC3D,MAAM,GAAG,QAAQ,OAAO,aAAa,QAAQ,IAAI,GAAG,OAAO;CAI3D,QAHoC,MAAM,QAAQ,OAAO,IACtD,QAAQ,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IACpC,OAAO,QAAQ,OAAO,EAAA,CACV,KAAK,CAAC,MAAM,aAAa;EACvC;EACA;EACA,GAAG,aAAa,YAAY,QAAQ;GAAE,KAAK,OAAO;GAAG;GAAQ;GAAU;EAAU,CAAC;CACnF,EAAE;AACH"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MixOptions } from "../color/mix.js";
|
|
2
|
+
import { Color } from "../color/types.js";
|
|
3
|
+
/** A color at a position on the gradient line. */
|
|
4
|
+
export interface GradientStop {
|
|
5
|
+
/** A CSS color string or a color object. Its alpha is the stop's alpha. */
|
|
6
|
+
color: Color | string;
|
|
7
|
+
/**
|
|
8
|
+
* Where the stop sits, with 0 the start and 1 the end; it may fall outside
|
|
9
|
+
* 0 to 1. Leave it out to spread the stop evenly between its neighbors, as
|
|
10
|
+
* CSS gradients do.
|
|
11
|
+
*/
|
|
12
|
+
at?: number;
|
|
13
|
+
}
|
|
14
|
+
/** `in` and `hue` work as they do in `mix()`. The default space is `oklab`. */
|
|
15
|
+
export type GradientOptions = MixOptions;
|
|
16
|
+
/**
|
|
17
|
+
* Builds `count` colors, evenly spaced from 0 to 1 along a gradient through
|
|
18
|
+
* `stops`, the way a CSS gradient blends them: each pair of neighboring stops
|
|
19
|
+
* is mixed with `mix()`, so `in` and `hue` work as they do there. A stop is
|
|
20
|
+
* `{ color, at? }`, or a bare color when it needs no position. Before the
|
|
21
|
+
* first stop and after the last, the color holds; where two stops share a
|
|
22
|
+
* position, the color changes there and the sample at that position takes the
|
|
23
|
+
* later stop. Colors come back in the `in` space, default `oklab`.
|
|
24
|
+
*
|
|
25
|
+
* Alpha comes from each stop's color, 1 when the color has none, so
|
|
26
|
+
* `rgb(0 0 255 / 0.2)` is a stop at alpha 0.2. Alpha is interpolated linearly
|
|
27
|
+
* between stops, and the other components are premultiplied by it, so a
|
|
28
|
+
* transparent stop does not pull its neighbor's color toward its own. A color
|
|
29
|
+
* whose alpha is 1 has no `alpha` key; a `none` alpha takes the neighboring
|
|
30
|
+
* stop's.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* createGradient([{ color: 'blue', at: 0 }, { color: 'lime', at: 0.6 }, { color: 'yellow', at: 1 }], 72)
|
|
34
|
+
* createGradient(['blue', 'lime'], 5, { in: 'oklch' })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createGradient(stops: readonly (GradientStop | Color | string)[], count: number, options?: GradientOptions): Color[];
|
|
38
|
+
//# sourceMappingURL=gradient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradient.d.ts","names":[],"sources":["../../src/scale/gradient.ts"],"mappings":";;;iBAKiB;;EAEhB,OAAO;;;;;;EAMP;;;YAIW,kBAAkB;;;;;;;;;;;;;;;;;;;;;;wBAqDd,eACf,iBAAiB,eAAe,mBAChC,eACA,UAAS,kBACP"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mix } from "../color/mix.js";
|
|
2
|
+
import { toColor } from "../color/parse.js";
|
|
3
|
+
// src/scale/gradient.ts
|
|
4
|
+
/**
|
|
5
|
+
* Places each stop the way CSS gradients do (CSS Images 3 section 3.5.3): a
|
|
6
|
+
* first stop without a position goes at 0 and a last one at 1, a position
|
|
7
|
+
* before an earlier stop's moves up to it, and a run of stops without a
|
|
8
|
+
* position spreads evenly between the stops around it.
|
|
9
|
+
*/
|
|
10
|
+
function positions(stops) {
|
|
11
|
+
const n = stops.length;
|
|
12
|
+
const at = stops.map((s, i) => {
|
|
13
|
+
if (s.at !== void 0 && !Number.isFinite(s.at)) throw new Error(`stop ${i} has a position of ${s.at}`);
|
|
14
|
+
return s.at;
|
|
15
|
+
});
|
|
16
|
+
if (at[0] === void 0) at[0] = 0;
|
|
17
|
+
if (at[n - 1] === void 0) at[n - 1] = 1;
|
|
18
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
19
|
+
for (let i = 0; i < n; i++) {
|
|
20
|
+
const p = at[i];
|
|
21
|
+
if (p !== void 0) at[i] = max = Math.max(max, p);
|
|
22
|
+
}
|
|
23
|
+
for (let i = 1; i < n; i++) {
|
|
24
|
+
if (at[i] !== void 0) continue;
|
|
25
|
+
let j = i;
|
|
26
|
+
while (at[j] === void 0) j++;
|
|
27
|
+
const from = at[i - 1];
|
|
28
|
+
const step = (at[j] - from) / (j - i + 1);
|
|
29
|
+
for (let k = i; k < j; k++) at[k] = from + step * (k - i + 1);
|
|
30
|
+
}
|
|
31
|
+
return at;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds `count` colors, evenly spaced from 0 to 1 along a gradient through
|
|
35
|
+
* `stops`, the way a CSS gradient blends them: each pair of neighboring stops
|
|
36
|
+
* is mixed with `mix()`, so `in` and `hue` work as they do there. A stop is
|
|
37
|
+
* `{ color, at? }`, or a bare color when it needs no position. Before the
|
|
38
|
+
* first stop and after the last, the color holds; where two stops share a
|
|
39
|
+
* position, the color changes there and the sample at that position takes the
|
|
40
|
+
* later stop. Colors come back in the `in` space, default `oklab`.
|
|
41
|
+
*
|
|
42
|
+
* Alpha comes from each stop's color, 1 when the color has none, so
|
|
43
|
+
* `rgb(0 0 255 / 0.2)` is a stop at alpha 0.2. Alpha is interpolated linearly
|
|
44
|
+
* between stops, and the other components are premultiplied by it, so a
|
|
45
|
+
* transparent stop does not pull its neighbor's color toward its own. A color
|
|
46
|
+
* whose alpha is 1 has no `alpha` key; a `none` alpha takes the neighboring
|
|
47
|
+
* stop's.
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* createGradient([{ color: 'blue', at: 0 }, { color: 'lime', at: 0.6 }, { color: 'yellow', at: 1 }], 72)
|
|
51
|
+
* createGradient(['blue', 'lime'], 5, { in: 'oklch' })
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
function createGradient(stops, count, options = {}) {
|
|
55
|
+
if (stops.length < 2) throw new Error(`a gradient needs at least 2 stops, got ${stops.length}`);
|
|
56
|
+
if (!Number.isInteger(count) || count < 2) throw new Error(`count must be an integer of at least 2, got ${count}`);
|
|
57
|
+
const list = stops.map((s) => typeof s === "string" || "colorSpace" in s ? { color: s } : s);
|
|
58
|
+
const colors = list.map((s) => toColor(s.color));
|
|
59
|
+
const at = positions(list);
|
|
60
|
+
const last = list.length - 2;
|
|
61
|
+
return Array.from({ length: count }, (_, i) => {
|
|
62
|
+
const t = i / (count - 1);
|
|
63
|
+
let s = 0;
|
|
64
|
+
while (s < last && at[s + 1] <= t) s++;
|
|
65
|
+
const from = at[s];
|
|
66
|
+
const width = at[s + 1] - from;
|
|
67
|
+
const local = width > 0 ? Math.min(1, Math.max(0, (t - from) / width)) : t < from ? 0 : 1;
|
|
68
|
+
return mix(colors[s], colors[s + 1], local, options);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export { createGradient };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=gradient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gradient.js","names":[],"sources":["../../src/scale/gradient.ts"],"sourcesContent":["import { type MixOptions, mix } from '../color/mix.js'\nimport { toColor } from '../color/parse.js'\nimport type { Color } from '../color/types.js'\n\n/** A color at a position on the gradient line. */\nexport interface GradientStop {\n\t/** A CSS color string or a color object. Its alpha is the stop's alpha. */\n\tcolor: Color | string\n\t/**\n\t * Where the stop sits, with 0 the start and 1 the end; it may fall outside\n\t * 0 to 1. Leave it out to spread the stop evenly between its neighbors, as\n\t * CSS gradients do.\n\t */\n\tat?: number\n}\n\n/** `in` and `hue` work as they do in `mix()`. The default space is `oklab`. */\nexport type GradientOptions = MixOptions\n\n/**\n * Places each stop the way CSS gradients do (CSS Images 3 section 3.5.3): a\n * first stop without a position goes at 0 and a last one at 1, a position\n * before an earlier stop's moves up to it, and a run of stops without a\n * position spreads evenly between the stops around it.\n */\nfunction positions(stops: readonly GradientStop[]): number[] {\n\tconst n = stops.length\n\tconst at: (number | undefined)[] = stops.map((s, i) => {\n\t\tif (s.at !== undefined && !Number.isFinite(s.at)) throw new Error(`stop ${i} has a position of ${s.at}`)\n\t\treturn s.at\n\t})\n\tif (at[0] === undefined) at[0] = 0\n\tif (at[n - 1] === undefined) at[n - 1] = 1\n\tlet max = Number.NEGATIVE_INFINITY\n\tfor (let i = 0; i < n; i++) {\n\t\tconst p = at[i]\n\t\tif (p !== undefined) at[i] = max = Math.max(max, p)\n\t}\n\tfor (let i = 1; i < n; i++) {\n\t\tif (at[i] !== undefined) continue\n\t\tlet j = i\n\t\twhile (at[j] === undefined) j++\n\t\tconst from = at[i - 1] as number\n\t\tconst step = ((at[j] as number) - from) / (j - i + 1)\n\t\tfor (let k = i; k < j; k++) at[k] = from + step * (k - i + 1)\n\t}\n\treturn at as number[]\n}\n\n/**\n * Builds `count` colors, evenly spaced from 0 to 1 along a gradient through\n * `stops`, the way a CSS gradient blends them: each pair of neighboring stops\n * is mixed with `mix()`, so `in` and `hue` work as they do there. A stop is\n * `{ color, at? }`, or a bare color when it needs no position. Before the\n * first stop and after the last, the color holds; where two stops share a\n * position, the color changes there and the sample at that position takes the\n * later stop. Colors come back in the `in` space, default `oklab`.\n *\n * Alpha comes from each stop's color, 1 when the color has none, so\n * `rgb(0 0 255 / 0.2)` is a stop at alpha 0.2. Alpha is interpolated linearly\n * between stops, and the other components are premultiplied by it, so a\n * transparent stop does not pull its neighbor's color toward its own. A color\n * whose alpha is 1 has no `alpha` key; a `none` alpha takes the neighboring\n * stop's.\n *\n * ```ts\n * createGradient([{ color: 'blue', at: 0 }, { color: 'lime', at: 0.6 }, { color: 'yellow', at: 1 }], 72)\n * createGradient(['blue', 'lime'], 5, { in: 'oklch' })\n * ```\n */\nexport function createGradient(\n\tstops: readonly (GradientStop | Color | string)[],\n\tcount: number,\n\toptions: GradientOptions = {}\n): Color[] {\n\tif (stops.length < 2) throw new Error(`a gradient needs at least 2 stops, got ${stops.length}`)\n\tif (!Number.isInteger(count) || count < 2) throw new Error(`count must be an integer of at least 2, got ${count}`)\n\tconst list = stops.map((s): GradientStop => (typeof s === 'string' || 'colorSpace' in s ? { color: s } : s))\n\tconst colors = list.map((s) => toColor(s.color))\n\tconst at = positions(list)\n\tconst last = list.length - 2\n\treturn Array.from({ length: count }, (_, i) => {\n\t\tconst t = i / (count - 1)\n\t\tlet s = 0\n\t\twhile (s < last && (at[s + 1] as number) <= t) s++\n\t\tconst from = at[s] as number\n\t\tconst width = (at[s + 1] as number) - from\n\t\tconst local = width > 0 ? Math.min(1, Math.max(0, (t - from) / width)) : t < from ? 0 : 1\n\t\treturn mix(colors[s] as Color, colors[s + 1] as Color, local, options)\n\t})\n}\n"],"mappings":";;;;;;;;;AAyBA,SAAS,UAAU,OAA0C;CAC5D,MAAM,IAAI,MAAM;CAChB,MAAM,KAA6B,MAAM,KAAK,GAAG,MAAM;EACtD,IAAI,EAAE,OAAO,KAAA,KAAa,CAAC,OAAO,SAAS,EAAE,EAAE,GAAG,MAAM,IAAI,MAAM,QAAQ,EAAE,qBAAqB,EAAE,IAAI;EACvG,OAAO,EAAE;CACV,CAAC;CACD,IAAI,GAAG,OAAO,KAAA,GAAW,GAAG,KAAK;CACjC,IAAI,GAAG,IAAI,OAAO,KAAA,GAAW,GAAG,IAAI,KAAK;CACzC,IAAI,MAAM,OAAO;CACjB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;EAC3B,MAAM,IAAI,GAAG;EACb,IAAI,MAAM,KAAA,GAAW,GAAG,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC;CACnD;CACA,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;EAC3B,IAAI,GAAG,OAAO,KAAA,GAAW;EACzB,IAAI,IAAI;EACR,OAAO,GAAG,OAAO,KAAA,GAAW;EAC5B,MAAM,OAAO,GAAG,IAAI;EACpB,MAAM,QAAS,GAAG,KAAgB,SAAS,IAAI,IAAI;EACnD,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,OAAO,QAAQ,IAAI,IAAI;CAC5D;CACA,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,eACf,OACA,OACA,UAA2B,CAAC,GAClB;CACV,IAAI,MAAM,SAAS,GAAG,MAAM,IAAI,MAAM,0CAA0C,MAAM,QAAQ;CAC9F,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG,MAAM,IAAI,MAAM,+CAA+C,OAAO;CACjH,MAAM,OAAO,MAAM,KAAK,MAAqB,OAAO,MAAM,YAAY,gBAAgB,IAAI,EAAE,OAAO,EAAE,IAAI,CAAE;CAC3G,MAAM,SAAS,KAAK,KAAK,MAAM,QAAQ,EAAE,KAAK,CAAC;CAC/C,MAAM,KAAK,UAAU,IAAI;CACzB,MAAM,OAAO,KAAK,SAAS;CAC3B,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM;EAC9C,MAAM,IAAI,KAAK,QAAQ;EACvB,IAAI,IAAI;EACR,OAAO,IAAI,QAAS,GAAG,IAAI,MAAiB,GAAG;EAC/C,MAAM,OAAO,GAAG;EAChB,MAAM,QAAS,GAAG,IAAI,KAAgB;EACtC,MAAM,QAAQ,QAAQ,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,QAAQ,KAAK,CAAC,IAAI,IAAI,OAAO,IAAI;EACxF,OAAO,IAAI,OAAO,IAAa,OAAO,IAAI,IAAa,OAAO,OAAO;CACtE,CAAC;AACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BrandScaleOptions, scaleFromBrand } from "./brand.js";
|
|
2
|
+
import { ContrastScaleOptions, ContrastStep, createContrastScale } from "./contrast-scale.js";
|
|
3
|
+
import { GradientOptions, GradientStop, createGradient } from "./gradient.js";
|
|
4
|
+
import { ScaleOptions, ScalePreset, createScale } from "./scale.js";
|
|
5
|
+
import { MATERIAL_TONES, TonalPaletteOptions, lstar, tonalPalette, toneColor } from "./tone.js";
|
|
6
|
+
export { type BrandScaleOptions, type ContrastScaleOptions, type ContrastStep, type GradientOptions, type GradientStop, MATERIAL_TONES, type ScaleOptions, type ScalePreset, type TonalPaletteOptions, createContrastScale, createGradient, createScale, lstar, scaleFromBrand, tonalPalette, toneColor };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { scaleFromBrand } from "./brand.js";
|
|
2
|
+
import { createContrastScale } from "./contrast-scale.js";
|
|
3
|
+
import { createGradient } from "./gradient.js";
|
|
4
|
+
import { createScale } from "./scale.js";
|
|
5
|
+
import { MATERIAL_TONES, lstar, tonalPalette, toneColor } from "./tone.js";
|
|
6
|
+
export { MATERIAL_TONES, createContrastScale, createGradient, createScale, lstar, scaleFromBrand, tonalPalette, toneColor };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { GamutId } from "../color/gamut.js";
|
|
2
|
+
import { Color } from "../color/types.js";
|
|
3
|
+
/** The built-in step curves. */
|
|
4
|
+
export type ScalePreset = 'tailwind-11' | 'radix-12' | 'tone-13' | 'even';
|
|
5
|
+
/** Options for `createScale`. Only `seed` is required. */
|
|
6
|
+
export interface ScaleOptions {
|
|
7
|
+
/** The color the scale is built around. Its hue and chroma set the anchor step. */
|
|
8
|
+
seed: Color | string;
|
|
9
|
+
/**
|
|
10
|
+
* A preset, or a number of steps for an even scale. Default `tailwind-11`.
|
|
11
|
+
* `tone-13` steps are CIE L* tones, so any two steps whose tones differ by
|
|
12
|
+
* 40 reach contrast 3 and by 50 about 4.5.
|
|
13
|
+
*/
|
|
14
|
+
steps?: ScalePreset | number;
|
|
15
|
+
/** Overrides the curve's lightness, one per step: OKLCH L 0 to 1 (L* tones 0 to 100 for `tone-13`). */
|
|
16
|
+
lightness?: number[];
|
|
17
|
+
/** Overrides chroma, as a multiplier of the seed's chroma, one per step. */
|
|
18
|
+
chroma?: number[];
|
|
19
|
+
/** Overrides hue drift, as degrees added to the seed's hue, one per step; a number is applied to every step. */
|
|
20
|
+
hueShift?: number[] | number;
|
|
21
|
+
/** The gamut every step must fit. Default `srgb`. */
|
|
22
|
+
gamut?: GamutId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Builds a scale from light to dark around `seed`: one color per step, in
|
|
26
|
+
* OKLCH, mapped into `gamut`. The preset gives each step's lightness, chroma
|
|
27
|
+
* (relative to the seed's) and hue drift; pass `lightness`, `chroma` or
|
|
28
|
+
* `hueShift` to replace any of them.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createScale(options: ScaleOptions): Color[];
|
|
31
|
+
//# sourceMappingURL=scale.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scale.d.ts","names":[],"sources":["../../src/scale/scale.ts"],"mappings":";;;YASY;;iBA2DK;;EAEhB,MAAM;;;;;;EAMN,QAAQ;;EAER;;EAEA;;EAEA;;EAEA,QAAQ;;;;;;;;wBAoBO,YAAY,SAAS,eAAe"}
|