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

![CleanShot 2023-03-16 at 20 44 01@2x](https://user-images.githubusercontent.com/15033141/225620947-18fe19aa-c5e2-45a5-a0cc-151275844af7.png)

<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/84245
diff --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 {