Skip to content

Commit 8cd2df1

Browse files
Support mixed color spaces in Color.lerp (#182934)
Instead of asserting that both colors must share the same color space, Color.lerp now converts both colors to the wider gamut color space before interpolating. For example, lerping between an sRGB color and a Display P3 color produces a Display P3 result. Instead of asserting that both colors must share the same color space, Color.lerp now converts both colors to the wider gamut color space before interpolating. For example, lerping between an sRGB color and a Display P3 color produces a Display P3 result. Fixes: #182777 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 59a6919 commit 8cd2df1

3 files changed

Lines changed: 124 additions & 26 deletions

File tree

‎engine/src/flutter/lib/ui/painting.dart‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ Color _scaleAlpha(Color x, double factor) {
5757
return x.withValues(alpha: clampDouble(x.a * factor, 0, 1));
5858
}
5959

60+
ColorSpace _widerColorSpace(ColorSpace a, ColorSpace b) {
61+
return a == ColorSpace.displayP3 || b == ColorSpace.displayP3 ? ColorSpace.displayP3 : a;
62+
}
63+
6064
/// An immutable color value in ARGB format.
6165
///
6266
/// Consider the light teal of the [Flutter logo](https://flutter.dev/brand). It
@@ -412,6 +416,11 @@ class Color {
412416
///
413417
/// Values for `t` are usually obtained from an [Animation<double>], such as
414418
/// an [AnimationController].
419+
///
420+
/// If the two colors are in different color spaces, both are converted to
421+
/// the wider gamut color space before interpolating. The result will be in
422+
/// the wider gamut color space. For example, interpolating between an sRGB
423+
/// color and a Display P3 color will produce a Display P3 result.
415424
static Color? lerp(Color? x, Color? y, double t) {
416425
assert(x?.colorSpace != ColorSpace.extendedSRGB);
417426
assert(y?.colorSpace != ColorSpace.extendedSRGB);
@@ -425,13 +434,24 @@ class Color {
425434
if (x == null) {
426435
return _scaleAlpha(y, t);
427436
} else {
428-
assert(x.colorSpace == y.colorSpace);
437+
final Color a;
438+
final Color b;
439+
final ColorSpace resultColorSpace;
440+
if (x.colorSpace == y.colorSpace) {
441+
a = x;
442+
b = y;
443+
resultColorSpace = x.colorSpace;
444+
} else {
445+
resultColorSpace = _widerColorSpace(x.colorSpace, y.colorSpace);
446+
a = x.withValues(colorSpace: resultColorSpace);
447+
b = y.withValues(colorSpace: resultColorSpace);
448+
}
429449
return Color.from(
430-
alpha: clampDouble(_lerpDouble(x.a, y.a, t), 0, 1),
431-
red: clampDouble(_lerpDouble(x.r, y.r, t), 0, 1),
432-
green: clampDouble(_lerpDouble(x.g, y.g, t), 0, 1),
433-
blue: clampDouble(_lerpDouble(x.b, y.b, t), 0, 1),
434-
colorSpace: x.colorSpace,
450+
alpha: clampDouble(_lerpDouble(a.a, b.a, t), 0, 1),
451+
red: clampDouble(_lerpDouble(a.r, b.r, t), 0, 1),
452+
green: clampDouble(_lerpDouble(a.g, b.g, t), 0, 1),
453+
blue: clampDouble(_lerpDouble(a.b, b.b, t), 0, 1),
454+
colorSpace: resultColorSpace,
435455
);
436456
}
437457
}

‎engine/src/flutter/lib/web_ui/lib/painting.dart‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Color _scaleAlpha(Color x, double factor) {
2121
return x.withValues(alpha: (x.a * factor).clamp(0, 1));
2222
}
2323

24+
ColorSpace _widerColorSpace(ColorSpace a, ColorSpace b) {
25+
return a == ColorSpace.displayP3 || b == ColorSpace.displayP3 ? ColorSpace.displayP3 : a;
26+
}
27+
2428
class Color {
2529
const Color(int value)
2630
: this._fromARGBC(value >> 24, value >> 16, value >> 8, value, ColorSpace.sRGB);
@@ -158,13 +162,24 @@ class Color {
158162
if (x == null) {
159163
return _scaleAlpha(y, t);
160164
} else {
161-
assert(x.colorSpace == y.colorSpace);
165+
final Color a;
166+
final Color b;
167+
final ColorSpace resultColorSpace;
168+
if (x.colorSpace == y.colorSpace) {
169+
a = x;
170+
b = y;
171+
resultColorSpace = x.colorSpace;
172+
} else {
173+
resultColorSpace = _widerColorSpace(x.colorSpace, y.colorSpace);
174+
a = x.withValues(colorSpace: resultColorSpace);
175+
b = y.withValues(colorSpace: resultColorSpace);
176+
}
162177
return Color.from(
163-
alpha: _lerpDouble(x.a, y.a, t).clamp(0, 1),
164-
red: _lerpDouble(x.r, y.r, t).clamp(0, 1),
165-
green: _lerpDouble(x.g, y.g, t).clamp(0, 1),
166-
blue: _lerpDouble(x.b, y.b, t).clamp(0, 1),
167-
colorSpace: x.colorSpace,
178+
alpha: _lerpDouble(a.a, b.a, t).clamp(0, 1),
179+
red: _lerpDouble(a.r, b.r, t).clamp(0, 1),
180+
green: _lerpDouble(a.g, b.g, t).clamp(0, 1),
181+
blue: _lerpDouble(a.b, b.b, t).clamp(0, 1),
182+
colorSpace: resultColorSpace,
168183
);
169184
}
170185
}

‎engine/src/flutter/testing/dart/color_test.dart‎

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,6 @@ void main() {
102102
);
103103
});
104104

105-
test('Color.lerp different colorspaces', () {
106-
var didThrow = false;
107-
try {
108-
Color.lerp(
109-
const Color.from(alpha: 1, red: 1, green: 0, blue: 0, colorSpace: ColorSpace.displayP3),
110-
const Color.from(alpha: 1, red: 1, green: 0, blue: 0),
111-
0.0,
112-
);
113-
} catch (ex) {
114-
didThrow = true;
115-
}
116-
expect(didThrow, isTrue);
117-
});
118-
119105
test('Color.lerp same colorspaces', () {
120106
expect(
121107
Color.lerp(
@@ -129,6 +115,83 @@ void main() {
129115
);
130116
});
131117

118+
test('Color.lerp mixed colorspaces produces displayP3 result', () {
119+
final Color? result = Color.lerp(
120+
const Color.from(alpha: 1, red: 1, green: 0, blue: 0, colorSpace: ColorSpace.displayP3),
121+
const Color.from(alpha: 1, red: 0, green: 0, blue: 1),
122+
0.0,
123+
);
124+
expect(result!.colorSpace, equals(ColorSpace.displayP3));
125+
});
126+
127+
test('Color.lerp mixed colorspaces sRGB x and displayP3 y at t=0', () {
128+
final Color? result = Color.lerp(
129+
const Color.from(alpha: 1, red: 1, green: 0, blue: 0),
130+
const Color.from(alpha: 1, red: 0, green: 1, blue: 0, colorSpace: ColorSpace.displayP3),
131+
0.0,
132+
);
133+
expect(result!.colorSpace, equals(ColorSpace.displayP3));
134+
final Color expectedP3 = const Color.from(
135+
alpha: 1,
136+
red: 1,
137+
green: 0,
138+
blue: 0,
139+
).withValues(colorSpace: ColorSpace.displayP3);
140+
expect(result, colorMatches(expectedP3));
141+
});
142+
143+
test('Color.lerp mixed colorspaces displayP3 x and sRGB y at t=1', () {
144+
final Color? result = Color.lerp(
145+
const Color.from(alpha: 1, red: 0, green: 1, blue: 0, colorSpace: ColorSpace.displayP3),
146+
const Color.from(alpha: 1, red: 1, green: 0, blue: 0),
147+
1.0,
148+
);
149+
expect(result!.colorSpace, equals(ColorSpace.displayP3));
150+
final Color expectedP3 = const Color.from(
151+
alpha: 1,
152+
red: 1,
153+
green: 0,
154+
blue: 0,
155+
).withValues(colorSpace: ColorSpace.displayP3);
156+
expect(result, colorMatches(expectedP3));
157+
});
158+
159+
test('Color.lerp mixed colorspaces at midpoint', () {
160+
const srgbRed = Color.from(alpha: 1, red: 1, green: 0, blue: 0);
161+
const p3Green = Color.from(
162+
alpha: 1,
163+
red: 0,
164+
green: 1,
165+
blue: 0,
166+
colorSpace: ColorSpace.displayP3,
167+
);
168+
final Color? result = Color.lerp(srgbRed, p3Green, 0.5);
169+
expect(result!.colorSpace, equals(ColorSpace.displayP3));
170+
final Color srgbRedAsP3 = srgbRed.withValues(colorSpace: ColorSpace.displayP3);
171+
final expected = Color.from(
172+
alpha: (srgbRedAsP3.a + p3Green.a) / 2,
173+
red: (srgbRedAsP3.r + p3Green.r) / 2,
174+
green: (srgbRedAsP3.g + p3Green.g) / 2,
175+
blue: (srgbRedAsP3.b + p3Green.b) / 2,
176+
colorSpace: ColorSpace.displayP3,
177+
);
178+
expect(result, colorMatches(expected, threshold: 1e-4));
179+
});
180+
181+
test('Color.lerp mixed colorspaces with different alpha values', () {
182+
const srgbColor = Color.from(alpha: 0.5, red: 1, green: 0, blue: 0);
183+
const p3Color = Color.from(
184+
alpha: 1,
185+
red: 0,
186+
green: 0,
187+
blue: 1,
188+
colorSpace: ColorSpace.displayP3,
189+
);
190+
final Color? result = Color.lerp(srgbColor, p3Color, 0.5);
191+
expect(result!.colorSpace, equals(ColorSpace.displayP3));
192+
expect(result.a, closeTo(0.75, 1e-4));
193+
});
194+
132195
test('Color.alphaBlend', () {
133196
expect(
134197
Color.alphaBlend(const Color(0x00000000), const Color(0x00000000)),

0 commit comments

Comments
 (0)