Skip to content

Commit

Permalink
Properly support degenerate channel values
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Jul 9, 2024
1 parent 2d01ae8 commit 4476944
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 72 deletions.
62 changes: 33 additions & 29 deletions lib/src/functions/color.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ final global = UnmodifiableListView([
}

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

warnForDeprecation(
"lighten() is deprecated. "
Expand All @@ -156,8 +156,8 @@ final global = UnmodifiableListView([
}

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

warnForDeprecation(
"darken() is deprecated. "
Expand Down Expand Up @@ -187,8 +187,10 @@ final global = UnmodifiableListView([
}

var result = color.changeHsl(
saturation: (color.saturation + amount.valueInRange(0, 100, "amount"))
.clamp(0, 100));
saturation: clampLikeCss(
color.saturation + amount.valueInRange(0, 100, "amount"),
0,
100));

warnForDeprecation(
"saturate() is deprecated. "
Expand All @@ -210,8 +212,8 @@ final global = UnmodifiableListView([
}

var result = color.changeHsl(
saturation: (color.saturation - amount.valueInRange(0, 100, "amount"))
.clamp(0, 100));
saturation: clampLikeCss(
color.saturation - amount.valueInRange(0, 100, "amount"), 0, 100));

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

/// Returns [oldValue] adjusted by [adjustmentArg] according to the definition
/// in [color]'s space's [channel].
Expand Down Expand Up @@ -1062,9 +1064,10 @@ Value _rgb(String name, List<Value> arguments) {
arguments[0].assertNumber("red"),
arguments[1].assertNumber("green"),
arguments[2].assertNumber("blue"),
alpha.andThen((alpha) =>
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")
.clamp(0, 1)) ??
alpha.andThen((alpha) => clampLikeCss(
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"),
0,
1)) ??
1,
fromRgbFunction: true);
}
Expand Down Expand Up @@ -1100,8 +1103,8 @@ Value _rgbTwoArg(String name, List<Value> arguments) {
}

var alpha = arguments[1].assertNumber("alpha");
return color
.changeAlpha(_percentageOrUnitless(alpha, 1, "alpha").clamp(0, 1));
return color.changeAlpha(
clampLikeCss(_percentageOrUnitless(alpha, 1, "alpha"), 0, 1));
}

/// The implementation of the three- and four-argument `hsl()` and `hsla()`
Expand All @@ -1120,9 +1123,10 @@ Value _hsl(String name, List<Value> arguments) {
arguments[0].assertNumber("hue"),
arguments[1].assertNumber("saturation"),
arguments[2].assertNumber("lightness"),
alpha.andThen((alpha) =>
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")
.clamp(0, 1)) ??
alpha.andThen((alpha) => clampLikeCss(
_percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"),
0,
1)) ??
1);
}

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

var result = color.changeAlpha(
(color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", ""))
.clamp(0, 1));
var result = color.changeAlpha(clampLikeCss(
(color.alpha + amount.valueInRangeWithUnit(0, 1, "amount", "")), 0, 1));

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

var result = color.changeAlpha(
(color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", ""))
.clamp(0, 1));
var result = color.changeAlpha(clampLikeCss(
(color.alpha - amount.valueInRangeWithUnit(0, 1, "amount", "")), 0, 1));

warnForDeprecation(
"$name() is deprecated. "
Expand Down Expand Up @@ -1390,8 +1392,10 @@ Value _parseChannels(String functionName, Value input,
var alpha = switch (alphaValue) {
null => 1.0,
SassString(hasQuotes: false, text: 'none') => null,
_ => _percentageOrUnitless(alphaValue.assertNumber(name), 1, 'alpha')
.clamp(0, 1)
_ => clampLikeCss(
_percentageOrUnitless(alphaValue.assertNumber(name), 1, 'alpha'),
0,
1)
.toDouble()
};

Expand Down Expand Up @@ -1549,10 +1553,10 @@ double? _channelFromValue(ColorChannel channel, SassNumber? value,
_percentageOrUnitless(value, channel.max, channel.name),
LinearChannel() when !clamp =>
_percentageOrUnitless(value, channel.max, channel.name),
LinearChannel(:var lowerClamped, :var upperClamped) =>
_percentageOrUnitless(value, channel.max, channel.name).clamp(
lowerClamped ? channel.min : double.negativeInfinity,
upperClamped ? channel.max : double.infinity),
LinearChannel(:var lowerClamped, :var upperClamped) => clampLikeCss(
_percentageOrUnitless(value, channel.max, channel.name),
lowerClamped ? channel.min : double.negativeInfinity,
upperClamped ? channel.max : double.infinity),
_ => value.coerceValueToUnit('deg', channel.name) % 360
});

Expand Down
10 changes: 6 additions & 4 deletions lib/src/js/legacy/value/color.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:js/js.dart';

import '../../../util/nullable.dart';
import '../../../util/number.dart';
import '../../../value.dart';
import '../../reflection.dart';
Expand Down Expand Up @@ -45,8 +46,8 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color',
red = redOrArgb!;
}

thisArg.dartValue = SassColor.rgb(
_clamp(red), _clamp(green), _clamp(blue), alpha?.clamp(0, 1) ?? 1);
thisArg.dartValue = SassColor.rgb(_clamp(red), _clamp(green), _clamp(blue),
alpha.andThen((alpha) => clampLikeCss(alpha.toDouble(), 0, 1)) ?? 1);
})
..defineMethods({
'getR': (_NodeSassColor thisArg) => thisArg.dartValue.red,
Expand All @@ -63,10 +64,11 @@ final JSClass legacyColorClass = createJSClass('sass.types.Color',
thisArg.dartValue = thisArg.dartValue.changeRgb(blue: _clamp(value));
},
'setA': (_NodeSassColor thisArg, num value) {
thisArg.dartValue = thisArg.dartValue.changeRgb(alpha: value.clamp(0, 1));
thisArg.dartValue = thisArg.dartValue
.changeRgb(alpha: clampLikeCss(value.toDouble(), 0, 1));
}
});

/// Clamps [channel] within the range 0, 255 and rounds it to the nearest
/// integer.
int _clamp(num channel) => fuzzyRound(channel.clamp(0, 255));
int _clamp(num channel) => fuzzyRound(clampLikeCss(channel.toDouble(), 0, 255));
6 changes: 6 additions & 0 deletions lib/src/util/number.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ double moduloLikeSass(double num1, double num2) {
return result == 0 ? 0 : result + num2;
}

//// Returns [num] clamped between [lowerBound] and [upperBound], with `NaN`
//// preferring the lower bound (unlike Dart for which it prefers the upper
//// bound).
double clampLikeCss(double number, double lowerBound, double upperBound) =>
number.isNaN ? lowerBound : number.clamp(lowerBound, upperBound);

/// Returns the square root of [number].
SassNumber sqrt(SassNumber number) {
number.assertNoUnits("number");
Expand Down
16 changes: 0 additions & 16 deletions lib/src/value/color.dart
Original file line number Diff line number Diff line change
Expand Up @@ -523,22 +523,6 @@ class SassColor extends Value {
alpha.andThen((alpha) => fuzzyAssertRange(alpha, 0, 1, "alpha")) {
assert(format == null || _space == ColorSpace.rgb);
assert(space != ColorSpace.lms);

_checkChannel(channel0OrNull, space.channels[0].name);
_checkChannel(channel1OrNull, space.channels[1].name);
_checkChannel(channel2OrNull, space.channels[2].name);
}

/// Throws a [RangeError] if [channel] isn't a finite number.
void _checkChannel(double? channel, String name) {
switch (channel) {
case null:
return;
case double(isNaN: true):
throw RangeError.value(channel, name, 'must be a number.');
case double(isFinite: false):
throw RangeError.value(channel, name, 'must be finite.');
}
}

/// If [hue] isn't null, normalizes it to the range `[0, 360)`.
Expand Down
3 changes: 2 additions & 1 deletion lib/src/value/color/gamut_map_method/clip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:meta/meta.dart';

import '../../../util/number.dart';
import '../../color.dart';

/// Gamut mapping by clipping individual channels.
Expand All @@ -24,7 +25,7 @@ final class ClipGamutMap extends GamutMapMethod {
double? _clampChannel(double? value, ColorChannel channel) => value == null
? null
: switch (channel) {
LinearChannel(:var min, :var max) => value.clamp(min, max),
LinearChannel(:var min, :var max) => clampLikeCss(value, min, max),
_ => value
};
}
36 changes: 14 additions & 22 deletions lib/src/visitor/serialize.dart
Original file line number Diff line number Diff line change
Expand Up @@ -597,14 +597,11 @@ final class _SerializeVisitor
_buffer
..write(value.space)
..writeCharCode($lparen);
_writeChannel(value.channel0OrNull);
if (!_isCompressed && !value.isChannel0Missing) _buffer.write('deg');
_writeChannel(value.channel0OrNull, _isCompressed ? null : 'deg');
_buffer.writeCharCode($space);
_writeChannel(value.channel1OrNull);
if (!value.isChannel1Missing) _buffer.writeCharCode($percent);
_writeChannel(value.channel1OrNull, '%');
_buffer.writeCharCode($space);
_writeChannel(value.channel2OrNull);
if (!value.isChannel2Missing) _buffer.writeCharCode($percent);
_writeChannel(value.channel2OrNull, '%');
_maybeWriteSlashAlpha(value);
_buffer.writeCharCode($rparen);

Expand Down Expand Up @@ -672,10 +669,8 @@ final class _SerializeVisitor
_buffer.writeCharCode($space);
_writeChannel(value.channel1OrNull);
_buffer.writeCharCode($space);
_writeChannel(value.channel2OrNull);
if (!_isCompressed && !value.isChannel2Missing && polar) {
_buffer.write('deg');
}
_writeChannel(
value.channel2OrNull, polar && !_isCompressed ? 'deg' : null);
_maybeWriteSlashAlpha(value);
_buffer.writeCharCode($rparen);

Expand All @@ -685,11 +680,14 @@ final class _SerializeVisitor
}

/// Writes a [channel] which may be missing.
void _writeChannel(double? channel) {
void _writeChannel(double? channel, [String? unit]) {
if (channel == null) {
_buffer.write('none');
} else {
} else if (channel.isFinite) {
_writeNumber(channel);
if (unit != null) _buffer.write(unit);
} else {
visitNumber(SassNumber(channel, unit));
}
}

Expand Down Expand Up @@ -866,13 +864,11 @@ final class _SerializeVisitor
var opaque = fuzzyEquals(color.alpha, 1);
var hsl = color.toSpace(ColorSpace.hsl);
_buffer.write(opaque ? "hsl(" : "hsla(");
_writeNumber(hsl.channel('hue'));
_writeChannel(hsl.channel('hue'));
_buffer.write(_commaSeparator);
_writeNumber(hsl.channel('saturation'));
_buffer.writeCharCode($percent);
_writeChannel(hsl.channel('saturation'), '%');
_buffer.write(_commaSeparator);
_writeNumber(hsl.channel('lightness'));
_buffer.writeCharCode($percent);
_writeChannel(hsl.channel('lightness'), '%');

if (!opaque) {
_buffer.write(_commaSeparator);
Expand Down Expand Up @@ -948,11 +944,7 @@ final class _SerializeVisitor
_writeOptionalSpace();
_buffer.writeCharCode($slash);
_writeOptionalSpace();
if (color.isAlphaMissing) {
_buffer.write('none');
} else {
_writeNumber(color.alpha);
}
_writeChannel(color.alphaOrNull);
}

void visitFunction(SassFunction function) {
Expand Down

0 comments on commit 4476944

Please sign in to comment.