From 19186742956b72ec34693d98d0ebbd30bfe23b53 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 17 Sep 2020 16:18:12 -0700 Subject: [PATCH 1/4] Support HWB colors in SassColor (#1085) See sass/sass#2835 --- CHANGELOG.md | 9 +++ lib/src/value/color.dart | 66 +++++++++++++++---- lib/src/value/external/color.dart | 16 +++++ pubspec.yaml | 2 +- test/dart_api/value/color_test.dart | 99 +++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b82dd9..56819570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.27.0 + +### Dart API + +* Add [HWB] support to the `SassColor` class, including a `SassColor.hwb()` + constructor, `whiteness` and `blackness` getters, and a `changeHwb()` method. + +[HWB]: https://en.wikipedia.org/wiki/HWB_color_model + ## 1.26.11 * **Potentially breaking bug fix:** `selector.nest()` now throws an error diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index bbce5acd..36261b89 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -55,6 +55,20 @@ class SassColor extends Value implements ext.SassColor { num _lightness; + num get whiteness { + // Because HWB is (currently) used much less frequently than HSL or RGB, we + // don't cache its values because we expect the memory overhead of doing so + // to outweigh the cost of recalculating it on access. + return math.min(math.min(red, green), blue) / 255 * 100; + } + + num get blackness { + // Because HWB is (currently) used much less frequently than HSL or RGB, we + // don't cache its values because we expect the memory overhead of doing so + // to outweigh the cost of recalculating it on access. + return 100 - math.max(math.max(red, green), blue) / 255 * 100; + } + final num alpha; /// The original string representation of this color, or `null` if one is @@ -81,6 +95,35 @@ class SassColor extends Value implements ext.SassColor { alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha"), originalSpan = null; + factory SassColor.hwb(num hue, num whiteness, num blackness, [num alpha]) { + // From https://www.w3.org/TR/css-color-4/#hwb-to-rgb + var scaledHue = hue % 360 / 360; + var scaledWhiteness = fuzzyAssertRange(whiteness, 0, 100, "whiteness") / 100; + var scaledBlackness = fuzzyAssertRange(blackness, 0, 100, "blackness") / 100; + + var sum = scaledWhiteness + scaledBlackness; + if (sum > 1) { + scaledWhiteness /= sum; + scaledBlackness /= sum; + } + + var factor = 1 - scaledWhiteness - scaledBlackness; + int toRgb(num hue) { + var channel = _hueToRgb(0, 1, hue) * factor + scaledWhiteness; + return fuzzyRound(channel * 255); + } + + // Because HWB is (currently) used much less frequently than HSL or RGB, we + // don't cache its values because we expect the memory overhead of doing so + // to outweigh the cost of recalculating it on access. Instead, we eagerly + // convert it to RGB and then convert back if necessary. + return SassColor.rgb( + toRgb(scaledHue + 1/3), + toRgb(scaledHue), + toRgb(scaledHue - 1/3), + alpha); + } + SassColor._(this._red, this._green, this._blue, this._hue, this._saturation, this._lightness, this.alpha) : originalSpan = null; @@ -97,6 +140,10 @@ class SassColor extends Value implements ext.SassColor { SassColor.hsl(hue ?? this.hue, saturation ?? this.saturation, lightness ?? this.lightness, alpha ?? this.alpha); + SassColor changeHwb({num hue, num whiteness, num blackness, num alpha}) => + SassColor.hwb(hue ?? this.hue, whiteness ?? this.whiteness, + blackness ?? this.blackness, alpha ?? this.alpha); + SassColor changeAlpha(num alpha) => SassColor._(_red, _green, _blue, _hue, _saturation, _lightness, fuzzyAssertRange(alpha, 0, 1, "alpha")); @@ -177,29 +224,26 @@ class SassColor extends Value implements ext.SassColor { scaledSaturation - scaledLightness * scaledSaturation; var m1 = scaledLightness * 2 - m2; - _red = _hueToRgb(m1, m2, scaledHue + 1 / 3); - _green = _hueToRgb(m1, m2, scaledHue); - _blue = _hueToRgb(m1, m2, scaledHue - 1 / 3); + _red = fuzzyRound(_hueToRgb(m1, m2, scaledHue + 1 / 3) * 255); + _green = fuzzyRound(_hueToRgb(m1, m2, scaledHue) * 255); + _blue = fuzzyRound(_hueToRgb(m1, m2, scaledHue - 1 / 3) * 255); } /// An algorithm from the CSS3 spec: /// http://www.w3.org/TR/css3-color/#hsl-color. - int _hueToRgb(num m1, num m2, num hue) { + static num _hueToRgb(num m1, num m2, num hue) { if (hue < 0) hue += 1; if (hue > 1) hue -= 1; - num result; if (hue < 1 / 6) { - result = m1 + (m2 - m1) * hue * 6; + return m1 + (m2 - m1) * hue * 6; } else if (hue < 1 / 2) { - result = m2; + return m2; } else if (hue < 2 / 3) { - result = m1 + (m2 - m1) * (2 / 3 - hue) * 6; + return m1 + (m2 - m1) * (2 / 3 - hue) * 6; } else { - result = m1; + return m1; } - - return fuzzyRound(result * 255); } /// Returns an `rgb()` or `rgba()` function call that will evaluate to this diff --git a/lib/src/value/external/color.dart b/lib/src/value/external/color.dart index 4497f7e3..3bdc4efd 100644 --- a/lib/src/value/external/color.dart +++ b/lib/src/value/external/color.dart @@ -28,6 +28,12 @@ abstract class SassColor extends Value { /// This color's lightness, a percentage between `0` and `100`. num get lightness; + /// This color's whiteness, a percentage between `0` and `100`. + num get whiteness; + + /// This color's blackness, a percentage between `0` and `100`. + num get blackness; + /// This color's alpha channel, between `0` and `1`. num get alpha; @@ -45,12 +51,22 @@ abstract class SassColor extends Value { factory SassColor.hsl(num hue, num saturation, num lightness, [num alpha]) = internal.SassColor.hsl; + /// Creates an HWB color. + /// + /// Throws a [RangeError] if [whiteness] or [blackness] aren't between `0` and + /// `100`, or if [alpha] isn't between `0` and `1`. + factory SassColor.hwb(num hue, num whiteness, num blackness, [num alpha]) = + internal.SassColor.hwb; + /// Changes one or more of this color's RGB channels and returns the result. SassColor changeRgb({int red, int green, int blue, num alpha}); /// Changes one or more of this color's HSL channels and returns the result. SassColor changeHsl({num hue, num saturation, num lightness, num alpha}); + /// Changes one or more of this color's HWB channels and returns the result. + SassColor changeHwb({num hue, num whiteness, num blackness, num alpha}); + /// Returns a new copy of this color with the alpha channel set to [alpha]. SassColor changeAlpha(num alpha); } diff --git a/pubspec.yaml b/pubspec.yaml index 03d66083..aa86787d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.26.11-dev +version: 1.27.0-dev description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/dart_api/value/color_test.dart b/test/dart_api/value/color_test.dart index 6b296c01..d2eef137 100644 --- a/test/dart_api/value/color_test.dart +++ b/test/dart_api/value/color_test.dart @@ -28,6 +28,12 @@ void main() { expect(value.lightness, equals(20.392156862745097)); }); + test("has HWB channels", () { + expect(value.hue, equals(210)); + expect(value.whiteness, equals(7.0588235294117645)); + expect(value.blackness, equals(66.27450980392157)); + }); + test("has an alpha channel", () { expect(value.alpha, equals(1)); }); @@ -114,6 +120,46 @@ void main() { }); }); + group("changeHwb()", () { + test("changes HWB values", () { + expect(value.changeHwb(hue: 120), + equals(SassColor.hwb(120, 7.0588235294117645, 66.27450980392157))); + expect(value.changeHwb(whiteness: 20), + equals(SassColor.hwb(210, 20, 66.27450980392157))); + expect(value.changeHwb(blackness: 42), + equals(SassColor.hwb(210, 7.0588235294117645, 42))); + expect( + value.changeHwb(alpha: 0.5), + equals( + SassColor.hwb(210, 7.0588235294117645, 66.27450980392157, 0.5))); + expect( + value.changeHwb( + hue: 120, whiteness: 42, blackness: 42, alpha: 0.5), + equals(SassColor.hwb(120, 42, 42, 0.5))); + expect( + value.changeHwb(whiteness: 50), + equals(SassColor.hwb(210, 43, 57))); + }); + + test("allows valid values", () { + expect(value.changeHwb(whiteness: 0).whiteness, equals(0)); + expect(value.changeHwb(whiteness: 100).whiteness, equals(60.0)); + expect(value.changeHwb(blackness: 0).blackness, equals(0)); + expect(value.changeHwb(blackness: 100).blackness, equals(93.33333333333333)); + expect(value.changeHwb(alpha: 0).alpha, equals(0)); + expect(value.changeHwb(alpha: 1).alpha, equals(1)); + }); + + test("disallows invalid values", () { + expect(() => value.changeHwb(whiteness: -0.1), throwsRangeError); + expect(() => value.changeHwb(whiteness: 100.1), throwsRangeError); + expect(() => value.changeHwb(blackness: -0.1), throwsRangeError); + expect(() => value.changeHwb(blackness: 100.1), throwsRangeError); + expect(() => value.changeHwb(alpha: -0.1), throwsRangeError); + expect(() => value.changeHwb(alpha: 1.1), throwsRangeError); + }); + }); + group("changeAlpha()", () { test("changes the alpha value", () { expect(value.changeAlpha(0.5), @@ -160,6 +206,11 @@ void main() { expect(value.lightness, equals(42)); }); + test("has HWB channels", () { + expect(value.whiteness, equals(24.313725490196077)); + expect(value.blackness, equals(40.3921568627451)); + }); + test("has an alpha channel", () { expect(value.alpha, equals(1)); }); @@ -167,6 +218,7 @@ void main() { test("equals the same color", () { expect(value, equalsWithHash(SassColor.rgb(0x3E, 0x98, 0x3E))); expect(value, equalsWithHash(SassColor.hsl(120, 42, 42))); + expect(value, equalsWithHash(SassColor.hwb(120, 24.313725490196077, 40.3921568627451))); }); }); @@ -209,4 +261,51 @@ void main() { expect(() => SassColor.hsl(0, 0, 0, 1.1), throwsRangeError); }); }); + + group("new SassColor.hwb()", () { + SassColor value; + setUp(() => value = SassColor.hwb(120, 42, 42)); + + test("has RGB channels", () { + expect(value.red, equals(0x6B)); + expect(value.green, equals(0x94)); + expect(value.blue, equals(0x6B)); + }); + + test("has HSL channels", () { + expect(value.hue, equals(120)); + expect(value.saturation, equals(16.078431372549026)); + expect(value.lightness, equals(50)); + }); + + test("has HWB channels", () { + expect(value.whiteness, equals(41.96078431372549)); + expect(value.blackness, equals(41.96078431372548)); + }); + + test("has an alpha channel", () { + expect(value.alpha, equals(1)); + }); + + test("equals the same color", () { + expect(value, equalsWithHash(SassColor.rgb(0x6B, 0x94, 0x6B))); + expect(value, equalsWithHash(SassColor.hsl(120, 16, 50))); + expect(value, equalsWithHash(SassColor.hwb(120, 42, 42))); + }); + + test("allows valid values", () { + expect(SassColor.hwb(0, 0, 0, 0), equals(parseValue("rgba(255, 0, 0, 0)"))); + expect(SassColor.hwb(4320, 100, 100, 1), + equals(parseValue("grey"))); + }); + + test("disallows invalid values", () { + expect(() => SassColor.hwb(0, -0.1, 0, 0), throwsRangeError); + expect(() => SassColor.hwb(0, 0, -0.1, 0), throwsRangeError); + expect(() => SassColor.hwb(0, 0, 0, -0.1), throwsRangeError); + expect(() => SassColor.hwb(0, 100.1, 0, 0), throwsRangeError); + expect(() => SassColor.hwb(0, 0, 100.1, 0), throwsRangeError); + expect(() => SassColor.hwb(0, 0, 0, 1.1), throwsRangeError); + }); + }); } From d3691d44f0830e9adc9178f34e8db7ee971f8e0d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 23 Sep 2020 11:45:31 -0700 Subject: [PATCH 2/4] Add a color.hwb() function (#1092) See sass/sass#2834 See #1089 --- lib/src/functions/color.dart | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index ba61d56c..faa8ba53 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -265,6 +265,22 @@ final module = BuiltInModule("color", functions: [ return color.changeHsl(saturation: 0); }), + // ### HWB + BuiltInCallable.overloadedFunction("hwb", { + r"$hue, $whiteness, $blackness, $alpha: 1": (arguments) => _hwb(arguments), + r"$channels": (arguments) { + var parsed = _parseChannels( + "hwb", [r"$hue", r"$whiteness", r"$blackness"], arguments.first); + + // `hwb()` doesn't (currently) support special number or variable strings. + if (parsed is SassString) { + throw SassScriptException('Expected numeric channels, got "$parsed".'); + } else { + return _hwb(parsed as List); + } + } + }), + // ### Opacity _removedColorFunction("opacify", "alpha"), _removedColorFunction("fade-in", "alpha"), @@ -636,6 +652,25 @@ Value _hsl(String name, List arguments) { : _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")); } +/// Create an HWB color from the given [arguments]. +Value _hwb(List arguments) { + var alpha = arguments.length > 3 ? arguments[3] : null; + var hue = arguments[0].assertNumber("hue"); + var whiteness = arguments[1].assertNumber("whiteness"); + var blackness = arguments[2].assertNumber("blackness"); + + whiteness.assertUnit("%", "whiteness"); + blackness.assertUnit("%", "whiteness"); + + return SassColor.hwb( + hue.value, + whiteness.valueInRange(0, 100, "whiteness"), + blackness.valueInRange(0, 100, "whiteness"), + alpha == null + ? null + : _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")); +} + Object /* SassString | List */ _parseChannels( String name, List argumentNames, Value channels) { if (channels.isVar) return _functionString(name, [channels]); From 7ba5128423b1437ac0aa59a3a443cd8960b524da Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 14 Oct 2020 16:15:46 -0700 Subject: [PATCH 3/4] Add color.whiteness() and color.blackness() functions (#1110) See sass/sass#2834 See #1089 --- CHANGELOG.md | 11 +++++++++++ lib/src/functions/color.dart | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5162f63..3fcaa0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## 1.28.0 +* Add a [`color.hwb()`] function to `sass:color` that can express colors in [HWB] format. + +[`color.hwb()`]: https://sass-lang.com/documentation/modules/color#hwb +[HWB]: https://en.wikipedia.org/wiki/HWB_color_model + +* Add [`color.whiteness()`] and [`color.blackness()`] functions to `sass:color` + to get a color's [HWB] whiteness and blackness components. + +[`color.whiteness()`]: https://sass-lang.com/documentation/modules/color#whiteness +[`color.blackness()`]: https://sass-lang.com/documentation/modules/color#blackness + ### Dart API * Add [HWB] support to the `SassColor` class, including a `SassColor.hwb()` diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index faa8ba53..99153fa7 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -281,6 +281,18 @@ final module = BuiltInModule("color", functions: [ } }), + _function( + "whiteness", + r"$color", + (arguments) => + SassNumber(arguments.first.assertColor("color").whiteness, "%")), + + _function( + "blackness", + r"$color", + (arguments) => + SassNumber(arguments.first.assertColor("color").blackness, "%")), + // ### Opacity _removedColorFunction("opacify", "alpha"), _removedColorFunction("fade-in", "alpha"), From 6c679e13d4f2504569ec7d49f611103f73cf5cd6 Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Wed, 14 Oct 2020 19:45:35 -0700 Subject: [PATCH 4/4] Support HWB in adjust, change, and scale (#1111) This adds `$whiteness` and `$blackness` parameters to the `adjust`, `change`, and `scale` functions in the color module. In doing so, I generalized the logic for all three functions into a single `_updateComponents` helper since they all shared a lot of similar logic for validating their parameters. --- CHANGELOG.md | 8 + lib/src/functions/color.dart | 278 ++++++++++++++--------------------- 2 files changed, 118 insertions(+), 168 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fcaa0cd..b1c6753c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ [`color.whiteness()`]: https://sass-lang.com/documentation/modules/color#whiteness [`color.blackness()`]: https://sass-lang.com/documentation/modules/color#blackness +* Add `$whiteness` and `$blackness` parameters to [`color.adjust()`], + [`color.change()`], and [`color.scale()`] to modify a color's [HWB] whiteness + and blackness components. + +[`color.adjust()`]: https://sass-lang.com/documentation/modules/color#adjust +[`color.change()`]: https://sass-lang.com/documentation/modules/color#change +[`color.scale()`]: https://sass-lang.com/documentation/modules/color#scale + ### Dart API * Add [HWB] support to the `SassColor` class, including a `SassColor.hwb()` diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 99153fa7..7a13d96c 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -282,16 +282,16 @@ final module = BuiltInModule("color", functions: [ }), _function( - "whiteness", - r"$color", - (arguments) => - SassNumber(arguments.first.assertColor("color").whiteness, "%")), + "whiteness", + r"$color", + (arguments) => + SassNumber(arguments.first.assertColor("color").whiteness, "%")), _function( - "blackness", - r"$color", - (arguments) => - SassNumber(arguments.first.assertColor("color").blackness, "%")), + "blackness", + r"$color", + (arguments) => + SassNumber(arguments.first.assertColor("color").blackness, "%")), // ### Opacity _removedColorFunction("opacify", "alpha"), @@ -396,165 +396,14 @@ final _complement = _function("complement", r"$color", (arguments) { // Miscellaneous -final _adjust = _function("adjust", r"$color, $kwargs...", (arguments) { - var color = arguments[0].assertColor("color"); - var argumentList = arguments[1] as SassArgumentList; - if (argumentList.asList.isNotEmpty) { - throw SassScriptException( - "Only one positional argument is allowed. All other arguments must " - "be passed by name."); - } +final _adjust = _function("adjust", r"$color, $kwargs...", + (arguments) => _updateComponents(arguments, adjust: true)); - var keywords = Map.of(argumentList.keywords); - num getInRange(String name, num min, num max) => - keywords.remove(name)?.assertNumber(name)?.valueInRange(min, max, name); +final _scale = _function("scale", r"$color, $kwargs...", + (arguments) => _updateComponents(arguments, scale: true)); - var red = _fuzzyRoundOrNull(getInRange("red", -255, 255)); - var green = _fuzzyRoundOrNull(getInRange("green", -255, 255)); - var blue = _fuzzyRoundOrNull(getInRange("blue", -255, 255)); - var hue = keywords.remove("hue")?.assertNumber("hue")?.value; - var saturation = getInRange("saturation", -100, 100); - var lightness = getInRange("lightness", -100, 100); - var alpha = getInRange("alpha", -1, 1); - - if (keywords.isNotEmpty) { - throw SassScriptException( - "No ${pluralize('argument', keywords.length)} named " - "${toSentence(keywords.keys.map((name) => "\$$name"), 'or')}."); - } - - var hasRgb = red != null || green != null || blue != null; - var hasHsl = hue != null || saturation != null || lightness != null; - if (hasRgb) { - if (hasHsl) { - throw SassScriptException( - "RGB parameters may not be passed along with HSL parameters."); - } - - return color.changeRgb( - red: (color.red + (red ?? 0)).clamp(0, 255) as int, - green: (color.green + (green ?? 0)).clamp(0, 255) as int, - blue: (color.blue + (blue ?? 0)).clamp(0, 255) as int, - alpha: (color.alpha + (alpha ?? 0)).clamp(0, 1)); - } else if (hasHsl) { - return color.changeHsl( - hue: color.hue + (hue ?? 0), - saturation: (color.saturation + (saturation ?? 0)).clamp(0, 100), - lightness: (color.lightness + (lightness ?? 0)).clamp(0, 100), - alpha: (color.alpha + (alpha ?? 0)).clamp(0, 1)); - } else if (alpha != null) { - return color.changeAlpha((color.alpha + (alpha ?? 0)).clamp(0, 1)); - } else { - return color; - } -}); - -final _scale = _function("scale", r"$color, $kwargs...", (arguments) { - var color = arguments[0].assertColor("color"); - var argumentList = arguments[1] as SassArgumentList; - if (argumentList.asList.isNotEmpty) { - throw SassScriptException( - "Only one positional argument is allowed. All other arguments must " - "be passed by name."); - } - - var keywords = Map.of(argumentList.keywords); - num getScale(String name) { - var value = keywords.remove(name); - if (value == null) return null; - var number = value.assertNumber(name); - number.assertUnit("%", name); - return number.valueInRange(-100, 100, name) / 100; - } - - num scaleValue(num current, num scale, num max) { - if (scale == null) return current; - return current + (scale > 0 ? max - current : current) * scale; - } - - var red = getScale("red"); - var green = getScale("green"); - var blue = getScale("blue"); - var saturation = getScale("saturation"); - var lightness = getScale("lightness"); - var alpha = getScale("alpha"); - - if (keywords.isNotEmpty) { - throw SassScriptException( - "No ${pluralize('argument', keywords.length)} named " - "${toSentence(keywords.keys.map((name) => "\$$name"), 'or')}."); - } - - var hasRgb = red != null || green != null || blue != null; - var hasHsl = saturation != null || lightness != null; - if (hasRgb) { - if (hasHsl) { - throw SassScriptException( - "RGB parameters may not be passed along with HSL parameters."); - } - - return color.changeRgb( - red: fuzzyRound(scaleValue(color.red, red, 255)), - green: fuzzyRound(scaleValue(color.green, green, 255)), - blue: fuzzyRound(scaleValue(color.blue, blue, 255)), - alpha: scaleValue(color.alpha, alpha, 1)); - } else if (hasHsl) { - return color.changeHsl( - saturation: scaleValue(color.saturation, saturation, 100), - lightness: scaleValue(color.lightness, lightness, 100), - alpha: scaleValue(color.alpha, alpha, 1)); - } else if (alpha != null) { - return color.changeAlpha(scaleValue(color.alpha, alpha, 1)); - } else { - return color; - } -}); - -final _change = _function("change", r"$color, $kwargs...", (arguments) { - var color = arguments[0].assertColor("color"); - var argumentList = arguments[1] as SassArgumentList; - if (argumentList.asList.isNotEmpty) { - throw SassScriptException( - "Only one positional argument is allowed. All other arguments must " - "be passed by name."); - } - - var keywords = Map.of(argumentList.keywords); - num getInRange(String name, num min, num max) => - keywords.remove(name)?.assertNumber(name)?.valueInRange(min, max, name); - - var red = _fuzzyRoundOrNull(getInRange("red", 0, 255)); - var green = _fuzzyRoundOrNull(getInRange("green", 0, 255)); - var blue = _fuzzyRoundOrNull(getInRange("blue", 0, 255)); - var hue = keywords.remove("hue")?.assertNumber("hue")?.value; - var saturation = getInRange("saturation", 0, 100); - var lightness = getInRange("lightness", 0, 100); - var alpha = getInRange("alpha", 0, 1); - - if (keywords.isNotEmpty) { - throw SassScriptException( - "No ${pluralize('argument', keywords.length)} named " - "${toSentence(keywords.keys.map((name) => "\$$name"), 'or')}."); - } - - var hasRgb = red != null || green != null || blue != null; - var hasHsl = hue != null || saturation != null || lightness != null; - if (hasRgb) { - if (hasHsl) { - throw SassScriptException( - "RGB parameters may not be passed along with HSL parameters."); - } - - return color.changeRgb(red: red, green: green, blue: blue, alpha: alpha); - } else if (hasHsl) { - return color.changeHsl( - hue: hue, saturation: saturation, lightness: lightness, alpha: alpha); - } else if (alpha != null) { - return color.changeAlpha(alpha); - } else { - return color; - } -}); +final _change = _function("change", r"$color, $kwargs...", + (arguments) => _updateComponents(arguments, change: true)); final _ieHexStr = _function("ie-hex-str", r"$color", (arguments) { var color = arguments[0].assertColor("color"); @@ -566,6 +415,102 @@ final _ieHexStr = _function("ie-hex-str", r"$color", (arguments) { quotes: false); }); +/// Implementation for `color.change`, `color.adjust`, and `color.scale`. +/// +/// Exactly one of [change], [adjust], and [scale] must be true to determine +/// which function should be executed. +SassColor _updateComponents(List arguments, + {bool change = false, bool adjust = false, bool scale = false}) { + assert([change, adjust, scale].where((x) => x).length == 1); + + var color = arguments[0].assertColor("color"); + var argumentList = arguments[1] as SassArgumentList; + if (argumentList.asList.isNotEmpty) { + throw SassScriptException( + "Only one positional argument is allowed. All other arguments must " + "be passed by name."); + } + + var keywords = Map.of(argumentList.keywords); + + /// Gets and validates the parameter with [name] from keywords. + /// + /// [max] should be 255 for RGB channels, 1 for the alpha channel, and 100 + /// for saturation, lightness, whiteness, and blackness. + num getParam(String name, num max, {bool assertPercent = false}) { + var number = keywords.remove(name)?.assertNumber(name); + if (number == null) return null; + if (scale || assertPercent) number.assertUnit("%", name); + if (scale) max = 100; + return number.valueInRange(change ? 0 : -max, max, name); + } + + var alpha = getParam("alpha", 1); + var red = getParam("red", 255); + var green = getParam("green", 255); + var blue = getParam("blue", 255); + var hue = scale ? null : keywords.remove("hue")?.assertNumber("hue")?.value; + var saturation = getParam("saturation", 100); + var lightness = getParam("lightness", 100); + var whiteness = getParam("whiteness", 100, assertPercent: true); + var blackness = getParam("blackness", 100, assertPercent: true); + + if (keywords.isNotEmpty) { + throw SassScriptException( + "No ${pluralize('argument', keywords.length)} named " + "${toSentence(keywords.keys.map((name) => '\$$name'), 'or')}."); + } + + var hasRgb = red != null || green != null || blue != null; + var hasSL = saturation != null || lightness != null; + var hasWB = whiteness != null || blackness != null; + + if (hasRgb && (hasSL || hasWB || hue != null)) { + throw SassScriptException("RGB parameters may not be passed along with " + "${hasWB ? 'HWB' : 'HSL'} parameters."); + } + + if (hasSL && hasWB) { + throw SassScriptException( + "HSL parameters may not be passed along with HWB parameters."); + } + + /// Updates [current] based on [param], clamped within [max]. + num updateValue(num current, num param, num max) { + if (param == null) return current; + if (change) return param; + if (adjust) return (current + param).clamp(0, max); + return current + (param > 0 ? max - current : current) * (param / 100); + } + + int updateRgb(int current, num param) => + fuzzyRound(updateValue(current, param, 255)); + + if (hasRgb) { + return color.changeRgb( + red: updateRgb(color.red, red), + green: updateRgb(color.green, green), + blue: updateRgb(color.blue, blue), + alpha: updateValue(color.alpha, alpha, 1)); + } else if (hasWB) { + return color.changeHwb( + hue: change ? hue : color.hue + (hue ?? 0), + whiteness: updateValue(color.whiteness, whiteness, 100), + blackness: updateValue(color.blackness, blackness, 100), + alpha: updateValue(color.alpha, alpha, 1)); + } else if (hue != null || hasSL) { + return color.changeHsl( + hue: change ? hue : color.hue + (hue ?? 0), + saturation: updateValue(color.saturation, saturation, 100), + lightness: updateValue(color.lightness, lightness, 100), + alpha: updateValue(color.alpha, alpha, 1)); + } else if (alpha != null) { + return color.changeAlpha(updateValue(color.alpha, alpha, 1)); + } else { + return color; + } +} + /// Returns a string representation of [name] called with [arguments], as though /// it were a plain CSS function. SassString _functionString(String name, Iterable arguments) => @@ -820,9 +765,6 @@ SassColor _transparentize(List arguments) { (color.alpha - amount.valueInRange(0, 1, "amount")).clamp(0, 1)); } -/// Like [fuzzyRound], but returns `null` if [number] is `null`. -int _fuzzyRoundOrNull(num number) => number == null ? null : fuzzyRound(number); - /// Like [new BuiltInCallable.function], but always sets the URL to /// `sass:color`. BuiltInCallable _function(