diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index bc34ceb0f..155314241 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -476,11 +476,12 @@ final module = BuiltInModule("color", functions: [ var channelInfo = color.space.channels[channelIndex]; var channelValue = color.channels[channelIndex]; + var unit = channelInfo.associatedUnit; + if (unit == '%') { + channelValue = channelValue * 100 / (channelInfo as LinearChannel).max; + } - return channelInfo is LinearChannel - ? SassNumber(channelValue, - channelInfo.min == 0 && channelInfo.max == 100 ? '%' : null) - : SassNumber(channelValue, 'deg'); + return SassNumber(channelValue, unit); }), _function("same", r"$color1, $color2", (arguments) { @@ -1346,14 +1347,15 @@ SassColor _colorFromChannels(ColorSpace space, SassNumber? channel0, alpha, fromRgbFunction ? ColorFormat.rgbFunction : null); - case ColorSpace.lab: - case ColorSpace.lch: - case ColorSpace.oklab: - case ColorSpace.oklch: + case ColorSpace.lab || + ColorSpace.lch || + ColorSpace.oklab || + ColorSpace.oklch: return SassColor.forSpaceInternal( space, - _channelFromValue(space.channels[0], channel0) - .andThen((lightness) => fuzzyClamp(lightness, 0, 100)), + _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); diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 96abc5263..6ffdb2bfa 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -531,7 +531,8 @@ class SassColor extends Value { return SassColor.forSpaceInternal( space, clampChannel0 - ? channels[0].andThen((value) => fuzzyClamp(value, 0, 100)) + ? channels[0].andThen((value) => fuzzyClamp( + value, 0, (space.channels[0] as LinearChannel).max)) : channels[0], clampChannel12 ? channels[1].andThen((value) => fuzzyClamp(value, 0, 100)) diff --git a/lib/src/value/color/channel.dart b/lib/src/value/color/channel.dart index 6fe9f9dac..61cd115cf 100644 --- a/lib/src/value/color/channel.dart +++ b/lib/src/value/color/channel.dart @@ -21,9 +21,21 @@ class ColorChannel { /// This is true if and only if this is not a [LinearChannel]. final bool isPolarAngle; + /// The unit that's associated with this channel. + /// + /// Some channels are typically written without units, while others have a + /// specific unit that is conventionally applied to their values. Although any + /// compatible unit or unitless value will work for input¹, this unit is used + /// when the value is serialized or returned from a Sass function. + /// + /// 1: Unless [LinearChannel.requiresPercent] is set, in which case unitless + /// values are not allowed. + final String? associatedUnit; + /// @nodoc @internal - const ColorChannel(this.name, {required this.isPolarAngle}); + const ColorChannel(this.name, + {required this.isPolarAngle, this.associatedUnit}); /// Returns whether this channel is [analogous] to [other]. /// @@ -65,9 +77,19 @@ class LinearChannel extends ColorChannel { /// forbids unitless values. final bool requiresPercent; + /// Creates a linear color channel. + /// + /// By default, [ColorChannel.associatedUnit] is set to `%` if and only if + /// [min] is 0 and [max] is 100. However, if [conventionallyPercent] is + /// true, it's set to `%`, and if it's false, it's set to null. + /// /// @nodoc @internal const LinearChannel(String name, this.min, this.max, - {this.requiresPercent = false}) - : super(name, isPolarAngle: false); + {this.requiresPercent = false, bool? conventionallyPercent}) + : super(name, + isPolarAngle: false, + associatedUnit: (conventionallyPercent ?? (min == 0 && max == 100)) + ? '%' + : null); } diff --git a/lib/src/value/color/space/oklab.dart b/lib/src/value/color/space/oklab.dart index de63b7673..cf8951bbd 100644 --- a/lib/src/value/color/space/oklab.dart +++ b/lib/src/value/color/space/oklab.dart @@ -23,7 +23,7 @@ class OklabColorSpace extends ColorSpace { const OklabColorSpace() : super('oklab', const [ - LinearChannel('lightness', 0, 1), + LinearChannel('lightness', 0, 1, conventionallyPercent: 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 6bd63c736..30887f6e7 100644 --- a/lib/src/value/color/space/oklch.dart +++ b/lib/src/value/color/space/oklch.dart @@ -23,7 +23,7 @@ class OklchColorSpace extends ColorSpace { const OklchColorSpace() : super('oklch', const [ - LinearChannel('lightness', 0, 1), + LinearChannel('lightness', 0, 1, conventionallyPercent: true), LinearChannel('chroma', 0, 0.4), hueChannel ]); diff --git a/lib/src/value/color/space/utils.dart b/lib/src/value/color/space/utils.dart index b1e16f2d9..427da3188 100644 --- a/lib/src/value/color/space/utils.dart +++ b/lib/src/value/color/space/utils.dart @@ -14,7 +14,8 @@ const labKappa = 24389 / 27; // 29^3/3^3; const labEpsilon = 216 / 24389; // 6^3/29^3; /// The hue channel shared across all polar color spaces. -const hueChannel = ColorChannel('hue', isPolarAngle: true); +const hueChannel = + ColorChannel('hue', isPolarAngle: true, associatedUnit: 'deg'); /// The color channels shared across all RGB color spaces (except the legacy RGB space). const rgbChannels = [ diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 93b112201..46f0bc270 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -565,38 +565,29 @@ final class _SerializeVisitor case ColorSpace.rgb || ColorSpace.hsl || ColorSpace.hwb: _writeLegacyColor(value); - case ColorSpace.lab || ColorSpace.oklab: + case ColorSpace.lab || + ColorSpace.oklab || + ColorSpace.lch || + ColorSpace.oklch: _buffer ..write(value.space) ..writeCharCode($lparen); - _writeChannel(value.channel0OrNull); - if (!_isCompressed && - value.space == ColorSpace.lab && - !value.isChannel0Missing) { + if (!_isCompressed && !value.isChannel0Missing) { + var max = (value.space.channels[0] as LinearChannel).max; + _writeNumber(value.channel0 * 100 / max); _buffer.writeCharCode($percent); + } else { + _writeChannel(value.channel0OrNull); } _buffer.writeCharCode($space); _writeChannel(value.channel1OrNull); _buffer.writeCharCode($space); _writeChannel(value.channel2OrNull); - _maybeWriteSlashAlpha(value); - _buffer.writeCharCode($rparen); - - case ColorSpace.lch || ColorSpace.oklch: - _buffer - ..write(value.space) - ..writeCharCode($lparen); - _writeChannel(value.channel0OrNull); if (!_isCompressed && - value.space == ColorSpace.lch && - !value.isChannel0Missing) { - _buffer.writeCharCode($percent); + !value.isChannel2Missing && + value.space.channels[2].isPolarAngle) { + _buffer.write('deg'); } - _buffer.writeCharCode($space); - _writeChannel(value.channel1OrNull); - _buffer.writeCharCode($space); - _writeChannel(value.channel2OrNull); - if (!_isCompressed && !value.isChannel2Missing) _buffer.write('deg'); _maybeWriteSlashAlpha(value); _buffer.writeCharCode($rparen);