Skip to content

Commit

Permalink
[web] Fixes incorrect transform when context save and transforms are …
Browse files Browse the repository at this point in the history
…deferred. (flutter#16412)

* Fix transform order in clipStack replay
  • Loading branch information
ferhatb committed Feb 5, 2020
1 parent 31bf3e1 commit 8f89bac
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 17 deletions.
2 changes: 1 addition & 1 deletion lib/web_ui/dev/goldens_lock.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: 43254f4abddc2542ece540f222545970caf12908
revision: 1637835646ef187884ceeb59011d70c463429876
61 changes: 45 additions & 16 deletions lib/web_ui/lib/src/engine/canvas_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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) {
Expand All @@ -160,23 +170,39 @@ 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;
}

void _replayClipStack() {
// 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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions lib/web_ui/test/golden_tests/engine/canvas_context_test.dart
Original file line number Diff line number Diff line change
@@ -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<void> _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 <flt-scene> 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');
});
}

0 comments on commit 8f89bac

Please sign in to comment.