Skip to content

Commit 10ce61d

Browse files
committed
[WIP] preserve missing channels through conversions
1 parent ed1a6b0 commit 10ce61d

15 files changed

+257
-113
lines changed

lib/src/functions/color.dart

+5-6
Original file line numberDiff line numberDiff line change
@@ -1092,12 +1092,11 @@ SassColor _transparentize(String name, List<Value> arguments) {
10921092
/// `sassNull`, it defaults to the color's existing space.
10931093
SassColor _colorInSpace(Value colorUntyped, Value spaceUntyped) {
10941094
var color = colorUntyped.assertColor("color");
1095-
if (spaceUntyped == sassNull) return color;
1096-
1097-
var space = ColorSpace.fromName(
1098-
(spaceUntyped.assertString("space")..assertUnquoted("space")).text,
1099-
"space");
1100-
return color.space == space ? color : color.toSpace(space);
1095+
return spaceUntyped == sassNull
1096+
? color
1097+
: color.toSpace(ColorSpace.fromName(
1098+
(spaceUntyped.assertString("space")..assertUnquoted("space")).text,
1099+
"space"));
11011100
}
11021101

11031102
/// Returns the color space named by [space], or throws a [SassScriptException]

lib/src/value/color.dart

+46-3
Original file line numberDiff line numberDiff line change
@@ -546,9 +546,52 @@ class SassColor extends Value {
546546
/// This currently can't produce an error, but it will likely do so in the
547547
/// future when Sass adds support for color spaces that don't support
548548
/// automatic conversions.
549-
SassColor toSpace(ColorSpace space) => this.space == space
550-
? this
551-
: this.space.convert(space, channel0, channel1, channel2, alpha);
549+
SassColor toSpace(ColorSpace space) {
550+
if (this.space != space) {
551+
return this.space.convert(space, channel0, channel1, channel2, alpha);
552+
}
553+
554+
// Converting to a color space triggers powerless channel analysis, even if
555+
// the target space is the same as the source. So for spaces that can have
556+
// powerless channels, we need to check if those channels are indeed
557+
// powerless and if so make sure they're marked as missing.
558+
//
559+
// We implement separate logic for each space rather than just checking all
560+
// three channels for every space because we want converting a color to its
561+
// own space to be as efficient as possible.
562+
switch (space) {
563+
case ColorSpace.hsl:
564+
var needsMissing0 = !isChannel0Missing && isChannel0Powerless;
565+
var needsMissing1 = !isChannel1Missing && isChannel1Powerless;
566+
return needsMissing0 || needsMissing1
567+
? SassColor.forSpaceInternal(space, needsMissing0 ? null : channel0,
568+
needsMissing1 ? null : channel1, channel2, alpha)
569+
: this;
570+
571+
case ColorSpace.hwb:
572+
return !isChannel0Missing && isChannel0Powerless
573+
? SassColor.forSpaceInternal(space, null, channel1, channel2, alpha)
574+
: this;
575+
576+
case ColorSpace.lab:
577+
case ColorSpace.oklab:
578+
case ColorSpace.lch:
579+
case ColorSpace.oklch:
580+
var needsMissing1 = !isChannel1Missing && isChannel1Powerless;
581+
var needsMissing2 = !isChannel2Missing && isChannel2Powerless;
582+
return needsMissing1 || needsMissing2
583+
? SassColor.forSpaceInternal(
584+
space,
585+
channel0,
586+
needsMissing1 ? null : channel1,
587+
needsMissing2 ? null : channel2,
588+
alpha)
589+
: this;
590+
591+
default:
592+
return this;
593+
}
594+
}
552595

553596
/// Returns a copy of this color that's in-gamut in the current color space.
554597
SassColor toGamut() {

lib/src/value/color/space.dart

+20-15
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ abstract class ColorSpace {
106106
/// https://www.w3.org/TR/css-color-4/#ok-lab
107107
static const ColorSpace oklab = OklabColorSpace();
108108

109-
110109
/// The Oklch color space.
111110
///
112111
/// https://www.w3.org/TR/css-color-4/#ok-lab
@@ -198,8 +197,8 @@ abstract class ColorSpace {
198197
///
199198
/// @nodoc
200199
@internal
201-
SassColor convert(ColorSpace dest, double channel0, double channel1,
202-
double channel2, double alpha) {
200+
SassColor convert(ColorSpace dest, double? channel0, double? channel1,
201+
double? channel2, double alpha) {
203202
var linearDest = dest;
204203
switch (dest) {
205204
case ColorSpace.hsl:
@@ -218,26 +217,32 @@ abstract class ColorSpace {
218217
break;
219218
}
220219

221-
double transformed0;
222-
double transformed1;
223-
double transformed2;
220+
double? transformed0;
221+
double? transformed1;
222+
double? transformed2;
224223
if (linearDest == this) {
225224
transformed0 = channel0;
226225
transformed1 = channel1;
227226
transformed2 = channel2;
228227
} else {
229-
var linear0 = toLinear(channel0);
230-
var linear1 = toLinear(channel1);
231-
var linear2 = toLinear(channel2);
228+
var linear0 = toLinear(channel0 ?? 0);
229+
var linear1 = toLinear(channel1?? 0);
230+
var linear2 = toLinear(channel2??0);
232231
var matrix = transformationMatrix(linearDest);
233232

234233
// (matrix * [linear0, linear1, linear2]).map(linearDest.fromLinear)
235-
transformed0 = linearDest.fromLinear(
236-
matrix[0] * linear0 + matrix[1] * linear1 + matrix[2] * linear2);
237-
transformed1 = linearDest.fromLinear(
238-
matrix[3] * linear0 + matrix[4] * linear1 + matrix[5] * linear2);
239-
transformed2 = linearDest.fromLinear(
240-
matrix[6] * linear0 + matrix[7] * linear1 + matrix[8] * linear2);
234+
transformed0 = channel0 == null
235+
? null
236+
: linearDest.fromLinear(
237+
matrix[0] * linear0 + matrix[1] * linear1 + matrix[2] * linear2);
238+
transformed1 = channel1 == null
239+
? null
240+
: linearDest.fromLinear(
241+
matrix[3] * linear0 + matrix[4] * linear1 + matrix[5] * linear2);
242+
transformed2 = channel2 == null
243+
? null
244+
: linearDest.fromLinear(
245+
matrix[6] * linear0 + matrix[7] * linear1 + matrix[8] * linear2);
241246
}
242247

243248
switch (dest) {

lib/src/value/color/space/hsl.dart

+15-11
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ class HslColorSpace extends ColorSpace {
2626
LinearChannel('lightness', 0, 100, requiresPercent: true)
2727
]);
2828

29-
SassColor convert(ColorSpace dest, double hue, double saturation,
30-
double lightness, double alpha) {
29+
SassColor convert(ColorSpace dest, double? hue, double? saturation,
30+
double? lightness, double alpha) {
3131
// Algorithm from the CSS3 spec: https://www.w3.org/TR/css3-color/#hsl-color.
32-
var scaledHue = (hue / 360) % 1;
33-
var scaledSaturation = saturation / 100;
34-
var scaledLightness = lightness / 100;
32+
var scaledHue = ((hue ?? 0) / 360) % 1;
33+
var scaledSaturation = (saturation ?? 0) / 100;
34+
var scaledLightness = (lightness ?? 0) / 100;
3535

3636
var m2 = scaledLightness <= 0.5
3737
? scaledLightness * (scaledSaturation + 1)
@@ -40,11 +40,15 @@ class HslColorSpace extends ColorSpace {
4040
scaledLightness * scaledSaturation;
4141
var m1 = scaledLightness * 2 - m2;
4242

43-
return ColorSpace.srgb.convert(
44-
dest,
45-
hueToRgb(m1, m2, scaledHue + 1 / 3),
46-
hueToRgb(m1, m2, scaledHue),
47-
hueToRgb(m1, m2, scaledHue - 1 / 3),
48-
alpha);
43+
return forwardMissingChannels(
44+
ColorSpace.srgb.convert(
45+
dest,
46+
hueToRgb(m1, m2, scaledHue + 1 / 3),
47+
hueToRgb(m1, m2, scaledHue),
48+
hueToRgb(m1, m2, scaledHue - 1 / 3),
49+
alpha),
50+
missingLightness: lightness == null,
51+
missingColorfulness: saturation == null,
52+
missingHue: hue == null);
4953
}
5054
}

lib/src/value/color/space/hwb.dart

+9-9
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ class HwbColorSpace extends ColorSpace {
2626
LinearChannel('blackness', 0, 100, requiresPercent: true)
2727
]);
2828

29-
SassColor convert(ColorSpace dest, double hue, double whiteness,
30-
double blackness, double alpha) {
29+
SassColor convert(ColorSpace dest, double? hue, double? whiteness,
30+
double? blackness, double alpha) {
3131
// From https://www.w3.org/TR/css-color-4/#hwb-to-rgb
32-
var scaledHue = hue % 360 / 360;
33-
var scaledWhiteness = whiteness / 100;
34-
var scaledBlackness = blackness / 100;
32+
var scaledHue = (hue ?? 0) % 360 / 360;
33+
var scaledWhiteness = (whiteness ?? 0) / 100;
34+
var scaledBlackness = (blackness ?? 0) / 100;
3535

3636
var sum = scaledWhiteness + scaledBlackness;
3737
if (sum > 1) {
@@ -42,9 +42,9 @@ class HwbColorSpace extends ColorSpace {
4242
var factor = 1 - scaledWhiteness - scaledBlackness;
4343
double toRgb(double hue) => hueToRgb(0, 1, hue) * factor + scaledWhiteness;
4444

45-
// Non-null because an in-gamut HSL color is guaranteed to be in-gamut for
46-
// HWB as well.
47-
return ColorSpace.srgb.convert(dest, toRgb(scaledHue + 1 / 3),
48-
toRgb(scaledHue), toRgb(scaledHue - 1 / 3), alpha);
45+
return forwardMissingChannels(
46+
ColorSpace.srgb.convert(dest, toRgb(scaledHue + 1 / 3),
47+
toRgb(scaledHue), toRgb(scaledHue - 1 / 3), alpha),
48+
missingHue: hue == null);
4949
}
5050
}

lib/src/value/color/space/lab.dart

+15-12
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ class LabColorSpace extends ColorSpace {
3030
]);
3131

3232
SassColor convert(
33-
ColorSpace dest, double lightness, double a, double b, double alpha) {
33+
ColorSpace dest, double? lightness, double? a, double? b, double alpha) {
3434
switch (dest) {
3535
case ColorSpace.lab:
36-
var powerlessAB = fuzzyEquals(lightness, 0);
36+
var powerlessAB = lightness == null || fuzzyEquals(lightness, 0);
3737
return SassColor.lab(
3838
lightness, powerlessAB ? null : a, powerlessAB ? null : b, alpha);
3939

@@ -43,17 +43,20 @@ class LabColorSpace extends ColorSpace {
4343
default:
4444
// Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code
4545
// and http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
46-
var f1 = (lightness + 16) / 116;
46+
var lightnessOr0 = lightness ?? 0;
47+
var f1 = (lightnessOr0 + 16) / 116;
4748

48-
return ColorSpace.xyzD50.convert(
49-
dest,
50-
_convertFToXorZ(a / 500 + f1) * d50[0],
51-
(lightness > labKappa * labEpsilon
52-
? math.pow((lightness + 16) / 116, 3) * 1.0
53-
: lightness / labKappa) *
54-
d50[1],
55-
_convertFToXorZ(f1 - b / 200) * d50[2],
56-
alpha);
49+
return forwardMissingChannels(
50+
ColorSpace.xyzD50.convert(
51+
dest,
52+
_convertFToXorZ((a??0) / 500 + f1) * d50[0],
53+
(lightnessOr0 > labKappa * labEpsilon
54+
? math.pow(f1, 3) * 1.0
55+
: lightnessOr0 / labKappa) *
56+
d50[1],
57+
_convertFToXorZ(f1 - (b??0) / 200) * d50[2],
58+
alpha),
59+
missingLightness: lightness == null);
5760
}
5861
}
5962

lib/src/value/color/space/lch.dart

+13-5
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ class LchColorSpace extends ColorSpace {
2828
hueChannel
2929
]);
3030

31-
SassColor convert(ColorSpace dest, double lightness, double chroma,
32-
double hue, double alpha) {
33-
var hueRadians = hue * math.pi / 180;
34-
return ColorSpace.lab.convert(dest, lightness,
35-
chroma * math.cos(hueRadians), chroma * math.sin(hueRadians), alpha);
31+
SassColor convert(ColorSpace dest, double? lightness, double? chroma,
32+
double? hue, double alpha) {
33+
var hueRadians = (hue ?? 0) * math.pi / 180;
34+
return forwardMissingChannels(
35+
ColorSpace.lab.convert(
36+
dest,
37+
lightness ?? 0,
38+
(chroma ?? 0) * math.cos(hueRadians),
39+
(chroma ?? 0) * math.sin(hueRadians),
40+
alpha),
41+
missingLightness: lightness == null,
42+
missingColorfulness: chroma == null,
43+
missingHue: hue == null);
3644
}
3745
}

lib/src/value/color/space/lms.dart

+7-7
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ class LmsColorSpace extends ColorSpace {
3333
]);
3434

3535
SassColor convert(
36-
ColorSpace dest, double long, double medium, double short, double alpha) {
36+
ColorSpace dest, double? long, double? medium, double? short, double alpha) {
3737
switch (dest) {
3838
case ColorSpace.oklab:
3939
// Algorithm from https://drafts.csswg.org/css-color-4/#color-conversion-code
40-
var longScaled = math.pow(long, 1 / 3);
41-
var mediumScaled = math.pow(medium, 1 / 3);
42-
var shortScaled = math.pow(short, 1 / 3);
40+
var longScaled = math.pow(long ?? 0, 1 / 3);
41+
var mediumScaled = math.pow(medium ?? 0, 1 / 3);
42+
var shortScaled = math.pow(short ?? 0, 1 / 3);
4343
var lightness = lmsToOklab[0] * longScaled +
4444
lmsToOklab[1] * mediumScaled +
4545
lmsToOklab[2] * shortScaled;
@@ -62,9 +62,9 @@ class LmsColorSpace extends ColorSpace {
6262
// This is equivalent to converting to OKLab and then to OKLCH, but we
6363
// do it inline to avoid extra list allocations since we expect
6464
// conversions to and from OKLCH to be very common.
65-
var longScaled = math.pow(long, 1 / 3);
66-
var mediumScaled = math.pow(medium, 1 / 3);
67-
var shortScaled = math.pow(short, 1 / 3);
65+
var longScaled = math.pow(long ?? 0, 1 / 3);
66+
var mediumScaled = math.pow(medium ?? 0, 1 / 3);
67+
var shortScaled = math.pow(short ?? 0, 1 / 3);
6868
return labToLch(
6969
dest,
7070
lmsToOklab[0] * longScaled +

lib/src/value/color/space/oklab.dart

+27-22
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,37 @@ class OklabColorSpace extends ColorSpace {
2929
]);
3030

3131
SassColor convert(
32-
ColorSpace dest, double lightness, double a, double b, double alpha) {
32+
ColorSpace dest, double? lightness, double? a, double? b, double alpha) {
3333
if (dest == ColorSpace.oklch) {
3434
return labToLch(dest, lightness, a, b, alpha);
3535
}
3636

3737
// Algorithm from https://www.w3.org/TR/css-color-4/#color-conversion-code
38-
return ColorSpace.lms.convert(
39-
dest,
40-
math.pow(
41-
oklabToLms[0] * lightness +
42-
oklabToLms[1] * a +
43-
oklabToLms[2] * b,
44-
3) +
45-
0.0,
46-
math.pow(
47-
oklabToLms[3] * lightness +
48-
oklabToLms[4] * a +
49-
oklabToLms[5] * b,
50-
3) +
51-
0.0,
52-
math.pow(
53-
oklabToLms[6] * lightness +
54-
oklabToLms[7] * a +
55-
oklabToLms[8] * b,
56-
3) +
57-
0.0,
58-
alpha);
38+
lightness ??= 0;
39+
a ??= 0;
40+
b ??= 0;
41+
return forwardMissingChannels(
42+
ColorSpace.lms.convert(
43+
dest,
44+
math.pow(
45+
oklabToLms[0] * lightness +
46+
oklabToLms[1] * a +
47+
oklabToLms[2] * b,
48+
3) +
49+
0.0,
50+
math.pow(
51+
oklabToLms[3] * lightness +
52+
oklabToLms[4] * a +
53+
oklabToLms[5] * b,
54+
3) +
55+
0.0,
56+
math.pow(
57+
oklabToLms[6] * lightness +
58+
oklabToLms[7] * a +
59+
oklabToLms[8] * b,
60+
3) +
61+
0.0,
62+
alpha),
63+
missingLightness: lightness == null);
5964
}
6065
}

lib/src/value/color/space/oklch.dart

+12-4
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ class OklchColorSpace extends ColorSpace {
2828
hueChannel
2929
]);
3030

31-
SassColor convert(ColorSpace dest, double lightness, double chroma,
32-
double hue, double alpha) {
31+
SassColor convert(ColorSpace dest, double? lightness, double? chroma,
32+
double? hue, double alpha) {
3333
var hueRadians = hue * math.pi / 180;
34-
return ColorSpace.oklab.convert(dest, lightness,
35-
chroma * math.cos(hueRadians), chroma * math.sin(hueRadians), alpha);
34+
return forwardMissingChannels(
35+
ColorSpace.oklab.convert(
36+
dest,
37+
lightness ?? 0,
38+
(chroma ?? 0) * math.cos(hueRadians),
39+
(chroma ?? 0) * math.sin(hueRadians),
40+
alpha),
41+
missingLightness: lightness == null,
42+
missingColorfulness: chroma == null,
43+
missingHue: hue == null);
3644
}
3745
}

0 commit comments

Comments
 (0)