From 0c708f040fa1964cc588e00075e147017fdf1f65 Mon Sep 17 00:00:00 2001 From: xuty Date: Thu, 10 Aug 2023 05:38:59 +0800 Subject: [PATCH] [web] Fix rendering of gradients in html mode (flutter/engine#40345) ![CleanShot 2023-03-16 at 20 44 01@2x](https://user-images.githubusercontent.com/15033141/225620947-18fe19aa-c5e2-45a5-a0cc-151275844af7.png)
Code Example ```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 = [ Colors.red, Colors.green, Colors.blue, Colors.yellow, ]; const stops = [0.0, 0.25, 0.5, 1.0]; return MaterialApp( debugShowCheckedModeBanner: false, home: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: TileMode.values.length, ), children: [ 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(), ), ), ), ], ), ); } } ```
Fixes: https://github.com/flutter/flutter/issues/84245 --- .../lib/src/engine/html/shaders/shader.dart | 55 ++++-- .../html/shaders/gradient_golden_test.dart | 187 +++++++++++++++++- .../shaders/linear_gradient_golden_test.dart | 2 +- 3 files changed, 224 insertions(+), 20 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart index 9a8fb3ac98..8faa06617c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -98,13 +98,23 @@ class GradientSweep extends EngineGradient { 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 @@ class GradientSweep extends EngineGradient { // 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 @@ class GradientLinear extends EngineGradient { // 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 @@ String _writeSharedGradientShader(ShaderBuilder builder, ShaderMethod method, 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 @@ class GradientRadial extends EngineGradient { @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 @@ class GradientRadial extends EngineGradient { 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 @@ class GradientRadial extends EngineGradient { // 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 @@ class GradientConical extends GradientRadial { // 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/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart b/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart index 8dfa8fc665..e56de2d87b 100644 --- a/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart @@ -352,7 +352,7 @@ Future testMain() async { 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 @@ Future testMain() async { 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 @@ Future testMain() async { 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 colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78), + ]; + + const List stops = [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 colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78), + ]; + + const List stops = [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 colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78), + ]; + + const List stops = [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 engineLinearGradientToShader(GradientLinear gradient, Rect rect) { ); } +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/engine/src/flutter/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart b/engine/src/flutter/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart index bf1ab3752c..433d697348 100644 --- a/engine/src/flutter/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/html/shaders/linear_gradient_golden_test.dart @@ -84,7 +84,7 @@ Future testMain() async { } 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 {