diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index d055e785b7de1..df5209f9d5b1c 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: 43254f4abddc2542ece540f222545970caf12908 +revision: 1637835646ef187884ceeb59011d70c463429876 diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index 228b626362461..8808b5fa7737f 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -101,11 +101,7 @@ class _CanvasPool extends _SaveStackTracking { _rootElement.append(_canvas); _context = _canvas.context2D; _contextHandle = ContextStateHandle(_context); - _initializeViewport(); - if (requiresClearRect) { - // Now that the context is reset, clear old contents. - _context.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels); - } + _initializeViewport(requiresClearRect); _replayClipStack(); } @@ -136,20 +132,34 @@ class _CanvasPool extends _SaveStackTracking { translate(transform.dx, transform.dy); } - int _replaySingleSaveEntry( - int clipDepth, Matrix4 transform, List<_SaveClipEntry> clipStack) { + int _replaySingleSaveEntry(int clipDepth, Matrix4 prevTransform, + Matrix4 transform, List<_SaveClipEntry> clipStack) { final html.CanvasRenderingContext2D ctx = _context; - if (!transform.isIdentity()) { - final double ratio = EngineWindow.browserDevicePixelRatio; - ctx.setTransform(ratio, 0, 0, ratio, 0, 0); - ctx.transform(transform[0], transform[1], transform[4], transform[5], - transform[12], transform[13]); - } if (clipStack != null) { for (int clipCount = clipStack.length; clipDepth < clipCount; clipDepth++) { _SaveClipEntry clipEntry = clipStack[clipDepth]; + Matrix4 clipTimeTransform = clipEntry.currentTransform; + // If transform for entry recording change since last element, update. + // Comparing only matrix3 elements since Canvas API restricted. + if (clipTimeTransform[0] != prevTransform[0] || + clipTimeTransform[1] != prevTransform[1] || + clipTimeTransform[4] != prevTransform[4] || + clipTimeTransform[5] != prevTransform[5] || + clipTimeTransform[12] != prevTransform[12] || + clipTimeTransform[13] != prevTransform[13]) { + final double ratio = EngineWindow.browserDevicePixelRatio; + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + ctx.transform( + clipTimeTransform[0], + clipTimeTransform[1], + clipTimeTransform[4], + clipTimeTransform[5], + clipTimeTransform[12], + clipTimeTransform[13]); + prevTransform = clipTimeTransform; + } if (clipEntry.rect != null) { _clipRect(ctx, clipEntry.rect); } else if (clipEntry.rrect != null) { @@ -160,6 +170,19 @@ class _CanvasPool extends _SaveStackTracking { } } } + // If transform was changed between last clip operation and save call, + // update. + if (transform[0] != prevTransform[0] || + transform[1] != prevTransform[1] || + transform[4] != prevTransform[4] || + transform[5] != prevTransform[5] || + transform[12] != prevTransform[12] || + transform[13] != prevTransform[13]) { + final double ratio = EngineWindow.browserDevicePixelRatio; + ctx.setTransform(ratio, 0, 0, ratio, 0, 0); + ctx.transform(transform[0], transform[1], transform[4], transform[5], + transform[12], transform[13]); + } return clipDepth; } @@ -167,16 +190,19 @@ class _CanvasPool extends _SaveStackTracking { // Replay save/clip stack on this canvas now. html.CanvasRenderingContext2D ctx = _context; int clipDepth = 0; + Matrix4 prevTransform = Matrix4.identity(); for (int saveStackIndex = 0, len = _saveStack.length; saveStackIndex < len; saveStackIndex++) { _SaveStackEntry saveEntry = _saveStack[saveStackIndex]; clipDepth = _replaySingleSaveEntry( - clipDepth, saveEntry.transform, saveEntry.clipStack); + clipDepth, prevTransform, saveEntry.transform, saveEntry.clipStack); + prevTransform = saveEntry.transform; ctx.save(); ++_saveContextCount; } - _replaySingleSaveEntry(clipDepth, _currentTransform, _clipStack); + _replaySingleSaveEntry( + clipDepth, prevTransform, _currentTransform, _clipStack); } // Marks this pool for reuse. @@ -216,7 +242,7 @@ class _CanvasPool extends _SaveStackTracking { /// Configures the canvas such that its coordinate system follows the scene's /// coordinate system, and the pixel ratio is applied such that CSS pixels are /// translated to bitmap pixels. - void _initializeViewport() { + void _initializeViewport(bool clearCanvas) { html.CanvasRenderingContext2D ctx = context; // Save the canvas state with top-level transforms so we can undo // any clips later when we reuse the canvas. @@ -226,6 +252,9 @@ class _CanvasPool extends _SaveStackTracking { // We always start with identity transform because the surrounding transform // is applied on the DOM elements. ctx.setTransform(1, 0, 0, 1, 0, 0); + if (clearCanvas) { + ctx.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels); + } // This scale makes sure that 1 CSS pixel is translated to the correct // number of bitmap pixels. diff --git a/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart new file mode 100644 index 0000000000000..31ce13fc207be --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_context_test.dart @@ -0,0 +1,100 @@ +// 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:html' as html; +import 'dart:js_util' as js_util; + +import 'package:ui/ui.dart' hide TextStyle; +import 'package:ui/src/engine.dart' as engine; +import 'package:test/test.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +/// Tests context save/restore. +void main() 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(engine.RecordingCanvas rc, String fileName, + {Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { + final engine.EngineCanvas engineCanvas = engine.BitmapCanvas(screenRect); + + rc.apply(engineCanvas); + + // 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, maxDiffRate: 0.1); + } finally { + // The page is reused across tests, so remove the element after taking the + // Scuba screenshot. + sceneElement.remove(); + } + } + + setUp(() async { + debugEmulateFlutterTesterEnvironment = true; + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + // Regression test for https://github.com/flutter/flutter/issues/49429 + // Should clip with correct transform. + test('Clips image with oval clip path', () async { + final engine.RecordingCanvas rc = + engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + final Paint paint = Paint() + ..color = Color(0xFF00FF00) + ..style = PaintingStyle.fill; + rc.save(); + final Path ovalPath = Path(); + ovalPath.addOval(Rect.fromLTWH(100, 30, 200, 100)); + rc.clipPath(ovalPath); + rc.translate(-500, -500); + rc.save(); + rc.translate(500, 500); + rc.drawPath(ovalPath, paint); + // The line below was causing SaveClipStack to incorrectly set + // transform before path painting. + rc.translate(-1000, -1000); + rc.save(); + rc.restore(); + rc.restore(); + rc.restore(); + // The rectangle should paint without clipping since we restored + // context. + rc.drawRect(Rect.fromLTWH(0, 0, 4, 200), paint); + await _checkScreenshot(rc, 'context_save_restore_transform'); + }); + + test('Should restore clip path', () async { + final engine.RecordingCanvas rc = + engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + final Paint goodPaint = Paint() + ..color = Color(0x8000FF00) + ..style = PaintingStyle.fill; + final Paint badPaint = Paint() + ..color = Color(0xFFFF0000) + ..style = PaintingStyle.fill; + rc.save(); + final Path ovalPath = Path(); + ovalPath.addOval(Rect.fromLTWH(100, 30, 200, 100)); + rc.clipPath(ovalPath); + rc.translate(-500, -500); + rc.save(); + rc.restore(); + // The rectangle should be clipped against oval. + rc.drawRect(Rect.fromLTWH(0, 0, 300, 300), badPaint); + rc.restore(); + // The rectangle should paint without clipping since we restored + // context. + rc.drawRect(Rect.fromLTWH(0, 0, 200, 200), goodPaint); + await _checkScreenshot(rc, 'context_save_restore_clip'); + }); +}