Skip to content

Commit 4476944

Browse files
committed
Properly support degenerate channel values
1 parent 2d01ae8 commit 4476944

File tree

6 files changed

+61
-72
lines changed

6 files changed

+61
-72
lines changed

lib/src/functions/color.dart

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ final global = UnmodifiableListView([
134134
}
135135

136136
var result = color.changeHsl(
137-
lightness: (color.lightness + amount.valueInRange(0, 100, "amount"))
138-
.clamp(0, 100));
137+
lightness: clampLikeCss(
138+
color.lightness + amount.valueInRange(0, 100, "amount"), 0, 100));
139139

140140
warnForDeprecation(
141141
"lighten() is deprecated. "
@@ -156,8 +156,8 @@ final global = UnmodifiableListView([
156156
}
157157

158158
var result = color.changeHsl(
159-
lightness: (color.lightness - amount.valueInRange(0, 100, "amount"))
160-
.clamp(0, 100));
159+
lightness: clampLikeCss(
160+
color.lightness - amount.valueInRange(0, 100, "amount"), 0, 100));
161161

162162
warnForDeprecation(
163163
"darken() is deprecated. "
@@ -187,8 +187,10 @@ final global = UnmodifiableListView([
187187
}
188188

189189
var result = color.changeHsl(
190-
saturation: (color.saturation + amount.valueInRange(0, 100, "amount"))
191-
.clamp(0, 100));
190+
saturation: clampLikeCss(
191+
color.saturation + amount.valueInRange(0, 100, "amount"),
192+
0,
193+
100));
192194

193195
warnForDeprecation(
194196
"saturate() is deprecated. "
@@ -210,8 +212,8 @@ final global = UnmodifiableListView([
210212
}
211213

212214
var result = color.changeHsl(
213-
saturation: (color.saturation - amount.valueInRange(0, 100, "amount"))
214-
.clamp(0, 100));
215+
saturation: clampLikeCss(
216+
color.saturation - amount.valueInRange(0, 100, "amount"), 0, 100));
215217

216218
warnForDeprecation(
217219
"desaturate() is deprecated. "
@@ -947,7 +949,7 @@ SassColor _adjustColor(
947949
// The color space doesn't matter for alpha, as long as it's not
948950
// strictly bounded.
949951
_adjustChannel(color, ColorChannel.alpha, color.alphaOrNull, alphaArg)
950-
?.clamp(0, 1));
952+
.andThen((alpha) => clampLikeCss(alpha, 0, 1)));
951953

952954
/// Returns [oldValue] adjusted by [adjustmentArg] according to the definition
953955
/// in [color]'s space's [channel].
@@ -1062,9 +1064,10 @@ Value _rgb(String name, List<Value> arguments) {
10621064
arguments[0].assertNumber("red"),
10631065
arguments[1].assertNumber("green"),
10641066
arguments[2].assertNumber("blue"),
1065-
alpha.andThen((alpha) =>
1066-
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")
1067-
.clamp(0, 1)) ??
1067+
alpha.andThen((alpha) => clampLikeCss(
1068+
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"),
1069+
0,
1070+
1)) ??
10681071
1,
10691072
fromRgbFunction: true);
10701073
}
@@ -1100,8 +1103,8 @@ Value _rgbTwoArg(String name, List<Value> arguments) {
11001103
}
11011104

11021105
var alpha = arguments[1].assertNumber("alpha");
1103-
return color
1104-
.changeAlpha(_percentageOrUnitless(alpha, 1, "alpha").clamp(0, 1));
1106+
return color.changeAlpha(
1107+
clampLikeCss(_percentageOrUnitless(alpha, 1, "alpha"), 0, 1));
11051108
}
11061109

11071110
/// The implementation of the three- and four-argument `hsl()` and `hsla()`
@@ -1120,9 +1123,10 @@ Value _hsl(String name, List<Value> arguments) {
11201123
arguments[0].assertNumber("hue"),
11211124
arguments[1].assertNumber("saturation"),
11221125
arguments[2].assertNumber("lightness"),
1123-
alpha.andThen((alpha) =>
1124-
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")
1125-
.clamp(0, 1)) ??
1126+
alpha.andThen((alpha) => clampLikeCss(
1127+
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"),
1128+
0,
1129+
1)) ??
11261130
1);
11271131
}
11281132

@@ -1235,9 +1239,8 @@ SassColor _opacify(String name, List<Value> arguments) {
12351239
"color.adjust() instead with an explicit \$space argument.");
12361240
}
12371241

1238-
var result = color.changeAlpha(
1239-
(color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", ""))
1240-
.clamp(0, 1));
1242+
var result = color.changeAlpha(clampLikeCss(
1243+
(color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", "")), 0, 1));
12411244

12421245
warnForDeprecation(
12431246
"$name() is deprecated. "
@@ -1258,9 +1261,8 @@ SassColor _transparentize(String name, List<Value> arguments) {
12581261
"color.adjust() instead with an explicit \$space argument.");
12591262
}
12601263

1261-
var result = color.changeAlpha(
1262-
(color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", ""))
1263-
.clamp(0, 1));
1264+
var result = color.changeAlpha(clampLikeCss(
1265+
(color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", "")), 0, 1));
12641266

12651267
warnForDeprecation(
12661268
"$name() is deprecated. "
@@ -1390,8 +1392,10 @@ Value _parseChannels(String functionName, Value input,
13901392
var alpha = switch (alphaValue) {
13911393
null => 1.0,
13921394
SassString(hasQuotes: false, text: 'none') => null,
1393-
_ => _percentageOrUnitless(alphaValue.assertNumber(name), 1, 'alpha')
1394-
.clamp(0, 1)
1395+
_ => clampLikeCss(
1396+
_percentageOrUnitless(alphaValue.assertNumber(name), 1, 'alpha'),
1397+
0,
1398+
1)
13951399
.toDouble()
13961400
};
13971401

@@ -1549,10 +1553,10 @@ double? _channelFromValue(ColorChannel channel, SassNumber? value,
15491553
_percentageOrUnitless(value, channel.max, channel.name),
15501554
LinearChannel() when !clamp =>
15511555
_percentageOrUnitless(value, channel.max, channel.name),
1552-
LinearChannel(:var lowerClamped, :var upperClamped) =>
1553-
_percentageOrUnitless(value, channel.max, channel.name).clamp(
1554-
lowerClamped ? channel.min : double.negativeInfinity,
1555-
upperClamped ? channel.max : double.infinity),
1556+
LinearChannel(:var lowerClamped, :var upperClamped) => clampLikeCss(
1557+
_percentageOrUnitless(value, channel.max, channel.name),
1558+
lowerClamped ? channel.min : double.negativeInfinity,
1559+
upperClamped ? channel.max : double.infinity),
15561560
_ => value.coerceValueToUnit('deg', channel.name) % 360
15571561
});
15581562

lib/src/js/legacy/value/color.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:js/js.dart';
66

7+
import '../../../util/nullable.dart';
78
import '../../../util/number.dart';
89
import '../../../value.dart';
910
import '../../reflection.dart';
@@ -45,8 +46,8 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color',
4546
red = redOrArgb!;
4647
}
4748

48-
thisArg.dartValue = SassColor.rgb(
49-
_clamp(red), _clamp(green), _clamp(blue), alpha?.clamp(0, 1) ?? 1);
49+
thisArg.dartValue = SassColor.rgb(_clamp(red), _clamp(green), _clamp(blue),
50+
alpha.andThen((alpha) => clampLikeCss(alpha.toDouble(), 0, 1)) ?? 1);
5051
})
5152
..defineMethods({
5253
'getR': (_NodeSassColor thisArg) => thisArg.dartValue.red,
@@ -63,10 +64,11 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color',
6364
thisArg.dartValue = thisArg.dartValue.changeRgb(blue: _clamp(value));
6465
},
6566
'setA': (_NodeSassColor thisArg, num value) {
66-
thisArg.dartValue = thisArg.dartValue.changeRgb(alpha: value.clamp(0, 1));
67+
thisArg.dartValue = thisArg.dartValue
68+
.changeRgb(alpha: clampLikeCss(value.toDouble(), 0, 1));
6769
}
6870
});
6971

7072
/// Clamps [channel] within the range 0, 255 and rounds it to the nearest
7173
/// integer.
72-
int _clamp(num channel) => fuzzyRound(channel.clamp(0, 255));
74+
int _clamp(num channel) => fuzzyRound(clampLikeCss(channel.toDouble(), 0, 255));

lib/src/util/number.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ double moduloLikeSass(double num1, double num2) {
140140
return result == 0 ? 0 : result + num2;
141141
}
142142

143+
//// Returns [num] clamped between [lowerBound] and [upperBound], with `NaN`
144+
//// preferring the lower bound (unlike Dart for which it prefers the upper
145+
//// bound).
146+
double clampLikeCss(double number, double lowerBound, double upperBound) =>
147+
number.isNaN ? lowerBound : number.clamp(lowerBound, upperBound);
148+
143149
/// Returns the square root of [number].
144150
SassNumber sqrt(SassNumber number) {
145151
number.assertNoUnits("number");

lib/src/value/color.dart

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -523,22 +523,6 @@ class SassColor extends Value {
523523
alpha.andThen((alpha) => fuzzyAssertRange(alpha, 0, 1, "alpha")) {
524524
assert(format == null || _space == ColorSpace.rgb);
525525
assert(space != ColorSpace.lms);
526-
527-
_checkChannel(channel0OrNull, space.channels[0].name);
528-
_checkChannel(channel1OrNull, space.channels[1].name);
529-
_checkChannel(channel2OrNull, space.channels[2].name);
530-
}
531-
532-
/// Throws a [RangeError] if [channel] isn't a finite number.
533-
void _checkChannel(double? channel, String name) {
534-
switch (channel) {
535-
case null:
536-
return;
537-
case double(isNaN: true):
538-
throw RangeError.value(channel, name, 'must be a number.');
539-
case double(isFinite: false):
540-
throw RangeError.value(channel, name, 'must be finite.');
541-
}
542526
}
543527

544528
/// If [hue] isn't null, normalizes it to the range `[0, 360)`.

lib/src/value/color/gamut_map_method/clip.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:meta/meta.dart';
66

7+
import '../../../util/number.dart';
78
import '../../color.dart';
89

910
/// Gamut mapping by clipping individual channels.
@@ -24,7 +25,7 @@ final class ClipGamutMap extends GamutMapMethod {
2425
double? _clampChannel(double? value, ColorChannel channel) => value == null
2526
? null
2627
: switch (channel) {
27-
LinearChannel(:var min, :var max) => value.clamp(min, max),
28+
LinearChannel(:var min, :var max) => clampLikeCss(value, min, max),
2829
_ => value
2930
};
3031
}

lib/src/visitor/serialize.dart

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -597,14 +597,11 @@ final class _SerializeVisitor
597597
_buffer
598598
..write(value.space)
599599
..writeCharCode($lparen);
600-
_writeChannel(value.channel0OrNull);
601-
if (!_isCompressed && !value.isChannel0Missing) _buffer.write('deg');
600+
_writeChannel(value.channel0OrNull, _isCompressed ? null : 'deg');
602601
_buffer.writeCharCode($space);
603-
_writeChannel(value.channel1OrNull);
604-
if (!value.isChannel1Missing) _buffer.writeCharCode($percent);
602+
_writeChannel(value.channel1OrNull, '%');
605603
_buffer.writeCharCode($space);
606-
_writeChannel(value.channel2OrNull);
607-
if (!value.isChannel2Missing) _buffer.writeCharCode($percent);
604+
_writeChannel(value.channel2OrNull, '%');
608605
_maybeWriteSlashAlpha(value);
609606
_buffer.writeCharCode($rparen);
610607

@@ -672,10 +669,8 @@ final class _SerializeVisitor
672669
_buffer.writeCharCode($space);
673670
_writeChannel(value.channel1OrNull);
674671
_buffer.writeCharCode($space);
675-
_writeChannel(value.channel2OrNull);
676-
if (!_isCompressed && !value.isChannel2Missing && polar) {
677-
_buffer.write('deg');
678-
}
672+
_writeChannel(
673+
value.channel2OrNull, polar && !_isCompressed ? 'deg' : null);
679674
_maybeWriteSlashAlpha(value);
680675
_buffer.writeCharCode($rparen);
681676

@@ -685,11 +680,14 @@ final class _SerializeVisitor
685680
}
686681

687682
/// Writes a [channel] which may be missing.
688-
void _writeChannel(double? channel) {
683+
void _writeChannel(double? channel, [String? unit]) {
689684
if (channel == null) {
690685
_buffer.write('none');
691-
} else {
686+
} else if (channel.isFinite) {
692687
_writeNumber(channel);
688+
if (unit != null) _buffer.write(unit);
689+
} else {
690+
visitNumber(SassNumber(channel, unit));
693691
}
694692
}
695693

@@ -866,13 +864,11 @@ final class _SerializeVisitor
866864
var opaque = fuzzyEquals(color.alpha, 1);
867865
var hsl = color.toSpace(ColorSpace.hsl);
868866
_buffer.write(opaque ? "hsl(" : "hsla(");
869-
_writeNumber(hsl.channel('hue'));
867+
_writeChannel(hsl.channel('hue'));
870868
_buffer.write(_commaSeparator);
871-
_writeNumber(hsl.channel('saturation'));
872-
_buffer.writeCharCode($percent);
869+
_writeChannel(hsl.channel('saturation'), '%');
873870
_buffer.write(_commaSeparator);
874-
_writeNumber(hsl.channel('lightness'));
875-
_buffer.writeCharCode($percent);
871+
_writeChannel(hsl.channel('lightness'), '%');
876872

877873
if (!opaque) {
878874
_buffer.write(_commaSeparator);
@@ -948,11 +944,7 @@ final class _SerializeVisitor
948944
_writeOptionalSpace();
949945
_buffer.writeCharCode($slash);
950946
_writeOptionalSpace();
951-
if (color.isAlphaMissing) {
952-
_buffer.write('none');
953-
} else {
954-
_writeNumber(color.alpha);
955-
}
947+
_writeChannel(color.alphaOrNull);
956948
}
957949

958950
void visitFunction(SassFunction function) {

0 commit comments

Comments
 (0)