diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter
index 99124c9effd..a1a2c18dccb 100755
--- a/engine/src/flutter/ci/licenses_golden/licenses_flutter
+++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter
@@ -465,6 +465,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart
+FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/color_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/image_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/offset.dart
diff --git a/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml b/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml
index 7daac2c7ae5..7022f8ec955 100644
--- a/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml
+++ b/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
-revision: f26d68c3596eece3d40112e9dff01dc55d9bae97
+revision: 8a831253654d151635ee9cfb71389c257413de5d
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart
index 232b00e4c60..eabb13c4989 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart
@@ -67,6 +67,7 @@ part 'engine/history.dart';
part 'engine/html/backdrop_filter.dart';
part 'engine/html/canvas.dart';
part 'engine/html/clip.dart';
+part 'engine/html/color_filter.dart';
part 'engine/html/debug_canvas_reuse_overlay.dart';
part 'engine/html/image_filter.dart';
part 'engine/html/offset.dart';
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart
index 1954fa52313..4aafaa3e90c 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart
@@ -573,57 +573,10 @@ class BitmapCanvas extends EngineCanvas {
ui.Color? filterColor, ui.BlendMode colorFilterBlendMode,
SurfacePaintData paint) {
// For srcIn blendMode, we use an svg filter to apply to image element.
- String? svgFilter;
- switch (colorFilterBlendMode) {
- case ui.BlendMode.srcIn:
- case ui.BlendMode.srcATop:
- svgFilter = _srcInColorFilterToSvg(filterColor);
- break;
- case ui.BlendMode.srcOut:
- svgFilter = _srcOutColorFilterToSvg(filterColor);
- break;
- case ui.BlendMode.xor:
- svgFilter = _xorColorFilterToSvg(filterColor);
- break;
- case ui.BlendMode.plus:
- // Porter duff source + destination.
- svgFilter = _compositeColorFilterToSvg(filterColor, 0, 1, 1, 0);
- break;
- case ui.BlendMode.modulate:
- // Porter duff source * destination but preserves alpha.
- svgFilter = _modulateColorFilterToSvg(filterColor!);
- break;
- case ui.BlendMode.overlay:
- // Since overlay is the same as hard-light by swapping layers,
- // pass hard-light blend function.
- svgFilter = _blendColorFilterToSvg(filterColor, 'hard-light',
- swapLayers: true);
- break;
- // Several of the filters below (although supported) do not render the
- // same (close but not exact) as native flutter when used as blend mode
- // for a background-image with a background color. They only look
- // identical when feBlend is used within an svg filter definition.
- //
- // Saturation filter uses destination when source is transparent.
- // cMax = math.max(r, math.max(b, g));
- // cMin = math.min(r, math.min(b, g));
- // delta = cMax - cMin;
- // lightness = (cMax + cMin) / 2.0;
- // saturation = delta / (1.0 - (2 * lightness - 1.0).abs());
- case ui.BlendMode.saturation:
- case ui.BlendMode.colorDodge:
- case ui.BlendMode.colorBurn:
- case ui.BlendMode.hue:
- case ui.BlendMode.color:
- case ui.BlendMode.luminosity:
- svgFilter = _blendColorFilterToSvg(filterColor,
- _stringForBlendMode(colorFilterBlendMode));
- break;
- default:
- break;
- }
+ String? svgFilter = svgFilterFromBlendMode(filterColor,
+ colorFilterBlendMode);
final html.Element filterElement =
- html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer());
+ html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer());
rootElement.append(filterElement);
_children.add(filterElement);
final html.HtmlElement imgElement = _reuseOrCreateImage(image);
@@ -1013,109 +966,3 @@ String _maskFilterToCanvasFilter(ui.MaskFilter? maskFilter) {
}
}
-int _filterIdCounter = 0;
-
-// The color matrix for feColorMatrix element changes colors based on
-// the following:
-//
-// | R' | | r1 r2 r3 r4 r5 | | R |
-// | G' | | g1 g2 g3 g4 g5 | | G |
-// | B' | = | b1 b2 b3 b4 b5 | * | B |
-// | A' | | a1 a2 a3 a4 a5 | | A |
-// | 1 | | 0 0 0 0 1 | | 1 |
-//
-// R' = r1*R + r2*G + r3*B + r4*A + r5
-// G' = g1*R + g2*G + g3*B + g4*A + g5
-// B' = b1*R + b2*G + b3*B + b4*A + b5
-// A' = a1*R + a2*G + a3*B + a4*A + a5
-String _srcInColorFilterToSvg(ui.Color? color) {
- _filterIdCounter += 1;
- return '';
-}
-
-String _srcOutColorFilterToSvg(ui.Color? color) {
- _filterIdCounter += 1;
- return '';
-}
-
-String _xorColorFilterToSvg(ui.Color? color) {
- _filterIdCounter += 1;
- return '';
-}
-
-// The source image and color are composited using :
-// result = k1 *in*in2 + k2*in + k3*in2 + k4.
-String _compositeColorFilterToSvg(ui.Color? color, double k1, double k2, double k3 , double k4) {
- _filterIdCounter += 1;
- return '';
-}
-
-// Porter duff source * destination , keep source alpha.
-// First apply color filter to source to change it to [color], then
-// composite using multiplication.
-String _modulateColorFilterToSvg(ui.Color color) {
- _filterIdCounter += 1;
- final double r = color.red / 255.0;
- final double b = color.blue / 255.0;
- final double g = color.green / 255.0;
- return '';
-}
-
-// Uses feBlend element to blend source image with a color.
-String _blendColorFilterToSvg(ui.Color? color, String? feBlend,
- {bool swapLayers = false}) {
- _filterIdCounter += 1;
- return '';
-}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart
new file mode 100644
index 00000000000..2a37cfd7966
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart
@@ -0,0 +1,292 @@
+// 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.10
+part of engine;
+
+/// A surface that applies an [ColorFilter] to its children.
+class PersistedColorFilter extends PersistedContainerSurface
+ implements ui.ColorFilterEngineLayer {
+ PersistedColorFilter(PersistedColorFilter? oldLayer, this.filter)
+ : super(oldLayer);
+
+ @override
+ html.Element? get childContainer => _childContainer;
+
+ /// The dedicated child container element that's separate from the
+ /// [rootElement] is used to compensate for the coordinate system shift
+ /// introduced by the [rootElement] translation.
+ html.Element? _childContainer;
+
+ final ui.ColorFilter filter;
+ html.Element? _filterElement;
+ bool containerVisible = true;
+
+ @override
+ void adoptElements(PersistedColorFilter oldSurface) {
+ super.adoptElements(oldSurface);
+ _childContainer = oldSurface._childContainer;
+ oldSurface._childContainer = null;
+ }
+
+ @override
+ void discard() {
+ super.discard();
+ // Do not detach the child container from the root. It is permanently
+ // attached. The elements are reused together and are detached from the DOM
+ // together.
+ _childContainer = null;
+ }
+
+ @override
+ html.Element createElement() {
+ html.Element element = defaultCreateElement('flt-color-filter');
+ html.Element container = html.Element.tag('flt-filter-interior');
+ container.style.position = 'absolute';
+ _childContainer = container;
+ element.append(_childContainer!);
+ return element;
+ }
+
+ @override
+ void apply() {
+ if (_filterElement != null) {
+ _filterElement?.remove();
+ }
+ final EngineColorFilter? engineValue = filter as EngineColorFilter?;
+ if (engineValue == null) {
+ rootElement!.style.backgroundColor = '';
+ childContainer?.style.visibility = 'visible';
+ return;
+ }
+ if (engineValue._blendMode == null) {
+ rootElement!.style.backgroundColor =
+ colorToCssString(engineValue._color!);
+ childContainer?.style.visibility = 'visible';
+ return;
+ }
+
+ 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;
+ }
+
+ // 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;
+ }
+ }
+ }
+
+ @override
+ void update(PersistedColorFilter oldSurface) {
+ super.update(oldSurface);
+
+ if (oldSurface.filter != filter) {
+ apply();
+ }
+ }
+}
+
+String? svgFilterFromBlendMode(
+ ui.Color? filterColor, ui.BlendMode colorFilterBlendMode) {
+ String? svgFilter;
+ switch (colorFilterBlendMode) {
+ case ui.BlendMode.srcIn:
+ case ui.BlendMode.srcATop:
+ svgFilter = _srcInColorFilterToSvg(filterColor);
+ break;
+ case ui.BlendMode.srcOut:
+ svgFilter = _srcOutColorFilterToSvg(filterColor);
+ break;
+ case ui.BlendMode.xor:
+ svgFilter = _xorColorFilterToSvg(filterColor);
+ break;
+ case ui.BlendMode.plus:
+ // Porter duff source + destination.
+ svgFilter = _compositeColorFilterToSvg(filterColor, 0, 1, 1, 0);
+ break;
+ case ui.BlendMode.modulate:
+ // Porter duff source * destination but preserves alpha.
+ svgFilter = _modulateColorFilterToSvg(filterColor!);
+ break;
+ case ui.BlendMode.overlay:
+ // Since overlay is the same as hard-light by swapping layers,
+ // pass hard-light blend function.
+ svgFilter =
+ _blendColorFilterToSvg(filterColor, 'hard-light', swapLayers: true);
+ break;
+ // Several of the filters below (although supported) do not render the
+ // same (close but not exact) as native flutter when used as blend mode
+ // for a background-image with a background color. They only look
+ // identical when feBlend is used within an svg filter definition.
+ //
+ // Saturation filter uses destination when source is transparent.
+ // cMax = math.max(r, math.max(b, g));
+ // cMin = math.min(r, math.min(b, g));
+ // delta = cMax - cMin;
+ // lightness = (cMax + cMin) / 2.0;
+ // saturation = delta / (1.0 - (2 * lightness - 1.0).abs());
+ case ui.BlendMode.saturation:
+ case ui.BlendMode.colorDodge:
+ case ui.BlendMode.colorBurn:
+ case ui.BlendMode.hue:
+ case ui.BlendMode.color:
+ case ui.BlendMode.luminosity:
+ case ui.BlendMode.multiply:
+ 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:
+ svgFilter = _blendColorFilterToSvg(
+ filterColor, _stringForBlendMode(colorFilterBlendMode));
+ break;
+ default:
+ break;
+ }
+ return svgFilter;
+}
+
+int _filterIdCounter = 0;
+
+// The color matrix for feColorMatrix element changes colors based on
+// the following:
+//
+// | R' | | r1 r2 r3 r4 r5 | | R |
+// | G' | | g1 g2 g3 g4 g5 | | G |
+// | B' | = | b1 b2 b3 b4 b5 | * | B |
+// | A' | | a1 a2 a3 a4 a5 | | A |
+// | 1 | | 0 0 0 0 1 | | 1 |
+//
+// R' = r1*R + r2*G + r3*B + r4*A + r5
+// G' = g1*R + g2*G + g3*B + g4*A + g5
+// B' = b1*R + b2*G + b3*B + b4*A + b5
+// A' = a1*R + a2*G + a3*B + a4*A + a5
+String _srcInColorFilterToSvg(ui.Color? color) {
+ _filterIdCounter += 1;
+ return '';
+}
+
+String _srcOutColorFilterToSvg(ui.Color? color) {
+ _filterIdCounter += 1;
+ return '';
+}
+
+String _xorColorFilterToSvg(ui.Color? color) {
+ _filterIdCounter += 1;
+ return '';
+}
+
+// The source image and color are composited using :
+// result = k1 *in*in2 + k2*in + k3*in2 + k4.
+String _compositeColorFilterToSvg(
+ ui.Color? color, double k1, double k2, double k3, double k4) {
+ _filterIdCounter += 1;
+ return '';
+}
+
+// Porter duff source * destination , keep source alpha.
+// First apply color filter to source to change it to [color], then
+// composite using multiplication.
+String _modulateColorFilterToSvg(ui.Color color) {
+ _filterIdCounter += 1;
+ final double r = color.red / 255.0;
+ final double b = color.blue / 255.0;
+ final double g = color.green / 255.0;
+ return '';
+}
+
+// Uses feBlend element to blend source image with a color.
+String _blendColorFilterToSvg(ui.Color? color, String? feBlend,
+ {bool swapLayers = false}) {
+ _filterIdCounter += 1;
+ return '';
+}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart
index e6cb5d185f7..77a9197ca2d 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart
@@ -179,7 +179,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
ui.ColorFilterEngineLayer? oldLayer,
}) {
assert(filter != null); // ignore: unnecessary_null_comparison
- throw UnimplementedError();
+ return _pushSurface(PersistedColorFilter(oldLayer as PersistedColorFilter?, filter)) as ui.ColorFilterEngineLayer;
}
/// Pushes an image filter operation onto the operation stack.
diff --git a/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart
new file mode 100644
index 00000000000..97ca81b2942
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart
@@ -0,0 +1,89 @@
+// 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:html' as html;
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/ui.dart';
+import 'package:ui/src/engine.dart';
+
+import 'package:web_engine_tester/golden_tester.dart';
+
+final Rect region = Rect.fromLTWH(0, 0, 500, 500);
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+void testMain() async {
+ setUp(() async {
+ debugShowClipLayers = true;
+ SurfaceSceneBuilder.debugForgetFrameScene();
+ for (html.Node scene in html.document.querySelectorAll('flt-scene')) {
+ scene.remove();
+ }
+
+ await webOnlyInitializePlatform();
+ webOnlyFontCollection.debugRegisterTestFonts();
+ await webOnlyFontCollection.ensureFontsLoaded();
+ });
+
+ test('Should apply color filter to image', () async {
+ final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
+ final Picture backgroundPicture = _drawBackground();
+ builder.addPicture(Offset.zero, backgroundPicture);
+ builder.pushColorFilter(EngineColorFilter.mode(Color(0xF0000080),
+ BlendMode.color));
+ final Picture circles1 = _drawTestPictureWithCircles(30, 30);
+ builder.addPicture(Offset.zero, circles1);
+ builder.pop();
+ html.document.body.append(builder
+ .build()
+ .webOnlyRootElement);
+
+ await matchGoldenFile('color_filter_blendMode_color.png', region: region);
+ });
+}
+
+Picture _drawTestPictureWithCircles(double offsetX, double offsetY) {
+ final EnginePictureRecorder recorder = PictureRecorder();
+ final RecordingCanvas canvas =
+ recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
+ canvas.drawCircle(
+ Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill);
+ canvas.drawCircle(
+ Offset(offsetX + 60, offsetY + 10),
+ 10,
+ Paint()
+ ..style = PaintingStyle.fill
+ ..color = const Color.fromRGBO(255, 0, 0, 1));
+ canvas.drawCircle(
+ Offset(offsetX + 10, offsetY + 60),
+ 10,
+ Paint()
+ ..style = PaintingStyle.fill
+ ..color = const Color.fromRGBO(0, 255, 0, 1));
+ canvas.drawCircle(
+ Offset(offsetX + 60, offsetY + 60),
+ 10,
+ Paint()
+ ..style = PaintingStyle.fill
+ ..color = const Color.fromRGBO(0, 0, 255, 1));
+ return recorder.endRecording();
+}
+
+Picture _drawBackground() {
+ final EnginePictureRecorder recorder = PictureRecorder();
+ final RecordingCanvas canvas =
+ recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
+ canvas.drawRect(
+ Rect.fromLTWH(8, 8, 400.0 - 16, 400.0 - 16),
+ Paint()
+ ..style = PaintingStyle.fill
+ ..color = Color(0xFFE0FFE0)
+ );
+ return recorder.endRecording();
+}