mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Fix some clip and stroke calculations (flutter/engine#36801)
This commit is contained in:
parent
b926c27d4a
commit
e684d9c8a6
@ -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);
|
||||
|
||||
@ -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─────────────────────────┐
|
||||
/// │ │
|
||||
/// │ x───────────────x │←←
|
||||
/// │ │ │ │ |
|
||||
/// │ │ │ │ | height
|
||||
/// │ │ │ │ |
|
||||
/// │ x───────────────x │←←
|
||||
/// │ │
|
||||
/// └─────────────────────────┘
|
||||
/// 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);
|
||||
}
|
||||
|
||||
77
engine/src/flutter/lib/web_ui/test/html/dom_canvas_test.dart
Normal file
77
engine/src/flutter/lib/web_ui/test/html/dom_canvas_test.dart
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user