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 all 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
40 changes: 39 additions & 1 deletion lib/web_ui/lib/src/engine/canvaskit/image_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:typed_data';

import 'package:ui/ui.dart' as ui;

import 'canvaskit_api.dart';
import 'color_filter.dart';
import 'skia_object_cache.dart';
import '../util.dart';

/// An [ImageFilter] that can create a managed skia [SkImageFilter] object.
///
Expand All @@ -22,7 +25,7 @@ abstract class CkManagedSkImageFilterConvertible<T extends Object>

/// The CanvasKit implementation of [ui.ImageFilter].
///
/// Currently only supports `blur`.
/// Currently only supports `blur`, `matrix`, and ColorFilters.
abstract class CkImageFilter extends ManagedSkiaObject<SkImageFilter>
implements CkManagedSkImageFilterConvertible<SkImageFilter> {
factory CkImageFilter.blur(
Expand All @@ -31,6 +34,9 @@ abstract class CkImageFilter extends ManagedSkiaObject<SkImageFilter>
required ui.TileMode tileMode}) = _CkBlurImageFilter;
factory CkImageFilter.color({required CkColorFilter colorFilter}) =
CkColorFilterImageFilter;
factory CkImageFilter.matrix(
{required Float64List matrix,
required ui.FilterQuality filterQuality}) = _CkMatrixImageFilter;

CkImageFilter._();

Expand Down Expand Up @@ -126,3 +132,35 @@ class _CkBlurImageFilter extends CkImageFilter {
return 'ImageFilter.blur($sigmaX, $sigmaY, $_modeString)';
}
}

class _CkMatrixImageFilter extends CkImageFilter {
_CkMatrixImageFilter({ required Float64List matrix, required this.filterQuality })
: this.matrix = Float64List.fromList(matrix),
super._();

final Float64List matrix;
final ui.FilterQuality filterQuality;

SkImageFilter _initSkiaObject() {
return canvasKit.ImageFilter.MakeMatrixTransform(
toSkMatrixFromFloat64(matrix),
toSkFilterQuality(filterQuality),
null,
);
}

@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is _CkMatrixImageFilter
&& other.filterQuality == filterQuality
&& listEquals<double>(other.matrix, matrix);
}

@override
int get hashCode => ui.hashValues(filterQuality, ui.hashList(matrix));

@override
String toString() => 'ImageFilter.matrix($matrix, $filterQuality)';
}
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/html/backdrop_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ class PersistedBackdropFilter extends PersistedContainerSurface
// the blur will fall within 2 * sigma pixels.
if (browserEngine == BrowserEngine.webkit) {
DomRenderer.setElementStyle(_filterElement!, '-webkit-backdrop-filter',
_imageFilterToCss(filter));
filter.filterAttribute);
}
DomRenderer.setElementStyle(_filterElement!, 'backdrop-filter', _imageFilterToCss(filter));
DomRenderer.setElementStyle(_filterElement!, 'backdrop-filter', filter.filterAttribute);
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/web_ui/lib/src/engine/html/image_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class PersistedImageFilter extends PersistedContainerSurface

@override
void apply() {
rootElement!.style.filter = _imageFilterToCss(filter as EngineImageFilter);
rootElement!.style.filter = (filter as EngineImageFilter).filterAttribute;
rootElement!.style.transform = (filter as EngineImageFilter).transformAttribute;
}

@override
Expand Down
7 changes: 0 additions & 7 deletions lib/web_ui/lib/src/engine/html/scene_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -582,10 +582,3 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
throw UnimplementedError();
}
}

// HTML only supports a single radius, but Flutter ImageFilter supports separate
// horizontal and vertical radii. The best approximation we can provide is to
// average the two radii together for a single compromise value.
String _imageFilterToCss(EngineImageFilter filter) {
return 'blur(${(filter.sigmaX + filter.sigmaY) / 2}px)';
}
66 changes: 60 additions & 6 deletions lib/web_ui/lib/src/engine/html/shaders/shader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -660,25 +660,79 @@ class GradientConical extends GradientRadial {

/// Backend implementation of [ui.ImageFilter].
///
/// Currently only `blur` is supported.
class EngineImageFilter implements ui.ImageFilter {
EngineImageFilter.blur({this.sigmaX = 0.0, this.sigmaY = 0.0});
/// Currently only `blur` and `matrix` are supported.
abstract class EngineImageFilter implements ui.ImageFilter {
factory EngineImageFilter.blur({
required double sigmaX,
required double sigmaY,
required ui.TileMode tileMode,
}) = _BlurEngineImageFilter;

factory EngineImageFilter.matrix({
required Float64List matrix,
required ui.FilterQuality filterQuality,
}) = _MatrixEngineImageFilter;

EngineImageFilter._();

String get filterAttribute => '';
String get transformAttribute => '';
}

class _BlurEngineImageFilter extends EngineImageFilter {
_BlurEngineImageFilter({ this.sigmaX = 0.0, this.sigmaY = 0.0, this.tileMode = ui.TileMode.clamp }) : super._();

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

// TODO(flutter_web): implement TileMode.
String get filterAttribute => blurSigmasToCssString(sigmaX, sigmaY);

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

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

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

class _MatrixEngineImageFilter extends EngineImageFilter {
_MatrixEngineImageFilter({ required Float64List matrix, required this.filterQuality })
: webMatrix = Float64List.fromList(matrix),
super._();

final Float64List webMatrix;
final ui.FilterQuality filterQuality;

// TODO(flutter_web): implement FilterQuality.
String get transformAttribute => float64ListToCssTransform(webMatrix);

@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is _MatrixEngineImageFilter
&& other.filterQuality == filterQuality
&& listEquals<double>(other.webMatrix, webMatrix);
}

@override
int get hashCode => ui.hashValues(ui.hashList(webMatrix), filterQuality);

@override
String toString() {
return 'ImageFilter.blur($sigmaX, $sigmaY)';
return 'ImageFilter.matrix($webMatrix, $filterQuality)';
}
}
19 changes: 13 additions & 6 deletions lib/web_ui/lib/src/engine/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ void setElementTransform(html.Element element, Float32List matrix4) {
/// See also:
/// * https://github.com/flutter/flutter/issues/32274
/// * https://bugs.chromium.org/p/chromium/issues/detail?id=1040222
String float64ListToCssTransform(Float32List matrix) {
String float64ListToCssTransform(List<double> matrix) {
assert(matrix.length == 16);
final TransformKind transformKind = transformKindOf(matrix);
if (transformKind == TransformKind.transform2d) {
Expand Down Expand Up @@ -113,9 +113,9 @@ enum TransformKind {
}

/// Detects the kind of transform the [matrix] performs.
TransformKind transformKindOf(Float32List matrix) {
TransformKind transformKindOf(List<double> matrix) {
assert(matrix.length == 16);
final Float32List m = matrix;
final List<double> m = matrix;

// If matrix contains scaling, rotation, z translation or
// perspective transform, it is not considered simple.
Expand Down Expand Up @@ -171,15 +171,15 @@ bool isIdentityFloat32ListTransform(Float32List matrix) {
/// permitted. However, it is inefficient to construct a matrix for an identity
/// transform. Consider removing the CSS `transform` property from elements
/// that apply identity transform.
String float64ListToCssTransform2d(Float32List matrix) {
String float64ListToCssTransform2d(List<double> matrix) {
assert(transformKindOf(matrix) != TransformKind.complex);
return 'matrix(${matrix[0]},${matrix[1]},${matrix[4]},${matrix[5]},${matrix[12]},${matrix[13]})';
}

/// Converts [matrix] to a 3D CSS transform value.
String float64ListToCssTransform3d(Float32List matrix) {
String float64ListToCssTransform3d(List<double> matrix) {
assert(matrix.length == 16);
final Float32List m = matrix;
final List<double> m = matrix;
if (m[0] == 1.0 &&
m[1] == 0.0 &&
m[2] == 0.0 &&
Expand Down Expand Up @@ -552,3 +552,10 @@ bool listEquals<T>(List<T>? a, List<T>? b) {
}
return true;
}

// HTML only supports a single radius, but Flutter ImageFilter supports separate
// horizontal and vertical radii. The best approximation we can provide is to
// average the two radii together for a single compromise value.
String blurSigmasToCssString(double sigmaX, double sigmaY) {
return 'blur(${(sigmaX + sigmaY) * 0.5}px)';
}
16 changes: 10 additions & 6 deletions lib/web_ui/lib/src/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,18 @@ class ImageFilter {
if (engine.useCanvasKit) {
return engine.CkImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode);
}
return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY);
// TODO(flutter_web): implement TileMode.
return engine.EngineImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode);
}

ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) {
// TODO(flutter_web): add implementation.
throw UnimplementedError('ImageFilter.matrix not implemented for web platform.');
// if (matrix4.length != 16)
// throw ArgumentError('"matrix4" must have 16 entries.');
factory ImageFilter.matrix(Float64List matrix4, {FilterQuality filterQuality = FilterQuality.low}) {
if (matrix4.length != 16)
throw ArgumentError('"matrix4" must have 16 entries.');
if (engine.useCanvasKit) {
return engine.CkImageFilter.matrix(matrix: matrix4, filterQuality: filterQuality);
}
// TODO(flutter_web): implement FilterQuality.
return engine.EngineImageFilter.matrix(matrix: matrix4, filterQuality: filterQuality);
}

ImageFilter.compose({required ImageFilter outer, required ImageFilter inner}) {
Expand Down
79 changes: 79 additions & 0 deletions lib/web_ui/test/engine/surface/filters/image_filter_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:typed_data';

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
group('ImageFilter constructors', () {
test('matrix is copied', () {
Matrix4 matrix = Matrix4.identity();
Float64List storage = matrix.toFloat64();
ImageFilter filter1 = ImageFilter.matrix(storage);
storage[0] = 2.0;
ImageFilter filter2 = ImageFilter.matrix(storage);
expect(filter1, filter1);
expect(filter2, filter2);
expect(filter1, isNot(equals(filter2)));
expect(filter2, isNot(equals(filter1)));
});

test('matrix tests all values on ==', () {
Matrix4 matrix = Matrix4.identity();
Float64List storage = matrix.toFloat64();
ImageFilter filter1a = ImageFilter.matrix(storage, filterQuality: FilterQuality.none);
ImageFilter filter1b = ImageFilter.matrix(storage, filterQuality: FilterQuality.high);

storage[0] = 2.0;
ImageFilter filter2a = ImageFilter.matrix(storage, filterQuality: FilterQuality.none);
ImageFilter filter2b = ImageFilter.matrix(storage, filterQuality: FilterQuality.high);

expect(filter1a, filter1a);
expect(filter1a, isNot(equals(filter1b)));
expect(filter1a, isNot(equals(filter2a)));
expect(filter1a, isNot(equals(filter2b)));

expect(filter1b, isNot(equals(filter1a)));
expect(filter1b, filter1b);
expect(filter1b, isNot(equals(filter2a)));
expect(filter1b, isNot(equals(filter2b)));

expect(filter2a, isNot(equals(filter1a)));
expect(filter2a, isNot(equals(filter1b)));
expect(filter2a, filter2a);
expect(filter2a, isNot(equals(filter2b)));

expect(filter2b, isNot(equals(filter1a)));
expect(filter2b, isNot(equals(filter1b)));
expect(filter2b, isNot(equals(filter2a)));
expect(filter2b, filter2b);
});

test('blur tests all values on ==', () {
ImageFilter filter1 = ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0, tileMode: TileMode.decal);
ImageFilter filter2 = ImageFilter.blur(sigmaX: 2.0, sigmaY: 3.0, tileMode: TileMode.decal);
ImageFilter filter3 = ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0, tileMode: TileMode.mirror);

expect(filter1, filter1);
expect(filter1, isNot(equals(filter2)));
expect(filter1, isNot(equals(filter3)));

expect(filter2, isNot(equals(filter1)));
expect(filter2, filter2);
expect(filter2, isNot(equals(filter3)));

expect(filter3, isNot(equals(filter1)));
expect(filter3, isNot(equals(filter2)));
expect(filter3, filter3);
});
});
}
21 changes: 20 additions & 1 deletion lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ void testMain() async {
viewElement6.remove();
});

test('pushImageFilter', () async {
test('pushImageFilter blur', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushImageFilter(
ImageFilter.blur(sigmaX: 1, sigmaY: 3),
Expand All @@ -374,6 +374,25 @@ void testMain() async {
await matchGoldenFile('compositing_image_filter.png', region: region);
});

test('pushImageFilter matrix', () async {
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
builder.pushImageFilter(
ImageFilter.matrix(
(
Matrix4.identity()
..translate(40, 10)
..rotateZ(math.pi / 6)
..scale(0.75, 0.75)
).toFloat64()),
);
_drawTestPicture(builder);
builder.pop();

html.document.body.append(builder.build().webOnlyRootElement);

await matchGoldenFile('compositing_image_filter_matrix.png', region: region);
});

group('Cull rect computation', () {
_testCullRectComputation();
});
Expand Down