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