From 1bf5c8b084b7a981d0da0d9eed70064324b2bac9 Mon Sep 17 00:00:00 2001 From: Ferhat Date: Fri, 20 Nov 2020 09:06:55 -0800 Subject: [PATCH] [web] Implement tilemode for gradient shaders. (#22597) --- lib/web_ui/dev/goldens_lock.yaml | 2 +- .../lib/src/engine/canvaskit/shader.dart | 4 +- .../lib/src/engine/html/render_vertices.dart | 4 +- .../lib/src/engine/html/shaders/shader.dart | 334 ++++++++++++++---- lib/web_ui/lib/src/engine/util.dart | 6 +- lib/web_ui/lib/src/ui/painting.dart | 3 +- .../engine/linear_gradient_golden_test.dart | 94 +++-- .../engine/radial_gradient_golden_test.dart | 88 +++-- 8 files changed, 394 insertions(+), 141 deletions(-) diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index f558ffe268551..bbef280f85336 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 67f22ef933be27ba2be8b27df1b71b2c69eb86e5 +revision: 06e0333b8371965dce5dc05e140e6dfb454f33fa diff --git a/lib/web_ui/lib/src/engine/canvaskit/shader.dart b/lib/web_ui/lib/src/engine/canvaskit/shader.dart index edd96abc52a83..efcecf9f1e70b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/shader.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/shader.dart @@ -66,7 +66,7 @@ class CkGradientLinear extends CkShader implements ui.Gradient { assert(_offsetIsValid(to)), assert(colors != null), // ignore: unnecessary_null_comparison assert(tileMode != null), // ignore: unnecessary_null_comparison - this.matrix4 = matrix == null ? null : _FastMatrix64(matrix) { + this.matrix4 = matrix { if (assertionsEnabled) { _validateColorStops(colors, colorStops); } @@ -77,7 +77,7 @@ class CkGradientLinear extends CkShader implements ui.Gradient { final List colors; final List? colorStops; final ui.TileMode tileMode; - final _FastMatrix64? matrix4; + final Float64List? matrix4; @override SkShader createDefault() { 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 cc8720fa4a6e3..d8d2985ff94f3 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -126,7 +126,7 @@ class _WebGlRenderer implements _GlRenderer { final String fragmentShader = _writeVerticesFragmentShader(); _GlContext gl = _GlContextCache.createGlContext(widthInPixels, heightInPixels)!; - _GlProgram glProgram = gl.useAndCacheProgram(vertexShader, fragmentShader)!; + _GlProgram glProgram = gl.useAndCacheProgram(vertexShader, fragmentShader); Object transformUniform = gl.getUniformLocation(glProgram.program, 'u_ctransform'); @@ -487,7 +487,7 @@ class _GlContext { left, top, _widthInPixels, _heightInPixels]); } - _GlProgram? useAndCacheProgram( + _GlProgram useAndCacheProgram( String vertexShaderSource, String fragmentShaderSource) { String cacheKey = '$vertexShaderSource||$fragmentShaderSource'; _GlProgram? cachedProgram = _programCache[cacheKey]; 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 1d103aeaa367a..40b0e8497e7dc 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -50,7 +50,7 @@ class GradientSweep extends EngineGradient { _GlProgram glProgram = gl.useAndCacheProgram( _WebGlRenderer.writeBaseVertexShader(), - _createSweepFragmentShader(normalizedGradient, tileMode))!; + _createSweepFragmentShader(normalizedGradient, tileMode)); Object tileOffset = gl.getUniformLocation(glProgram.program, 'u_tile_offset'); double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width); @@ -98,34 +98,10 @@ class GradientSweep extends EngineGradient { method.addStatement('' 'float st = angle;'); - method.addStatement('vec4 bias;'); - method.addStatement('vec4 scale;'); - // Write uniforms for each threshold, bias and scale. - for (int i = 0; i < (gradient.thresholdCount - 1) ~/ 4 + 1; i++) { - builder.addUniform(ShaderType.kVec4, name: 'threshold_${i}'); - } - for (int i = 0; i < gradient.thresholdCount; i++) { - builder.addUniform(ShaderType.kVec4, name: 'bias_$i'); - builder.addUniform(ShaderType.kVec4, name: 'scale_$i'); - } - String probeName = 'st'; - switch (tileMode) { - case ui.TileMode.clamp: - break; - case ui.TileMode.repeated: - method.addStatement('float tiled_st = fract(st);'); - probeName = 'tiled_st'; - break; - case ui.TileMode.mirror: - method.addStatement('float t_1 = (st - 1.0);'); - method.addStatement('float tiled_st = abs((t_1 - 2.0 * floor(t_1 * 0.5)) - 1.0);'); - probeName = 'tiled_st'; - break; - } - _writeUnrolledBinarySearch(method, 0, gradient.thresholdCount - 1, - probe: probeName, sourcePrefix: 'threshold', - biasName: 'bias', scaleName: 'scale'); + final String probeName = + _writeSharedGradientShader(builder, method, gradient, tileMode); method.addStatement('${fragColor.name} = ${probeName} * scale + bias;'); + String shader = builder.build(); return shader; } @@ -140,18 +116,17 @@ class GradientSweep extends EngineGradient { } class GradientLinear extends EngineGradient { - GradientLinear( - this.from, - this.to, - this.colors, - this.colorStops, - this.tileMode, - Float64List? matrix, - ) : assert(_offsetIsValid(from)), + GradientLinear(this.from, + this.to, + this.colors, + this.colorStops, + this.tileMode, + Float32List? matrix,) + : assert(_offsetIsValid(from)), assert(_offsetIsValid(to)), assert(colors != null), // ignore: unnecessary_null_comparison assert(tileMode != null), // ignore: unnecessary_null_comparison - this.matrix4 = matrix == null ? null : _FastMatrix64(matrix), + this.matrix4 = matrix == null ? null : _FastMatrix32(matrix), super._() { if (assertionsEnabled) { _validateColorStops(colors, colorStops); @@ -163,12 +138,22 @@ class GradientLinear extends EngineGradient { final List colors; final List? colorStops; final ui.TileMode tileMode; - final _FastMatrix64? matrix4; + final _FastMatrix32? matrix4; @override - html.CanvasGradient createPaintStyle(html.CanvasRenderingContext2D? ctx, + Object createPaintStyle(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds, double density) { + if (tileMode == ui.TileMode.clamp) { + return _createCanvasGradient(ctx, shaderBounds, density); + } else { + initWebGl(); + return _createGlGradient(ctx, shaderBounds, density); + } + } + + html.CanvasGradient _createCanvasGradient(html.CanvasRenderingContext2D? ctx, ui.Rect? shaderBounds, double density) { - _FastMatrix64? matrix4 = this.matrix4; + _FastMatrix32? matrix4 = this.matrix4; html.CanvasGradient gradient; final double offsetX = shaderBounds!.left; final double offsetY = shaderBounds.top; @@ -180,29 +165,175 @@ class GradientLinear extends EngineGradient { final double fromY = matrix4.transformedY + centerY; matrix4.transform(to.dx - centerX, to.dy - centerY); gradient = ctx!.createLinearGradient(fromX - offsetX, fromY - offsetY, - matrix4.transformedX + centerX - offsetX, matrix4.transformedY - offsetY + centerY); + matrix4.transformedX + centerX - offsetX, + matrix4.transformedY - offsetY + centerY); } else { - gradient = ctx!.createLinearGradient(from.dx - offsetX, from.dy - offsetY, to.dx - offsetX, to.dy - offsetY); + gradient = ctx!.createLinearGradient( + from.dx - offsetX, from.dy - offsetY, to.dx - offsetX, + to.dy - offsetY); } + _addColorStopsToCanvasGradient(gradient, colors, colorStops); + return gradient; + } + + /// Creates a linear gradient with tiling repeat or mirror. + html.CanvasPattern _createGlGradient(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds, double density) { + assert(shaderBounds != null); + int widthInPixels = shaderBounds!.width.ceil(); + int heightInPixels = shaderBounds.height.ceil(); + assert(widthInPixels > 0 && heightInPixels > 0); + + // Render gradient into a bitmap and create a canvas pattern. + _OffScreenCanvas offScreenCanvas = + _OffScreenCanvas(widthInPixels, heightInPixels); + _GlContext gl = _OffScreenCanvas.supported + ? _GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) + : _GlContext.fromCanvas(offScreenCanvas._glCanvas!, + webGLVersion == WebGLVersion.webgl1); + gl.setViewportSize(widthInPixels, heightInPixels); + + NormalizedGradient normalizedGradient = NormalizedGradient( + colors, stops: colorStops); + + _GlProgram glProgram = gl.useAndCacheProgram( + _WebGlRenderer.writeBaseVertexShader(), + _createLinearFragmentShader(normalizedGradient, tileMode)); - final List? colorStops = this.colorStops; - if (colorStops == null) { - assert(colors.length == 2); - gradient.addColorStop(0, colorToCssString(colors[0])!); - gradient.addColorStop(1, colorToCssString(colors[1])!); - return gradient; + // Setup from/to uniforms. + // + // To compute t value between 0..1 for any point on the screen, + // we need to use from,to point pair to construct a matrix that will + // take any fragment coordinate and transform it to a t value. + // + // We compute the matrix by: + // 1- Shift from,to vector to origin. + // 2- Rotate the vector to align with x axis. + // 3- Scale it to unit vector. + double dx = to.dx - from.dx; + double dy = to.dy - from.dy; + double length = math.sqrt(dx * dx + dy * dy); + // sin(theta) = dy / length. + // cos(theta) = dx / length. + // Flip dy for gl flip. + double sinVal = length < kFltEpsilon ? 0 : -dy / length; + double cosVal = length < kFltEpsilon ? 1 : dx / length; + final Matrix4 translateToOrigin = matrix4 == null + ? Matrix4.translationValues(-from.dx, -from.dy, 0) + : Matrix4.fromFloat32List(matrix4!.matrix) + ..translate(-from.dx, -from.dy); + // Rotate around Z axis. + final Matrix4 rotationZ = Matrix4.identity(); + final Float32List storage = rotationZ.storage; + storage[0] = cosVal; + storage[1] = -sinVal; + storage[4] = sinVal; + storage[5] = cosVal; + Matrix4 gradientTransform = Matrix4.identity(); + if (length > kFltEpsilon) { + gradientTransform.scale(1.0 / length); } + gradientTransform.multiply(rotationZ); + gradientTransform.multiply(translateToOrigin); + + // Setup gradient uniforms for t search. + normalizedGradient.setupUniforms(gl, glProgram); + // Setup matrix transform uniform. + Object gradientMatrix = gl.getUniformLocation( + glProgram.program, 'm_gradient'); + gl.setUniformMatrix4fv(gradientMatrix, false, gradientTransform.storage); + + Object uRes = gl.getUniformLocation(glProgram.program, 'u_resolution'); + gl.setUniform2f( + uRes, widthInPixels.toDouble(), heightInPixels.toDouble()); + + // Draw gradient and convert to pattern. + Object? imageBitmap = _glRenderer!.drawRect(ui.Rect.fromLTWH( + 0, 0, shaderBounds.width, shaderBounds.height) /* !! shaderBounds */, + gl, + glProgram, normalizedGradient, widthInPixels, heightInPixels, + ); + return ctx!.createPattern(imageBitmap!, 'no-repeat')!; + } + + String _createLinearFragmentShader(NormalizedGradient gradient, + ui.TileMode tileMode) { + ShaderBuilder builder = ShaderBuilder.fragment(webGLVersion); + builder.floatPrecision = ShaderPrecision.kMedium; + builder.addIn(ShaderType.kVec4, name: 'v_color'); + builder.addUniform(ShaderType.kVec2, name: 'u_resolution'); + builder.addUniform(ShaderType.kMat4, name: 'm_gradient'); + ShaderDeclaration fragColor = builder.fragmentColor; + ShaderMethod method = builder.addMethod('main'); + // Linear gradient. + // Multiply with m_gradient transform to convert from fragment coordinate to + // distance on the from-to line. + method.addStatement( + 'vec4 localCoord = vec4(gl_FragCoord.x, ' + 'u_resolution.y - gl_FragCoord.y, 0, 1) * m_gradient;'); + method.addStatement('float st = localCoord.x;'); + final String probeName = + _writeSharedGradientShader(builder, method, gradient, tileMode); + method.addStatement('${fragColor.name} = ${probeName} * scale + bias;'); + String shader = builder.build(); + return shader; + } +} + +void _addColorStopsToCanvasGradient(html.CanvasGradient gradient, + List colors, List? colorStops) { + if (colorStops == null) { + assert(colors.length == 2); + gradient.addColorStop(0, colorToCssString(colors[0])!); + gradient.addColorStop(1, colorToCssString(colors[1])!); + } else { for (int i = 0; i < colors.length; i++) { gradient.addColorStop(colorStops[i], colorToCssString(colors[i])!); } - return gradient; } } -// TODO(flutter_web): For transforms and tile modes implement as webgl -// For now only GradientRotation is supported in flutter which is implemented -// for linear gradient. -// See https://github.com/flutter/flutter/issues/32819 +/// Writes shader code to map fragment value to gradient color. +/// +/// Returns name of gradient treshold variable to use to compute color. +String _writeSharedGradientShader(ShaderBuilder builder, + ShaderMethod method, + NormalizedGradient gradient, + ui.TileMode tileMode) { + method.addStatement('vec4 bias;'); + method.addStatement('vec4 scale;'); + // Write uniforms for each threshold, bias and scale. + for (int i = 0; i < (gradient.thresholdCount - 1) ~/ 4 + 1; i++) { + builder.addUniform(ShaderType.kVec4, name: 'threshold_${i}'); + } + for (int i = 0; i < gradient.thresholdCount; i++) { + builder.addUniform(ShaderType.kVec4, name: 'bias_$i'); + builder.addUniform(ShaderType.kVec4, name: 'scale_$i'); + } + + // Use st variable name if clamped, otherwise write code to comnpute + // tiled_st. + String probeName = 'st'; + switch (tileMode) { + case ui.TileMode.clamp: + break; + case ui.TileMode.repeated: + method.addStatement('float tiled_st = fract(st);'); + probeName = 'tiled_st'; + break; + case ui.TileMode.mirror: + method.addStatement('float t_1 = (st - 1.0);'); + method.addStatement( + 'float tiled_st = abs((t_1 - 2.0 * floor(t_1 * 0.5)) - 1.0);'); + probeName = 'tiled_st'; + break; + } + _writeUnrolledBinarySearch(method, 0, gradient.thresholdCount - 1, + probe: probeName, sourcePrefix: 'threshold', + biasName: 'bias', scaleName: 'scale'); + return probeName; +} + class GradientRadial extends EngineGradient { GradientRadial(this.center, this.radius, this.colors, this.colorStops, this.tileMode, this.matrix4) @@ -218,30 +349,95 @@ class GradientRadial extends EngineGradient { @override Object createPaintStyle(html.CanvasRenderingContext2D? ctx, ui.Rect? shaderBounds, double density) { - if (!useCanvasKit) { - if (tileMode != ui.TileMode.clamp) { - throw UnimplementedError( - 'TileMode not supported in GradientRadial shader'); - } + if (tileMode == ui.TileMode.clamp) { + return _createCanvasGradient(ctx, shaderBounds, density); + } else { + initWebGl(); + return _createGlGradient(ctx, shaderBounds, density); } + } + + Object _createCanvasGradient(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds, double density) { final double offsetX = shaderBounds!.left; final double offsetY = shaderBounds.top; final html.CanvasGradient gradient = ctx!.createRadialGradient( center.dx - offsetX, center.dy - offsetY, 0, center.dx - offsetX, center.dy - offsetY, radius); - final List? colorStops = this.colorStops; - if (colorStops == null) { - assert(colors.length == 2); - gradient.addColorStop(0, colorToCssString(colors[0])!); - gradient.addColorStop(1, colorToCssString(colors[1])!); - return gradient; - } else { - for (int i = 0; i < colors.length; i++) { - gradient.addColorStop(colorStops[i], colorToCssString(colors[i])!); - } - } + _addColorStopsToCanvasGradient(gradient, colors, colorStops); return gradient; } + + /// Creates a radial gradient with tiling repeat or mirror. + html.CanvasPattern _createGlGradient(html.CanvasRenderingContext2D? ctx, + ui.Rect? shaderBounds, double density) { + assert(shaderBounds != null); + int widthInPixels = shaderBounds!.width.ceil(); + int heightInPixels = shaderBounds.height.ceil(); + assert(widthInPixels > 0 && heightInPixels > 0); + + initWebGl(); + // Render gradient into a bitmap and create a canvas pattern. + _OffScreenCanvas offScreenCanvas = + _OffScreenCanvas(widthInPixels, heightInPixels); + _GlContext gl = _OffScreenCanvas.supported + ? _GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) + : _GlContext.fromCanvas(offScreenCanvas._glCanvas!, + webGLVersion == WebGLVersion.webgl1); + gl.setViewportSize(widthInPixels, heightInPixels); + + NormalizedGradient normalizedGradient = NormalizedGradient( + colors, stops: colorStops); + + _GlProgram glProgram = gl.useAndCacheProgram( + _WebGlRenderer.writeBaseVertexShader(), + _createRadialFragmentShader(normalizedGradient, tileMode)); + + Object tileOffset = gl.getUniformLocation(glProgram.program, 'u_tile_offset'); + double centerX = (center.dx - shaderBounds.left) / (shaderBounds.width); + double centerY = (center.dy - shaderBounds.top) / (shaderBounds.height); + gl.setUniform2f(tileOffset, + 2 * (shaderBounds.width * (centerX - 0.5)), + 2 * (shaderBounds.height * (centerY - 0.5))); + Object radiusUniform = gl.getUniformLocation(glProgram.program, 'u_radius'); + gl.setUniform1f(radiusUniform, radius); + normalizedGradient.setupUniforms(gl, glProgram); + Object gradientMatrix = gl.getUniformLocation( + glProgram.program, 'm_gradient'); + gl.setUniformMatrix4fv(gradientMatrix, false, matrix4 == null ? Matrix4.identity().storage : matrix4!); + + Object? imageBitmap = _glRenderer!.drawRect(ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), + gl, glProgram, normalizedGradient, widthInPixels, heightInPixels); + + return ctx!.createPattern(imageBitmap!, 'no-repeat')!; + } + + String _createRadialFragmentShader(NormalizedGradient gradient, + ui.TileMode tileMode) { + ShaderBuilder builder = ShaderBuilder.fragment(webGLVersion); + builder.floatPrecision = ShaderPrecision.kMedium; + builder.addIn(ShaderType.kVec4, name: 'v_color'); + builder.addUniform(ShaderType.kVec2, name: 'u_resolution'); + builder.addUniform(ShaderType.kVec2, name: 'u_tile_offset'); + builder.addUniform(ShaderType.kFloat, name: 'u_radius'); + builder.addUniform(ShaderType.kMat4, name: 'm_gradient'); + ShaderDeclaration fragColor = builder.fragmentColor; + ShaderMethod method = builder.addMethod('main'); + // 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;'); + method.addStatement( + 'float dist = length(localCoord);'); + method.addStatement('' + 'float st = abs(dist / u_radius);'); + final String probeName = + _writeSharedGradientShader(builder, method, gradient, tileMode); + method.addStatement('${fragColor.name} = ${probeName} * scale + bias;'); + String shader = builder.build(); + return shader; + } } class GradientConical extends EngineGradient { diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index cfb2a73cfb9fa..877bd519f2b23 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -526,10 +526,10 @@ FutureOr sendFontChangeMessage() async { } // Stores matrix in a form that allows zero allocation transforms. -class _FastMatrix64 { - final Float64List matrix; +class _FastMatrix32 { + final Float32List matrix; double transformedX = 0, transformedY = 0; - _FastMatrix64(this.matrix); + _FastMatrix32(this.matrix); void transform(double x, double y) { transformedX = matrix[12] + (matrix[0] * x) + (matrix[4] * y); diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index c4014e44dee5f..729b3d0f0b29f 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -280,7 +280,8 @@ abstract class Gradient extends Shader { Float64List? matrix4, ]) => engine.useCanvasKit ? engine.CkGradientLinear(from, to, colors, colorStops, tileMode, matrix4) - : engine.GradientLinear(from, to, colors, colorStops, tileMode, matrix4); + : engine.GradientLinear(from, to, colors, colorStops, tileMode, + matrix4 == null ? null : engine.toMatrix32(matrix4)); factory Gradient.radial( Offset center, double radius, diff --git a/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart index 6eccec76d3234..922d9fb8ee202 100644 --- a/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/linear_gradient_golden_test.dart @@ -3,45 +3,23 @@ // found in the LICENSE file. // @dart = 2.6 -import 'dart:html' as html; import 'dart:math' as math; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; - -import 'package:web_engine_tester/golden_tester.dart'; +import 'screenshot.dart'; void main() { internalBootstrapBrowserTest(() => testMain); } void testMain() async { - const double screenWidth = 600.0; - const double screenHeight = 800.0; + const double screenWidth = 500.0; + const double screenHeight = 500.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); - // Commit a recording canvas to a bitmap, and compare with the expected - Future _checkScreenshot(RecordingCanvas rc, String fileName, - {Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { - final EngineCanvas engineCanvas = BitmapCanvas(screenRect); - rc.endRecording(); - rc.apply(engineCanvas, screenRect); - - // Wrap in so that our CSS selectors kick in. - final html.Element sceneElement = html.Element.tag('flt-scene'); - try { - sceneElement.append(engineCanvas.rootElement); - html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region); - } finally { - // The page is reused across tests, so remove the element after taking the - // golden screenshot. - sceneElement.remove(); - } - } - setUp(() async { debugEmulateFlutterTesterEnvironment = true; await webOnlyInitializePlatform(); @@ -59,7 +37,9 @@ void testMain() async { [Color(0xFFcfdfd2), Color(0xFF042a85)]); rc.drawRect(shaderRect, paint); expect(rc.hasArbitraryPaint, isTrue); - await _checkScreenshot(rc, 'linear_gradient_rect'); + await canvasScreenshot(rc, 'linear_gradient_rect', + region: screenRect, + maxDiffRatePercent: 0.01); }); test('Should draw linear gradient with transform.', () async { @@ -85,7 +65,9 @@ void testMain() async { yOffset += 120; } expect(rc.hasArbitraryPaint, isTrue); - await _checkScreenshot(rc, 'linear_gradient_oval_matrix'); + await canvasScreenshot(rc, 'linear_gradient_oval_matrix', + region: screenRect, + maxDiffRatePercent: 0.2); }); // Regression test for https://github.com/flutter/flutter/issues/50010 @@ -99,6 +81,62 @@ void testMain() async { [Color(0xFFcfdfd2), Color(0xFF042a85)]); rc.drawRRect(RRect.fromRectAndRadius(shaderRect, Radius.circular(16)), paint); expect(rc.hasArbitraryPaint, isTrue); - await _checkScreenshot(rc, 'linear_gradient_rounded_rect'); + await canvasScreenshot(rc, 'linear_gradient_rounded_rect', + region: screenRect, + maxDiffRatePercent: 0.1); + }); + + test('Should draw tiled repeated linear gradient with transform.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + List angles = [0.0, 30.0, 210.0]; + double yOffset = 0; + for (double angle in angles) { + final Rect shaderRect = Rect.fromLTWH(50, 50 + yOffset, 100, 100); + final Paint paint = Paint() + ..shader = Gradient.linear( + Offset(shaderRect.left, shaderRect.top), + Offset(shaderRect.left + shaderRect.width / 2, shaderRect.top), + [Color(0xFFFF0000), Color(0xFF042a85)], + null, + TileMode.repeated, + Matrix4 + .rotationZ((angle / 180) * math.pi) + .toFloat64()); + rc.drawRect(shaderRect, Paint() + ..color = Color(0xFF000000)); + rc.drawOval(shaderRect, paint); + yOffset += 120; + } + expect(rc.hasArbitraryPaint, isTrue); + await canvasScreenshot(rc, 'linear_gradient_tiled_repeated_rect', + region: screenRect); + }); + + test('Should draw tiled mirrored linear gradient with transform.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + List angles = [0.0, 30.0, 210.0]; + double yOffset = 0; + for (double angle in angles) { + final Rect shaderRect = Rect.fromLTWH(50, 50 + yOffset, 100, 100); + final Paint paint = Paint() + ..shader = Gradient.linear( + Offset(shaderRect.left, shaderRect.top), + Offset(shaderRect.left + shaderRect.width / 2, shaderRect.top), + [Color(0xFFFF0000), Color(0xFF042a85)], + null, + TileMode.mirror, + Matrix4 + .rotationZ((angle / 180) * math.pi) + .toFloat64()); + rc.drawRect(shaderRect, Paint() + ..color = Color(0xFF000000)); + rc.drawOval(shaderRect, paint); + yOffset += 120; + } + expect(rc.hasArbitraryPaint, isTrue); + await canvasScreenshot(rc, 'linear_gradient_tiled_mirrored_rect', + region: screenRect); }); } diff --git a/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart b/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart index 098233fe1cfb3..e4d68192018e7 100644 --- a/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/radial_gradient_golden_test.dart @@ -3,44 +3,17 @@ // found in the LICENSE file. // @dart = 2.6 -import 'dart:html' as html; - import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart'; - -import 'package:web_engine_tester/golden_tester.dart'; +import 'screenshot.dart'; void main() { internalBootstrapBrowserTest(() => testMain); } void testMain() async { - const double screenWidth = 600.0; - const double screenHeight = 800.0; - const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); - - // Commit a recording canvas to a bitmap, and compare with the expected - Future _checkScreenshot(RecordingCanvas rc, String fileName, - {Rect region = const Rect.fromLTWH(0, 0, 500, 500), - bool write = false}) async { - final EngineCanvas engineCanvas = BitmapCanvas(screenRect); - rc.endRecording(); - rc.apply(engineCanvas, screenRect); - - // Wrap in so that our CSS selectors kick in. - final html.Element sceneElement = html.Element.tag('flt-scene'); - try { - sceneElement.append(engineCanvas.rootElement); - html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region, write: write); - } finally { - // The page is reused across tests, so remove the element after taking the - // golden screenshot. - sceneElement.remove(); - } - } setUp(() async { debugEmulateFlutterTesterEnvironment = true; @@ -51,14 +24,17 @@ void testMain() async { Future _testGradient(String fileName, Shader shader, {Rect paintRect = const Rect.fromLTRB(50, 50, 300, 300), - Rect shaderRect = const Rect.fromLTRB(50, 50, 300, 300)}) async { - final RecordingCanvas rc = - RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + Rect shaderRect = const Rect.fromLTRB(50, 50, 300, 300), + bool write = false, + double maxDiffRatePercent = 0, + Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { + final RecordingCanvas rc = RecordingCanvas(region); final Paint paint = Paint()..shader = shader; final Path path = Path(); path.addRect(paintRect); rc.drawPath(path, paint); - await _checkScreenshot(rc, fileName); + await canvasScreenshot(rc, fileName, write: write, region: region, + maxDiffRatePercent: maxDiffRatePercent); } test('Should draw centered radial gradient.', () async { @@ -73,7 +49,8 @@ void testMain() async { const Color.fromARGB(255, 0, 0, 0), const Color.fromARGB(255, 0, 0, 255) ]), - shaderRect: shaderRect); + shaderRect: shaderRect, + maxDiffRatePercent: 0.2); }); test('Should draw right bottom centered radial gradient.', () async { @@ -85,7 +62,8 @@ void testMain() async { const Color.fromARGB(255, 0, 0, 0), const Color.fromARGB(255, 0, 0, 255) ]), - shaderRect: shaderRect); + shaderRect: shaderRect, + maxDiffRatePercent: 0.3); }); test('Should draw with radial gradient with TileMode.clamp.', () async { @@ -102,6 +80,46 @@ void testMain() async { ], [0.0, 1.0], TileMode.clamp), - shaderRect: shaderRect); + shaderRect: shaderRect, + maxDiffRatePercent: 0.2); + }); + + const List colors = [ + Color(0xFF000000), + Color(0xFFFF3C38), + Color(0xFFFF8C42), + Color(0xFFFFF275), + Color(0xFF6699CC), + Color(0xFF656D78),]; + const List colorStops = [0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; + + test('Should draw with radial gradient with TileMode.repeated.', () async { + Rect shaderRect = const Rect.fromLTRB(50, 50, 100, 100); + await _testGradient( + 'radial_gradient_tilemode_repeated', + Gradient.radial( + Offset((shaderRect.left + shaderRect.right) / 2, + (shaderRect.top + shaderRect.bottom) / 2), + shaderRect.width / 2, + colors, + colorStops, + TileMode.repeated), + shaderRect: shaderRect, + region: const Rect.fromLTWH(0, 0, 600, 800)); + }); + + test('Should draw with radial gradient with TileMode.mirrored.', () async { + Rect shaderRect = const Rect.fromLTRB(50, 50, 100, 100); + await _testGradient( + 'radial_gradient_tilemode_mirror', + Gradient.radial( + Offset((shaderRect.left + shaderRect.right) / 2, + (shaderRect.top + shaderRect.bottom) / 2), + shaderRect.width / 2, + colors, + colorStops, + TileMode.mirror), + shaderRect: shaderRect, + region: const Rect.fromLTWH(0, 0, 600, 800)); }); }