colorizr 5.0.1 → 5.1.1

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/dist/index.d.mts CHANGED
@@ -9,7 +9,6 @@ type ColorTypeInput = ColorType | 'named';
9
9
  type ColorValue = ColorModel | HEX;
10
10
  type ConverterParameters<TModel extends ColorModel> = TModel | ColorTuple;
11
11
  type HEX = `#${string}`;
12
- type PlainObject<T = any> = Record<string, T>;
13
12
  interface Analysis {
14
13
  brightnessDifference: number;
15
14
  colorDifference: number;
@@ -524,7 +523,7 @@ declare function desaturate(input: string, amount: number, format?: ColorType):
524
523
  type ExtractColorPartsReturn = {
525
524
  alpha?: number;
526
525
  model: ColorModelKey;
527
- } & PlainObject<number>;
526
+ } & Record<string, number>;
528
527
  /**
529
528
  * Extract the color parts from a CSS color string.
530
529
  * Hex colors are supported via conversion.
@@ -919,8 +918,20 @@ declare function rotate(input: string, degrees: number, format?: ColorType): str
919
918
  */
920
919
  declare function saturate(input: string, amount: number, format?: ColorType): string;
921
920
 
922
- type ScaleMode = 'light' | 'dark';
921
+ type ScaleMode = 'light' | 'dark' | 'reversed';
923
922
  type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
923
+ /**
924
+ * Parabolic chroma curve with a movable center.
925
+ */
926
+ interface ScaleChromaPeak {
927
+ /** Blend between constant chroma (0) and the full parabola (1). */
928
+ amount: number;
929
+ /**
930
+ * Lightness (exclusive 0-1) where chroma stays at full base.
931
+ * @default 0.5
932
+ */
933
+ peak?: number;
934
+ }
924
935
  /**
925
936
  * Options for generating a color scale.
926
937
  *
@@ -931,14 +942,29 @@ type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
931
942
  */
932
943
  interface ScaleOptions {
933
944
  /**
934
- * Controls chroma distribution across lightness levels (0-1).
945
+ * Controls how chroma flows across the scale. Three forms:
946
+ *
947
+ * Scalar (0-1) — parabolic blend centered at mid-lightness:
935
948
  * - 0: Constant chroma across all steps (default).
936
- * - 1: Parabolic curvefull chroma at mid-lightness, reduced toward extremes.
937
- * - Values between interpolate between constant and parabolic.
949
+ * - 1: Full parabolapeak chroma at mid-lightness, reduced toward extremes.
950
+ * Shorthand for `{ amount: x }`.
951
+ *
952
+ * `{ amount, peak }` — the same parabola with a movable center.
953
+ * `peak` is the lightness (exclusive 0-1) that keeps full base chroma;
954
+ * sides away from it are reduced. Defaults to 0.5.
955
+ *
956
+ * `{ low, high }` — fractions (0-1) of the maximum P3 chroma at each step,
957
+ * blended from `low` (50-end) through the input color's own fraction at the
958
+ * lock/middle step to `high` (950-end). NOT equivalent to the scalar form —
959
+ * it is a different model: values set chroma relative to the gamut ceiling
960
+ * instead of bending the input's chroma. Being gamut-relative, the input's
961
+ * chroma matters only at the anchor step; an achromatic input collapses the
962
+ * anchor fraction to 0, yielding a V-shaped, colored scale (full at the ends,
963
+ * gray at the anchor).
938
964
  *
939
965
  * @default 0
940
966
  */
941
- chromaCurve?: number;
967
+ chromaCurve?: number | ScaleChromaPeak | ScaleRange;
942
968
  /**
943
969
  * Output color format.
944
970
  *
@@ -947,14 +973,36 @@ interface ScaleOptions {
947
973
  * If not specified, the output will match the format of the input color.
948
974
  */
949
975
  format?: ColorType;
976
+ /**
977
+ * Hue rotation across the scale, in degrees.
978
+ *
979
+ * A scalar `x` is shorthand for `{ low: -x, high: x }`.
980
+ * `low` shifts the low-key end (50), `high` the high-key end (950); shifts
981
+ * blend to 0° at the lock step (or the middle step when not locked), so the
982
+ * input hue is preserved there. The per-step gamut clamp follows the
983
+ * shifted hue.
984
+ *
985
+ * Values must be within [-180, 180].
986
+ *
987
+ * @default 0
988
+ */
989
+ hueShift?: number | ScaleRange;
950
990
  /**
951
991
  * The lightness tuning factor for the scale.
952
992
  * - 1: Linear lightness distribution.
953
993
  * - >1: Lighter tones are emphasized.
954
994
  * - <1: Darker tones are emphasized.
995
+ *
996
+ * A scalar `x` is shorthand for `{ low: x, high: x }`. With `{ low, high }`
997
+ * each half of the scale gets its own exponent: `low` shapes the low-key
998
+ * half (toward 50), `high` the high-key half (toward 950), split at the
999
+ * lock step when locked or blended continuously when not.
1000
+ * Reference palettes typically fit `{ low: 1.5, high: 1 }`.
1001
+ *
1002
+ * Values must be greater than 0.
955
1003
  * @default 1.5
956
1004
  */
957
- lightnessCurve?: number;
1005
+ lightnessCurve?: number | ScaleRange;
958
1006
  /**
959
1007
  * Lock input color's lightness at specific step position.
960
1008
  *
@@ -965,6 +1013,11 @@ interface ScaleOptions {
965
1013
  * Must be a valid step key for the current step count.
966
1014
  * Default step keys (11 steps): 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950.
967
1015
  * Step keys vary based on the `steps` option (3-20 steps supported).
1016
+ *
1017
+ * When locked at the first or last step, only one side of the scale remains,
1018
+ * so the unused-side value (`low`/`high`) of asymmetric options
1019
+ * (`hueShift`, `lightnessCurve`, endpoint `chromaCurve`) has no effect and
1020
+ * emits a warning.
968
1021
  */
969
1022
  lock?: number;
970
1023
  /**
@@ -990,7 +1043,14 @@ interface ScaleOptions {
990
1043
  * Theme-aware lightness direction.
991
1044
  *
992
1045
  * - 'light': Low keys (50) are lightest, high keys (950) are darkest
993
- * - 'dark': Low keys (50) are darkest, high keys (950) are lightest
1046
+ * - 'dark': Low keys (50) are darkest, high keys (950) are lightest, re-derived
1047
+ * with the lightness ramp running in the dark direction
1048
+ * - 'reversed': the 'light' palette with its keys reversed — identical colors in
1049
+ * mirrored order (50↔950, 100↔900, …)
1050
+ *
1051
+ * `dark` re-derives the scale, so its intermediate steps are not a reversal of
1052
+ * `light` (they coincide only when `lightnessCurve` is linear). `reversed`
1053
+ * re-labels the `light` palette, so it mirrors exactly at any curve.
994
1054
  *
995
1055
  * @default 'light'
996
1056
  */
@@ -1022,6 +1082,16 @@ interface ScaleOptions {
1022
1082
  */
1023
1083
  variant?: ScaleVariant;
1024
1084
  }
1085
+ /**
1086
+ * Endpoint values for an asymmetric scale option.
1087
+ *
1088
+ * `low` applies at the low-key end (50), `high` at the high-key end (950),
1089
+ * regardless of `mode` — keys, not lightness, define the ends.
1090
+ */
1091
+ interface ScaleRange {
1092
+ high: number;
1093
+ low: number;
1094
+ }
1025
1095
  /**
1026
1096
  * Generate a scale of colors based on the input color.
1027
1097
  *
@@ -1078,4 +1148,4 @@ declare function toGamut(input: string, format?: ColorType): string;
1078
1148
  */
1079
1149
  declare function transparentize(input: string, alpha: number, format?: ColorType): string;
1080
1150
 
1081
- export { APCA_VERSION, type Analysis, CLMS_TO_OKLAB, type ColorKeysTuple, type ColorModel, type ColorModelKey, type ColorModelKeys, type ColorReturn, type ColorTuple, type ColorType, type ColorTypeInput, type ColorValue, type ColorizrOptions, type ConverterParameters, DEG2RAD, DELTA_E_JND, type FormatCSSOptions, GAMUT_EPSILON, type HEX, type HSL, type HueMode, type LAB, type LCH, LMS_TO_LRGB, LRGB_TO_LMS, type MixOptions, OKLAB_TO_CLMS, P3_TO_SRGB, P3_TO_XYZ, PRECISION, type PaletteOptions, type PlainObject, RAD2DEG, type RGB, type ReadableColorMethod, type ReadableColorOptions, SRGB_TO_P3, type ScaleOptions, type ScaleVariant, type Scheme, type SchemeOptions, XYZ_TO_SRGB, addAlphaToHex, apcaContrast, brightnessDifference, chroma, colorDifference, compare, contrast, convertAlphaToHex, convertCSS, darken, Colorizr as default, deltaE, desaturate, extractAlphaFromHex, extractColorParts, formatCSS, formatHex, getColorType, getP3MaxChroma, getP3MaxColor, getScaleStepKeys, grayscale, hex2hsl, hex2oklab, hex2oklch, hex2rgb, hsl2hex, hsl2oklab, hsl2oklch, hsl2rgb, invert, isHSL, isHex, isInGamut, isLAB, isLCH, isRGB, isValidColor, lighten, luminance, mix, name, oklab2hex, oklab2hsl, oklab2oklch, oklab2rgb, oklabToLinearP3, oklabToLinearSRGB, oklch2hex, oklch2hsl, oklch2oklab, oklch2rgb, opacify, opacity, palette, parseCSS, random, readableColor, removeAlphaFromHex, rgb2hex, rgb2hsl, rgb2oklab, rgb2oklch, rotate, saturate, scale, scheme, srgbGammaDecode, srgbGammaEncode, toGamut, transparentize };
1151
+ export { APCA_VERSION, type Analysis, CLMS_TO_OKLAB, type ColorKeysTuple, type ColorModel, type ColorModelKey, type ColorModelKeys, type ColorReturn, type ColorTuple, type ColorType, type ColorTypeInput, type ColorValue, type ColorizrOptions, type ConverterParameters, DEG2RAD, DELTA_E_JND, type FormatCSSOptions, GAMUT_EPSILON, type HEX, type HSL, type HueMode, type LAB, type LCH, LMS_TO_LRGB, LRGB_TO_LMS, type MixOptions, OKLAB_TO_CLMS, P3_TO_SRGB, P3_TO_XYZ, PRECISION, type PaletteOptions, RAD2DEG, type RGB, type RandomOptions, type ReadableColorMethod, type ReadableColorOptions, SRGB_TO_P3, type ScaleChromaPeak, type ScaleMode, type ScaleOptions, type ScaleRange, type ScaleVariant, type Scheme, type SchemeOptions, XYZ_TO_SRGB, addAlphaToHex, apcaContrast, brightnessDifference, chroma, colorDifference, compare, contrast, convertAlphaToHex, convertCSS, darken, Colorizr as default, deltaE, desaturate, extractAlphaFromHex, extractColorParts, formatCSS, formatHex, getColorType, getP3MaxChroma, getP3MaxColor, getScaleStepKeys, grayscale, hex2hsl, hex2oklab, hex2oklch, hex2rgb, hsl2hex, hsl2oklab, hsl2oklch, hsl2rgb, invert, isHSL, isHex, isInGamut, isLAB, isLCH, isRGB, isValidColor, lighten, luminance, mix, name, oklab2hex, oklab2hsl, oklab2oklch, oklab2rgb, oklabToLinearP3, oklabToLinearSRGB, oklch2hex, oklch2hsl, oklch2oklab, oklch2rgb, opacify, opacity, palette, parseCSS, random, readableColor, removeAlphaFromHex, rgb2hex, rgb2hsl, rgb2oklab, rgb2oklch, rotate, saturate, scale, scheme, srgbGammaDecode, srgbGammaEncode, toGamut, transparentize };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,6 @@ type ColorTypeInput = ColorType | 'named';
9
9
  type ColorValue = ColorModel | HEX;
10
10
  type ConverterParameters<TModel extends ColorModel> = TModel | ColorTuple;
11
11
  type HEX = `#${string}`;
12
- type PlainObject<T = any> = Record<string, T>;
13
12
  interface Analysis {
14
13
  brightnessDifference: number;
15
14
  colorDifference: number;
@@ -524,7 +523,7 @@ declare function desaturate(input: string, amount: number, format?: ColorType):
524
523
  type ExtractColorPartsReturn = {
525
524
  alpha?: number;
526
525
  model: ColorModelKey;
527
- } & PlainObject<number>;
526
+ } & Record<string, number>;
528
527
  /**
529
528
  * Extract the color parts from a CSS color string.
530
529
  * Hex colors are supported via conversion.
@@ -919,8 +918,20 @@ declare function rotate(input: string, degrees: number, format?: ColorType): str
919
918
  */
920
919
  declare function saturate(input: string, amount: number, format?: ColorType): string;
921
920
 
922
- type ScaleMode = 'light' | 'dark';
921
+ type ScaleMode = 'light' | 'dark' | 'reversed';
923
922
  type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
923
+ /**
924
+ * Parabolic chroma curve with a movable center.
925
+ */
926
+ interface ScaleChromaPeak {
927
+ /** Blend between constant chroma (0) and the full parabola (1). */
928
+ amount: number;
929
+ /**
930
+ * Lightness (exclusive 0-1) where chroma stays at full base.
931
+ * @default 0.5
932
+ */
933
+ peak?: number;
934
+ }
924
935
  /**
925
936
  * Options for generating a color scale.
926
937
  *
@@ -931,14 +942,29 @@ type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
931
942
  */
932
943
  interface ScaleOptions {
933
944
  /**
934
- * Controls chroma distribution across lightness levels (0-1).
945
+ * Controls how chroma flows across the scale. Three forms:
946
+ *
947
+ * Scalar (0-1) — parabolic blend centered at mid-lightness:
935
948
  * - 0: Constant chroma across all steps (default).
936
- * - 1: Parabolic curvefull chroma at mid-lightness, reduced toward extremes.
937
- * - Values between interpolate between constant and parabolic.
949
+ * - 1: Full parabolapeak chroma at mid-lightness, reduced toward extremes.
950
+ * Shorthand for `{ amount: x }`.
951
+ *
952
+ * `{ amount, peak }` — the same parabola with a movable center.
953
+ * `peak` is the lightness (exclusive 0-1) that keeps full base chroma;
954
+ * sides away from it are reduced. Defaults to 0.5.
955
+ *
956
+ * `{ low, high }` — fractions (0-1) of the maximum P3 chroma at each step,
957
+ * blended from `low` (50-end) through the input color's own fraction at the
958
+ * lock/middle step to `high` (950-end). NOT equivalent to the scalar form —
959
+ * it is a different model: values set chroma relative to the gamut ceiling
960
+ * instead of bending the input's chroma. Being gamut-relative, the input's
961
+ * chroma matters only at the anchor step; an achromatic input collapses the
962
+ * anchor fraction to 0, yielding a V-shaped, colored scale (full at the ends,
963
+ * gray at the anchor).
938
964
  *
939
965
  * @default 0
940
966
  */
941
- chromaCurve?: number;
967
+ chromaCurve?: number | ScaleChromaPeak | ScaleRange;
942
968
  /**
943
969
  * Output color format.
944
970
  *
@@ -947,14 +973,36 @@ interface ScaleOptions {
947
973
  * If not specified, the output will match the format of the input color.
948
974
  */
949
975
  format?: ColorType;
976
+ /**
977
+ * Hue rotation across the scale, in degrees.
978
+ *
979
+ * A scalar `x` is shorthand for `{ low: -x, high: x }`.
980
+ * `low` shifts the low-key end (50), `high` the high-key end (950); shifts
981
+ * blend to 0° at the lock step (or the middle step when not locked), so the
982
+ * input hue is preserved there. The per-step gamut clamp follows the
983
+ * shifted hue.
984
+ *
985
+ * Values must be within [-180, 180].
986
+ *
987
+ * @default 0
988
+ */
989
+ hueShift?: number | ScaleRange;
950
990
  /**
951
991
  * The lightness tuning factor for the scale.
952
992
  * - 1: Linear lightness distribution.
953
993
  * - >1: Lighter tones are emphasized.
954
994
  * - <1: Darker tones are emphasized.
995
+ *
996
+ * A scalar `x` is shorthand for `{ low: x, high: x }`. With `{ low, high }`
997
+ * each half of the scale gets its own exponent: `low` shapes the low-key
998
+ * half (toward 50), `high` the high-key half (toward 950), split at the
999
+ * lock step when locked or blended continuously when not.
1000
+ * Reference palettes typically fit `{ low: 1.5, high: 1 }`.
1001
+ *
1002
+ * Values must be greater than 0.
955
1003
  * @default 1.5
956
1004
  */
957
- lightnessCurve?: number;
1005
+ lightnessCurve?: number | ScaleRange;
958
1006
  /**
959
1007
  * Lock input color's lightness at specific step position.
960
1008
  *
@@ -965,6 +1013,11 @@ interface ScaleOptions {
965
1013
  * Must be a valid step key for the current step count.
966
1014
  * Default step keys (11 steps): 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950.
967
1015
  * Step keys vary based on the `steps` option (3-20 steps supported).
1016
+ *
1017
+ * When locked at the first or last step, only one side of the scale remains,
1018
+ * so the unused-side value (`low`/`high`) of asymmetric options
1019
+ * (`hueShift`, `lightnessCurve`, endpoint `chromaCurve`) has no effect and
1020
+ * emits a warning.
968
1021
  */
969
1022
  lock?: number;
970
1023
  /**
@@ -990,7 +1043,14 @@ interface ScaleOptions {
990
1043
  * Theme-aware lightness direction.
991
1044
  *
992
1045
  * - 'light': Low keys (50) are lightest, high keys (950) are darkest
993
- * - 'dark': Low keys (50) are darkest, high keys (950) are lightest
1046
+ * - 'dark': Low keys (50) are darkest, high keys (950) are lightest, re-derived
1047
+ * with the lightness ramp running in the dark direction
1048
+ * - 'reversed': the 'light' palette with its keys reversed — identical colors in
1049
+ * mirrored order (50↔950, 100↔900, …)
1050
+ *
1051
+ * `dark` re-derives the scale, so its intermediate steps are not a reversal of
1052
+ * `light` (they coincide only when `lightnessCurve` is linear). `reversed`
1053
+ * re-labels the `light` palette, so it mirrors exactly at any curve.
994
1054
  *
995
1055
  * @default 'light'
996
1056
  */
@@ -1022,6 +1082,16 @@ interface ScaleOptions {
1022
1082
  */
1023
1083
  variant?: ScaleVariant;
1024
1084
  }
1085
+ /**
1086
+ * Endpoint values for an asymmetric scale option.
1087
+ *
1088
+ * `low` applies at the low-key end (50), `high` at the high-key end (950),
1089
+ * regardless of `mode` — keys, not lightness, define the ends.
1090
+ */
1091
+ interface ScaleRange {
1092
+ high: number;
1093
+ low: number;
1094
+ }
1025
1095
  /**
1026
1096
  * Generate a scale of colors based on the input color.
1027
1097
  *
@@ -1078,4 +1148,4 @@ declare function toGamut(input: string, format?: ColorType): string;
1078
1148
  */
1079
1149
  declare function transparentize(input: string, alpha: number, format?: ColorType): string;
1080
1150
 
1081
- export { APCA_VERSION, type Analysis, CLMS_TO_OKLAB, type ColorKeysTuple, type ColorModel, type ColorModelKey, type ColorModelKeys, type ColorReturn, type ColorTuple, type ColorType, type ColorTypeInput, type ColorValue, type ColorizrOptions, type ConverterParameters, DEG2RAD, DELTA_E_JND, type FormatCSSOptions, GAMUT_EPSILON, type HEX, type HSL, type HueMode, type LAB, type LCH, LMS_TO_LRGB, LRGB_TO_LMS, type MixOptions, OKLAB_TO_CLMS, P3_TO_SRGB, P3_TO_XYZ, PRECISION, type PaletteOptions, type PlainObject, RAD2DEG, type RGB, type ReadableColorMethod, type ReadableColorOptions, SRGB_TO_P3, type ScaleOptions, type ScaleVariant, type Scheme, type SchemeOptions, XYZ_TO_SRGB, addAlphaToHex, apcaContrast, brightnessDifference, chroma, colorDifference, compare, contrast, convertAlphaToHex, convertCSS, darken, Colorizr as default, deltaE, desaturate, extractAlphaFromHex, extractColorParts, formatCSS, formatHex, getColorType, getP3MaxChroma, getP3MaxColor, getScaleStepKeys, grayscale, hex2hsl, hex2oklab, hex2oklch, hex2rgb, hsl2hex, hsl2oklab, hsl2oklch, hsl2rgb, invert, isHSL, isHex, isInGamut, isLAB, isLCH, isRGB, isValidColor, lighten, luminance, mix, name, oklab2hex, oklab2hsl, oklab2oklch, oklab2rgb, oklabToLinearP3, oklabToLinearSRGB, oklch2hex, oklch2hsl, oklch2oklab, oklch2rgb, opacify, opacity, palette, parseCSS, random, readableColor, removeAlphaFromHex, rgb2hex, rgb2hsl, rgb2oklab, rgb2oklch, rotate, saturate, scale, scheme, srgbGammaDecode, srgbGammaEncode, toGamut, transparentize };
1151
+ export { APCA_VERSION, type Analysis, CLMS_TO_OKLAB, type ColorKeysTuple, type ColorModel, type ColorModelKey, type ColorModelKeys, type ColorReturn, type ColorTuple, type ColorType, type ColorTypeInput, type ColorValue, type ColorizrOptions, type ConverterParameters, DEG2RAD, DELTA_E_JND, type FormatCSSOptions, GAMUT_EPSILON, type HEX, type HSL, type HueMode, type LAB, type LCH, LMS_TO_LRGB, LRGB_TO_LMS, type MixOptions, OKLAB_TO_CLMS, P3_TO_SRGB, P3_TO_XYZ, PRECISION, type PaletteOptions, RAD2DEG, type RGB, type RandomOptions, type ReadableColorMethod, type ReadableColorOptions, SRGB_TO_P3, type ScaleChromaPeak, type ScaleMode, type ScaleOptions, type ScaleRange, type ScaleVariant, type Scheme, type SchemeOptions, XYZ_TO_SRGB, addAlphaToHex, apcaContrast, brightnessDifference, chroma, colorDifference, compare, contrast, convertAlphaToHex, convertCSS, darken, Colorizr as default, deltaE, desaturate, extractAlphaFromHex, extractColorParts, formatCSS, formatHex, getColorType, getP3MaxChroma, getP3MaxColor, getScaleStepKeys, grayscale, hex2hsl, hex2oklab, hex2oklch, hex2rgb, hsl2hex, hsl2oklab, hsl2oklch, hsl2rgb, invert, isHSL, isHex, isInGamut, isLAB, isLCH, isRGB, isValidColor, lighten, luminance, mix, name, oklab2hex, oklab2hsl, oklab2oklch, oklab2rgb, oklabToLinearP3, oklabToLinearSRGB, oklch2hex, oklch2hsl, oklch2oklab, oklch2rgb, opacify, opacity, palette, parseCSS, random, readableColor, removeAlphaFromHex, rgb2hex, rgb2hsl, rgb2oklab, rgb2oklch, rotate, saturate, scale, scheme, srgbGammaDecode, srgbGammaEncode, toGamut, transparentize };
package/dist/index.js CHANGED
@@ -2177,11 +2177,53 @@ var chromaScale = {
2177
2177
  subtle: 0.2,
2178
2178
  vibrant: 1.25
2179
2179
  };
2180
+ function assertScaleObject(value, label) {
2181
+ invariant(isPlainObject(value), `${label} must be a number or an object.`);
2182
+ }
2183
+ function computeLightnessMapWithLock(lock, inputLightness, keys, lightnessCurve, maxLightness, minLightness, mode) {
2184
+ const lightnessMap = {};
2185
+ const lockIndex = keys.indexOf(lock);
2186
+ lightnessMap[lock] = inputLightness;
2187
+ if (lockIndex > 0) {
2188
+ const target = mode === "light" ? maxLightness : minLightness;
2189
+ for (let index = 0; index < lockIndex; index++) {
2190
+ const t = index / lockIndex;
2191
+ lightnessMap[keys[index]] = target + (inputLightness - target) * t ** lightnessCurve.low;
2192
+ }
2193
+ }
2194
+ const remaining = keys.length - 1 - lockIndex;
2195
+ if (remaining > 0) {
2196
+ const target = mode === "light" ? minLightness : maxLightness;
2197
+ for (let index = lockIndex + 1; index < keys.length; index++) {
2198
+ const t = (index - lockIndex) / remaining;
2199
+ lightnessMap[keys[index]] = inputLightness + (target - inputLightness) * t ** lightnessCurve.high;
2200
+ }
2201
+ }
2202
+ return lightnessMap;
2203
+ }
2204
+ function computeLightnessMapWithoutLock(keys, lightnessCurve, maxLightness, minLightness, mode) {
2205
+ const lightnessMap = {};
2206
+ for (let index = 0; index < keys.length; index++) {
2207
+ const tBase = index / (keys.length - 1);
2208
+ const exponent = lightnessCurve.low + (lightnessCurve.high - lightnessCurve.low) * tBase;
2209
+ const t = tBase ** exponent;
2210
+ lightnessMap[keys[index]] = mode === "light" ? maxLightness - (maxLightness - minLightness) * t : minLightness + (maxLightness - minLightness) * t;
2211
+ }
2212
+ return lightnessMap;
2213
+ }
2214
+ function computeStepHue(hue, hueShift, index, anchorIndex, keysLength) {
2215
+ if (hueShift.low === 0 && hueShift.high === 0 || index === anchorIndex) {
2216
+ return hue;
2217
+ }
2218
+ const shift = index < anchorIndex ? hueShift.low * (1 - index / anchorIndex) : hueShift.high * ((index - anchorIndex) / (keysLength - 1 - anchorIndex));
2219
+ return (hue + shift + 360) % 360;
2220
+ }
2180
2221
  function generatePalette(options) {
2181
2222
  const {
2182
2223
  baseChroma,
2183
2224
  chromaCurve,
2184
2225
  hue,
2226
+ hueShift,
2185
2227
  inputLightness,
2186
2228
  keys,
2187
2229
  lightnessCurve,
@@ -2191,52 +2233,113 @@ function generatePalette(options) {
2191
2233
  mode
2192
2234
  } = options;
2193
2235
  const palette2 = {};
2194
- const lightnessMap = {};
2195
- if (lock !== void 0 && inputLightness !== void 0) {
2196
- const lockIndex = keys.indexOf(lock);
2197
- lightnessMap[lock] = inputLightness;
2198
- if (lockIndex > 0) {
2199
- const target = mode === "light" ? maxLightness : minLightness;
2200
- for (let index = 0; index < lockIndex; index++) {
2201
- const t = index / lockIndex;
2202
- lightnessMap[keys[index]] = target + (inputLightness - target) * t ** lightnessCurve;
2203
- }
2204
- }
2205
- const remaining = keys.length - 1 - lockIndex;
2206
- if (remaining > 0) {
2207
- const target = mode === "light" ? minLightness : maxLightness;
2208
- for (let index = lockIndex + 1; index < keys.length; index++) {
2209
- const t = (index - lockIndex) / remaining;
2210
- lightnessMap[keys[index]] = inputLightness + (target - inputLightness) * t ** lightnessCurve;
2211
- }
2212
- }
2213
- } else {
2214
- for (let index = 0; index < keys.length; index++) {
2215
- const t = (index / (keys.length - 1)) ** lightnessCurve;
2216
- lightnessMap[keys[index]] = mode === "light" ? maxLightness - (maxLightness - minLightness) * t : minLightness + (maxLightness - minLightness) * t;
2217
- }
2218
- }
2219
- for (const key of keys) {
2236
+ const lightnessMap = lock !== void 0 && inputLightness !== void 0 ? computeLightnessMapWithLock(
2237
+ lock,
2238
+ inputLightness,
2239
+ keys,
2240
+ lightnessCurve,
2241
+ maxLightness,
2242
+ minLightness,
2243
+ mode
2244
+ ) : computeLightnessMapWithoutLock(keys, lightnessCurve, maxLightness, minLightness, mode);
2245
+ const anchorIndex = lock !== void 0 ? keys.indexOf(lock) : Math.floor((keys.length - 1) / 2);
2246
+ for (let index = 0; index < keys.length; index++) {
2247
+ const key = keys[index];
2220
2248
  const lightness = lightnessMap[key];
2221
- const chroma2 = getStepChroma(lightness, baseChroma, chromaCurve);
2222
- const maxChroma = getP3MaxChroma({ l: lightness, c: 0, h: hue });
2223
- palette2[key] = { l: lightness, c: Math.min(chroma2, maxChroma), h: hue };
2249
+ const stepHue = computeStepHue(hue, hueShift, index, anchorIndex, keys.length);
2250
+ const maxChroma = getP3MaxChroma({ l: lightness, c: 0, h: stepHue });
2251
+ const chroma2 = chromaCurve.type === "endpoints" ? getStepRelativeChroma(chromaCurve, index, anchorIndex, keys.length) * maxChroma : getStepChroma(lightness, baseChroma, chromaCurve.amount, chromaCurve.peak);
2252
+ palette2[key] = { l: lightness, c: Math.min(chroma2, maxChroma), h: stepHue };
2224
2253
  }
2225
2254
  return palette2;
2226
2255
  }
2227
- function getStepChroma(lightness, baseChroma, chromaCurve) {
2228
- if (chromaCurve === 0) {
2256
+ function getStepChroma(lightness, baseChroma, amount, peak) {
2257
+ if (amount === 0) {
2229
2258
  return baseChroma;
2230
2259
  }
2231
- const parabolic = 4 * lightness * (1 - lightness);
2232
- const curveScale = 1 - chromaCurve * (1 - parabolic);
2260
+ const parabolic = peak === 0.5 ? 4 * lightness * (1 - lightness) : Math.max(0, 1 - ((lightness - peak) / Math.max(peak, 1 - peak)) ** 2);
2261
+ const curveScale = 1 - amount * (1 - parabolic);
2233
2262
  return Math.max(0, baseChroma * curveScale);
2234
2263
  }
2264
+ function getStepRelativeChroma(curve, index, anchorIndex, total) {
2265
+ const { high, low, mid } = curve;
2266
+ if (index === anchorIndex) {
2267
+ return mid;
2268
+ }
2269
+ if (index < anchorIndex) {
2270
+ return low + (mid - low) * (index / anchorIndex);
2271
+ }
2272
+ return mid + (high - mid) * ((index - anchorIndex) / (total - 1 - anchorIndex));
2273
+ }
2274
+ function normalizeChromaCurve(chromaCurve, lch, baseChroma) {
2275
+ if (isNumber(chromaCurve)) {
2276
+ invariant(isNumberInRange(chromaCurve, 0, 1), "chromaCurve must be within the range [0, 1].");
2277
+ return { amount: chromaCurve, peak: 0.5, type: "parabola" };
2278
+ }
2279
+ assertScaleObject(chromaCurve, "chromaCurve");
2280
+ if ("amount" in chromaCurve) {
2281
+ const { amount, peak = 0.5 } = chromaCurve;
2282
+ invariant(isNumberInRange(amount, 0, 1), "chromaCurve amount must be within the range [0, 1].");
2283
+ invariant(
2284
+ isNumber(peak) && peak > 0 && peak < 1,
2285
+ "chromaCurve peak must be within the range (0, 1)."
2286
+ );
2287
+ return { amount, peak, type: "parabola" };
2288
+ }
2289
+ const { high, low } = chromaCurve;
2290
+ invariant(
2291
+ isNumberInRange(low, 0, 1) && isNumberInRange(high, 0, 1),
2292
+ "chromaCurve low/high must be within the range [0, 1]."
2293
+ );
2294
+ const maxChromaAtInput = getP3MaxChroma(lch);
2295
+ return {
2296
+ high,
2297
+ low,
2298
+ mid: maxChromaAtInput > 0 ? clamp(baseChroma / maxChromaAtInput, 0, 1) : 0,
2299
+ type: "endpoints"
2300
+ };
2301
+ }
2302
+ function resolveBaseChroma(saturation, variant, lch) {
2303
+ if (saturation !== void 0) {
2304
+ return clamp(saturation, 0, 100) / 100 * getP3MaxChroma(lch);
2305
+ }
2306
+ if (variant && chromaScale[variant]) {
2307
+ return lch.c * chromaScale[variant];
2308
+ }
2309
+ if (variant) {
2310
+ warn(`variant: ${variant} is not valid, ignoring`);
2311
+ }
2312
+ return lch.c;
2313
+ }
2314
+ function warnIgnoredLockEndpoints(lock, keys, options) {
2315
+ if (lock === void 0) {
2316
+ return;
2317
+ }
2318
+ const lockIndex = keys.indexOf(lock);
2319
+ if (lockIndex !== 0 && lockIndex !== keys.length - 1) {
2320
+ return;
2321
+ }
2322
+ const { chromaCurve, hueShift, lightnessCurve } = options;
2323
+ const ignored = lockIndex === 0 ? "low" : "high";
2324
+ const ranges = [
2325
+ ["hueShift", hueShift],
2326
+ ["lightnessCurve", lightnessCurve]
2327
+ ];
2328
+ if (chromaCurve.type === "endpoints") {
2329
+ ranges.push(["chromaCurve", { high: chromaCurve.high, low: chromaCurve.low }]);
2330
+ }
2331
+ for (const [name2, range] of ranges) {
2332
+ if (range.low !== range.high) {
2333
+ warn(`lock at ${lock} ignores ${name2}.${ignored} (${range[ignored]})`);
2334
+ }
2335
+ }
2336
+ }
2235
2337
  function scale(input, options = {}) {
2236
2338
  invariant(isString(input), MESSAGES.inputString);
2237
2339
  const {
2238
2340
  chromaCurve = 0,
2239
2341
  format,
2342
+ hueShift = 0,
2240
2343
  lightnessCurve = 1.5,
2241
2344
  lock: lockOption,
2242
2345
  maxLightness = 0.97,
@@ -2250,6 +2353,22 @@ function scale(input, options = {}) {
2250
2353
  maxLightness > minLightness && maxLightness <= 1 && minLightness >= 0,
2251
2354
  "maxLightness must be greater than minLightness and within the range [0, 1]."
2252
2355
  );
2356
+ if (!isNumber(hueShift)) {
2357
+ assertScaleObject(hueShift, "hueShift");
2358
+ }
2359
+ const hueShiftRange = isNumber(hueShift) ? { low: -hueShift, high: hueShift } : hueShift;
2360
+ invariant(
2361
+ isNumberInRange(hueShiftRange.low, -180, 180) && isNumberInRange(hueShiftRange.high, -180, 180),
2362
+ "hueShift values must be within the range [-180, 180]."
2363
+ );
2364
+ if (!isNumber(lightnessCurve)) {
2365
+ assertScaleObject(lightnessCurve, "lightnessCurve");
2366
+ }
2367
+ const lightnessCurveRange = isNumber(lightnessCurve) ? { low: lightnessCurve, high: lightnessCurve } : lightnessCurve;
2368
+ invariant(
2369
+ isNumber(lightnessCurveRange.low) && lightnessCurveRange.low > 0 && isNumber(lightnessCurveRange.high) && lightnessCurveRange.high > 0,
2370
+ "lightnessCurve values must be greater than 0."
2371
+ );
2253
2372
  const steps = stepsOption !== void 0 ? Math.round(stepsOption) : 11;
2254
2373
  const keys = getScaleStepKeys(steps);
2255
2374
  let lock = lockOption;
@@ -2259,30 +2378,30 @@ function scale(input, options = {}) {
2259
2378
  }
2260
2379
  const parsed = resolveColor(input);
2261
2380
  const lch = parsed.oklch;
2262
- let baseChroma;
2263
- if (saturation !== void 0) {
2264
- const maxChroma = getP3MaxChroma(lch);
2265
- baseChroma = clamp(saturation, 0, 100) / 100 * maxChroma;
2266
- } else if (variant && chromaScale[variant]) {
2267
- baseChroma = lch.c * chromaScale[variant];
2268
- } else {
2269
- baseChroma = lch.c;
2270
- }
2381
+ const baseChroma = resolveBaseChroma(saturation, variant, lch);
2271
2382
  const colorFormat = format ?? parsed.type;
2383
+ const chromaCurveConfig = normalizeChromaCurve(chromaCurve, lch, baseChroma);
2384
+ warnIgnoredLockEndpoints(lock, keys, {
2385
+ chromaCurve: chromaCurveConfig,
2386
+ hueShift: hueShiftRange,
2387
+ lightnessCurve: lightnessCurveRange
2388
+ });
2272
2389
  const palette2 = generatePalette({
2273
2390
  baseChroma,
2274
- chromaCurve,
2391
+ chromaCurve: chromaCurveConfig,
2275
2392
  hue: lch.h,
2393
+ hueShift: hueShiftRange,
2276
2394
  inputLightness: lock !== void 0 ? lch.l : void 0,
2277
2395
  keys,
2278
- lightnessCurve,
2396
+ lightnessCurve: lightnessCurveRange,
2279
2397
  lock,
2280
2398
  maxLightness,
2281
2399
  minLightness,
2282
- mode
2400
+ mode: mode === "reversed" ? "light" : mode
2283
2401
  });
2402
+ const ordered = mode === "reversed" ? Object.fromEntries(keys.map((key, index) => [key, palette2[keys[keys.length - 1 - index]]])) : palette2;
2284
2403
  return Object.fromEntries(
2285
- Object.entries(palette2).map(([key, value]) => [key, formatCSS(value, { format: colorFormat })])
2404
+ Object.entries(ordered).map(([key, value]) => [key, formatCSS(value, { format: colorFormat })])
2286
2405
  );
2287
2406
  }
2288
2407