Framework wide color (flutter/engine#54415)

issue: https://github.com/flutter/flutter/issues/127855
integration test: https://github.com/flutter/engine/pull/54415

This does the preliminary work for implementing wide gamut colors in the
Flutter framework. Here are the following changes:
1) colors now specify a colorspace with which they are to be interpreted
1) colors now store their components as floats to accommodate bit depths
more than 8

The storage of this Color class is weird with float/int storage but that
is a temporary solution to support a smooth transition. Here is the plan
for landing this:
1) Land this PR
1) Wait for it to roll into the Framework
1) Land https://github.com/flutter/flutter/pull/153938 which will make
CupertinoDynamicColor implement Color
1) Land another engine PR that rips out the int storage:
https://github.com/flutter/engine/pull/54714

Here are follow up PRs:
1) https://github.com/flutter/engine/pull/54473 - changes DlColor so the
wide gamut colors are rendered
1) https://github.com/flutter/engine/pull/54567 - Hooks up these changes
to take advantage of wide DlColor
1) https://github.com/flutter/flutter/pull/153319 - the integration test
for the framework repo

There are some things that have been left as follow up PRs since they
are technically breaking:
1) The math on `lerp` hasn't been updated to take advantage of the
higher bit depth
1) `operator==` hasn't been updated to take advantage of the higher bit
depth
1) `hashCode` hasn't been updated to take advantage of the higher bit
depth
1) `alphaBlend` hasn't been updated to take advantage of the higher bit
depth
1) `toString` hasn't been updated to take advantage of the higher bit
depth

## 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] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
gaaclarke
2024-08-22 12:36:28 -07:00
committed by GitHub
parent a967a4758f
commit 6f9776424c
6 changed files with 809 additions and 78 deletions

View File

@@ -95,6 +95,15 @@ base class RenderTarget {
final DepthStencilAttachment? depthStencilAttachment;
}
// TODO(gaaclarke): Refactor this to support wide gamut colors.
int _colorToInt(ui.Color color) {
assert(color.colorSpace == ui.ColorSpace.sRGB);
return ((color.a * 255.0).round() << 24) |
((color.r * 255.0).round() << 16) |
((color.g * 255.0).round() << 8) |
((color.b * 255.0).round() << 0);
}
base class RenderPass extends NativeFieldWrapperClass1 {
/// Creates a new RenderPass.
RenderPass._(CommandBuffer commandBuffer, RenderTarget renderTarget) {
@@ -105,7 +114,7 @@ base class RenderPass extends NativeFieldWrapperClass1 {
index,
color.loadAction.index,
color.storeAction.index,
color.clearValue.value,
_colorToInt(color.clearValue),
color.texture,
color.resolveTexture);
if (error != null) {

View File

@@ -26,7 +26,7 @@ double? lerpDouble(num? a, num? b, double t) {
///
/// Same as [lerpDouble] but specialized for non-null `double` type.
double _lerpDouble(double a, double b, double t) {
return a * (1.0 - t) + b * t;
return a + (b - a) * t;
}
/// Linearly interpolate between two integers.

View File

@@ -83,7 +83,7 @@ Color _scaleAlpha(Color a, double factor) {
/// * [Colors](https://api.flutter.dev/flutter/material/Colors-class.html), which
/// defines the colors found in the Material Design specification.
class Color {
/// Construct a color from the lower 32 bits of an [int].
/// Construct an sRGB color from the lower 32 bits of an [int].
///
/// The bits are interpreted as follows:
///
@@ -99,9 +99,32 @@ class Color {
/// For example, to get a fully opaque orange, you would use `const
/// Color(0xFFFF9000)` (`FF` for the alpha, `FF` for the red, `90` for the
/// green, and `00` for the blue).
const Color(int value) : value = value & 0xFFFFFFFF;
const Color(int value)
: _value = value & 0xFFFFFFFF,
colorSpace = ColorSpace.sRGB,
_a = null,
_r = null,
_g = null,
_b = null;
/// Construct a color from the lower 8 bits of four integers.
/// Construct a color with normalized color components.
///
/// Normalized color components allows arbitrary bit depths for color
/// components to be be supported. The values will be normalized relative to
/// the [ColorSpace] argument.
const Color.from(
{required double alpha,
required double red,
required double green,
required double blue,
this.colorSpace = ColorSpace.sRGB})
: _value = 0,
_a = alpha,
_r = red,
_g = green,
_b = blue;
/// Construct an sRGB color from the lower 8 bits of four integers.
///
/// * `a` is the alpha value, with 0 being transparent and 255 being fully
/// opaque.
@@ -113,13 +136,32 @@ class Color {
///
/// See also [fromRGBO], which takes the alpha value as a floating point
/// value.
const Color.fromARGB(int a, int r, int g, int b) :
value = (((a & 0xff) << 24) |
((r & 0xff) << 16) |
((g & 0xff) << 8) |
((b & 0xff) << 0)) & 0xFFFFFFFF;
const Color.fromARGB(int a, int r, int g, int b)
: _value = (((a & 0xff) << 24) |
((r & 0xff) << 16) |
((g & 0xff) << 8) |
((b & 0xff) << 0)) &
0xFFFFFFFF,
colorSpace = ColorSpace.sRGB,
_a = null,
_r = null,
_g = null,
_b = null;
/// Create a color from red, green, blue, and opacity, similar to `rgba()` in CSS.
const Color._fromARGBC(
int alpha, int red, int green, int blue, this.colorSpace)
: _value = (((alpha & 0xff) << 24) |
((red & 0xff) << 16) |
((green & 0xff) << 8) |
((blue & 0xff) << 0)) &
0xFFFFFFFF,
_a = null,
_r = null,
_g = null,
_b = null;
/// Create an sRGB color from red, green, blue, and opacity, similar to
/// `rgba()` in CSS.
///
/// * `r` is [red], from 0 to 255.
/// * `g` is [green], from 0 to 255.
@@ -130,11 +172,43 @@ class Color {
/// Out of range values are brought into range using modulo 255.
///
/// See also [fromARGB], which takes the opacity as an integer value.
const Color.fromRGBO(int r, int g, int b, double opacity) :
value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) |
((r & 0xff) << 16) |
((g & 0xff) << 8) |
((b & 0xff) << 0)) & 0xFFFFFFFF;
const Color.fromRGBO(int r, int g, int b, double opacity)
: _value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) |
((r & 0xff) << 16) |
((g & 0xff) << 8) |
((b & 0xff) << 0)) &
0xFFFFFFFF,
colorSpace = ColorSpace.sRGB,
_a = null,
_r = null,
_g = null,
_b = null;
/// The alpha channel of this color.
///
/// A value of 0.0 means this color is fully transparent. A value of 1.0 means
/// this color is fully opaque.
double get a => _a ?? (alpha / 255);
final double? _a;
/// The red channel of this color.
double get r => _r ?? (red / 255);
final double? _r;
/// The green channel of this color.
double get g => _g ?? (green / 255);
final double? _g;
/// The blue channel of this color.
double get b => _b ?? (blue / 255);
final double? _b;
/// The color space of this color.
final ColorSpace colorSpace;
static int _floatToInt8(double x) {
return ((x * 255.0).round()) & 0xff;
}
/// A 32 bit value representing this color.
///
@@ -144,29 +218,74 @@ class Color {
/// * Bits 16-23 are the red value.
/// * Bits 8-15 are the green value.
/// * Bits 0-7 are the blue value.
final int value;
@Deprecated('Use component accessors like .r or .g.')
int get value {
if (_a != null && _r != null && _g != null && _b != null) {
return _floatToInt8(_a) << 24 |
_floatToInt8(_r) << 16 |
_floatToInt8(_g) << 8 |
_floatToInt8(_b) << 0;
} else {
return _value;
}
}
final int _value;
/// The alpha channel of this color in an 8 bit value.
///
/// A value of 0 means this color is fully transparent. A value of 255 means
/// this color is fully opaque.
@Deprecated('Use .a.')
int get alpha => (0xff000000 & value) >> 24;
/// The alpha channel of this color as a double.
///
/// A value of 0.0 means this color is fully transparent. A value of 1.0 means
/// this color is fully opaque.
@Deprecated('Use .a.')
double get opacity => alpha / 0xFF;
/// The red channel of this color in an 8 bit value.
@Deprecated('Use .r.')
int get red => (0x00ff0000 & value) >> 16;
/// The green channel of this color in an 8 bit value.
@Deprecated('Use .g.')
int get green => (0x0000ff00 & value) >> 8;
/// The blue channel of this color in an 8 bit value.
@Deprecated('Use .b.')
int get blue => (0x000000ff & value) >> 0;
/// Returns a new color that matches this color with the passed in components
/// changed.
///
/// Changes to color components will be applied before applying changes to the
/// color space.
Color withValues(
{double? alpha,
double? red,
double? green,
double? blue,
ColorSpace? colorSpace}) {
Color? updatedComponents;
if (alpha != null || red != null || green != null || blue != null) {
updatedComponents = Color.from(
alpha: alpha ?? a,
red: red ?? r,
green: green ?? g,
blue: blue ?? b,
colorSpace: this.colorSpace);
}
if (colorSpace != null && colorSpace != this.colorSpace) {
final _ColorTransform transform =
_getColorTransform(this.colorSpace, colorSpace);
return transform.transform(updatedComponents ?? this, colorSpace);
} else {
return updatedComponents ?? this;
}
}
/// Returns a new color that matches this color with the alpha channel
/// replaced with `a` (which ranges from 0 to 255).
///
@@ -179,6 +298,7 @@ class Color {
/// replaced with the given `opacity` (which ranges from 0.0 to 1.0).
///
/// Out of range values will have unexpected effects.
@Deprecated('Use .withValues() to avoid precision loss.')
Color withOpacity(double opacity) {
assert(opacity >= 0.0 && opacity <= 1.0);
return withAlpha((255.0 * opacity).round());
@@ -253,6 +373,10 @@ class Color {
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static Color? lerp(Color? a, Color? b, double t) {
// TODO(gaaclarke): Update math to use floats. This was already attempted
// but it leads to subtle changes that change test results.
assert(a?.colorSpace != ColorSpace.extendedSRGB);
assert(b?.colorSpace != ColorSpace.extendedSRGB);
if (b == null) {
if (a == null) {
return null;
@@ -263,11 +387,13 @@ class Color {
if (a == null) {
return _scaleAlpha(b, t);
} else {
return Color.fromARGB(
assert(a.colorSpace == b.colorSpace);
return Color._fromARGBC(
_clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
_clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
_clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
_clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
a.colorSpace,
);
}
}
@@ -282,6 +408,10 @@ class Color {
/// operations for two things that are solid colors with the same shape, but
/// overlay each other: instead, just paint one with the combined color.
static Color alphaBlend(Color foreground, Color background) {
assert(foreground.colorSpace == background.colorSpace);
assert(foreground.colorSpace != ColorSpace.extendedSRGB);
// TODO(gaaclarke): Update math to use floats. This was already attempted
// but it leads to subtle changes that change test results.
final int alpha = foreground.alpha;
if (alpha == 0x00) { // Foreground completely transparent.
return background;
@@ -289,21 +419,23 @@ class Color {
final int invAlpha = 0xff - alpha;
int backAlpha = background.alpha;
if (backAlpha == 0xff) { // Opaque background case
return Color.fromARGB(
return Color._fromARGBC(
0xff,
(alpha * foreground.red + invAlpha * background.red) ~/ 0xff,
(alpha * foreground.green + invAlpha * background.green) ~/ 0xff,
(alpha * foreground.blue + invAlpha * background.blue) ~/ 0xff,
foreground.colorSpace,
);
} else { // General case
backAlpha = (backAlpha * invAlpha) ~/ 0xff;
final int outAlpha = alpha + backAlpha;
assert(outAlpha != 0x00);
return Color.fromARGB(
return Color._fromARGBC(
outAlpha,
(foreground.red * alpha + background.red * backAlpha) ~/ outAlpha,
(foreground.green * alpha + background.green * backAlpha) ~/ outAlpha,
(foreground.blue * alpha + background.blue * backAlpha) ~/ outAlpha,
foreground.colorSpace,
);
}
}
@@ -323,13 +455,15 @@ class Color {
if (other.runtimeType != runtimeType) {
return false;
}
return other is Color
&& other.value == value;
return other is Color &&
other.value == value &&
other.colorSpace == colorSpace;
}
@override
int get hashCode => value.hashCode;
int get hashCode => Object.hash(value, colorSpace);
// TODO(gaaclarke): Make toString() print out float values.
@override
String toString() => 'Color(0x${value.toRadixString(16).padLeft(8, '0')})';
}
@@ -1126,22 +1260,31 @@ final class Paint {
@pragma('vm:entry-point')
final ByteData _data = ByteData(_kDataByteCount);
// Must match //lib/ui/painting/paint.cc.
static const int _kIsAntiAliasIndex = 0;
static const int _kColorIndex = 1;
static const int _kBlendModeIndex = 2;
static const int _kStyleIndex = 3;
static const int _kStrokeWidthIndex = 4;
static const int _kStrokeCapIndex = 5;
static const int _kStrokeJoinIndex = 6;
static const int _kStrokeMiterLimitIndex = 7;
static const int _kFilterQualityIndex = 8;
static const int _kMaskFilterIndex = 9;
static const int _kMaskFilterBlurStyleIndex = 10;
static const int _kMaskFilterSigmaIndex = 11;
static const int _kInvertColorIndex = 12;
static const int _kColorRedIndex = 1;
static const int _kColorGreenIndex = 2;
static const int _kColorBlueIndex = 3;
static const int _kColorAlphaIndex = 4;
static const int _kColorSpaceIndex = 5;
static const int _kBlendModeIndex = 6;
static const int _kStyleIndex = 7;
static const int _kStrokeWidthIndex = 8;
static const int _kStrokeCapIndex = 9;
static const int _kStrokeJoinIndex = 10;
static const int _kStrokeMiterLimitIndex = 11;
static const int _kFilterQualityIndex = 12;
static const int _kMaskFilterIndex = 13;
static const int _kMaskFilterBlurStyleIndex = 14;
static const int _kMaskFilterSigmaIndex = 15;
static const int _kInvertColorIndex = 16;
static const int _kIsAntiAliasOffset = _kIsAntiAliasIndex << 2;
static const int _kColorOffset = _kColorIndex << 2;
static const int _kColorRedOffset = _kColorRedIndex << 2;
static const int _kColorGreenOffset = _kColorGreenIndex << 2;
static const int _kColorBlueOffset = _kColorBlueIndex << 2;
static const int _kColorAlphaOffset = _kColorAlphaIndex << 2;
static const int _kColorSpaceOffset = _kColorSpaceIndex << 2;
static const int _kBlendModeOffset = _kBlendModeIndex << 2;
static const int _kStyleOffset = _kStyleIndex << 2;
static const int _kStrokeWidthOffset = _kStrokeWidthIndex << 2;
@@ -1155,7 +1298,7 @@ final class Paint {
static const int _kInvertColorOffset = _kInvertColorIndex << 2;
// If you add more fields, remember to update _kDataByteCount.
static const int _kDataByteCount = 52; // 4 * (last index + 1).
static const int _kDataByteCount = 68; // 4 * (last index + 1).
// Binary format must match the deserialization code in paint.cc.
// C++ unit tests access this.
@@ -1201,12 +1344,28 @@ final class Paint {
/// This color is not used when compositing. To colorize a layer, use
/// [colorFilter].
Color get color {
final int encoded = _data.getInt32(_kColorOffset, _kFakeHostEndian);
return Color(encoded ^ _kColorDefault);
final double red = _data.getFloat32(_kColorRedOffset, _kFakeHostEndian);
final double green = _data.getFloat32(_kColorGreenOffset, _kFakeHostEndian);
final double blue = _data.getFloat32(_kColorBlueOffset, _kFakeHostEndian);
final double alpha =
1.0 - _data.getFloat32(_kColorAlphaOffset, _kFakeHostEndian);
final ColorSpace colorSpace = _indexToColorSpace(
_data.getInt32(_kColorSpaceOffset, _kFakeHostEndian));
return Color.from(
alpha: alpha,
red: red,
green: green,
blue: blue,
colorSpace: colorSpace);
}
set color(Color value) {
final int encoded = value.value ^ _kColorDefault;
_data.setInt32(_kColorOffset, encoded, _kFakeHostEndian);
_data.setFloat32(_kColorRedOffset, value.r, _kFakeHostEndian);
_data.setFloat32(_kColorGreenOffset, value.g, _kFakeHostEndian);
_data.setFloat32(_kColorBlueOffset, value.b, _kFakeHostEndian);
_data.setFloat32(_kColorAlphaOffset, 1.0 - value.a, _kFakeHostEndian);
_data.setInt32(_kColorSpaceOffset, _colorSpaceToIndex(value.colorSpace),
_kFakeHostEndian);
}
// Must be kept in sync with the default in paint.cc.
@@ -1580,6 +1739,38 @@ enum ColorSpace {
/// see the extended values an [ImageByteFormat] like
/// [ImageByteFormat.rawExtendedRgba128] must be used.
extendedSRGB,
/// The Display P3 color space.
///
/// This is a wide gamut color space that has broad hardware support. It's
/// supported in cases like using Impeller on iOS. When used on a platform
/// that doesn't support Display P3, the colors will be clamped to sRGB.
///
/// See also: https://en.wikipedia.org/wiki/DCI-P3
displayP3,
}
int _colorSpaceToIndex(ColorSpace colorSpace) {
switch (colorSpace) {
case ColorSpace.sRGB:
return 0;
case ColorSpace.extendedSRGB:
return 1;
case ColorSpace.displayP3:
return 2;
}
}
ColorSpace _indexToColorSpace(int index) {
switch(index) {
case 0:
return ColorSpace.sRGB;
case 1:
return ColorSpace.extendedSRGB;
case 2:
return ColorSpace.displayP3;
default:
throw ArgumentError('Unknown color space: $index');
}
}
/// The format in which image bytes should be returned when using
@@ -3464,6 +3655,121 @@ class MaskFilter {
String toString() => 'MaskFilter.blur($_style, ${_sigma.toStringAsFixed(1)})';
}
abstract class _ColorTransform {
Color transform(Color color, ColorSpace resultColorSpace);
}
class _IdentityColorTransform implements _ColorTransform {
const _IdentityColorTransform();
@override
Color transform(Color color, ColorSpace resultColorSpace) => color;
}
class _ClampTransform implements _ColorTransform {
const _ClampTransform(this.child);
final _ColorTransform child;
@override
Color transform(Color color, ColorSpace resultColorSpace) {
return Color.from(
alpha: clampDouble(color.a, 0, 1),
red: clampDouble(color.r, 0, 1),
green: clampDouble(color.g, 0, 1),
blue: clampDouble(color.b, 0, 1),
colorSpace: resultColorSpace);
}
}
class _MatrixColorTransform implements _ColorTransform {
/// Row-major.
const _MatrixColorTransform(this.values);
final List<double> values;
@override
Color transform(Color color, ColorSpace resultColorSpace) {
return Color.from(
alpha: color.a,
red: values[0] * color.r +
values[1] * color.g +
values[2] * color.b +
values[3],
green: values[4] * color.r +
values[5] * color.g +
values[6] * color.b +
values[7],
blue: values[8] * color.r +
values[9] * color.g +
values[10] * color.b +
values[11],
colorSpace: resultColorSpace);
}
}
_ColorTransform _getColorTransform(ColorSpace source, ColorSpace destination) {
// The transforms were calculated with the following octave script from known
// conversions. These transforms have a white point that matches Apple's.
//
// p3Colors = [
// 1, 0, 0, 0.25;
// 0, 1, 0, 0.5;
// 0, 0, 1, 0.75;
// 1, 1, 1, 1;
// ];
// srgbColors = [
// 1.0930908918380737, -0.5116420984268188, -0.0003518527664709836, 0.12397786229848862;
// -0.22684034705162048, 1.0182716846466064, 0.00027732315356843174, 0.5073589086532593;
// -0.15007957816123962, -0.31062406301498413, 1.0420056581497192, 0.771118700504303;
// 1, 1, 1, 1;
// ];
//
// format long
// p3ToSrgb = srgbColors * inv(p3Colors)
// srgbToP3 = inv(p3ToSrgb)
const _MatrixColorTransform srgbToP3 = _MatrixColorTransform(<double>[
0.808052267214446, 0.220292047628890, -0.139648846160100,
0.145738111193222, //
0.096480880462996, 0.916386732581291, -0.086093928394828,
0.089490172325882, //
-0.127099563510240, -0.068983484963878, 0.735426667591299, 0.233655661600230
]);
const _ColorTransform p3ToSrgb = _MatrixColorTransform(<double>[
1.306671048092539, -0.298061942172353, 0.213228303487995,
-0.213580156254466, //
-0.117390025596251, 1.127722006101976, 0.109727644608938,
-0.109450321455370, //
0.214813187718391, 0.054268702864647, 1.406898424029350, -0.364892765879631
]);
switch (source) {
case ColorSpace.sRGB:
switch (destination) {
case ColorSpace.sRGB:
return const _IdentityColorTransform();
case ColorSpace.extendedSRGB:
return const _IdentityColorTransform();
case ColorSpace.displayP3:
return srgbToP3;
}
case ColorSpace.extendedSRGB:
switch (destination) {
case ColorSpace.sRGB:
return const _ClampTransform(_IdentityColorTransform());
case ColorSpace.extendedSRGB:
return const _IdentityColorTransform();
case ColorSpace.displayP3:
return const _ClampTransform(srgbToP3);
}
case ColorSpace.displayP3:
switch (destination) {
case ColorSpace.sRGB:
return const _ClampTransform(p3ToSrgb);
case ColorSpace.extendedSRGB:
return p3ToSrgb;
case ColorSpace.displayP3:
return const _IdentityColorTransform();
}
}
}
/// A description of a color filter to apply when drawing a shape or compositing
/// a layer with a particular [Paint]. A color filter is a function that takes
/// two colors, and outputs one color. When applied during compositing, it is

View File

@@ -21,20 +21,25 @@
namespace flutter {
// Indices for 32bit values.
// Must match //lib/ui/painting.dart.
constexpr int kIsAntiAliasIndex = 0;
constexpr int kColorIndex = 1;
constexpr int kBlendModeIndex = 2;
constexpr int kStyleIndex = 3;
constexpr int kStrokeWidthIndex = 4;
constexpr int kStrokeCapIndex = 5;
constexpr int kStrokeJoinIndex = 6;
constexpr int kStrokeMiterLimitIndex = 7;
constexpr int kFilterQualityIndex = 8;
constexpr int kMaskFilterIndex = 9;
constexpr int kMaskFilterBlurStyleIndex = 10;
constexpr int kMaskFilterSigmaIndex = 11;
constexpr int kInvertColorIndex = 12;
constexpr size_t kDataByteCount = 52; // 4 * (last index + 1)
constexpr int kColorRedIndex = 1;
constexpr int kColorGreenIndex = 2;
constexpr int kColorBlueIndex = 3;
constexpr int kColorAlphaIndex = 4;
constexpr int kColorSpaceIndex = 5;
constexpr int kBlendModeIndex = 6;
constexpr int kStyleIndex = 7;
constexpr int kStrokeWidthIndex = 8;
constexpr int kStrokeCapIndex = 9;
constexpr int kStrokeJoinIndex = 10;
constexpr int kStrokeMiterLimitIndex = 11;
constexpr int kFilterQualityIndex = 12;
constexpr int kMaskFilterIndex = 13;
constexpr int kMaskFilterBlurStyleIndex = 14;
constexpr int kMaskFilterSigmaIndex = 15;
constexpr int kInvertColorIndex = 16;
constexpr size_t kDataByteCount = 68; // 4 * (last index + 1)
static_assert(kDataByteCount == sizeof(uint32_t) * (kInvertColorIndex + 1),
"kDataByteCount must match the size of the data array.");
@@ -44,9 +49,6 @@ constexpr int kColorFilterIndex = 1;
constexpr int kImageFilterIndex = 2;
constexpr int kObjectCount = 3; // One larger than largest object index.
// Must be kept in sync with the default in painting.dart.
constexpr uint32_t kColorDefault = 0xFF000000;
// Must be kept in sync with the default in painting.dart.
constexpr uint32_t kBlendModeDefault =
static_cast<uint32_t>(SkBlendMode::kSrcOver);
@@ -58,6 +60,28 @@ constexpr float kStrokeMiterLimitDefault = 4.0f;
// Must be kept in sync with the MaskFilter private constants in painting.dart.
enum MaskFilterType { kNull, kBlur };
namespace {
DlColor ReadColor(const tonic::DartByteData& byte_data) {
const uint32_t* uint_data = static_cast<const uint32_t*>(byte_data.data());
const float* float_data = static_cast<const float*>(byte_data.data());
float red = float_data[kColorRedIndex];
float green = float_data[kColorGreenIndex];
float blue = float_data[kColorBlueIndex];
// Invert alpha so 0 initialized buffer has default value;
float alpha = 1.f - float_data[kColorAlphaIndex];
uint32_t colorspace = uint_data[kColorSpaceIndex];
(void)colorspace;
uint32_t encoded_color =
static_cast<uint8_t>(std::round(alpha * 255.f)) << 24 | //
static_cast<uint8_t>(std::round(red * 255.f)) << 16 | //
static_cast<uint8_t>(std::round(green * 255.f)) << 8 | //
static_cast<uint8_t>(std::round(blue * 255.f)) << 0;
// TODO(gaaclarke): Pass down color info to DlColor.
return DlColor(encoded_color);
}
} // namespace
Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data)
: paint_objects_(paint_objects), paint_data_(paint_data) {}
@@ -137,8 +161,7 @@ const DlPaint* Paint::paint(DlPaint& paint,
}
if (flags.applies_alpha_or_color()) {
uint32_t encoded_color = uint_data[kColorIndex];
paint.setColor(DlColor(encoded_color ^ kColorDefault));
paint.setColor(ReadColor(byte_data));
}
if (flags.applies_blend()) {
@@ -238,8 +261,7 @@ void Paint::toDlPaint(DlPaint& paint) const {
paint.setAntiAlias(uint_data[kIsAntiAliasIndex] == 0);
uint32_t encoded_color = uint_data[kColorIndex];
paint.setColor(DlColor(encoded_color ^ kColorDefault));
paint.setColor(ReadColor(byte_data));
uint32_t encoded_blend_mode = uint_data[kBlendModeIndex];
uint32_t blend_mode = encoded_blend_mode ^ kBlendModeDefault;

View File

@@ -22,25 +22,126 @@ Color _scaleAlpha(Color a, double factor) {
}
class Color {
const Color(int value) : value = value & 0xFFFFFFFF;
const Color(int value)
: _value = value & 0xFFFFFFFF,
colorSpace = ColorSpace.sRGB,
_a = null,
_r = null,
_g = null,
_b = null;
const Color.from(
{required double alpha,
required double red,
required double green,
required double blue,
this.colorSpace = ColorSpace.sRGB})
: _value = 0,
_a = alpha,
_r = red,
_g = green,
_b = blue;
const Color.fromARGB(int a, int r, int g, int b)
: value = (((a & 0xff) << 24) |
: _value = (((a & 0xff) << 24) |
((r & 0xff) << 16) |
((g & 0xff) << 8) |
((b & 0xff) << 0)) &
0xFFFFFFFF;
0xFFFFFFFF,
colorSpace = ColorSpace.sRGB,
_a = null,
_r = null,
_g = null,
_b = null;
const Color._fromARGBC(
int alpha, int red, int green, int blue, this.colorSpace)
: _value = (((alpha & 0xff) << 24) |
((red & 0xff) << 16) |
((green & 0xff) << 8) |
((blue & 0xff) << 0)) &
0xFFFFFFFF,
_a = null,
_r = null,
_g = null,
_b = null;
const Color.fromRGBO(int r, int g, int b, double opacity)
: value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) |
: _value = ((((opacity * 0xff ~/ 1) & 0xff) << 24) |
((r & 0xff) << 16) |
((g & 0xff) << 8) |
((b & 0xff) << 0)) &
0xFFFFFFFF;
final int value;
0xFFFFFFFF,
colorSpace = ColorSpace.sRGB,
_a = null,
_r = null,
_g = null,
_b = null;
double get a => _a ?? (alpha / 255);
final double? _a;
double get r => _r ?? (red / 255);
final double? _r;
double get g => _g ?? (green / 255);
final double? _g;
double get b => _b ?? (blue / 255);
final double? _b;
final ColorSpace colorSpace;
static int _floatToInt8(double x) {
return ((x * 255.0).round()) & 0xff;
}
int get value {
if (_a != null && _r != null && _g != null && _b != null) {
return _floatToInt8(_a) << 24 |
_floatToInt8(_r) << 16 |
_floatToInt8(_g) << 8 |
_floatToInt8(_b) << 0;
} else {
return _value;
}
}
final int _value;
int get alpha => (0xff000000 & value) >> 24;
double get opacity => alpha / 0xFF;
int get red => (0x00ff0000 & value) >> 16;
int get green => (0x0000ff00 & value) >> 8;
int get blue => (0x000000ff & value) >> 0;
Color withValues(
{double? alpha,
double? red,
double? green,
double? blue,
ColorSpace? colorSpace}) {
Color? updatedComponents;
if (alpha != null || red != null || green != null || blue != null) {
updatedComponents = Color.from(
alpha: alpha ?? a,
red: red ?? r,
green: green ?? g,
blue: blue ?? b,
colorSpace: this.colorSpace);
}
if (colorSpace != null && colorSpace != this.colorSpace) {
final _ColorTransform transform =
_getColorTransform(this.colorSpace, colorSpace);
return transform.transform(updatedComponents ?? this, colorSpace);
} else {
return updatedComponents ?? this;
}
}
Color withAlpha(int a) {
return Color.fromARGB(a, red, green, blue);
}
@@ -79,6 +180,8 @@ class Color {
}
static Color? lerp(Color? a, Color? b, double t) {
assert(a?.colorSpace != ColorSpace.extendedSRGB);
assert(b?.colorSpace != ColorSpace.extendedSRGB);
if (b == null) {
if (a == null) {
return null;
@@ -89,42 +192,45 @@ class Color {
if (a == null) {
return _scaleAlpha(b, t);
} else {
return Color.fromARGB(
assert(a.colorSpace == b.colorSpace);
return Color._fromARGBC(
engine.clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
engine.clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
engine.clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
engine.clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
a.colorSpace,
);
}
}
}
static Color alphaBlend(Color foreground, Color background) {
assert(foreground.colorSpace == background.colorSpace);
assert(foreground.colorSpace != ColorSpace.extendedSRGB);
final int alpha = foreground.alpha;
if (alpha == 0x00) {
// Foreground completely transparent.
return background;
}
final int invAlpha = 0xff - alpha;
int backAlpha = background.alpha;
if (backAlpha == 0xff) {
// Opaque background case
return Color.fromARGB(
return Color._fromARGBC(
0xff,
(alpha * foreground.red + invAlpha * background.red) ~/ 0xff,
(alpha * foreground.green + invAlpha * background.green) ~/ 0xff,
(alpha * foreground.blue + invAlpha * background.blue) ~/ 0xff,
foreground.colorSpace,
);
} else {
// General case
backAlpha = (backAlpha * invAlpha) ~/ 0xff;
final int outAlpha = alpha + backAlpha;
assert(outAlpha != 0x00);
return Color.fromARGB(
return Color._fromARGBC(
outAlpha,
(foreground.red * alpha + background.red * backAlpha) ~/ outAlpha,
(foreground.green * alpha + background.green * backAlpha) ~/ outAlpha,
(foreground.blue * alpha + background.blue * backAlpha) ~/ outAlpha,
foreground.colorSpace,
);
}
}
@@ -141,16 +247,16 @@ class Color {
if (other.runtimeType != runtimeType) {
return false;
}
return other is Color && other.value == value;
return other is Color &&
other.value == value &&
other.colorSpace == colorSpace;
}
@override
int get hashCode => value.hashCode;
int get hashCode => Object.hash(value, colorSpace);
@override
String toString() {
return 'Color(0x${value.toRadixString(16).padLeft(8, '0')})';
}
String toString() => 'Color(0x${value.toRadixString(16).padLeft(8, '0')})';
}
enum StrokeCap {
@@ -414,6 +520,101 @@ class MaskFilter {
String toString() => 'MaskFilter.blur($_style, ${_sigma.toStringAsFixed(1)})';
}
abstract class _ColorTransform {
Color transform(Color color, ColorSpace resultColorSpace);
}
class _IdentityColorTransform implements _ColorTransform {
const _IdentityColorTransform();
@override
Color transform(Color color, ColorSpace resultColorSpace) => color;
}
class _ClampTransform implements _ColorTransform {
const _ClampTransform(this.child);
final _ColorTransform child;
@override
Color transform(Color color, ColorSpace resultColorSpace) {
return Color.from(
alpha: clampDouble(color.a, 0, 1),
red: clampDouble(color.r, 0, 1),
green: clampDouble(color.g, 0, 1),
blue: clampDouble(color.b, 0, 1),
colorSpace: resultColorSpace);
}
}
class _MatrixColorTransform implements _ColorTransform {
const _MatrixColorTransform(this.values);
final List<double> values;
@override
Color transform(Color color, ColorSpace resultColorSpace) {
return Color.from(
alpha: color.a,
red: values[0] * color.r +
values[1] * color.g +
values[2] * color.b +
values[3],
green: values[4] * color.r +
values[5] * color.g +
values[6] * color.b +
values[7],
blue: values[8] * color.r +
values[9] * color.g +
values[10] * color.b +
values[11],
colorSpace: resultColorSpace);
}
}
_ColorTransform _getColorTransform(ColorSpace source, ColorSpace destination) {
const _MatrixColorTransform srgbToP3 = _MatrixColorTransform(<double>[
0.808052267214446, 0.220292047628890, -0.139648846160100,
0.145738111193222, //
0.096480880462996, 0.916386732581291, -0.086093928394828,
0.089490172325882, //
-0.127099563510240, -0.068983484963878, 0.735426667591299, 0.233655661600230
]);
const _ColorTransform p3ToSrgb = _MatrixColorTransform(<double>[
1.306671048092539, -0.298061942172353, 0.213228303487995,
-0.213580156254466, //
-0.117390025596251, 1.127722006101976, 0.109727644608938,
-0.109450321455370, //
0.214813187718391, 0.054268702864647, 1.406898424029350, -0.364892765879631
]);
switch (source) {
case ColorSpace.sRGB:
switch (destination) {
case ColorSpace.sRGB:
return const _IdentityColorTransform();
case ColorSpace.extendedSRGB:
return const _IdentityColorTransform();
case ColorSpace.displayP3:
return srgbToP3;
}
case ColorSpace.extendedSRGB:
switch (destination) {
case ColorSpace.sRGB:
return const _ClampTransform(_IdentityColorTransform());
case ColorSpace.extendedSRGB:
return const _IdentityColorTransform();
case ColorSpace.displayP3:
return const _ClampTransform(srgbToP3);
}
case ColorSpace.displayP3:
switch (destination) {
case ColorSpace.sRGB:
return const _ClampTransform(p3ToSrgb);
case ColorSpace.extendedSRGB:
return p3ToSrgb;
case ColorSpace.displayP3:
return const _IdentityColorTransform();
}
}
}
// This needs to be kept in sync with the "_FilterQuality" enum in skwasm's canvas.cpp
enum FilterQuality {
none,
@@ -453,6 +654,7 @@ class ImageFilter {
enum ColorSpace {
sRGB,
extendedSRGB,
displayP3,
}
// This must be kept in sync with the `ImageByteFormat` enum in Skwasm's surface.cpp.

View File

@@ -10,6 +10,10 @@ class NotAColor extends Color {
const NotAColor(super.value);
}
Matcher approxEquals(dynamic o) => (v) {
Expect.approxEquals(o as num, v as num);
};
void main() {
test('color accessors should work', () {
const Color foo = Color(0x12345678);
@@ -76,6 +80,51 @@ void main() {
);
});
test('Color.lerp different colorspaces', () {
bool didThrow = false;
try {
Color.lerp(
const Color.from(
alpha: 1,
red: 1,
green: 0,
blue: 0,
colorSpace: ColorSpace.displayP3),
const Color.from(
alpha: 1, red: 1, green: 0, blue: 0),
0.0);
} catch (ex) {
didThrow = true;
}
expect(didThrow, isTrue);
});
test('Color.lerp same colorspaces', () {
expect(
Color.lerp(
const Color.from(
alpha: 1,
red: 0,
green: 0,
blue: 0,
colorSpace: ColorSpace.displayP3),
const Color.from(
alpha: 1,
red: 1,
green: 0,
blue: 0,
colorSpace: ColorSpace.displayP3),
0.2),
equals(
const Color.from(
alpha: 1,
red: 0.2,
green: 0,
blue: 0,
colorSpace: ColorSpace.displayP3),
));
});
test('Color.alphaBlend', () {
expect(
Color.alphaBlend(const Color(0x00000000), const Color(0x00000000)),
@@ -119,6 +168,29 @@ void main() {
);
});
test('Color.alphaBlend keeps colorspace', () {
expect(
Color.alphaBlend(
const Color.from(
alpha: 0.5,
red: 1,
green: 1,
blue: 1,
colorSpace: ColorSpace.displayP3),
const Color.from(
alpha: 1,
red: 0,
green: 0,
blue: 0,
colorSpace: ColorSpace.displayP3)),
const Color.from(
alpha: 1,
red: 0.5,
green: 0.5,
blue: 0.5,
colorSpace: ColorSpace.displayP3));
});
test('compute gray luminance', () {
// Each color component is at 20%.
const Color lightGray = Color(0xFF333333);
@@ -134,4 +206,124 @@ void main() {
// 0.0722 * ((0.18823529411 + 0.055) / 1.055) ^ 2.4
expect(brightRed.computeLuminance(), equals(0.24601329637099723));
});
test('from and accessors', () {
const Color color = Color.from(alpha: 0.1, red: 0.2, green: 0.3, blue: 0.4);
expect(color.a, equals(0.1));
expect(color.r, equals(0.2));
expect(color.g, equals(0.3));
expect(color.b, equals(0.4));
expect(color.colorSpace, equals(ColorSpace.sRGB));
expect(color.alpha, equals(26));
expect(color.red, equals(51));
expect(color.green, equals(77));
expect(color.blue, equals(102));
expect(color.value, equals(0x1a334d66));
});
test('fromARGB and accessors', () {
const Color color = Color.fromARGB(10, 20, 35, 47);
expect(color.alpha, equals(10));
expect(color.red, equals(20));
expect(color.green, equals(35));
expect(color.blue, equals(47));
});
test('constructor and accessors', () {
const Color color = Color(0xffeeddcc);
expect(color.alpha, equals(0xff));
expect(color.red, equals(0xee));
expect(color.green, equals(0xdd));
expect(color.blue, equals(0xcc));
});
test('p3 to extended srgb', () {
const Color p3 = Color.from(
alpha: 1, red: 1, green: 0, blue: 0, colorSpace: ColorSpace.displayP3);
final Color srgb = p3.withValues(colorSpace: ColorSpace.extendedSRGB);
expect(srgb.a, equals(1.0));
expect(srgb.r, approxEquals(1.0931));
expect(srgb.g, approxEquals(-0.22684034705162098));
expect(srgb.b, approxEquals(-0.15007957816123998));
expect(srgb.colorSpace, equals(ColorSpace.extendedSRGB));
});
test('p3 to srgb', () {
const Color p3 = Color.from(
alpha: 1, red: 1, green: 0, blue: 0, colorSpace: ColorSpace.displayP3);
final Color srgb = p3.withValues(colorSpace: ColorSpace.sRGB);
expect(srgb.a, equals(1.0));
expect(srgb.r, approxEquals(1));
expect(srgb.g, approxEquals(0));
expect(srgb.b, approxEquals(0));
expect(srgb.colorSpace, equals(ColorSpace.sRGB));
});
test('extended srgb to p3', () {
const Color srgb = Color.from(
alpha: 1,
red: 1.0931,
green: -0.2268,
blue: -0.1501,
colorSpace: ColorSpace.extendedSRGB);
final Color p3 = srgb.withValues(colorSpace: ColorSpace.displayP3);
expect(p3.a, equals(1.0));
expect(p3.r, approxEquals(1));
expect(p3.g, approxEquals(0));
expect(p3.b, approxEquals(0));
expect(p3.colorSpace, equals(ColorSpace.displayP3));
});
test('extended srgb to p3 clamped', () {
const Color srgb = Color.from(
alpha: 1,
red: 2,
green: 0,
blue: 0,
colorSpace: ColorSpace.extendedSRGB);
final Color p3 = srgb.withValues(colorSpace: ColorSpace.displayP3);
expect(srgb.a, equals(1.0));
expect(p3.r <= 1.0, isTrue);
expect(p3.g <= 1.0, isTrue);
expect(p3.b <= 1.0, isTrue);
expect(p3.r >= 0.0, isTrue);
expect(p3.g >= 0.0, isTrue);
expect(p3.b >= 0.0, isTrue);
});
test('hash considers colorspace', () {
const Color srgb = Color.from(
alpha: 1, red: 1, green: 0, blue: 0);
const Color p3 = Color.from(
alpha: 1, red: 1, green: 0, blue: 0, colorSpace: ColorSpace.displayP3);
expect(srgb.hashCode, notEquals(p3.hashCode));
});
test('equality considers colorspace', () {
const Color srgb = Color.from(
alpha: 1, red: 1, green: 0, blue: 0);
const Color p3 = Color.from(
alpha: 1, red: 1, green: 0, blue: 0, colorSpace: ColorSpace.displayP3);
expect(srgb, notEquals(p3));
});
// Regression test for https://github.com/flutter/flutter/issues/41257
// CupertinoDynamicColor was overriding base class and calling super(0).
test('subclass of Color can override value', () {
const DynamicColorClass color = DynamicColorClass(0xF0E0D0C0);
expect(color.value, 0xF0E0D0C0);
// Call base class member, make sure it uses overridden value.
expect(color.red, 0xE0);
});
}
class DynamicColorClass extends Color {
const DynamicColorClass(int newValue) : _newValue = newValue, super(0);
final int _newValue;
@override
int get value => _newValue;
}