Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions lib/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3137,10 +3137,11 @@ class _ColorFilter extends NativeFieldWrapperClass2 {
/// this class as a child layer filter.
abstract class ImageFilter {
/// Creates an image filter that applies a Gaussian blur.
factory ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 }) {
factory ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0, TileMode tileMode = TileMode.clamp }) {
assert(sigmaX != null); // ignore: unnecessary_null_comparison
assert(sigmaY != null); // ignore: unnecessary_null_comparison
return _GaussianBlurImageFilter(sigmaX: sigmaX, sigmaY: sigmaY);
assert(tileMode != null); // ignore: unnecessary_null_comparison
return _GaussianBlurImageFilter(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode);
}

/// Creates an image filter that applies a matrix transformation.
Expand Down Expand Up @@ -3206,29 +3207,40 @@ class _MatrixImageFilter implements ImageFilter {
}

class _GaussianBlurImageFilter implements ImageFilter {
_GaussianBlurImageFilter({ required this.sigmaX, required this.sigmaY });
_GaussianBlurImageFilter({ required this.sigmaX, required this.sigmaY, required this.tileMode });

final double sigmaX;
final double sigmaY;
final TileMode tileMode;

// MakeBlurFilter
late final _ImageFilter nativeFilter = _ImageFilter.blur(this);
@override
_ImageFilter _toNativeImageFilter() => nativeFilter;

String get _modeString {
switch(tileMode) {
case TileMode.clamp: return 'clamp';
case TileMode.mirror: return 'mirror';
case TileMode.repeated: return 'repeated';
case TileMode.decal: return 'decal';
}
}

@override
String get _shortDescription => 'blur($sigmaX, $sigmaY)';
String get _shortDescription => 'blur($sigmaX, $sigmaY, $_modeString)';

@override
String toString() => 'ImageFilter.blur($sigmaX, $sigmaY)';
String toString() => 'ImageFilter.blur($sigmaX, $sigmaY, $_modeString)';

@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is _GaussianBlurImageFilter
&& other.sigmaX == sigmaX
&& other.sigmaY == sigmaY;
&& other.sigmaY == sigmaY
&& other.tileMode == tileMode;
}

@override
Expand Down Expand Up @@ -3278,9 +3290,9 @@ class _ImageFilter extends NativeFieldWrapperClass2 {
: assert(filter != null), // ignore: unnecessary_null_comparison
creator = filter { // ignore: prefer_initializing_formals
_constructor();
_initBlur(filter.sigmaX, filter.sigmaY);
_initBlur(filter.sigmaX, filter.sigmaY, filter.tileMode.index);
}
void _initBlur(double sigmaX, double sigmaY) native 'ImageFilter_initBlur';
void _initBlur(double sigmaX, double sigmaY, int tileMode) native 'ImageFilter_initBlur';

/// Creates an image filter that applies a matrix transformation.
///
Expand Down Expand Up @@ -3330,14 +3342,21 @@ class Shader extends NativeFieldWrapperClass2 {
Shader._();
}

/// Defines what happens at the edge of the gradient.
/// Defines what happens at the edge of a gradient or the sampling of a source image
/// in an [ImageFilter].
///
/// A gradient is defined along a finite inner area. In the case of a linear
/// gradient, it's between the parallel lines that are orthogonal to the line
/// drawn between two points. In the case of radial gradients, it's the disc
/// that covers the circle centered on a particular point up to a given radius.
///
/// This enum is used to define how the gradient should paint the regions
/// An image filter reads source samples from a source image and performs operations
/// on those samples to produce a result image. The bounds of the source image contain
/// the pixels over which it is defined, but the operation performed by the image
/// filter may need to combine those samples with other samples read from outside
/// the image - particularly in the case of a blur operation.
///
/// This enum is used to define how the gradient or image filter should treat the regions
/// outside that defined inner area.
///
/// See also:
Expand All @@ -3349,13 +3368,16 @@ class Shader extends NativeFieldWrapperClass2 {
/// * [dart:ui.Gradient], the low-level class used when dealing with the
/// [Paint.shader] property directly, with its [Gradient.linear] and
/// [Gradient.radial] constructors.
// These enum values must be kept in sync with SkShader::TileMode.
// These enum values must be kept in sync with SkTileMode.
enum TileMode {
/// Edge is clamped to the final color.
///
/// The gradient will paint the all the regions outside the inner area with
/// the color of the point closest to that region.
/// the color of the point closest to that region. An image filter will continue
/// to read the nearest edge pixel of an image for all samples required from
/// outside the source image.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png)
/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png)
clamp,

Expand All @@ -3365,6 +3387,9 @@ enum TileMode {
/// to 2.0, 2.0 to 3.0, and so forth (and for linear gradients, similarly from
/// -1.0 to 0.0, -2.0 to -1.0, etc).
///
/// An image filter will treat the image as tiled across the sample space from
/// which it is reading.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png)
/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png)
repeated,
Expand All @@ -3376,9 +3401,19 @@ enum TileMode {
/// 4.0 to 3.0, and so forth (and for linear gradients, similarly from in the
/// negative direction).
///
/// An image filter will treat the image as tiled in an alternating forwards and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "An image filter will treat the image ..." makes me think TileMode is a property of the image, not the filter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"An image filter will treat its source as ..."?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates pushed...

/// backwards or upwards and downwards direction across the sample space from which
/// it is reading.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png)
/// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png)
mirror,

/// Edge is transparent black.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe "Extrapolates the source image with transparent black (0x00000000) pixels, when the image filter samples outside of the bounds of the source image"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of the entries are documented as "Edge is ...". I agree that I don't like the way that is all phrased. I'll look for some better wording for all of them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates pushed...

///
/// The gradient or image sampling operation will evaluate as transparent black pixels
/// all regions outside of the image or primary gradient area.
decal,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess decal is as good a name for this as any, and has the advantage of matching Skia.

}

Int32List _encodeColorList(List<Color> colors) {
Expand Down
7 changes: 4 additions & 3 deletions lib/ui/painting/image_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ void ImageFilter::initPicture(Picture* picture) {
filter_ = SkPictureImageFilter::Make(picture->picture());
}

void ImageFilter::initBlur(double sigma_x, double sigma_y) {
filter_ = SkBlurImageFilter::Make(sigma_x, sigma_y, nullptr, nullptr,
SkBlurImageFilter::kClamp_TileMode);
void ImageFilter::initBlur(double sigma_x,
double sigma_y,
SkTileMode tile_mode) {
filter_ = SkImageFilters::Blur(sigma_x, sigma_y, tile_mode, nullptr, nullptr);
}

void ImageFilter::initMatrix(const tonic::Float64List& matrix4,
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/painting/image_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ImageFilter : public RefCountedDartWrappable<ImageFilter> {

void initImage(CanvasImage* image);
void initPicture(Picture*);
void initBlur(double sigma_x, double sigma_y);
void initBlur(double sigma_x, double sigma_y, SkTileMode tile_mode);
void initMatrix(const tonic::Float64List& matrix4, int filter_quality);
void initColorFilter(ColorFilter* colorFilter);
void initComposeFilter(ImageFilter* outer, ImageFilter* inner);
Expand Down
2 changes: 2 additions & 0 deletions lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ class SkTileModeEnum {
external SkTileMode get Clamp;
external SkTileMode get Repeat;
external SkTileMode get Mirror;
external SkTileMode get Decal;
}

@JS()
Expand All @@ -653,6 +654,7 @@ final List<SkTileMode> _skTileModes = <SkTileMode>[
canvasKit.TileMode.Clamp,
canvasKit.TileMode.Repeat,
canvasKit.TileMode.Mirror,
canvasKit.TileMode.Decal,
];

SkTileMode toSkTileMode(ui.TileMode mode) {
Expand Down
23 changes: 17 additions & 6 deletions lib/web_ui/lib/src/engine/canvaskit/image_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ abstract class _CkManagedSkImageFilterConvertible<T extends Object> implements u
///
/// Currently only supports `blur`.
abstract class CkImageFilter extends ManagedSkiaObject<SkImageFilter> implements _CkManagedSkImageFilterConvertible<SkImageFilter> {
factory CkImageFilter.blur({ required double sigmaX, required double sigmaY }) = _CkBlurImageFilter;
factory CkImageFilter.blur({ required double sigmaX, required double sigmaY, required ui.TileMode tileMode }) = _CkBlurImageFilter;
factory CkImageFilter.color({ required CkColorFilter colorFilter }) = _CkColorFilterImageFilter;

CkImageFilter._();
Expand Down Expand Up @@ -66,17 +66,27 @@ class _CkColorFilterImageFilter extends CkImageFilter {
}

class _CkBlurImageFilter extends CkImageFilter {
_CkBlurImageFilter({ required this.sigmaX, required this.sigmaY }) : super._();
_CkBlurImageFilter({ required this.sigmaX, required this.sigmaY, required this.tileMode }) : super._();

final double sigmaX;
final double sigmaY;
final ui.TileMode tileMode;

String get _modeString {
switch (tileMode) {
case ui.TileMode.clamp: return 'clamp';
case ui.TileMode.mirror: return 'mirror';
case ui.TileMode.repeated: return 'repeated';
case ui.TileMode.decal: return 'decal';
}
}

@override
SkImageFilter _initSkiaObject() {
return canvasKit.ImageFilter.MakeBlur(
sigmaX,
sigmaY,
canvasKit.TileMode.Clamp,
toSkTileMode(tileMode),
null,
);
}
Expand All @@ -87,15 +97,16 @@ class _CkBlurImageFilter extends CkImageFilter {
return false;
return other is _CkBlurImageFilter
&& other.sigmaX == sigmaX
&& other.sigmaY == sigmaY;
&& other.sigmaY == sigmaY
&& other.tileMode == tileMode;
}

@override
int get hashCode => ui.hashValues(sigmaX, sigmaY);
int get hashCode => ui.hashValues(sigmaX, sigmaY, tileMode);

@override
String toString() {
return 'ImageFilter.blur($sigmaX, $sigmaY)';
return 'ImageFilter.blur($sigmaX, $sigmaY, $_modeString)';
}
}

31 changes: 22 additions & 9 deletions lib/web_ui/lib/src/engine/html/shaders/shader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class GradientLinear extends EngineGradient {
@override
Object createPaintStyle(html.CanvasRenderingContext2D? ctx,
ui.Rect? shaderBounds, double density) {
if (tileMode == ui.TileMode.clamp) {
if (tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal) {
return _createCanvasGradient(ctx, shaderBounds, density);
} else {
initWebGl();
Expand Down Expand Up @@ -172,7 +172,7 @@ class GradientLinear extends EngineGradient {
from.dx - offsetX, from.dy - offsetY, to.dx - offsetX,
to.dy - offsetY);
}
_addColorStopsToCanvasGradient(gradient, colors, colorStops);
_addColorStopsToCanvasGradient(gradient, colors, colorStops, tileMode == ui.TileMode.decal);
return gradient;
}

Expand Down Expand Up @@ -281,16 +281,28 @@ class GradientLinear extends EngineGradient {
}

void _addColorStopsToCanvasGradient(html.CanvasGradient gradient,
List<ui.Color> colors, List<double>? colorStops) {
List<ui.Color> colors, List<double>? colorStops, bool isDecal) {
double scale, offset;
if (isDecal) {
scale = 0.999;
offset = (1.0 - scale) / 2.0;
gradient.addColorStop(0, '#00000000');
} else {
scale = 1.0;
offset = 0.0;
}
if (colorStops == null) {
assert(colors.length == 2);
gradient.addColorStop(0, colorToCssString(colors[0])!);
gradient.addColorStop(1, colorToCssString(colors[1])!);
gradient.addColorStop(offset, colorToCssString(colors[0])!);
gradient.addColorStop(1 - offset, colorToCssString(colors[1])!);
} else {
for (int i = 0; i < colors.length; i++) {
gradient.addColorStop(colorStops[i], colorToCssString(colors[i])!);
gradient.addColorStop(colorStops[i] * scale + offset, colorToCssString(colors[i])!);
}
}
if (isDecal) {
gradient.addColorStop(1, '#00000000');
}
}

/// Writes shader code to map fragment value to gradient color.
Expand All @@ -311,11 +323,12 @@ String _writeSharedGradientShader(ShaderBuilder builder,
builder.addUniform(ShaderType.kVec4, name: 'scale_$i');
}

// Use st variable name if clamped, otherwise write code to comnpute
// Use st variable name if clamped or decaled, otherwise write code to compute
// tiled_st.
String probeName = 'st';
switch (tileMode) {
case ui.TileMode.clamp:
case ui.TileMode.decal:
break;
case ui.TileMode.repeated:
method.addStatement('float tiled_st = fract(st);');
Expand Down Expand Up @@ -349,7 +362,7 @@ class GradientRadial extends EngineGradient {
@override
Object createPaintStyle(html.CanvasRenderingContext2D? ctx,
ui.Rect? shaderBounds, double density) {
if (tileMode == ui.TileMode.clamp) {
if (tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal) {
return _createCanvasGradient(ctx, shaderBounds, density);
} else {
initWebGl();
Expand All @@ -364,7 +377,7 @@ class GradientRadial extends EngineGradient {
final html.CanvasGradient gradient = ctx!.createRadialGradient(
center.dx - offsetX, center.dy - offsetY, 0,
center.dx - offsetX, center.dy - offsetY, radius);
_addColorStopsToCanvasGradient(gradient, colors, colorStops);
_addColorStopsToCanvasGradient(gradient, colors, colorStops, tileMode == ui.TileMode.decal);
return gradient;
}

Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,9 @@ enum FilterQuality {
}

class ImageFilter {
factory ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) {
factory ImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0, TileMode tileMode = TileMode.clamp}) {
if (engine.useCanvasKit) {
return engine.CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY);
return engine.CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode);
}
return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY);
}
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/ui/tile_mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ enum TileMode {
clamp,
repeated,
mirror,
decal,
}
15 changes: 8 additions & 7 deletions testing/dart/image_filter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ void main() {
return bytes.buffer.asUint32List();
}

ImageFilter makeBlur(double sigmaX, double sigmaY) =>
ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY);
ImageFilter makeBlur(double sigmaX, double sigmaY, [TileMode tileMode = TileMode.clamp]) =>
ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode);

ImageFilter makeScale(double scX, double scY,
[double trX = 0.0, double trY = 0.0,
Expand Down Expand Up @@ -105,6 +105,7 @@ void main() {
List<ImageFilter> makeList() {
return <ImageFilter>[
makeBlur(10.0, 10.0),
makeBlur(10.0, 10.0, TileMode.decal),
makeBlur(10.0, 20.0),
makeBlur(20.0, 20.0),
makeScale(10.0, 10.0),
Expand Down Expand Up @@ -272,14 +273,14 @@ void main() {

test('Composite ImageFilter toString', () {
expect(
ImageFilter.compose(outer: makeBlur(20.0, 20.0), inner: makeBlur(10.0, 10.0)).toString(),
contains('blur(10.0, 10.0) -> blur(20.0, 20.0)'),
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)'),
);

// Produces a flat list of filters
expect(
ImageFilter.compose(
outer: ImageFilter.compose(outer: makeBlur(30.0, 30.0), inner: makeBlur(20.0, 20.0)),
outer: ImageFilter.compose(outer: makeBlur(30.0, 30.0, TileMode.mirror), inner: makeBlur(20.0, 20.0, TileMode.repeated)),
inner: ImageFilter.compose(
outer: const ColorFilter.mode(null, null),
inner: makeScale(10.0, 10.0),
Expand All @@ -288,8 +289,8 @@ void main() {
contains(
'matrix([10.0, 0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -0.0, -0.0, 0.0, 1.0], FilterQuality.low) -> '
'ColorFilter.mode(null, null) -> '
'blur(20.0, 20.0) -> '
'blur(30.0, 30.0)'
'blur(20.0, 20.0, repeated) -> '
'blur(30.0, 30.0, mirror)'
),
);
});
Expand Down