colorizr 5.0.1 → 5.1.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/dist/index.d.mts CHANGED
@@ -921,6 +921,18 @@ declare function saturate(input: string, amount: number, format?: ColorType): st
921
921
 
922
922
  type ScaleMode = 'light' | 'dark';
923
923
  type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
924
+ /**
925
+ * Parabolic chroma curve with a movable center.
926
+ */
927
+ interface ScaleChromaPeak {
928
+ /** Blend between constant chroma (0) and the full parabola (1). */
929
+ amount: number;
930
+ /**
931
+ * Lightness (exclusive 0-1) where chroma stays at full base.
932
+ * @default 0.5
933
+ */
934
+ peak?: number;
935
+ }
924
936
  /**
925
937
  * Options for generating a color scale.
926
938
  *
@@ -931,14 +943,29 @@ type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
931
943
  */
932
944
  interface ScaleOptions {
933
945
  /**
934
- * Controls chroma distribution across lightness levels (0-1).
946
+ * Controls how chroma flows across the scale. Three forms:
947
+ *
948
+ * Scalar (0-1) — parabolic blend centered at mid-lightness:
935
949
  * - 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.
950
+ * - 1: Full parabolapeak chroma at mid-lightness, reduced toward extremes.
951
+ * Shorthand for `{ amount: x }`.
952
+ *
953
+ * `{ amount, peak }` — the same parabola with a movable center.
954
+ * `peak` is the lightness (exclusive 0-1) that keeps full base chroma;
955
+ * sides away from it are reduced. Defaults to 0.5.
956
+ *
957
+ * `{ low, high }` — fractions (0-1) of the maximum P3 chroma at each step,
958
+ * blended from `low` (50-end) through the input color's own fraction at the
959
+ * lock/middle step to `high` (950-end). NOT equivalent to the scalar form —
960
+ * it is a different model: values set chroma relative to the gamut ceiling
961
+ * instead of bending the input's chroma. Being gamut-relative, the input's
962
+ * chroma matters only at the anchor step; an achromatic input collapses the
963
+ * anchor fraction to 0, yielding a V-shaped, colored scale (full at the ends,
964
+ * gray at the anchor).
938
965
  *
939
966
  * @default 0
940
967
  */
941
- chromaCurve?: number;
968
+ chromaCurve?: number | ScaleChromaPeak | ScaleRange;
942
969
  /**
943
970
  * Output color format.
944
971
  *
@@ -947,14 +974,36 @@ interface ScaleOptions {
947
974
  * If not specified, the output will match the format of the input color.
948
975
  */
949
976
  format?: ColorType;
977
+ /**
978
+ * Hue rotation across the scale, in degrees.
979
+ *
980
+ * A scalar `x` is shorthand for `{ low: -x, high: x }`.
981
+ * `low` shifts the low-key end (50), `high` the high-key end (950); shifts
982
+ * blend to 0° at the lock step (or the middle step when not locked), so the
983
+ * input hue is preserved there. The per-step gamut clamp follows the
984
+ * shifted hue.
985
+ *
986
+ * Values must be within [-180, 180].
987
+ *
988
+ * @default 0
989
+ */
990
+ hueShift?: number | ScaleRange;
950
991
  /**
951
992
  * The lightness tuning factor for the scale.
952
993
  * - 1: Linear lightness distribution.
953
994
  * - >1: Lighter tones are emphasized.
954
995
  * - <1: Darker tones are emphasized.
996
+ *
997
+ * A scalar `x` is shorthand for `{ low: x, high: x }`. With `{ low, high }`
998
+ * each half of the scale gets its own exponent: `low` shapes the low-key
999
+ * half (toward 50), `high` the high-key half (toward 950), split at the
1000
+ * lock step when locked or blended continuously when not.
1001
+ * Reference palettes typically fit `{ low: 1.5, high: 1 }`.
1002
+ *
1003
+ * Values must be greater than 0.
955
1004
  * @default 1.5
956
1005
  */
957
- lightnessCurve?: number;
1006
+ lightnessCurve?: number | ScaleRange;
958
1007
  /**
959
1008
  * Lock input color's lightness at specific step position.
960
1009
  *
@@ -965,6 +1014,11 @@ interface ScaleOptions {
965
1014
  * Must be a valid step key for the current step count.
966
1015
  * Default step keys (11 steps): 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950.
967
1016
  * Step keys vary based on the `steps` option (3-20 steps supported).
1017
+ *
1018
+ * When locked at the first or last step, only one side of the scale remains,
1019
+ * so the unused-side value (`low`/`high`) of asymmetric options
1020
+ * (`hueShift`, `lightnessCurve`, endpoint `chromaCurve`) has no effect and
1021
+ * emits a warning.
968
1022
  */
969
1023
  lock?: number;
970
1024
  /**
@@ -1022,6 +1076,16 @@ interface ScaleOptions {
1022
1076
  */
1023
1077
  variant?: ScaleVariant;
1024
1078
  }
1079
+ /**
1080
+ * Endpoint values for an asymmetric scale option.
1081
+ *
1082
+ * `low` applies at the low-key end (50), `high` at the high-key end (950),
1083
+ * regardless of `mode` — keys, not lightness, define the ends.
1084
+ */
1085
+ interface ScaleRange {
1086
+ high: number;
1087
+ low: number;
1088
+ }
1025
1089
  /**
1026
1090
  * Generate a scale of colors based on the input color.
1027
1091
  *
@@ -1078,4 +1142,4 @@ declare function toGamut(input: string, format?: ColorType): string;
1078
1142
  */
1079
1143
  declare function transparentize(input: string, alpha: number, format?: ColorType): string;
1080
1144
 
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 };
1145
+ 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 ScaleChromaPeak, 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
@@ -921,6 +921,18 @@ declare function saturate(input: string, amount: number, format?: ColorType): st
921
921
 
922
922
  type ScaleMode = 'light' | 'dark';
923
923
  type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
924
+ /**
925
+ * Parabolic chroma curve with a movable center.
926
+ */
927
+ interface ScaleChromaPeak {
928
+ /** Blend between constant chroma (0) and the full parabola (1). */
929
+ amount: number;
930
+ /**
931
+ * Lightness (exclusive 0-1) where chroma stays at full base.
932
+ * @default 0.5
933
+ */
934
+ peak?: number;
935
+ }
924
936
  /**
925
937
  * Options for generating a color scale.
926
938
  *
@@ -931,14 +943,29 @@ type ScaleVariant = 'deep' | 'neutral' | 'pastel' | 'subtle' | 'vibrant';
931
943
  */
932
944
  interface ScaleOptions {
933
945
  /**
934
- * Controls chroma distribution across lightness levels (0-1).
946
+ * Controls how chroma flows across the scale. Three forms:
947
+ *
948
+ * Scalar (0-1) — parabolic blend centered at mid-lightness:
935
949
  * - 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.
950
+ * - 1: Full parabolapeak chroma at mid-lightness, reduced toward extremes.
951
+ * Shorthand for `{ amount: x }`.
952
+ *
953
+ * `{ amount, peak }` — the same parabola with a movable center.
954
+ * `peak` is the lightness (exclusive 0-1) that keeps full base chroma;
955
+ * sides away from it are reduced. Defaults to 0.5.
956
+ *
957
+ * `{ low, high }` — fractions (0-1) of the maximum P3 chroma at each step,
958
+ * blended from `low` (50-end) through the input color's own fraction at the
959
+ * lock/middle step to `high` (950-end). NOT equivalent to the scalar form —
960
+ * it is a different model: values set chroma relative to the gamut ceiling
961
+ * instead of bending the input's chroma. Being gamut-relative, the input's
962
+ * chroma matters only at the anchor step; an achromatic input collapses the
963
+ * anchor fraction to 0, yielding a V-shaped, colored scale (full at the ends,
964
+ * gray at the anchor).
938
965
  *
939
966
  * @default 0
940
967
  */
941
- chromaCurve?: number;
968
+ chromaCurve?: number | ScaleChromaPeak | ScaleRange;
942
969
  /**
943
970
  * Output color format.
944
971
  *
@@ -947,14 +974,36 @@ interface ScaleOptions {
947
974
  * If not specified, the output will match the format of the input color.
948
975
  */
949
976
  format?: ColorType;
977
+ /**
978
+ * Hue rotation across the scale, in degrees.
979
+ *
980
+ * A scalar `x` is shorthand for `{ low: -x, high: x }`.
981
+ * `low` shifts the low-key end (50), `high` the high-key end (950); shifts
982
+ * blend to 0° at the lock step (or the middle step when not locked), so the
983
+ * input hue is preserved there. The per-step gamut clamp follows the
984
+ * shifted hue.
985
+ *
986
+ * Values must be within [-180, 180].
987
+ *
988
+ * @default 0
989
+ */
990
+ hueShift?: number | ScaleRange;
950
991
  /**
951
992
  * The lightness tuning factor for the scale.
952
993
  * - 1: Linear lightness distribution.
953
994
  * - >1: Lighter tones are emphasized.
954
995
  * - <1: Darker tones are emphasized.
996
+ *
997
+ * A scalar `x` is shorthand for `{ low: x, high: x }`. With `{ low, high }`
998
+ * each half of the scale gets its own exponent: `low` shapes the low-key
999
+ * half (toward 50), `high` the high-key half (toward 950), split at the
1000
+ * lock step when locked or blended continuously when not.
1001
+ * Reference palettes typically fit `{ low: 1.5, high: 1 }`.
1002
+ *
1003
+ * Values must be greater than 0.
955
1004
  * @default 1.5
956
1005
  */
957
- lightnessCurve?: number;
1006
+ lightnessCurve?: number | ScaleRange;
958
1007
  /**
959
1008
  * Lock input color's lightness at specific step position.
960
1009
  *
@@ -965,6 +1014,11 @@ interface ScaleOptions {
965
1014
  * Must be a valid step key for the current step count.
966
1015
  * Default step keys (11 steps): 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950.
967
1016
  * Step keys vary based on the `steps` option (3-20 steps supported).
1017
+ *
1018
+ * When locked at the first or last step, only one side of the scale remains,
1019
+ * so the unused-side value (`low`/`high`) of asymmetric options
1020
+ * (`hueShift`, `lightnessCurve`, endpoint `chromaCurve`) has no effect and
1021
+ * emits a warning.
968
1022
  */
969
1023
  lock?: number;
970
1024
  /**
@@ -1022,6 +1076,16 @@ interface ScaleOptions {
1022
1076
  */
1023
1077
  variant?: ScaleVariant;
1024
1078
  }
1079
+ /**
1080
+ * Endpoint values for an asymmetric scale option.
1081
+ *
1082
+ * `low` applies at the low-key end (50), `high` at the high-key end (950),
1083
+ * regardless of `mode` — keys, not lightness, define the ends.
1084
+ */
1085
+ interface ScaleRange {
1086
+ high: number;
1087
+ low: number;
1088
+ }
1025
1089
  /**
1026
1090
  * Generate a scale of colors based on the input color.
1027
1091
  *
@@ -1078,4 +1142,4 @@ declare function toGamut(input: string, format?: ColorType): string;
1078
1142
  */
1079
1143
  declare function transparentize(input: string, alpha: number, format?: ColorType): string;
1080
1144
 
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 };
1145
+ 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 ScaleChromaPeak, 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,23 +2378,22 @@ 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,