Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Fixes incorrect transform when context save and transforms are deferred. #16412

Merged
merged 7 commits into from
Feb 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
}