From 3cac435ea5efd7d4ff17d9a366d3cb317e959000 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 10 Oct 2023 16:40:14 -0700 Subject: [PATCH 01/18] Never gamut-map during color space conversions --- lib/src/value/color/space/srgb.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/value/color/space/srgb.dart b/lib/src/value/color/space/srgb.dart index 677456818..2b35a7c6d 100644 --- a/lib/src/value/color/space/srgb.dart +++ b/lib/src/value/color/space/srgb.dart @@ -31,12 +31,6 @@ class SrgbColorSpace extends ColorSpace { ColorSpace dest, double red, double green, double blue, double alpha) { switch (dest) { case ColorSpace.hsl || ColorSpace.hwb: - if (fuzzyCheckRange(red, 0, 1) == null || - fuzzyCheckRange(green, 0, 1) == null || - fuzzyCheckRange(blue, 0, 1) == null) { - return SassColor.srgb(red, green, blue).toGamut().toSpace(dest); - } - // Algorithm from https://en.wikipedia.org/wiki/HSL_and_HSV#RGB_to_HSL_and_HSV var max = math.max(math.max(red, green), blue); var min = math.min(math.min(red, green), blue); From 3252cde98ee8b9d096b32560e0bfd5139f68c3ce Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 17 Oct 2023 17:26:40 -0700 Subject: [PATCH 02/18] Emit the correct space for color.to-gamut() --- lib/src/functions/color.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 472006e6b..241bf1a53 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -459,7 +459,7 @@ final module = BuiltInModule("color", functions: [ ? ColorSpace.srgb : space) .toGamut() - .toSpace(space); + .toSpace(color.space); }), _function("channel", r"$color, $channel, $space: null", (arguments) { From 341d20aa6ab0ec6d994fa16b97c2c9f49bdf7521 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 17 Oct 2023 17:26:51 -0700 Subject: [PATCH 03/18] Don't emit powerless components for legacy colors in color.to-space --- lib/src/functions/color.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 241bf1a53..9b780243f 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -423,11 +423,20 @@ final module = BuiltInModule("color", functions: [ (arguments) => SassString(arguments.first.assertColor("color").space.name, quotes: false)), - _function( - "to-space", - r"$color, $space", - (arguments) => - _colorInSpace(arguments[0], arguments[1].assertString("space"))), + _function("to-space", r"$color, $space", (arguments) { + var converted = _colorInSpace(arguments[0], arguments[1]); + // `color.to-space()` never returns missing channels for legacy color + // spaces because they're less compatible and users are probably using a + // legacy space because they want a highly compatible color. + return converted.isLegacy && + (converted.isChannel0Missing || + converted.isChannel1Missing || + converted.isChannel2Missing || + converted.isAlphaMissing) + ? SassColor.forSpaceInternal(converted.space, converted.channel0, + converted.channel1, converted.channel2, converted.alpha) + : converted; + }), _function("is-legacy", r"$color", (arguments) => SassBoolean(arguments[0].assertColor("color").isLegacy)), From 391182665dce6b844536c3394143666c80e6071d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 17 Oct 2023 18:03:57 -0700 Subject: [PATCH 04/18] Make Dart Errors from user-defined callbacks easier to debug --- lib/src/visitor/async_evaluate.dart | 29 ++++++++++++++------------- lib/src/visitor/evaluate.dart | 31 +++++++++++++++-------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index a380c9f39..58f7dbd12 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1728,13 +1728,7 @@ final class _EvaluateVisitor } on ArgumentError catch (error, stackTrace) { throwWithTrace(_exception(error.toString()), error, stackTrace); } catch (error, stackTrace) { - String? message; - try { - message = (error as dynamic).message as String; - } catch (_) { - message = error.toString(); - } - throwWithTrace(_exception(message), error, stackTrace); + throwWithTrace(_exception(_getErrorMessage(error)), error, stackTrace); } finally { _importSpan = null; } @@ -2995,13 +2989,8 @@ final class _EvaluateVisitor } on SassException { rethrow; } catch (error, stackTrace) { - String? message; - try { - message = (error as dynamic).message as String; - } catch (_) { - message = error.toString(); - } - throwWithTrace(_exception(message, nodeWithSpan.span), error, stackTrace); + throwWithTrace(_exception(_getErrorMessage(error), nodeWithSpan.span), + error, stackTrace); } _callableNode = oldCallableNode; @@ -3841,6 +3830,18 @@ final class _EvaluateVisitor stackTrace); } } + + /// Returns the best human-readable message for [error]. + String _getErrorMessage(Object error) { + // Built-in Dart Error objects often require their full toString()s for + // full context. + if (error is Error) return error.toString(); + try { + return (error as dynamic).message as String; + } catch (_) { + return error.toString(); + } + } } /// A helper class for [_EvaluateVisitor] that adds `@import`ed CSS nodes to the diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index b87658a68..b0256c07c 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 08ee05790c0d5cce3b190278723c52dadaee1701 +// Checksum: ba8431368d89d6d8b141fbcca43b0170409af9ba // // ignore_for_file: unused_import @@ -1725,13 +1725,7 @@ final class _EvaluateVisitor } on ArgumentError catch (error, stackTrace) { throwWithTrace(_exception(error.toString()), error, stackTrace); } catch (error, stackTrace) { - String? message; - try { - message = (error as dynamic).message as String; - } catch (_) { - message = error.toString(); - } - throwWithTrace(_exception(message), error, stackTrace); + throwWithTrace(_exception(_getErrorMessage(error)), error, stackTrace); } finally { _importSpan = null; } @@ -2971,13 +2965,8 @@ final class _EvaluateVisitor } on SassException { rethrow; } catch (error, stackTrace) { - String? message; - try { - message = (error as dynamic).message as String; - } catch (_) { - message = error.toString(); - } - throwWithTrace(_exception(message, nodeWithSpan.span), error, stackTrace); + throwWithTrace(_exception(_getErrorMessage(error), nodeWithSpan.span), + error, stackTrace); } _callableNode = oldCallableNode; @@ -3789,6 +3778,18 @@ final class _EvaluateVisitor stackTrace); } } + + /// Returns the best human-readable message for [error]. + String _getErrorMessage(Object error) { + // Built-in Dart Error objects often require their full toString()s for + // full context. + if (error is Error) return error.toString(); + try { + return (error as dynamic).message as String; + } catch (_) { + return error.toString(); + } + } } /// A helper class for [_EvaluateVisitor] that adds `@import`ed CSS nodes to the From 0091d01cf3ea41c844416b1dacd1d7c99ecd8280 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 18 Oct 2023 17:00:59 -0700 Subject: [PATCH 05/18] Properly pass missing channels through color conversions --- lib/src/value/color.dart | 3 +- lib/src/value/color/space.dart | 97 ++++++++++++++------- lib/src/value/color/space/a98_rgb.dart | 2 +- lib/src/value/color/space/display_p3.dart | 2 +- lib/src/value/color/space/hsl.dart | 20 +++-- lib/src/value/color/space/hwb.dart | 18 ++-- lib/src/value/color/space/lab.dart | 27 ++++-- lib/src/value/color/space/lch.dart | 19 ++-- lib/src/value/color/space/lms.dart | 48 ++++++---- lib/src/value/color/space/oklab.dart | 26 ++++-- lib/src/value/color/space/oklch.dart | 19 ++-- lib/src/value/color/space/prophoto_rgb.dart | 2 +- lib/src/value/color/space/rec2020.dart | 2 +- lib/src/value/color/space/rgb.dart | 13 ++- lib/src/value/color/space/srgb.dart | 36 +++++--- lib/src/value/color/space/srgb_linear.dart | 13 +-- lib/src/value/color/space/utils.dart | 22 +++-- lib/src/value/color/space/xyz_d50.dart | 32 +++++-- lib/src/value/color/space/xyz_d65.dart | 2 +- 19 files changed, 273 insertions(+), 130 deletions(-) diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 392c1aff8..36d5ab8fc 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -694,7 +694,8 @@ class SassColor extends Value { /// automatic conversions. SassColor toSpace(ColorSpace space) => this.space == space ? this - : this.space.convert(space, channel0, channel1, channel2, alpha); + : this.space.convert( + space, channel0OrNull, channel1OrNull, channel2OrNull, alpha); /// Returns a copy of this color that's in-gamut in the current color space. SassColor toGamut() { diff --git a/lib/src/value/color/space.dart b/lib/src/value/color/space.dart index 882f5be91..2127a200e 100644 --- a/lib/src/value/color/space.dart +++ b/lib/src/value/color/space.dart @@ -29,7 +29,7 @@ import 'space/xyz_d65.dart'; /// /// {@category Value} @sealed -abstract class ColorSpace { +abstract base class ColorSpace { /// The legacy RGB color space. static const ColorSpace rgb = RgbColorSpace(); @@ -178,48 +178,83 @@ abstract class ColorSpace { /// /// @nodoc @internal - SassColor convert(ColorSpace dest, double channel0, double channel1, - double channel2, double alpha) { + SassColor convert(ColorSpace dest, double? channel0, double? channel1, + double? channel2, double? alpha) => + convertLinear(dest, channel0, channel1, channel2, alpha); + + /// The default implementation of [convert], which always starts with a linear + /// transformation from RGB or XYZ channels to a linear destination space, + /// which may then further convert to a polar space. + /// + /// @nodoc + @internal + @protected + @nonVirtual + SassColor convertLinear( + ColorSpace dest, double? red, double? green, double? blue, double? alpha, + {bool missingLightness = false, + bool missingChroma = false, + bool missingHue = false, + bool missingA = false, + bool missingB = false}) { var linearDest = switch (dest) { - ColorSpace.hsl || ColorSpace.hwb => ColorSpace.srgb, - ColorSpace.lab || ColorSpace.lch => ColorSpace.xyzD50, - ColorSpace.oklab || ColorSpace.oklch => ColorSpace.lms, + ColorSpace.hsl || ColorSpace.hwb => const SrgbColorSpace(), + ColorSpace.lab || ColorSpace.lch => const XyzD50ColorSpace(), + ColorSpace.oklab || ColorSpace.oklch => const LmsColorSpace(), _ => dest }; - double transformed0; - double transformed1; - double transformed2; + double? transformedRed; + double? transformedGreen; + double? transformedBlue; if (linearDest == this) { - transformed0 = channel0; - transformed1 = channel1; - transformed2 = channel2; + transformedRed = red; + transformedGreen = green; + transformedBlue = blue; } else { - var linear0 = toLinear(channel0); - var linear1 = toLinear(channel1); - var linear2 = toLinear(channel2); + var linearRed = toLinear(red ?? 0); + var linearGreen = toLinear(green ?? 0); + var linearBlue = toLinear(blue ?? 0); var matrix = transformationMatrix(linearDest); - // (matrix * [linear0, linear1, linear2]).map(linearDest.fromLinear) - transformed0 = linearDest.fromLinear( - matrix[0] * linear0 + matrix[1] * linear1 + matrix[2] * linear2); - transformed1 = linearDest.fromLinear( - matrix[3] * linear0 + matrix[4] * linear1 + matrix[5] * linear2); - transformed2 = linearDest.fromLinear( - matrix[6] * linear0 + matrix[7] * linear1 + matrix[8] * linear2); + // (matrix * [linearRed, linearGreen, linearBlue]).map(linearDest.fromLinear) + transformedRed = linearDest.fromLinear(matrix[0] * linearRed + + matrix[1] * linearGreen + + matrix[2] * linearBlue); + transformedGreen = linearDest.fromLinear(matrix[3] * linearRed + + matrix[4] * linearGreen + + matrix[5] * linearBlue); + transformedBlue = linearDest.fromLinear(matrix[6] * linearRed + + matrix[7] * linearGreen + + matrix[8] * linearBlue); } return switch (dest) { - ColorSpace.hsl || - ColorSpace.hwb || - ColorSpace.lab || - ColorSpace.lch || - ColorSpace.oklab || - ColorSpace.oklch => - linearDest.convert( - dest, transformed0, transformed1, transformed2, alpha), + ColorSpace.hsl || ColorSpace.hwb => const SrgbColorSpace().convert( + dest, transformedRed, transformedGreen, transformedBlue, alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue), + ColorSpace.lab || ColorSpace.lch => const XyzD50ColorSpace().convert( + dest, transformedRed, transformedGreen, transformedBlue, alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue, + missingA: missingA, + missingB: missingB), + ColorSpace.oklab || ColorSpace.oklch => const LmsColorSpace().convert( + dest, transformedRed, transformedGreen, transformedBlue, alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue, + missingA: missingA, + missingB: missingB), _ => SassColor.forSpaceInternal( - dest, transformed0, transformed1, transformed2, alpha) + dest, + red == null ? null : transformedRed, + green == null ? null : transformedGreen, + blue == null ? null : transformedBlue, + alpha) }; } diff --git a/lib/src/value/color/space/a98_rgb.dart b/lib/src/value/color/space/a98_rgb.dart index 940ea8c94..df61a6d50 100644 --- a/lib/src/value/color/space/a98_rgb.dart +++ b/lib/src/value/color/space/a98_rgb.dart @@ -17,7 +17,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class A98RgbColorSpace extends ColorSpace { +final class A98RgbColorSpace extends ColorSpace { bool get isBoundedInternal => true; const A98RgbColorSpace() : super('a98-rgb', rgbChannels); diff --git a/lib/src/value/color/space/display_p3.dart b/lib/src/value/color/space/display_p3.dart index 2cff9f830..b1c56df5d 100644 --- a/lib/src/value/color/space/display_p3.dart +++ b/lib/src/value/color/space/display_p3.dart @@ -16,7 +16,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class DisplayP3ColorSpace extends ColorSpace { +final class DisplayP3ColorSpace extends ColorSpace { bool get isBoundedInternal => true; const DisplayP3ColorSpace() : super('display-p3', rgbChannels); diff --git a/lib/src/value/color/space/hsl.dart b/lib/src/value/color/space/hsl.dart index ac357ebd9..94bfba84d 100644 --- a/lib/src/value/color/space/hsl.dart +++ b/lib/src/value/color/space/hsl.dart @@ -7,13 +7,14 @@ import 'package:meta/meta.dart'; import '../../color.dart'; +import 'srgb.dart'; import 'utils.dart'; /// The legacy HSL color space. /// /// @nodoc @internal -class HslColorSpace extends ColorSpace { +final class HslColorSpace extends ColorSpace { bool get isBoundedInternal => true; bool get isStrictlyBoundedInternal => true; bool get isLegacyInternal => true; @@ -26,12 +27,12 @@ class HslColorSpace extends ColorSpace { LinearChannel('lightness', 0, 100, requiresPercent: true) ]); - SassColor convert(ColorSpace dest, double hue, double saturation, - double lightness, double alpha) { + SassColor convert(ColorSpace dest, double? hue, double? saturation, + double? lightness, double? alpha) { // Algorithm from the CSS3 spec: https://www.w3.org/TR/css3-color/#hsl-color. - var scaledHue = (hue / 360) % 1; - var scaledSaturation = saturation / 100; - var scaledLightness = lightness / 100; + var scaledHue = ((hue ?? 0) / 360) % 1; + var scaledSaturation = (saturation ?? 0) / 100; + var scaledLightness = (lightness ?? 0) / 100; var m2 = scaledLightness <= 0.5 ? scaledLightness * (scaledSaturation + 1) @@ -40,11 +41,14 @@ class HslColorSpace extends ColorSpace { scaledLightness * scaledSaturation; var m1 = scaledLightness * 2 - m2; - return ColorSpace.srgb.convert( + return const SrgbColorSpace().convert( dest, hueToRgb(m1, m2, scaledHue + 1 / 3), hueToRgb(m1, m2, scaledHue), hueToRgb(m1, m2, scaledHue - 1 / 3), - alpha); + alpha, + missingLightness: lightness == null, + missingChroma: saturation == null, + missingHue: hue == null); } } diff --git a/lib/src/value/color/space/hwb.dart b/lib/src/value/color/space/hwb.dart index be15226c8..9f0ecd131 100644 --- a/lib/src/value/color/space/hwb.dart +++ b/lib/src/value/color/space/hwb.dart @@ -7,13 +7,14 @@ import 'package:meta/meta.dart'; import '../../color.dart'; +import 'srgb.dart'; import 'utils.dart'; /// The legacy HWB color space. /// /// @nodoc @internal -class HwbColorSpace extends ColorSpace { +final class HwbColorSpace extends ColorSpace { bool get isBoundedInternal => true; bool get isStrictlyBoundedInternal => true; bool get isLegacyInternal => true; @@ -26,12 +27,12 @@ class HwbColorSpace extends ColorSpace { LinearChannel('blackness', 0, 100, requiresPercent: true) ]); - SassColor convert(ColorSpace dest, double hue, double whiteness, - double blackness, double alpha) { + SassColor convert(ColorSpace dest, double? hue, double? whiteness, + double? blackness, double? alpha) { // From https://www.w3.org/TR/css-color-4/#hwb-to-rgb - var scaledHue = hue % 360 / 360; - var scaledWhiteness = whiteness / 100; - var scaledBlackness = blackness / 100; + var scaledHue = (hue ?? 0) % 360 / 360; + var scaledWhiteness = (whiteness ?? 0) / 100; + var scaledBlackness = (blackness ?? 0) / 100; var sum = scaledWhiteness + scaledBlackness; if (sum > 1) { @@ -44,7 +45,8 @@ class HwbColorSpace extends ColorSpace { // Non-null because an in-gamut HSL color is guaranteed to be in-gamut for // HWB as well. - return ColorSpace.srgb.convert(dest, toRgb(scaledHue + 1 / 3), - toRgb(scaledHue), toRgb(scaledHue - 1 / 3), alpha); + return const SrgbColorSpace().convert(dest, toRgb(scaledHue + 1 / 3), + toRgb(scaledHue), toRgb(scaledHue - 1 / 3), alpha, + missingHue: hue == null); } } diff --git a/lib/src/value/color/space/lab.dart b/lib/src/value/color/space/lab.dart index bd7b18792..58a94bd68 100644 --- a/lib/src/value/color/space/lab.dart +++ b/lib/src/value/color/space/lab.dart @@ -12,6 +12,7 @@ import '../../../util/number.dart'; import '../../color.dart'; import '../conversions.dart'; import 'utils.dart'; +import 'xyz_d50.dart'; /// The Lab color space. /// @@ -19,7 +20,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class LabColorSpace extends ColorSpace { +final class LabColorSpace extends ColorSpace { bool get isBoundedInternal => false; const LabColorSpace() @@ -30,30 +31,38 @@ class LabColorSpace extends ColorSpace { ]); SassColor convert( - ColorSpace dest, double lightness, double a, double b, double alpha) { + ColorSpace dest, double? lightness, double? a, double? b, double? alpha, + {bool missingChroma = false, bool missingHue = false}) { switch (dest) { case ColorSpace.lab: - var powerlessAB = fuzzyEquals(lightness, 0); - return SassColor.lab( - lightness, powerlessAB ? null : a, powerlessAB ? null : b, alpha); + var powerlessAB = lightness == null || fuzzyEquals(lightness, 0); + return SassColor.lab(lightness, a == null || powerlessAB ? null : a, + b == null || powerlessAB ? null : b, alpha); case ColorSpace.lch: return labToLch(dest, lightness, a, b, alpha); default: + var missingLightness = lightness == null; + lightness ??= 0; // Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code // and http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html var f1 = (lightness + 16) / 116; - return ColorSpace.xyzD50.convert( + return const XyzD50ColorSpace().convert( dest, - _convertFToXorZ(a / 500 + f1) * d50[0], + _convertFToXorZ((a ?? 0) / 500 + f1) * d50[0], (lightness > labKappa * labEpsilon ? math.pow((lightness + 16) / 116, 3) * 1.0 : lightness / labKappa) * d50[1], - _convertFToXorZ(f1 - b / 200) * d50[2], - alpha); + _convertFToXorZ(f1 - (b ?? 0) / 200) * d50[2], + alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue, + missingA: a == null, + missingB: b == null); } } diff --git a/lib/src/value/color/space/lch.dart b/lib/src/value/color/space/lch.dart index c66baab64..47c5848bb 100644 --- a/lib/src/value/color/space/lch.dart +++ b/lib/src/value/color/space/lch.dart @@ -9,6 +9,7 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; import '../../color.dart'; +import 'lab.dart'; import 'utils.dart'; /// The LCH color space. @@ -17,7 +18,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class LchColorSpace extends ColorSpace { +final class LchColorSpace extends ColorSpace { bool get isBoundedInternal => false; bool get isPolarInternal => true; @@ -28,10 +29,16 @@ class LchColorSpace extends ColorSpace { hueChannel ]); - SassColor convert(ColorSpace dest, double lightness, double chroma, - double hue, double alpha) { - var hueRadians = hue * math.pi / 180; - return ColorSpace.lab.convert(dest, lightness, - chroma * math.cos(hueRadians), chroma * math.sin(hueRadians), alpha); + SassColor convert(ColorSpace dest, double? lightness, double? chroma, + double? hue, double? alpha) { + var hueRadians = (hue ?? 0) * math.pi / 180; + return const LabColorSpace().convert( + dest, + lightness, + (chroma ?? 0) * math.cos(hueRadians), + (chroma ?? 0) * math.sin(hueRadians), + alpha, + missingChroma: chroma == null, + missingHue: hue == null); } } diff --git a/lib/src/value/color/space/lms.dart b/lib/src/value/color/space/lms.dart index 62596fa9b..675bbe74b 100644 --- a/lib/src/value/color/space/lms.dart +++ b/lib/src/value/color/space/lms.dart @@ -22,7 +22,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class LmsColorSpace extends ColorSpace { +final class LmsColorSpace extends ColorSpace { bool get isBoundedInternal => false; const LmsColorSpace() @@ -32,26 +32,31 @@ class LmsColorSpace extends ColorSpace { LinearChannel('short', 0, 1) ]); - SassColor convert( - ColorSpace dest, double long, double medium, double short, double alpha) { + SassColor convert(ColorSpace dest, double? long, double? medium, + double? short, double? alpha, + {bool missingLightness = false, + bool missingChroma = false, + bool missingHue = false, + bool missingA = false, + bool missingB = false}) { switch (dest) { case ColorSpace.oklab: // Algorithm from https://drafts.csswg.org/css-color-4/#color-conversion-code - var longScaled = _cubeRootPreservingSign(long); - var mediumScaled = _cubeRootPreservingSign(medium); - var shortScaled = _cubeRootPreservingSign(short); + var longScaled = _cubeRootPreservingSign(long ?? 0); + var mediumScaled = _cubeRootPreservingSign(medium ?? 0); + var shortScaled = _cubeRootPreservingSign(short ?? 0); var lightness = lmsToOklab[0] * longScaled + lmsToOklab[1] * mediumScaled + lmsToOklab[2] * shortScaled; return SassColor.oklab( - lightness, - fuzzyEquals(lightness, 0) + missingLightness ? null : lightness, + missingA || fuzzyEquals(lightness, 0) ? null : lmsToOklab[3] * longScaled + lmsToOklab[4] * mediumScaled + lmsToOklab[5] * shortScaled, - fuzzyEquals(lightness, 0) + missingB || fuzzyEquals(lightness, 0) ? null : lmsToOklab[6] * longScaled + lmsToOklab[7] * mediumScaled + @@ -62,24 +67,33 @@ class LmsColorSpace extends ColorSpace { // This is equivalent to converting to OKLab and then to OKLCH, but we // do it inline to avoid extra list allocations since we expect // conversions to and from OKLCH to be very common. - var longScaled = _cubeRootPreservingSign(long); - var mediumScaled = _cubeRootPreservingSign(medium); - var shortScaled = _cubeRootPreservingSign(short); + var longScaled = _cubeRootPreservingSign(long ?? 0); + var mediumScaled = _cubeRootPreservingSign(medium ?? 0); + var shortScaled = _cubeRootPreservingSign(short ?? 0); return labToLch( dest, - lmsToOklab[0] * longScaled + - lmsToOklab[1] * mediumScaled + - lmsToOklab[2] * shortScaled, + missingLightness + ? null + : lmsToOklab[0] * longScaled + + lmsToOklab[1] * mediumScaled + + lmsToOklab[2] * shortScaled, lmsToOklab[3] * longScaled + lmsToOklab[4] * mediumScaled + lmsToOklab[5] * shortScaled, lmsToOklab[6] * longScaled + lmsToOklab[7] * mediumScaled + lmsToOklab[8] * shortScaled, - alpha); + alpha, + missingChroma: missingChroma, + missingHue: missingHue); default: - return super.convert(dest, long, medium, short, alpha); + return super.convertLinear(dest, long, medium, short, alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue, + missingA: missingA, + missingB: missingB); } } diff --git a/lib/src/value/color/space/oklab.dart b/lib/src/value/color/space/oklab.dart index cf8951bbd..8af468e11 100644 --- a/lib/src/value/color/space/oklab.dart +++ b/lib/src/value/color/space/oklab.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; import '../../color.dart'; import '../conversions.dart'; +import 'lms.dart'; import 'utils.dart'; /// The OKLab color space. @@ -18,7 +19,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class OklabColorSpace extends ColorSpace { +final class OklabColorSpace extends ColorSpace { bool get isBoundedInternal => false; const OklabColorSpace() @@ -29,11 +30,21 @@ class OklabColorSpace extends ColorSpace { ]); SassColor convert( - ColorSpace dest, double lightness, double a, double b, double alpha) { - if (dest == ColorSpace.oklch) return labToLch(dest, lightness, a, b, alpha); + ColorSpace dest, double? lightness, double? a, double? b, double? alpha, + {bool missingChroma = false, bool missingHue = false}) { + if (dest == ColorSpace.oklch) { + return labToLch(dest, lightness, a, b, alpha, + missingChroma: missingChroma, missingHue: missingHue); + } + var missingLightness = lightness == null; + var missingA = a == null; + var missingB = b == null; + lightness ??= 0; + a ??= 0; + b ??= 0; // Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code - return ColorSpace.lms.convert( + return const LmsColorSpace().convert( dest, math.pow( oklabToLms[0] * lightness + @@ -53,6 +64,11 @@ class OklabColorSpace extends ColorSpace { oklabToLms[8] * b, 3) + 0.0, - alpha); + alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue, + missingA: missingA, + missingB: missingB); } } diff --git a/lib/src/value/color/space/oklch.dart b/lib/src/value/color/space/oklch.dart index 30887f6e7..61ed5e55d 100644 --- a/lib/src/value/color/space/oklch.dart +++ b/lib/src/value/color/space/oklch.dart @@ -9,6 +9,7 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; import '../../color.dart'; +import 'oklab.dart'; import 'utils.dart'; /// The OKLCH color space. @@ -17,7 +18,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class OklchColorSpace extends ColorSpace { +final class OklchColorSpace extends ColorSpace { bool get isBoundedInternal => false; bool get isPolarInternal => true; @@ -28,10 +29,16 @@ class OklchColorSpace extends ColorSpace { hueChannel ]); - SassColor convert(ColorSpace dest, double lightness, double chroma, - double hue, double alpha) { - var hueRadians = hue * math.pi / 180; - return ColorSpace.oklab.convert(dest, lightness, - chroma * math.cos(hueRadians), chroma * math.sin(hueRadians), alpha); + SassColor convert(ColorSpace dest, double? lightness, double? chroma, + double? hue, double? alpha) { + var hueRadians = (hue ?? 0) * math.pi / 180; + return const OklabColorSpace().convert( + dest, + lightness, + (chroma ?? 0) * math.cos(hueRadians), + (chroma ?? 0) * math.sin(hueRadians), + alpha, + missingChroma: chroma == null, + missingHue: hue == null); } } diff --git a/lib/src/value/color/space/prophoto_rgb.dart b/lib/src/value/color/space/prophoto_rgb.dart index 5fcddafdf..0de23ada9 100644 --- a/lib/src/value/color/space/prophoto_rgb.dart +++ b/lib/src/value/color/space/prophoto_rgb.dart @@ -17,7 +17,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class ProphotoRgbColorSpace extends ColorSpace { +final class ProphotoRgbColorSpace extends ColorSpace { bool get isBoundedInternal => true; const ProphotoRgbColorSpace() : super('prophoto-rgb', rgbChannels); diff --git a/lib/src/value/color/space/rec2020.dart b/lib/src/value/color/space/rec2020.dart index 414977cab..ca5dcf0e5 100644 --- a/lib/src/value/color/space/rec2020.dart +++ b/lib/src/value/color/space/rec2020.dart @@ -23,7 +23,7 @@ const _beta = 0.018053968510807; /// /// @nodoc @internal -class Rec2020ColorSpace extends ColorSpace { +final class Rec2020ColorSpace extends ColorSpace { bool get isBoundedInternal => true; const Rec2020ColorSpace() : super('rec2020', rgbChannels); diff --git a/lib/src/value/color/space/rgb.dart b/lib/src/value/color/space/rgb.dart index ca2ba3187..9933f9cea 100644 --- a/lib/src/value/color/space/rgb.dart +++ b/lib/src/value/color/space/rgb.dart @@ -13,7 +13,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class RgbColorSpace extends ColorSpace { +final class RgbColorSpace extends ColorSpace { bool get isBoundedInternal => true; bool get isLegacyInternal => true; @@ -24,9 +24,14 @@ class RgbColorSpace extends ColorSpace { LinearChannel('blue', 0, 255) ]); - SassColor convert(ColorSpace dest, double red, double green, double blue, - double alpha) => - ColorSpace.srgb.convert(dest, red / 255, green / 255, blue / 255, alpha); + SassColor convert(ColorSpace dest, double? red, double? green, double? blue, + double? alpha) => + ColorSpace.srgb.convert( + dest, + red == null ? null : red / 255, + green == null ? null : green / 255, + blue == null ? null : blue / 255, + alpha); @protected double toLinear(double channel) => srgbAndDisplayP3ToLinear(channel / 255); diff --git a/lib/src/value/color/space/srgb.dart b/lib/src/value/color/space/srgb.dart index 2b35a7c6d..7c107e68b 100644 --- a/lib/src/value/color/space/srgb.dart +++ b/lib/src/value/color/space/srgb.dart @@ -22,15 +22,22 @@ import 'utils.dart'; /// /// @nodoc @internal -class SrgbColorSpace extends ColorSpace { +final class SrgbColorSpace extends ColorSpace { bool get isBoundedInternal => true; const SrgbColorSpace() : super('srgb', rgbChannels); SassColor convert( - ColorSpace dest, double red, double green, double blue, double alpha) { + ColorSpace dest, double? red, double? green, double? blue, double? alpha, + {bool missingLightness = false, + bool missingChroma = false, + bool missingHue = false}) { switch (dest) { case ColorSpace.hsl || ColorSpace.hwb: + red ??= 0; + green ??= 0; + blue ??= 0; + // Algorithm from https://en.wikipedia.org/wiki/HSL_and_HSV#RGB_to_HSL_and_HSV var max = math.max(math.max(red, green), blue); var min = math.min(math.min(red, green), blue); @@ -66,30 +73,39 @@ class SrgbColorSpace extends ColorSpace { return SassColor.forSpaceInternal( dest, - saturation == 0 || saturation == null ? null : hue, - saturation, - lightness, + missingHue || saturation == 0 || saturation == null ? null : hue, + missingChroma ? null : saturation, + missingLightness ? null : lightness, alpha); } else { var whiteness = fuzzyClamp(min * 100, 0, 100); var blackness = fuzzyClamp(100 - max * 100, 0, 100); return SassColor.forSpaceInternal( dest, - fuzzyEquals(whiteness + blackness, 100) ? null : hue, + missingHue || fuzzyEquals(whiteness + blackness, 100) + ? null + : hue, whiteness, blackness, alpha); } case ColorSpace.rgb: - return SassColor.rgb(red * 255, green * 255, blue * 255, alpha); + return SassColor.rgb( + red == null ? null : red * 255, + green == null ? null : green * 255, + blue == null ? null : blue * 255, + alpha); case ColorSpace.srgbLinear: - return SassColor.forSpaceInternal( - dest, toLinear(red), toLinear(green), toLinear(blue), alpha); + return SassColor.forSpaceInternal(dest, red.andThen(toLinear), + green.andThen(toLinear), blue.andThen(toLinear), alpha); default: - return super.convert(dest, red, green, blue, alpha); + return super.convertLinear(dest, red, green, blue, alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue); } } diff --git a/lib/src/value/color/space/srgb_linear.dart b/lib/src/value/color/space/srgb_linear.dart index b9a0fd13a..3e5151da4 100644 --- a/lib/src/value/color/space/srgb_linear.dart +++ b/lib/src/value/color/space/srgb_linear.dart @@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; +import '../../../util/nullable.dart'; import '../../color.dart'; import '../conversions.dart'; import 'utils.dart'; @@ -18,13 +19,13 @@ import 'utils.dart'; /// /// @nodoc @internal -class SrgbLinearColorSpace extends ColorSpace { +final class SrgbLinearColorSpace extends ColorSpace { bool get isBoundedInternal => true; const SrgbLinearColorSpace() : super('srgb-linear', rgbChannels); - SassColor convert(ColorSpace dest, double red, double green, double blue, - double alpha) => + SassColor convert(ColorSpace dest, double? red, double? green, double? blue, + double? alpha) => switch (dest) { ColorSpace.rgb || ColorSpace.hsl || @@ -32,9 +33,9 @@ class SrgbLinearColorSpace extends ColorSpace { ColorSpace.srgb => ColorSpace.srgb.convert( dest, - srgbAndDisplayP3FromLinear(red), - srgbAndDisplayP3FromLinear(green), - srgbAndDisplayP3FromLinear(blue), + red.andThen(srgbAndDisplayP3FromLinear), + green.andThen(srgbAndDisplayP3FromLinear), + blue.andThen(srgbAndDisplayP3FromLinear), alpha), _ => super.convert(dest, red, green, blue, alpha) }; diff --git a/lib/src/value/color/space/utils.dart b/lib/src/value/color/space/utils.dart index 427da3188..87dedc9c8 100644 --- a/lib/src/value/color/space/utils.dart +++ b/lib/src/value/color/space/utils.dart @@ -68,16 +68,26 @@ double srgbAndDisplayP3FromLinear(double channel) { } /// Converts a Lab or OKLab color to LCH or OKLCH, respectively. +/// +/// The [missingChroma] and [missingHue] arguments indicate whether this came +/// from a color that was missing its chroma or hue channels, respectively. SassColor labToLch( - ColorSpace dest, double lightness, double a, double b, double alpha) { + ColorSpace dest, double? lightness, double? a, double? b, double? alpha, + {bool missingChroma = false, bool missingHue = false}) { // Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code - if (fuzzyEquals(lightness, 0)) { + if (lightness == null || fuzzyEquals(lightness, 0)) { return SassColor.forSpaceInternal(dest, 0, null, null, alpha); } - var chroma = math.sqrt(math.pow(a, 2) + math.pow(b, 2)); - var hue = fuzzyEquals(chroma, 0) ? null : math.atan2(b, a) * 180 / math.pi; + var chroma = math.sqrt(math.pow(a ?? 0, 2) + math.pow(b ?? 0, 2)); + var hue = missingHue || fuzzyEquals(chroma, 0) + ? null + : math.atan2(b ?? 0, a ?? 0) * 180 / math.pi; - return SassColor.forSpaceInternal(dest, lightness, chroma, - hue == null || hue >= 0 ? hue : hue + 360, alpha); + return SassColor.forSpaceInternal( + dest, + lightness, + missingChroma ? null : chroma, + hue == null || hue >= 0 ? hue : hue + 360, + alpha); } diff --git a/lib/src/value/color/space/xyz_d50.dart b/lib/src/value/color/space/xyz_d50.dart index d135a85b1..fab064cb9 100644 --- a/lib/src/value/color/space/xyz_d50.dart +++ b/lib/src/value/color/space/xyz_d50.dart @@ -19,26 +19,42 @@ import 'utils.dart'; /// /// @nodoc @internal -class XyzD50ColorSpace extends ColorSpace { +final class XyzD50ColorSpace extends ColorSpace { bool get isBoundedInternal => false; const XyzD50ColorSpace() : super('xyz-d50', xyzChannels); SassColor convert( - ColorSpace dest, double x, double y, double z, double alpha) { + ColorSpace dest, double? x, double? y, double? z, double? alpha, + {bool missingLightness = false, + bool missingChroma = false, + bool missingHue = false, + bool missingA = false, + bool missingB = false}) { switch (dest) { case ColorSpace.lab || ColorSpace.lch: // Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code // and http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html - var f0 = _convertComponentToLabF(x / d50[0]); - var f1 = _convertComponentToLabF(y / d50[1]); - var f2 = _convertComponentToLabF(z / d50[2]); + var f0 = _convertComponentToLabF((x ?? 0) / d50[0]); + var f1 = _convertComponentToLabF((y ?? 0) / d50[1]); + var f2 = _convertComponentToLabF((z ?? 0) / d50[2]); + var lightness = missingLightness ? null : (116 * f1) - 16; + var a = 500 * (f0 - f1); + var b = 200 * (f1 - f2); - return ColorSpace.lab.convert( - dest, (116 * f1) - 16, 500 * (f0 - f1), 200 * (f1 - f2), alpha); + return dest == ColorSpace.lab + ? SassColor.lab( + lightness, missingA ? null : a, missingB ? null : b, alpha) + : labToLch(ColorSpace.lch, lightness, a, b, alpha, + missingChroma: missingChroma, missingHue: missingHue); default: - return super.convert(dest, x, y, z, alpha); + return super.convertLinear(dest, x, y, z, alpha, + missingLightness: missingLightness, + missingChroma: missingChroma, + missingHue: missingHue, + missingA: missingA, + missingB: missingB); } } diff --git a/lib/src/value/color/space/xyz_d65.dart b/lib/src/value/color/space/xyz_d65.dart index 979e7b126..4915a8cbc 100644 --- a/lib/src/value/color/space/xyz_d65.dart +++ b/lib/src/value/color/space/xyz_d65.dart @@ -16,7 +16,7 @@ import 'utils.dart'; /// /// @nodoc @internal -class XyzD65ColorSpace extends ColorSpace { +final class XyzD65ColorSpace extends ColorSpace { bool get isBoundedInternal => false; const XyzD65ColorSpace() : super('xyz', xyzChannels); From 1870b61ec9169bf993b5b7e833f1bdcde987bbc5 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 18 Oct 2023 17:57:10 -0700 Subject: [PATCH 06/18] Don't treat max/min lightness as making components powerless See sass/sass#3654 --- lib/src/value/color.dart | 19 +++---------------- lib/src/value/color/space/lms.dart | 4 ++-- lib/src/value/color/space/utils.dart | 4 ---- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 36d5ab8fc..b76e5d34f 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -71,7 +71,7 @@ class SassColor extends Value { /// @nodoc @internal bool get isChannel0Powerless => switch (space) { - ColorSpace.hsl => fuzzyEquals(channel1, 0) || fuzzyEquals(channel2, 0), + ColorSpace.hsl => fuzzyEquals(channel1, 0), ColorSpace.hwb => fuzzyEquals(channel1 + channel2, 100), _ => false }; @@ -109,15 +109,7 @@ class SassColor extends Value { /// /// @nodoc @internal - bool get isChannel1Powerless => switch (space) { - ColorSpace.hsl => fuzzyEquals(channel2, 0), - ColorSpace.lab || - ColorSpace.oklab || - ColorSpace.lch || - ColorSpace.oklch => - fuzzyEquals(channel0, 0) || fuzzyEquals(channel0, 100), - _ => false - }; + final bool isChannel1Powerless = false; /// This color's second channel. /// @@ -144,12 +136,7 @@ class SassColor extends Value { /// @nodoc @internal bool get isChannel2Powerless => switch (space) { - ColorSpace.lab || - ColorSpace.oklab => - fuzzyEquals(channel0, 0) || fuzzyEquals(channel0, 100), - ColorSpace.lch || ColorSpace.oklch => fuzzyEquals(channel0, 0) || - fuzzyEquals(channel0, 100) || - fuzzyEquals(channel1, 0), + ColorSpace.lch || ColorSpace.oklch => fuzzyEquals(channel1, 0), _ => false }; diff --git a/lib/src/value/color/space/lms.dart b/lib/src/value/color/space/lms.dart index 675bbe74b..c05b880b4 100644 --- a/lib/src/value/color/space/lms.dart +++ b/lib/src/value/color/space/lms.dart @@ -51,12 +51,12 @@ final class LmsColorSpace extends ColorSpace { return SassColor.oklab( missingLightness ? null : lightness, - missingA || fuzzyEquals(lightness, 0) + missingA ? null : lmsToOklab[3] * longScaled + lmsToOklab[4] * mediumScaled + lmsToOklab[5] * shortScaled, - missingB || fuzzyEquals(lightness, 0) + missingB ? null : lmsToOklab[6] * longScaled + lmsToOklab[7] * mediumScaled + diff --git a/lib/src/value/color/space/utils.dart b/lib/src/value/color/space/utils.dart index 87dedc9c8..d5ecf2eb4 100644 --- a/lib/src/value/color/space/utils.dart +++ b/lib/src/value/color/space/utils.dart @@ -75,10 +75,6 @@ SassColor labToLch( ColorSpace dest, double? lightness, double? a, double? b, double? alpha, {bool missingChroma = false, bool missingHue = false}) { // Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code - if (lightness == null || fuzzyEquals(lightness, 0)) { - return SassColor.forSpaceInternal(dest, 0, null, null, alpha); - } - var chroma = math.sqrt(math.pow(a ?? 0, 2) + math.pow(b ?? 0, 2)); var hue = missingHue || fuzzyEquals(chroma, 0) ? null From 930c18c7a19cb05ac6d2cb052589b8791b698ca1 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 19 Oct 2023 14:56:18 -0700 Subject: [PATCH 07/18] Return legacy colors with missing channels as-is from to-space() --- lib/src/functions/color.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 9b780243f..5c1c77283 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -432,7 +432,8 @@ final module = BuiltInModule("color", functions: [ (converted.isChannel0Missing || converted.isChannel1Missing || converted.isChannel2Missing || - converted.isAlphaMissing) + converted.isAlphaMissing) && + converted.space != (arguments[0] as SassColor).space ? SassColor.forSpaceInternal(converted.space, converted.channel0, converted.channel1, converted.channel2, converted.alpha) : converted; From 70b9abbab833d313525a91d9945eb2b5806ff34c Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 27 Mar 2024 12:31:27 -0700 Subject: [PATCH 08/18] [Color 4] Update behavior to match sass/sass#3819 --- lib/src/functions/color.dart | 77 ++++++----- lib/src/util/number.dart | 5 + lib/src/value/color.dart | 194 ++++++++++----------------- lib/src/value/color/channel.dart | 13 +- lib/src/value/color/space.dart | 19 +-- lib/src/value/color/space/hsl.dart | 7 +- lib/src/value/color/space/hwb.dart | 1 - lib/src/value/color/space/lab.dart | 3 +- lib/src/value/color/space/lch.dart | 5 +- lib/src/value/color/space/oklab.dart | 5 +- lib/src/value/color/space/oklch.dart | 7 +- lib/src/value/color/space/srgb.dart | 43 +++--- lib/src/visitor/serialize.dart | 43 ++++++ 13 files changed, 217 insertions(+), 205 deletions(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 5c1c77283..c607317c1 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'dart:collection'; +import 'dart:math' as math; import 'package:collection/collection.dart'; @@ -769,7 +770,8 @@ SassColor _changeColor( return alphaArg.value; } }) ?? - color.alpha); + color.alpha, + clamp: false); } /// Returns a copy of [color] with its channel values scaled by the values in @@ -862,10 +864,15 @@ double _adjustChannel(ColorSpace space, ColorChannel channel, double oldValue, adjustmentArg = SassNumber(adjustmentArg.value); } - var result = oldValue + _channelFromValue(channel, adjustmentArg)!; - return space.isStrictlyBounded && channel is LinearChannel - ? fuzzyClamp(result, channel.min, channel.max) - : result; + var result = + oldValue + _channelFromValue(channel, adjustmentArg, clamp: false)!; + return switch (channel) { + LinearChannel(lowerClamped: true, :var min) when result < min => + oldValue < min ? math.max(oldValue, result) : min, + LinearChannel(upperClamped: true, :var max) when result > max => + oldValue > max ? math.min(oldValue, result) : max, + _ => result + }; } /// Given a map of arguments passed to [_updateComponents] for a legacy color, @@ -1321,24 +1328,28 @@ Value? _parseNumberOrNone(String text) { /// Creates a [SassColor] for the given [space] from the given channel values, /// or throws a [SassScriptException] if the channel values are invalid. +/// +/// If [clamp] is true, this will clamp any clamped channels. SassColor _colorFromChannels(ColorSpace space, SassNumber? channel0, SassNumber? channel1, SassNumber? channel2, double? alpha, - {bool fromRgbFunction = false}) { + {bool clamp = true, bool fromRgbFunction = false}) { switch (space) { case ColorSpace.hsl: if (channel1 != null) _checkPercent(channel1, 'saturation'); if (channel2 != null) _checkPercent(channel2, 'lightness'); return SassColor.hsl( channel0.andThen((channel0) => _angleValue(channel0, 'hue')), - channel1?.value.clamp(0, 100).toDouble(), - channel2?.value.clamp(0, 100).toDouble(), + _channelFromValue(space.channels[1], _forcePercent(channel1), + clamp: clamp), + _channelFromValue(space.channels[2], _forcePercent(channel2), + clamp: clamp), alpha); case ColorSpace.hwb: channel1?.assertUnit('%', 'whiteness'); channel2?.assertUnit('%', 'blackness'); - var whiteness = channel1?.value.clamp(0, 100).toDouble(); - var blackness = channel2?.value.clamp(0, 100).toDouble(); + var whiteness = channel1?.value.toDouble(); + var blackness = channel2?.value.toDouble(); if (whiteness != null && blackness != null && @@ -1356,44 +1367,48 @@ SassColor _colorFromChannels(ColorSpace space, SassNumber? channel0, case ColorSpace.rgb: return SassColor.rgbInternal( - _channelFromValue(space.channels[0], channel0), - _channelFromValue(space.channels[1], channel1), - _channelFromValue(space.channels[2], channel2), + _channelFromValue(space.channels[0], channel0, clamp: clamp), + _channelFromValue(space.channels[1], channel1, clamp: clamp), + _channelFromValue(space.channels[2], channel2, clamp: clamp), alpha, fromRgbFunction ? ColorFormat.rgbFunction : null); - case ColorSpace.lab || - ColorSpace.lch || - ColorSpace.oklab || - ColorSpace.oklch: - return SassColor.forSpaceInternal( - space, - _channelFromValue(space.channels[0], channel0).andThen((lightness) => - fuzzyClamp( - lightness, 0, (space.channels[0] as LinearChannel).max)), - _channelFromValue(space.channels[1], channel1), - _channelFromValue(space.channels[2], channel2), - alpha); - default: return SassColor.forSpaceInternal( space, - _channelFromValue(space.channels[0], channel0), - _channelFromValue(space.channels[1], channel1), - _channelFromValue(space.channels[2], channel2), + _channelFromValue(space.channels[0], channel0, clamp: clamp), + _channelFromValue(space.channels[1], channel1, clamp: clamp), + _channelFromValue(space.channels[2], channel2, clamp: clamp), alpha); } } +/// Returns [number] with unit `'%'` regardless of its original unit. +SassNumber? _forcePercent(SassNumber? number) => switch (number) { + null => null, + SassNumber(numeratorUnits: ['%'], denominatorUnits: []) => number, + _ => SassNumber(number.value, '%') + }; + /// Converts a channel value from a [SassNumber] into a [double] according to /// [channel]. -double? _channelFromValue(ColorChannel channel, SassNumber? value) => +/// +/// If [clamp] is true, this clamps [value] according to [channel]'s clamping +/// rules. +double? _channelFromValue(ColorChannel channel, SassNumber? value, + {bool clamp = true}) => value.andThen((value) => switch (channel) { LinearChannel(requiresPercent: true) when !value.hasUnit('%') => throw SassScriptException( 'Expected $value to have unit "%".', channel.name), - LinearChannel() => + LinearChannel(lowerClamped: false, upperClamped: false) => + _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), _ => value.coerceValueToUnit('deg', channel.name) % 360 }); diff --git a/lib/src/util/number.dart b/lib/src/util/number.dart index 21c5e2680..83138e9d1 100644 --- a/lib/src/util/number.dart +++ b/lib/src/util/number.dart @@ -93,6 +93,11 @@ double fuzzyClamp(double number, double min, double max) { return number; } +/// Returns whether [number] is within [min] and [max] inclusive, using fuzzy +/// equality. +bool fuzzyInRange(double number, num min, num max) => + fuzzyGreaterThanOrEquals(number, min) && fuzzyLessThanOrEquals(number, max); + /// Returns [number] if it's within [min] and [max], or `null` if it's not. /// /// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index b76e5d34f..85721d790 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -196,23 +196,24 @@ class SassColor extends Value { /// Whether this color is in-gamut for its color space. bool get isInGamut { - // Strictly-bounded spaces can't even represent out-of-gamut colors, so - // any color that exists must be bounded. - if (!space.isBounded || space.isStrictlyBounded) return true; + if (!space.isBounded) return true; // There aren't (currently) any color spaces that are bounded but not // STRICTLY bounded, and have polar-angle channels. - var channel0Info = space.channels[0] as LinearChannel; - var channel1Info = space.channels[1] as LinearChannel; - var channel2Info = space.channels[2] as LinearChannel; - return fuzzyLessThanOrEquals(channel0, channel0Info.max) && - fuzzyGreaterThanOrEquals(channel0, channel0Info.min) && - fuzzyLessThanOrEquals(channel1, channel1Info.max) && - fuzzyGreaterThanOrEquals(channel1, channel1Info.min) && - fuzzyLessThanOrEquals(channel2, channel2Info.max) && - fuzzyGreaterThanOrEquals(channel2, channel2Info.min); + return _isChannelInGamut(channel0, space.channels[0]) && + _isChannelInGamut(channel1, space.channels[1]) && + _isChannelInGamut(channel2, space.channels[2]); } + /// Returns whether [value] is in-gamut for the given [channel]. + bool _isChannelInGamut(double value, ColorChannel channel) => + switch (channel) { + LinearChannel(:var min, :var max) => + fuzzyLessThanOrEquals(value, max) && + fuzzyGreaterThanOrEquals(value, min), + _ => true + }; + /// This color's red channel, between `0` and `255`. /// /// **Note:** This is rounded to the nearest integer, which may be lossy. Use @@ -272,8 +273,8 @@ class SassColor extends Value { @internal factory SassColor.rgbInternal(num? red, num? green, num? blue, [num? alpha = 1, ColorFormat? format]) => - SassColor.forSpaceInternal(ColorSpace.rgb, red?.toDouble(), - green?.toDouble(), blue?.toDouble(), alpha?.toDouble(), format); + SassColor._forSpace(ColorSpace.rgb, red?.toDouble(), green?.toDouble(), + blue?.toDouble(), alpha?.toDouble(), format); /// Creates a color in [ColorSpace.hsl]. /// @@ -286,14 +287,8 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.hsl(num? hue, num? saturation, num? lightness, [num? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.hsl, - _normalizeHue(hue?.toDouble()), - saturation.andThen((saturation) => - fuzzyAssertRange(saturation.toDouble(), 0, 100, "saturation")), - lightness.andThen((lightness) => - fuzzyAssertRange(lightness.toDouble(), 0, 100, "lightness")), - alpha?.toDouble()); + SassColor.forSpaceInternal(ColorSpace.hsl, hue?.toDouble(), + saturation?.toDouble(), lightness?.toDouble(), alpha?.toDouble()); /// Creates a color in [ColorSpace.hwb]. /// @@ -306,14 +301,8 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.hwb(num? hue, num? whiteness, num? blackness, [num? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.hwb, - _normalizeHue(hue?.toDouble()), - whiteness.andThen((whiteness) => - fuzzyAssertRange(whiteness.toDouble(), 0, 100, "whiteness")), - blackness.andThen((blackness) => - fuzzyAssertRange(blackness.toDouble(), 0, 100, "blackness")), - alpha?.toDouble()); + SassColor.forSpaceInternal(ColorSpace.hwb, hue?.toDouble(), + whiteness?.toDouble(), blackness?.toDouble(), alpha?.toDouble()); /// Creates a color in [ColorSpace.srgb]. /// @@ -326,7 +315,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.srgb(double? red, double? green, double? blue, [double? alpha = 1]) => - SassColor.forSpaceInternal(ColorSpace.srgb, red, green, blue, alpha); + SassColor._forSpace(ColorSpace.srgb, red, green, blue, alpha); /// Creates a color in [ColorSpace.srgbLinear]. /// @@ -339,8 +328,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.srgbLinear(double? red, double? green, double? blue, [double? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.srgbLinear, red, green, blue, alpha); + SassColor._forSpace(ColorSpace.srgbLinear, red, green, blue, alpha); /// Creates a color in [ColorSpace.displayP3]. /// @@ -353,7 +341,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.displayP3(double? red, double? green, double? blue, [double? alpha = 1]) => - SassColor.forSpaceInternal(ColorSpace.displayP3, red, green, blue, alpha); + SassColor._forSpace(ColorSpace.displayP3, red, green, blue, alpha); /// Creates a color in [ColorSpace.a98Rgb]. /// @@ -366,7 +354,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.a98Rgb(double? red, double? green, double? blue, [double? alpha = 1]) => - SassColor.forSpaceInternal(ColorSpace.a98Rgb, red, green, blue, alpha); + SassColor._forSpace(ColorSpace.a98Rgb, red, green, blue, alpha); /// Creates a color in [ColorSpace.prophotoRgb]. /// @@ -379,8 +367,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.prophotoRgb(double? red, double? green, double? blue, [double? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.prophotoRgb, red, green, blue, alpha); + SassColor._forSpace(ColorSpace.prophotoRgb, red, green, blue, alpha); /// Creates a color in [ColorSpace.rec2020]. /// @@ -393,7 +380,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.rec2020(double? red, double? green, double? blue, [double? alpha = 1]) => - SassColor.forSpaceInternal(ColorSpace.rec2020, red, green, blue, alpha); + SassColor._forSpace(ColorSpace.rec2020, red, green, blue, alpha); /// Creates a color in [ColorSpace.xyzD50]. /// @@ -406,7 +393,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.xyzD50(double? x, double? y, double? z, [double? alpha = 1]) => - SassColor.forSpaceInternal(ColorSpace.xyzD50, x, y, z, alpha); + SassColor._forSpace(ColorSpace.xyzD50, x, y, z, alpha); /// Creates a color in [ColorSpace.xyzD65]. /// @@ -419,7 +406,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.xyzD65(double? x, double? y, double? z, [double? alpha = 1]) => - SassColor.forSpaceInternal(ColorSpace.xyzD65, x, y, z, alpha); + SassColor._forSpace(ColorSpace.xyzD65, x, y, z, alpha); /// Creates a color in [ColorSpace.lab]. /// @@ -432,13 +419,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.lab(double? lightness, double? a, double? b, [double? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.lab, - lightness.andThen( - (lightness) => fuzzyAssertRange(lightness, 0, 100, "lightness")), - a, - b, - alpha); + SassColor._forSpace(ColorSpace.lab, lightness, a, b, alpha); /// Creates a color in [ColorSpace.lch]. /// @@ -451,13 +432,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.lch(double? lightness, double? chroma, double? hue, [double? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.lch, - lightness.andThen( - (lightness) => fuzzyAssertRange(lightness, 0, 100, "lightness")), - chroma, - _normalizeHue(hue), - alpha); + SassColor.forSpaceInternal(ColorSpace.lch, lightness, chroma, hue, alpha); /// Creates a color in [ColorSpace.oklab]. /// @@ -470,13 +445,7 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1`. factory SassColor.oklab(double? lightness, double? a, double? b, [double? alpha = 1]) => - SassColor.forSpaceInternal( - ColorSpace.oklab, - lightness.andThen( - (lightness) => fuzzyAssertRange(lightness, 0, 100, "lightness")), - a, - b, - alpha); + SassColor._forSpace(ColorSpace.oklab, lightness, a, b, alpha); /// Creates a color in [ColorSpace.oklch]. /// @@ -490,12 +459,7 @@ class SassColor extends Value { factory SassColor.oklch(double? lightness, double? chroma, double? hue, [double? alpha = 1]) => SassColor.forSpaceInternal( - ColorSpace.oklch, - lightness.andThen( - (lightness) => fuzzyAssertRange(lightness, 0, 100, "lightness")), - chroma, - _normalizeHue(hue), - alpha); + ColorSpace.oklch, lightness, chroma, hue, alpha); /// Creates a color in the color space named [space]. /// @@ -508,52 +472,50 @@ class SassColor extends Value { /// Throws a [RangeError] if [alpha] isn't between `0` and `1` or if /// [channels] is the wrong length for [space]. factory SassColor.forSpace(ColorSpace space, List channels, - [double? alpha = 1]) { - if (channels.length != space.channels.length) { - throw RangeError.value(channels.length, "channels.length", - 'must be exactly ${space.channels.length} for color space "$space"'); - } else { - var clampChannel0 = space.channels[0].name == "lightness"; - var clampChannel12 = space == ColorSpace.hsl || space == ColorSpace.hwb; - return SassColor.forSpaceInternal( - space, - clampChannel0 - ? channels[0].andThen((value) => fuzzyClamp( - value, 0, (space.channels[0] as LinearChannel).max)) - : channels[0], - clampChannel12 - ? channels[1].andThen((value) => fuzzyClamp(value, 0, 100)) - : channels[1], - clampChannel12 - ? channels[2].andThen((value) => fuzzyClamp(value, 0, 100)) - : channels[2], - alpha); - } - } + [double? alpha = 1]) => + channels.length == space.channels.length + ? SassColor.forSpaceInternal( + space, channels[0], channels[1], channels[2], alpha) + : throw RangeError.value(channels.length, "channels.length", + 'must be exactly ${space.channels.length} for color space "$space"'); /// Like [forSpace], but takes three channels explicitly rather than wrapping /// and unwrapping them in an array. /// /// @nodoc + factory SassColor.forSpaceInternal(ColorSpace space, double? channel0, + double? channel1, double? channel2, + [double? alpha = 1]) => + switch (space) { + ColorSpace.hsl => SassColor._forSpace( + space, + _normalizeHue(channel0, + invert: channel1 != null && fuzzyLessThan(channel1, 0)), + channel1?.abs(), + channel2, + alpha), + ColorSpace.hwb => SassColor._forSpace(space, + _normalizeHue(channel0, invert: false), channel1, channel2, alpha), + ColorSpace.lch || ColorSpace.oklch => SassColor._forSpace( + space, + channel0, + channel1?.abs(), + _normalizeHue(channel2, + invert: channel1 != null && fuzzyLessThan(channel1, 0)), + alpha), + _ => SassColor._forSpace(space, channel0, channel1, channel2, alpha) + }; + + /// Like [forSpaceInternal], but doesn't do _any_ pre-processing of any + /// channels. + /// + /// @nodoc @internal - SassColor.forSpaceInternal(this._space, this.channel0OrNull, - this.channel1OrNull, this.channel2OrNull, double? alpha, [this.format]) + SassColor._forSpace(this._space, this.channel0OrNull, this.channel1OrNull, + this.channel2OrNull, double? alpha, [this.format]) : alphaOrNull = alpha.andThen((alpha) => fuzzyAssertRange(alpha, 0, 1, "alpha")) { assert(format == null || _space == ColorSpace.rgb); - assert( - !(space == ColorSpace.hsl || space == ColorSpace.hwb) || - (fuzzyCheckRange(channel1, 0, 100) != null && - fuzzyCheckRange(channel2, 0, 100) != null), - "[BUG] Tried to create " - "$_space(${channel0OrNull ?? 'none'}, ${channel1OrNull ?? 'none'}, " - "${channel2OrNull ?? 'none'})"); - assert( - space.channels[0].name != "lightness" || - fuzzyCheckRange(channel0, 0, 100) != null, - "[BUG] Tried to create " - "$_space(${channel0OrNull ?? 'none'}, ${channel1OrNull ?? 'none'}, " - "${channel2OrNull ?? 'none'})"); assert(space != ColorSpace.lms); _checkChannel(channel0OrNull, space.channels[0].name); @@ -574,9 +536,11 @@ class SassColor extends Value { } /// If [hue] isn't null, normalizes it to the range `[0, 360)`. - static double? _normalizeHue(double? hue) { + /// + /// If [invert] is true, this returns the hue 180deg offset from the original value. + static double? _normalizeHue(double? hue, {required bool invert}) { if (hue == null) return hue; - return (hue % 360 + 360) % 360; + return (hue % 360 + 360 + (invert ? 180 : 0)) % 360; } /// @nodoc @@ -892,22 +856,8 @@ class SassColor extends Value { } } - return SassColor.forSpaceInternal( - this.space, - _clampChannelIfNecessary(new0, this.space, 0) ?? channel0OrNull, - _clampChannelIfNecessary(new1, this.space, 1) ?? channel1OrNull, - _clampChannelIfNecessary(new2, this.space, 2) ?? channel2OrNull, - alpha ?? alphaOrNull); - } - - /// If [space] is strictly bounded and its [index]th channel isn't polar, - /// clamps [value] between its minimum and maximum. - double? _clampChannelIfNecessary(double? value, ColorSpace space, int index) { - if (value == null) return value; - if (!space.isStrictlyBounded) return value; - var channel = space.channels[index]; - if (channel is! LinearChannel) return value; - return fuzzyClamp(value, channel.min, channel.max); + return SassColor.forSpaceInternal(this.space, new0 ?? channel0OrNull, + new1 ?? channel1OrNull, new2 ?? channel2OrNull, alpha ?? alphaOrNull); } /// Returns a color partway between [this] and [other] according to [method], diff --git a/lib/src/value/color/channel.dart b/lib/src/value/color/channel.dart index 61cd115cf..378de1cd9 100644 --- a/lib/src/value/color/channel.dart +++ b/lib/src/value/color/channel.dart @@ -77,6 +77,14 @@ class LinearChannel extends ColorChannel { /// forbids unitless values. final bool requiresPercent; + /// Whether the lower bound of this channel is clamped when the color is + /// created using the global function syntax. + final bool lowerClamped; + + /// Whether the upper bound of this channel is clamped when the color is + /// created using the global function syntax. + final bool upperClamped; + /// Creates a linear color channel. /// /// By default, [ColorChannel.associatedUnit] is set to `%` if and only if @@ -86,7 +94,10 @@ class LinearChannel extends ColorChannel { /// @nodoc @internal const LinearChannel(String name, this.min, this.max, - {this.requiresPercent = false, bool? conventionallyPercent}) + {this.requiresPercent = false, + this.lowerClamped = false, + this.upperClamped = false, + bool? conventionallyPercent}) : super(name, isPolarAngle: false, associatedUnit: (conventionallyPercent ?? (min == 0 && max == 100)) diff --git a/lib/src/value/color/space.dart b/lib/src/value/color/space.dart index 2127a200e..1d96ae705 100644 --- a/lib/src/value/color/space.dart +++ b/lib/src/value/color/space.dart @@ -121,12 +121,6 @@ abstract base class ColorSpace { @internal bool get isBoundedInternal; - /// See [SassApiColorSpace.isStrictlyBounded]. - /// - /// @nodoc - @internal - bool get isStrictlyBoundedInternal => false; - /// See [SassApiColorSpace.isLegacy]. /// /// @nodoc @@ -183,8 +177,8 @@ abstract base class ColorSpace { convertLinear(dest, channel0, channel1, channel2, alpha); /// The default implementation of [convert], which always starts with a linear - /// transformation from RGB or XYZ channels to a linear destination space, - /// which may then further convert to a polar space. + /// transformation from RGB or XYZ channels to a linear destination space, and + /// may then further convert to a polar space. /// /// @nodoc @internal @@ -321,15 +315,6 @@ extension SassApiColorSpace on ColorSpace { /// Whether this color space has a bounded gamut. bool get isBounded => isBoundedInternal; - /// Whether this color space is _strictly_ bounded. - /// - /// If this is `true`, channel values outside of their bounds are meaningless - /// and therefore forbidden, rather than being considered valid but - /// out-of-gamut. - /// - /// This is only `true` if [isBounded] is also `true`. - bool get isStrictlyBounded => isStrictlyBoundedInternal; - /// Whether this is a legacy color space. bool get isLegacy => isLegacyInternal; diff --git a/lib/src/value/color/space/hsl.dart b/lib/src/value/color/space/hsl.dart index 94bfba84d..94ec83553 100644 --- a/lib/src/value/color/space/hsl.dart +++ b/lib/src/value/color/space/hsl.dart @@ -16,15 +16,16 @@ import 'utils.dart'; @internal final class HslColorSpace extends ColorSpace { bool get isBoundedInternal => true; - bool get isStrictlyBoundedInternal => true; bool get isLegacyInternal => true; bool get isPolarInternal => true; const HslColorSpace() : super('hsl', const [ hueChannel, - LinearChannel('saturation', 0, 100, requiresPercent: true), - LinearChannel('lightness', 0, 100, requiresPercent: true) + LinearChannel('saturation', 0, 100, + requiresPercent: true, lowerClamped: true), + LinearChannel('lightness', 0, 100, + requiresPercent: true, lowerClamped: true, upperClamped: true) ]); SassColor convert(ColorSpace dest, double? hue, double? saturation, diff --git a/lib/src/value/color/space/hwb.dart b/lib/src/value/color/space/hwb.dart index 9f0ecd131..956768b19 100644 --- a/lib/src/value/color/space/hwb.dart +++ b/lib/src/value/color/space/hwb.dart @@ -16,7 +16,6 @@ import 'utils.dart'; @internal final class HwbColorSpace extends ColorSpace { bool get isBoundedInternal => true; - bool get isStrictlyBoundedInternal => true; bool get isLegacyInternal => true; bool get isPolarInternal => true; diff --git a/lib/src/value/color/space/lab.dart b/lib/src/value/color/space/lab.dart index 58a94bd68..7766e706d 100644 --- a/lib/src/value/color/space/lab.dart +++ b/lib/src/value/color/space/lab.dart @@ -25,7 +25,8 @@ final class LabColorSpace extends ColorSpace { const LabColorSpace() : super('lab', const [ - LinearChannel('lightness', 0, 100), + LinearChannel('lightness', 0, 100, + lowerClamped: true, upperClamped: true), LinearChannel('a', -125, 125), LinearChannel('b', -125, 125) ]); diff --git a/lib/src/value/color/space/lch.dart b/lib/src/value/color/space/lch.dart index 47c5848bb..095babeef 100644 --- a/lib/src/value/color/space/lch.dart +++ b/lib/src/value/color/space/lch.dart @@ -24,8 +24,9 @@ final class LchColorSpace extends ColorSpace { const LchColorSpace() : super('lch', const [ - LinearChannel('lightness', 0, 100), - LinearChannel('chroma', 0, 150), + LinearChannel('lightness', 0, 100, + lowerClamped: true, upperClamped: true), + LinearChannel('chroma', 0, 150, lowerClamped: true), hueChannel ]); diff --git a/lib/src/value/color/space/oklab.dart b/lib/src/value/color/space/oklab.dart index 8af468e11..c24806635 100644 --- a/lib/src/value/color/space/oklab.dart +++ b/lib/src/value/color/space/oklab.dart @@ -24,7 +24,10 @@ final class OklabColorSpace extends ColorSpace { const OklabColorSpace() : super('oklab', const [ - LinearChannel('lightness', 0, 1, conventionallyPercent: true), + LinearChannel('lightness', 0, 1, + conventionallyPercent: true, + lowerClamped: true, + upperClamped: true), LinearChannel('a', -0.4, 0.4), LinearChannel('b', -0.4, 0.4) ]); diff --git a/lib/src/value/color/space/oklch.dart b/lib/src/value/color/space/oklch.dart index 61ed5e55d..bdf7fea65 100644 --- a/lib/src/value/color/space/oklch.dart +++ b/lib/src/value/color/space/oklch.dart @@ -24,8 +24,11 @@ final class OklchColorSpace extends ColorSpace { const OklchColorSpace() : super('oklch', const [ - LinearChannel('lightness', 0, 1, conventionallyPercent: true), - LinearChannel('chroma', 0, 0.4), + LinearChannel('lightness', 0, 1, + conventionallyPercent: true, + lowerClamped: true, + upperClamped: true), + LinearChannel('chroma', 0, 0.4, lowerClamped: true), hueChannel ]); diff --git a/lib/src/value/color/space/srgb.dart b/lib/src/value/color/space/srgb.dart index 7c107e68b..963cdf733 100644 --- a/lib/src/value/color/space/srgb.dart +++ b/lib/src/value/color/space/srgb.dart @@ -38,53 +38,48 @@ final class SrgbColorSpace extends ColorSpace { green ??= 0; blue ??= 0; - // Algorithm from https://en.wikipedia.org/wiki/HSL_and_HSV#RGB_to_HSL_and_HSV + // Algorithm from https://drafts.csswg.org/css-color-4/#rgb-to-hsl var max = math.max(math.max(red, green), blue); var min = math.min(math.min(red, green), blue); var delta = max - min; - double? hue; + double hue; if (max == min) { hue = 0; } else if (max == red) { - hue = (60 * (green - blue) / delta) % 360; + hue = 60 * (green - blue) / delta + 360; } else if (max == green) { - hue = (120 + 60 * (blue - red) / delta) % 360; + hue = 60 * (blue - red) / delta + 120; } else { // max == blue - hue = (240 + 60 * (red - green) / delta) % 360; + hue = 60 * (red - green) / delta + 240; } if (dest == ColorSpace.hsl) { - var lightness = fuzzyClamp(50 * (max + min), 0, 100); - - double? saturation; - if (lightness == 0 || lightness == 100) { - saturation = null; - } else if (fuzzyEquals(max, min)) { - saturation = 0; - } else if (lightness < 50) { - saturation = 100 * delta / (max + min); - } else { - saturation = 100 * delta / (2 - max - min); + var lightness = (min + max) / 2; + + var saturation = lightness == 0 || lightness == 1 + ? 0.0 + : 100 * (max - lightness) / math.min(lightness, 1 - lightness); + if (saturation < 0) { + hue += 180; + saturation = saturation.abs(); } - saturation = saturation - .andThen((saturation) => fuzzyClamp(saturation, 0, 100)); return SassColor.forSpaceInternal( dest, - missingHue || saturation == 0 || saturation == null ? null : hue, + missingHue || fuzzyEquals(saturation, 0) ? null : hue % 360, missingChroma ? null : saturation, - missingLightness ? null : lightness, + missingLightness ? null : lightness * 100, alpha); } else { - var whiteness = fuzzyClamp(min * 100, 0, 100); - var blackness = fuzzyClamp(100 - max * 100, 0, 100); + var whiteness = min * 100; + var blackness = 100 - max * 100; return SassColor.forSpaceInternal( dest, - missingHue || fuzzyEquals(whiteness + blackness, 100) + missingHue || fuzzyGreaterThanOrEquals(whiteness + blackness, 100) ? null - : hue, + : hue % 360, whiteness, blackness, alpha); diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 62527112f..af9d547d7 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -589,6 +589,15 @@ final class _SerializeVisitor _maybeWriteSlashAlpha(value); _buffer.writeCharCode($rparen); + // case ColorSpace.lab || + // ColorSpace.oklab || + // ColorSpace.lch || + // ColorSpace.oklch when fuzzyInRange(value.channel0, 0, 100) && !value.isChannel1Missing && !value.isChannel2Missing: + // case ColorSpace.lch || + // ColorSpace.oklch when fuzzyLessThan(value.channel1, 0) && !value.isChannel0Missing && !value.isChannel1Missing: + // // color-mix() is currently more widely supported than relative color + // // syntax, so we use it to serialize out-of-gamut + case ColorSpace.lab || ColorSpace.oklab || ColorSpace.lch || @@ -643,6 +652,14 @@ final class _SerializeVisitor void _writeLegacyColor(SassColor color) { var opaque = fuzzyEquals(color.alpha, 1); + // Out-of-gamut colors can _only_ be represented accurately as HSL, because + // only HSL isn't clamped at parse time (except negative saturation which + // isn't necessary anyway). + if (!color.isInGamut && !_inspect) { + _writeHsl(color); + return; + } + // In compressed mode, emit colors in the shortest representation possible. if (_isCompressed) { var rgb = color.toSpace(ColorSpace.rgb); @@ -688,6 +705,9 @@ final class _SerializeVisitor if (color.space == ColorSpace.hsl) { _writeHsl(color); return; + } else if (_inspect && color.space == ColorSpace.hwb) { + _writeHwb(color); + return; } switch (color.format) { @@ -813,6 +833,29 @@ final class _SerializeVisitor _buffer.writeCharCode($rparen); } + /// Writes [value] as an `hwb()` function. + /// + /// This is only used in inspect mode, and so only supports the new color syntax. + void _writeHwb(SassColor color) { + var opaque = fuzzyEquals(color.alpha, 1); + _buffer.write("hwb("); + var hwb = color.toSpace(ColorSpace.hwb); + _writeNumber(hwb.channel('hue')); + _buffer.writeCharCode($space); + _writeNumber(hwb.channel('whiteness')); + _buffer.writeCharCode($percent); + _buffer.writeCharCode($space); + _writeNumber(hwb.channel('blackness')); + _buffer.writeCharCode($percent); + + if (!fuzzyEquals(color.alpha, 1)) { + _buffer.write(' / '); + _writeNumber(color.alpha); + } + + _buffer.writeCharCode($rparen); + } + /// Returns whether [color]'s hex pair representation is symmetrical (e.g. /// `FF`). bool _isSymmetricalHex(int color) => color & 0xF == color >> 4; From 7269a443a15456425dc44acaee2a532c3373d7c9 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 28 Mar 2024 14:10:43 -0700 Subject: [PATCH 09/18] Fix analysis issues --- lib/src/value/color.dart | 3 --- lib/src/value/color/channel.dart | 5 ++--- lib/src/value/color/space/lms.dart | 1 - lib/src/visitor/serialize.dart | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 85721d790..631aa83c6 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -508,9 +508,6 @@ class SassColor extends Value { /// Like [forSpaceInternal], but doesn't do _any_ pre-processing of any /// channels. - /// - /// @nodoc - @internal SassColor._forSpace(this._space, this.channel0OrNull, this.channel1OrNull, this.channel2OrNull, double? alpha, [this.format]) : alphaOrNull = diff --git a/lib/src/value/color/channel.dart b/lib/src/value/color/channel.dart index 378de1cd9..125d21390 100644 --- a/lib/src/value/color/channel.dart +++ b/lib/src/value/color/channel.dart @@ -93,13 +93,12 @@ class LinearChannel extends ColorChannel { /// /// @nodoc @internal - const LinearChannel(String name, this.min, this.max, + const LinearChannel(super.name, this.min, this.max, {this.requiresPercent = false, this.lowerClamped = false, this.upperClamped = false, bool? conventionallyPercent}) - : super(name, - isPolarAngle: false, + : super(isPolarAngle: false, associatedUnit: (conventionallyPercent ?? (min == 0 && max == 100)) ? '%' : null); diff --git a/lib/src/value/color/space/lms.dart b/lib/src/value/color/space/lms.dart index c05b880b4..0ea82eb01 100644 --- a/lib/src/value/color/space/lms.dart +++ b/lib/src/value/color/space/lms.dart @@ -9,7 +9,6 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; -import '../../../util/number.dart'; import '../../color.dart'; import '../conversions.dart'; import 'utils.dart'; diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 443ab0749..e40414314 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -856,7 +856,6 @@ final class _SerializeVisitor /// /// This is only used in inspect mode, and so only supports the new color syntax. void _writeHwb(SassColor color) { - var opaque = fuzzyEquals(color.alpha, 1); _buffer.write("hwb("); var hwb = color.toSpace(ColorSpace.hwb); _writeNumber(hwb.channel('hue')); From b70c96b611c8a0f2eb76230944134c37961e58c3 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 28 Mar 2024 16:52:58 -0700 Subject: [PATCH 10/18] Update HWB powerless definition --- lib/src/value/color.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 631aa83c6..f398e5077 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -72,7 +72,7 @@ class SassColor extends Value { @internal bool get isChannel0Powerless => switch (space) { ColorSpace.hsl => fuzzyEquals(channel1, 0), - ColorSpace.hwb => fuzzyEquals(channel1 + channel2, 100), + ColorSpace.hwb => fuzzyGreaterThanOrEquals(channel1 + channel2, 100), _ => false }; From e29122e4381f81494a025ee22594e85339a58b38 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 29 Mar 2024 13:55:40 -0700 Subject: [PATCH 11/18] Reformat --- lib/src/value/color/channel.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/value/color/channel.dart b/lib/src/value/color/channel.dart index 125d21390..eb07f8ff1 100644 --- a/lib/src/value/color/channel.dart +++ b/lib/src/value/color/channel.dart @@ -98,7 +98,8 @@ class LinearChannel extends ColorChannel { this.lowerClamped = false, this.upperClamped = false, bool? conventionallyPercent}) - : super(isPolarAngle: false, + : super( + isPolarAngle: false, associatedUnit: (conventionallyPercent ?? (min == 0 && max == 100)) ? '%' : null); From 2d44fad117c9500410342a8b37b10edbbcd35f5d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 29 Mar 2024 14:08:24 -0700 Subject: [PATCH 12/18] Fix embedded test --- test/embedded/function_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/embedded/function_test.dart b/test/embedded/function_test.dart index 158d51aa5..3b03c941b 100644 --- a/test/embedded/function_test.dart +++ b/test/embedded/function_test.dart @@ -968,17 +968,17 @@ void main() { test("with red above 255", () async { expect(await _deprotofy(_rgb(256, 0, 0, 1.0)), - equals('rgb(256, 0, 0)')); + equals('hsl(0, 100.7874015748%, 50.1960784314%)')); }); test("with green above 255", () async { expect(await _deprotofy(_rgb(0, 256, 0, 1.0)), - equals('rgb(0, 256, 0)')); + equals('hsl(120, 100.7874015748%, 50.1960784314%)')); }); test("with blue above 255", () async { expect(await _deprotofy(_rgb(0, 0, 256, 1.0)), - equals('rgb(0, 0, 256)')); + equals('hsl(240, 100.7874015748%, 50.1960784314%)')); }); }); From 60d1561f03052e659c82d4e45565a32b13744c7d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 29 Mar 2024 14:53:51 -0700 Subject: [PATCH 13/18] Poke CI From 975fc867d2a441879bdf8694c3267542e5b0c8fa Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 29 Mar 2024 17:35:41 -0700 Subject: [PATCH 14/18] Don't clamp HSL lightness --- lib/src/value/color/space/hsl.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/value/color/space/hsl.dart b/lib/src/value/color/space/hsl.dart index 94ec83553..bc4a02164 100644 --- a/lib/src/value/color/space/hsl.dart +++ b/lib/src/value/color/space/hsl.dart @@ -24,8 +24,7 @@ final class HslColorSpace extends ColorSpace { hueChannel, LinearChannel('saturation', 0, 100, requiresPercent: true, lowerClamped: true), - LinearChannel('lightness', 0, 100, - requiresPercent: true, lowerClamped: true, upperClamped: true) + LinearChannel('lightness', 0, 100, requiresPercent: true) ]); SassColor convert(ColorSpace dest, double? hue, double? saturation, From cd6a903c65731c568b392ff0bc082923552188f1 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 1 Apr 2024 12:16:45 -0700 Subject: [PATCH 15/18] Support missing channels in color.change() --- lib/src/functions/color.dart | 98 ++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index c607317c1..61feb112c 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -717,7 +717,7 @@ SassColor _updateComponents(List arguments, : _colorInSpace(originalColor, spaceKeyword ?? sassNull); var oldChannels = color.channels; - var channelArgs = List.filled(oldChannels.length, null); + var channelArgs = List.filled(oldChannels.length, null); var channelInfo = color.space.channels; for (var (name, value) in keywords.pairs) { var channelIndex = channelInfo.indexWhere((info) => name == info.name); @@ -727,14 +727,21 @@ SassColor _updateComponents(List arguments, name); } - channelArgs[channelIndex] = value.assertNumber(name); + channelArgs[channelIndex] = value; } - var result = change - ? _changeColor(color, channelArgs, alphaArg) - : scale - ? _scaleColor(color, channelArgs, alphaArg) - : _adjustColor(color, channelArgs, alphaArg); + SassColor result; + if (change) { + result = _changeColor(color, channelArgs, alphaArg); + } else { + var channelNumbers = [ + for (var i = 0; i < channelInfo.length; i++) + channelArgs[i]?.assertNumber(channelInfo[i].name) + ]; + result = scale + ? _scaleColor(color, channelNumbers, alphaArg) + : _adjustColor(color, channelNumbers, alphaArg); + } return result.toSpace(originalColor.space); } @@ -742,36 +749,53 @@ SassColor _updateComponents(List arguments, /// Returns a copy of [color] with its channel values replaced by those in /// [channelArgs] and [alphaArg], if specified. SassColor _changeColor( - SassColor color, List channelArgs, SassNumber? alphaArg) { - var latterUnits = - color.space == ColorSpace.hsl || color.space == ColorSpace.hwb - ? '%' - : null; - return _colorFromChannels( - color.space, - channelArgs[0] ?? SassNumber(color.channel0), - channelArgs[1] ?? SassNumber(color.channel1, latterUnits), - channelArgs[2] ?? SassNumber(color.channel2, latterUnits), - alphaArg.andThen((alphaArg) { - if (!alphaArg.hasUnits) { - return alphaArg.value; - } else if (alphaArg.hasUnit('%')) { - return alphaArg.value / 100; - } else { - warnForDeprecation( - "\$alpha: Passing a unit other than % ($alphaArg) is " - "deprecated.\n" - "\n" - "To preserve current behavior: " - "${alphaArg.unitSuggestion('alpha')}\n" - "\n" - "See https://sass-lang.com/d/function-units", - Deprecation.functionUnits); - return alphaArg.value; - } - }) ?? - color.alpha, - clamp: false); + SassColor color, List channelArgs, SassNumber? alphaArg) => + _colorFromChannels( + color.space, + _channelForChange(channelArgs[0], color, 0), + _channelForChange(channelArgs[1], color, 1), + _channelForChange(channelArgs[2], color, 2), + alphaArg.andThen((alphaArg) { + if (!alphaArg.hasUnits) { + return alphaArg.value; + } else if (alphaArg.hasUnit('%')) { + return alphaArg.value / 100; + } else { + warnForDeprecation( + "\$alpha: Passing a unit other than % ($alphaArg) is " + "deprecated.\n" + "\n" + "To preserve current behavior: " + "${alphaArg.unitSuggestion('alpha')}\n" + "\n" + "See https://sass-lang.com/d/function-units", + Deprecation.functionUnits); + return alphaArg.value; + } + }) ?? + color.alpha, + clamp: false); + +/// Returns the value for a single channel in `color.change()`. +/// +/// The [channelArg] is the argument passed in by the user, if one exists. If no +/// argument is passed, the channel at [index] in [color] is used instead. +SassNumber? _channelForChange(Value? channelArg, SassColor color, int channel) { + if (channelArg == null) { + return switch (color.channelsOrNull[channel]) { + var value? => SassNumber( + value, + (color.space == ColorSpace.hsl || color.space == ColorSpace.hwb) && + channel > 0 + ? '%' + : null), + _ => null + }; + } + if (_isNone(channelArg)) return null; + if (channelArg is SassNumber) return channelArg; + throw SassScriptException('$channelArg is not a number or unquoted "none".', + color.space.channels[channel].name); } /// Returns a copy of [color] with its channel values scaled by the values in From f3869d9ba5afecf6d75a5cfb88a0a49c81961ee0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 1 Apr 2024 12:17:01 -0700 Subject: [PATCH 16/18] Properly serialize out-of-gamut {ok,}l{ab,ch} colors --- lib/src/visitor/serialize.dart | 84 ++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index e40414314..9b497d45e 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -608,14 +608,35 @@ final class _SerializeVisitor _maybeWriteSlashAlpha(value); _buffer.writeCharCode($rparen); - // case ColorSpace.lab || - // ColorSpace.oklab || - // ColorSpace.lch || - // ColorSpace.oklch when fuzzyInRange(value.channel0, 0, 100) && !value.isChannel1Missing && !value.isChannel2Missing: - // case ColorSpace.lch || - // ColorSpace.oklch when fuzzyLessThan(value.channel1, 0) && !value.isChannel0Missing && !value.isChannel1Missing: - // // color-mix() is currently more widely supported than relative color - // // syntax, so we use it to serialize out-of-gamut + case ColorSpace.lab || + ColorSpace.oklab || + ColorSpace.lch || + ColorSpace.oklch + when !_inspect && + !fuzzyInRange(value.channel0, 0, 100) && + !value.isChannel1Missing && + !value.isChannel2Missing: + case ColorSpace.lch || ColorSpace.oklch + when !_inspect && + fuzzyLessThan(value.channel1, 0) && + !value.isChannel0Missing && + !value.isChannel1Missing: + // color-mix() is currently more widely supported than relative color + // syntax, so we use it to serialize out-of-gamut colors in a way that + // maintains the color space defined in Sass while (per spec) not + // clamping their values. In practice, all browsers clamp out-of-gamut + // values, but there's not much we can do about that at time of writing. + _buffer.write('color-mix(in '); + _buffer.write(value.space); + _buffer.write(_commaSeparator); + // The XYZ space has no gamut restrictions, so we use it to represent + // the out-of-gamut color before converting into the target space. + _writeColorFunction(value.toSpace(ColorSpace.xyzD65)); + _writeOptionalSpace(); + _buffer.write('100%'); + _buffer.write(_commaSeparator); + _buffer.write(_isCompressed ? 'red' : 'black'); + _buffer.writeCharCode($rparen); case ColorSpace.lab || ColorSpace.oklab || @@ -624,6 +645,21 @@ final class _SerializeVisitor _buffer ..write(value.space) ..writeCharCode($lparen); + + // color-mix() can't represent out-of-bounds colors with missing + // channels, so in this case we use the less-supported but + // more-expressive relative color syntax instead. Relative color syntax + // never clamps channels. + var polar = value.space.channels[2].isPolarAngle; + if (!_inspect && + (!fuzzyInRange(value.channel0, 0, 100) || + (polar && fuzzyLessThan(value.channel1, 0)))) { + _buffer + ..write('from ') + ..write(_isCompressed ? 'red' : 'black') + ..writeCharCode($space); + } + if (!_isCompressed && !value.isChannel0Missing) { var max = (value.space.channels[0] as LinearChannel).max; _writeNumber(value.channel0 * 100 / max); @@ -635,22 +671,14 @@ final class _SerializeVisitor _writeChannel(value.channel1OrNull); _buffer.writeCharCode($space); _writeChannel(value.channel2OrNull); - if (!_isCompressed && - !value.isChannel2Missing && - value.space.channels[2].isPolarAngle) { + if (!_isCompressed && !value.isChannel2Missing && polar) { _buffer.write('deg'); } _maybeWriteSlashAlpha(value); _buffer.writeCharCode($rparen); case _: - _buffer - ..write('color(') - ..write(value.space) - ..writeCharCode($space); - _writeBetween(value.channelsOrNull, ' ', _writeChannel); - _maybeWriteSlashAlpha(value); - _buffer.writeCharCode($rparen); + _writeColorFunction(value); } } @@ -874,6 +902,26 @@ final class _SerializeVisitor _buffer.writeCharCode($rparen); } + /// Writes [color] using the `color()` function syntax. + void _writeColorFunction(SassColor color) { + assert(!{ + ColorSpace.rgb, + ColorSpace.hsl, + ColorSpace.hwb, + ColorSpace.lab, + ColorSpace.oklab, + ColorSpace.lch, + ColorSpace.oklch + }.contains(color.space)); + _buffer + ..write('color(') + ..write(color.space) + ..writeCharCode($space); + _writeBetween(color.channelsOrNull, ' ', _writeChannel); + _maybeWriteSlashAlpha(color); + _buffer.writeCharCode($rparen); + } + /// Returns whether [color]'s hex pair representation is symmetrical (e.g. /// `FF`). bool _isSymmetricalHex(int color) => color & 0xF == color >> 4; From 4d7b7087bc05251c3d7d86a50bc5e9d56f52407a Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 1 Apr 2024 12:35:13 -0700 Subject: [PATCH 17/18] Add a note about new behavior of color.adjust() --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index badcd0736..d33101437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ is now interpreted as a percentage, instead of ignoring the unit. For example, `color.change(red, $alpha: 50%)` now returns `rgb(255 0 0 / 0.5)`. +* **Potentially breaking compatibility fix**: Passing large positive or negative + values to `color.adjust()` can now cause a color's channels to go outside that + color's gamut. In most cases this will currently be clipped by the browser and + end up showing the same color as before, but once browsers implement gamut + mapping it may produce a different result. + * Add support for CSS Color Level 4 [color spaces]. Each color value now tracks its color space along with the values of each channel in that color space. There are two general principles to keep in mind when dealing with new color From 602c60d14eb4a1f66d3d328c8c59f9fabaf1a859 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 3 Apr 2024 12:57:24 -0700 Subject: [PATCH 18/18] Mark the rgb space as clampeed --- lib/src/value/color/space/rgb.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/value/color/space/rgb.dart b/lib/src/value/color/space/rgb.dart index 9933f9cea..b12fd40bd 100644 --- a/lib/src/value/color/space/rgb.dart +++ b/lib/src/value/color/space/rgb.dart @@ -19,9 +19,10 @@ final class RgbColorSpace extends ColorSpace { const RgbColorSpace() : super('rgb', const [ - LinearChannel('red', 0, 255), - LinearChannel('green', 0, 255), - LinearChannel('blue', 0, 255) + LinearChannel('red', 0, 255, lowerClamped: true, upperClamped: true), + LinearChannel('green', 0, 255, + lowerClamped: true, upperClamped: true), + LinearChannel('blue', 0, 255, lowerClamped: true, upperClamped: true) ]); SassColor convert(ColorSpace dest, double? red, double? green, double? blue,