3
3
// https://opensource.org/licenses/MIT.
4
4
5
5
import 'dart:collection' ;
6
+ import 'dart:math' as math;
6
7
7
8
import 'package:collection/collection.dart' ;
8
9
@@ -423,11 +424,21 @@ final module = BuiltInModule("color", functions: <Callable>[
423
424
(arguments) => SassString (arguments.first.assertColor ("color" ).space.name,
424
425
quotes: false )),
425
426
426
- _function (
427
- "to-space" ,
428
- r"$color, $space" ,
429
- (arguments) =>
430
- _colorInSpace (arguments[0 ], arguments[1 ].assertString ("space" ))),
427
+ _function ("to-space" , r"$color, $space" , (arguments) {
428
+ var converted = _colorInSpace (arguments[0 ], arguments[1 ]);
429
+ // `color.to-space()` never returns missing channels for legacy color
430
+ // spaces because they're less compatible and users are probably using a
431
+ // legacy space because they want a highly compatible color.
432
+ return converted.isLegacy &&
433
+ (converted.isChannel0Missing ||
434
+ converted.isChannel1Missing ||
435
+ converted.isChannel2Missing ||
436
+ converted.isAlphaMissing) &&
437
+ converted.space != (arguments[0 ] as SassColor ).space
438
+ ? SassColor .forSpaceInternal (converted.space, converted.channel0,
439
+ converted.channel1, converted.channel2, converted.alpha)
440
+ : converted;
441
+ }),
431
442
432
443
_function ("is-legacy" , r"$color" ,
433
444
(arguments) => SassBoolean (arguments[0 ].assertColor ("color" ).isLegacy)),
@@ -459,7 +470,7 @@ final module = BuiltInModule("color", functions: <Callable>[
459
470
? ColorSpace .srgb
460
471
: space)
461
472
.toGamut ()
462
- .toSpace (space);
473
+ .toSpace (color. space);
463
474
}),
464
475
465
476
_function ("channel" , r"$color, $channel, $space: null" , (arguments) {
@@ -706,7 +717,7 @@ SassColor _updateComponents(List<Value> arguments,
706
717
: _colorInSpace (originalColor, spaceKeyword ?? sassNull);
707
718
708
719
var oldChannels = color.channels;
709
- var channelArgs = List <SassNumber ?>.filled (oldChannels.length, null );
720
+ var channelArgs = List <Value ?>.filled (oldChannels.length, null );
710
721
var channelInfo = color.space.channels;
711
722
for (var (name, value) in keywords.pairs) {
712
723
var channelIndex = channelInfo.indexWhere ((info) => name == info.name);
@@ -716,50 +727,75 @@ SassColor _updateComponents(List<Value> arguments,
716
727
name);
717
728
}
718
729
719
- channelArgs[channelIndex] = value. assertNumber (name) ;
730
+ channelArgs[channelIndex] = value;
720
731
}
721
732
722
- var result = change
723
- ? _changeColor (color, channelArgs, alphaArg)
724
- : scale
725
- ? _scaleColor (color, channelArgs, alphaArg)
726
- : _adjustColor (color, channelArgs, alphaArg);
733
+ SassColor result;
734
+ if (change) {
735
+ result = _changeColor (color, channelArgs, alphaArg);
736
+ } else {
737
+ var channelNumbers = [
738
+ for (var i = 0 ; i < channelInfo.length; i++ )
739
+ channelArgs[i]? .assertNumber (channelInfo[i].name)
740
+ ];
741
+ result = scale
742
+ ? _scaleColor (color, channelNumbers, alphaArg)
743
+ : _adjustColor (color, channelNumbers, alphaArg);
744
+ }
727
745
728
746
return result.toSpace (originalColor.space);
729
747
}
730
748
731
749
/// Returns a copy of [color] with its channel values replaced by those in
732
750
/// [channelArgs] and [alphaArg] , if specified.
733
751
SassColor _changeColor (
734
- SassColor color, List <SassNumber ?> channelArgs, SassNumber ? alphaArg) {
735
- var latterUnits =
736
- color.space == ColorSpace .hsl || color.space == ColorSpace .hwb
737
- ? '%'
738
- : null ;
739
- return _colorFromChannels (
740
- color.space,
741
- channelArgs[0 ] ?? SassNumber (color.channel0),
742
- channelArgs[1 ] ?? SassNumber (color.channel1, latterUnits),
743
- channelArgs[2 ] ?? SassNumber (color.channel2, latterUnits),
744
- alphaArg.andThen ((alphaArg) {
745
- if (! alphaArg.hasUnits) {
746
- return alphaArg.value;
747
- } else if (alphaArg.hasUnit ('%' )) {
748
- return alphaArg.value / 100 ;
749
- } else {
750
- warnForDeprecation (
751
- "\$ alpha: Passing a unit other than % ($alphaArg ) is "
752
- "deprecated.\n "
753
- "\n "
754
- "To preserve current behavior: "
755
- "${alphaArg .unitSuggestion ('alpha' )}\n "
756
- "\n "
757
- "See https://sass-lang.com/d/function-units" ,
758
- Deprecation .functionUnits);
759
- return alphaArg.value;
760
- }
761
- }) ??
762
- color.alpha);
752
+ SassColor color, List <Value ?> channelArgs, SassNumber ? alphaArg) =>
753
+ _colorFromChannels (
754
+ color.space,
755
+ _channelForChange (channelArgs[0 ], color, 0 ),
756
+ _channelForChange (channelArgs[1 ], color, 1 ),
757
+ _channelForChange (channelArgs[2 ], color, 2 ),
758
+ alphaArg.andThen ((alphaArg) {
759
+ if (! alphaArg.hasUnits) {
760
+ return alphaArg.value;
761
+ } else if (alphaArg.hasUnit ('%' )) {
762
+ return alphaArg.value / 100 ;
763
+ } else {
764
+ warnForDeprecation (
765
+ "\$ alpha: Passing a unit other than % ($alphaArg ) is "
766
+ "deprecated.\n "
767
+ "\n "
768
+ "To preserve current behavior: "
769
+ "${alphaArg .unitSuggestion ('alpha' )}\n "
770
+ "\n "
771
+ "See https://sass-lang.com/d/function-units" ,
772
+ Deprecation .functionUnits);
773
+ return alphaArg.value;
774
+ }
775
+ }) ??
776
+ color.alpha,
777
+ clamp: false );
778
+
779
+ /// Returns the value for a single channel in `color.change()` .
780
+ ///
781
+ /// The [channelArg] is the argument passed in by the user, if one exists. If no
782
+ /// argument is passed, the channel at [index] in [color] is used instead.
783
+ SassNumber ? _channelForChange (Value ? channelArg, SassColor color, int channel) {
784
+ if (channelArg == null ) {
785
+ return switch (color.channelsOrNull[channel]) {
786
+ var value? => SassNumber (
787
+ value,
788
+ (color.space == ColorSpace .hsl || color.space == ColorSpace .hwb) &&
789
+ channel > 0
790
+ ? '%'
791
+ : null ),
792
+ _ => null
793
+ };
794
+ }
795
+ if (_isNone (channelArg)) return null ;
796
+ if (channelArg is SassNumber ) return channelArg;
797
+ throw SassScriptException ('$channelArg is not a number or unquoted "none".' ,
798
+ color.space.channels[channel].name);
763
799
}
764
800
765
801
/// Returns a copy of [color] with its channel values scaled by the values in
@@ -852,10 +888,15 @@ double _adjustChannel(ColorSpace space, ColorChannel channel, double oldValue,
852
888
adjustmentArg = SassNumber (adjustmentArg.value);
853
889
}
854
890
855
- var result = oldValue + _channelFromValue (channel, adjustmentArg)! ;
856
- return space.isStrictlyBounded && channel is LinearChannel
857
- ? fuzzyClamp (result, channel.min, channel.max)
858
- : result;
891
+ var result =
892
+ oldValue + _channelFromValue (channel, adjustmentArg, clamp: false )! ;
893
+ return switch (channel) {
894
+ LinearChannel (lowerClamped: true , : var min) when result < min =>
895
+ oldValue < min ? math.max (oldValue, result) : min,
896
+ LinearChannel (upperClamped: true , : var max) when result > max =>
897
+ oldValue > max ? math.min (oldValue, result) : max,
898
+ _ => result
899
+ };
859
900
}
860
901
861
902
/// Given a map of arguments passed to [_updateComponents] for a legacy color,
@@ -1311,24 +1352,28 @@ Value? _parseNumberOrNone(String text) {
1311
1352
1312
1353
/// Creates a [SassColor] for the given [space] from the given channel values,
1313
1354
/// or throws a [SassScriptException] if the channel values are invalid.
1355
+ ///
1356
+ /// If [clamp] is true, this will clamp any clamped channels.
1314
1357
SassColor _colorFromChannels (ColorSpace space, SassNumber ? channel0,
1315
1358
SassNumber ? channel1, SassNumber ? channel2, double ? alpha,
1316
- {bool fromRgbFunction = false }) {
1359
+ {bool clamp = true , bool fromRgbFunction = false }) {
1317
1360
switch (space) {
1318
1361
case ColorSpace .hsl:
1319
1362
if (channel1 != null ) _checkPercent (channel1, 'saturation' );
1320
1363
if (channel2 != null ) _checkPercent (channel2, 'lightness' );
1321
1364
return SassColor .hsl (
1322
1365
channel0.andThen ((channel0) => _angleValue (channel0, 'hue' )),
1323
- channel1? .value.clamp (0 , 100 ).toDouble (),
1324
- channel2? .value.clamp (0 , 100 ).toDouble (),
1366
+ _channelFromValue (space.channels[1 ], _forcePercent (channel1),
1367
+ clamp: clamp),
1368
+ _channelFromValue (space.channels[2 ], _forcePercent (channel2),
1369
+ clamp: clamp),
1325
1370
alpha);
1326
1371
1327
1372
case ColorSpace .hwb:
1328
1373
channel1? .assertUnit ('%' , 'whiteness' );
1329
1374
channel2? .assertUnit ('%' , 'blackness' );
1330
- var whiteness = channel1? .value.clamp ( 0 , 100 ). toDouble ();
1331
- var blackness = channel2? .value.clamp ( 0 , 100 ). toDouble ();
1375
+ var whiteness = channel1? .value.toDouble ();
1376
+ var blackness = channel2? .value.toDouble ();
1332
1377
1333
1378
if (whiteness != null &&
1334
1379
blackness != null &&
@@ -1346,44 +1391,48 @@ SassColor _colorFromChannels(ColorSpace space, SassNumber? channel0,
1346
1391
1347
1392
case ColorSpace .rgb:
1348
1393
return SassColor .rgbInternal (
1349
- _channelFromValue (space.channels[0 ], channel0),
1350
- _channelFromValue (space.channels[1 ], channel1),
1351
- _channelFromValue (space.channels[2 ], channel2),
1394
+ _channelFromValue (space.channels[0 ], channel0, clamp : clamp ),
1395
+ _channelFromValue (space.channels[1 ], channel1, clamp : clamp ),
1396
+ _channelFromValue (space.channels[2 ], channel2, clamp : clamp ),
1352
1397
alpha,
1353
1398
fromRgbFunction ? ColorFormat .rgbFunction : null );
1354
1399
1355
- case ColorSpace .lab ||
1356
- ColorSpace .lch ||
1357
- ColorSpace .oklab ||
1358
- ColorSpace .oklch:
1359
- return SassColor .forSpaceInternal (
1360
- space,
1361
- _channelFromValue (space.channels[0 ], channel0).andThen ((lightness) =>
1362
- fuzzyClamp (
1363
- lightness, 0 , (space.channels[0 ] as LinearChannel ).max)),
1364
- _channelFromValue (space.channels[1 ], channel1),
1365
- _channelFromValue (space.channels[2 ], channel2),
1366
- alpha);
1367
-
1368
1400
default :
1369
1401
return SassColor .forSpaceInternal (
1370
1402
space,
1371
- _channelFromValue (space.channels[0 ], channel0),
1372
- _channelFromValue (space.channels[1 ], channel1),
1373
- _channelFromValue (space.channels[2 ], channel2),
1403
+ _channelFromValue (space.channels[0 ], channel0, clamp : clamp ),
1404
+ _channelFromValue (space.channels[1 ], channel1, clamp : clamp ),
1405
+ _channelFromValue (space.channels[2 ], channel2, clamp : clamp ),
1374
1406
alpha);
1375
1407
}
1376
1408
}
1377
1409
1410
+ /// Returns [number] with unit `'%'` regardless of its original unit.
1411
+ SassNumber ? _forcePercent (SassNumber ? number) => switch (number) {
1412
+ null => null ,
1413
+ SassNumber (numeratorUnits: ['%' ], denominatorUnits: []) => number,
1414
+ _ => SassNumber (number.value, '%' )
1415
+ };
1416
+
1378
1417
/// Converts a channel value from a [SassNumber] into a [double] according to
1379
1418
/// [channel] .
1380
- double ? _channelFromValue (ColorChannel channel, SassNumber ? value) =>
1419
+ ///
1420
+ /// If [clamp] is true, this clamps [value] according to [channel] 's clamping
1421
+ /// rules.
1422
+ double ? _channelFromValue (ColorChannel channel, SassNumber ? value,
1423
+ {bool clamp = true }) =>
1381
1424
value.andThen ((value) => switch (channel) {
1382
1425
LinearChannel (requiresPercent: true ) when ! value.hasUnit ('%' ) =>
1383
1426
throw SassScriptException (
1384
1427
'Expected $value to have unit "%".' , channel.name),
1385
- LinearChannel () =>
1428
+ LinearChannel (lowerClamped: false , upperClamped: false ) =>
1429
+ _percentageOrUnitless (value, channel.max, channel.name),
1430
+ LinearChannel () when ! clamp =>
1386
1431
_percentageOrUnitless (value, channel.max, channel.name),
1432
+ LinearChannel (: var lowerClamped, : var upperClamped) =>
1433
+ _percentageOrUnitless (value, channel.max, channel.name).clamp (
1434
+ lowerClamped ? channel.min : double .negativeInfinity,
1435
+ upperClamped ? channel.max : double .infinity),
1387
1436
_ => value.coerceValueToUnit ('deg' , channel.name) % 360
1388
1437
});
1389
1438
0 commit comments