From e872177c1fc025d5824e83f1ea38e1a6fb520a35 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:34:02 -0800 Subject: [PATCH] Exposing ColorFilter to ImageFilter conversion and Compose() (#20309) --- lib/ui/painting.dart | 268 ++++++++++++------ lib/ui/painting/image_filter.cc | 23 +- lib/ui/painting/image_filter.h | 3 + lib/web_ui/lib/src/engine/bitmap_canvas.dart | 22 +- .../lib/src/engine/canvaskit/canvas.dart | 4 +- .../src/engine/canvaskit/canvaskit_api.dart | 10 + .../src/engine/canvaskit/color_filter.dart | 181 ++++++++---- .../src/engine/canvaskit/image_filter.dart | 74 ++++- .../lib/src/engine/canvaskit/painting.dart | 31 +- lib/web_ui/lib/src/engine/color_filter.dart | 85 +----- .../lib/src/engine/html/color_filter.dart | 119 ++++---- lib/web_ui/lib/src/ui/painting.dart | 6 + .../test/canvaskit/canvaskit_api_test.dart | 20 ++ lib/web_ui/test/canvaskit/filter_test.dart | 96 +++++++ testing/dart/image_filter_test.dart | 176 ++++++++++++ 15 files changed, 797 insertions(+), 321 deletions(-) create mode 100644 lib/web_ui/test/canvaskit/filter_test.dart diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 58c379497a..7b63b50e01 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -2900,7 +2900,7 @@ class MaskFilter { /// /// Instances of this class are used with [Paint.colorFilter] on [Paint] /// objects. -class ColorFilter { +class ColorFilter implements ImageFilter { /// Creates a color filter that applies the blend mode given as the second /// argument. The source color is the one given as the first argument, and the /// destination color is the one from the layer being composited. @@ -2912,7 +2912,7 @@ class ColorFilter { : _color = color, _blendMode = blendMode, _matrix = null, - _type = _TypeMode; + _type = _kTypeMode; /// Construct a color filter that transforms a color by a 5x5 matrix, where /// the fifth row is implicitly added in an identity configuration. @@ -2978,7 +2978,7 @@ class ColorFilter { : _color = null, _blendMode = null, _matrix = matrix, - _type = _TypeMatrix; + _type = _kTypeMatrix; /// Construct a color filter that applies the sRGB gamma curve to the RGB /// channels. @@ -2986,7 +2986,7 @@ class ColorFilter { : _color = null, _blendMode = null, _matrix = null, - _type = _TypeLinearToSrgbGamma; + _type = _kTypeLinearToSrgbGamma; /// Creates a color filter that applies the inverse of the sRGB gamma curve /// to the RGB channels. @@ -2994,7 +2994,7 @@ class ColorFilter { : _color = null, _blendMode = null, _matrix = null, - _type = _TypeSrgbToLinearGamma; + _type = _kTypeSrgbToLinearGamma; final Color? _color; final BlendMode? _blendMode; @@ -3002,55 +3002,77 @@ class ColorFilter { final int _type; // The type of SkColorFilter class to create for Skia. - static const int _TypeMode = 1; // MakeModeFilter - static const int _TypeMatrix = 2; // MakeMatrixFilterRowMajor255 - static const int _TypeLinearToSrgbGamma = 3; // MakeLinearToSRGBGamma - static const int _TypeSrgbToLinearGamma = 4; // MakeSRGBToLinearGamma + static const int _kTypeMode = 1; // MakeModeFilter + static const int _kTypeMatrix = 2; // MakeMatrixFilterRowMajor255 + static const int _kTypeLinearToSrgbGamma = 3; // MakeLinearToSRGBGamma + static const int _kTypeSrgbToLinearGamma = 4; // MakeSRGBToLinearGamma + // SkImageFilters::ColorFilter @override - bool operator ==(Object other) { - return other is ColorFilter - && other._type == _type - && _listEquals(other._matrix, _matrix) - && other._color == _color - && other._blendMode == _blendMode; - } + _ImageFilter _toNativeImageFilter() => _ImageFilter.fromColorFilter(this); _ColorFilter? _toNativeColorFilter() { switch (_type) { - case _TypeMode: + case _kTypeMode: if (_color == null || _blendMode == null) { return null; } return _ColorFilter.mode(this); - case _TypeMatrix: + case _kTypeMatrix: if (_matrix == null) { return null; } assert(_matrix!.length == 20, 'Color Matrix must have 20 entries.'); return _ColorFilter.matrix(this); - case _TypeLinearToSrgbGamma: + case _kTypeLinearToSrgbGamma: return _ColorFilter.linearToSrgbGamma(this); - case _TypeSrgbToLinearGamma: + case _kTypeSrgbToLinearGamma: return _ColorFilter.srgbToLinearGamma(this); default: throw StateError('Unknown mode $_type for ColorFilter.'); } } + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is ColorFilter + && other._type == _type + && _listEquals(other._matrix, _matrix) + && other._color == _color + && other._blendMode == _blendMode; + } + @override int get hashCode => hashValues(_color, _blendMode, hashList(_matrix), _type); + @override + String get _shortDescription { + switch (_type) { + case _kTypeMode: + return 'ColorFilter.mode($_color, $_blendMode)'; + case _kTypeMatrix: + return 'ColorFilter.matrix($_matrix)'; + case _kTypeLinearToSrgbGamma: + return 'ColorFilter.linearToSrgbGamma()'; + case _kTypeSrgbToLinearGamma: + return 'ColorFilter.srgbToLinearGamma()'; + default: + return 'unknow ColorFilter'; + } + } + @override String toString() { switch (_type) { - case _TypeMode: + case _kTypeMode: return 'ColorFilter.mode($_color, $_blendMode)'; - case _TypeMatrix: + case _kTypeMatrix: return 'ColorFilter.matrix($_matrix)'; - case _TypeLinearToSrgbGamma: + case _kTypeLinearToSrgbGamma: return 'ColorFilter.linearToSrgbGamma()'; - case _TypeSrgbToLinearGamma: + case _kTypeSrgbToLinearGamma: return 'ColorFilter.srgbToLinearGamma()'; default: return 'Unknown ColorFilter type. This is an error. If you\'re seeing this, please file an issue at https://github.com/flutter/flutter/issues/new.'; @@ -3067,27 +3089,27 @@ class ColorFilter { class _ColorFilter extends NativeFieldWrapperClass2 { _ColorFilter.mode(this.creator) : assert(creator != null), // ignore: unnecessary_null_comparison - assert(creator._type == ColorFilter._TypeMode) { + assert(creator._type == ColorFilter._kTypeMode) { _constructor(); _initMode(creator._color!.value, creator._blendMode!.index); } _ColorFilter.matrix(this.creator) : assert(creator != null), // ignore: unnecessary_null_comparison - assert(creator._type == ColorFilter._TypeMatrix) { + assert(creator._type == ColorFilter._kTypeMatrix) { _constructor(); _initMatrix(Float32List.fromList(creator._matrix!)); } _ColorFilter.linearToSrgbGamma(this.creator) : assert(creator != null), // ignore: unnecessary_null_comparison - assert(creator._type == ColorFilter._TypeLinearToSrgbGamma) { + assert(creator._type == ColorFilter._kTypeLinearToSrgbGamma) { _constructor(); _initLinearToSrgbGamma(); } _ColorFilter.srgbToLinearGamma(this.creator) : assert(creator != null), // ignore: unnecessary_null_comparison - assert(creator._type == ColorFilter._TypeSrgbToLinearGamma) { + assert(creator._type == ColorFilter._kTypeSrgbToLinearGamma) { _constructor(); _initSrgbToLinearGamma(); } @@ -3113,80 +3135,134 @@ class _ColorFilter extends NativeFieldWrapperClass2 { /// this class as a backdrop filter. /// * [SceneBuilder.pushImageFilter], which is the low-level API for using /// this class as a child layer filter. -class ImageFilter { +abstract class ImageFilter { /// Creates an image filter that applies a Gaussian blur. - ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 }) - : assert(sigmaX != null), // ignore: unnecessary_null_comparison - assert(sigmaY != null), // ignore: unnecessary_null_comparison - _data = _makeList(sigmaX, sigmaY), - _filterQuality = null, - _type = _kTypeBlur; + factory ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 }) { + assert(sigmaX != null); // ignore: unnecessary_null_comparison + assert(sigmaY != null); // ignore: unnecessary_null_comparison + return _GaussianBlurImageFilter(sigmaX: sigmaX, sigmaY: sigmaY); + } /// Creates an image filter that applies a matrix transformation. /// /// For example, applying a positive scale matrix (see [Matrix4.diagonal3]) /// when used with [BackdropFilter] would magnify the background image. - ImageFilter.matrix(Float64List matrix4, - { FilterQuality filterQuality = FilterQuality.low }) - : assert(matrix4 != null), // ignore: unnecessary_null_comparison - _data = Float64List.fromList(matrix4), - _filterQuality = filterQuality, - _type = _kTypeMatrix { + factory ImageFilter.matrix(Float64List matrix4, + { FilterQuality filterQuality = FilterQuality.low }) { + assert(matrix4 != null); // ignore: unnecessary_null_comparison + assert(filterQuality != null); // ignore: unnecessary_null_comparison if (matrix4.length != 16) throw ArgumentError('"matrix4" must have 16 entries.'); + return _MatrixImageFilter(data: Float64List.fromList(matrix4), filterQuality: filterQuality); } - static Float64List _makeList(double a, double b) { - final Float64List list = Float64List(2); - list[0] = a; - list[1] = b; - return list; + /// Composes the `inner` filter with `outer`, to combine their effects. + /// + /// Creates a single [ImageFilter] that when applied, has the same effect as + /// subsequently applying `inner` and `outer`, i.e., + /// result = outer(inner(source)). + factory ImageFilter.compose({ required ImageFilter outer, required ImageFilter inner }) { + assert (inner != null && outer != null); // ignore: unnecessary_null_comparison + return _ComposeImageFilter(innerFilter: inner, outerFilter: outer); } - final Float64List _data; - final FilterQuality? _filterQuality; - final int _type; - _ImageFilter? _nativeFilter; + // Converts this to a native SkImageFilter. See the comments of this method in + // subclasses for the exact type of SkImageFilter this method converts to. + _ImageFilter _toNativeImageFilter(); - // The type of SkImageFilter class to create for Skia. - static const int _kTypeBlur = 0; // MakeBlurFilter - static const int _kTypeMatrix = 1; // MakeMatrixFilterRowMajor255 + // The description text to show when the filter is part of a composite + // [ImageFilter] created using [ImageFilter.compose]. + String get _shortDescription; +} + +class _MatrixImageFilter implements ImageFilter { + _MatrixImageFilter({ required this.data, required this.filterQuality }); + + final Float64List data; + final FilterQuality filterQuality; + + // MakeMatrixFilterRowMajor255 + late final _ImageFilter nativeFilter = _ImageFilter.matrix(this); + @override + _ImageFilter _toNativeImageFilter() => nativeFilter; + + @override + String get _shortDescription => 'matrix($data, $filterQuality)'; + + @override + String toString() => 'ImageFilter.matrix($data, $filterQuality)'; @override bool operator ==(Object other) { - return other is ImageFilter - && other._type == _type - && _listEquals(other._data, _data) - && other._filterQuality == _filterQuality; + if (other.runtimeType != runtimeType) + return false; + return other is _MatrixImageFilter + && other.filterQuality == filterQuality + && _listEquals(other.data, data); } - _ImageFilter _toNativeImageFilter() => _nativeFilter ??= _makeNativeImageFilter(); + @override + int get hashCode => hashValues(filterQuality, hashList(data)); +} - _ImageFilter _makeNativeImageFilter() { - switch (_type) { - case _kTypeBlur: - return _ImageFilter.blur(this); - case _kTypeMatrix: - return _ImageFilter.matrix(this); - default: - throw StateError('Unknown mode $_type for ImageFilter.'); - } +class _GaussianBlurImageFilter implements ImageFilter { + _GaussianBlurImageFilter({ required this.sigmaX, required this.sigmaY }); + + final double sigmaX; + final double sigmaY; + + // MakeBlurFilter + late final _ImageFilter nativeFilter = _ImageFilter.blur(this); + @override + _ImageFilter _toNativeImageFilter() => nativeFilter; + + @override + String get _shortDescription => 'blur($sigmaX, $sigmaY)'; + + @override + String toString() => 'ImageFilter.blur($sigmaX, $sigmaY)'; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is _GaussianBlurImageFilter + && other.sigmaX == sigmaX + && other.sigmaY == sigmaY; } @override - int get hashCode => hashValues(_filterQuality, hashList(_data), _type); + int get hashCode => hashValues(sigmaX, sigmaY); +} + +class _ComposeImageFilter implements ImageFilter { + _ComposeImageFilter({ required this.innerFilter, required this.outerFilter }); + final ImageFilter innerFilter; + final ImageFilter outerFilter; + + // SkImageFilters::Compose + late final _ImageFilter nativeFilter = _ImageFilter.composed(this); @override - String toString() { - switch (_type) { - case _kTypeBlur: - return 'ImageFilter.blur(${_data[0]}, ${_data[1]})'; - case _kTypeMatrix: - return 'ImageFilter.matrix($_data, $_filterQuality)'; - default: - return 'Unknown ImageFilter type. This is an error. If you\'re seeing this, please file an issue at https://github.com/flutter/flutter/issues/new.'; - } + _ImageFilter _toNativeImageFilter() => nativeFilter; + + @override + String get _shortDescription => '${innerFilter._shortDescription} -> ${outerFilter._shortDescription}'; + + @override + String toString() => 'ImageFilter.compose(source -> $_shortDescription -> result)'; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is _ComposeImageFilter + && other.innerFilter == innerFilter + && other.outerFilter == outerFilter; } + + @override + int get hashCode => hashValues(innerFilter, outerFilter); } /// An [ImageFilter] that is backed by a native SkImageFilter. @@ -3198,11 +3274,11 @@ class _ImageFilter extends NativeFieldWrapperClass2 { void _constructor() native 'ImageFilter_constructor'; /// Creates an image filter that applies a Gaussian blur. - _ImageFilter.blur(this.creator) - : assert(creator != null), // ignore: unnecessary_null_comparison - assert(creator._type == ImageFilter._kTypeBlur) { + _ImageFilter.blur(_GaussianBlurImageFilter filter) + : assert(filter != null), // ignore: unnecessary_null_comparison + creator = filter { // ignore: prefer_initializing_formals _constructor(); - _initBlur(creator._data[0], creator._data[1]); + _initBlur(filter.sigmaX, filter.sigmaY); } void _initBlur(double sigmaX, double sigmaY) native 'ImageFilter_initBlur'; @@ -3210,16 +3286,36 @@ class _ImageFilter extends NativeFieldWrapperClass2 { /// /// For example, applying a positive scale matrix (see [Matrix4.diagonal3]) /// when used with [BackdropFilter] would magnify the background image. - _ImageFilter.matrix(this.creator) - : assert(creator != null), // ignore: unnecessary_null_comparison - assert(creator._type == ImageFilter._kTypeMatrix) { - if (creator._data.length != 16) + _ImageFilter.matrix(_MatrixImageFilter filter) + : assert(filter != null), // ignore: unnecessary_null_comparison + creator = filter { // ignore: prefer_initializing_formals + if (filter.data.length != 16) throw ArgumentError('"matrix4" must have 16 entries.'); _constructor(); - _initMatrix(creator._data, creator._filterQuality!.index); + _initMatrix(filter.data, filter.filterQuality.index); } void _initMatrix(Float64List matrix4, int filterQuality) native 'ImageFilter_initMatrix'; + /// Converts a color filter to an image filter. + _ImageFilter.fromColorFilter(ColorFilter filter) + : assert(filter != null), // ignore: unnecessary_null_comparison + creator = filter { // ignore: prefer_initializing_formals + _constructor(); + final _ColorFilter? nativeFilter = filter._toNativeColorFilter(); + _initColorFilter(nativeFilter); + } + void _initColorFilter(_ColorFilter? colorFilter) native 'ImageFilter_initColorFilter'; + + /// Composes `_innerFilter` with `_outerFilter`. + _ImageFilter.composed(_ComposeImageFilter filter) + : assert(filter != null), // ignore: unnecessary_null_comparison + creator = filter { // ignore: prefer_initializing_formals + _constructor(); + final _ImageFilter nativeFilterInner = filter.innerFilter._toNativeImageFilter(); + final _ImageFilter nativeFilterOuter = filter.outerFilter._toNativeImageFilter(); + _initComposed(nativeFilterOuter, nativeFilterInner); + } + void _initComposed(_ImageFilter outerFilter, _ImageFilter innerFilter) native 'ImageFilter_initComposeFilter'; /// The original Dart object that created the native wrapper, which retains /// the values used for the filter. final ImageFilter creator; diff --git a/lib/ui/painting/image_filter.cc b/lib/ui/painting/image_filter.cc index 75b883cb48..6161fb7d4f 100644 --- a/lib/ui/painting/image_filter.cc +++ b/lib/ui/painting/image_filter.cc @@ -6,6 +6,7 @@ #include "flutter/lib/ui/painting/matrix.h" #include "third_party/skia/include/effects/SkBlurImageFilter.h" +#include "third_party/skia/include/effects/SkImageFilters.h" #include "third_party/skia/include/effects/SkImageSource.h" #include "third_party/skia/include/effects/SkPictureImageFilter.h" #include "third_party/tonic/converter/dart_converter.h" @@ -22,11 +23,13 @@ static void ImageFilter_constructor(Dart_NativeArguments args) { IMPLEMENT_WRAPPERTYPEINFO(ui, ImageFilter); -#define FOR_EACH_BINDING(V) \ - V(ImageFilter, initImage) \ - V(ImageFilter, initPicture) \ - V(ImageFilter, initBlur) \ - V(ImageFilter, initMatrix) +#define FOR_EACH_BINDING(V) \ + V(ImageFilter, initImage) \ + V(ImageFilter, initPicture) \ + V(ImageFilter, initBlur) \ + V(ImageFilter, initMatrix) \ + V(ImageFilter, initColorFilter) \ + V(ImageFilter, initComposeFilter) FOR_EACH_BINDING(DART_NATIVE_CALLBACK) @@ -64,4 +67,14 @@ void ImageFilter::initMatrix(const tonic::Float64List& matrix4, nullptr); } +void ImageFilter::initColorFilter(ColorFilter* colorFilter) { + filter_ = SkImageFilters::ColorFilter( + colorFilter ? colorFilter->filter() : nullptr, nullptr); +} + +void ImageFilter::initComposeFilter(ImageFilter* outer, ImageFilter* inner) { + filter_ = SkImageFilters::Compose(outer ? outer->filter() : nullptr, + inner ? inner->filter() : nullptr); +} + } // namespace flutter diff --git a/lib/ui/painting/image_filter.h b/lib/ui/painting/image_filter.h index c44243016b..b1654d865d 100644 --- a/lib/ui/painting/image_filter.h +++ b/lib/ui/painting/image_filter.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_UI_PAINTING_IMAGE_FILTER_H_ #include "flutter/lib/ui/dart_wrapper.h" +#include "flutter/lib/ui/painting/color_filter.h" #include "flutter/lib/ui/painting/image.h" #include "flutter/lib/ui/painting/picture.h" #include "third_party/skia/include/core/SkImageFilter.h" @@ -25,6 +26,8 @@ class ImageFilter : public RefCountedDartWrappable { void initPicture(Picture*); void initBlur(double sigma_x, double sigma_y); void initMatrix(const tonic::Float64List& matrix4, int filter_quality); + void initColorFilter(ColorFilter* colorFilter); + void initComposeFilter(ImageFilter* outer, ImageFilter* inner); const sk_sp& filter() const { return filter_; } diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 201b44acb7..a343a7bef5 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -573,15 +573,10 @@ class BitmapCanvas extends EngineCanvas { ui.Image image, ui.Offset p, SurfacePaintData paint) { final HtmlImage htmlImage = image as HtmlImage; final ui.BlendMode? blendMode = paint.blendMode; - final EngineColorFilter? colorFilter = - paint.colorFilter as EngineColorFilter?; - final ui.BlendMode? colorFilterBlendMode = colorFilter?._blendMode; + final EngineColorFilter? colorFilter = paint.colorFilter as EngineColorFilter?; html.HtmlElement imgElement; - if (colorFilterBlendMode == null) { - // No Blending, create an image by cloning original loaded image. - imgElement = _reuseOrCreateImage(htmlImage); - } else { - switch (colorFilterBlendMode) { + if (colorFilter is _CkBlendModeColorFilter) { + switch (colorFilter.blendMode) { case ui.BlendMode.colorBurn: case ui.BlendMode.colorDodge: case ui.BlendMode.hue: @@ -595,14 +590,17 @@ class BitmapCanvas extends EngineCanvas { case ui.BlendMode.color: case ui.BlendMode.luminosity: case ui.BlendMode.xor: - imgElement = _createImageElementWithSvgFilter( - image, colorFilter!._color, colorFilterBlendMode, paint); + imgElement = _createImageElementWithSvgFilter(image, + colorFilter.color, colorFilter.blendMode, paint); break; default: - imgElement = _createBackgroundImageWithBlend( - image, colorFilter!._color, colorFilterBlendMode, paint); + imgElement = _createBackgroundImageWithBlend(image, + colorFilter.color, colorFilter.blendMode, paint); break; } + } else { + // No Blending, create an image by cloning original loaded image. + imgElement = _reuseOrCreateImage(htmlImage); } imgElement.style.mixBlendMode = _stringForBlendMode(blendMode) ?? ''; if (_canvasPool.isClipped) { diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 2bee6bcfba..61ceb231e8 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -241,11 +241,11 @@ class CkCanvas { } void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { - final CkImageFilter skImageFilter = filter as CkImageFilter; + final _CkManagedSkImageFilterConvertible convertible = filter as _CkManagedSkImageFilterConvertible; return skCanvas.saveLayer( null, toSkRect(bounds), - skImageFilter.skiaObject, + convertible._imageFilter.skiaObject, 0, ); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index fc396b4bd3..40ee255c71 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -823,6 +823,16 @@ class SkImageFilterNamespace { SkFilterQuality filterQuality, Null input, // we don't use this yet ); + + external SkImageFilter MakeColorFilter( + SkColorFilter colorFilter, + Null input, // we don't use this yet + ); + + external SkImageFilter MakeCompose( + SkImageFilter outer, + SkImageFilter inner, + ); } @JS() diff --git a/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart index 2c219b4b19..86bb58cbcd 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart @@ -5,62 +5,147 @@ // @dart = 2.10 part of engine; -/// A [ui.ColorFilter] backed by Skia's [CkColorFilter]. -class CkColorFilter extends ManagedSkiaObject { - final EngineColorFilter _engineFilter; - - CkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter; - - CkColorFilter.matrix(EngineColorFilter filter) : _engineFilter = filter; - - CkColorFilter.linearToSrgbGamma(EngineColorFilter filter) - : _engineFilter = filter; - - CkColorFilter.srgbToLinearGamma(EngineColorFilter filter) - : _engineFilter = filter; - - SkColorFilter _createSkiaObjectFromFilter() { - SkColorFilter skColorFilter; - switch (_engineFilter._type) { - case EngineColorFilter._TypeMode: - skColorFilter = canvasKit.SkColorFilter.MakeBlend( - toSharedSkColor1(_engineFilter._color!), - toSkBlendMode(_engineFilter._blendMode!), - ); - break; - case EngineColorFilter._TypeMatrix: - final Float32List colorMatrix = Float32List(20); - final List matrix = _engineFilter._matrix!; - for (int i = 0; i < 20; i++) { - colorMatrix[i] = matrix[i]; - } - skColorFilter = canvasKit.SkColorFilter.MakeMatrix(colorMatrix); - break; - case EngineColorFilter._TypeLinearToSrgbGamma: - skColorFilter = canvasKit.SkColorFilter.MakeLinearToSRGBGamma(); - break; - case EngineColorFilter._TypeSrgbToLinearGamma: - skColorFilter = canvasKit.SkColorFilter.MakeSRGBToLinearGamma(); - break; - default: - throw StateError( - 'Unknown mode ${_engineFilter._type} for ColorFilter.'); - } - return skColorFilter; +/// A concrete [ManagedSkiaObject] subclass that owns a [SkColorFilter] and +/// manages its lifecycle. +/// +/// Seealso: +/// +/// * [CkPaint.colorFilter], which uses a [_ManagedSkColorFilter] to manage +/// the lifecycle of its [SkColorFilter]. +class _ManagedSkColorFilter extends ManagedSkiaObject { + _ManagedSkColorFilter(CkColorFilter ckColorFilter) + : this.ckColorFilter = ckColorFilter; + + final CkColorFilter ckColorFilter; + + @override + SkColorFilter createDefault() => ckColorFilter._initRawColorFilter(); + + @override + SkColorFilter resurrect() => ckColorFilter._initRawColorFilter(); + + @override + void delete() { + rawSkiaObject?.delete(); } @override - SkColorFilter createDefault() { - return _createSkiaObjectFromFilter(); + int get hashCode => ckColorFilter.hashCode; + + @override + bool operator ==(Object other) { + if (runtimeType != other.runtimeType) + return false; + return other is _ManagedSkColorFilter + && other.ckColorFilter == ckColorFilter; } @override - SkColorFilter resurrect() { - return _createSkiaObjectFromFilter(); + String toString() => ckColorFilter.toString(); +} + +/// A [ui.ColorFilter] backed by Skia's [SkColorFilter]. +/// +/// Additionally, this class provides the interface for converting itself to a +/// [ManagedSkiaObject] that manages a skia image filter. +abstract class CkColorFilter implements _CkManagedSkImageFilterConvertible, EngineColorFilter { + const CkColorFilter(); + + /// Called by [ManagedSkiaObject.createDefault] and + /// [ManagedSkiaObject.resurrect] to create a new [SKImageFilter], when this + /// filter is used as an [ImageFilter]. + SkImageFilter _initRawImageFilter() => canvasKit.SkImageFilter.MakeColorFilter(_initRawColorFilter(), null); + + /// Called by [ManagedSkiaObject.createDefault] and + /// [ManagedSkiaObject.resurrect] to create a new [SKColorFilter], when this + /// filter is used as a [ColorFilter]. + SkColorFilter _initRawColorFilter(); + + ManagedSkiaObject get _imageFilter => _CkColorFilterImageFilter(colorFilter: this); +} + +class _CkBlendModeColorFilter extends CkColorFilter { + const _CkBlendModeColorFilter(this.color, this.blendMode); + + final ui.Color color; + final ui.BlendMode blendMode; + + @override + SkColorFilter _initRawColorFilter() { + return canvasKit.SkColorFilter.MakeBlend( + toSharedSkColor1(color), + toSkBlendMode(blendMode), + ); } @override - void delete() { - rawSkiaObject?.delete(); + int get hashCode => ui.hashValues(color, blendMode); + + @override + bool operator ==(Object other) { + if (runtimeType != other.runtimeType) + return false; + return other is _CkBlendModeColorFilter + && other.color == color + && other.blendMode == blendMode; + } + + @override + String toString() => 'ColorFilter.mode($color, $blendMode)'; +} + +class _CkMatrixColorFilter extends CkColorFilter { + const _CkMatrixColorFilter(this.matrix); + + final List matrix; + + @override + SkColorFilter _initRawColorFilter() { + assert(this.matrix.length == 20, 'Color Matrix must have 20 entries.'); + final List matrix = this.matrix; + if (matrix is Float32List) + return canvasKit.SkColorFilter.MakeMatrix(matrix); + final Float32List float32Matrix = Float32List(20); + for (int i = 0; i < 20; i++) { + float32Matrix[i] = matrix[i]; + } + return canvasKit.SkColorFilter.MakeMatrix(float32Matrix); } + + @override + int get hashCode => ui.hashList(matrix); + + @override + bool operator ==(Object other) { + return runtimeType == other.runtimeType + && other is _CkMatrixColorFilter + && _listEquals(matrix, other.matrix); + } + + @override + String toString() => 'ColorFilter.matrix($matrix)'; +} + +class _CkLinearToSrgbGammaColorFilter extends CkColorFilter { + const _CkLinearToSrgbGammaColorFilter(); + @override + SkColorFilter _initRawColorFilter() => canvasKit.SkColorFilter.MakeLinearToSRGBGamma(); + + @override + bool operator ==(Object other) => runtimeType == other.runtimeType; + + @override + String toString() => 'ColorFilter.linearToSrgbGamma()'; +} + +class _CkSrgbToLinearGammaColorFilter extends CkColorFilter { + const _CkSrgbToLinearGammaColorFilter(); + @override + SkColorFilter _initRawColorFilter() => canvasKit.SkColorFilter.MakeSRGBToLinearGamma(); + + @override + bool operator ==(Object other) => runtimeType == other.runtimeType; + + @override + String toString() => 'ColorFilter.srgbToLinearGamma()'; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart index 02ebb4bcca..5562e84c65 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -5,16 +5,30 @@ // @dart = 2.10 part of engine; +/// An [ImageFilter] that can create a managed skia [SkImageFilter] object. +/// +/// Concrete subclasses of this interface must provide efficient implementation +/// of [operator==], to avoid re-creating the underlying skia filters +/// whenever possible. +/// +/// Currently implemented by [CkImageFilter] and [CkColorFilter]. +abstract class _CkManagedSkImageFilterConvertible implements ui.ImageFilter { + ManagedSkiaObject get _imageFilter; +} + /// The CanvasKit implementation of [ui.ImageFilter]. /// /// Currently only supports `blur`. -class CkImageFilter extends ManagedSkiaObject implements ui.ImageFilter { - CkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) - : _sigmaX = sigmaX, - _sigmaY = sigmaY; +abstract class CkImageFilter extends ManagedSkiaObject implements _CkManagedSkImageFilterConvertible { + factory CkImageFilter.blur({ required double sigmaX, required double sigmaY }) = _CkBlurImageFilter; + factory CkImageFilter.color({ required CkColorFilter colorFilter }) = _CkColorFilterImageFilter; + + CkImageFilter._(); + + @override + ManagedSkiaObject get _imageFilter => this; - final double _sigmaX; - final double _sigmaY; + SkImageFilter _initSkiaObject(); @override SkImageFilter createDefault() => _initSkiaObject(); @@ -26,11 +40,42 @@ class CkImageFilter extends ManagedSkiaObject implements ui.Image void delete() { rawSkiaObject?.delete(); } +} + +class _CkColorFilterImageFilter extends CkImageFilter { + _CkColorFilterImageFilter({ required this.colorFilter }) : super._(); + + final CkColorFilter colorFilter; + @override + SkImageFilter _initSkiaObject() => colorFilter._initRawImageFilter(); + + @override + int get hashCode => colorFilter.hashCode; + + @override + bool operator ==(Object other) { + if (runtimeType != other.runtimeType) + return false; + return other is _CkColorFilterImageFilter + && other.colorFilter == colorFilter; + } + + @override + String toString() => colorFilter.toString(); +} + +class _CkBlurImageFilter extends CkImageFilter { + _CkBlurImageFilter({ required this.sigmaX, required this.sigmaY }) : super._(); + + final double sigmaX; + final double sigmaY; + + @override SkImageFilter _initSkiaObject() { return canvasKit.SkImageFilter.MakeBlur( - _sigmaX, - _sigmaY, + sigmaX, + sigmaY, canvasKit.TileMode.Clamp, null, ); @@ -38,16 +83,19 @@ class CkImageFilter extends ManagedSkiaObject implements ui.Image @override bool operator ==(Object other) { - return other is CkImageFilter - && other._sigmaX == _sigmaX - && other._sigmaY == _sigmaY; + if (runtimeType != other.runtimeType) + return false; + return other is _CkBlurImageFilter + && other.sigmaX == sigmaX + && other.sigmaY == sigmaY; } @override - int get hashCode => ui.hashValues(_sigmaX, _sigmaY); + int get hashCode => ui.hashValues(sigmaX, sigmaY); @override String toString() { - return 'ImageFilter.blur($_sigmaX, $_sigmaY)'; + return 'ImageFilter.blur($sigmaX, $sigmaY)'; } } + diff --git a/lib/web_ui/lib/src/engine/canvaskit/painting.dart b/lib/web_ui/lib/src/engine/canvaskit/painting.dart index 276e6bb1cc..69ab374db0 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/painting.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -165,20 +165,22 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { ui.FilterQuality _filterQuality = ui.FilterQuality.none; @override - ui.ColorFilter? get colorFilter => _colorFilter; + ui.ColorFilter? get colorFilter => _managedColorFilter?.ckColorFilter; @override set colorFilter(ui.ColorFilter? value) { - if (_colorFilter == value) { + if (colorFilter == value) { return; } - final EngineColorFilter? engineValue = value as EngineColorFilter?; - _colorFilter = engineValue; - _ckColorFilter = engineValue?._toCkColorFilter(); - skiaObject.setColorFilter(_ckColorFilter?.skiaObject); + + if (value == null) { + _managedColorFilter = null; + } else { + _managedColorFilter = _ManagedSkColorFilter(value as CkColorFilter); + } + skiaObject.setColorFilter(_managedColorFilter?.skiaObject); } - EngineColorFilter? _colorFilter; - CkColorFilter? _ckColorFilter; + _ManagedSkColorFilter? _managedColorFilter; @override double get strokeMiterLimit => _strokeMiterLimit; @@ -200,11 +202,14 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { if (_imageFilter == value) { return; } - _imageFilter = value as CkImageFilter?; - skiaObject.setImageFilter(_imageFilter?.skiaObject); + + _imageFilter = value as _CkManagedSkImageFilterConvertible?; + _managedImageFilter = _imageFilter?._imageFilter; + skiaObject.setImageFilter(_managedImageFilter?.skiaObject); } - CkImageFilter? _imageFilter; + _CkManagedSkImageFilterConvertible? _imageFilter; + ManagedSkiaObject? _managedImageFilter; @override SkPaint createDefault() { @@ -224,8 +229,8 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { paint.setColorInt(_color.value); paint.setShader(_shader?.skiaObject); paint.setMaskFilter(_ckMaskFilter?.skiaObject); - paint.setColorFilter(_ckColorFilter?.skiaObject); - paint.setImageFilter(_imageFilter?.skiaObject); + paint.setColorFilter(_managedColorFilter?.skiaObject); + paint.setImageFilter(_managedImageFilter?.skiaObject); paint.setFilterQuality(toSkFilterQuality(_filterQuality)); paint.setStrokeCap(toSkStrokeCap(_strokeCap)); paint.setStrokeJoin(toSkStrokeJoin(_strokeJoin)); diff --git a/lib/web_ui/lib/src/engine/color_filter.dart b/lib/web_ui/lib/src/engine/color_filter.dart index 230cdf9a2a..3513a605dc 100644 --- a/lib/web_ui/lib/src/engine/color_filter.dart +++ b/lib/web_ui/lib/src/engine/color_filter.dart @@ -21,11 +21,7 @@ class EngineColorFilter implements ui.ColorFilter { /// The output of this filter is then composited into the background according /// to the [Paint.blendMode], using the output of this filter as the source /// and the background as the destination. - const EngineColorFilter.mode(ui.Color color, ui.BlendMode blendMode) - : _color = color, - _blendMode = blendMode, - _matrix = null, - _type = _TypeMode; + const factory EngineColorFilter.mode(ui.Color color, ui.BlendMode blendMode) = _CkBlendModeColorFilter; /// Construct a color filter that transforms a color by a 5x5 matrix, where /// the fifth row is implicitly added in an identity configuration. @@ -87,86 +83,13 @@ class EngineColorFilter implements ui.ColorFilter { /// 0, 0, 0, 1, 0, /// ]); /// ``` - const EngineColorFilter.matrix(List matrix) - : _color = null, - _blendMode = null, - _matrix = matrix, - _type = _TypeMatrix; + const factory EngineColorFilter.matrix(List matrix) = _CkMatrixColorFilter; /// Construct a color filter that applies the sRGB gamma curve to the RGB /// channels. - const EngineColorFilter.linearToSrgbGamma() - : _color = null, - _blendMode = null, - _matrix = null, - _type = _TypeLinearToSrgbGamma; + const factory EngineColorFilter.linearToSrgbGamma() = _CkLinearToSrgbGammaColorFilter; /// Creates a color filter that applies the inverse of the sRGB gamma curve /// to the RGB channels. - const EngineColorFilter.srgbToLinearGamma() - : _color = null, - _blendMode = null, - _matrix = null, - _type = _TypeSrgbToLinearGamma; - - final ui.Color? _color; - final ui.BlendMode? _blendMode; - final List? _matrix; - final int _type; - - // The type of CkColorFilter class to create for Skia. - static const int _TypeMode = 1; // MakeModeFilter - static const int _TypeMatrix = 2; // MakeMatrixFilterRowMajor255 - static const int _TypeLinearToSrgbGamma = 3; // MakeLinearToSRGBGamma - static const int _TypeSrgbToLinearGamma = 4; // MakeSRGBToLinearGamma - - @override - bool operator ==(Object other) { - return other is EngineColorFilter - && other._type == _type - && _listEquals(other._matrix, _matrix) - && other._color == _color - && other._blendMode == _blendMode; - } - - CkColorFilter? _toCkColorFilter() { - switch (_type) { - case _TypeMode: - if (_color == null || _blendMode == null) { - return null; - } - return CkColorFilter.mode(this); - case _TypeMatrix: - if (_matrix == null) { - return null; - } - assert(_matrix!.length == 20, 'Color Matrix must have 20 entries.'); - return CkColorFilter.matrix(this); - case _TypeLinearToSrgbGamma: - return CkColorFilter.linearToSrgbGamma(this); - case _TypeSrgbToLinearGamma: - return CkColorFilter.srgbToLinearGamma(this); - default: - throw StateError('Unknown mode $_type for ColorFilter.'); - } - } - - @override - int get hashCode => ui.hashValues(_color, _blendMode, ui.hashList(_matrix), _type); - - @override - String toString() { - switch (_type) { - case _TypeMode: - return 'ColorFilter.mode($_color, $_blendMode)'; - case _TypeMatrix: - return 'ColorFilter.matrix($_matrix)'; - case _TypeLinearToSrgbGamma: - return 'ColorFilter.linearToSrgbGamma()'; - case _TypeSrgbToLinearGamma: - return 'ColorFilter.srgbToLinearGamma()'; - default: - return 'Unknown ColorFilter type. This is an error. If you\'re seeing this, please file an issue at https://github.com/flutter/flutter/issues/new.'; - } - } + const factory EngineColorFilter.srgbToLinearGamma() = _CkSrgbToLinearGammaColorFilter; } diff --git a/lib/web_ui/lib/src/engine/html/color_filter.dart b/lib/web_ui/lib/src/engine/html/color_filter.dart index 6ac54922eb..abf08bb53e 100644 --- a/lib/web_ui/lib/src/engine/html/color_filter.dart +++ b/lib/web_ui/lib/src/engine/html/color_filter.dart @@ -60,74 +60,71 @@ class PersistedColorFilter extends PersistedContainerSurface childContainer?.style.visibility = 'visible'; return; } - if (engineValue._blendMode == null) { - rootElement!.style.backgroundColor = - colorToCssString(engineValue._color!); + + if (engineValue is! _CkBlendModeColorFilter) { childContainer?.style.visibility = 'visible'; return; } - ui.Color filterColor = engineValue._color!; - ui.BlendMode? colorFilterBlendMode = engineValue._blendMode; + ui.Color filterColor = engineValue.color; + ui.BlendMode colorFilterBlendMode = engineValue.blendMode; html.CssStyleDeclaration style = rootElement!.style; - if (colorFilterBlendMode != null) { - switch (colorFilterBlendMode) { - case ui.BlendMode.clear: - case ui.BlendMode.dstOut: - case ui.BlendMode.srcOut: - childContainer?.style.visibility = 'hidden'; - return; - case ui.BlendMode.dst: - case ui.BlendMode.dstIn: - // Noop. - return; - case ui.BlendMode.src: - case ui.BlendMode.srcOver: - // Uses source filter color. - // Since we don't have a size, we can't use background color. - // Use svg filter srcIn instead. - colorFilterBlendMode = ui.BlendMode.srcIn; - break; - case ui.BlendMode.dstOver: - case ui.BlendMode.srcIn: - case ui.BlendMode.srcATop: - case ui.BlendMode.dstATop: - case ui.BlendMode.xor: - case ui.BlendMode.plus: - case ui.BlendMode.modulate: - case ui.BlendMode.screen: - case ui.BlendMode.overlay: - case ui.BlendMode.darken: - case ui.BlendMode.lighten: - case ui.BlendMode.colorDodge: - case ui.BlendMode.colorBurn: - case ui.BlendMode.hardLight: - case ui.BlendMode.softLight: - case ui.BlendMode.difference: - case ui.BlendMode.exclusion: - case ui.BlendMode.multiply: - case ui.BlendMode.hue: - case ui.BlendMode.saturation: - case ui.BlendMode.color: - case ui.BlendMode.luminosity: - break; - } - - // Use SVG filter for blend mode. - String? svgFilter = - svgFilterFromBlendMode(filterColor, colorFilterBlendMode); - if (svgFilter != null) { - _filterElement = - html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer()); - rootElement!.append(_filterElement!); - rootElement!.style.filter = 'url(#_fcf${_filterIdCounter})'; - if (colorFilterBlendMode == ui.BlendMode.saturation || - colorFilterBlendMode == ui.BlendMode.multiply || - colorFilterBlendMode == ui.BlendMode.modulate) { - style.backgroundColor = colorToCssString(filterColor); - } + switch (colorFilterBlendMode) { + case ui.BlendMode.clear: + case ui.BlendMode.dstOut: + case ui.BlendMode.srcOut: + childContainer?.style.visibility = 'hidden'; + return; + case ui.BlendMode.dst: + case ui.BlendMode.dstIn: + // Noop. return; + case ui.BlendMode.src: + case ui.BlendMode.srcOver: + // Uses source filter color. + // Since we don't have a size, we can't use background color. + // Use svg filter srcIn instead. + colorFilterBlendMode = ui.BlendMode.srcIn; + break; + case ui.BlendMode.dstOver: + case ui.BlendMode.srcIn: + case ui.BlendMode.srcATop: + case ui.BlendMode.dstATop: + case ui.BlendMode.xor: + case ui.BlendMode.plus: + case ui.BlendMode.modulate: + case ui.BlendMode.screen: + case ui.BlendMode.overlay: + case ui.BlendMode.darken: + case ui.BlendMode.lighten: + case ui.BlendMode.colorDodge: + case ui.BlendMode.colorBurn: + case ui.BlendMode.hardLight: + case ui.BlendMode.softLight: + case ui.BlendMode.difference: + case ui.BlendMode.exclusion: + case ui.BlendMode.multiply: + case ui.BlendMode.hue: + case ui.BlendMode.saturation: + case ui.BlendMode.color: + case ui.BlendMode.luminosity: + break; + } + + // Use SVG filter for blend mode. + String? svgFilter = + svgFilterFromBlendMode(filterColor, colorFilterBlendMode); + if (svgFilter != null) { + _filterElement = + html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer()); + rootElement!.append(_filterElement!); + rootElement!.style.filter = 'url(#_fcf${_filterIdCounter})'; + if (colorFilterBlendMode == ui.BlendMode.saturation || + colorFilterBlendMode == ui.BlendMode.multiply || + colorFilterBlendMode == ui.BlendMode.modulate) { + style.backgroundColor = colorToCssString(filterColor); } + return; } } diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 695bcd9362..1316783d4b 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -406,6 +406,12 @@ class ImageFilter { // if (matrix4.length != 16) // throw ArgumentError('"matrix4" must have 16 entries.'); } + + ImageFilter.compose({required ImageFilter outer, required ImageFilter inner}) { + // TODO(flutter_web): add implementation. + throw UnimplementedError( + 'ImageFilter.compose not implemented for web platform.'); + } } enum ImageByteFormat { diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 15a5a80f59..b1d56509d4 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -474,6 +474,26 @@ void _imageFilterTests() { isNotNull, ); }); + + test('MakeColorFilter', () { + expect( + canvasKit.SkImageFilter.MakeColorFilter( + canvasKit.SkColorFilter.MakeLinearToSRGBGamma(), + null, + ), + isNotNull, + ); + }); + + test('MakeCompose', () { + expect( + canvasKit.SkImageFilter.MakeCompose( + canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), + canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), + ), + isNotNull, + ); + }); } void _mallocTests() { diff --git a/lib/web_ui/test/canvaskit/filter_test.dart b/lib/web_ui/test/canvaskit/filter_test.dart new file mode 100644 index 0000000000..e46dc7aad6 --- /dev/null +++ b/lib/web_ui/test/canvaskit/filter_test.dart @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + List createColorFilters() { + return [ + EngineColorFilter.mode(ui.Color(0x12345678), ui.BlendMode.srcOver) as CkColorFilter, + EngineColorFilter.mode(ui.Color(0x12345678), ui.BlendMode.dstOver) as CkColorFilter, + EngineColorFilter.mode(ui.Color(0x87654321), ui.BlendMode.dstOver) as CkColorFilter, + EngineColorFilter.matrix([ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + ]) as CkColorFilter, + EngineColorFilter.matrix(Float32List.fromList([ + 2, 0, 0, 0, 0, + 0, 2, 0, 0, 0, + 0, 0, 2, 0, 0, + 0, 0, 0, 2, 0, + ])) as CkColorFilter, + EngineColorFilter.linearToSrgbGamma() as CkColorFilter, + EngineColorFilter.srgbToLinearGamma() as CkColorFilter, + ]; + } + + List createImageFilters() { + return [ + CkImageFilter.blur(sigmaX: 5, sigmaY: 6), + CkImageFilter.blur(sigmaX: 6, sigmaY: 5), + for (final CkColorFilter colorFilter in createColorFilters()) CkImageFilter.color(colorFilter: colorFilter), + ]; + } + + group('ImageFilters', () { + setUpAll(() async { + await ui.webOnlyInitializePlatform(); + }); + + test('can be constructed', () { + final CkImageFilter imageFilter = CkImageFilter.blur(sigmaX: 5, sigmaY: 10); + expect(imageFilter, isA()); + expect(imageFilter.createDefault(), isNotNull); + expect(imageFilter.resurrect(), isNotNull); + }); + + + test('== operator', () { + final List filters1 = [ + ...createImageFilters(), + ...createColorFilters(), + ]; + final List filters2 = [ + ...createImageFilters(), + ...createColorFilters(), + ]; + + for (int index1 = 0; index1 < filters1.length; index1 += 1) { + final ui.ImageFilter imageFilter1 = filters1[index1]; + expect(imageFilter1 == imageFilter1, isTrue); + for (int index2 = 0; index2 < filters2.length; index2 += 1) { + final ui.ImageFilter imageFilter2 = filters2[index2]; + expect(imageFilter1 == imageFilter2, imageFilter2 == imageFilter1); + expect(imageFilter1 == imageFilter2, index1 == index2); + } + } + }); + + test('reuses the Skia filter', () { + final CkPaint paint = CkPaint(); + paint.imageFilter = CkImageFilter.blur(sigmaX: 5, sigmaY: 10); + + final ManagedSkiaObject managedFilter = paint.imageFilter as ManagedSkiaObject; + final Object skiaFilter = managedFilter?.skiaObject; + + paint.imageFilter = CkImageFilter.blur(sigmaX: 5, sigmaY: 10); + expect((paint.imageFilter as ManagedSkiaObject).skiaObject, same(skiaFilter)); + }); + + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} diff --git a/testing/dart/image_filter_test.dart b/testing/dart/image_filter_test.dart index fb0297bc3e..b3f747d9f6 100644 --- a/testing/dart/image_filter_test.dart +++ b/testing/dart/image_filter_test.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:test/test.dart'; +const Color red = Color(0xFFAA0000); const Color green = Color(0xFF00AA00); const int greenCenterBlurred = 0x1C001300; @@ -18,6 +19,34 @@ const int greenCenterScaled = 0xFF00AA00; const int greenSideScaled = 0x80005500; const int greenCornerScaled = 0x40002B00; +const List grayscaleColorMatrix = [ + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, +]; + +const List identityColorMatrix = [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, +]; + +const List constValueColorMatrix = [ + 0, 0, 0, 0, 2, + 0, 0, 0, 0, 2, + 0, 0, 0, 0, 2, + 0, 0, 0, 0, 255, +]; + +const List halvesBrightnessColorMatrix = [ + 0.5, 0, 0, 0, 0, + 0, 0.5, 0, 0, 0, + 0, 0, 0.5, 0, 0, + 0, 0, 0, 1, 0, +]; + void main() { Future getBytesForPaint(Paint paint, {int width = 3, int height = 3}) async { final PictureRecorder recorder = PictureRecorder(); @@ -31,6 +60,18 @@ void main() { return bytes.buffer.asUint32List(); } + Future getBytesForColorPaint(Paint paint, {int width = 1, int height = 1}) async { + final PictureRecorder recorder = PictureRecorder(); + final Canvas recorderCanvas = Canvas(recorder); + recorderCanvas.drawPaint(paint); + final Picture picture = recorder.endRecording(); + final Image image = await picture.toImage(width, height); + final ByteData bytes = await image.toByteData(); + + expect(bytes.lengthInBytes, width * height * 4); + return bytes.buffer.asUint32List(); + } + ImageFilter makeBlur(double sigmaX, double sigmaY) => ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); @@ -47,6 +88,20 @@ void main() { ]), filterQuality: quality); } + List colorFilters() { + // Create new color filter instances on each invocation. + return [ // ignore: prefer_const_constructors + ColorFilter.mode(null, null), // ignore: prefer_const_constructors + ColorFilter.mode(green, BlendMode.color), // ignore: prefer_const_constructors + ColorFilter.mode(red, BlendMode.color), // ignore: prefer_const_constructors + ColorFilter.mode(red, BlendMode.screen), // ignore: prefer_const_constructors + ColorFilter.matrix(null), // ignore: prefer_const_constructors + ColorFilter.matrix(grayscaleColorMatrix), // ignore: prefer_const_constructors + ColorFilter.linearToSrgbGamma(), // ignore: prefer_const_constructors + ColorFilter.srgbToLinearGamma(), // ignore: prefer_const_constructors + ]; + } + List makeList() { return [ makeBlur(10.0, 10.0), @@ -59,6 +114,7 @@ void main() { makeScale(10.0, 10.0, 0.0, 0.0, FilterQuality.medium), makeScale(10.0, 10.0, 0.0, 0.0, FilterQuality.high), makeScale(10.0, 10.0, 0.0, 0.0, FilterQuality.none), + ...colorFilters(), ]; } @@ -78,12 +134,19 @@ void main() { } } + List composed(List a, List b) { + return [for (final ImageFilter x in a) for (final ImageFilter y in b) ImageFilter.compose(outer: x, inner: y)]; + } + test('ImageFilter - equals', () async { final List A = makeList(); final List B = makeList(); checkEquality(A, A); checkEquality(A, B); checkEquality(B, B); + checkEquality(composed(A, A), composed(A, A)); + checkEquality(composed(A, B), composed(B, A)); + checkEquality(composed(B, B), composed(B, B)); }); void checkBytes(Uint32List bytes, int center, int side, int corner) { @@ -117,4 +180,117 @@ void main() { final Uint32List bytes = await getBytesForPaint(paint); checkBytes(bytes, greenCenterScaled, greenSideScaled, greenCornerScaled); }); + + test('ImageFilter - matrix: copies the list', () async { + final Float64List matrix = Float64List.fromList([ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + ]); + + final ImageFilter filter = ImageFilter.matrix(matrix); + final String originalDescription = filter.toString(); + + // Modify the matrix. + matrix[0] = 12345; + expect(filter.toString(), contains('[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]')); + expect(filter.toString(), originalDescription); + }); + + test('ImageFilter - null color filters do not throw', () { + dynamic error; + final Paint paint = Paint(); + try { + paint + ..color = green + ..imageFilter = const ColorFilter.mode(null, null); + } catch (e) { + error = e; + } + + expect(error, isNull); + }); + + test('ImageFilter - from color filters', () async { + final Paint paint = Paint() + ..color = green + ..imageFilter = const ColorFilter.matrix(constValueColorMatrix); + + final Uint32List bytes = await getBytesForColorPaint(paint); + expect(bytes[0], 0xFF020202); + }); + + test('ImageFilter - null filter composition', () async { + const ImageFilter nullFilter = ColorFilter.mode(null, null); + const ImageFilter identityFilter = ColorFilter.matrix(identityColorMatrix); + + // Verify that null filter == identity. + Future verifyAgainst(ImageFilter filter) async { + final ImageFilter comp0 = ImageFilter.compose(outer: filter, inner: identityFilter); + final ImageFilter comp1 = ImageFilter.compose(outer: filter, inner: nullFilter); + final ImageFilter comp2 = ImageFilter.compose(outer: nullFilter, inner: filter); + final Paint paint = Paint()..color = green; + + paint.imageFilter = comp0; + final Uint32List bytes = await getBytesForColorPaint(paint); + + paint.imageFilter = comp1; + expect(bytes, equals(await getBytesForColorPaint(paint))); + + paint.imageFilter = comp2; + expect(bytes, equals(await getBytesForColorPaint(paint))); + } + + makeList().forEach(verifyAgainst); + }); + + test('ImageFilter - color filter composition', () async { + final ImageFilter compOrder1 = ImageFilter.compose( + outer: const ColorFilter.matrix(halvesBrightnessColorMatrix), + inner: const ColorFilter.matrix(constValueColorMatrix), + ); + + final ImageFilter compOrder2 = ImageFilter.compose( + outer: const ColorFilter.matrix(constValueColorMatrix), + inner: const ColorFilter.matrix(halvesBrightnessColorMatrix), + ); + + final Paint paint = Paint() + ..color = green + ..imageFilter = compOrder1; + + Uint32List bytes = await getBytesForColorPaint(paint); + expect(bytes[0], 0xFF010101); + + paint + ..color = green + ..imageFilter = compOrder2; + bytes = await getBytesForColorPaint(paint); + expect(bytes[0], 0xFF020202); + }); + + test('Composite ImageFilter toString', () { + expect( + ImageFilter.compose(outer: makeBlur(20.0, 20.0), inner: makeBlur(10.0, 10.0)).toString(), + contains('blur(10.0, 10.0) -> blur(20.0, 20.0)'), + ); + + // Produces a flat list of filters + expect( + ImageFilter.compose( + outer: ImageFilter.compose(outer: makeBlur(30.0, 30.0), inner: makeBlur(20.0, 20.0)), + inner: ImageFilter.compose( + outer: const ColorFilter.mode(null, null), + inner: makeScale(10.0, 10.0), + ), + ).toString(), + contains( + 'matrix([10.0, 0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -0.0, -0.0, 0.0, 1.0], FilterQuality.low) -> ' + 'ColorFilter.mode(null, null) -> ' + 'blur(20.0, 20.0) -> ' + 'blur(30.0, 30.0)' + ), + ); + }); } -- GitLab