Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
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: 33b24a20bb9717699b847f633278f2b7bffde875
revision: 04c9d19f106cf0b1bf409bf691bd5334950553ea
105 changes: 54 additions & 51 deletions lib/web_ui/lib/src/engine/bitmap_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,29 +198,12 @@ class BitmapCanvas extends EngineCanvas {
}

/// Sets the global paint styles to correspond to [paint].
void _applyPaint(SurfacePaintData paint) {
ContextStateHandle contextHandle = _canvasPool.contextHandle;
contextHandle
..lineWidth = paint.strokeWidth ?? 1.0
..blendMode = paint.blendMode
..strokeCap = paint.strokeCap
..strokeJoin = paint.strokeJoin
..filter = _maskFilterToCss(paint.maskFilter);

if (paint.shader != null) {
final EngineGradient engineShader = paint.shader;
final Object paintStyle =
engineShader.createPaintStyle(_canvasPool.context);
contextHandle.fillStyle = paintStyle;
contextHandle.strokeStyle = paintStyle;
} else if (paint.color != null) {
final String colorString = colorToCssString(paint.color);
contextHandle.fillStyle = colorString;
contextHandle.strokeStyle = colorString;
} else {
contextHandle.fillStyle = '';
contextHandle.strokeStyle = '';
}
void _setUpPaint(SurfacePaintData paint) {
_canvasPool.contextHandle.setUpPaint(paint);
}

void _tearDownPaint() {
_canvasPool.contextHandle.tearDownPaint();
}

@override
Expand Down Expand Up @@ -299,50 +282,58 @@ class BitmapCanvas extends EngineCanvas {

@override
void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.strokeLine(p1, p2);
_tearDownPaint();
}

@override
void drawPaint(SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.fill();
_tearDownPaint();
}

@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.drawRect(rect, paint.style);
_tearDownPaint();
}

@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.drawRRect(rrect, paint.style);
_tearDownPaint();
}

@override
void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.drawDRRect(outer, inner, paint.style);
_tearDownPaint();
}

@override
void drawOval(ui.Rect rect, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.drawOval(rect, paint.style);
_tearDownPaint();
}

@override
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.drawCircle(c, radius, paint.style);
_tearDownPaint();
}

@override
void drawPath(ui.Path path, SurfacePaintData paint) {
_applyPaint(paint);
_setUpPaint(paint);
_canvasPool.drawPath(path, paint.style);
_tearDownPaint();
}

@override
Expand Down Expand Up @@ -442,8 +433,6 @@ class BitmapCanvas extends EngineCanvas {
!requiresClipping &&
paint.colorFilter == null) {
_drawImage(image, dst.topLeft, paint);
_childOverdraw = true;
_canvasPool.closeCurrentCanvas();
} else {
if (requiresClipping) {
save();
Expand Down Expand Up @@ -683,14 +672,15 @@ class BitmapCanvas extends EngineCanvas {
ctx.font = style.cssFontString;
_cachedLastStyle = style;
}
_applyPaint(paragraph._paint.paintData);

_setUpPaint(paragraph._paint.paintData);
double y = offset.dy + paragraph.alphabeticBaseline;
final int len = lines.length;
for (int i = 0; i < len; i++) {
_drawTextLine(style, lines[i], offset.dx, y);
y += paragraph._lineHeight;
}
_tearDownPaint();

return;
}

Expand Down Expand Up @@ -769,23 +759,28 @@ class BitmapCanvas extends EngineCanvas {
_canvasPool.currentTransform, vertices, blendMode, paint);
}

/// Stores paint data used by [drawPoints]. We cannot use the original paint
/// data object because painting style is determined by [ui.PointMode] and
/// not by [SurfacePointData.style].
static SurfacePaintData _drawPointsPaint = SurfacePaintData()
..strokeCap = ui.StrokeCap.round
..strokeJoin = ui.StrokeJoin.round
..blendMode = ui.BlendMode.srcOver;

@override
void drawPoints(ui.PointMode pointMode, Float32List points,
double strokeWidth, ui.Color color) {
ContextStateHandle contextHandle = _canvasPool.contextHandle;
contextHandle
..lineWidth = strokeWidth
..blendMode = ui.BlendMode.srcOver
..strokeCap = ui.StrokeCap.round
..strokeJoin = ui.StrokeJoin.round
..filter = '';
final String cssColor = colorToCssString(color);
void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) {
if (pointMode == ui.PointMode.points) {
contextHandle.fillStyle = cssColor;
_drawPointsPaint.style = ui.PaintingStyle.stroke;
} else {
contextHandle.strokeStyle = cssColor;
_drawPointsPaint.style = ui.PaintingStyle.fill;
}
_canvasPool.drawPoints(pointMode, points, strokeWidth / 2.0);
_drawPointsPaint.color = paint.color;
_drawPointsPaint.strokeWidth = paint.strokeWidth;
_drawPointsPaint.maskFilter = paint.maskFilter;

_setUpPaint(_drawPointsPaint);
_canvasPool.drawPoints(pointMode, points, paint.strokeWidth / 2.0);
_tearDownPaint();
}

@override
Expand Down Expand Up @@ -969,11 +964,19 @@ List<html.Element> _clipContent(List<_SaveClipEntry> clipStack,
return <html.Element>[root]..addAll(clipDefs);
}

String _maskFilterToCss(ui.MaskFilter maskFilter) {
if (maskFilter == null) {
/// Converts a [maskFilter] to the value to be used on a `<canvas>`.
///
/// Only supported in non-WebKit browsers.
String _maskFilterToCanvasFilter(ui.MaskFilter maskFilter) {
assert(
browserEngine != BrowserEngine.webkit,
'WebKit (Safari) does not support `filter` canvas property.',
);
if (maskFilter != null) {
return 'blur(${maskFilter.webOnlySigma}px)';
} else {
return 'none';
}
return 'blur(${maskFilter.webOnlySigma}px)';
}

int _filterIdCounter = 0;
Expand Down
139 changes: 128 additions & 11 deletions lib/web_ui/lib/src/engine/canvas_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class _CanvasPool extends _SaveStackTracking {
++_activeElementCount;

_context = _canvas.context2D;
_contextHandle = ContextStateHandle(_context);
_contextHandle = ContextStateHandle(this, _context);
_initializeViewport(requiresClearRect);
_replayClipStack();
}
Expand Down Expand Up @@ -624,7 +624,7 @@ class _CanvasPool extends _SaveStackTracking {
// alpha for the paint the path is painted in addition to the shadow,
// which is undesirable.
context.translate(shadow.offset.dx, shadow.offset.dy);
context.filter = _maskFilterToCss(
context.filter = _maskFilterToCanvasFilter(
ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blurWidth));
context.strokeStyle = '';
context.fillStyle = solidColor;
Expand Down Expand Up @@ -690,8 +690,10 @@ class _CanvasPool extends _SaveStackTracking {
// to initialize current values.
//
class ContextStateHandle {
html.CanvasRenderingContext2D context;
ContextStateHandle(this.context);
final html.CanvasRenderingContext2D context;
final _CanvasPool _canvasPool;

ContextStateHandle(this._canvasPool, this.context);
ui.BlendMode _currentBlendMode = ui.BlendMode.srcOver;
ui.StrokeCap _currentStrokeCap = ui.StrokeCap.butt;
ui.StrokeJoin _currentStrokeJoin = ui.StrokeJoin.miter;
Expand All @@ -700,7 +702,6 @@ class ContextStateHandle {
Object _currentFillStyle;
Object _currentStrokeStyle;
double _currentLineWidth = 1.0;
String _currentFilter = 'none';

set blendMode(ui.BlendMode blendMode) {
if (blendMode != _currentBlendMode) {
Expand Down Expand Up @@ -747,10 +748,124 @@ class ContextStateHandle {
}
}

set filter(String filter) {
if (_currentFilter != filter) {
_currentFilter = filter;
context.filter = filter;
ui.MaskFilter _currentFilter;
SurfacePaintData _lastUsedPaint;

/// The painting state.
///
/// Used to validate that the [setUpPaint] and [tearDownPaint] are called in
/// a correct sequence.
bool _debugIsPaintSetUp = false;

/// Whether to use WebKit's method of rendering [MaskFilter].
///
/// This is used in screenshot tests to test Safari codepaths.
static bool debugEmulateWebKitMaskFilter = false;

bool get _renderMaskFilterForWebkit => browserEngine == BrowserEngine.webkit || debugEmulateWebKitMaskFilter;

/// Sets paint properties on the current canvas.
///
/// [tearDownPaint] must be called after calling this method.
void setUpPaint(SurfacePaintData paint) {
if (assertionsEnabled) {
assert(!_debugIsPaintSetUp);
_debugIsPaintSetUp = true;
}

_lastUsedPaint = paint;
lineWidth = paint.strokeWidth ?? 1.0;
blendMode = paint.blendMode;
strokeCap = paint.strokeCap;
strokeJoin = paint.strokeJoin;

if (paint.shader != null) {
final EngineGradient engineShader = paint.shader;
final Object paintStyle =
engineShader.createPaintStyle(_canvasPool.context);
fillStyle = paintStyle;
strokeStyle = paintStyle;
} else if (paint.color != null) {
final String colorString = colorToCssString(paint.color);
fillStyle = colorString;
strokeStyle = colorString;
} else {
fillStyle = '';
strokeStyle = '';
}

final ui.MaskFilter maskFilter = paint?.maskFilter;
if (!_renderMaskFilterForWebkit) {
if (_currentFilter != maskFilter) {
_currentFilter = maskFilter;
context.filter = _maskFilterToCanvasFilter(maskFilter);
}
} else {
// WebKit does not support the `filter` property. Instead we apply a
// shadow to the shape of the same color as the paint and the same blur
// as the mask filter.
//
// Note that on WebKit the cached value of _currentFilter is not useful.
// Instead we destructure it into the shadow properties and cache those.
if (maskFilter != null) {
context.save();
context.shadowBlur = convertSigmaToRadius(maskFilter.webOnlySigma);
if (paint?.color != null) {
// Shadow color must be fully opaque.
context.shadowColor = colorToCssString(paint.color.withAlpha(255));
} else {
context.shadowColor = colorToCssString(const ui.Color(0xFF000000));
}

// On the web a shadow must always be painted together with the shape
// that casts it. In order to paint just the shadow, we offset the shape
// by a large enough value that it moved outside the canvas bounds, then
// offset the shadow in the opposite direction such that it lands exactly
// where the shape is.
const double kOutsideTheBoundsOffset = 50000;

context.translate(-kOutsideTheBoundsOffset, 0);

// Shadow offset is not affected by the current canvas context transform.
// We have to apply the transform ourselves. To do that we transform the
// tip of the vector from the shape to the shadow, then we transform the
// origin (0, 0). The desired shadow offset is the difference between the
// two. In vector notation, this is:
//
// transformedShadowDelta = M*shadowDelta - M*origin.
final Float32List tempVector = Float32List(2);
tempVector[0] = kOutsideTheBoundsOffset * window.devicePixelRatio;
_canvasPool.currentTransform.transform2(tempVector);
double shadowOffsetX = tempVector[0];
double shadowOffsetY = tempVector[1];

tempVector[0] = tempVector[1] = 0;
_canvasPool.currentTransform.transform2(tempVector);
context.shadowOffsetX = shadowOffsetX - tempVector[0];
context.shadowOffsetY = shadowOffsetY - tempVector[1];
}
}
}

/// Removes paint properties on the current canvas used by the last draw
/// command.
///
/// Not all properties are cleared. Properties that are set by all paint
/// commands prior to painting do not need to be cleared.
///
/// Must be called after calling [setUpPaint].
void tearDownPaint() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this get removed dart2js release? Maybe place all tearDownPaint() calls inside asserts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be removed. Otherwise the context won't be restored.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. likely very small will be inlined

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if we had access to dart2js inlining annotations.

if (assertionsEnabled) {
assert(_debugIsPaintSetUp);
_debugIsPaintSetUp = false;
}

final ui.MaskFilter maskFilter = _lastUsedPaint?.maskFilter;
if (maskFilter != null && _renderMaskFilterForWebkit) {
// On Safari (WebKit) we use a translated shadow to emulate
// MaskFilter.blur. We use restore to undo the translation and
// shadow attributes.
context.restore();
}
}

Expand Down Expand Up @@ -782,8 +897,10 @@ class ContextStateHandle {
_currentFillStyle = context.fillStyle;
context.strokeStyle = '';
_currentStrokeStyle = context.strokeStyle;
context.filter = 'none';
_currentFilter = 'none';
context.shadowBlur = 0;
context.shadowColor = 'none';
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.globalCompositeOperation = 'source-over';
_currentBlendMode = ui.BlendMode.srcOver;
context.lineWidth = 1.0;
Expand Down
3 changes: 1 addition & 2 deletions lib/web_ui/lib/src/engine/dom_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
}

@override
void drawPoints(ui.PointMode pointMode, Float32List points,
double strokeWidth, ui.Color color) {
void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) {
throw UnimplementedError();
}

Expand Down
3 changes: 1 addition & 2 deletions lib/web_ui/lib/src/engine/engine_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ abstract class EngineCanvas {
void drawVertices(
ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint);

void drawPoints(ui.PointMode pointMode, Float32List points,
double strokeWidth, ui.Color color);
void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint);

/// Extension of Canvas API to mark the end of a stream of painting commands
/// to enable re-use/dispose optimizations.
Expand Down
Loading