Skip to content

Commit 8dab8a8

Browse files
ferhatbNoamDev
authored andcommitted
[web] Fixes incorrect transform when context save and transforms are deferred. (flutter#16412)
* Fix transform order in clipStack replay
1 parent f7a4994 commit 8dab8a8

3 files changed

Lines changed: 146 additions & 17 deletions

File tree

lib/web_ui/dev/goldens_lock.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
repository: https://github.com/flutter/goldens.git
2-
revision: 43254f4abddc2542ece540f222545970caf12908
2+
revision: 1637835646ef187884ceeb59011d70c463429876

lib/web_ui/lib/src/engine/canvas_pool.dart

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,7 @@ class _CanvasPool extends _SaveStackTracking {
101101
_rootElement.append(_canvas);
102102
_context = _canvas.context2D;
103103
_contextHandle = ContextStateHandle(_context);
104-
_initializeViewport();
105-
if (requiresClearRect) {
106-
// Now that the context is reset, clear old contents.
107-
_context.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels);
108-
}
104+
_initializeViewport(requiresClearRect);
109105
_replayClipStack();
110106
}
111107

@@ -136,20 +132,34 @@ class _CanvasPool extends _SaveStackTracking {
136132
translate(transform.dx, transform.dy);
137133
}
138134

139-
int _replaySingleSaveEntry(
140-
int clipDepth, Matrix4 transform, List<_SaveClipEntry> clipStack) {
135+
int _replaySingleSaveEntry(int clipDepth, Matrix4 prevTransform,
136+
Matrix4 transform, List<_SaveClipEntry> clipStack) {
141137
final html.CanvasRenderingContext2D ctx = _context;
142-
if (!transform.isIdentity()) {
143-
final double ratio = EngineWindow.browserDevicePixelRatio;
144-
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
145-
ctx.transform(transform[0], transform[1], transform[4], transform[5],
146-
transform[12], transform[13]);
147-
}
148138
if (clipStack != null) {
149139
for (int clipCount = clipStack.length;
150140
clipDepth < clipCount;
151141
clipDepth++) {
152142
_SaveClipEntry clipEntry = clipStack[clipDepth];
143+
Matrix4 clipTimeTransform = clipEntry.currentTransform;
144+
// If transform for entry recording change since last element, update.
145+
// Comparing only matrix3 elements since Canvas API restricted.
146+
if (clipTimeTransform[0] != prevTransform[0] ||
147+
clipTimeTransform[1] != prevTransform[1] ||
148+
clipTimeTransform[4] != prevTransform[4] ||
149+
clipTimeTransform[5] != prevTransform[5] ||
150+
clipTimeTransform[12] != prevTransform[12] ||
151+
clipTimeTransform[13] != prevTransform[13]) {
152+
final double ratio = EngineWindow.browserDevicePixelRatio;
153+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
154+
ctx.transform(
155+
clipTimeTransform[0],
156+
clipTimeTransform[1],
157+
clipTimeTransform[4],
158+
clipTimeTransform[5],
159+
clipTimeTransform[12],
160+
clipTimeTransform[13]);
161+
prevTransform = clipTimeTransform;
162+
}
153163
if (clipEntry.rect != null) {
154164
_clipRect(ctx, clipEntry.rect);
155165
} else if (clipEntry.rrect != null) {
@@ -160,23 +170,39 @@ class _CanvasPool extends _SaveStackTracking {
160170
}
161171
}
162172
}
173+
// If transform was changed between last clip operation and save call,
174+
// update.
175+
if (transform[0] != prevTransform[0] ||
176+
transform[1] != prevTransform[1] ||
177+
transform[4] != prevTransform[4] ||
178+
transform[5] != prevTransform[5] ||
179+
transform[12] != prevTransform[12] ||
180+
transform[13] != prevTransform[13]) {
181+
final double ratio = EngineWindow.browserDevicePixelRatio;
182+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
183+
ctx.transform(transform[0], transform[1], transform[4], transform[5],
184+
transform[12], transform[13]);
185+
}
163186
return clipDepth;
164187
}
165188

166189
void _replayClipStack() {
167190
// Replay save/clip stack on this canvas now.
168191
html.CanvasRenderingContext2D ctx = _context;
169192
int clipDepth = 0;
193+
Matrix4 prevTransform = Matrix4.identity();
170194
for (int saveStackIndex = 0, len = _saveStack.length;
171195
saveStackIndex < len;
172196
saveStackIndex++) {
173197
_SaveStackEntry saveEntry = _saveStack[saveStackIndex];
174198
clipDepth = _replaySingleSaveEntry(
175-
clipDepth, saveEntry.transform, saveEntry.clipStack);
199+
clipDepth, prevTransform, saveEntry.transform, saveEntry.clipStack);
200+
prevTransform = saveEntry.transform;
176201
ctx.save();
177202
++_saveContextCount;
178203
}
179-
_replaySingleSaveEntry(clipDepth, _currentTransform, _clipStack);
204+
_replaySingleSaveEntry(
205+
clipDepth, prevTransform, _currentTransform, _clipStack);
180206
}
181207

182208
// Marks this pool for reuse.
@@ -216,7 +242,7 @@ class _CanvasPool extends _SaveStackTracking {
216242
/// Configures the canvas such that its coordinate system follows the scene's
217243
/// coordinate system, and the pixel ratio is applied such that CSS pixels are
218244
/// translated to bitmap pixels.
219-
void _initializeViewport() {
245+
void _initializeViewport(bool clearCanvas) {
220246
html.CanvasRenderingContext2D ctx = context;
221247
// Save the canvas state with top-level transforms so we can undo
222248
// any clips later when we reuse the canvas.
@@ -226,6 +252,9 @@ class _CanvasPool extends _SaveStackTracking {
226252
// We always start with identity transform because the surrounding transform
227253
// is applied on the DOM elements.
228254
ctx.setTransform(1, 0, 0, 1, 0, 0);
255+
if (clearCanvas) {
256+
ctx.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels);
257+
}
229258

230259
// This scale makes sure that 1 CSS pixel is translated to the correct
231260
// number of bitmap pixels.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:html' as html;
6+
import 'dart:js_util' as js_util;
7+
8+
import 'package:ui/ui.dart' hide TextStyle;
9+
import 'package:ui/src/engine.dart' as engine;
10+
import 'package:test/test.dart';
11+
12+
import 'package:web_engine_tester/golden_tester.dart';
13+
14+
/// Tests context save/restore.
15+
void main() async {
16+
const double screenWidth = 600.0;
17+
const double screenHeight = 800.0;
18+
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
19+
20+
// Commit a recording canvas to a bitmap, and compare with the expected
21+
Future<void> _checkScreenshot(engine.RecordingCanvas rc, String fileName,
22+
{Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async {
23+
final engine.EngineCanvas engineCanvas = engine.BitmapCanvas(screenRect);
24+
25+
rc.apply(engineCanvas);
26+
27+
// Wrap in <flt-scene> so that our CSS selectors kick in.
28+
final html.Element sceneElement = html.Element.tag('flt-scene');
29+
try {
30+
sceneElement.append(engineCanvas.rootElement);
31+
html.document.body.append(sceneElement);
32+
await matchGoldenFile('$fileName.png', region: region, maxDiffRate: 0.1);
33+
} finally {
34+
// The page is reused across tests, so remove the element after taking the
35+
// Scuba screenshot.
36+
sceneElement.remove();
37+
}
38+
}
39+
40+
setUp(() async {
41+
debugEmulateFlutterTesterEnvironment = true;
42+
await webOnlyInitializePlatform();
43+
webOnlyFontCollection.debugRegisterTestFonts();
44+
await webOnlyFontCollection.ensureFontsLoaded();
45+
});
46+
47+
// Regression test for https://github.com/flutter/flutter/issues/49429
48+
// Should clip with correct transform.
49+
test('Clips image with oval clip path', () async {
50+
final engine.RecordingCanvas rc =
51+
engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
52+
final Paint paint = Paint()
53+
..color = Color(0xFF00FF00)
54+
..style = PaintingStyle.fill;
55+
rc.save();
56+
final Path ovalPath = Path();
57+
ovalPath.addOval(Rect.fromLTWH(100, 30, 200, 100));
58+
rc.clipPath(ovalPath);
59+
rc.translate(-500, -500);
60+
rc.save();
61+
rc.translate(500, 500);
62+
rc.drawPath(ovalPath, paint);
63+
// The line below was causing SaveClipStack to incorrectly set
64+
// transform before path painting.
65+
rc.translate(-1000, -1000);
66+
rc.save();
67+
rc.restore();
68+
rc.restore();
69+
rc.restore();
70+
// The rectangle should paint without clipping since we restored
71+
// context.
72+
rc.drawRect(Rect.fromLTWH(0, 0, 4, 200), paint);
73+
await _checkScreenshot(rc, 'context_save_restore_transform');
74+
});
75+
76+
test('Should restore clip path', () async {
77+
final engine.RecordingCanvas rc =
78+
engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
79+
final Paint goodPaint = Paint()
80+
..color = Color(0x8000FF00)
81+
..style = PaintingStyle.fill;
82+
final Paint badPaint = Paint()
83+
..color = Color(0xFFFF0000)
84+
..style = PaintingStyle.fill;
85+
rc.save();
86+
final Path ovalPath = Path();
87+
ovalPath.addOval(Rect.fromLTWH(100, 30, 200, 100));
88+
rc.clipPath(ovalPath);
89+
rc.translate(-500, -500);
90+
rc.save();
91+
rc.restore();
92+
// The rectangle should be clipped against oval.
93+
rc.drawRect(Rect.fromLTWH(0, 0, 300, 300), badPaint);
94+
rc.restore();
95+
// The rectangle should paint without clipping since we restored
96+
// context.
97+
rc.drawRect(Rect.fromLTWH(0, 0, 200, 200), goodPaint);
98+
await _checkScreenshot(rc, 'context_save_restore_clip');
99+
});
100+
}

0 commit comments

Comments
 (0)