diff --git a/engine/src/flutter/lib/ui/compositing/scene_builder.cc b/engine/src/flutter/lib/ui/compositing/scene_builder.cc index 4c2659ffa1e..93651ade387 100644 --- a/engine/src/flutter/lib/ui/compositing/scene_builder.cc +++ b/engine/src/flutter/lib/ui/compositing/scene_builder.cc @@ -152,7 +152,8 @@ void SceneBuilder::pushImageFilter(Dart_Handle layer_handle, double dy, const fml::RefPtr& old_layer) { auto layer = std::make_shared( - image_filter->filter(), SkPoint::Make(SafeNarrow(dx), SafeNarrow(dy))); + image_filter->filter(DlTileMode::kDecal), + SkPoint::Make(SafeNarrow(dx), SafeNarrow(dy))); PushLayer(layer); EngineLayer::MakeRetained(layer_handle, layer); @@ -175,7 +176,7 @@ void SceneBuilder::pushBackdropFilter( } auto layer = std::make_shared( - filter->filter(), static_cast(blend_mode), + filter->filter(DlTileMode::kMirror), static_cast(blend_mode), converted_backdrop_id); PushLayer(layer); EngineLayer::MakeRetained(layer_handle, layer); diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart index 6fb63090ca5..cd73c6aaca0 100644 --- a/engine/src/flutter/lib/ui/painting.dart +++ b/engine/src/flutter/lib/ui/painting.dart @@ -4008,7 +4008,7 @@ abstract class ImageFilter { ImageFilter._(); // ignore: unused_element /// Creates an image filter that applies a Gaussian blur. - factory ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0, TileMode tileMode = TileMode.clamp }) { + factory ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0, TileMode? tileMode }) { return _GaussianBlurImageFilter(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); } @@ -4090,7 +4090,7 @@ class _GaussianBlurImageFilter implements ImageFilter { final double sigmaX; final double sigmaY; - final TileMode tileMode; + final TileMode? tileMode; // MakeBlurFilter late final _ImageFilter nativeFilter = _ImageFilter.blur(this); @@ -4103,6 +4103,7 @@ class _GaussianBlurImageFilter implements ImageFilter { case TileMode.mirror: return 'mirror'; case TileMode.repeated: return 'repeated'; case TileMode.decal: return 'decal'; + case null: return 'unspecified'; } } @@ -4228,7 +4229,7 @@ base class _ImageFilter extends NativeFieldWrapperClass1 { _ImageFilter.blur(_GaussianBlurImageFilter filter) : creator = filter { _constructor(); - _initBlur(filter.sigmaX, filter.sigmaY, filter.tileMode.index); + _initBlur(filter.sigmaX, filter.sigmaY, filter.tileMode?.index ?? -1); } /// Creates an image filter that dilates each input pixel's channel values diff --git a/engine/src/flutter/lib/ui/painting/canvas.cc b/engine/src/flutter/lib/ui/painting/canvas.cc index 1da304df658..79f902a3cd9 100644 --- a/engine/src/flutter/lib/ui/painting/canvas.cc +++ b/engine/src/flutter/lib/ui/painting/canvas.cc @@ -60,7 +60,8 @@ void Canvas::saveLayerWithoutBounds(Dart_Handle paint_objects, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - const DlPaint* save_paint = paint.paint(dl_paint, kSaveLayerWithPaintFlags); + const DlPaint* save_paint = + paint.paint(dl_paint, kSaveLayerWithPaintFlags, DlTileMode::kDecal); FML_DCHECK(save_paint); TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); builder()->SaveLayer(nullptr, save_paint); @@ -80,7 +81,8 @@ void Canvas::saveLayer(double left, SafeNarrow(right), SafeNarrow(bottom)); if (display_list_builder_) { DlPaint dl_paint; - const DlPaint* save_paint = paint.paint(dl_paint, kSaveLayerWithPaintFlags); + const DlPaint* save_paint = + paint.paint(dl_paint, kSaveLayerWithPaintFlags, DlTileMode::kDecal); FML_DCHECK(save_paint); TRACE_EVENT0("flutter", "ui.Canvas::saveLayer (Recorded)"); builder()->SaveLayer(&bounds, save_paint); @@ -229,7 +231,7 @@ void Canvas::drawLine(double x1, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawLineFlags); + paint.paint(dl_paint, kDrawLineFlags, DlTileMode::kDecal); builder()->DrawLine(SkPoint::Make(SafeNarrow(x1), SafeNarrow(y1)), SkPoint::Make(SafeNarrow(x2), SafeNarrow(y2)), dl_paint); @@ -242,7 +244,7 @@ void Canvas::drawPaint(Dart_Handle paint_objects, Dart_Handle paint_data) { FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawPaintFlags); + paint.paint(dl_paint, kDrawPaintFlags, DlTileMode::kClamp); std::shared_ptr filter = dl_paint.getImageFilter(); if (filter && !filter->asColorFilter()) { // drawPaint does an implicit saveLayer if an SkImageFilter is @@ -264,7 +266,7 @@ void Canvas::drawRect(double left, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawRectFlags); + paint.paint(dl_paint, kDrawRectFlags, DlTileMode::kDecal); builder()->DrawRect(SkRect::MakeLTRB(SafeNarrow(left), SafeNarrow(top), SafeNarrow(right), SafeNarrow(bottom)), dl_paint); @@ -279,7 +281,7 @@ void Canvas::drawRRect(const RRect& rrect, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawRRectFlags); + paint.paint(dl_paint, kDrawRRectFlags, DlTileMode::kDecal); builder()->DrawRRect(rrect.sk_rrect, dl_paint); } } @@ -293,7 +295,7 @@ void Canvas::drawDRRect(const RRect& outer, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawDRRectFlags); + paint.paint(dl_paint, kDrawDRRectFlags, DlTileMode::kDecal); builder()->DrawDRRect(outer.sk_rrect, inner.sk_rrect, dl_paint); } } @@ -309,7 +311,7 @@ void Canvas::drawOval(double left, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawOvalFlags); + paint.paint(dl_paint, kDrawOvalFlags, DlTileMode::kDecal); builder()->DrawOval(SkRect::MakeLTRB(SafeNarrow(left), SafeNarrow(top), SafeNarrow(right), SafeNarrow(bottom)), dl_paint); @@ -326,7 +328,7 @@ void Canvas::drawCircle(double x, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawCircleFlags); + paint.paint(dl_paint, kDrawCircleFlags, DlTileMode::kDecal); builder()->DrawCircle(SkPoint::Make(SafeNarrow(x), SafeNarrow(y)), SafeNarrow(radius), dl_paint); } @@ -346,9 +348,9 @@ void Canvas::drawArc(double left, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, useCenter // - ? kDrawArcWithCenterFlags - : kDrawArcNoCenterFlags); + paint.paint(dl_paint, + useCenter ? kDrawArcWithCenterFlags : kDrawArcNoCenterFlags, + DlTileMode::kDecal); builder()->DrawArc( SkRect::MakeLTRB(SafeNarrow(left), SafeNarrow(top), SafeNarrow(right), SafeNarrow(bottom)), @@ -371,7 +373,7 @@ void Canvas::drawPath(const CanvasPath* path, } if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawPathFlags); + paint.paint(dl_paint, kDrawPathFlags, DlTileMode::kDecal); builder()->DrawPath(path->path(), dl_paint); } } @@ -401,7 +403,8 @@ Dart_Handle Canvas::drawImage(const CanvasImage* image, auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex); if (display_list_builder_) { DlPaint dl_paint; - const DlPaint* opt_paint = paint.paint(dl_paint, kDrawImageWithPaintFlags); + const DlPaint* opt_paint = + paint.paint(dl_paint, kDrawImageWithPaintFlags, DlTileMode::kClamp); builder()->DrawImage(dl_image, SkPoint::Make(SafeNarrow(x), SafeNarrow(y)), sampling, opt_paint); } @@ -444,7 +447,7 @@ Dart_Handle Canvas::drawImageRect(const CanvasImage* image, if (display_list_builder_) { DlPaint dl_paint; const DlPaint* opt_paint = - paint.paint(dl_paint, kDrawImageRectWithPaintFlags); + paint.paint(dl_paint, kDrawImageRectWithPaintFlags, DlTileMode::kClamp); builder()->DrawImageRect(dl_image, src, dst, sampling, opt_paint, DlCanvas::SrcRectConstraint::kFast); } @@ -489,7 +492,7 @@ Dart_Handle Canvas::drawImageNine(const CanvasImage* image, if (display_list_builder_) { DlPaint dl_paint; const DlPaint* opt_paint = - paint.paint(dl_paint, kDrawImageNineWithPaintFlags); + paint.paint(dl_paint, kDrawImageNineWithPaintFlags, DlTileMode::kClamp); builder()->DrawImageNine(dl_image, icenter, dst, filter, opt_paint); } return Dart_Null(); @@ -524,13 +527,13 @@ void Canvas::drawPoints(Dart_Handle paint_objects, DlPaint dl_paint; switch (point_mode) { case DlCanvas::PointMode::kPoints: - paint.paint(dl_paint, kDrawPointsAsPointsFlags); + paint.paint(dl_paint, kDrawPointsAsPointsFlags, DlTileMode::kDecal); break; case DlCanvas::PointMode::kLines: - paint.paint(dl_paint, kDrawPointsAsLinesFlags); + paint.paint(dl_paint, kDrawPointsAsLinesFlags, DlTileMode::kDecal); break; case DlCanvas::PointMode::kPolygon: - paint.paint(dl_paint, kDrawPointsAsPolygonFlags); + paint.paint(dl_paint, kDrawPointsAsPolygonFlags, DlTileMode::kDecal); break; } builder()->DrawPoints(point_mode, @@ -554,7 +557,7 @@ void Canvas::drawVertices(const Vertices* vertices, FML_DCHECK(paint.isNotNull()); if (display_list_builder_) { DlPaint dl_paint; - paint.paint(dl_paint, kDrawVerticesFlags); + paint.paint(dl_paint, kDrawVerticesFlags, DlTileMode::kDecal); builder()->DrawVertices(vertices->vertices(), blend_mode, dl_paint); } } @@ -603,7 +606,8 @@ Dart_Handle Canvas::drawAtlas(Dart_Handle paint_objects, } DlPaint dl_paint; - const DlPaint* opt_paint = paint.paint(dl_paint, kDrawAtlasWithPaintFlags); + const DlPaint* opt_paint = + paint.paint(dl_paint, kDrawAtlasWithPaintFlags, DlTileMode::kClamp); builder()->DrawAtlas( dl_image, reinterpret_cast(transforms.data()), reinterpret_cast(rects.data()), dl_color.data(), diff --git a/engine/src/flutter/lib/ui/painting/image_filter.cc b/engine/src/flutter/lib/ui/painting/image_filter.cc index 74f89834fe7..5c276e1f42e 100644 --- a/engine/src/flutter/lib/ui/painting/image_filter.cc +++ b/engine/src/flutter/lib/ui/painting/image_filter.cc @@ -51,37 +51,69 @@ ImageFilter::ImageFilter() {} ImageFilter::~ImageFilter() {} +const std::shared_ptr ImageFilter::filter( + DlTileMode mode) const { + if (is_dynamic_tile_mode_) { + FML_DCHECK(filter_.get() != nullptr); + const DlBlurImageFilter* blur_filter = filter_->asBlur(); + FML_DCHECK(blur_filter != nullptr); + if (blur_filter->tile_mode() != mode) { + return DlBlurImageFilter::Make(blur_filter->sigma_x(), + blur_filter->sigma_y(), mode); + } + } + return filter_; +} + void ImageFilter::initBlur(double sigma_x, double sigma_y, - DlTileMode tile_mode) { + int tile_mode_index) { + DlTileMode tile_mode; + bool is_dynamic; + if (tile_mode_index < 0) { + is_dynamic = true; + tile_mode = DlTileMode::kClamp; + } else { + is_dynamic = false; + tile_mode = static_cast(tile_mode_index); + } filter_ = DlBlurImageFilter::Make(SafeNarrow(sigma_x), SafeNarrow(sigma_y), tile_mode); + // If it was a NOP filter, don't bother processing dynamic substitutions + // (They'd fail the FML_DCHECK anyway) + is_dynamic_tile_mode_ = is_dynamic && filter_; } void ImageFilter::initDilate(double radius_x, double radius_y) { + is_dynamic_tile_mode_ = false; filter_ = DlDilateImageFilter::Make(SafeNarrow(radius_x), SafeNarrow(radius_y)); } void ImageFilter::initErode(double radius_x, double radius_y) { + is_dynamic_tile_mode_ = false; filter_ = DlErodeImageFilter::Make(SafeNarrow(radius_x), SafeNarrow(radius_y)); } void ImageFilter::initMatrix(const tonic::Float64List& matrix4, int filterQualityIndex) { + is_dynamic_tile_mode_ = false; auto sampling = ImageFilter::SamplingFromIndex(filterQualityIndex); filter_ = DlMatrixImageFilter::Make(ToSkMatrix(matrix4), sampling); } void ImageFilter::initColorFilter(ColorFilter* colorFilter) { FML_DCHECK(colorFilter); + is_dynamic_tile_mode_ = false; filter_ = DlColorFilterImageFilter::Make(colorFilter->filter()); } void ImageFilter::initComposeFilter(ImageFilter* outer, ImageFilter* inner) { FML_DCHECK(outer && inner); - filter_ = DlComposeImageFilter::Make(outer->filter(), inner->filter()); + is_dynamic_tile_mode_ = false; + filter_ = DlComposeImageFilter::Make(outer->filter(DlTileMode::kClamp), + inner->filter(DlTileMode::kClamp)); } } // namespace flutter diff --git a/engine/src/flutter/lib/ui/painting/image_filter.h b/engine/src/flutter/lib/ui/painting/image_filter.h index 68d70b3f150..af6835de40b 100644 --- a/engine/src/flutter/lib/ui/painting/image_filter.h +++ b/engine/src/flutter/lib/ui/painting/image_filter.h @@ -28,14 +28,14 @@ class ImageFilter : public RefCountedDartWrappable { static DlImageSampling SamplingFromIndex(int filterQualityIndex); static DlFilterMode FilterModeFromIndex(int index); - void initBlur(double sigma_x, double sigma_y, DlTileMode tile_mode); + void initBlur(double sigma_x, double sigma_y, int tile_mode_index); void initDilate(double radius_x, double radius_y); void initErode(double radius_x, double radius_y); void initMatrix(const tonic::Float64List& matrix4, int filter_quality_index); void initColorFilter(ColorFilter* colorFilter); void initComposeFilter(ImageFilter* outer, ImageFilter* inner); - const std::shared_ptr filter() const { return filter_; } + const std::shared_ptr filter(DlTileMode mode) const; static void RegisterNatives(tonic::DartLibraryNatives* natives); @@ -43,6 +43,7 @@ class ImageFilter : public RefCountedDartWrappable { ImageFilter(); std::shared_ptr filter_; + bool is_dynamic_tile_mode_ = false; }; } // namespace flutter diff --git a/engine/src/flutter/lib/ui/painting/paint.cc b/engine/src/flutter/lib/ui/painting/paint.cc index 19a535520c1..ea0ceebe9c1 100644 --- a/engine/src/flutter/lib/ui/painting/paint.cc +++ b/engine/src/flutter/lib/ui/painting/paint.cc @@ -83,7 +83,8 @@ Paint::Paint(Dart_Handle paint_objects, Dart_Handle paint_data) : paint_objects_(paint_objects), paint_data_(paint_data) {} const DlPaint* Paint::paint(DlPaint& paint, - const DisplayListAttributeFlags& flags) const { + const DisplayListAttributeFlags& flags, + DlTileMode tile_mode) const { if (isNull()) { return nullptr; } @@ -148,7 +149,7 @@ const DlPaint* Paint::paint(DlPaint& paint, } else { ImageFilter* decoded = tonic::DartConverter::FromDart(image_filter); - paint.setImageFilter(decoded->filter()); + paint.setImageFilter(decoded->filter(tile_mode)); } } } @@ -208,7 +209,7 @@ const DlPaint* Paint::paint(DlPaint& paint, return &paint; } -void Paint::toDlPaint(DlPaint& paint) const { +void Paint::toDlPaint(DlPaint& paint, DlTileMode tile_mode) const { if (isNull()) { return; } @@ -252,7 +253,7 @@ void Paint::toDlPaint(DlPaint& paint) const { if (!Dart_IsNull(image_filter)) { ImageFilter* decoded = tonic::DartConverter::FromDart(image_filter); - paint.setImageFilter(decoded->filter()); + paint.setImageFilter(decoded->filter(tile_mode)); } } diff --git a/engine/src/flutter/lib/ui/painting/paint.h b/engine/src/flutter/lib/ui/painting/paint.h index e87313a489d..2fdf1fae8f8 100644 --- a/engine/src/flutter/lib/ui/painting/paint.h +++ b/engine/src/flutter/lib/ui/painting/paint.h @@ -18,9 +18,10 @@ class Paint { Paint(Dart_Handle paint_objects, Dart_Handle paint_data); const DlPaint* paint(DlPaint& paint, - const DisplayListAttributeFlags& flags) const; + const DisplayListAttributeFlags& flags, + DlTileMode tile_mode) const; - void toDlPaint(DlPaint& paint) const; + void toDlPaint(DlPaint& paint, DlTileMode tile_mode) const; bool isNull() const { return Dart_IsNull(paint_data_); } bool isNotNull() const { return !Dart_IsNull(paint_data_); } diff --git a/engine/src/flutter/lib/ui/painting/paint_unittests.cc b/engine/src/flutter/lib/ui/painting/paint_unittests.cc index 87447fa5481..2b051f00b0d 100644 --- a/engine/src/flutter/lib/ui/painting/paint_unittests.cc +++ b/engine/src/flutter/lib/ui/painting/paint_unittests.cc @@ -21,7 +21,7 @@ TEST_F(ShellTest, ConvertPaintToDlPaint) { Dart_GetField(dart_paint, tonic::ToDart("_objects")); Dart_Handle paint_data = Dart_GetField(dart_paint, tonic::ToDart("_data")); Paint ui_paint(paint_objects, paint_data); - ui_paint.toDlPaint(dl_paint); + ui_paint.toDlPaint(dl_paint, DlTileMode::kClamp); message_latch->Signal(); }; diff --git a/engine/src/flutter/lib/ui/text/paragraph_builder.cc b/engine/src/flutter/lib/ui/text/paragraph_builder.cc index bb3c7416bce..d5ea7cc2fc1 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_builder.cc +++ b/engine/src/flutter/lib/ui/text/paragraph_builder.cc @@ -458,7 +458,7 @@ void ParagraphBuilder::pushStyle(const tonic::Int32List& encoded, Paint background(background_objects, background_data); if (background.isNotNull()) { DlPaint dl_paint; - background.toDlPaint(dl_paint); + background.toDlPaint(dl_paint, DlTileMode::kDecal); style.background = dl_paint; } } @@ -467,7 +467,7 @@ void ParagraphBuilder::pushStyle(const tonic::Int32List& encoded, Paint foreground(foreground_objects, foreground_data); if (foreground.isNotNull()) { DlPaint dl_paint; - foreground.toDlPaint(dl_paint); + foreground.toDlPaint(dl_paint, DlTileMode::kDecal); style.foreground = dl_paint; } } diff --git a/engine/src/flutter/lib/web_ui/lib/painting.dart b/engine/src/flutter/lib/web_ui/lib/painting.dart index 7bdd7466333..82fa3d021f8 100644 --- a/engine/src/flutter/lib/web_ui/lib/painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/painting.dart @@ -591,7 +591,7 @@ class ImageFilter { factory ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0, - TileMode tileMode = TileMode.clamp + TileMode? tileMode }) => engine.renderer.createBlurImageFilter( sigmaX: sigmaX, sigmaY: sigmaY, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 03eaee4466c..dd7cb93f398 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -101,7 +101,7 @@ class CkCanvas { Uint32List? colors, ui.BlendMode blendMode, ) { - final skPaint = paint.toSkPaint(); + final skPaint = paint.toSkPaint(defaultBlurTileMode: ui.TileMode.clamp); skCanvas.drawAtlas( atlas.skImage, rects, @@ -143,7 +143,7 @@ class CkCanvas { void drawImage(CkImage image, ui.Offset offset, CkPaint paint) { final ui.FilterQuality filterQuality = paint.filterQuality; - final skPaint = paint.toSkPaint(); + final skPaint = paint.toSkPaint(defaultBlurTileMode: ui.TileMode.clamp); if (filterQuality == ui.FilterQuality.high) { skCanvas.drawImageCubic( image.skImage, @@ -168,7 +168,7 @@ class CkCanvas { void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) { final ui.FilterQuality filterQuality = paint.filterQuality; - final skPaint = paint.toSkPaint(); + final skPaint = paint.toSkPaint(defaultBlurTileMode: ui.TileMode.clamp); if (filterQuality == ui.FilterQuality.high) { skCanvas.drawImageRectCubic( image.skImage, @@ -193,7 +193,7 @@ class CkCanvas { void drawImageNine( CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) { - final skPaint = paint.toSkPaint(); + final skPaint = paint.toSkPaint(defaultBlurTileMode: ui.TileMode.clamp); skCanvas.drawImageNine( image.skImage, toSkRect(center), @@ -315,13 +315,14 @@ class CkCanvas { toSkRect(bounds), null, null, + canvasKit.TileMode.Clamp, ); skPaint?.delete(); } void saveLayerWithoutBounds(CkPaint? paint) { final skPaint = paint?.toSkPaint(); - skCanvas.saveLayer(skPaint, null, null, null); + skCanvas.saveLayer(skPaint, null, null, null, canvasKit.TileMode.Clamp); skPaint?.delete(); } @@ -333,16 +334,26 @@ class CkCanvas { } else { convertible = filter as CkManagedSkImageFilterConvertible; } + // There are 2 ImageFilter objects applied here. The filter in the paint + // object is applied to the contents and its default tile mode is decal + // (automatically applied by toSkPaint). + // The filter supplied as an argument to this function [convertible] will + // be applied to the backdrop and its default tile mode will be mirror. + // We also pass in the blur tile mode as an argument to saveLayer because + // that operation will not adopt the tile mode from the backdrop filter + // and instead needs it supplied to the saveLayer call itself as a + // separate argument. convertible.withSkImageFilter((SkImageFilter filter) { - final skPaint = paint?.toSkPaint(); + final skPaint = paint?.toSkPaint(/*ui.TileMode.decal*/); skCanvas.saveLayer( skPaint, toSkRect(bounds), filter, 0, + toSkTileMode(convertible.backdropTileMode ?? ui.TileMode.mirror), ); skPaint?.delete(); - }); + }, defaultBlurTileMode: ui.TileMode.mirror); } void scale(double sx, double sy) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 18181388d9d..b9fdcfce7bf 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -987,8 +987,8 @@ final List _skTileModes = [ canvasKit.TileMode.Decal, ]; -SkTileMode toSkTileMode(ui.TileMode mode) { - return _skTileModes[mode.index]; +SkTileMode toSkTileMode(ui.TileMode? mode) { + return mode == null ? canvasKit.TileMode.Clamp : _skTileModes[mode.index]; } @JS() @@ -2579,13 +2579,15 @@ extension SkCanvasExtension on SkCanvas { JSFloat32Array? bounds, SkImageFilter? backdrop, JSNumber? flags, + SkTileMode backdropTileMode, ); void saveLayer( SkPaint? paint, Float32List? bounds, SkImageFilter? backdrop, int? flags, - ) => _saveLayer(paint, bounds?.toJS, backdrop, flags?.toJS); + SkTileMode backdropTileMode, + ) => _saveLayer(paint, bounds?.toJS, backdrop, flags?.toJS, backdropTileMode); external JSVoid restore(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart index 71c4123b458..51e5fcbcd20 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart @@ -71,7 +71,9 @@ abstract class CkColorFilter implements CkManagedSkImageFilterConvertible { SkColorFilter _initRawColorFilter(); @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { // Since ColorFilter has a const constructor it cannot store dynamically // created Skia objects. Therefore a new SkImageFilter is created every time // it's used. However, once used it's no longer needed, so it's deleted @@ -81,6 +83,13 @@ abstract class CkColorFilter implements CkManagedSkImageFilterConvertible { skImageFilter.delete(); } + /// The blur ImageFilter will override this and return the necessary + /// value to hand to the saveLayer call. It is the only filter type that + /// needs to pass along a tile mode so we just return a default value of + /// clamp for color filters. + @override + ui.TileMode? get backdropTileMode => ui.TileMode.clamp; + @override Matrix4 get transform => Matrix4.identity(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart index 6da70c1c6e8..e97327a43eb 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -24,9 +24,16 @@ abstract class CkManagedSkImageFilterConvertible implements ui.ImageFilter { /// Creates a temporary [SkImageFilter], passes it to [borrow], and then /// immediately deletes it. /// + /// If (and only if) the filter is a blur ImageFilter, then the indicated + /// [defaultBlurTileMode] is used in place of a missing (null) tile mode. + /// /// [SkImageFilter] objects are not kept around so that their memory is /// reclaimed immediately, rather than waiting for the GC cycle. - void withSkImageFilter(SkImageFilterBorrow borrow); + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }); + + ui.TileMode? get backdropTileMode; Matrix4 get transform; } @@ -38,7 +45,7 @@ abstract class CkImageFilter implements CkManagedSkImageFilterConvertible { factory CkImageFilter.blur( {required double sigmaX, required double sigmaY, - required ui.TileMode tileMode}) = _CkBlurImageFilter; + required ui.TileMode? tileMode}) = _CkBlurImageFilter; factory CkImageFilter.color({required CkColorFilter colorFilter}) = CkColorFilterImageFilter; factory CkImageFilter.matrix( @@ -55,6 +62,13 @@ abstract class CkImageFilter implements CkManagedSkImageFilterConvertible { CkImageFilter._(); + // The blur ImageFilter will override this and return the necessary + // value to hand to the saveLayer call. It is the only filter type that + // needs to pass along a tile mode so we just return a default value of + // clamp for all other image filters. + @override + ui.TileMode? get backdropTileMode => ui.TileMode.clamp; + @override Matrix4 get transform => Matrix4.identity(); } @@ -65,7 +79,9 @@ class CkColorFilterImageFilter extends CkImageFilter { final CkColorFilter colorFilter; @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { final skImageFilter = colorFilter.initRawImageFilter(); borrow(skImageFilter); skImageFilter.delete(); @@ -94,10 +110,15 @@ class _CkBlurImageFilter extends CkImageFilter { final double sigmaX; final double sigmaY; - final ui.TileMode tileMode; + final ui.TileMode? tileMode; @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + ui.TileMode? get backdropTileMode => tileMode; + + @override + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { /// Return the identity matrix when both sigmaX and sigmaY are 0. Replicates /// effect of applying no filter final SkImageFilter skImageFilter; @@ -110,7 +131,7 @@ class _CkBlurImageFilter extends CkImageFilter { skImageFilter = canvasKit.ImageFilter.MakeBlur( sigmaX, sigmaY, - toSkTileMode(tileMode), + toSkTileMode(tileMode ?? defaultBlurTileMode), null, ); } @@ -151,7 +172,9 @@ class _CkMatrixImageFilter extends CkImageFilter { final Matrix4 _transform; @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { final skImageFilter = canvasKit.ImageFilter.MakeMatrixTransform( toSkMatrixFromFloat64(matrix), @@ -190,7 +213,9 @@ class _CkDilateImageFilter extends CkImageFilter { final double radiusY; @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { final skImageFilter = canvasKit.ImageFilter.MakeDilate( radiusX, radiusY, @@ -227,7 +252,9 @@ class _CkErodeImageFilter extends CkImageFilter { final double radiusY; @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { final skImageFilter = canvasKit.ImageFilter.MakeErode( radiusX, radiusY, @@ -264,7 +291,9 @@ class _CkComposeImageFilter extends CkImageFilter { final CkImageFilter inner; @override - void withSkImageFilter(SkImageFilterBorrow borrow) { + void withSkImageFilter(SkImageFilterBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { outer.withSkImageFilter((skOuter) { inner.withSkImageFilter((skInner) { final skImageFilter = canvasKit.ImageFilter.MakeCompose( @@ -273,8 +302,8 @@ class _CkComposeImageFilter extends CkImageFilter { ); borrow(skImageFilter); skImageFilter.delete(); - }); - }); + }, defaultBlurTileMode: defaultBlurTileMode); + }, defaultBlurTileMode: defaultBlurTileMode); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart index 68b08dc5fe1..9850c75c99f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart @@ -402,7 +402,7 @@ class MeasureVisitor extends LayerVisitor { transformedBounds = rectFromSkIRect( skFilter.getOutputBounds(toSkRect(transformedBounds)), ); - }); + }, defaultBlurTileMode: ui.TileMode.decal); } picture.sceneBounds = transformedBounds; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart index 8d791e94e59..c56ddce6c8f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -29,7 +29,7 @@ class CkPaint implements ui.Paint { /// /// The caller is responsible for deleting the returned object when it's no /// longer needed. - SkPaint toSkPaint() { + SkPaint toSkPaint({ui.TileMode defaultBlurTileMode = ui.TileMode.decal}) { final skPaint = SkPaint(); skPaint.setAntiAlias(isAntiAlias); skPaint.setBlendMode(toSkBlendMode(blendMode)); @@ -65,7 +65,7 @@ class CkPaint implements ui.Paint { if (localImageFilter != null) { localImageFilter.withSkImageFilter((skImageFilter) { skPaint.setImageFilter(skImageFilter); - }); + }, defaultBlurTileMode: defaultBlurTileMode); } return skPaint; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index d78e0b17546..6676ab45446 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -184,7 +184,7 @@ class CanvasKitRenderer implements Renderer { ui.ImageFilter createBlurImageFilter( {double sigmaX = 0.0, double sigmaY = 0.0, - ui.TileMode tileMode = ui.TileMode.clamp}) => + ui.TileMode? tileMode}) => CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart index 1bcdc2f53a1..fa9aee9f78a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/renderer.dart @@ -111,7 +111,7 @@ class HtmlRenderer implements Renderer { ui.ImageFilter createBlurImageFilter( {double sigmaX = 0.0, double sigmaY = 0.0, - ui.TileMode tileMode = ui.TileMode.clamp}) => + ui.TileMode? tileMode}) => EngineImageFilter.blur( sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart index 126c14770de..62f722eee77 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -717,7 +717,7 @@ abstract class EngineImageFilter implements ui.ImageFilter { factory EngineImageFilter.blur({ required double sigmaX, required double sigmaY, - required ui.TileMode tileMode, + required ui.TileMode? tileMode, }) = _BlurEngineImageFilter; factory EngineImageFilter.matrix({ @@ -732,11 +732,11 @@ abstract class EngineImageFilter implements ui.ImageFilter { } class _BlurEngineImageFilter extends EngineImageFilter { - _BlurEngineImageFilter({ this.sigmaX = 0.0, this.sigmaY = 0.0, this.tileMode = ui.TileMode.clamp }) : super._(); + _BlurEngineImageFilter({ this.sigmaX = 0.0, this.sigmaY = 0.0, this.tileMode }) : super._(); final double sigmaX; final double sigmaY; - final ui.TileMode tileMode; + final ui.TileMode? tileMode; // TODO(ferhat): implement TileMode. @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart index e0c3a59d63a..aa3e8bfc9be 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart @@ -120,7 +120,7 @@ abstract class Renderer { ui.ImageFilter createBlurImageFilter({ double sigmaX = 0.0, double sigmaY = 0.0, - ui.TileMode tileMode = ui.TileMode.clamp}); + ui.TileMode? tileMode}); ui.ImageFilter createDilateImageFilter({ double radiusX = 0.0, double radiusY = 0.0}); ui.ImageFilter createErodeImageFilter({ double radiusX = 0.0, double radiusY = 0.0}); ui.ImageFilter createMatrixImageFilter( diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index 9bbb98ff244..2c918a11890 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -32,28 +32,41 @@ class SkwasmCanvas implements SceneCanvas { final paintHandle = (paint as SkwasmPaint).toRawPaint(); if (bounds != null) { withStackScope((StackScope s) { - canvasSaveLayer(_handle, s.convertRectToNative(bounds), paintHandle, nullptr); + canvasSaveLayer(_handle, s.convertRectToNative(bounds), paintHandle, nullptr, + ui.TileMode.clamp.index); }); } else { - canvasSaveLayer(_handle, nullptr, paintHandle, nullptr); + canvasSaveLayer(_handle, nullptr, paintHandle, nullptr, ui.TileMode.clamp.index); } paintDispose(paintHandle); } @override void saveLayerWithFilter(ui.Rect? bounds, ui.Paint paint, ui.ImageFilter imageFilter) { + // There are 2 ImageFilter objects applied here. The filter in the paint + // object is applied to the contents and its default tile mode is decal + // (automatically applied by toSkPaint). + // The filter supplied as an argument to this function [nativeFilter] will + // be applied to the backdrop and its default tile mode will be mirror. + // We also pass in the blur tile mode as an argument to saveLayer because + // that operation will not adopt the tile mode from the backdrop filter + // and instead needs it supplied to the saveLayer call itself as a + // separate argument. final SkwasmImageFilter nativeFilter = SkwasmImageFilter.fromUiFilter(imageFilter); - final paintHandle = (paint as SkwasmPaint).toRawPaint(); + final ui.TileMode? backdropTileMode = nativeFilter.backdropTileMode; + final paintHandle = (paint as SkwasmPaint).toRawPaint(/*ui.TileMode.decal*/); if (bounds != null) { withStackScope((StackScope s) { nativeFilter.withRawImageFilter((nativeFilterHandle) { - canvasSaveLayer(_handle, s.convertRectToNative(bounds), paintHandle, nativeFilterHandle); - }); + canvasSaveLayer(_handle, s.convertRectToNative(bounds), paintHandle, nativeFilterHandle, + (backdropTileMode ?? ui.TileMode.mirror).index); + }, defaultBlurTileMode: ui.TileMode.mirror); }); } else { nativeFilter.withRawImageFilter((nativeFilterHandle) { - canvasSaveLayer(_handle, nullptr, paintHandle, nativeFilterHandle); - }); + canvasSaveLayer(_handle, nullptr, paintHandle, nativeFilterHandle, + (backdropTileMode ?? ui.TileMode.mirror).index); + }, defaultBlurTileMode: ui.TileMode.mirror); } paintDispose(paintHandle); } @@ -212,7 +225,9 @@ class SkwasmCanvas implements SceneCanvas { @override void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) { - final paintHandle = (paint as SkwasmPaint).toRawPaint(); + final paintHandle = (paint as SkwasmPaint).toRawPaint( + defaultBlurTileMode: ui.TileMode.clamp, + ); canvasDrawImage( _handle, (image as SkwasmImage).handle, @@ -234,7 +249,9 @@ class SkwasmCanvas implements SceneCanvas { withStackScope((StackScope scope) { final Pointer sourceRect = scope.convertRectToNative(src); final Pointer destRect = scope.convertRectToNative(dst); - final paintHandle = (paint as SkwasmPaint).toRawPaint(); + final paintHandle = (paint as SkwasmPaint).toRawPaint( + defaultBlurTileMode: ui.TileMode.clamp, + ); canvasDrawImageRect( _handle, (image as SkwasmImage).handle, @@ -257,7 +274,9 @@ class SkwasmCanvas implements SceneCanvas { withStackScope((StackScope scope) { final Pointer centerRect = scope.convertIRectToNative(center); final Pointer destRect = scope.convertRectToNative(dst); - final paintHandle = (paint as SkwasmPaint).toRawPaint(); + final paintHandle = (paint as SkwasmPaint).toRawPaint( + defaultBlurTileMode: ui.TileMode.clamp, + ); canvasDrawImageNine( _handle, (image as SkwasmImage).handle, @@ -355,7 +374,9 @@ class SkwasmCanvas implements SceneCanvas { final RawRect rawCullRect = cullRect != null ? scope.convertRectToNative(cullRect) : nullptr; - final paintHandle = (paint as SkwasmPaint).toRawPaint(); + final paintHandle = (paint as SkwasmPaint).toRawPaint( + defaultBlurTileMode: ui.TileMode.clamp, + ); canvasDrawAtlas( _handle, (atlas as SkwasmImage).handle, @@ -388,7 +409,9 @@ class SkwasmCanvas implements SceneCanvas { final RawRect rawCullRect = cullRect != null ? scope.convertRectToNative(cullRect) : nullptr; - final paintHandle = (paint as SkwasmPaint).toRawPaint(); + final paintHandle = (paint as SkwasmPaint).toRawPaint( + defaultBlurTileMode: ui.TileMode.clamp, + ); canvasDrawAtlas( _handle, (atlas as SkwasmImage).handle, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart index 06b8c47cc34..615a0285cdf 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart @@ -16,7 +16,7 @@ abstract class SkwasmImageFilter implements SceneImageFilter { factory SkwasmImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0, - ui.TileMode tileMode = ui.TileMode.clamp, + ui.TileMode? tileMode, }) => SkwasmBlurFilter(sigmaX, sigmaY, tileMode); factory SkwasmImageFilter.dilate({ @@ -58,9 +58,16 @@ abstract class SkwasmImageFilter implements SceneImageFilter { /// Creates a temporary [ImageFilterHandle] and passes it to the [borrow] /// function. /// + /// If (and only if) the filter is a blur ImageFilter, then the indicated + /// [defaultBlurTileMode] is used in place of a missing (null) tile mode. + /// /// The handle is deleted immediately after [borrow] returns. The [borrow] /// function must not store the handle to avoid dangling pointer bugs. - void withRawImageFilter(ImageFilterHandleBorrow borrow); + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }); + + ui.TileMode? get backdropTileMode => ui.TileMode.clamp; @override ui.Rect filterBounds(ui.Rect inputBounds) => withStackScope((StackScope scope) { @@ -77,11 +84,16 @@ class SkwasmBlurFilter extends SkwasmImageFilter { final double sigmaX; final double sigmaY; - final ui.TileMode tileMode; + final ui.TileMode? tileMode; @override - void withRawImageFilter(ImageFilterHandleBorrow borrow) { - final rawImageFilter = imageFilterCreateBlur(sigmaX, sigmaY, tileMode.index); + ui.TileMode? get backdropTileMode => tileMode; + + @override + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { + final rawImageFilter = imageFilterCreateBlur(sigmaX, sigmaY, (tileMode ?? defaultBlurTileMode).index); borrow(rawImageFilter); imageFilterDispose(rawImageFilter); } @@ -100,7 +112,9 @@ class SkwasmDilateFilter extends SkwasmImageFilter { final double radiusY; @override - void withRawImageFilter(ImageFilterHandleBorrow borrow) { + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { final rawImageFilter = imageFilterCreateDilate(radiusX, radiusY); borrow(rawImageFilter); imageFilterDispose(rawImageFilter); @@ -120,7 +134,9 @@ class SkwasmErodeFilter extends SkwasmImageFilter { final double radiusY; @override - void withRawImageFilter(ImageFilterHandleBorrow borrow) { + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { final rawImageFilter = imageFilterCreateErode(radiusX, radiusY); borrow(rawImageFilter); imageFilterDispose(rawImageFilter); @@ -140,7 +156,9 @@ class SkwasmMatrixFilter extends SkwasmImageFilter { final ui.FilterQuality filterQuality; @override - void withRawImageFilter(ImageFilterHandleBorrow borrow) { + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { withStackScope((scope) { final rawImageFilter = imageFilterCreateMatrix( scope.convertMatrix4toSkMatrix(matrix4), @@ -164,7 +182,9 @@ class SkwasmColorImageFilter extends SkwasmImageFilter { final SkwasmColorFilter filter; @override - void withRawImageFilter(ImageFilterHandleBorrow borrow) { + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { filter.withRawColorFilter((colroFilterHandle) { final rawImageFilter = imageFilterCreateFromColorFilter(colroFilterHandle); borrow(rawImageFilter); @@ -186,14 +206,16 @@ class SkwasmComposedImageFilter extends SkwasmImageFilter { final SkwasmImageFilter inner; @override - void withRawImageFilter(ImageFilterHandleBorrow borrow) { + void withRawImageFilter(ImageFilterHandleBorrow borrow, { + ui.TileMode defaultBlurTileMode = ui.TileMode.clamp, + }) { outer.withRawImageFilter((outerHandle) { inner.withRawImageFilter((innerHandle) { final rawImageFilter = imageFilterCompose(outerHandle, innerHandle); borrow(rawImageFilter); imageFilterDispose(rawImageFilter); - }); - }); + }, defaultBlurTileMode: defaultBlurTileMode); + }, defaultBlurTileMode: defaultBlurTileMode); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart index a6eb94d1b9b..1d8ab97c44d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart @@ -16,7 +16,7 @@ class SkwasmPaint implements ui.Paint { /// /// It is the responsibility of the caller to dispose of the returned handle /// when it's no longer needed. - PaintHandle toRawPaint() { + PaintHandle toRawPaint({ui.TileMode defaultBlurTileMode = ui.TileMode.decal}) { final rawPaint = paintCreate( isAntiAlias, blendMode.index, @@ -47,7 +47,7 @@ class SkwasmPaint implements ui.Paint { final skwasmImageFilter = SkwasmImageFilter.fromUiFilter(filter); skwasmImageFilter.withRawImageFilter((nativeHandle) { paintSetImageFilter(rawPaint, nativeHandle); - }); + }, defaultBlurTileMode: defaultBlurTileMode); } return rawPaint; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart index 599085ea6c1..391e18a82dc 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart @@ -21,12 +21,14 @@ external void canvasSave(CanvasHandle canvas); RawRect, PaintHandle, ImageFilterHandle, + Int )>(symbol: 'canvas_saveLayer', isLeaf: true) external void canvasSaveLayer( CanvasHandle canvas, RawRect rect, PaintHandle paint, ImageFilterHandle handle, + int backdropTileMode, ); @Native(symbol: 'canvas_restore', isLeaf: true) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index c73c63d0060..58fe61a1b03 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -58,7 +58,7 @@ class SkwasmRenderer implements Renderer { ui.ImageFilter createBlurImageFilter({ double sigmaX = 0.0, double sigmaY = 0.0, - ui.TileMode tileMode = ui.TileMode.clamp + ui.TileMode? tileMode, }) => SkwasmImageFilter.blur( sigmaX: sigmaX, sigmaY: sigmaY, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart index 2a2e6b5137a..9aa24ed398f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart @@ -28,7 +28,7 @@ class SkwasmRenderer implements Renderer { } @override - ui.ImageFilter createBlurImageFilter({double sigmaX = 0.0, double sigmaY = 0.0, ui.TileMode tileMode = ui.TileMode.clamp}) { + ui.ImageFilter createBlurImageFilter({double sigmaX = 0.0, double sigmaY = 0.0, ui.TileMode? tileMode}) { throw UnimplementedError('Skwasm not implemented on this platform.'); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart index 9dbf6a3ce31..3734ec541fd 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart @@ -887,7 +887,7 @@ class LruCache { } /// Returns the VM-compatible string for the tile mode. -String tileModeString(ui.TileMode tileMode) { +String tileModeString(ui.TileMode? tileMode) { switch (tileMode) { case ui.TileMode.clamp: return 'clamp'; @@ -897,6 +897,8 @@ String tileModeString(ui.TileMode tileMode) { return 'repeated'; case ui.TileMode.decal: return 'decal'; + case null: + return 'unspecified'; } } diff --git a/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp b/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp index 9a8da8ad06b..cf53f5e2bd1 100644 --- a/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp +++ b/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp @@ -33,8 +33,10 @@ constexpr SkScalar kShadowLightYOffset = -450; SKWASM_EXPORT void canvas_saveLayer(SkCanvas* canvas, SkRect* rect, SkPaint* paint, - SkImageFilter* backdrop) { - canvas->saveLayer(SkCanvas::SaveLayerRec(rect, paint, backdrop, 0)); + SkImageFilter* backdrop, + SkTileMode backdropTileMode) { + canvas->saveLayer(SkCanvas::SaveLayerRec(rect, paint, backdrop, + backdropTileMode, nullptr, 0)); } SKWASM_EXPORT void canvas_save(SkCanvas* canvas) { diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart index ccd84b02d8c..66f12821ab3 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -450,7 +450,7 @@ void drawTestPicture(CkCanvas canvas) { canvas.drawCircle(const ui.Offset(30, 30), 10, CkPaint()); { canvas.saveLayerWithFilter( - kDefaultRegion, ui.ImageFilter.blur(sigmaX: 5, sigmaY: 10)); + kDefaultRegion, ui.ImageFilter.blur(sigmaX: 5, sigmaY: 10, tileMode: ui.TileMode.clamp)); canvas.drawCircle(const ui.Offset(10, 10), 10, CkPaint()); canvas.drawCircle(const ui.Offset(50, 50), 10, CkPaint()); canvas.restore(); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index f574f9c2a68..b70007019b2 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1126,11 +1126,12 @@ void _canvasTests() { toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)), null, null, + canvasKit.TileMode.Clamp, ); }); test('saveLayer without bounds', () { - canvas.saveLayer(SkPaint(), null, null, null); + canvas.saveLayer(SkPaint(), null, null, null, canvasKit.TileMode.Clamp); }); test('saveLayer with filter', () { @@ -1139,6 +1140,7 @@ void _canvasTests() { toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)), canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), 0, + canvasKit.TileMode.Repeat, ); }); diff --git a/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart b/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart index fc7ce4aa604..980e53e9bb6 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart @@ -189,4 +189,244 @@ Future testMain() async { await drawTestImageWithPaint(ui.Paint()..maskFilter = maskFilter); await matchGoldenFile('ui_filter_blur_maskfilter.png', region: region); }); + + ui.Image makeCheckerBoard(int width, int height) { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + + const double left = 0; + final double centerX = width * 0.5; + final double right = width.toDouble(); + + const double top = 0; + final double centerY = height * 0.5; + final double bottom = height.toDouble(); + + canvas.drawRect(ui.Rect.fromLTRB(left, top, centerX, centerY), + ui.Paint()..color = const ui.Color.fromARGB(255, 0, 255, 0)); + canvas.drawRect(ui.Rect.fromLTRB(centerX, top, right, centerY), + ui.Paint()..color = const ui.Color.fromARGB(255, 255, 255, 0)); + canvas.drawRect(ui.Rect.fromLTRB(left, centerY, centerX, bottom), + ui.Paint()..color = const ui.Color.fromARGB(255, 0, 0, 255)); + canvas.drawRect(ui.Rect.fromLTRB(centerX, centerY, right, bottom), + ui.Paint()..color = const ui.Color.fromARGB(255, 255, 0, 0)); + + final picture = recorder.endRecording(); + return picture.toImageSync(width, height); + } + + Future renderingOpsWithTileMode(ui.TileMode? tileMode) async { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + canvas.drawColor(const ui.Color.fromARGB(255, 224, 224, 224), ui.BlendMode.src); + + const ui.Rect zone = ui.Rect.fromLTWH(15, 15, 20, 20); + final ui.Rect arena = zone.inflate(15); + const ui.Rect ovalZone = ui.Rect.fromLTWH(20, 15, 10, 20); + + final gradient = ui.Gradient.linear( + zone.topLeft, + zone.bottomRight, + [ + const ui.Color.fromARGB(255, 0, 255, 0), + const ui.Color.fromARGB(255, 0, 0, 255), + ], + [0, 1], + ); + final filter = ui.ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0, tileMode: tileMode); + final ui.Paint white = ui.Paint()..color = const ui.Color.fromARGB(255, 255, 255, 255); + final ui.Paint grey = ui.Paint()..color = const ui.Color.fromARGB(255, 127, 127, 127); + final ui.Paint unblurredFill = ui.Paint()..shader = gradient; + final ui.Paint blurredFill = ui.Paint.from(unblurredFill) + ..imageFilter = filter; + final ui.Paint unblurredStroke = ui.Paint.from(unblurredFill) + ..style = ui.PaintingStyle.stroke + ..strokeCap = ui.StrokeCap.round + ..strokeJoin = ui.StrokeJoin.round + ..strokeWidth = 10; + final ui.Paint blurredStroke = ui.Paint.from(unblurredStroke) + ..imageFilter = filter; + final ui.Image image = makeCheckerBoard(20, 20); + const ui.Rect imageBounds = ui.Rect.fromLTRB(0, 0, 20, 20); + const ui.Rect imageCenter = ui.Rect.fromLTRB(5, 5, 9, 9); + final points = [ + zone.topLeft, + zone.topCenter, + zone.topRight, + zone.centerLeft, + zone.center, + zone.centerRight, + zone.bottomLeft, + zone.bottomCenter, + zone.bottomRight, + ]; + final vertices = ui.Vertices( + ui.VertexMode.triangles, + [ + zone.topLeft, + zone.bottomRight, + zone.topRight, + zone.topLeft, + zone.bottomRight, + zone.bottomLeft, + ], + colors: [ + const ui.Color.fromARGB(255, 0, 255, 0), + const ui.Color.fromARGB(255, 255, 0, 0), + const ui.Color.fromARGB(255, 255, 255, 0), + const ui.Color.fromARGB(255, 0, 255, 0), + const ui.Color.fromARGB(255, 255, 0, 0), + const ui.Color.fromARGB(255, 0, 0, 255), + ], + ); + final atlasXforms = [ + ui.RSTransform.fromComponents( + rotation: 0.0, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.topLeft.dx, + translateY: zone.topLeft.dy, + ), + ui.RSTransform.fromComponents( + rotation: math.pi / 2, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.topRight.dx, + translateY: zone.topRight.dy, + ), + ui.RSTransform.fromComponents( + rotation: math.pi, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.bottomRight.dx, + translateY: zone.bottomRight.dy, + ), + ui.RSTransform.fromComponents( + rotation: math.pi * 3 / 2, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.bottomLeft.dx, + translateY: zone.bottomLeft.dy, + ), + ui.RSTransform.fromComponents( + rotation: math.pi / 4, + scale: 1.0, + anchorX: 4, + anchorY: 4, + translateX: zone.center.dx, + translateY: zone.center.dy, + ), + ]; + const atlasRects = [ + ui.Rect.fromLTRB(6, 6, 14, 14), + ui.Rect.fromLTRB(6, 6, 14, 14), + ui.Rect.fromLTRB(6, 6, 14, 14), + ui.Rect.fromLTRB(6, 6, 14, 14), + ui.Rect.fromLTRB(6, 6, 14, 14), + ]; + + const double pad = 10; + final double offset = arena.width + pad; + const int columns = 5; + final ui.Rect pairArena = ui.Rect.fromLTRB(arena.left - 3, arena.top - 3, + arena.right + 3, arena.bottom + offset + 3); + + final List renderers = [ + (canvas, fill, stroke) { + canvas.saveLayer(zone.inflate(5), fill); + canvas.drawLine(zone.topLeft, zone.bottomRight, unblurredStroke); + canvas.drawLine(zone.topRight, zone.bottomLeft, unblurredStroke); + canvas.restore(); + }, + (canvas, fill, stroke) => canvas.drawLine(zone.topLeft, zone.bottomRight, stroke), + (canvas, fill, stroke) => canvas.drawRect(zone, fill), + (canvas, fill, stroke) => canvas.drawOval(ovalZone, fill), + (canvas, fill, stroke) => canvas.drawCircle(zone.center, zone.width * 0.5, fill), + (canvas, fill, stroke) => canvas.drawRRect(ui.RRect.fromRectXY(zone, 4.0, 4.0), fill), + (canvas, fill, stroke) => canvas.drawDRRect( + ui.RRect.fromRectXY(zone, 4.0, 4.0), + ui.RRect.fromRectXY(zone.deflate(4), 4.0, 4.0), + fill), + (canvas, fill, stroke) => canvas.drawArc(zone, math.pi / 4, math.pi * 3 / 2, true, fill), + (canvas, fill, stroke) => canvas.drawPath(ui.Path() + ..moveTo(zone.left, zone.top) + ..lineTo(zone.right, zone.top) + ..lineTo(zone.left, zone.bottom) + ..lineTo(zone.right, zone.bottom), + stroke), + (canvas, fill, stroke) => canvas.drawImage(image, zone.topLeft, fill), + (canvas, fill, stroke) => canvas.drawImageRect(image, imageBounds, zone.inflate(2), fill), + (canvas, fill, stroke) => canvas.drawImageNine(image, imageCenter, zone.inflate(2), fill), + (canvas, fill, stroke) => canvas.drawPoints(ui.PointMode.points, points, stroke), + (canvas, fill, stroke) => canvas.drawVertices(vertices, ui.BlendMode.dstOver, fill), + (canvas, fill, stroke) => canvas.drawAtlas(image, atlasXforms, atlasRects, + null, null, null, fill), + ]; + + canvas.save(); + canvas.translate(pad, pad); + int renderIndex = 0; + int rows = 0; + while (renderIndex < renderers.length) { + rows += 2; + canvas.save(); + for (int col = 0; col < columns && renderIndex < renderers.length; col++) { + final renderer = renderers[renderIndex++]; + canvas.drawRect(pairArena, grey); + canvas.drawRect(arena, white); + renderer(canvas, unblurredFill, unblurredStroke); + canvas.save(); + canvas.translate(0, offset); + canvas.drawRect(arena, white); + renderer(canvas, blurredFill, blurredStroke); + canvas.restore(); + canvas.translate(offset, 0); + } + canvas.restore(); + canvas.translate(0, offset * 2); + } + canvas.restore(); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + return ui.Rect.fromLTWH(0, 0, offset * columns + pad, offset * rows + pad); + } + + test('Rendering ops with ImageFilter blur with default tile mode', () async { + final region = await renderingOpsWithTileMode(null); + await matchGoldenFile('ui_filter_blurred_rendering_with_default_tile_mode.png', region: region); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('Rendering ops with ImageFilter blur with clamp tile mode', () async { + final region = await renderingOpsWithTileMode(ui.TileMode.clamp); + await matchGoldenFile('ui_filter_blurred_rendering_with_clamp_tile_mode.png', region: region); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('Rendering ops with ImageFilter blur with decal tile mode', () async { + final region = await renderingOpsWithTileMode(ui.TileMode.decal); + await matchGoldenFile('ui_filter_blurred_rendering_with_decal_tile_mode.png', region: region); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('Rendering ops with ImageFilter blur with mirror tile mode', () async { + final region = await renderingOpsWithTileMode(ui.TileMode.mirror); + await matchGoldenFile('ui_filter_blurred_rendering_with_mirror_tile_mode.png', region: region); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('Rendering ops with ImageFilter blur with repeated tile mode', () async { + final region = await renderingOpsWithTileMode(ui.TileMode.repeated); + await matchGoldenFile('ui_filter_blurred_rendering_with_repeated_tile_mode.png', region: region); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); } diff --git a/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart b/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart index 11650de3b43..d81e59befd6 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/scene_builder_test.dart @@ -501,6 +501,61 @@ Future testMain() async { 'scene_builder_opacity_layer_with_transformed_children.png', region: region); }); + + test('backdrop layer with default blur tile mode', () async { + final scene = backdropBlurWithTileMode(null, 10, 50); + await renderScene(scene); + + await matchGoldenFile( + 'scene_builder_backdrop_filter_blur_default_tile_mode.png', + region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50)); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('backdrop layer with clamp blur tile mode', () async { + final scene = backdropBlurWithTileMode(ui.TileMode.clamp, 10, 50); + await renderScene(scene); + + await matchGoldenFile( + 'scene_builder_backdrop_filter_blur_clamp_tile_mode.png', + region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50)); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('backdrop layer with mirror blur tile mode', () async { + final scene = backdropBlurWithTileMode(ui.TileMode.mirror, 10, 50); + await renderScene(scene); + + await matchGoldenFile( + 'scene_builder_backdrop_filter_blur_mirror_tile_mode.png', + region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50)); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('backdrop layer with repeated blur tile mode', () async { + final scene = backdropBlurWithTileMode(ui.TileMode.repeated, 10, 50); + await renderScene(scene); + + await matchGoldenFile( + 'scene_builder_backdrop_filter_blur_repeated_tile_mode.png', + region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50)); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); + + test('backdrop layer with decal blur tile mode', () async { + final scene = backdropBlurWithTileMode(ui.TileMode.decal, 10, 50); + await renderScene(scene); + + await matchGoldenFile( + 'scene_builder_backdrop_filter_blur_decal_tile_mode.png', + region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50)); + }, + // HTML renderer doesn't have tile modes + skip: isHtml); }); } @@ -510,3 +565,58 @@ ui.Picture drawPicture(void Function(ui.Canvas) drawCommands) { drawCommands(canvas); return recorder.endRecording(); } + +ui.Scene backdropBlurWithTileMode(ui.TileMode? tileMode, + final double rectSize, + final int count) { + final double imgSize = rectSize * count; + + const ui.Color white = ui.Color(0xFFFFFFFF); + const ui.Color purple = ui.Color(0xFFFF00FF); + const ui.Color blue = ui.Color(0xFF0000FF); + const ui.Color green = ui.Color(0xFF00FF00); + const ui.Color yellow = ui.Color(0xFFFFFF00); + const ui.Color red = ui.Color(0xFFFF0000); + + final ui.Picture blueGreenGridPicture = drawPicture((ui.Canvas canvas) { + canvas.drawColor(white, ui.BlendMode.src); + for (int i = 0; i < count; i++) { + for (int j = 0; j < count; j++) { + final bool rectOdd = (i + j) & 1 == 0; + final ui.Color fg = (i < count / 2) + ? ((j < count / 2) ? green : blue) + : ((j < count / 2) ? yellow : red); + canvas.drawRect(ui.Rect.fromLTWH(i * rectSize, j * rectSize, rectSize, rectSize), + ui.Paint()..color = rectOdd ? fg : white); + } + } + canvas.drawRect(ui.Rect.fromLTWH(0, 0, imgSize, 1), ui.Paint()..color = purple); + canvas.drawRect(ui.Rect.fromLTWH(0, 0, 1, imgSize), ui.Paint()..color = purple); + canvas.drawRect(ui.Rect.fromLTWH(0, imgSize - 1, imgSize, 1), ui.Paint()..color = purple); + canvas.drawRect(ui.Rect.fromLTWH(imgSize - 1, 0, 1, imgSize), ui.Paint()..color = purple); + }); + + final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); + // We push a clipRect layer with the SaveLayer behavior so that it creates + // a layer of predetermined size in which the backdrop filter will apply + // its filter to show the edge effects on predictable edges. + sceneBuilder.pushClipRect(ui.Rect.fromLTWH(0, 0, imgSize, imgSize), + clipBehavior: ui.Clip.antiAliasWithSaveLayer); + sceneBuilder.addPicture(ui.Offset.zero, blueGreenGridPicture); + sceneBuilder.pushBackdropFilter(ui.ImageFilter.blur(sigmaX: 20, sigmaY: 20, tileMode: tileMode)); + // The following picture prevents the saveLayer in the backdrop filter from + // being completely ignored on the skwasm backend due to an interaction with + // SkPictureRecorder eliminating saveLayer entries with no content even if + // they have a backdrop filter. It draws nothing because the pixels below + // it are opaque and dstOver is a NOP in that case, but it is unlikely that + // a recording process would be able to figure that out without extensive + // analysis between the pictures and layers. + sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { + canvas.drawRect(ui.Rect.fromLTWH(imgSize * 0.5 - 10, imgSize * 0.5 - 10, 20, 20), + ui.Paint()..color = purple..blendMode = ui.BlendMode.dstOver); + })); + sceneBuilder.pop(); + sceneBuilder.pop(); + + return sceneBuilder.build(); +} diff --git a/engine/src/flutter/testing/dart/canvas_test.dart b/engine/src/flutter/testing/dart/canvas_test.dart index dc62b9c77e2..604a6ce4832 100644 --- a/engine/src/flutter/testing/dart/canvas_test.dart +++ b/engine/src/flutter/testing/dart/canvas_test.dart @@ -1394,6 +1394,236 @@ void main() async { final ByteData? data = await resultImage.toByteData(); expect(data, isNotNull); }); + + Image makeCheckerBoard(int width, int height) { + final recorder = PictureRecorder(); + final canvas = Canvas(recorder); + + const double left = 0; + final double centerX = width * 0.5; + final double right = width.toDouble(); + + const double top = 0; + final double centerY = height * 0.5; + final double bottom = height.toDouble(); + + canvas.drawRect(Rect.fromLTRB(left, top, centerX, centerY), + Paint()..color = const Color.fromARGB(255, 0, 255, 0)); + canvas.drawRect(Rect.fromLTRB(centerX, top, right, centerY), + Paint()..color = const Color.fromARGB(255, 255, 255, 0)); + canvas.drawRect(Rect.fromLTRB(left, centerY, centerX, bottom), + Paint()..color = const Color.fromARGB(255, 0, 0, 255)); + canvas.drawRect(Rect.fromLTRB(centerX, centerY, right, bottom), + Paint()..color = const Color.fromARGB(255, 255, 0, 0)); + + final picture = recorder.endRecording(); + return picture.toImageSync(width, height); + } + + Image renderingOpsWithTileMode(TileMode? tileMode) { + final recorder = PictureRecorder(); + final canvas = Canvas(recorder); + canvas.drawColor(const Color.fromARGB(255, 224, 224, 224), BlendMode.src); + + const Rect zone = Rect.fromLTWH(15, 15, 20, 20); + final Rect arena = zone.inflate(15); + const Rect ovalZone = Rect.fromLTWH(20, 15, 10, 20); + + final gradient = Gradient.linear( + zone.topLeft, + zone.bottomRight, + [ + const Color.fromARGB(255, 0, 255, 0), + const Color.fromARGB(255, 0, 0, 255), + ], + [0, 1], + ); + final filter = ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0, tileMode: tileMode); + final Paint white = Paint()..color = const Color.fromARGB(255, 255, 255, 255); + final Paint grey = Paint()..color = const Color.fromARGB(255, 127, 127, 127); + final Paint unblurredFill = Paint()..shader = gradient; + final Paint blurredFill = Paint.from(unblurredFill) + ..imageFilter = filter; + final Paint unblurredStroke = Paint.from(unblurredFill) + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = 10; + final Paint blurredStroke = Paint.from(unblurredStroke) + ..imageFilter = filter; + final Image image = makeCheckerBoard(20, 20); + const Rect imageBounds = Rect.fromLTRB(0, 0, 20, 20); + const Rect imageCenter = Rect.fromLTRB(5, 5, 9, 9); + final points = [ + zone.topLeft, + zone.topCenter, + zone.topRight, + zone.centerLeft, + zone.center, + zone.centerRight, + zone.bottomLeft, + zone.bottomCenter, + zone.bottomRight, + ]; + final vertices = Vertices( + VertexMode.triangles, + [ + zone.topLeft, + zone.bottomRight, + zone.topRight, + zone.topLeft, + zone.bottomRight, + zone.bottomLeft, + ], + colors: [ + const Color.fromARGB(255, 0, 255, 0), + const Color.fromARGB(255, 255, 0, 0), + const Color.fromARGB(255, 255, 255, 0), + const Color.fromARGB(255, 0, 255, 0), + const Color.fromARGB(255, 255, 0, 0), + const Color.fromARGB(255, 0, 0, 255), + ], + ); + final atlasXforms = [ + RSTransform.fromComponents( + rotation: 0.0, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.topLeft.dx, + translateY: zone.topLeft.dy, + ), + RSTransform.fromComponents( + rotation: pi / 2, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.topRight.dx, + translateY: zone.topRight.dy, + ), + RSTransform.fromComponents( + rotation: pi, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.bottomRight.dx, + translateY: zone.bottomRight.dy, + ), + RSTransform.fromComponents( + rotation: pi * 3 / 2, + scale: 1.0, + anchorX: 0, + anchorY: 0, + translateX: zone.bottomLeft.dx, + translateY: zone.bottomLeft.dy, + ), + RSTransform.fromComponents( + rotation: pi / 4, + scale: 1.0, + anchorX: 4, + anchorY: 4, + translateX: zone.center.dx, + translateY: zone.center.dy, + ), + ]; + const atlasRects = [ + Rect.fromLTRB(6, 6, 14, 14), + Rect.fromLTRB(6, 6, 14, 14), + Rect.fromLTRB(6, 6, 14, 14), + Rect.fromLTRB(6, 6, 14, 14), + Rect.fromLTRB(6, 6, 14, 14), + ]; + + const double pad = 10; + final double offset = arena.width + pad; + const int columns = 5; + final Rect pairArena = Rect.fromLTRB(arena.left - 3, arena.top - 3, + arena.right + 3, arena.bottom + offset + 3); + + final List renderers = [ + (canvas, fill, stroke) { + canvas.saveLayer(zone.inflate(5), fill); + canvas.drawLine(zone.topLeft, zone.bottomRight, unblurredStroke); + canvas.drawLine(zone.topRight, zone.bottomLeft, unblurredStroke); + canvas.restore(); + }, + (canvas, fill, stroke) => canvas.drawLine(zone.topLeft, zone.bottomRight, stroke), + (canvas, fill, stroke) => canvas.drawRect(zone, fill), + (canvas, fill, stroke) => canvas.drawOval(ovalZone, fill), + (canvas, fill, stroke) => canvas.drawCircle(zone.center, zone.width * 0.5, fill), + (canvas, fill, stroke) => canvas.drawRRect(RRect.fromRectXY(zone, 4.0, 4.0), fill), + (canvas, fill, stroke) => canvas.drawDRRect(RRect.fromRectXY(zone, 4.0, 4.0), + RRect.fromRectXY(zone.deflate(4), 4.0, 4.0), + fill), + (canvas, fill, stroke) => canvas.drawArc(zone, pi / 4, pi * 3 / 2, true, fill), + (canvas, fill, stroke) => canvas.drawPath(Path() + ..moveTo(zone.left, zone.top) + ..lineTo(zone.right, zone.top) + ..lineTo(zone.left, zone.bottom) + ..lineTo(zone.right, zone.bottom), + stroke), + (canvas, fill, stroke) => canvas.drawImage(image, zone.topLeft, fill), + (canvas, fill, stroke) => canvas.drawImageRect(image, imageBounds, zone.inflate(2), fill), + (canvas, fill, stroke) => canvas.drawImageNine(image, imageCenter, zone.inflate(2), fill), + (canvas, fill, stroke) => canvas.drawPoints(PointMode.points, points, stroke), + (canvas, fill, stroke) => canvas.drawVertices(vertices, BlendMode.dstOver, fill), + (canvas, fill, stroke) => canvas.drawAtlas(image, atlasXforms, atlasRects, + null, null, null, fill), + ]; + + canvas.save(); + canvas.translate(pad, pad); + int renderIndex = 0; + int rows = 0; + while (renderIndex < renderers.length) { + rows += 2; + canvas.save(); + for (int col = 0; col < columns && renderIndex < renderers.length; col++) { + final renderer = renderers[renderIndex++]; + canvas.drawRect(pairArena, grey); + canvas.drawRect(arena, white); + renderer(canvas, unblurredFill, unblurredStroke); + canvas.save(); + canvas.translate(0, offset); + canvas.drawRect(arena, white); + renderer(canvas, blurredFill, blurredStroke); + canvas.restore(); + canvas.translate(offset, 0); + } + canvas.restore(); + canvas.translate(0, offset * 2); + } + canvas.restore(); + + final picture = recorder.endRecording(); + return picture.toImageSync((offset * columns + pad).round(), + (offset * rows + pad).round()); + } + + test('Rendering ops with ImageFilter blur with default tile mode', () async { + final image = renderingOpsWithTileMode(null); + await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_default_tile_mode.png'); + }); + + test('Rendering ops with ImageFilter blur with clamp tile mode', () async { + final image = renderingOpsWithTileMode(TileMode.clamp); + await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_clamp_tile_mode.png'); + }); + + test('Rendering ops with ImageFilter blur with mirror tile mode', () async { + final image = renderingOpsWithTileMode(TileMode.mirror); + await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_mirror_tile_mode.png'); + }); + + test('Rendering ops with ImageFilter blur with repeated tile mode', () async { + final image = renderingOpsWithTileMode(TileMode.repeated); + await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_repeated_tile_mode.png'); + }); + + test('Rendering ops with ImageFilter blur with decal tile mode', () async { + final image = renderingOpsWithTileMode(TileMode.decal); + await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_decal_tile_mode.png'); + }); } Future createTestImage() async { diff --git a/engine/src/flutter/testing/dart/compositing_test.dart b/engine/src/flutter/testing/dart/compositing_test.dart index b363981ebbc..44377b04298 100644 --- a/engine/src/flutter/testing/dart/compositing_test.dart +++ b/engine/src/flutter/testing/dart/compositing_test.dart @@ -336,7 +336,7 @@ void main() { return builder.pushOpacity(100, oldLayer: oldLayer as OpacityEngineLayer?); }); testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { - return builder.pushBackdropFilter(ImageFilter.blur(), oldLayer: oldLayer as BackdropFilterEngineLayer?); + return builder.pushBackdropFilter(ImageFilter.blur(sigmaX: 1.0), oldLayer: oldLayer as BackdropFilterEngineLayer?); }); testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { return builder.pushShaderMask( diff --git a/engine/src/flutter/testing/dart/image_filter_test.dart b/engine/src/flutter/testing/dart/image_filter_test.dart index dab24162715..5818038f465 100644 --- a/engine/src/flutter/testing/dart/image_filter_test.dart +++ b/engine/src/flutter/testing/dart/image_filter_test.dart @@ -68,7 +68,7 @@ void main() async { return bytes.buffer.asUint32List(); } - ImageFilter makeBlur(double sigmaX, double sigmaY, [TileMode tileMode = TileMode.clamp]) => + ImageFilter makeBlur(double sigmaX, double sigmaY, [TileMode? tileMode]) => ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); ImageFilter makeDilate(double radiusX, double radiusY) => @@ -106,6 +106,9 @@ void main() async { return [ makeBlur(10.0, 10.0), makeBlur(10.0, 10.0, TileMode.decal), + makeBlur(10.0, 10.0, TileMode.clamp), + makeBlur(10.0, 10.0, TileMode.mirror), + makeBlur(10.0, 10.0, TileMode.repeated), makeBlur(10.0, 20.0), makeBlur(20.0, 20.0), makeDilate(10.0, 20.0), @@ -183,6 +186,24 @@ void main() async { checkBytes(bytes, greenCenterBlurred, greenSideBlurred, greenCornerBlurred); }); + test('ImageFilter - blur toString', () async { + + var filter = makeBlur(1.9, 2.1); + expect(filter.toString(), 'ImageFilter.blur(1.9, 2.1, unspecified)'); + + filter = makeBlur(1.9, 2.1, TileMode.decal); + expect(filter.toString(), 'ImageFilter.blur(1.9, 2.1, decal)'); + + filter = makeBlur(1.9, 2.1, TileMode.clamp); + expect(filter.toString(), 'ImageFilter.blur(1.9, 2.1, clamp)'); + + filter = makeBlur(1.9, 2.1, TileMode.mirror); + expect(filter.toString(), 'ImageFilter.blur(1.9, 2.1, mirror)'); + + filter = makeBlur(1.9, 2.1, TileMode.repeated); + expect(filter.toString(), 'ImageFilter.blur(1.9, 2.1, repeated)'); + }); + test('ImageFilter - dilate', () async { final Paint paint = Paint() ..color = green @@ -279,7 +300,7 @@ void main() async { test('Composite ImageFilter toString', () { expect( ImageFilter.compose(outer: makeBlur(20.0, 20.0, TileMode.decal), inner: makeBlur(10.0, 10.0)).toString(), - contains('blur(10.0, 10.0, clamp) -> blur(20.0, 20.0, decal)'), + contains('blur(10.0, 10.0, unspecified) -> blur(20.0, 20.0, decal)'), ); // Produces a flat list of filters diff --git a/engine/src/flutter/testing/dart/painting_test.dart b/engine/src/flutter/testing/dart/painting_test.dart index 3fe451784bc..58ea2a5dd0f 100644 --- a/engine/src/flutter/testing/dart/painting_test.dart +++ b/engine/src/flutter/testing/dart/painting_test.dart @@ -109,7 +109,7 @@ void main() { redClippedPicture.dispose(); }); - Image backdropBlurWithTileMode(TileMode tileMode) { + Image backdropBlurWithTileMode(TileMode? tileMode) { Picture makePicture(CanvasCallback callback) { final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); @@ -194,6 +194,20 @@ void main() { image.dispose(); }); + test('BackdropFilter with Blur default TileMode acts as TileMode.mirror', () async { + final Image image = backdropBlurWithTileMode(null); + + final ImageComparer comparer = await ImageComparer.create(); + // It would be nice to compare the output here to the "mirror" golden + // image generated above, but this file name is where the results of + // this test will be written and the comparison will be done independently + // in a separate step. If we repeated the name of the "mirror" golden, + // we would just overwrite the results of the mirror test above. + await comparer.addGoldenImage(image, 'dart_ui_backdrop_filter_blur_default_tile_mode.png'); + + image.dispose(); + }); + test('ImageFilter.matrix defaults to FilterQuality.medium', () { final Float64List data = Matrix4.identity().storage; expect( diff --git a/engine/src/flutter/testing/scenario_app/lib/src/platform_view.dart b/engine/src/flutter/testing/scenario_app/lib/src/platform_view.dart index 241317f3213..ce712b64385 100644 --- a/engine/src/flutter/testing/scenario_app/lib/src/platform_view.dart +++ b/engine/src/flutter/testing/scenario_app/lib/src/platform_view.dart @@ -2089,7 +2089,7 @@ class PlatformViewWithOtherBackDropFilter extends PlatformViewScenario { final Picture picture = recorder.endRecording(); builder.addPicture(Offset.zero, picture); - final ImageFilter filter = ImageFilter.blur(sigmaX: 8, sigmaY: 8); + final ImageFilter filter = ImageFilter.blur(sigmaX: 8, sigmaY: 8, tileMode: TileMode.clamp); builder.pushBackdropFilter(filter); final PictureRecorder recorder2 = PictureRecorder(); @@ -2190,7 +2190,7 @@ class TwoPlatformViewsWithOtherBackDropFilter extends Scenario final Picture picture2 = recorder2.endRecording(); builder.addPicture(const Offset(100, 100), picture2); - final ImageFilter filter = ImageFilter.blur(sigmaX: 8, sigmaY: 8); + final ImageFilter filter = ImageFilter.blur(sigmaX: 8, sigmaY: 8, tileMode: TileMode.clamp); builder.pushBackdropFilter(filter); builder.pushOffset(0, 600); @@ -2277,7 +2277,7 @@ class PlatformViewWithNegativeBackDropFilter extends Scenario final Picture picture2 = recorder2.endRecording(); builder.addPicture(const Offset(100, 100), picture2); - final ImageFilter filter = ImageFilter.blur(sigmaX: -8, sigmaY: 8); + final ImageFilter filter = ImageFilter.blur(sigmaX: -8, sigmaY: 8, tileMode: TileMode.clamp); builder.pushBackdropFilter(filter); final Scene scene = builder.build();