From fec9b64aa03416cbac8671affe6388703e132697 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 9 Feb 2022 10:36:02 -0800 Subject: [PATCH 1/4] https://github.com/flutter/flutter/issues/97762 There are actually three separate issues here to fix: 1) UV coordinates for the repeated gradient are messed up. The reason for this is that there is a transformation to the gradient matrix that we were for some reason skipping when the tile mode was "repeated". We need to apply that transformation in the "repeated" tile mode as well. 2) Coloration of some gradients is wrong when using a color with an alpha channel. The reason this is occurring is because the transferToImageBitmap doesn't properly preserve the alpha channel. The fix is to use the fallback path if the gradient contains a non-opaque color. 3) Sweep gradient wasn't drawing properly at all. This is because if there was no transformation matrix passed into the shader, we didn't set the gradient matrix uniform at all. We should set it to the identity matrix if there is none specified. --- lib/web_ui/lib/src/engine/html/render_vertices.dart | 2 +- .../lib/src/engine/html/shaders/image_shader.dart | 2 +- .../src/engine/html/shaders/normalized_gradient.dart | 6 ++++-- lib/web_ui/lib/src/engine/html/shaders/shader.dart | 10 +++------- lib/web_ui/lib/src/engine/safe_browser_api.dart | 8 +++++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/web_ui/lib/src/engine/html/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart index 73b612d84c23d..3ef24fc1b6938 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -317,7 +317,7 @@ class _WebGlRenderer implements GlRenderer { NormalizedGradient gradient, int widthInPixels, int heightInPixels) { drawRectToGl( targetRect, gl, glProgram, gradient, widthInPixels, heightInPixels); - final Object? image = gl.readPatternData(); + final Object? image = gl.readPatternData(gradient.isOpaque); gl.bindArrayBuffer(null); gl.bindElementArrayBuffer(null); return image; diff --git a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart index 86aeb0a3e92f0..a884fcd3d704c 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart @@ -260,7 +260,7 @@ class EngineImageShader implements ui.ImageShader { gl.unbindVertexArray(); } - final Object? bitmapImage = gl.readPatternData(); + final Object? bitmapImage = gl.readPatternData(false); gl.bindArrayBuffer(null); gl.bindElementArrayBuffer(null); return context!.createPattern(bitmapImage!, 'no-repeat')!; diff --git a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart index 6cba78f11d6a7..72b8301120165 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart @@ -26,6 +26,7 @@ class NormalizedGradient { final Float32List _bias; final Float32List _scale; final int thresholdCount; + final bool isOpaque; factory NormalizedGradient(List colors, {List? stops}) { // If colorStops is not provided, then only two stops, at 0.0 and 1.0, @@ -34,6 +35,7 @@ class NormalizedGradient { stops ??= const [0.0, 1.0]; final int colorCount = colors.length; int normalizedCount = colorCount; + bool isOpaque = !colors.any((c) => c.alpha < 1.0); final bool addFirst = stops[0] != 0.0; final bool addLast = stops.last != 1.0; if (addFirst) { @@ -94,11 +96,11 @@ class NormalizedGradient { bias[colorIndex + 2] -= t * scale[colorIndex + 2]; bias[colorIndex + 3] -= t * scale[colorIndex + 3]; } - return NormalizedGradient._(normalizedCount, thresholds, scale, bias); + return NormalizedGradient._(normalizedCount, thresholds, scale, bias, isOpaque); } NormalizedGradient._( - this.thresholdCount, this._thresholds, this._scale, this._bias); + this.thresholdCount, this._thresholds, this._scale, this._bias, this.isOpaque); /// Sets uniforms for threshold, bias and scale for program. void setupUniforms(GlContext gl, GlProgram glProgram) { 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 0ae291470b0f1..c80cdfde046b7 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -79,11 +79,9 @@ class GradientSweep extends EngineGradient { final Object angleRange = gl.getUniformLocation(glProgram.program, 'angle_range'); gl.setUniform2f(angleRange, startAngle, endAngle); normalizedGradient.setupUniforms(gl, glProgram); - if (matrix4 != null) { - final Object gradientMatrix = + final Object gradientMatrix = gl.getUniformLocation(glProgram.program, 'm_gradient'); - gl.setUniformMatrix4fv(gradientMatrix, false, matrix4!); - } + gl.setUniformMatrix4fv(gradientMatrix, false, matrix4 ?? Matrix4.identity().storage); if (createDataUrl) { return glRenderer!.drawRectToImageUrl( ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), @@ -293,9 +291,7 @@ class GradientLinear extends EngineGradient { // We compute location based on gl_FragCoord to center distance which // returns 0.0 at center. To make sure we align center of gradient to this // point, we shift by 0.5 to get st value for center of gradient. - if (tileMode != ui.TileMode.repeated) { - gradientTransform.translate(0.5, 0); - } + gradientTransform.translate(0.5, 0); if (length > kFltEpsilon) { gradientTransform.scale(1.0 / length); } diff --git a/lib/web_ui/lib/src/engine/safe_browser_api.dart b/lib/web_ui/lib/src/engine/safe_browser_api.dart index 498428df10025..448d555f384fe 100644 --- a/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -814,12 +814,14 @@ class GlContext { /// Returns image data in a form that can be used to create Canvas /// context patterns. - Object? readPatternData() { + Object? readPatternData(bool isOpaque) { // When using OffscreenCanvas and transferToImageBitmap is supported by // browser create ImageBitmap otherwise use more expensive canvas - // allocation. + // allocation. However, transferToImageBitmap does not properly preserve + // the alpha channel, so only use it if the pattern is opaque. if (_canvas != null && - js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { + js_util.hasProperty(_canvas!, 'transferToImageBitmap') && + isOpaque) { // TODO(yjbanov): find out why we need to call getContext and ignore the return value. js_util.callMethod(_canvas!, 'getContext', ['webgl2']); final Object? imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', From 985bee6fb86b87eb1a8b518d27cd3b7cd8972b67 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 9 Feb 2022 11:13:13 -0800 Subject: [PATCH 2/4] Fix issues from dart analyzer. --- lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart index 72b8301120165..138f207d2d058 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart @@ -35,7 +35,7 @@ class NormalizedGradient { stops ??= const [0.0, 1.0]; final int colorCount = colors.length; int normalizedCount = colorCount; - bool isOpaque = !colors.any((c) => c.alpha < 1.0); + final bool isOpaque = !colors.any((ui.Color c) => c.alpha < 1.0); final bool addFirst = stops[0] != 0.0; final bool addLast = stops.last != 1.0; if (addFirst) { From d936d5095c5054a8284cff9df1f682d139326add Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 9 Feb 2022 14:13:43 -0800 Subject: [PATCH 3/4] Change some of the tests to cover the default case with no rotation. --- .../html/shaders/gradient_golden_test.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 72a2c353bafad..599ae80754026 100644 --- a/lib/web_ui/test/html/shaders/gradient_golden_test.dart +++ b/lib/web_ui/test/html/shaders/gradient_golden_test.dart @@ -80,7 +80,7 @@ Future testMain() async { GradientSweep sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, 0, 360.0 / 180.0 * math.pi, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); final GradientSweep sweepGradientRotated = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, @@ -105,7 +105,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); rectBounds = rectBounds.translate(kBoxWidth + 10, 0); canvas.drawRect(rectBounds, @@ -117,7 +117,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.repeated, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); canvas.drawRect(rectBounds, SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); @@ -128,7 +128,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.mirror, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); canvas.drawRect(rectBounds, SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); canvas.drawRect(rectBounds, borderPaint); @@ -159,7 +159,7 @@ Future testMain() async { GradientSweep sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, 0, 360.0 / 180.0 * math.pi, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); final GradientSweep sweepGradientRotated = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, @@ -184,7 +184,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); rectBounds = rectBounds.translate(kBoxWidth + 10, 0); canvas.drawOval(rectBounds, @@ -196,7 +196,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.repeated, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); canvas.drawOval(rectBounds, SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); @@ -207,7 +207,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.mirror, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); canvas.drawOval(rectBounds, SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); canvas.drawRect(rectBounds, borderPaint); @@ -238,7 +238,7 @@ Future testMain() async { GradientSweep sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, 0, 360.0 / 180.0 * math.pi, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); final GradientSweep sweepGradientRotated = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, @@ -265,7 +265,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.clamp, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); rectBounds = rectBounds.translate(kBoxWidth + 10, 0); path = samplePathFromRect(rectBounds); @@ -278,7 +278,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.repeated, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); path = samplePathFromRect(rectBounds); canvas.drawPath(path, @@ -290,7 +290,7 @@ Future testMain() async { sweepGradient = GradientSweep(const Offset(0.5, 0.5), colors, stops, TileMode.mirror, math.pi / 6, 3 * math.pi / 4, - Matrix4.rotationZ(math.pi / 6.0).storage); + null); path = samplePathFromRect(rectBounds); canvas.drawPath(path, SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); From c217f27c05a991db660bea3508f8c7f55155ecb2 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 9 Feb 2022 15:22:40 -0800 Subject: [PATCH 4/4] Add new test that includes alpha blending of linear gradient. --- .../shaders/linear_gradient_golden_test.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 75ba08279356b..37837c8d96e9b 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 @@ -45,6 +45,27 @@ Future testMain() async { maxDiffRatePercent: 0.01); }); + test('Should blend linear gradient with alpha channel correctly.', () async { + const Rect canvasRect = Rect.fromLTRB(0, 0, 500, 500); + final RecordingCanvas rc = + RecordingCanvas(canvasRect); + final SurfacePaint backgroundPaint = SurfacePaint() + ..style = PaintingStyle.fill + ..color = const Color(0xFFFF0000); + rc.drawRect(canvasRect, backgroundPaint); + + const Rect shaderRect = Rect.fromLTRB(50, 50, 300, 300); + final SurfacePaint paint = SurfacePaint()..shader = Gradient.linear( + Offset(shaderRect.left, shaderRect.top), + Offset(shaderRect.right, shaderRect.bottom), + const [Color(0x00000000), Color(0xFF0000FF)]); + rc.drawRect(shaderRect, paint); + expect(rc.renderStrategy.hasArbitraryPaint, isTrue); + await canvasScreenshot(rc, 'linear_gradient_rect_alpha', + region: screenRect, + maxDiffRatePercent: 0.01); + }); + test('Should draw linear gradient with transform.', () async { final RecordingCanvas rc = RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500));