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); + }); + }); }