| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| import 'dart:typed_data'; |
| |
| import 'package:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| import 'package:ui/src/engine.dart'; |
| import 'package:ui/ui.dart'; |
| |
| import '../screenshot.dart'; |
| |
| // TODO(yjbanov): unskip Firefox tests when Firefox implements WebGL in headless mode. |
| // https://github.com/flutter/flutter/issues/86623 |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| Future<void> testMain() async { |
| const double screenWidth = 600.0; |
| const double screenHeight = 800.0; |
| const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); |
| const Rect region = Rect.fromLTWH(0, 0, 500, 240); |
| |
| setUp(() async { |
| debugEmulateFlutterTesterEnvironment = true; |
| }); |
| |
| setUpStableTestFonts(); |
| |
| test('Paints sweep gradient rectangles', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); |
| canvas.save(); |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| 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]; |
| |
| GradientSweep sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.clamp, |
| 0, 360.0 / 180.0 * math.pi, |
| null); |
| |
| final GradientSweep sweepGradientRotated = 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); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 80; |
| // Gradient with default center. |
| Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Gradient with shifted center and rotation. |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Gradient with start/endangle. |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.clamp, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode repeat |
| rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.repeated, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode mirror |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.mirror, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| canvas.restore(); |
| await canvasScreenshot(canvas, 'sweep_gradient_rect', canvasRect: screenRect, region: region); |
| }, skip: isFirefox); |
| |
| test('Paints sweep gradient ovals', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); |
| canvas.save(); |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| const List<Color> colors = <Color>[ |
| Color(0xFF000000), |
| Color(0xFFFF3C38), |
| Color(0xFFFF8C42), |
| Color(0xFFFFF275), |
| Color(0xFF6699CC), |
| Color(0xFF656D78),]; |
| final List<double> stops = <double>[0.0, 0.05, 0.4, 0.6, 0.9, 1.0]; |
| |
| GradientSweep sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.clamp, |
| 0, 360.0 / 180.0 * math.pi, |
| null); |
| |
| final GradientSweep sweepGradientRotated = 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); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 80; |
| // Gradient with default center. |
| Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); |
| canvas.drawOval(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Gradient with shifted center and rotation. |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| canvas.drawOval(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Gradient with start/endangle. |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.clamp, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| canvas.drawOval(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode repeat |
| rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.repeated, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| |
| canvas.drawOval(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode mirror |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.mirror, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| canvas.drawOval(rectBounds, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| canvas.restore(); |
| await canvasScreenshot(canvas, 'sweep_gradient_oval', canvasRect: screenRect, region: region); |
| }, skip: isFirefox); |
| |
| test('Paints sweep gradient paths', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); |
| canvas.save(); |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| 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]; |
| |
| GradientSweep sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.clamp, |
| 0, 360.0 / 180.0 * math.pi, |
| null); |
| |
| final GradientSweep sweepGradientRotated = 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); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 80; |
| // Gradient with default center. |
| Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); |
| Path path = samplePathFromRect(rectBounds); |
| canvas.drawPath(path, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Gradient with shifted center and rotation. |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| path = samplePathFromRect(rectBounds); |
| canvas.drawPath(path, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradientRotated, Rect.fromLTWH(rectBounds.center.dx, rectBounds.top, rectBounds.width / 2, rectBounds.height))); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Gradient with start/endangle. |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.clamp, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| path = samplePathFromRect(rectBounds); |
| canvas.drawPath(path, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode repeat |
| rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.repeated, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| |
| path = samplePathFromRect(rectBounds); |
| canvas.drawPath(path, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode mirror |
| rectBounds = rectBounds.translate(kBoxWidth + 10, 0); |
| sweepGradient = GradientSweep(const Offset(0.5, 0.5), |
| colors, stops, TileMode.mirror, |
| math.pi / 6, 3 * math.pi / 4, |
| null); |
| path = samplePathFromRect(rectBounds); |
| canvas.drawPath(path, |
| SurfacePaint()..shader = engineGradientToShader(sweepGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| canvas.restore(); |
| await canvasScreenshot(canvas, 'sweep_gradient_path', canvasRect: screenRect, region: region); |
| }, skip: isFirefox); |
| |
| /// Regression test for https://github.com/flutter/flutter/issues/74137. |
| test('Paints rotated and shifted linear gradient', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); |
| canvas.save(); |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| 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]; |
| |
| GradientLinear linearGradient = GradientLinear(const Offset(50, 50), |
| const Offset(200,130), |
| colors, stops, TileMode.clamp, |
| Matrix4.identity().storage); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 80; |
| // Gradient with default center. |
| Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode repeat |
| rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); |
| linearGradient = GradientLinear(const Offset(50, 50), |
| const Offset(200,130), |
| colors, stops, TileMode.repeated, |
| Matrix4.identity().storage); |
| |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| canvas.restore(); |
| await canvasScreenshot(canvas, 'linear_gradient_rect_shifted', canvasRect: screenRect, region: region); |
| }, skip: isFirefox); |
| |
| /// Regression test for https://github.com/flutter/flutter/issues/82748. |
| test('Paints gradient with gradient stop outside range', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); |
| canvas.save(); |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| const List<Color> colors = <Color>[ |
| Color(0xFF000000), |
| Color(0xFFFF3C38)]; |
| const List<double> stops = <double>[0.0, 10.0]; |
| |
| final GradientLinear linearGradient = GradientLinear(const Offset(50, 50), |
| const Offset(200,130), |
| colors, stops, TileMode.clamp, |
| Matrix4.identity().storage); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 80; |
| // Gradient with default center. |
| const Rect rectBounds = Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| canvas.restore(); |
| |
| final EngineCanvas engineCanvas = BitmapCanvas(screenRect, |
| RenderStrategy()); |
| canvas.endRecording(); |
| canvas.apply(engineCanvas, screenRect); |
| }); |
| |
| test("Creating lots of gradients doesn't create too many webgl contexts", |
| () async { |
| final DomCanvasElement sideCanvas = |
| createDomCanvasElement(width: 5, height: 5); |
| final DomCanvasRenderingContextWebGl? context = |
| sideCanvas.getContext('webgl') as DomCanvasRenderingContextWebGl?; |
| expect(context, isNotNull); |
| |
| final EngineCanvas engineCanvas = |
| BitmapCanvas(const Rect.fromLTRB(0, 0, 100, 100), RenderStrategy()); |
| for (double x = 0; x < 100; x += 10) { |
| for (double y = 0; y < 100; y += 10) { |
| const List<Color> colors = <Color>[ |
| Color(0xFFFF0000), |
| Color(0xFF0000FF), |
| ]; |
| |
| final GradientLinear linearGradient = GradientLinear( |
| Offset.zero, |
| const Offset(10, 10), |
| colors, |
| null, |
| TileMode.clamp, |
| Matrix4.identity().storage); |
| engineCanvas.drawRect(Rect.fromLTWH(x, y, 10, 10), |
| SurfacePaintData()..shader = linearGradient); |
| } |
| } |
| |
| expect(context!.isContextLost(), isFalse); |
| }, skip: isFirefox); |
| |
| test('Paints clamped, rotated and shifted linear gradient', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); |
| canvas.save(); |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| 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]; |
| |
| GradientLinear linearGradient = GradientLinear(const Offset(50, 50), |
| const Offset(200,130), |
| colors, stops, TileMode.clamp, |
| Matrix4.identity().storage); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 80; |
| // Gradient with default center. |
| Rect rectBounds = const Rect.fromLTWH(10, 20, kBoxWidth, kBoxHeight); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| // Tile mode repeat |
| rectBounds = const Rect.fromLTWH(10, 110, kBoxWidth, kBoxHeight); |
| linearGradient = GradientLinear(const Offset(50, 50), |
| const Offset(200,130), |
| colors, stops, TileMode.clamp, |
| Matrix4.identity().storage); |
| |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| canvas.restore(); |
| await canvasScreenshot(canvas, 'linear_gradient_rect_clamp_rotated', canvasRect: screenRect, region: region); |
| }); |
| |
| test('Paints linear gradient properly when within svg context', () async { |
| final RecordingCanvas canvas = |
| RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 240)); |
| canvas.save(); |
| |
| canvas.renderStrategy.isInsideSvgFilterTree = true; |
| |
| final SurfacePaint borderPaint = SurfacePaint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1 |
| ..color = const Color(0xFF000000); |
| |
| const List<Color> colors = <Color>[ |
| Color(0xFFFF0000), |
| Color(0xFF0000FF), |
| ]; |
| |
| final GradientLinear linearGradient = GradientLinear(const Offset(125, 75), |
| const Offset(175, 125), |
| colors, null, TileMode.clamp, |
| Matrix4.identity().storage); |
| |
| const double kBoxWidth = 150; |
| const double kBoxHeight = 100; |
| // Gradient with default center. |
| const Rect rectBounds = Rect.fromLTWH(100, 50, kBoxWidth, kBoxHeight); |
| canvas.drawRect(rectBounds, |
| SurfacePaint()..shader = engineLinearGradientToShader(linearGradient, rectBounds)); |
| canvas.drawRect(rectBounds, borderPaint); |
| |
| canvas.restore(); |
| await canvasScreenshot(canvas, 'linear_gradient_in_svg_context', canvasRect: screenRect, region: region); |
| }); |
| } |
| |
| Shader engineGradientToShader(GradientSweep gradient, Rect rect) { |
| return Gradient.sweep( |
| Offset(rect.left + gradient.center.dx * rect.width, |
| rect.top + gradient.center.dy * rect.height), |
| gradient.colors, gradient.colorStops, gradient.tileMode, |
| gradient.startAngle, |
| gradient.endAngle, |
| gradient.matrix4 == null ? null : |
| Float64List.fromList(gradient.matrix4!), |
| ); |
| } |
| |
| Shader engineLinearGradientToShader(GradientLinear gradient, Rect rect) { |
| return Gradient.linear(gradient.from, gradient.to, |
| gradient.colors, gradient.colorStops, gradient.tileMode, |
| gradient.matrix4 == null ? null : Float64List.fromList( |
| gradient.matrix4!.matrix), |
| ); |
| } |
| |
| Path samplePathFromRect(Rect rectBounds) => |
| Path() |
| ..moveTo(rectBounds.center.dx, rectBounds.top) |
| ..lineTo(rectBounds.left, rectBounds.bottom) |
| ..quadraticBezierTo(rectBounds.center.dx + 20, rectBounds.bottom - 40, |
| rectBounds.right, rectBounds.bottom) |
| ..close(); |