[web] Fix some clip and stroke calculations (flutter/engine#36801)

This commit is contained in:
Mouad Debbar 2022-10-18 18:06:18 -04:00 committed by GitHub
parent b926c27d4a
commit e684d9c8a6
4 changed files with 311 additions and 53 deletions

View File

@ -440,13 +440,10 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFillAndStroke(paint)) {
rect = adjustRectForDom(rect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
} else {
setUpPaint(paint, rect);
_canvasPool.drawRect(rect, paint.style);
@ -482,16 +479,12 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
final ui.Rect rect = rrect.outerRect;
if (_useDomForRenderingFillAndStroke(paint)) {
final ui.Rect rect = adjustRectForDom(rrect.outerRect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-rrect', _canvasPool.currentTransform);
applyRRectBorderRadius(element.style, rrect);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
} else {
setUpPaint(paint, rrect.outerRect);
_canvasPool.drawRRect(rrect, paint.style);
@ -509,13 +502,10 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawOval(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFill(paint)) {
rect = adjustRectForDom(rect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-oval', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
element.style.borderRadius =
'${rect.width / 2.0}px / ${rect.height / 2.0}px';
} else {
@ -527,15 +517,11 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
final ui.Rect rect = ui.Rect.fromCircle(center: c, radius: radius);
if (_useDomForRenderingFillAndStroke(paint)) {
final ui.Rect rect = adjustRectForDom(ui.Rect.fromCircle(center: c, radius: radius), paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-circle', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
element.style.borderRadius = '50%';
} else {
setUpPaint(
@ -555,21 +541,19 @@ class BitmapCanvas extends EngineCanvas {
final SurfacePath surfacePath = path as SurfacePath;
final ui.Rect? pathAsLine = surfacePath.toStraightLine();
if (pathAsLine != null) {
final ui.Rect rect = (pathAsLine.top == pathAsLine.bottom)
ui.Rect rect = (pathAsLine.top == pathAsLine.bottom)
? ui.Rect.fromLTWH(
pathAsLine.left, pathAsLine.top, pathAsLine.width, 1)
: ui.Rect.fromLTWH(
pathAsLine.left, pathAsLine.top, 1, pathAsLine.height);
rect = adjustRectForDom(rect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(math.min(rect.left, rect.right),
math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
return;
}
final ui.Rect? pathAsRect = surfacePath.toRect();
if (pathAsRect != null) {
drawRect(pathAsRect, paint);

View File

@ -75,14 +75,16 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
rect = adjustRectForDom(rect, paint);
currentElement.append(
buildDrawRectElement(rect, paint, 'draw-rect', currentTransform));
}
@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
final ui.Rect outerRect = adjustRectForDom(rrect.outerRect, paint);
final DomElement element = buildDrawRectElement(
rrect.outerRect, paint, 'draw-rrect', currentTransform);
outerRect, paint, 'draw-rrect', currentTransform);
applyRRectBorderRadius(element.style, rrect);
currentElement.append(element);
}
@ -160,8 +162,77 @@ ui.Color blurColor(ui.Color color, double sigma) {
return ui.Color((reducedAlpha & 0xff) << 24 | (color.value & 0x00ffffff));
}
/// When drawing a shape (rect, rrect, circle, etc) in DOM/CSS, the [rect] given
/// by Flutter needs to be adjusted to what DOM/CSS expect.
///
/// This method takes Flutter's [rect] and produces a new rect that can be used
/// to generate the correct CSS properties to match Flutter's expectations.
///
///
/// Here's what Flutter's given [rect] and [paint.strokeWidth] represent:
///
/// top-left
///
/// x x
/// |
/// |
/// | height
/// |
/// |
/// x x
///
/// stroke-width ----
/// ------------------- width
///
///
///
/// In the DOM/CSS, here's how the coordinates should look like:
///
/// top-left
/// x
///
/// xx
/// |
/// | height
/// |
/// xx
///
///
/// border-width ----
/// --------------- width
///
/// As shown in the drawing above, the width/height don't start at the top-left
/// coordinates. Instead, they start from the inner top-left (inside the border).
ui.Rect adjustRectForDom(ui.Rect rect, SurfacePaintData paint) {
double left = math.min(rect.left, rect.right);
double top = math.min(rect.top, rect.bottom);
double width = rect.width.abs();
double height = rect.height.abs();
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
if (isStroke && strokeWidth > 0.0) {
left -= strokeWidth / 2.0;
top -= strokeWidth / 2.0;
// width and height shouldn't go below zero.
width = math.max(0, width - strokeWidth);
height = math.max(0, height - strokeWidth);
}
if (left != rect.left ||
top != rect.top ||
width != rect.width ||
height != rect.height) {
return ui.Rect.fromLTWH(left, top, width, height);
}
return rect;
}
DomHTMLElement buildDrawRectElement(
ui.Rect rect, SurfacePaintData paint, String tagName, Matrix4 transform) {
assert(rect.left <= rect.right);
assert(rect.top <= rect.bottom);
final DomHTMLElement rectangle = domDocument.createElement(tagName) as
DomHTMLElement;
assert(() {
@ -172,26 +243,11 @@ DomHTMLElement buildDrawRectElement(
String effectiveTransform;
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
final double left = math.min(rect.left, rect.right);
final double right = math.max(rect.left, rect.right);
final double top = math.min(rect.top, rect.bottom);
final double bottom = math.max(rect.top, rect.bottom);
if (transform.isIdentity()) {
if (isStroke) {
effectiveTransform =
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
} else {
effectiveTransform = 'translate(${left}px, ${top}px)';
}
effectiveTransform = 'translate(${rect.left}px, ${rect.top}px)';
} else {
// Clone to avoid mutating _transform.
final Matrix4 translated = transform.clone();
if (isStroke) {
translated.translate(
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
} else {
translated.translate(left, top);
}
// Clone to avoid mutating `transform`.
final Matrix4 translated = transform.clone()..translate(rect.left, rect.top);
effectiveTransform = matrix4ToCssTransform(translated);
}
final DomCSSStyleDeclaration style = rectangle.style;
@ -216,15 +272,14 @@ DomHTMLElement buildDrawRectElement(
}
}
style
..width = '${rect.width}px'
..height = '${rect.height}px';
if (isStroke) {
style
..width = '${right - left - strokeWidth}px'
..height = '${bottom - top - strokeWidth}px'
..border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
style.border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
} else {
style
..width = '${right - left}px'
..height = '${bottom - top}px'
..backgroundColor = cssColor
..backgroundImage = _getBackgroundImageCssValue(paint.shader, rect);
}

View File

@ -0,0 +1,77 @@
// 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 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
group('$adjustRectForDom', () {
test('does not change rect when not necessary', () async {
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
rect,
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=0),
rect,
);
});
test('takes stroke width into consideration', () async {
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=1),
const Rect.fromLTWH(9.5, 19.5, 139, 159),
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=10),
const Rect.fromLTWH(5, 15, 130, 150),
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=15),
const Rect.fromLTWH(2.5, 12.5, 125, 145),
);
});
test('flips rect when necessary', () {
Rect rect = const Rect.fromLTWH(100, 200, -40, -60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
const Rect.fromLTWH(60, 140, 40, 60),
);
rect = const Rect.fromLTWH(100, 200, 40, -60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
const Rect.fromLTWH(100, 140, 40, 60),
);
rect = const Rect.fromLTWH(100, 200, -40, 60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
const Rect.fromLTWH(60, 200, 40, 60),
);
});
test('handles stroke width greater than width or height', () {
const Rect rect = Rect.fromLTWH(100, 200, 20, 70);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=50),
const Rect.fromLTWH(75, 175, 0, 20),
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=80),
const Rect.fromLTWH(60, 160, 0, 0),
);
});
});
}

View File

@ -0,0 +1,142 @@
// 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 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
test('rect stroke with clip', () async {
const Rect region = Rect.fromLTWH(0, 0, 250, 250);
// Set `hasParagraphs` to true to force DOM rendering.
final BitmapCanvas canvas =
BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
const Rect rect = Rect.fromLTWH(0, 0, 150, 150);
canvas.clipRect(rect.inflate(10.0), ClipOp.intersect);
canvas.drawRect(
rect,
SurfacePaintData()
..color = const Color(0x6fff0000)
..strokeWidth = 20.0
..style = PaintingStyle.stroke,
);
canvas.drawRect(
rect,
SurfacePaintData()
..color = const Color(0x6f0000ff)
..strokeWidth = 10.0
..style = PaintingStyle.stroke,
);
canvas.drawRect(
rect,
SurfacePaintData()
..color = const Color(0xff000000)
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
domDocument.body!.style.margin = '0px';
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('rect_clip_strokes_dom.png', region: region);
canvas.rootElement.remove();
});
test('rrect stroke with clip', () async {
const Rect region = Rect.fromLTWH(0, 0, 250, 250);
// Set `hasParagraphs` to true to force DOM rendering.
final BitmapCanvas canvas =
BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
final RRect rrect = RRect.fromRectAndRadius(
const Rect.fromLTWH(0, 0, 150, 150),
const Radius.circular(20),
);
canvas.clipRect(rrect.outerRect.inflate(10.0), ClipOp.intersect);
canvas.drawRRect(
rrect,
SurfacePaintData()
..color = const Color(0x6fff0000)
..strokeWidth = 20.0
..style = PaintingStyle.stroke,
);
canvas.drawRRect(
rrect,
SurfacePaintData()
..color = const Color(0x6f0000ff)
..strokeWidth = 10.0
..style = PaintingStyle.stroke,
);
canvas.drawRRect(
rrect,
SurfacePaintData()
..color = const Color(0xff000000)
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
domDocument.body!.style.margin = '0px';
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('rrect_clip_strokes_dom.png', region: region);
canvas.rootElement.remove();
});
test('circle stroke with clip', () async {
const Rect region = Rect.fromLTWH(0, 0, 250, 250);
// Set `hasParagraphs` to true to force DOM rendering.
final BitmapCanvas canvas =
BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
const Rect rect = Rect.fromLTWH(0, 0, 150, 150);
canvas.clipRect(rect.inflate(10.0), ClipOp.intersect);
canvas.drawCircle(
rect.center,
rect.width / 2,
SurfacePaintData()
..color = const Color(0x6fff0000)
..strokeWidth = 20.0
..style = PaintingStyle.stroke,
);
canvas.drawCircle(
rect.center,
rect.width / 2,
SurfacePaintData()
..color = const Color(0x6f0000ff)
..strokeWidth = 10.0
..style = PaintingStyle.stroke,
);
canvas.drawCircle(
rect.center,
rect.width / 2,
SurfacePaintData()
..color = const Color(0xff000000)
..strokeWidth = 1.0
..style = PaintingStyle.stroke,
);
domDocument.body!.style.margin = '0px';
domDocument.body!.append(canvas.rootElement);
await matchGoldenFile('circle_clip_strokes_dom.png', region: region);
canvas.rootElement.remove();
});
}