diff --git a/lib/src/value/color.ts b/lib/src/value/color.ts index 2b0d53ca..f0e0b65f 100644 --- a/lib/src/value/color.ts +++ b/lib/src/value/color.ts @@ -114,30 +114,19 @@ function getColorSpace(options: ChannelOptions): KnownColorSpace { throw valueError('No color space found'); } -/** - * Convert from the ColorJS representation of a missing component (`NaN`) to - * `null`. - */ -function NaNtoNull(val: number): number | null { - return Number.isNaN(val) ? null : val; -} - -/** - * Convert from the ColorJS representation of a missing component (`NaN`) to - * `0`. - */ -function NaNtoZero(val: number): number { - return Number.isNaN(val) ? 0 : val; +/** Convert from sRGB (0-1) to RGB (0-255) units. */ +function coordToRgb(val: number | null): number | null { + return val === null ? val : val * 255; } -/** Convert from sRGB (0-1) to RGB (0-255) units. */ -function coordToRgb(val: number): number { - return val * 255; +/** Convert from RGB (0-255) to sRGB (0-1) units. */ +function rgbToCoord(val: number | null): number | null { + return val === null ? val : val / 255; } /** Normalize `hue` values to be within the range `[0, 360)`. */ -function normalizeHue(val: number): number { - return positiveMod(val, 360); +function normalizeHue(val: number | null): number | null { + return val === null ? val : positiveMod(val, 360); } /** @@ -265,24 +254,24 @@ function isPolarColorSpace(space: KnownColorSpace): space is PolarColorSpace { } /** - * Convert from ColorJS coordinates (which use `NaN` for missing components, and - * a range of `0-1` for `rgb` channel values) to Sass Color coordinates (which - * use `null` for missing components, and a range of `0-255` for `rgb` channel - * values). + * Convert from ColorJS coordinates (which use a range of `0-1` for `rgb` + * channel values) to Sass Color coordinates (which use a range of `0-255` for + * `rgb` channel values). */ function decodeCoordsFromColorJs( - coords: [number, number, number], // ColorJS coordinates + coords: [number | null, number | null, number | null], // ColorJS coordinates isRgb = false, // Whether this color is in the `rgb` color space ): [number | null, number | null, number | null] { let newCoords = coords; // If this color is in the `rgb` space, convert channel values to `0-255` - if (isRgb) newCoords = newCoords.map(coordToRgb) as [number, number, number]; - // Convert `NaN` values to `null` - return newCoords.map(NaNtoNull) as [ - number | null, - number | null, - number | null, - ]; + if (isRgb) { + newCoords = newCoords.map(coordToRgb) as [ + number | null, + number | null, + number | null, + ]; + } + return newCoords; } /** Returns `true` if `val` is a `number` or `null`. */ @@ -451,10 +440,10 @@ export class SassColor extends Value { const space = options.space ?? getColorSpace(options); this.setChannelIds(space); if (space === 'rgb') this.isRgb = true; - let alpha: number; + let alpha: number | null; if (options.alpha === null) { if (!options.space) emitNullAlphaDeprecation(); - alpha = NaN; + alpha = null; } else if (options.alpha === undefined) { alpha = 1; } else { @@ -464,14 +453,18 @@ export class SassColor extends Value { switch (space) { case 'rgb': case 'srgb': { - const red = options.red ?? NaN; - const green = options.green ?? NaN; - const blue = options.blue ?? NaN; + const red = options.red ?? null; + const green = options.green ?? null; + const blue = options.blue ?? null; if (this.isRgb) { this.color = new Color({ spaceId: encodeSpaceForColorJs(space), // convert from 0-255 to 0-1 - coords: [red / 255, green / 255, blue / 255], + coords: [red, green, blue].map(rgbToCoord) as [ + number | null, + number | null, + number | null, + ], alpha, }); } else { @@ -493,21 +486,23 @@ export class SassColor extends Value { this.color = new Color({ spaceId: encodeSpaceForColorJs(space), coords: [ - options.red ?? NaN, - options.green ?? NaN, - options.blue ?? NaN, + options.red ?? null, + options.green ?? null, + options.blue ?? null, ], alpha, }); break; case 'hsl': { - let hue = normalizeHue(options.hue ?? NaN); - let saturation = options.saturation ?? NaN; - const lightness = options.lightness ?? NaN; - if (!Number.isNaN(saturation) && fuzzyLessThan(saturation, 0)) { + let hue = normalizeHue(options.hue ?? null); + let saturation = options.saturation ?? null; + const lightness = options.lightness ?? null; + if (saturation !== null && fuzzyLessThan(saturation, 0)) { saturation = Math.abs(saturation); - hue = (hue + 180) % 360; + if (hue !== null) { + hue = (hue + 180) % 360; + } } this.color = new Color({ @@ -519,9 +514,9 @@ export class SassColor extends Value { } case 'hwb': { - const hue = normalizeHue(options.hue ?? NaN); - const whiteness = options.whiteness ?? NaN; - const blackness = options.blackness ?? NaN; + const hue = normalizeHue(options.hue ?? null); + const whiteness = options.whiteness ?? null; + const blackness = options.blackness ?? null; this.color = new Color({ spaceId: encodeSpaceForColorJs(space), coords: [hue, whiteness, blackness], @@ -532,9 +527,9 @@ export class SassColor extends Value { case 'lab': case 'oklab': { - const lightness = options.lightness ?? NaN; - const a = options.a ?? NaN; - const b = options.b ?? NaN; + const lightness = options.lightness ?? null; + const a = options.a ?? null; + const b = options.b ?? null; this.color = new Color({ spaceId: encodeSpaceForColorJs(space), coords: [lightness, a, b], @@ -545,12 +540,14 @@ export class SassColor extends Value { case 'lch': case 'oklch': { - const lightness = options.lightness ?? NaN; - let chroma = options.chroma ?? NaN; - let hue = normalizeHue(options.hue ?? NaN); - if (!Number.isNaN(chroma) && fuzzyLessThan(chroma, 0)) { + const lightness = options.lightness ?? null; + let chroma = options.chroma ?? null; + let hue = normalizeHue(options.hue ?? null); + if (chroma !== null && fuzzyLessThan(chroma, 0)) { chroma = Math.abs(chroma); - hue = (hue + 180) % 360; + if (hue !== null) { + hue = (hue + 180) % 360; + } } this.color = new Color({ @@ -566,23 +563,16 @@ export class SassColor extends Value { case 'xyz-d50': this.color = new Color({ spaceId: encodeSpaceForColorJs(space), - coords: [options.x ?? NaN, options.y ?? NaN, options.z ?? NaN], + coords: [options.x ?? null, options.y ?? null, options.z ?? null], alpha, }); break; } - - // @TODO Waiting on new release of ColorJS that includes allowing `alpha` - // to be `NaN` on initial construction. - // Fixed in: https://github.com/LeaVerou/color.js/commit/08b39c180565ae61408ad737d91bd71a1f79d3df - if (Number.isNaN(alpha)) { - this.color.alpha = NaN; - } } /** This color's alpha channel, between `0` and `1`. */ get alpha(): number { - return NaNtoZero(this.color.alpha); + return this.color.alpha ?? 0; } /** The name of this color's color space. */ @@ -607,9 +597,13 @@ export class SassColor extends Value { get channelsOrNull(): List { let coords = this.color.coords; if (this.space === 'rgb') { - coords = coords.map(coordToRgb) as [number, number, number]; + coords = coords.map(coordToRgb) as [ + number | null, + number | null, + number | null, + ]; } - return List(coords.map(NaNtoNull)); + return List(coords); } /** @@ -621,9 +615,13 @@ export class SassColor extends Value { get channels(): List { let coords = this.color.coords; if (this.space === 'rgb') { - coords = coords.map(coordToRgb) as [number, number, number]; + coords = coords.map(coordToRgb) as [ + number | null, + number | null, + number | null, + ]; } - return List(coords.map(NaNtoZero)); + return List(coords.map(val => val ?? 0)); } /** @@ -633,8 +631,7 @@ export class SassColor extends Value { */ get red(): number { emitColor4ApiGetterDeprecation('red'); - const val = NaNtoZero(coordToRgb(this.color.srgb.red)); - return fuzzyRound(val); + return fuzzyRound(coordToRgb(this.color.srgb.red)) ?? 0; } /** @@ -644,8 +641,7 @@ export class SassColor extends Value { */ get green(): number { emitColor4ApiGetterDeprecation('green'); - const val = NaNtoZero(coordToRgb(this.color.srgb.green)); - return fuzzyRound(val); + return fuzzyRound(coordToRgb(this.color.srgb.green)) ?? 0; } /** @@ -655,8 +651,7 @@ export class SassColor extends Value { */ get blue(): number { emitColor4ApiGetterDeprecation('blue'); - const val = NaNtoZero(coordToRgb(this.color.srgb.blue)); - return fuzzyRound(val); + return fuzzyRound(coordToRgb(this.color.srgb.blue)) ?? 0; } /** @@ -666,7 +661,7 @@ export class SassColor extends Value { */ get hue(): number { emitColor4ApiGetterDeprecation('hue'); - return NaNtoZero(this.color.hsl.hue); + return this.color.hsl.hue ?? 0; } /** @@ -676,7 +671,7 @@ export class SassColor extends Value { */ get saturation(): number { emitColor4ApiGetterDeprecation('saturation'); - return NaNtoZero(this.color.hsl.saturation); + return this.color.hsl.saturation ?? 0; } /** @@ -686,7 +681,7 @@ export class SassColor extends Value { */ get lightness(): number { emitColor4ApiGetterDeprecation('lightness'); - return NaNtoZero(this.color.hsl.lightness); + return this.color.hsl.lightness ?? 0; } /** @@ -696,7 +691,7 @@ export class SassColor extends Value { */ get whiteness(): number { emitColor4ApiGetterDeprecation('whiteness'); - return NaNtoZero(this.color.hwb.whiteness); + return this.color.hwb.whiteness ?? 0; } /** @@ -706,7 +701,7 @@ export class SassColor extends Value { */ get blackness(): number { emitColor4ApiGetterDeprecation('blackness'); - return NaNtoZero(this.color.hwb.blackness); + return this.color.hwb.blackness ?? 0; } assertColor(): SassColor { @@ -768,7 +763,7 @@ export class SassColor extends Value { channel(channel: ChannelNameXyz, options: {space: ColorSpaceXyz}): number; channel(channel: ChannelName, options?: {space: KnownColorSpace}): number { if (channel === 'alpha') return this.alpha; - let val: number; + let val: number | null; const space = options?.space ?? this.space; validateChannelInSpace(channel, space); if (options?.space) { @@ -783,7 +778,7 @@ export class SassColor extends Value { }); } if (space === 'rgb') val = coordToRgb(val); - return NaNtoZero(val); + return val ?? 0; } /** @@ -793,13 +788,13 @@ export class SassColor extends Value { * [missing channel]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#missing_color_components */ isChannelMissing(channel: ChannelName): boolean { - if (channel === 'alpha') return Number.isNaN(this.color.alpha); + if (channel === 'alpha') return this.color.alpha === null; validateChannelInSpace(channel, this.space); - return Number.isNaN( + return ( this.color.get({ space: this.color.spaceId, coordId: encodeChannelForColorJs(channel), - }), + }) === null ); } @@ -906,7 +901,7 @@ export class SassColor extends Value { [this.channel0Id]: coords[0], [this.channel1Id]: coords[1], [this.channel2Id]: coords[2], - alpha: NaNtoNull(this.color.alpha), + alpha: this.color.alpha, }); } @@ -1140,11 +1135,11 @@ export class SassColor extends Value { coords = this.color .to('srgb') .coords.map(coordToRgb) - .map(fuzzyRound) as [number, number, number]; + .map(fuzzyRound) as [number | null, number | null, number | null]; otherCoords = other.color .to('srgb') .coords.map(coordToRgb) - .map(fuzzyRound) as [number, number, number]; + .map(fuzzyRound) as [number | null, number | null, number | null]; } return ( fuzzyEquals(coords[0], otherCoords[0]) && diff --git a/lib/src/value/utils.ts b/lib/src/value/utils.ts index 7844c810..8f054675 100644 --- a/lib/src/value/utils.ts +++ b/lib/src/value/utils.ts @@ -16,7 +16,10 @@ export const precision = 10; const epsilon = 10 ** (-precision - 1); /** Whether `num1` and `num2` are equal within `epsilon`. */ -export function fuzzyEquals(num1: number, num2: number): boolean { +export function fuzzyEquals(num1: number | null, num2: number | null): boolean { + if (num1 === null || num2 === null) { + return false; + } return Math.abs(num1 - num2) < epsilon; } @@ -25,10 +28,8 @@ export function fuzzyEquals(num1: number, num2: number): boolean { * * Two numbers that `fuzzyEquals` each other must have the same hash code. */ -export function fuzzyHashCode(num: number): number { - return !isFinite(num) || isNaN(num) - ? hash(num) - : hash(Math.round(num / epsilon)); +export function fuzzyHashCode(num: number | null): number { + return num === null ? hash(num) : hash(Math.round(num / epsilon)); } /** Whether `num1` < `num2`, within `epsilon`. */ @@ -72,7 +73,10 @@ export function fuzzyAsInt(num: number): number | null { * * If `num` `fuzzyEquals` `x.5`, rounds away from zero. */ -export function fuzzyRound(num: number): number { +export function fuzzyRound(num: number | null): number | null { + if (num === null) { + return num; + } if (num > 0) { return fuzzyLessThan(num % 1, 0.5) ? Math.floor(num) : Math.ceil(num); } else { diff --git a/package.json b/package.json index 24a8bfbe..17e437ab 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dist/**/*" ], "engines": { - "node": ">=16.0.0" + "node": ">=20.19.0" }, "bin": { "sass": "dist/bin/sass.js" @@ -62,7 +62,7 @@ }, "dependencies": { "@bufbuild/protobuf": "^2.5.0", - "colorjs.io": "^0.5.0", + "colorjs.io": "^0.7.0", "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1",