From 156c2bebcb87c8cd1f540dae1e03dbf8057063a1 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Mon, 17 May 2021 15:44:01 -0700 Subject: [PATCH] Web ImageFilter.matrix support (#25982) --- .../src/engine/canvaskit/image_filter.dart | 40 +++++++++- .../lib/src/engine/html/backdrop_filter.dart | 4 +- .../lib/src/engine/html/image_filter.dart | 3 +- .../lib/src/engine/html/scene_builder.dart | 7 -- .../lib/src/engine/html/shaders/shader.dart | 66 ++++++++++++++-- lib/web_ui/lib/src/engine/util.dart | 19 +++-- lib/web_ui/lib/src/ui/painting.dart | 16 ++-- .../surface/filters/image_filter_test.dart | 79 +++++++++++++++++++ .../engine/compositing_golden_test.dart | 21 ++++- 9 files changed, 225 insertions(+), 30 deletions(-) create mode 100644 lib/web_ui/test/engine/surface/filters/image_filter_test.dart 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 7490fcf558..990360995e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; + import 'package:ui/ui.dart' as ui; import 'canvaskit_api.dart'; import 'color_filter.dart'; import 'skia_object_cache.dart'; +import '../util.dart'; /// An [ImageFilter] that can create a managed skia [SkImageFilter] object. /// @@ -22,7 +25,7 @@ abstract class CkManagedSkImageFilterConvertible /// The CanvasKit implementation of [ui.ImageFilter]. /// -/// Currently only supports `blur`. +/// Currently only supports `blur`, `matrix`, and ColorFilters. abstract class CkImageFilter extends ManagedSkiaObject implements CkManagedSkImageFilterConvertible { factory CkImageFilter.blur( @@ -31,6 +34,9 @@ abstract class CkImageFilter extends ManagedSkiaObject required ui.TileMode tileMode}) = _CkBlurImageFilter; factory CkImageFilter.color({required CkColorFilter colorFilter}) = CkColorFilterImageFilter; + factory CkImageFilter.matrix( + {required Float64List matrix, + required ui.FilterQuality filterQuality}) = _CkMatrixImageFilter; CkImageFilter._(); @@ -126,3 +132,35 @@ class _CkBlurImageFilter extends CkImageFilter { return 'ImageFilter.blur($sigmaX, $sigmaY, $_modeString)'; } } + +class _CkMatrixImageFilter extends CkImageFilter { + _CkMatrixImageFilter({ required Float64List matrix, required this.filterQuality }) + : this.matrix = Float64List.fromList(matrix), + super._(); + + final Float64List matrix; + final ui.FilterQuality filterQuality; + + SkImageFilter _initSkiaObject() { + return canvasKit.ImageFilter.MakeMatrixTransform( + toSkMatrixFromFloat64(matrix), + toSkFilterQuality(filterQuality), + null, + ); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is _CkMatrixImageFilter + && other.filterQuality == filterQuality + && listEquals(other.matrix, matrix); + } + + @override + int get hashCode => ui.hashValues(filterQuality, ui.hashList(matrix)); + + @override + String toString() => 'ImageFilter.matrix($matrix, $filterQuality)'; +} diff --git a/lib/web_ui/lib/src/engine/html/backdrop_filter.dart b/lib/web_ui/lib/src/engine/html/backdrop_filter.dart index 625d73dacf..64adaa3319 100644 --- a/lib/web_ui/lib/src/engine/html/backdrop_filter.dart +++ b/lib/web_ui/lib/src/engine/html/backdrop_filter.dart @@ -113,9 +113,9 @@ class PersistedBackdropFilter extends PersistedContainerSurface // the blur will fall within 2 * sigma pixels. if (browserEngine == BrowserEngine.webkit) { DomRenderer.setElementStyle(_filterElement!, '-webkit-backdrop-filter', - _imageFilterToCss(filter)); + filter.filterAttribute); } - DomRenderer.setElementStyle(_filterElement!, 'backdrop-filter', _imageFilterToCss(filter)); + DomRenderer.setElementStyle(_filterElement!, 'backdrop-filter', filter.filterAttribute); } } diff --git a/lib/web_ui/lib/src/engine/html/image_filter.dart b/lib/web_ui/lib/src/engine/html/image_filter.dart index bd46482d60..7c7df2a43d 100644 --- a/lib/web_ui/lib/src/engine/html/image_filter.dart +++ b/lib/web_ui/lib/src/engine/html/image_filter.dart @@ -18,7 +18,8 @@ class PersistedImageFilter extends PersistedContainerSurface @override void apply() { - rootElement!.style.filter = _imageFilterToCss(filter as EngineImageFilter); + rootElement!.style.filter = (filter as EngineImageFilter).filterAttribute; + rootElement!.style.transform = (filter as EngineImageFilter).transformAttribute; } @override diff --git a/lib/web_ui/lib/src/engine/html/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart index bb6801fa62..5fe793e5bb 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -582,10 +582,3 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { throw UnimplementedError(); } } - -// HTML only supports a single radius, but Flutter ImageFilter supports separate -// horizontal and vertical radii. The best approximation we can provide is to -// average the two radii together for a single compromise value. -String _imageFilterToCss(EngineImageFilter filter) { - return 'blur(${(filter.sigmaX + filter.sigmaY) / 2}px)'; -} diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart index dcadd6587d..c5f374b262 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -660,25 +660,79 @@ class GradientConical extends GradientRadial { /// Backend implementation of [ui.ImageFilter]. /// -/// Currently only `blur` is supported. -class EngineImageFilter implements ui.ImageFilter { - EngineImageFilter.blur({this.sigmaX = 0.0, this.sigmaY = 0.0}); +/// Currently only `blur` and `matrix` are supported. +abstract class EngineImageFilter implements ui.ImageFilter { + factory EngineImageFilter.blur({ + required double sigmaX, + required double sigmaY, + required ui.TileMode tileMode, + }) = _BlurEngineImageFilter; + + factory EngineImageFilter.matrix({ + required Float64List matrix, + required ui.FilterQuality filterQuality, + }) = _MatrixEngineImageFilter; + + EngineImageFilter._(); + + String get filterAttribute => ''; + String get transformAttribute => ''; +} + +class _BlurEngineImageFilter extends EngineImageFilter { + _BlurEngineImageFilter({ this.sigmaX = 0.0, this.sigmaY = 0.0, this.tileMode = ui.TileMode.clamp }) : super._(); final double sigmaX; final double sigmaY; + final ui.TileMode tileMode; + + // TODO(flutter_web): implement TileMode. + String get filterAttribute => blurSigmasToCssString(sigmaX, sigmaY); @override bool operator ==(Object other) { - return other is EngineImageFilter && + if (other.runtimeType != runtimeType) + return false; + return other is _BlurEngineImageFilter && + other.tileMode == tileMode && other.sigmaX == sigmaX && other.sigmaY == sigmaY; } @override - int get hashCode => ui.hashValues(sigmaX, sigmaY); + int get hashCode => ui.hashValues(sigmaX, sigmaY, tileMode); + + @override + String toString() { + return 'ImageFilter.blur($sigmaX, $sigmaY, $tileMode)'; + } +} + +class _MatrixEngineImageFilter extends EngineImageFilter { + _MatrixEngineImageFilter({ required Float64List matrix, required this.filterQuality }) + : webMatrix = Float64List.fromList(matrix), + super._(); + + final Float64List webMatrix; + final ui.FilterQuality filterQuality; + + // TODO(flutter_web): implement FilterQuality. + String get transformAttribute => float64ListToCssTransform(webMatrix); + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) + return false; + return other is _MatrixEngineImageFilter + && other.filterQuality == filterQuality + && listEquals(other.webMatrix, webMatrix); + } + + @override + int get hashCode => ui.hashValues(ui.hashList(webMatrix), filterQuality); @override String toString() { - return 'ImageFilter.blur($sigmaX, $sigmaY)'; + return 'ImageFilter.matrix($webMatrix, $filterQuality)'; } } diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 2b742702a9..46b609b145 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -81,7 +81,7 @@ void setElementTransform(html.Element element, Float32List matrix4) { /// See also: /// * https://github.com/flutter/flutter/issues/32274 /// * https://bugs.chromium.org/p/chromium/issues/detail?id=1040222 -String float64ListToCssTransform(Float32List matrix) { +String float64ListToCssTransform(List matrix) { assert(matrix.length == 16); final TransformKind transformKind = transformKindOf(matrix); if (transformKind == TransformKind.transform2d) { @@ -113,9 +113,9 @@ enum TransformKind { } /// Detects the kind of transform the [matrix] performs. -TransformKind transformKindOf(Float32List matrix) { +TransformKind transformKindOf(List matrix) { assert(matrix.length == 16); - final Float32List m = matrix; + final List m = matrix; // If matrix contains scaling, rotation, z translation or // perspective transform, it is not considered simple. @@ -171,15 +171,15 @@ bool isIdentityFloat32ListTransform(Float32List matrix) { /// permitted. However, it is inefficient to construct a matrix for an identity /// transform. Consider removing the CSS `transform` property from elements /// that apply identity transform. -String float64ListToCssTransform2d(Float32List matrix) { +String float64ListToCssTransform2d(List matrix) { assert(transformKindOf(matrix) != TransformKind.complex); return 'matrix(${matrix[0]},${matrix[1]},${matrix[4]},${matrix[5]},${matrix[12]},${matrix[13]})'; } /// Converts [matrix] to a 3D CSS transform value. -String float64ListToCssTransform3d(Float32List matrix) { +String float64ListToCssTransform3d(List matrix) { assert(matrix.length == 16); - final Float32List m = matrix; + final List m = matrix; if (m[0] == 1.0 && m[1] == 0.0 && m[2] == 0.0 && @@ -552,3 +552,10 @@ bool listEquals(List? a, List? b) { } return true; } + +// HTML only supports a single radius, but Flutter ImageFilter supports separate +// horizontal and vertical radii. The best approximation we can provide is to +// average the two radii together for a single compromise value. +String blurSigmasToCssString(double sigmaX, double sigmaY) { + return 'blur(${(sigmaX + sigmaY) * 0.5}px)'; +} diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 94a1214156..b42ee71725 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -399,14 +399,18 @@ class ImageFilter { if (engine.useCanvasKit) { return engine.CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); } - return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY); + // TODO(flutter_web): implement TileMode. + return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); } - ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) { - // TODO(flutter_web): add implementation. - throw UnimplementedError('ImageFilter.matrix not implemented for web platform.'); - // if (matrix4.length != 16) - // throw ArgumentError('"matrix4" must have 16 entries.'); + factory ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) { + if (matrix4.length != 16) + throw ArgumentError('"matrix4" must have 16 entries.'); + if (engine.useCanvasKit) { + return engine.CkImageFilter.matrix(matrix: matrix4, filterQuality: filterQuality); + } + // TODO(flutter_web): implement FilterQuality. + return engine.EngineImageFilter.matrix(matrix: matrix4, filterQuality: filterQuality); } ImageFilter.compose({required ImageFilter outer, required ImageFilter inner}) { diff --git a/lib/web_ui/test/engine/surface/filters/image_filter_test.dart b/lib/web_ui/test/engine/surface/filters/image_filter_test.dart new file mode 100644 index 0000000000..147594dd35 --- /dev/null +++ b/lib/web_ui/test/engine/surface/filters/image_filter_test.dart @@ -0,0 +1,79 @@ +// 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. + +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'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('ImageFilter constructors', () { + test('matrix is copied', () { + Matrix4 matrix = Matrix4.identity(); + Float64List storage = matrix.toFloat64(); + ImageFilter filter1 = ImageFilter.matrix(storage); + storage[0] = 2.0; + ImageFilter filter2 = ImageFilter.matrix(storage); + expect(filter1, filter1); + expect(filter2, filter2); + expect(filter1, isNot(equals(filter2))); + expect(filter2, isNot(equals(filter1))); + }); + + test('matrix tests all values on ==', () { + Matrix4 matrix = Matrix4.identity(); + Float64List storage = matrix.toFloat64(); + ImageFilter filter1a = ImageFilter.matrix(storage, filterQuality: FilterQuality.none); + ImageFilter filter1b = ImageFilter.matrix(storage, filterQuality: FilterQuality.high); + + storage[0] = 2.0; + ImageFilter filter2a = ImageFilter.matrix(storage, filterQuality: FilterQuality.none); + ImageFilter filter2b = ImageFilter.matrix(storage, filterQuality: FilterQuality.high); + + expect(filter1a, filter1a); + expect(filter1a, isNot(equals(filter1b))); + expect(filter1a, isNot(equals(filter2a))); + expect(filter1a, isNot(equals(filter2b))); + + expect(filter1b, isNot(equals(filter1a))); + expect(filter1b, filter1b); + expect(filter1b, isNot(equals(filter2a))); + expect(filter1b, isNot(equals(filter2b))); + + expect(filter2a, isNot(equals(filter1a))); + expect(filter2a, isNot(equals(filter1b))); + expect(filter2a, filter2a); + expect(filter2a, isNot(equals(filter2b))); + + expect(filter2b, isNot(equals(filter1a))); + expect(filter2b, isNot(equals(filter1b))); + expect(filter2b, isNot(equals(filter2a))); + expect(filter2b, filter2b); + }); + + test('blur tests all values on ==', () { + ImageFilter filter1 = ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0, tileMode: TileMode.decal); + ImageFilter filter2 = ImageFilter.blur(sigmaX: 2.0, sigmaY: 3.0, tileMode: TileMode.decal); + ImageFilter filter3 = ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0, tileMode: TileMode.mirror); + + expect(filter1, filter1); + expect(filter1, isNot(equals(filter2))); + expect(filter1, isNot(equals(filter3))); + + expect(filter2, isNot(equals(filter1))); + expect(filter2, filter2); + expect(filter2, isNot(equals(filter3))); + + expect(filter3, isNot(equals(filter1))); + expect(filter3, isNot(equals(filter2))); + expect(filter3, filter3); + }); + }); +} diff --git a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart index 6fd091aff8..4d9d751097 100644 --- a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart @@ -361,7 +361,7 @@ void testMain() async { viewElement6.remove(); }); - test('pushImageFilter', () async { + test('pushImageFilter blur', () async { final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); builder.pushImageFilter( ImageFilter.blur(sigmaX: 1, sigmaY: 3), @@ -374,6 +374,25 @@ void testMain() async { await matchGoldenFile('compositing_image_filter.png', region: region); }); + test('pushImageFilter matrix', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + builder.pushImageFilter( + ImageFilter.matrix( + ( + Matrix4.identity() + ..translate(40, 10) + ..rotateZ(math.pi / 6) + ..scale(0.75, 0.75) + ).toFloat64()), + ); + _drawTestPicture(builder); + builder.pop(); + + html.document.body.append(builder.build().webOnlyRootElement); + + await matchGoldenFile('compositing_image_filter_matrix.png', region: region); + }); + group('Cull rect computation', () { _testCullRectComputation(); }); -- GitLab