[web] Fix rendering of gradients in html mode (#40345)

<details>
<summary> Code Example</summary>
```dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class DemoGradientTransform implements GradientTransform {
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
return Matrix4.identity()
..scale(1.2, 1.7)
..rotateZ(0.25);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
var colors = <Color>[
Colors.red,
Colors.green,
Colors.blue,
Colors.yellow,
];
const stops = <double>[0.0, 0.25, 0.5, 1.0];
return MaterialApp(
debugShowCheckedModeBanner: false,
home: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: TileMode.values.length,
),
children: <Widget>[
for (final mode in TileMode.values)
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: colors,
stops: stops,
tileMode: mode,
transform: DemoGradientTransform(),
),
),
),
for (final mode in TileMode.values)
DecoratedBox(
decoration: BoxDecoration(
gradient: RadialGradient(
center: Alignment.topLeft,
radius: 0.5,
colors: colors,
stops: stops,
tileMode: mode,
transform: DemoGradientTransform(),
),
),
),
for (final mode in TileMode.values)
DecoratedBox(
decoration: BoxDecoration(
gradient: SweepGradient(
center: Alignment.topLeft,
startAngle: 0.0,
endAngle: 3.14,
colors: colors,
stops: stops,
tileMode: mode,
transform: DemoGradientTransform(),
),
),
),
],
),
);
}
}
```
</details>
Fixes: https://github.com/flutter/flutter/issues/84245diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart
index 9a8fb3a..8faa066 100644
--- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart
+++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart
@@ -98,13 +98,23 @@
final double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width);
final double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height);
gl.setUniform2f(tileOffset, 2 * (shaderBounds.width * (centerX - 0.5)),
- 2 * (shaderBounds.height * (centerY - 0.5)));
+ 2 * (shaderBounds.height * (0.5 - centerY)));
final Object angleRange = gl.getUniformLocation(glProgram.program, 'angle_range');
gl.setUniform2f(angleRange, startAngle, endAngle);
normalizedGradient.setupUniforms(gl, glProgram);
+
final Object gradientMatrix =
gl.getUniformLocation(glProgram.program, 'm_gradient');
- gl.setUniformMatrix4fv(gradientMatrix, false, matrix4 ?? Matrix4.identity().storage);
+ final Matrix4 gradientTransform = Matrix4.identity();
+ if (matrix4 != null) {
+ final Matrix4 m4 = Matrix4.zero()
+ ..copyInverse(Matrix4.fromFloat32List(matrix4!));
+ gradientTransform.translate(-center.dx, -center.dy);
+ gradientTransform.multiply(m4);
+ gradientTransform.translate(center.dx, center.dy);
+ }
+ gl.setUniformMatrix4fv(gradientMatrix, false, gradientTransform.storage);
+
final Object result = () {
if (createDataUrl) {
return glRenderer!.drawRectToImageUrl(
@@ -149,7 +159,7 @@
// Sweep gradient
method.addStatement('vec2 center = 0.5 * (u_resolution + u_tile_offset);');
method.addStatement(
- 'vec4 localCoord = vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1) * m_gradient;');
+ 'vec4 localCoord = m_gradient * vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1);');
method.addStatement(
'float angle = atan(-localCoord.y, -localCoord.x) + ${math.pi};');
method.addStatement('float sweep = angle_range.y - angle_range.x;');
@@ -317,14 +327,12 @@
// with flipped y axis.
// We flip y axis, translate to center, multiply matrix and translate
// and flip back so it is applied correctly.
- final Matrix4 m4 = Matrix4.fromFloat32List(matrix4!.matrix);
- gradientTransform.scale(1, -1);
- gradientTransform.translate(
- -shaderBounds.center.dx, -shaderBounds.center.dy);
+ final Matrix4 m4 = Matrix4.zero()
+ ..copyInverse(Matrix4.fromFloat32List(matrix4!.matrix));
+ final ui.Offset center = shaderBounds.center;
+ gradientTransform.translate(-center.dx, -center.dy);
gradientTransform.multiply(m4);
- gradientTransform.translate(
- shaderBounds.center.dx, shaderBounds.center.dy);
- gradientTransform.scale(1, -1);
+ gradientTransform.translate(center.dx, center.dy);
}
gradientTransform.multiply(rotationZ);
@@ -465,6 +473,12 @@
sourcePrefix: 'threshold',
biasName: 'bias',
scaleName: 'scale');
+ if (tileMode == ui.TileMode.decal) {
+ method.addStatement('if (st < 0.0 || st > 1.0) {');
+ method.addStatement(' ${builder.fragmentColor.name} = vec4(0, 0, 0, 0);');
+ method.addStatement(' return;');
+ method.addStatement('}');
+ }
return probeName;
}
@@ -483,7 +497,7 @@
@override
Object createPaintStyle(DomCanvasRenderingContext2D? ctx,
ui.Rect? shaderBounds, double density) {
- if (tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal) {
+ if (matrix4 == null && (tileMode == ui.TileMode.clamp || tileMode == ui.TileMode.decal)) {
return _createCanvasGradient(ctx, shaderBounds, density);
} else {
return _createGlGradient(ctx, shaderBounds, density);
@@ -533,15 +547,24 @@
final double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width);
final double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height);
gl.setUniform2f(tileOffset, 2 * (shaderBounds.width * (centerX - 0.5)),
- 2 * (shaderBounds.height * (centerY - 0.5)));
+ 2 * (shaderBounds.height * (0.5 - centerY)));
final Object radiusUniform = gl.getUniformLocation(glProgram.program, 'u_radius');
gl.setUniform1f(radiusUniform, radius);
normalizedGradient.setupUniforms(gl, glProgram);
final Object gradientMatrix =
gl.getUniformLocation(glProgram.program, 'm_gradient');
- gl.setUniformMatrix4fv(gradientMatrix, false,
- matrix4 == null ? Matrix4.identity().storage : matrix4!);
+
+ final Matrix4 gradientTransform = Matrix4.identity();
+
+ if (matrix4 != null) {
+ final Matrix4 m4 = Matrix4.zero()
+ ..copyInverse(Matrix4.fromFloat32List(matrix4!));
+ gradientTransform.translate(-center.dx, -center.dy);
+ gradientTransform.multiply(m4);
+ gradientTransform.translate(center.dx, center.dy);
+ }
+ gl.setUniformMatrix4fv(gradientMatrix, false, gradientTransform.storage);
final Object result = () {
if (createDataUrl) {
@@ -587,7 +610,7 @@
// Sweep gradient
method.addStatement('vec2 center = 0.5 * (u_resolution + u_tile_offset);');
method.addStatement(
- 'vec4 localCoord = vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1) * m_gradient;');
+ 'vec4 localCoord = m_gradient * vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1);');
method.addStatement('float dist = length(localCoord);');
method.addStatement(
'float st = abs(dist / u_radius);');
@@ -666,7 +689,7 @@
// Sweep gradient
method.addStatement('vec2 center = 0.5 * (u_resolution + u_tile_offset);');
method.addStatement(
- 'vec4 localCoord = vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1) * m_gradient;');
+ 'vec4 localCoord = m_gradient * vec4(gl_FragCoord.x - center.x, center.y - gl_FragCoord.y, 0, 1);');
method.addStatement('float dist = length(localCoord);');
final String f = (focalRadius /
(math.min(shaderBounds.width, shaderBounds.height) / 2.0))
diff --git a/lib/web_ui/test/html/shaders/gradient_golden_test.dart b/lib/web_ui/test/html/shaders/gradient_golden_test.dart
index 8dfa8fc..e56de2d 100644
--- a/lib/web_ui/test/html/shaders/gradient_golden_test.dart
+++ b/lib/web_ui/test/html/shaders/gradient_golden_test.dart
@@ -352,7 +352,7 @@
RenderStrategy());
canvas.endRecording();
canvas.apply(engineCanvas, screenRect);
- });
+ }, skip: isFirefox);
test("Creating lots of gradients doesn't create too many webgl contexts",
() async {
@@ -431,7 +431,7 @@
canvas.restore();
await canvasScreenshot(canvas, 'linear_gradient_rect_clamp_rotated', canvasRect: screenRect, region: region);
- });
+ }, skip: isFirefox);
test('Paints linear gradient properly when within svg context', () async {
final RecordingCanvas canvas =
@@ -465,7 +465,176 @@
canvas.restore();
await canvasScreenshot(canvas, 'linear_gradient_in_svg_context', canvasRect: screenRect, region: region);
- });
+ }, skip: isFirefox);
+
+ test('Paints transformed linear gradient', () async {
+ final RecordingCanvas canvas =
+ RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
+ canvas.save();
+
+ const List<Color> colors = <Color>[
+ Color(0xFF000000),
+ Color(0xFFFF3C38),
+ Color(0xFFFF8C42),
+ Color(0xFFFFF275),
+ Color(0xFF6699CC),
+ Color(0xFF656D78),
+ ];
+
+ const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
+
+ final Matrix4 transform = Matrix4.identity()
+ ..translate(50, 50)
+ ..scale(0.3, 0.7)
+ ..rotateZ(0.5);
+
+ final GradientLinear linearGradient = GradientLinear(
+ const Offset(5, 5),
+ const Offset(200, 130),
+ colors,
+ stops,
+ TileMode.clamp,
+ transform.storage,
+ );
+
+ const double kBoxWidth = 150;
+ const double kBoxHeight = 80;
+
+ Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
+ canvas.drawRect(
+ rectBounds,
+ SurfacePaint()
+ ..shader = engineLinearGradientToShader(linearGradient, rectBounds),
+ );
+
+ rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
+ canvas.drawOval(
+ rectBounds,
+ SurfacePaint()
+ ..shader = engineLinearGradientToShader(linearGradient, rectBounds),
+ );
+
+ canvas.restore();
+ await canvasScreenshot(
+ canvas,
+ 'linear_gradient_clamp_transformed',
+ canvasRect: screenRect,
+ region: region,
+ );
+ }, skip: isFirefox);
+
+ test('Paints transformed sweep gradient', () async {
+ final RecordingCanvas canvas =
+ RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
+ canvas.save();
+
+ const List<Color> colors = <Color>[
+ Color(0xFF000000),
+ Color(0xFFFF3C38),
+ Color(0xFFFF8C42),
+ Color(0xFFFFF275),
+ Color(0xFF6699CC),
+ Color(0xFF656D78),
+ ];
+
+ const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
+
+ final Matrix4 transform = Matrix4.identity()
+ ..translate(100, 150)
+ ..scale(0.3, 0.7)
+ ..rotateZ(0.5);
+
+ final GradientSweep sweepGradient = GradientSweep(
+ const Offset(0.5, 0.5),
+ colors,
+ stops,
+ TileMode.clamp,
+ 0.0,
+ 2 * math.pi,
+ transform.storage,
+ );
+
+ const double kBoxWidth = 150;
+ const double kBoxHeight = 80;
+
+ Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
+ canvas.drawRect(
+ rectBounds,
+ SurfacePaint()
+ ..shader = engineGradientToShader(sweepGradient, rectBounds),
+ );
+
+ rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
+ canvas.drawOval(
+ rectBounds,
+ SurfacePaint()
+ ..shader = engineGradientToShader(sweepGradient, rectBounds),
+ );
+
+ canvas.restore();
+ await canvasScreenshot(
+ canvas,
+ 'sweep_gradient_clamp_transformed',
+ canvasRect: screenRect,
+ region: region,
+ );
+ }, skip: isFirefox);
+
+ test('Paints transformed radial gradient', () async {
+ final RecordingCanvas canvas =
+ RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
+ canvas.save();
+
+ const List<Color> colors = <Color>[
+ Color(0xFF000000),
+ Color(0xFFFF3C38),
+ Color(0xFFFF8C42),
+ Color(0xFFFFF275),
+ Color(0xFF6699CC),
+ Color(0xFF656D78),
+ ];
+
+ const List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0];
+
+ final Matrix4 transform = Matrix4.identity()
+ ..translate(50, 50)
+ ..scale(0.3, 0.7)
+ ..rotateZ(0.5);
+
+ final GradientRadial radialGradient = GradientRadial(
+ const Offset(0.5, 0.5),
+ 400,
+ colors,
+ stops,
+ TileMode.clamp,
+ transform.storage,
+ );
+
+ const double kBoxWidth = 150;
+ const double kBoxHeight = 80;
+
+ Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight);
+ canvas.drawRect(
+ rectBounds,
+ SurfacePaint()
+ ..shader = engineRadialGradientToShader(radialGradient, rectBounds),
+ );
+
+ rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight);
+ canvas.drawOval(
+ rectBounds,
+ SurfacePaint()
+ ..shader = engineRadialGradientToShader(radialGradient, rectBounds),
+ );
+
+ canvas.restore();
+ await canvasScreenshot(
+ canvas,
+ 'radial_gradient_clamp_transformed',
+ canvasRect: screenRect,
+ region: region,
+ );
+ }, skip: isFirefox);
}
Shader engineGradientToShader(GradientSweep gradient, Rect rect) {
@@ -488,6 +657,18 @@
);
}
+Shader engineRadialGradientToShader(GradientRadial gradient, Rect rect) {
+ return Gradient.radial(
+ Offset(rect.left + gradient.center.dx * rect.width,
+ rect.top + gradient.center.dy * rect.height),
+ gradient.radius,
+ gradient.colors,
+ gradient.colorStops,
+ gradient.tileMode,
+ gradient.matrix4 == null ? null : Float64List.fromList(gradient.matrix4!),
+ );
+}
+
Path samplePathFromRect(Rect rectBounds) =>
Path()
..moveTo(rectBounds.center.dx, rectBounds.top)
diff --git a/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart b/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart
index bf1ab37..433d697 100644
--- a/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart
+++ b/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart
@@ -84,7 +84,7 @@
}
expect(rc.renderStrategy.hasArbitraryPaint, isTrue);
await canvasScreenshot(rc, 'linear_gradient_oval_matrix');
- });
+ }, skip: isFirefox);
// Regression test for https://github.com/flutter/flutter/issues/50010
test('Should draw linear gradient using rounded rect.', () async {