Skip to content

Commit 70b510b

Browse files
committed
Fix clamping channel values of hsl() and hwb()
Saturation > 100 and lightness/whiteness/blackness < 0 or > 100 are no longer clamped. This prevented producing NaN during color space conversion with infinite values, but w3c/csswg-drafts#10507 resolved on clamping infinite values to a UA defined limit. Also fixed rounding values declared in rgba() (oversight).
1 parent 0bc1b8a commit 70b510b

File tree

3 files changed

+50
-69
lines changed

3 files changed

+50
-69
lines changed

__tests__/value.js

Lines changed: 36 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2826,27 +2826,21 @@ describe('<color>', () => {
28262826
// Out of range arguments
28272827
['rgb(-1 0 0 / -1)', 'rgba(0, 0, 0, 0)'],
28282828
['rgb(256 0 0 / 2)', 'rgb(255, 0, 0)'],
2829-
['rgb(calc(-infinity) calc(-infinity) calc(infinity))', 'rgb(0, 0, 255)'],
2830-
['rgb(calc(infinity) calc(infinity) calc(-infinity))', 'rgb(255, 255, 0)'],
2831-
// Map <percentage> to <number>
2832-
['rgb(-1% 0% 0% / -1%)', 'rgba(0, 0, 0, 0)'],
2833-
['rgb(101% 100% 100% / 101%)', 'rgb(255, 255, 255)'],
2834-
// Map `none` to `0`
2835-
['rgb(none 0 0 / none)', 'rgba(0, 0, 0, 0)'],
2836-
['rgb(none 0% 0%)', 'rgb(0, 0, 0)'],
2829+
// Map <percentage> and `none` to <number>
2830+
['rgba(50% -1% 101% / 101%)', 'rgb(128, 0, 255)'],
2831+
['rgb(none none none / none)', 'rgba(0, 0, 0, 0)'],
28372832
// Precision (at least 8 bit integers)
28382833
['rgb(127.499 0 0 / 0.498)', 'rgba(127, 0, 0, 0.498)'],
28392834
['rgb(127.501 0 0 / 0.499)', 'rgba(128, 0, 0, 0.498)'],
28402835
['rgb(0 0 0 / 0.501)', 'rgba(0, 0, 0, 0.5)'],
28412836
['rgb(49.9% 50.1% 0% / 49.9%)', 'rgba(127, 128, 0, 0.498)'],
28422837
['rgb(0.501 0.499 0 / 50.1%)', 'rgba(1, 0, 0, 0.5)'],
28432838
// Numeric substitution function
2844-
['rgb(calc(-1) 0 0 / calc(-1))', 'rgba(0, 0, 0, 0)'],
2845-
['rgb(calc(256) 0 0 / calc(2))', 'rgb(255, 0, 0)'],
2846-
['rgb(calc(-1%) 0% 0% / calc(-1%))', 'rgba(0, 0, 0, 0)'],
2847-
['rgb(calc(101%) 0% 0% / calc(101%))', 'rgb(255, 0, 0)'],
2848-
['rgba(-1 calc(1em / 1px) 101% / 1)', 'rgb(0 calc(1em / 1px) 255)'],
2849-
['rgb(calc(1) sibling-count() progress(1, 0, 2))', 'rgb(1 sibling-count() 0.5)'],
2839+
['rgb(calc(50%) calc(-1%) calc(101%) / calc(-1%))', 'rgba(128, 0, 255, 0)'],
2840+
['rgb(calc(-infinity) calc(-infinity) calc(infinity))', 'rgb(0, 0, 255)'],
2841+
['rgb(calc(infinity) calc(infinity) calc(-infinity))', 'rgb(255, 255, 0)'],
2842+
['rgba(calc(1em / 1px) -1% 101% / -1%)', 'rgb(calc(1em / 1px) 0 255 / 0)'],
2843+
['rgb(sibling-count() calc(-1) calc(256) / calc(2))', 'rgb(sibling-count() 0 255)'],
28502844
// Relative color
28512845
['rgb(from green alpha calc(r) calc(g * 1%) / calc(b + 1 + 1))', 'rgb(from green alpha calc(r) calc(1% * g) / calc(2 + b))'],
28522846
['rgba(from rgba(-1 256 0 / -1) -100% 200% 0% / 101%)', 'rgb(from rgb(-1 256 0 / 0) -255 510 0)'],
@@ -2871,32 +2865,26 @@ describe('<color>', () => {
28712865
['hsla(0, 0%, 0%, 1)', 'rgb(0, 0, 0)'],
28722866
// Out of range arguments
28732867
['hsl(-540 -1 50 / -1)', 'rgba(128, 128, 128, 0)'],
2874-
['hsl(540 101 50 / 2)', 'rgb(0, 255, 255)'],
2875-
['hsl(0 0 -1)', 'rgb(0, 0, 0)'],
2876-
['hsl(0 0 101)', 'rgb(255, 255, 255)'],
2877-
['hsl(calc(-infinity) calc(-infinity) calc(infinity))', 'rgb(255, 255, 255)'],
2878-
['hsl(calc(infinity) calc(infinity) calc(-infinity))', 'rgb(0, 0, 0)'],
2879-
// Map <angle> and <percentage> to <number>
2880-
['hsl(-1.5turn -1% 50% / -1%)', 'rgba(128, 128, 128, 0)'],
2881-
['hsl(1.5turn 101% 50% / 101%)', 'rgb(0, 255, 255)'],
2882-
['hsl(0deg 0% -1%)', 'rgb(0, 0, 0)'],
2883-
['hsl(0deg 0% 101%)', 'rgb(255, 255, 255)'],
2884-
// Map `none` to `0`
2885-
['hsl(none 100 50 / none)', 'rgba(255, 0, 0, 0)'],
2886-
['hsl(0 none none)', 'rgb(0, 0, 0)'],
2868+
['hsl(540 101 49 / 2)', 'rgb(0, 251, 251)'],
2869+
['hsl(0 150 -1)', 'rgb(0, 1, 1)'],
2870+
['hsl(0 -150 101)', 'rgb(255, 255, 255)'],
2871+
// Map <angle>, <percentage>, `none`, to <number>
2872+
['hsla(-1.5turn -1% 50% / 101%)', 'rgb(128, 128, 128)'],
2873+
['hsl(none none 50 / none)', 'rgba(128, 128, 128, 0)'],
28872874
// Precision (at least 8 bit integers)
28882875
['hsl(0.498 100% 49.8% / 0.498)', 'rgba(254, 2, 0, 0.498)'],
28892876
['hsl(0.499 100% 49.9% / 0.499)', 'rgba(254, 2, 0, 0.498)'],
28902877
['hsl(0.501 100% 50.1% / 0.501)', 'rgba(255, 3, 1, 0.5)'],
28912878
['hsl(0 100% 50% / 49.9%)', 'rgba(255, 0, 0, 0.498)'],
28922879
['hsl(0 100% 50% / 50.1%)', 'rgba(255, 0, 0, 0.5)'],
28932880
// Numeric substitution function
2894-
['hsl(calc(-540) calc(101%) calc(50%) / calc(-1))', 'rgba(0, 255, 255, 0)'],
2895-
['hsl(calc(540) 100% 50% / calc(2))', 'rgb(0, 255, 255)'],
2896-
['hsl(calc(-540deg) 100% 50% / calc(-1%))', 'rgba(0, 255, 255, 0)'],
2897-
['hsl(calc(540deg) 100% 50% / 101%)', 'rgb(0, 255, 255)'],
2898-
['hsla(-540 calc(1em / 1px) 101% / 1)', 'hsl(180 calc(1em / 1px) 100)'],
2899-
['hsl(calc(1) sibling-count() progress(1, 0, 2))', 'hsl(1 sibling-count() 0.5)'],
2881+
['hsl(calc(-1.5turn) calc(-1%) calc(50%) / calc(-1%))', 'rgba(128, 128, 128, 0)'],
2882+
['hsl(calc(-infinity) calc(-infinity) 1)', 'rgb(3, 3, 3)'],
2883+
['hsl(calc(infinity) calc(infinity) 0)', 'rgb(0, 0, 0)'],
2884+
['hsl(0 0 calc(-infinity))', 'rgb(0, 0, 0)'],
2885+
['hsl(0 0 calc(infinity))', 'rgb(255, 255, 255)'],
2886+
['hsla(calc(1em / 1px) -1% 101% / -1%)', 'hsl(calc(1em / 1px) 0 101 / 0)'],
2887+
['hsl(sibling-count() calc(-1) calc(101) / calc(2))', 'hsl(sibling-count() 0 101)'],
29002888
// Relative color
29012889
['hsl(from green alpha calc(h) calc(s * 1%) / calc(l + 1 + 1))', 'hsl(from green alpha calc(h) calc(1% * s) / calc(2 + l))'],
29022890
['hsla(from hsla(540 -1 0 / -1) 540deg 101% 0% / 101%)', 'hsl(from hsl(180 -1 0 / 0) 180 101 0)'],
@@ -2912,23 +2900,13 @@ describe('<color>', () => {
29122900
// Out of range arguments
29132901
['hwb(-540 0 0 / -1)', 'rgba(0, 255, 255, 0)'],
29142902
['hwb(540 0 0 / 2)', 'rgb(0, 255, 255)'],
2915-
['hwb(0 -1 100)', 'rgb(0, 0, 0)'],
2916-
['hwb(0 100 -1)', 'rgb(255, 255, 255)'],
2917-
['hwb(0 1000 1)', 'rgb(252, 252, 252)'],
2918-
['hwb(0 1 1000)', 'rgb(3, 3, 3)'],
2919-
['hwb(calc(-infinity) calc(-infinity) calc(infinity))', 'rgb(0, 0, 0)'],
2920-
['hwb(calc(infinity) calc(infinity) calc(-infinity))', 'rgb(255, 255, 255)'],
2921-
['hwb(0 calc(infinity) calc(infinity))', 'rgb(128, 128, 128)'],
2922-
// Map <angle> and <percentage> to <number>
2923-
['hwb(-1.5turn 0% 0% / -1%)', 'rgba(0, 255, 255, 0)'],
2924-
['hwb(-1.5turn 0% 0% / 101%)', 'rgb(0, 255, 255)'],
2925-
['hwb(0 -1% 100%)', 'rgb(0, 0, 0)'],
2926-
['hwb(0 100% -1%)', 'rgb(255, 255, 255)'],
2927-
['hwb(0 1000% -1%)', 'rgb(255, 255, 255)'],
2928-
['hwb(0 1% 1000%)', 'rgb(3, 3, 3)'],
2929-
// Map `none` to `0`
2903+
['hwb(90 -1 0)', 'rgb(126, 255, 0)'],
2904+
['hwb(0 101 50)', 'rgb(171, 171, 171)'],
2905+
['hwb(90 0 -1)', 'rgb(129, 255, 0)'],
2906+
['hwb(0 50 101)', 'rgb(84, 84, 84)'],
2907+
// Map <angle>, <percentage>, `none`, to <number>
2908+
['hwb(-1.5turn -1% 50% / 101%)', 'rgb(0, 128, 128)'],
29302909
['hwb(none none none / none)', 'rgba(255, 0, 0, 0)'],
2931-
['hwb(0 none none)', 'rgb(255, 0, 0)'],
29322910
// Precision (at least 8 bit integers)
29332911
['hwb(0.498 0% 49.8% / 0.498)', 'rgba(128, 1, 0, 0.498)'],
29342912
['hwb(0.499 0% 49.9% / 0.499)', 'rgba(128, 1, 0, 0.498)'],
@@ -2937,12 +2915,15 @@ describe('<color>', () => {
29372915
['hwb(0 0% 0% / 49.9%)', 'rgba(255, 0, 0, 0.498)'],
29382916
['hwb(0 0% 0% / 50.1%)', 'rgba(255, 0, 0, 0.5)'],
29392917
// Numeric substitution functions
2940-
['hwb(calc(-540) calc(0%) calc(0%) / calc(-1))', 'rgba(0, 255, 255, 0)'],
2941-
['hwb(calc(540) 0% 0% / calc(2))', 'rgb(0, 255, 255)'],
2942-
['hwb(calc(-540deg) 0% 0% / calc(-1%))', 'rgba(0, 255, 255, 0)'],
2943-
['hwb(calc(540deg) 0% 0% / calc(101%))', 'rgb(0, 255, 255)'],
2944-
['hwb(-540 calc(1em / 1px) 101% / 1)', 'hwb(180 calc(1em / 1px) 100)'],
2945-
['hwb(calc(1) sibling-count() progress(1, 0, 2))', 'hwb(1 sibling-count() 0.5)'],
2918+
['hwb(calc(-1.5turn) calc(-1%) calc(50%) / calc(-1%))', 'rgba(0, 128, 128, 0)'],
2919+
['hwb(calc(-infinity) 0 0)', 'rgb(255, 0, 0)'],
2920+
['hwb(calc(infinity) 0 0)', 'rgb(255, 0, 0)'],
2921+
['hwb(90 calc(-infinity) 0)', 'rgb(0, 255, 0)'],
2922+
['hwb(0 calc(infinity) 50)', 'rgb(255, 255, 255)'],
2923+
['hwb(90 0 calc(-infinity))', 'rgb(255, 255, 0)'],
2924+
['hwb(0 50 calc(infinity))', 'rgb(0, 0, 0)'],
2925+
['hwb(calc(1em / 1px) -1% 101% / -1%)', 'hwb(calc(1em / 1px) -1 101 / 0)'],
2926+
['hwb(sibling-count() calc(-1) calc(101) / calc(101))', 'hwb(sibling-count() -1 101)'],
29462927
// Relative color
29472928
['hwb(from green alpha calc(h) calc(w * 1%) / calc(b + 1 + 1))', 'hwb(from green alpha calc(h) calc(1% * w) / calc(2 + b))'],
29482929
['hwb(from hwb(540 -1 0 / -1) 540deg -1% 0% / 101%)', 'hwb(from hwb(180 -1 0 / 0) 180 -1 0)'],

lib/serialize.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -483,32 +483,32 @@ function serializeColor({ name, types, value }, specified) {
483483
switch (name) {
484484
case 'hsl':
485485
case 'hsla':
486-
// Clamp saturation and lightness
487-
value = clamp(0, value, 100)
486+
// Clamp saturation in [0,∞], lightness in [-∞,∞]
487+
value = clamp(index === 1 ? 0 : MIN_INTEGER, value, MAX_INTEGER)
488488
break
489489
case 'hwb':
490-
// Clamp whiteness and blackness
491-
value = clamp(0, value, 100)
490+
// Clamp whiteness and blackness in [-∞,∞]
491+
value = clamp(MIN_INTEGER, value, MAX_INTEGER)
492492
break
493493
case 'lab':
494-
// Clamp lightness
495-
value = index === 0 ? clamp(0, value, 100) : value
494+
// Clamp lightness in [0,100], a and b in [-∞,∞]
495+
value = index === 0 ? clamp(0, value, 100) : clamp(MIN_INTEGER, value, MAX_INTEGER)
496496
break
497497
case 'lch':
498-
// Clamp lightness and negative chromaticity
499-
value = index === 0 ? clamp(0, value, 100) : Math.max(0, value)
498+
// Clamp lightness in [0,100], chromaticity in [0,∞]
499+
value = clamp(0, value, index === 0 ? 100 : MAX_INTEGER)
500500
break
501501
case 'oklab':
502-
// Clamp lightness
503-
value = index === 0 ? clamp(0, value, 1) : value
502+
// Clamp lightness in [0,1], a and b in [-∞,∞]
503+
value = index === 0 ? clamp(0, value, 1) : clamp(MIN_INTEGER, value, MAX_INTEGER)
504504
break
505505
case 'oklch':
506-
// Clamp lightness and negative chromaticity
506+
// Clamp lightness in [0,1], chromaticity in [0,∞]
507507
value = index === 0 ? clamp(0, value, 1) : Math.max(0, value)
508508
break
509509
case 'rgb':
510510
case 'rgba':
511-
// Clamp all channels
511+
// Clamp all channels in [0,255]
512512
value = clamp(0, value, 255)
513513
break
514514
}
@@ -531,7 +531,7 @@ function serializeColor({ name, types, value }, specified) {
531531
value = hslToRgb(...value).map(toEightBit)
532532
} else if (name === 'hwb') {
533533
value = hwbToRgb(...value).map(toEightBit)
534-
} else if (name === 'rgb') {
534+
} else if (name === 'rgb' || name === 'rgba') {
535535
value = value.map(Math.round)
536536
}
537537
value = value.map(value => typeof value === 'number' ? serializeNumber({ value }) : value)

lib/utils/math.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,5 @@ export function toRadians(degrees) {
134134
* @returns {number}
135135
*/
136136
export function toEightBit(number) {
137-
return Math.round(safeFloat(number * 255))
137+
return clamp(0, Math.round(safeFloat(number * 255)), 255)
138138
}

0 commit comments

Comments
 (0)