colorizr 3.0.2 → 3.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colorizr",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "Manipulate colors like a boss",
5
5
  "author": "Gil Barbara <gilbarbara@gmail.com>",
6
6
  "keywords": [
@@ -32,13 +32,13 @@
32
32
  "types": "dist/index.d.ts",
33
33
  "sideEffects": false,
34
34
  "devDependencies": {
35
- "@arethetypeswrong/cli": "^0.17.0",
35
+ "@arethetypeswrong/cli": "^0.17.2",
36
36
  "@gilbarbara/eslint-config": "^0.8.2",
37
37
  "@gilbarbara/prettier-config": "^1.0.0",
38
38
  "@gilbarbara/tsconfig": "^0.2.3",
39
39
  "@size-limit/preset-small-lib": "^11.1.6",
40
- "@types/node": "^22.10.1",
41
- "@vitest/coverage-v8": "^2.1.6",
40
+ "@types/node": "^22.10.2",
41
+ "@vitest/coverage-v8": "^2.1.8",
42
42
  "del-cli": "^6.0.0",
43
43
  "husky": "^9.1.7",
44
44
  "is-ci-cli": "^2.2.0",
@@ -46,9 +46,9 @@
46
46
  "size-limit": "^11.1.6",
47
47
  "ts-node": "^10.9.2",
48
48
  "tsup": "^8.3.5",
49
- "typescript": "^5.7.2",
50
- "vite-tsconfig-paths": "^5.1.3",
51
- "vitest": "^2.1.6",
49
+ "typescript": "^5.6.3",
50
+ "vite-tsconfig-paths": "^5.1.4",
51
+ "vitest": "^2.1.8",
52
52
  "watch-run": "^1.2.5"
53
53
  },
54
54
  "scripts": {
package/src/colorizr.ts CHANGED
@@ -20,9 +20,11 @@ import textColor from '~/text-color';
20
20
  import transparentize from '~/transparentize';
21
21
  import { Alpha, Amount, Analysis, ColorType, Degrees, HEX, HSL, LAB, LCH, RGB } from '~/types';
22
22
 
23
- export interface Options {
23
+ export interface ColorizrOptions {
24
24
  /**
25
- * The output color type
25
+ * Output color format.
26
+ *
27
+ * If not specified, the output will use the same format as the input color.
26
28
  */
27
29
  format?: ColorType;
28
30
  }
@@ -36,7 +38,7 @@ export default class Colorizr {
36
38
  public rgb: RGB;
37
39
  public type: ColorType;
38
40
 
39
- constructor(color: string | HSL | LAB | LCH | RGB, options: Options = {}) {
41
+ constructor(color: string | HSL | LAB | LCH | RGB, options: ColorizrOptions = {}) {
40
42
  invariant(!!color, 'color is required');
41
43
 
42
44
  const { alpha, hex, hsl, oklab, oklch, rgb, type } = parseColor(color);
package/src/format-css.ts CHANGED
@@ -7,10 +7,13 @@ import { isHex, isHSL, isLAB, isLCH, isValidColorModel } from '~/modules/validat
7
7
  import * as converters from '~/converters';
8
8
  import { Alpha, ColorModel, ColorType, HEX, HSL } from '~/types';
9
9
 
10
- export interface FormatOptions {
10
+ export interface FormatCSSOptions {
11
+ /**
12
+ * The alpha value of the color.
13
+ */
11
14
  alpha?: Alpha;
12
15
  /**
13
- * The output color type.
16
+ * Output color format.
14
17
  * @default 'hex'
15
18
  */
16
19
  format?: ColorType;
@@ -21,6 +24,7 @@ export interface FormatOptions {
21
24
  precision?: number;
22
25
  /**
23
26
  * The separator between the values.
27
+ *
24
28
  * oklab and oklch always use space as a separator.
25
29
  * @default ' '
26
30
  */
@@ -29,7 +33,7 @@ export interface FormatOptions {
29
33
 
30
34
  export default function formatCSS<T extends ColorModel | HEX>(
31
35
  input: T,
32
- options: FormatOptions = {},
36
+ options: FormatCSSOptions = {},
33
37
  ): string {
34
38
  invariant(isHex(input) || isValidColorModel(input), MESSAGES.invalid);
35
39
 
package/src/index.ts CHANGED
@@ -33,5 +33,12 @@ export * from '~/types';
33
33
  export * from '~/modules/hex-utils';
34
34
  export { isHex, isHSL, isLAB, isLCH, isRGB } from '~/modules/validators';
35
35
 
36
+ export type { ColorizrOptions } from '~/colorizr';
37
+ export type { FormatCSSOptions } from '~/format-css';
38
+ export type { PaletteOptions } from '~/palette';
39
+ export type { Scheme, SchemeOptions } from '~/scheme';
40
+ export type { Swatch, SwatchOptions, SwatchVariant } from '~/swatch';
41
+ export type { TextColorOptions } from '~/text-color';
42
+
36
43
  // eslint-disable-next-line unicorn/prefer-export-from
37
44
  export default Colorizr;
@@ -3,7 +3,6 @@ export function invariant(condition: boolean, message: string): asserts conditio
3
3
  return;
4
4
  }
5
5
 
6
- /* istanbul ignore else */
7
6
  if (process.env.NODE_ENV !== 'production') {
8
7
  if (message === undefined) {
9
8
  throw new Error('invariant requires an error message argument');
package/src/palette.ts CHANGED
@@ -11,14 +11,36 @@ import rotate from '~/rotate';
11
11
  import { ColorType, HEX } from '~/types';
12
12
 
13
13
  export interface PaletteOptions {
14
+ /**
15
+ * Output color format.
16
+ *
17
+ * If not specified, the output will use the same format as the input color.
18
+ */
14
19
  format?: ColorType;
20
+ /**
21
+ * Adjusts the lightness of the base color before generating the palette.
22
+ *
23
+ * Value should be between 0 and 100.
24
+ */
15
25
  lightness?: number;
26
+ /**
27
+ * Adjusts the saturation of the base color before generating the palette.
28
+ *
29
+ * Value should be between 0 and 100.
30
+ */
16
31
  saturation?: number;
17
32
  /**
18
- * The number of colors to generate
33
+ * The number of colors to generate in the palette.
34
+ *
35
+ * Minimum value is 2.
19
36
  * @default 6
20
37
  */
21
38
  size?: number;
39
+ /**
40
+ * Generate a monochromatic palette.
41
+ *
42
+ * For more options, use the `swatch` function.
43
+ */
22
44
  type?: 'monochromatic';
23
45
  }
24
46
 
@@ -28,31 +50,25 @@ export default function palette(input: string, options: PaletteOptions = {}): st
28
50
 
29
51
  const { format, lightness, saturation, size = 6, type } = options;
30
52
  const hsl = parseCSS(input, 'hsl');
31
- const output: string[] = [];
32
53
  const colorFormat = isHex(input) || isNamedColor(input) ? 'hex' : extractColorParts(input).model;
33
54
 
34
- switch (type) {
35
- case 'monochromatic': {
36
- const step = 80 / size;
55
+ const output: string[] = [];
37
56
 
38
- for (let index = size; index > 0; index--) {
39
- output.push(hsl2hex({ ...hsl, l: step * index }));
40
- }
57
+ if (type === 'monochromatic') {
58
+ const step = 80 / size;
41
59
 
42
- break;
60
+ for (let index = size; index > 0; index--) {
61
+ output.push(hsl2hex({ ...hsl, l: step * index }));
43
62
  }
44
- default: {
45
- const step = 360 / size;
46
-
47
- output.push(hsl2hex({ ...hsl, l: lightness ?? hsl.l, s: saturation ?? hsl.s }));
63
+ } else {
64
+ const step = 360 / size;
48
65
 
49
- for (let index = 1; index < size; index++) {
50
- const color = rotate(input, hsl.h + step * index, 'hex') as HEX;
66
+ output.push(hsl2hex({ ...hsl, l: lightness ?? hsl.l, s: saturation ?? hsl.s }));
51
67
 
52
- output.push(hsl2hex({ ...hex2hsl(color), l: lightness ?? hsl.l, s: saturation ?? hsl.s }));
53
- }
68
+ for (let index = 1; index < size; index++) {
69
+ const color = rotate(input, hsl.h + step * index, 'hex') as HEX;
54
70
 
55
- break;
71
+ output.push(hsl2hex({ ...hex2hsl(color), l: lightness ?? hsl.l, s: saturation ?? hsl.s }));
56
72
  }
57
73
  }
58
74
 
package/src/scheme.ts CHANGED
@@ -18,6 +18,11 @@ export type Scheme =
18
18
  | 'triadic';
19
19
 
20
20
  export interface SchemeOptions {
21
+ /**
22
+ * Output color format.
23
+ *
24
+ * If not specified, the output will use the same format as the input color.
25
+ */
21
26
  format?: ColorType;
22
27
  /**
23
28
  * The type of scheme to generate.
package/src/swatch.ts CHANGED
@@ -9,47 +9,81 @@ import oklch2rgb from '~/converters/oklch2rgb';
9
9
  import rgb2hex from '~/converters/rgb2hex';
10
10
  import extractColorParts from '~/extract-color-parts';
11
11
  import parseCSS from '~/parse-css';
12
- import { ColorType, HEX, LCH } from '~/types';
12
+ import { ColorTokens, ColorType, HEX, LCH } from '~/types';
13
13
 
14
- type ColorTokens = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
15
-
16
- type Swatch = {
14
+ export type Swatch = {
17
15
  [key in ColorTokens]: string;
18
16
  };
19
17
 
18
+ export type SwatchVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
19
+
20
20
  export interface SwatchOptions {
21
+ /**
22
+ * Output color format.
23
+ *
24
+ * If not specified, the output will use the same format as the input color.
25
+ */
21
26
  format?: ColorType;
22
27
  /**
23
- * The scale of the swatch.
24
- * Linear scale will have equal distance between each shade.
28
+ * The mode of the swatch.
29
+ * - 'light': Focuses on lighter tones starting from a higher lightness.
30
+ * - 'dark': Focuses on darker tones starting from a lower lightness.
31
+ *
32
+ * If omitted, a balanced palette is generated.
33
+ */
34
+ mode?: 'light' | 'dark';
35
+ /**
36
+ * Generate a monochromatic swatch.
37
+ *
38
+ * Disable chroma adjustments for all shades.
39
+ * @default false
40
+ */
41
+ monochromatic?: boolean;
42
+ /**
43
+ * Determines the scale type for the swatch.
44
+ * - 'linear': Shades are distributed with equal lightness intervals.
45
+ * - 'dynamic': Shades are distributed adaptively based on the input color.
25
46
  * @default 'dynamic'
26
47
  */
27
48
  scale?: 'dynamic' | 'linear';
49
+ /**
50
+ * The variant of the swatch.
51
+ * - 'deep': Generates rich and bold tones with significantly reduced lightness.
52
+ * - 'neutral': Generates muted tones by reducing chroma.
53
+ * - 'pastel': Produces soft and airy tones with significant chroma reduction.
54
+ * - 'subtle': Creates extremely desaturated tones, close to grayscale.
55
+ * - 'vibrant': Enhances chroma for bold and striking tones.
56
+ */
57
+ variant?: SwatchVariant;
28
58
  }
29
59
 
30
- const MIN_LIGHTNESS = 21;
31
- const MAX_LIGHTNESS = 97;
60
+ const MIN_LIGHTNESS = 5;
61
+ const MAX_LIGHTNESS = 95;
32
62
 
33
63
  /**
34
64
  * Generate a shade of a color based its lightness tuning factor
35
65
  */
36
- function shadeColorDynamic(input: LCH, lightnessTuningFactor: number, chromaTuningFactor = 0): HEX {
66
+ function shadeColorDynamic(input: LCH, lightnessTuningFactor: number, chromaFactor = 0): HEX {
37
67
  if (lightnessTuningFactor === 0) {
38
- return rgb2hex(oklch2rgb({ ...input, l: input.l / 100 }));
68
+ const { l } = input;
69
+
70
+ return rgb2hex(oklch2rgb({ ...input, l: l / 100 }));
39
71
  }
40
72
 
41
73
  // Convert back to RGB and make sure it's within the sRGB gamut
42
- return shadeColor(input, input.l + lightnessTuningFactor, chromaTuningFactor);
74
+ return shadeColor(input, input.l + lightnessTuningFactor, chromaFactor);
43
75
  }
44
76
 
45
77
  /**
46
78
  * Generate a shade of a color based its lightness tuning factor
47
79
  */
48
- function shadeColor(input: LCH, lightness: number, chromaTuningFactor = 0): HEX {
80
+ function shadeColor(input: LCH, lightness: number, chromaFactor = 0): HEX {
49
81
  const { c, h } = input;
50
82
 
83
+ const adjustedChroma = clamp(c + chromaFactor, 0, 0.4);
84
+
51
85
  // Convert back to RGB and make sure it's within the sRGB gamut
52
- return oklch2hex({ l: lightness / 100, c: clamp(c + chromaTuningFactor, 0, 0.4), h });
86
+ return oklch2hex({ l: lightness / 100, c: adjustedChroma, h });
53
87
  }
54
88
 
55
89
  /**
@@ -57,45 +91,68 @@ function shadeColor(input: LCH, lightness: number, chromaTuningFactor = 0): HEX
57
91
  */
58
92
  export default function swatch(input: string, options: SwatchOptions = {}): Swatch {
59
93
  invariant(isString(input), MESSAGES.inputString);
60
- const { format, scale = 'dynamic' } = options;
94
+ const { format, monochromatic = false, scale = 'dynamic', mode, variant = 'base' } = options;
61
95
 
62
96
  const lch = parseCSS(input, 'oklch');
97
+ const chromaScale = {
98
+ base: 1,
99
+ deep: 0.8,
100
+ neutral: 0.5,
101
+ pastel: 0.3,
102
+ subtle: 0.2,
103
+ vibrant: 1.25,
104
+ }[variant];
63
105
 
64
106
  lch.l = 50;
107
+ lch.c *= chromaScale;
108
+
109
+ if (variant === 'deep') {
110
+ lch.l *= 0.7;
111
+ }
65
112
 
66
113
  const colorFormat = isHex(input) || isNamedColor(input) ? 'hex' : extractColorParts(input).model;
67
114
 
68
115
  const currentLightness = lch.l;
69
- const safeMaxLightness = currentLightness >= 88.5 ? 99.5 : MAX_LIGHTNESS;
70
- const safeMinLightness = currentLightness <= 33 ? 0 : MIN_LIGHTNESS;
71
- const lightBase = (safeMaxLightness - currentLightness) / 5;
72
- const darkBase = (-1 * (currentLightness - safeMinLightness)) / 8;
116
+ let lightSteps = 5;
117
+ let darkSteps = 8;
118
+
119
+ if (mode) {
120
+ lightSteps *= mode === 'light' ? 0.75 : 1.25;
121
+ darkSteps *= mode === 'light' ? 1.25 : 0.75;
122
+ }
123
+
124
+ const lightStepSize = (MAX_LIGHTNESS - currentLightness) / lightSteps;
125
+ const darkStepSize = (-1 * (currentLightness - MIN_LIGHTNESS)) / darkSteps;
126
+
127
+ const getChromaFactor = (value: number): number => {
128
+ return monochromatic ? 0 : value; // Disable adjustments for monochromatic swatches
129
+ };
73
130
 
74
131
  const output: Swatch =
75
132
  scale === 'linear'
76
133
  ? {
77
- 50: shadeColor(lch, 95, -0.00375),
78
- 100: shadeColor(lch, 90, -0.00375),
79
- 200: shadeColor(lch, 80, -0.00375),
80
- 300: shadeColor(lch, 70, -0.00375),
81
- 400: shadeColor(lch, 60, -0.00375),
134
+ 50: shadeColor(lch, 95, getChromaFactor(-0.00375)),
135
+ 100: shadeColor(lch, 90, getChromaFactor(-0.00375)),
136
+ 200: shadeColor(lch, 80, getChromaFactor(-0.00375)),
137
+ 300: shadeColor(lch, 70, getChromaFactor(-0.00375)),
138
+ 400: shadeColor(lch, 60, getChromaFactor(-0.00375)),
82
139
  500: shadeColor(lch, 50),
83
- 600: shadeColor(lch, 40, 0.025),
84
- 700: shadeColor(lch, 30, 0.05),
85
- 800: shadeColor(lch, 20, 0.075),
86
- 900: shadeColor(lch, 10, 0.1),
140
+ 600: shadeColor(lch, 40, getChromaFactor(0.025)),
141
+ 700: shadeColor(lch, 30, getChromaFactor(0.05)),
142
+ 800: shadeColor(lch, 20, getChromaFactor(0.075)),
143
+ 900: shadeColor(lch, 10, getChromaFactor(0.1)),
87
144
  }
88
145
  : {
89
- 50: shadeColorDynamic(lch, 5 * lightBase, -0.00375),
90
- 100: shadeColorDynamic(lch, 4 * lightBase, -0.00375),
91
- 200: shadeColorDynamic(lch, 3 * lightBase, -0.00375),
92
- 300: shadeColorDynamic(lch, 2 * lightBase, -0.00375),
93
- 400: shadeColorDynamic(lch, lightBase, -0.00375),
146
+ 50: shadeColorDynamic(lch, 5 * lightStepSize, getChromaFactor(-0.00375)),
147
+ 100: shadeColorDynamic(lch, 4 * lightStepSize, getChromaFactor(-0.00375)),
148
+ 200: shadeColorDynamic(lch, 3 * lightStepSize, getChromaFactor(-0.00375)),
149
+ 300: shadeColorDynamic(lch, 2 * lightStepSize, getChromaFactor(-0.00375)),
150
+ 400: shadeColorDynamic(lch, lightStepSize, getChromaFactor(-0.00375)),
94
151
  500: shadeColorDynamic(lch, 0),
95
- 600: shadeColorDynamic(lch, 1.6 * darkBase, 0.025),
96
- 700: shadeColorDynamic(lch, 1.875 * 2 * darkBase, 0.05),
97
- 800: shadeColorDynamic(lch, 3 * 2 * darkBase, 0.075),
98
- 900: shadeColorDynamic(lch, 4 * 2 * darkBase, 0.1),
152
+ 600: shadeColorDynamic(lch, 1.6 * darkStepSize, getChromaFactor(0.025)),
153
+ 700: shadeColorDynamic(lch, 3.2 * darkStepSize, getChromaFactor(0.05)),
154
+ 800: shadeColorDynamic(lch, 4.8 * darkStepSize, getChromaFactor(0.075)),
155
+ 900: shadeColorDynamic(lch, 6.4 * darkStepSize, getChromaFactor(0.1)),
99
156
  };
100
157
 
101
158
  return Object.entries(output).reduce((acc, [key, value]) => {
package/src/text-color.ts CHANGED
@@ -5,7 +5,7 @@ import { isString } from '~/modules/validators';
5
5
  import hex2rgb from '~/converters/hex2rgb';
6
6
  import parseCSS from '~/parse-css';
7
7
 
8
- interface Options {
8
+ export interface TextColorOptions {
9
9
  /**
10
10
  * The dark color to return if the input is light.
11
11
  * @default '#000000'
@@ -18,6 +18,7 @@ interface Options {
18
18
  lightColor?: string;
19
19
  /**
20
20
  * The threshold to determine if the color is light or dark.
21
+ *
21
22
  * A number between 0 and 255.
22
23
  * @default 128
23
24
  */
@@ -27,14 +28,23 @@ interface Options {
27
28
  /**
28
29
  * Get the contrasted color for a given hex.
29
30
  */
30
- export default function textColor(input: string, options: Options = {}): string {
31
+ export default function textColor(input: string, options: TextColorOptions = {}): string {
31
32
  const { darkColor = '#000000', lightColor = '#ffffff', threshold = 128 } = options;
32
33
 
33
34
  invariant(isString(input), MESSAGES.inputString);
34
35
  invariant(threshold >= 0 && threshold <= 255, MESSAGES.threshold);
35
36
 
36
- const { r, g, b } = hex2rgb(parseCSS(input, 'hex'));
37
- const yiq = (r * 299 + g * 587 + b * 114) / 1000;
37
+ try {
38
+ const { r, g, b } = hex2rgb(parseCSS(input, 'hex'));
39
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
38
40
 
39
- return yiq >= threshold ? darkColor : lightColor;
41
+ return yiq >= threshold ? darkColor : lightColor;
42
+ } catch (error) {
43
+ /* eslint-disable no-console */
44
+ console.warn(`Invalid color input: ${input}`);
45
+ console.warn(error);
46
+ /* eslint-enable no-console */
47
+
48
+ return darkColor; // Default to dark color in case of error
49
+ }
40
50
  }
@@ -14,7 +14,8 @@ export type ColorModelKeys<TModel extends ColorModelKey> = TModel extends 'hsl'
14
14
 
15
15
  export type ColorKeysTuple = [string, string, string];
16
16
  export type ColorTuple = [number, number, number];
17
- export type ConverterParameters<TModel extends ColorModel> = TModel | ColorTuple;
17
+ export type ColorTokens = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
18
+
18
19
  export interface Colors {
19
20
  alpha: Alpha;
20
21
  hex: HEX;
@@ -25,6 +26,8 @@ export interface Colors {
25
26
  type: ColorType;
26
27
  }
27
28
 
29
+ export type ConverterParameters<TModel extends ColorModel> = TModel | ColorTuple;
30
+
28
31
  /* A number between 0 and 1 */
29
32
  export type Alpha = number;
30
33