[web] Fix webkit ColorFilter.mode for webkit (flutter/engine#27361)

This commit is contained in:
Ferhat 2021-07-19 16:48:20 -07:00 committed by GitHub
parent 8a12a40f1c
commit b6dbaa079a
14 changed files with 138 additions and 44 deletions

View File

@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: 25fad8d9927e443345c28b408220ded99b65d647
revision: 10ed22e7e6a5039b84e8c028828a86a7ff98b0be

View File

@ -76,6 +76,10 @@ class DomRenderer {
html.Element? get sceneHostElement => _sceneHostElement;
html.Element? _sceneHostElement;
/// A child element of body outside the shadowroot that hosts
/// global resources such svg filters and clip paths when using webkit.
html.Element? _resourcesHost;
/// The element that contains the semantics tree.
///
/// This element is created and inserted in the HTML DOM once. It is never
@ -275,6 +279,8 @@ class DomRenderer {
_styleElement?.remove();
_styleElement = html.StyleElement();
_resourcesHost?.remove();
_resourcesHost = null;
html.document.head!.append(_styleElement!);
final html.CssStyleSheet sheet = _styleElement!.sheet as html.CssStyleSheet;
applyGlobalCssRulesToSheet(
@ -615,6 +621,33 @@ class DomRenderer {
return null;
}
/// Add an element as a global resource to be referenced by CSS.
///
/// This call create a global resource host element on demand and either
/// place it as first element of body(webkit), or as a child of
/// glass pane element for other browsers to make sure url resolution
/// works correctly when content is inside a shadow root.
void addResource(html.Element element) {
final bool isWebKit = browserEngine == BrowserEngine.webkit;
if (_resourcesHost == null) {
_resourcesHost = html.DivElement()
..style.visibility = 'hidden';
if (isWebKit) {
final html.Node bodyNode = html.document.body!;
bodyNode.insertBefore(_resourcesHost!, bodyNode.firstChild);
} else {
_glassPaneShadow!.node.insertBefore(
_resourcesHost!, _glassPaneShadow!.node.firstChild);
}
}
_resourcesHost!.append(element);
}
/// Removes a global resource element.
void removeResource(html.Element? element) {
element?.remove();
}
/// Provides haptic feedback.
void vibrate(int durationMs) {
final html.Navigator navigator = html.window.navigator;

View File

@ -368,7 +368,7 @@ class BitmapCanvas extends EngineCanvas {
/// prefer DOM if canvas has not been allocated yet.
///
bool _useDomForRenderingFill(SurfacePaintData paint) =>
_renderStrategy.isInsideShaderMask ||
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
(_childOverdraw &&
_canvasPool.canvas == null &&
@ -380,7 +380,7 @@ class BitmapCanvas extends EngineCanvas {
///
/// DOM canvas is generated for simple strokes using borders.
bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) =>
_renderStrategy.isInsideShaderMask ||
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
((_childOverdraw ||
_renderStrategy.hasImageElements ||

View File

@ -243,6 +243,15 @@ class PersistedPhysicalShape extends PersistedContainerSurface
return super.createElement()..setAttribute('clip-type', 'physical-shape');
}
@override
void discard() {
super.discard();
_clipElement?.remove();
_clipElement = null;
_svgElement?.remove();
_svgElement = null;
}
@override
void apply() {
_applyShape();
@ -443,6 +452,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface
if (_svgElement != null) {
rootElement!.insertBefore(_svgElement!, childContainer);
}
oldSurface._svgElement = null;
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:ui/ui.dart' as ui;
import '../../engine.dart' show NullTreeSanitizer;
import '../canvaskit/color_filter.dart';
import '../color_filter.dart';
import '../dom_renderer.dart';
import '../util.dart';
import 'bitmap_canvas.dart';
import 'path_to_svg_clip.dart';
@ -36,12 +37,21 @@ class PersistedColorFilter extends PersistedContainerSurface
void adoptElements(PersistedColorFilter oldSurface) {
super.adoptElements(oldSurface);
_childContainer = oldSurface._childContainer;
_filterElement = oldSurface._filterElement;
oldSurface._childContainer = null;
}
@override
void preroll(PrerollSurfaceContext prerollContext) {
++prerollContext.activeColorFilterCount;
super.preroll(prerollContext);
--prerollContext.activeColorFilterCount;
}
@override
void discard() {
super.discard();
domRenderer.removeResource(_filterElement);
// 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.
@ -60,9 +70,8 @@ class PersistedColorFilter extends PersistedContainerSurface
@override
void apply() {
if (_filterElement != null) {
_filterElement?.remove();
}
domRenderer.removeResource(_filterElement);
_filterElement = null;
final EngineColorFilter? engineValue = filter as EngineColorFilter?;
if (engineValue == null) {
rootElement!.style.backgroundColor = '';
@ -81,12 +90,12 @@ class PersistedColorFilter extends PersistedContainerSurface
void _applyBlendModeFilter(CkBlendModeColorFilter colorFilter) {
final ui.Color filterColor = colorFilter.color;
ui.BlendMode colorFilterBlendMode = colorFilter.blendMode;
final html.CssStyleDeclaration style = rootElement!.style;
final html.CssStyleDeclaration style = childContainer!.style;
switch (colorFilterBlendMode) {
case ui.BlendMode.clear:
case ui.BlendMode.dstOut:
case ui.BlendMode.srcOut:
childContainer?.style.visibility = 'hidden';
style.visibility = 'hidden';
return;
case ui.BlendMode.dst:
case ui.BlendMode.dstIn:
@ -130,8 +139,9 @@ class PersistedColorFilter extends PersistedContainerSurface
if (svgFilter != null) {
_filterElement =
html.Element.html(svgFilter, treeSanitizer: NullTreeSanitizer());
rootElement!.append(_filterElement!);
rootElement!.style.filter = 'url(#_fcf${filterIdCounter})';
//rootElement!.insertBefore(_filterElement!, childContainer!);
domRenderer.addResource(_filterElement!);
style.filter = 'url(#_fcf${filterIdCounter})';
if (colorFilterBlendMode == ui.BlendMode.saturation ||
colorFilterBlendMode == ui.BlendMode.multiply ||
colorFilterBlendMode == ui.BlendMode.modulate) {
@ -145,8 +155,8 @@ class PersistedColorFilter extends PersistedContainerSurface
if (svgFilter != null) {
_filterElement =
html.Element.html(svgFilter, treeSanitizer: NullTreeSanitizer());
rootElement!.append(_filterElement!);
rootElement!.style.filter = 'url(#_fcf${filterIdCounter})';
domRenderer.addResource(_filterElement!);
childContainer!.style.filter = 'url(#_fcf${filterIdCounter})';
}
}
@ -220,7 +230,7 @@ String? svgFilterFromBlendMode(
case ui.BlendMode.difference:
case ui.BlendMode.exclusion:
svgFilter = _blendColorFilterToSvg(
filterColor, stringForBlendMode(colorFilterBlendMode));
filterColor, stringForBlendMode(colorFilterBlendMode)!);
break;
case ui.BlendMode.src:
case ui.BlendMode.dst:
@ -251,7 +261,7 @@ String? svgFilterFromColorMatrix(List<double> matrix) {
return '$kSvgResourceHeader'
'<filter id="_fcf$filterIdCounter" '
'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
'<feColorMatrix values="$sbMatrix" result="comp"/>'
'<feColorMatrix type="matrix" values="$sbMatrix" result="comp"/>'
'</filter></svg>';
}
@ -273,12 +283,12 @@ int filterIdCounter = 0;
String _srcInColorFilterToSvg(ui.Color? color) {
filterIdCounter += 1;
return '$kSvgResourceHeader'
'<filter id="_fcf$filterIdCounter" '
'<filter id="_fcf$filterIdCounter" color-interpolation-filters="sRGB" '
'filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">'
'<feColorMatrix values="0 0 0 0 1 ' // Ignore input, set it to absolute.
'0 0 0 0 1 '
'0 0 0 0 1 '
'0 0 0 1 0" result="destalpha"/>' // Just take alpha channel of destination
'<feColorMatrix type="matrix" values="0 0 0 0 1\n' // Ignore input, set it to absolute.
'0 0 0 0 1\n'
'0 0 0 0 1\n'
'0 0 0 1 0" result="destalpha"></feColorMatrix>>' // Just take alpha channel of destination
'<feFlood flood-color="${colorToCssString(color)}" flood-opacity="1" result="flood">'
'</feFlood>'
'<feComposite in="flood" in2="destalpha" '
@ -363,7 +373,7 @@ String _modulateColorFilterToSvg(ui.Color color) {
}
// Uses feBlend element to blend source image with a color.
String _blendColorFilterToSvg(ui.Color? color, String? feBlend,
String _blendColorFilterToSvg(ui.Color? color, String feBlend,
{bool swapLayers = false}) {
filterIdCounter += 1;
return '$kSvgResourceHeader'

View File

@ -18,7 +18,6 @@ import 'bitmap_canvas.dart';
import 'debug_canvas_reuse_overlay.dart';
import 'dom_canvas.dart';
import 'path/path_metrics.dart';
import 'shader_mask.dart';
import 'surface.dart';
import 'surface_stats.dart';
@ -128,11 +127,12 @@ class PersistedPicture extends PersistedLeafSurface {
}
@override
void preroll() {
if (PersistedShaderMask.activeShaderMaskCount != 0) {
picture.recordingCanvas?.renderStrategy.isInsideShaderMask = true;
void preroll(PrerollSurfaceContext prerollContext) {
if (prerollContext.activeShaderMaskCount != 0 ||
prerollContext.activeColorFilterCount != 0) {
picture.recordingCanvas?.renderStrategy.isInsideSvgFilterTree = true;
}
super.preroll();
super.preroll(prerollContext);
}
@override

View File

@ -2042,12 +2042,12 @@ class RenderStrategy {
/// This is used to decide whether to use simplified DomCanvas.
bool hasArbitraryPaint = false;
/// Whether commands are executed within a shadermask.
/// Whether commands are executed within a shadermask or color filter.
///
/// Webkit doesn't apply filters to canvas elements in its child
/// element tree. When this is set to true, we prevent canvas usage in
/// bitmap canvas and instead render using dom primitives and svg only.
bool isInsideShaderMask = false;
bool isInsideSvgFilterTree = false;
RenderStrategy();

View File

@ -553,7 +553,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
// Auto-pop layers that were pushed without a corresponding pop.
pop();
}
_persistedScene.preroll();
_persistedScene.preroll(PrerollSurfaceContext());
});
return timeAction<SurfaceScene>(kProfileApplyFrame, () {
if (_lastFrameScene == null) {

View File

@ -8,6 +8,7 @@ import 'package:ui/ui.dart' as ui;
import '../../engine.dart' show NullTreeSanitizer;
import '../browser_detection.dart';
import '../dom_renderer.dart';
import 'bitmap_canvas.dart';
import 'path_to_svg_clip.dart';
import 'shaders/shader.dart';
@ -40,7 +41,6 @@ class PersistedShaderMask extends PersistedContainerSurface
final ui.BlendMode blendMode;
final ui.FilterQuality filterQuality;
html.Element? _shaderElement;
static int activeShaderMaskCount = 0;
final bool isWebKit = browserEngine == BrowserEngine.webkit;
@override
@ -58,6 +58,7 @@ class PersistedShaderMask extends PersistedContainerSurface
@override
void discard() {
super.discard();
domRenderer.removeResource(_shaderElement);
// 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.
@ -65,10 +66,10 @@ class PersistedShaderMask extends PersistedContainerSurface
}
@override
void preroll() {
++activeShaderMaskCount;
super.preroll();
--activeShaderMaskCount;
void preroll(PrerollSurfaceContext prerollContext) {
++prerollContext.activeShaderMaskCount;
super.preroll(prerollContext);
--prerollContext.activeShaderMaskCount;
}
@override
@ -83,7 +84,7 @@ class PersistedShaderMask extends PersistedContainerSurface
@override
void apply() {
_shaderElement?.remove();
domRenderer.removeResource(_shaderElement);
_shaderElement = null;
if (shader is ui.Gradient) {
rootElement!.style
@ -164,7 +165,7 @@ class PersistedShaderMask extends PersistedContainerSurface
} else {
rootElement!.style.filter = 'url(#_fmf${_maskFilterIdCounter}';
}
rootElement!.append(_shaderElement!);
domRenderer.addResource(_shaderElement!);
}
}

View File

@ -568,7 +568,7 @@ abstract class PersistedSurface implements ui.EngineLayer {
///
/// This method recursively walks the surface tree calling `preroll` on all
/// descendants.
void preroll() {
void preroll(PrerollSurfaceContext prerollContext) {
recomputeTransformAndClip();
}
@ -651,11 +651,11 @@ abstract class PersistedContainerSurface extends PersistedSurface {
}
@override
void preroll() {
super.preroll();
void preroll(PrerollSurfaceContext prerollContext) {
super.preroll(prerollContext);
final int length = _children.length;
for (int i = 0; i < length; i += 1) {
_children[i].preroll();
_children[i].preroll(prerollContext);
}
}
@ -1227,3 +1227,13 @@ class _PersistedSurfaceMatch {
}
}
}
/// Data used during preroll to pass rendering hints efficiently to children
/// by optimizing (prevent parent lookups) and in cases like svg filters
/// drive the decision on whether canvas elements can be used to render.
class PrerollSurfaceContext {
/// Number of active color filters in parent surfaces.
int activeColorFilterCount = 0;
/// Number of active shader masks in parent surfaces.
int activeShaderMaskCount = 0;
}

View File

@ -158,6 +158,17 @@ void testMain() {
attachShadow = oldAttachShadow; // Restore ShadowDOM
});
test('should add/remove global resource', () {
final DomRenderer renderer = DomRenderer();
final html.DivElement resource = html.DivElement();
renderer.addResource(resource);
final html.Element? resourceRoot = resource.parent;
expect(resourceRoot, isNotNull);
expect(resourceRoot!.childNodes.length, 1);
renderer.removeResource(resource);
expect(resourceRoot.childNodes.length, 0);
});
}
@JS('Element.prototype.attachShadow')

View File

@ -143,7 +143,7 @@ void testMain() {
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 0);
scene1.preroll();
scene1.preroll(PrerollSurfaceContext());
scene1.build();
commitScene(scene1);
expect(picture.retainCount, 0);
@ -162,7 +162,7 @@ void testMain() {
opacity.state = PersistedSurfaceState.pendingRetention;
clip2.appendChild(opacity);
scene2.preroll();
scene2.preroll(PrerollSurfaceContext());
scene2.update(scene1);
commitScene(scene2);
expect(picture.retainCount, 1);
@ -181,7 +181,7 @@ void testMain() {
opacity.state = PersistedSurfaceState.pendingRetention;
clip3.appendChild(opacity);
scene3.preroll();
scene3.preroll(PrerollSurfaceContext());
scene3.update(scene2);
commitScene(scene3);
expect(picture.retainCount, 2);

View File

@ -68,6 +68,24 @@ void testMain() async {
maxDiffRatePercent: 12.0);
});
/// Regression test for https://github.com/flutter/flutter/issues/85733
test('Should apply mode color filter to circles', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture backgroundPicture = _drawBackground();
builder.addPicture(Offset.zero, backgroundPicture);
builder.pushColorFilter(
ColorFilter.mode(
Color(0xFFFF0000),
BlendMode.srcIn,
));
final Picture circles1 = _drawTestPictureWithCircles(30, 30);
builder.addPicture(Offset.zero, circles1);
builder.pop();
html.document.body!.append(builder.build().webOnlyRootElement!);
await matchGoldenFile('color_filter_mode.png', region: region,
maxDiffRatePercent: 12.0);
});
/// Regression test for https://github.com/flutter/flutter/issues/59451.
///
/// Picture with overlay blend inside a physical shape. Should show image

View File

@ -33,7 +33,8 @@ void testMain() async {
setUp(() async {
debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
for (html.Node scene in html.document.querySelectorAll('flt-scene')) {
for (html.Node scene in
domRenderer.sceneHostElement!.querySelectorAll('flt-scene')) {
scene.remove();
}
initWebGl();
@ -147,5 +148,5 @@ void _renderScene(BlendMode blendMode) {
builder.addPicture(Offset.zero, circles2);
builder.pop();
html.document.body!.append(builder.build().webOnlyRootElement!);
domRenderer.sceneHostElement!.append(builder.build().webOnlyRootElement!);
}