Change default TileMode for blur ImageFilter objects to null (flutter/engine#55552)

Fixes https://github.com/flutter/flutter/issues/154935

Historically ImageFilter.blur supported setting a TileMode and had a default mode of `clamp`, but few developers actually set the value and the default was not appropriate for some common uses like as a backdrop filter where the clamp mode produces flashing when scrolling high frequency pixel content underneath a blurred title bar.

This PR removes the default tile mode instead allowing a null value as the default which will allow the engine to use an appropriate context-dependent default tile mode depending on the action being performed. Typically:

- decal for rendering operations and saveLayers and ImageFilterLayer
- clamp for image operations
- mirror for backdrop filters
This commit is contained in:
Jim Graham 2024-10-29 15:47:32 -07:00 committed by GitHub
parent 728997cd8f
commit 80d757ef56
37 changed files with 872 additions and 112 deletions

View File

@ -152,7 +152,8 @@ void SceneBuilder::pushImageFilter(Dart_Handle layer_handle,
double dy,
const fml::RefPtr<EngineLayer>& old_layer) {
auto layer = std::make_shared<flutter::ImageFilterLayer>(
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<flutter::BackdropFilterLayer>(
filter->filter(), static_cast<DlBlendMode>(blend_mode),
filter->filter(DlTileMode::kMirror), static_cast<DlBlendMode>(blend_mode),
converted_backdrop_id);
PushLayer(layer);
EngineLayer::MakeRetained(layer_handle, layer);

View File

@ -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

View File

@ -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<const DlImageFilter> 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<const SkRSXform*>(transforms.data()),
reinterpret_cast<const SkRect*>(rects.data()), dl_color.data(),

View File

@ -51,37 +51,69 @@ ImageFilter::ImageFilter() {}
ImageFilter::~ImageFilter() {}
const std::shared_ptr<const DlImageFilter> 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<DlTileMode>(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

View File

@ -28,14 +28,14 @@ class ImageFilter : public RefCountedDartWrappable<ImageFilter> {
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<const DlImageFilter> filter() const { return filter_; }
const std::shared_ptr<const DlImageFilter> filter(DlTileMode mode) const;
static void RegisterNatives(tonic::DartLibraryNatives* natives);
@ -43,6 +43,7 @@ class ImageFilter : public RefCountedDartWrappable<ImageFilter> {
ImageFilter();
std::shared_ptr<const DlImageFilter> filter_;
bool is_dynamic_tile_mode_ = false;
};
} // namespace flutter

View File

@ -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<ImageFilter*>::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<ImageFilter*>::FromDart(image_filter);
paint.setImageFilter(decoded->filter());
paint.setImageFilter(decoded->filter(tile_mode));
}
}

View File

@ -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_); }

View File

@ -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();
};

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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) {

View File

@ -987,8 +987,8 @@ final List<SkTileMode> _skTileModes = <SkTileMode>[
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();

View File

@ -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();
}

View File

@ -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

View File

@ -402,7 +402,7 @@ class MeasureVisitor extends LayerVisitor {
transformedBounds = rectFromSkIRect(
skFilter.getOutputBounds(toSkRect(transformedBounds)),
);
});
}, defaultBlurTileMode: ui.TileMode.decal);
}
picture.sceneBounds = transformedBounds;

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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(

View File

@ -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<Float> sourceRect = scope.convertRectToNative(src);
final Pointer<Float> 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<Int32> centerRect = scope.convertIRectToNative(center);
final Pointer<Float> 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,

View File

@ -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

View File

@ -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;

View File

@ -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<Void Function(CanvasHandle)>(symbol: 'canvas_restore', isLeaf: true)

View File

@ -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,

View File

@ -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.');
}

View File

@ -887,7 +887,7 @@ class LruCache<K extends Object, V extends Object> {
}
/// 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';
}
}

View File

@ -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) {

View File

@ -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();

View File

@ -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,
);
});

View File

@ -189,4 +189,244 @@ Future<void> 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<ui.Rect> 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,
<ui.Color>[
const ui.Color.fromARGB(255, 0, 255, 0),
const ui.Color.fromARGB(255, 0, 0, 255),
],
<double>[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 = <ui.Offset>[
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,
<ui.Offset> [
zone.topLeft,
zone.bottomRight,
zone.topRight,
zone.topLeft,
zone.bottomRight,
zone.bottomLeft,
],
colors: <ui.Color>[
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>[
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>[
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<void Function(ui.Canvas canvas, ui.Paint fill, ui.Paint stroke)> 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);
}

View File

@ -501,6 +501,61 @@ Future<void> 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();
}

View File

@ -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,
<Color>[
const Color.fromARGB(255, 0, 255, 0),
const Color.fromARGB(255, 0, 0, 255),
],
<double>[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 = <Offset>[
zone.topLeft,
zone.topCenter,
zone.topRight,
zone.centerLeft,
zone.center,
zone.centerRight,
zone.bottomLeft,
zone.bottomCenter,
zone.bottomRight,
];
final vertices = Vertices(
VertexMode.triangles,
<Offset> [
zone.topLeft,
zone.bottomRight,
zone.topRight,
zone.topLeft,
zone.bottomRight,
zone.bottomLeft,
],
colors: <Color>[
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>[
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>[
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<void Function(Canvas canvas, Paint fill, Paint stroke)> 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<Image> createTestImage() async {

View File

@ -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(

View File

@ -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 <ImageFilter>[
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

View File

@ -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(

View File

@ -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();