From b84b4e6df775c421ca7d1db45e9a1b25ac61329c Mon Sep 17 00:00:00 2001 From: Ferhat Date: Thu, 27 Aug 2020 08:49:12 -0700 Subject: [PATCH] [web] Implement SceneBuilder.pushColorFilter for html (flutter/engine#20802) * Implement Color filter layer * Add test with BlendMode color * update licenses file * Move blend functions into color_filter.dart * dartfmt --- .../ci/licenses_golden/licenses_flutter | 1 + .../flutter/lib/web_ui/dev/goldens_lock.yaml | 2 +- .../flutter/lib/web_ui/lib/src/engine.dart | 1 + .../web_ui/lib/src/engine/bitmap_canvas.dart | 159 +--------- .../lib/src/engine/html/color_filter.dart | 292 ++++++++++++++++++ .../lib/src/engine/html/scene_builder.dart | 2 +- .../engine/color_filter_golden_test.dart | 89 ++++++ 7 files changed, 388 insertions(+), 158 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/html/color_filter.dart create mode 100644 engine/src/flutter/lib/web_ui/test/golden_tests/engine/color_filter_golden_test.dart 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 '' - '' - '' // Just take alpha channel of destination - '' - '' - '' - '' - ''; -} - -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 '' - '' - '' - '' + - (swapLayers - ? '' - : '') + - ''; -} 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 '' + '' + '' // Just take alpha channel of destination + '' + '' + '' + '' + ''; +} + +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 '' + '' + '' + '' + + (swapLayers + ? '' + : '') + + ''; +} 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(); +}