diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index b98df71628029..7a0d7439cb8ff 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -359,7 +359,7 @@ abstract class Image { String toString() => '[$width\u00D7$height]'; } -abstract class ColorFilter { +class ColorFilter implements ImageFilter { const factory ColorFilter.mode(Color color, BlendMode blendMode) = engine.EngineColorFilter.mode; const factory ColorFilter.matrix(List matrix) = engine.EngineColorFilter.matrix; const factory ColorFilter.linearToSrgbGamma() = engine.EngineColorFilter.linearToSrgbGamma; diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index b499f95abe3a6..63661de2a0455 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -7,7 +7,9 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; +import '../color_filter.dart'; import 'canvaskit_api.dart'; +import 'color_filter.dart'; import 'image.dart'; import 'image_filter.dart'; import 'painting.dart'; @@ -290,8 +292,12 @@ class CkCanvas { void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter, [CkPaint? paint]) { - final CkManagedSkImageFilterConvertible convertible = - filter as CkManagedSkImageFilterConvertible; + final CkManagedSkImageFilterConvertible convertible; + if (filter is ui.ColorFilter) { + convertible = createCkColorFilter(filter as EngineColorFilter)!; + } else { + convertible = filter as CkManagedSkImageFilterConvertible; + } return skCanvas.saveLayer( paint?.skiaObject, toSkRect(bounds), @@ -1163,8 +1169,12 @@ class CkSaveLayerWithFilterCommand extends CkPaintCommand { @override void apply(SkCanvas canvas) { - final CkManagedSkImageFilterConvertible convertible = - filter as CkManagedSkImageFilterConvertible; + final CkManagedSkImageFilterConvertible convertible; + if (filter is ui.ColorFilter) { + convertible = createCkColorFilter(filter as EngineColorFilter)!; + } else { + convertible = filter as CkManagedSkImageFilterConvertible; + } return canvas.saveLayer( paint?.skiaObject, toSkRect(bounds), 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 23d32191f3fed..bc3422cb05ed1 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart @@ -56,7 +56,7 @@ class ManagedSkColorFilter extends ManagedSkiaObject { /// Additionally, this class provides the interface for converting itself to a /// [ManagedSkiaObject] that manages a skia image filter. abstract class CkColorFilter - implements CkManagedSkImageFilterConvertible, EngineColorFilter { + implements CkManagedSkImageFilterConvertible { const CkColorFilter(); /// Called by [ManagedSkiaObject.createDefault] and @@ -227,3 +227,30 @@ class CkComposeColorFilter extends CkColorFilter { @override String toString() => 'ColorFilter.compose($outer, $inner)'; } + +/// Convert the current [ColorFilter] to a CkColorFilter. +/// +/// This workaround allows ColorFilter to be const constructbile and +/// efficiently comparable, so that widgets can check for ColorFilter equality to +/// avoid repainting. +CkColorFilter? createCkColorFilter(EngineColorFilter colorFilter) { + switch (colorFilter.type) { + case ColorFilterType.mode: + if (colorFilter.color == null || colorFilter.blendMode == null) { + return null; + } + return CkBlendModeColorFilter(colorFilter.color!, colorFilter.blendMode!); + case ColorFilterType.matrix: + if (colorFilter.matrix == null) { + return null; + } + assert(colorFilter.matrix!.length == 20, 'Color Matrix must have 20 entries.'); + return CkMatrixColorFilter(colorFilter.matrix!); + case ColorFilterType.linearToSrgbGamma: + return const CkLinearToSrgbGammaColorFilter(); + case ColorFilterType.srgbToLinearGamma: + return const CkSrgbToLinearGammaColorFilter(); + default: + throw StateError('Unknown mode $colorFilter.type for ColorFilter.'); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/painting.dart b/lib/web_ui/lib/src/engine/canvaskit/painting.dart index 419037574b9ba..b9a7296207c7e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/painting.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -7,6 +7,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; +import '../color_filter.dart'; import 'canvaskit_api.dart'; import 'color_filter.dart'; import 'image_filter.dart'; @@ -131,7 +132,8 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { _effectiveColorFilter = _invertColorFilter; } else { _effectiveColorFilter = ManagedSkColorFilter( - CkComposeColorFilter(_invertColorFilter, _effectiveColorFilter!)); + CkComposeColorFilter(_invertColorFilter, _effectiveColorFilter!) + ); } } skiaObject.setColorFilter(_effectiveColorFilter?.skiaObject); @@ -201,20 +203,23 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { } ui.FilterQuality _filterQuality = ui.FilterQuality.none; + EngineColorFilter? _engineColorFilter; @override - ui.ColorFilter? get colorFilter => _effectiveColorFilter?.colorFilter; + ui.ColorFilter? get colorFilter => _engineColorFilter; + @override set colorFilter(ui.ColorFilter? value) { - if (colorFilter == value) { + if (_engineColorFilter == value) { return; } - + _engineColorFilter = value as EngineColorFilter?; _originalColorFilter = null; if (value == null) { _effectiveColorFilter = null; } else { - _effectiveColorFilter = ManagedSkColorFilter(value as CkColorFilter); + final CkColorFilter ckColorFilter = createCkColorFilter(value)!; + _effectiveColorFilter = ManagedSkColorFilter(ckColorFilter); } if (invertColors) { @@ -223,7 +228,8 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { _effectiveColorFilter = _invertColorFilter; } else { _effectiveColorFilter = ManagedSkColorFilter( - CkComposeColorFilter(_invertColorFilter, _effectiveColorFilter!)); + CkComposeColorFilter(_invertColorFilter, _effectiveColorFilter!) + ); } } @@ -255,8 +261,12 @@ class CkPaint extends ManagedSkiaObject implements ui.Paint { if (_imageFilter == value) { return; } - - _imageFilter = value as CkManagedSkImageFilterConvertible?; + if (value is ui.ColorFilter) { + _imageFilter = createCkColorFilter(value as EngineColorFilter); + } + else { + _imageFilter = value as CkManagedSkImageFilterConvertible?; + } _managedImageFilter = _imageFilter?.imageFilter; skiaObject.setImageFilter(_managedImageFilter?.skiaObject); } @@ -305,8 +315,7 @@ final Float32List _invertColorMatrix = Float32List.fromList(const [ 1.0, 1.0, 1.0, 1.0, 0 ]); -final ManagedSkColorFilter _invertColorFilter = - ManagedSkColorFilter(CkMatrixColorFilter(_invertColorMatrix)); +final ManagedSkColorFilter _invertColorFilter = ManagedSkColorFilter(CkMatrixColorFilter(_invertColorMatrix)); class UniformData { const UniformData({ diff --git a/lib/web_ui/lib/src/engine/color_filter.dart b/lib/web_ui/lib/src/engine/color_filter.dart index 10b9202b00a2b..0fc408ea3e519 100644 --- a/lib/web_ui/lib/src/engine/color_filter.dart +++ b/lib/web_ui/lib/src/engine/color_filter.dart @@ -4,7 +4,12 @@ import 'package:ui/ui.dart' as ui; -import 'canvaskit/color_filter.dart'; +enum ColorFilterType { + mode, + matrix, + linearToSrgbGamma, + srgbToLinearGamma, +} /// A description of a color filter to apply when drawing a shape or compositing /// a layer with a particular [Paint]. A color filter is a function that takes @@ -22,8 +27,9 @@ 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 factory EngineColorFilter.mode(ui.Color color, ui.BlendMode blendMode) = - CkBlendModeColorFilter; + const EngineColorFilter.mode(ui.Color this.color, ui.BlendMode this.blendMode) + : matrix = null, + type = ColorFilterType.mode; /// Construct a color filter that transforms a color by a 5x5 matrix, where /// the fifth row is implicitly added in an identity configuration. @@ -85,16 +91,29 @@ class EngineColorFilter implements ui.ColorFilter { /// 0, 0, 0, 1, 0, /// ]); /// ``` - const factory EngineColorFilter.matrix(List matrix) = - CkMatrixColorFilter; + const EngineColorFilter.matrix(List this.matrix) + : color = null, + blendMode = null, + type = ColorFilterType.matrix; /// Construct a color filter that applies the sRGB gamma curve to the RGB /// channels. - const factory EngineColorFilter.linearToSrgbGamma() = - CkLinearToSrgbGammaColorFilter; + const EngineColorFilter.linearToSrgbGamma() + : color = null, + blendMode = null, + matrix = null, + type = ColorFilterType.linearToSrgbGamma; /// Creates a color filter that applies the inverse of the sRGB gamma curve /// to the RGB channels. - const factory EngineColorFilter.srgbToLinearGamma() = - CkSrgbToLinearGammaColorFilter; + const EngineColorFilter.srgbToLinearGamma() + : color = null, + blendMode = null, + matrix = null, + type = ColorFilterType.srgbToLinearGamma; + + final ui.Color? color; + final ui.BlendMode? blendMode; + final List? matrix; + final ColorFilterType type; } 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 ae669b11356c4..e2ab21e19731c 100644 --- a/lib/web_ui/lib/src/engine/html/backdrop_filter.dart +++ b/lib/web_ui/lib/src/engine/html/backdrop_filter.dart @@ -5,7 +5,9 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; +import '../color_filter.dart'; import '../dom.dart'; +import '../embedder.dart'; import '../util.dart'; import '../vector_math.dart'; import 'shaders/shader.dart'; @@ -17,7 +19,7 @@ class PersistedBackdropFilter extends PersistedContainerSurface implements ui.BackdropFilterEngineLayer { PersistedBackdropFilter(PersistedBackdropFilter? super.oldLayer, this.filter); - final EngineImageFilter filter; + final ui.ImageFilter filter; /// The dedicated child container element that's separate from the /// [rootElement] is used to host child in front of [filterElement] that @@ -26,6 +28,7 @@ class PersistedBackdropFilter extends PersistedContainerSurface DomElement? get childContainer => _childContainer; DomElement? _childContainer; DomElement? _filterElement; + DomElement? _svgFilter; ui.Rect? _activeClipBounds; // Cached inverted transform for [transform]. late Matrix4 _invertedTransform; @@ -37,6 +40,7 @@ class PersistedBackdropFilter extends PersistedContainerSurface super.adoptElements(oldSurface); _childContainer = oldSurface._childContainer; _filterElement = oldSurface._filterElement; + _svgFilter = oldSurface._svgFilter; oldSurface._childContainer = null; } @@ -62,12 +66,22 @@ class PersistedBackdropFilter extends PersistedContainerSurface // 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. + flutterViewEmbedder.removeResource(_svgFilter); + _svgFilter = null; _childContainer = null; _filterElement = null; } @override void apply() { + EngineImageFilter backendFilter; + if (filter is ui.ColorFilter) { + backendFilter = createHtmlColorFilter(filter as EngineColorFilter)!; + } else { + backendFilter = filter as EngineImageFilter; + } + flutterViewEmbedder.removeResource(_svgFilter); + _svgFilter = null; if (_previousTransform != transform) { _invertedTransform = Matrix4.inverted(transform!); _previousTransform = transform; @@ -115,14 +129,24 @@ class PersistedBackdropFilter extends PersistedContainerSurface ..backgroundColor = '#000' ..opacity = '0.2'; } else { + if (backendFilter is ModeHtmlColorFilter) { + _svgFilter = backendFilter.makeSvgFilter(_filterElement); + /// Some blendModes do not make an svgFilter. See [EngineHtmlColorFilter.makeSvgFilter()] + if (_svgFilter == null) { + return; + } + } else if (backendFilter is MatrixHtmlColorFilter) { + _svgFilter = backendFilter.makeSvgFilter(_filterElement); + } + // CSS uses pixel radius for blur. Flutter & SVG use sigma parameters. For // Gaussian blur with standard deviation (normal distribution), // the blur will fall within 2 * sigma pixels. if (browserEngine == BrowserEngine.webkit) { setElementStyle(_filterElement!, '-webkit-backdrop-filter', - filter.filterAttribute); + backendFilter.filterAttribute); } - setElementStyle(_filterElement!, 'backdrop-filter', filter.filterAttribute); + setElementStyle(_filterElement!, 'backdrop-filter', backendFilter.filterAttribute); } } diff --git a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index 844c8290ac7d0..5468b0bb066ea 100644 --- a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -9,8 +9,6 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../canvas_pool.dart'; -import '../canvaskit/color_filter.dart'; -import '../color_filter.dart'; import '../dom.dart'; import '../engine_canvas.dart'; import '../frame_reference.dart'; @@ -28,6 +26,7 @@ import 'path/path.dart'; import 'recording_canvas.dart'; import 'render_vertices.dart'; import 'shaders/image_shader.dart'; +import 'shaders/shader.dart'; /// A raw HTML canvas that is directly written to. class BitmapCanvas extends EngineCanvas { @@ -647,13 +646,12 @@ 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 EngineHtmlColorFilter? colorFilter = createHtmlColorFilter(paint.colorFilter); DomHTMLElement imgElement; - if (colorFilter is CkBlendModeColorFilter) { + if (colorFilter is ModeHtmlColorFilter) { imgElement = _createImageElementWithBlend( image, colorFilter.color, colorFilter.blendMode, paint); - } else if (colorFilter is CkMatrixColorFilter) { + } else if (colorFilter is MatrixHtmlColorFilter) { imgElement = _createImageElementWithSvgColorMatrixFilter( image, colorFilter.matrix, paint); } else { 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 3436f9837f9ae..4961d7c8dec6d 100644 --- a/lib/web_ui/lib/src/engine/html/color_filter.dart +++ b/lib/web_ui/lib/src/engine/html/color_filter.dart @@ -4,15 +4,15 @@ import 'package:ui/ui.dart' as ui; +import '../../engine/color_filter.dart'; import '../browser_detection.dart'; -import '../canvaskit/color_filter.dart'; -import '../color_filter.dart'; import '../dom.dart'; import '../embedder.dart'; import '../svg.dart'; import '../util.dart'; import 'bitmap_canvas.dart'; import 'path_to_svg_clip.dart'; +import 'shaders/shader.dart'; import 'surface.dart'; /// A surface that applies an [ColorFilter] to its children. @@ -73,84 +73,35 @@ class PersistedColorFilter extends PersistedContainerSurface void apply() { flutterViewEmbedder.removeResource(_filterElement); _filterElement = null; - final EngineColorFilter? engineValue = filter as EngineColorFilter?; + final EngineHtmlColorFilter? engineValue = createHtmlColorFilter(filter as EngineColorFilter); if (engineValue == null) { rootElement!.style.backgroundColor = ''; childContainer?.style.visibility = 'visible'; return; } - if (engineValue is CkBlendModeColorFilter) { + + if (engineValue is ModeHtmlColorFilter) { _applyBlendModeFilter(engineValue); - } else if (engineValue is CkMatrixColorFilter) { + } else if (engineValue is MatrixHtmlColorFilter) { _applyMatrixColorFilter(engineValue); } else { childContainer?.style.visibility = 'visible'; } } - void _applyBlendModeFilter(CkBlendModeColorFilter colorFilter) { - final ui.Color filterColor = colorFilter.color; - ui.BlendMode colorFilterBlendMode = colorFilter.blendMode; - final DomCSSStyleDeclaration style = childContainer!.style; - switch (colorFilterBlendMode) { - case ui.BlendMode.clear: - case ui.BlendMode.dstOut: - case ui.BlendMode.srcOut: - 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; - } + void _applyBlendModeFilter(ModeHtmlColorFilter colorFilter) { + _filterElement = colorFilter.makeSvgFilter(childContainer); - // Use SVG filter for blend mode. - final SvgFilter svgFilter = svgFilterFromBlendMode(filterColor, colorFilterBlendMode); - _filterElement = svgFilter.element; - flutterViewEmbedder.addResource(_filterElement!); - style.filter = 'url(#${svgFilter.id})'; - if (colorFilterBlendMode == ui.BlendMode.saturation || - colorFilterBlendMode == ui.BlendMode.multiply || - colorFilterBlendMode == ui.BlendMode.modulate) { - style.backgroundColor = colorToCssString(filterColor)!; + /// Some blendModes do not make an svgFilter. See [EngineHtmlColorFilter.makeSvgFilter()] + if (_filterElement == null) { + return; } + childContainer!.style.filter = colorFilter.filterAttribute; } - void _applyMatrixColorFilter(CkMatrixColorFilter colorFilter) { - final SvgFilter svgFilter = svgFilterFromColorMatrix(colorFilter.matrix); - _filterElement = svgFilter.element; - flutterViewEmbedder.addResource(_filterElement!); - childContainer!.style.filter = 'url(#${svgFilter.id})'; + void _applyMatrixColorFilter(MatrixHtmlColorFilter colorFilter) { + _filterElement = colorFilter.makeSvgFilter(childContainer); + childContainer!.style.filter = colorFilter.filterAttribute; } @override 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 736e2fa1543df..7fe37f641be43 100644 --- a/lib/web_ui/lib/src/engine/html/image_filter.dart +++ b/lib/web_ui/lib/src/engine/html/image_filter.dart @@ -4,7 +4,9 @@ import 'package:ui/ui.dart' as ui; +import '../color_filter.dart'; import '../dom.dart'; +import '../embedder.dart'; import 'shaders/shader.dart'; import 'surface.dart'; @@ -15,6 +17,21 @@ class PersistedImageFilter extends PersistedContainerSurface final ui.ImageFilter filter; + DomElement? _svgFilter; + + @override + void adoptElements(PersistedImageFilter oldSurface) { + super.adoptElements(oldSurface); + _svgFilter = oldSurface._svgFilter; + } + + @override + void discard() { + super.discard(); + flutterViewEmbedder.removeResource(_svgFilter); + _svgFilter = null; + } + @override DomElement createElement() { return defaultCreateElement('flt-image-filter'); @@ -22,8 +39,26 @@ class PersistedImageFilter extends PersistedContainerSurface @override void apply() { - rootElement!.style.filter = (filter as EngineImageFilter).filterAttribute; - rootElement!.style.transform = (filter as EngineImageFilter).transformAttribute; + EngineImageFilter backendFilter; + if (filter is ui.ColorFilter) { + backendFilter = createHtmlColorFilter(filter as EngineColorFilter)!; + } else { + backendFilter = filter as EngineImageFilter; + } + flutterViewEmbedder.removeResource(_svgFilter); + _svgFilter = null; + if (backendFilter is ModeHtmlColorFilter) { + _svgFilter = backendFilter.makeSvgFilter(rootElement); + /// Some blendModes do not make an svgFilter. See [EngineHtmlColorFilter.makeSvgFilter()] + if (_svgFilter == null) { + return; + } + } else if (backendFilter is MatrixHtmlColorFilter) { + _svgFilter = backendFilter.makeSvgFilter(rootElement); + } + + rootElement!.style.filter = backendFilter.filterAttribute; + rootElement!.style.transform = backendFilter.transformAttribute; } @override diff --git a/lib/web_ui/lib/src/engine/html/painting.dart b/lib/web_ui/lib/src/engine/html/painting.dart index 27ff910ec4769..dc3ccdc4f63a7 100644 --- a/lib/web_ui/lib/src/engine/html/painting.dart +++ b/lib/web_ui/lib/src/engine/html/painting.dart @@ -4,6 +4,7 @@ import 'package:ui/ui.dart' as ui; +import '../color_filter.dart'; import '../util.dart'; /// Implementation of [ui.Paint] used by the HTML rendering backend. @@ -149,7 +150,7 @@ class SurfacePaint implements ui.Paint { _paintData = _paintData.clone(); _frozen = false; } - _paintData.colorFilter = value; + _paintData.colorFilter = value as EngineColorFilter?; } // TODO(ferhat): see https://github.com/flutter/flutter/issues/33605 @@ -228,7 +229,7 @@ class SurfacePaintData { ui.Shader? shader; ui.MaskFilter? maskFilter; ui.FilterQuality? filterQuality; - ui.ColorFilter? colorFilter; + EngineColorFilter? colorFilter; // Internal for recording canvas use. SurfacePaintData clone() { 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 22ad25f5a9f89..1e552b15b5fc5 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -25,7 +25,6 @@ import 'picture.dart'; import 'platform_view.dart'; import 'scene.dart'; import 'shader_mask.dart'; -import 'shaders/shader.dart'; import 'surface.dart'; import 'transform.dart'; @@ -247,7 +246,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.BackdropFilterEngineLayer? oldLayer, }) { return _pushSurface(PersistedBackdropFilter( - oldLayer as PersistedBackdropFilter?, filter as EngineImageFilter)); + oldLayer as PersistedBackdropFilter?, filter)); } /// Pushes a shader mask operation onto the operation stack. 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 be3ebaa92e361..e438165383486 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -8,11 +8,14 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import '../../browser_detection.dart'; +import '../../color_filter.dart'; import '../../dom.dart'; +import '../../embedder.dart'; import '../../safe_browser_api.dart'; import '../../util.dart'; import '../../validators.dart'; import '../../vector_math.dart'; +import '../color_filter.dart'; import '../path/path_utils.dart'; import '../render_vertices.dart'; import 'normalized_gradient.dart'; @@ -771,3 +774,130 @@ class _MatrixEngineImageFilter extends EngineImageFilter { return 'ImageFilter.matrix($webMatrix, $filterQuality)'; } } + +/// The backend implementation of [ui.ColorFilter] +/// +/// Currently only 'mode' and 'matrix' are supported. +abstract class EngineHtmlColorFilter implements EngineImageFilter { + EngineHtmlColorFilter(); + + String? filterId; + + @override + String get filterAttribute => (filterId != null) ? 'url(#$filterId)' : ''; + + @override + String get transformAttribute => ''; + + /// Make an [SvgFilter] and add it as a globabl resource using [flutterViewEmbedder] + /// The [DomElement] from the made [SvgFilter] is returned so it can be managed + /// by the surface calling it. + DomElement? makeSvgFilter(DomElement? filterElement); +} + +class ModeHtmlColorFilter extends EngineHtmlColorFilter { + ModeHtmlColorFilter(this.color, this.blendMode); + + final ui.Color color; + ui.BlendMode blendMode; + + @override + DomElement? makeSvgFilter(DomElement? filterElement) { + switch (blendMode) { + case ui.BlendMode.clear: + case ui.BlendMode.dstOut: + case ui.BlendMode.srcOut: + filterElement!.style.visibility = 'hidden'; + return null; + case ui.BlendMode.dst: + case ui.BlendMode.dstIn: + // Noop. + return null; + 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. + blendMode = 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; + } + + final SvgFilter svgFilter = svgFilterFromBlendMode(color, blendMode); + flutterViewEmbedder.addResource(svgFilter.element); + filterId = svgFilter.id; + + if (blendMode == ui.BlendMode.saturation || + blendMode == ui.BlendMode.multiply || + blendMode == ui.BlendMode.modulate) { + filterElement!.style.backgroundColor = colorToCssString(color)!; + } + return svgFilter.element; + } +} + +class MatrixHtmlColorFilter extends EngineHtmlColorFilter { + MatrixHtmlColorFilter(this.matrix); + + final List matrix; + + @override + DomElement? makeSvgFilter(DomNode? filterElement) { + final SvgFilter svgFilter = svgFilterFromColorMatrix(matrix); + flutterViewEmbedder.addResource(svgFilter.element); + filterId = svgFilter.id; + return svgFilter.element; + } +} + +/// Convert the current [ColorFilter] to an EngineHtmlColorFilter +/// +/// This workaround allows ColorFilter to be const constructible and +/// efficiently comparable, so that widgets can check for COlorFIlter equality to +/// avoid repainting. +EngineHtmlColorFilter? createHtmlColorFilter(EngineColorFilter? colorFilter) { + if (colorFilter == null) { + return null; + } + switch (colorFilter.type) { + case ColorFilterType.mode: + if (colorFilter.color == null || colorFilter.blendMode == null) { + return null; + } + return ModeHtmlColorFilter(colorFilter.color!, colorFilter.blendMode!); + case ColorFilterType.matrix: + if (colorFilter.matrix == null) { + return null; + } + assert(colorFilter.matrix!.length == 20, 'Color Matrix must have 20 entries.'); + return MatrixHtmlColorFilter(colorFilter.matrix!); + case ColorFilterType.linearToSrgbGamma: + throw UnimplementedError('ColorFilter.linearToSrgbGamma not implemented for HTML renderer'); + case ColorFilterType.srgbToLinearGamma: + throw UnimplementedError('ColorFilter.srgbToLinearGamma not implemented for HTML renderer.'); + default: + throw StateError('Unknown mode $colorFilter.type for ColorFilter.'); + } +} diff --git a/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart b/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart index ca1d16f6c6826..00ccd76fbffac 100644 --- a/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart +++ b/lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart @@ -53,6 +53,30 @@ void testMain() { await matchGoldenFile('canvaskit_backdropfilter_blur_edges.png', region: region); }); + test('ImageFilter with ColorFilter as child', () async { + final LayerSceneBuilder builder = LayerSceneBuilder(); + const ui.Rect region = ui.Rect.fromLTRB(0, 0, 500, 250); + + builder.pushOffset(0, 0); + + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(region); + final ui.ColorFilter colorFilter = ui.ColorFilter.mode( + const ui.Color(0XFF00FF00).withOpacity(0.55), + ui.BlendMode.darken + ); + + // using a colorFilter as an imageFilter for backDrop filter + builder.pushBackdropFilter(colorFilter); + canvas.drawCircle( + const ui.Offset(75, 125), + 50, + CkPaint()..color = const ui.Color.fromARGB(255, 255, 0, 0), + ); + final CkPicture redCircle1 = recorder.endRecording(); + builder.addPicture(ui.Offset.zero, redCircle1); + await matchSceneGolden('canvaskit_red_circle_green_backdrop_colorFilter.png', builder.build(), region: region); + }); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 }, skip: isSafari || isFirefox); } diff --git a/lib/web_ui/test/canvaskit/filter_test.dart b/lib/web_ui/test/canvaskit/filter_test.dart index 811a292889e51..6ab7dac78a95e 100644 --- a/lib/web_ui/test/canvaskit/filter_test.dart +++ b/lib/web_ui/test/canvaskit/filter_test.dart @@ -17,23 +17,23 @@ void main() { void testMain() { List createColorFilters() { return [ - const EngineColorFilter.mode(ui.Color(0x12345678), ui.BlendMode.srcOver) as CkColorFilter, - const EngineColorFilter.mode(ui.Color(0x12345678), ui.BlendMode.dstOver) as CkColorFilter, - const EngineColorFilter.mode(ui.Color(0x87654321), ui.BlendMode.dstOver) as CkColorFilter, - const EngineColorFilter.matrix([ + createCkColorFilter(const EngineColorFilter.mode(ui.Color(0x12345678), ui.BlendMode.srcOver))!, + createCkColorFilter(const EngineColorFilter.mode(ui.Color(0x12345678), ui.BlendMode.dstOver))!, + createCkColorFilter(const EngineColorFilter.mode(ui.Color(0x87654321), ui.BlendMode.dstOver))!, + createCkColorFilter(const 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([ + ]))!, + createCkColorFilter(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, - const EngineColorFilter.linearToSrgbGamma() as CkColorFilter, - const EngineColorFilter.srgbToLinearGamma() as CkColorFilter, + ])))!, + createCkColorFilter(const EngineColorFilter.linearToSrgbGamma())!, + createCkColorFilter(const EngineColorFilter.srgbToLinearGamma())!, ]; } @@ -125,6 +125,35 @@ void testMain() { await matchSceneGolden('canvaskit_zero_sigma_blur.png', builder.build(), region: region); }); + + test('using a colorFilter', () async { + final CkColorFilter colorFilter = createCkColorFilter( + const EngineColorFilter.mode( + ui.Color.fromARGB(255, 0, 255, 0), + ui.BlendMode.srcIn + ))!; + + const ui.Rect region = ui.Rect.fromLTRB(0, 0, 500, 250); + + final LayerSceneBuilder builder = LayerSceneBuilder(); + builder.pushOffset(0,0); + + builder.pushImageFilter(colorFilter); + + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(region); + + canvas.drawCircle( + const ui.Offset(75, 125), + 50, + CkPaint()..color = const ui.Color.fromARGB(255, 255, 0, 0), + ); + final CkPicture redCircle1 = recorder.endRecording(); + builder.addPicture(ui.Offset.zero, redCircle1); + // The drawn red circle should actually be green with the colorFilter. + + await matchSceneGolden('canvaskit_imageFilter_using_colorFilter.png', builder.build(), region: region); + }); }); group('MaskFilter', () { diff --git a/lib/web_ui/test/html/compositing/backdrop_filter_golden_test.dart b/lib/web_ui/test/html/compositing/backdrop_filter_golden_test.dart index 402441b9bb1f6..41743319b73ef 100644 --- a/lib/web_ui/test/html/compositing/backdrop_filter_golden_test.dart +++ b/lib/web_ui/test/html/compositing/backdrop_filter_golden_test.dart @@ -140,6 +140,34 @@ Future testMain() async { await matchGoldenFile('backdrop_filter_no_child_rendering.png', region: region); }); + test('colorFilter as imageFilter', () async { + const Rect region = Rect.fromLTWH(0, 0, 190, 130); + + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture backgroundPicture = _drawBackground(region); + builder.addPicture(Offset.zero, backgroundPicture); + + builder.pushClipRect( + const Rect.fromLTRB(10, 10, 180, 120), + ); + final Picture circles1 = _drawTestPictureWithCircles(region, 30, 30); + + // current background color is light green, apply a light yellow colorFilter + const ColorFilter colorFilter = ColorFilter.mode( + Color(0xFFFFFFB1), + BlendMode.modulate + ); + builder.pushBackdropFilter(colorFilter); + builder.addPicture(Offset.zero, circles1); + builder.pop(); + + domDocument.body!.append(builder + .build() + .webOnlyRootElement!); + + await matchGoldenFile('backdrop_filter_colorFilter_as_imageFilter.png', + region: region); + }); } Picture _drawTestPictureWithCircles(Rect region, double offsetX, double offsetY) { diff --git a/lib/web_ui/test/html/compositing/compositing_golden_test.dart b/lib/web_ui/test/html/compositing/compositing_golden_test.dart index 5d237cd09b6f5..fd4e3d4791ebb 100644 --- a/lib/web_ui/test/html/compositing/compositing_golden_test.dart +++ b/lib/web_ui/test/html/compositing/compositing_golden_test.dart @@ -391,6 +391,41 @@ Future testMain() async { await matchGoldenFile('compositing_image_filter_matrix.png', region: region); }); + test('pushImageFilter using mode ColorFilter', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + // Applying the colorFilter should turn all the circles red. + builder.pushImageFilter( + const ui.ColorFilter.mode( + ui.Color(0xFFFF0000), + ui.BlendMode.srcIn, + )); + _drawTestPicture(builder); + builder.pop(); + + domDocument.body!.append(builder.build().webOnlyRootElement!); + + await matchGoldenFile('compositing_image_filter_using_mode_color_filter.png', region: region); + }); + + test('pushImageFilter using matrix ColorFilter', () async { + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + // Apply a "greyscale" color filter. + final List colorMatrix = [ + 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, // + ]; + + builder.pushImageFilter(ui.ColorFilter.matrix(colorMatrix)); + _drawTestPicture(builder); + builder.pop(); + + domDocument.body!.append(builder.build().webOnlyRootElement!); + + await matchGoldenFile('compositing_image_filter_using_matrix_color_filter.png', region: region); + }); + group('Cull rect computation', () { _testCullRectComputation(); });