diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 9a2ccfceda885..3f29cf7ba752e 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -398,6 +398,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/painting.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -426,7 +427,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/recording_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/render_vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -451,8 +451,11 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/clip.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/debug_canvas_reuse_overlay.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/offset.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/opacity.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/painting.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/picture.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/platform_view.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/recording_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index a45179ec73f17..092a6cbcb621b 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -38,6 +38,7 @@ part 'engine/compositor/layer_scene_builder.dart'; part 'engine/compositor/layer_tree.dart'; part 'engine/compositor/n_way_canvas.dart'; part 'engine/compositor/path.dart'; +part 'engine/compositor/painting.dart'; part 'engine/compositor/path_metrics.dart'; part 'engine/compositor/picture.dart'; part 'engine/compositor/picture_recorder.dart'; @@ -65,7 +66,6 @@ part 'engine/platform_views.dart'; part 'engine/plugins.dart'; part 'engine/pointer_binding.dart'; part 'engine/pointer_converter.dart'; -part 'engine/recording_canvas.dart'; part 'engine/render_vertices.dart'; part 'engine/rrect_renderer.dart'; part 'engine/semantics/accessibility.dart'; @@ -90,8 +90,11 @@ part 'engine/surface/clip.dart'; part 'engine/surface/debug_canvas_reuse_overlay.dart'; part 'engine/surface/offset.dart'; part 'engine/surface/opacity.dart'; +part 'engine/surface/painting.dart'; +part 'engine/surface/path_metrics.dart'; part 'engine/surface/picture.dart'; part 'engine/surface/platform_view.dart'; +part 'engine/surface/recording_canvas.dart'; part 'engine/surface/scene.dart'; part 'engine/surface/scene_builder.dart'; part 'engine/surface/surface.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 84cfdb45faf9b..e4dde41d591ca 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -244,7 +244,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { html.CanvasRenderingContext2D get ctx => _ctx; /// Sets the global paint styles to correspond to [paint]. - void _applyPaint(ui.PaintData paint) { + void _applyPaint(SurfacePaintData paint) { ctx.globalCompositeOperation = _stringForBlendMode(paint.blendMode) ?? 'source-over'; ctx.lineWidth = paint.strokeWidth ?? 1.0; @@ -273,7 +273,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } } - void _strokeOrFill(ui.PaintData paint, {bool resetPaint = true}) { + void _strokeOrFill(SurfacePaintData paint, {bool resetPaint = true}) { switch (paint.style) { case ui.PaintingStyle.stroke: ctx.stroke(); @@ -448,7 +448,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawLine(ui.Offset p1, ui.Offset p2, ui.PaintData paint) { + void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { _applyPaint(paint); ctx.beginPath(); ctx.moveTo(p1.dx, p1.dy); @@ -458,7 +458,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawPaint(ui.PaintData paint) { + void drawPaint(SurfacePaintData paint) { _applyPaint(paint); ctx.beginPath(); @@ -471,7 +471,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawRect(ui.Rect rect, ui.PaintData paint) { + void drawRect(ui.Rect rect, SurfacePaintData paint) { _applyPaint(paint); ctx.beginPath(); ctx.rect(rect.left, rect.top, rect.width, rect.height); @@ -479,14 +479,14 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawRRect(ui.RRect rrect, ui.PaintData paint) { + void drawRRect(ui.RRect rrect, SurfacePaintData paint) { _applyPaint(paint); _RRectToCanvasRenderer(ctx).render(rrect); _strokeOrFill(paint); } @override - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint) { + void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { _applyPaint(paint); _RRectRenderer renderer = _RRectToCanvasRenderer(ctx); renderer.render(outer); @@ -495,7 +495,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawOval(ui.Rect rect, ui.PaintData paint) { + void drawOval(ui.Rect rect, SurfacePaintData paint) { _applyPaint(paint); ctx.beginPath(); ctx.ellipse(rect.center.dx, rect.center.dy, rect.width / 2, rect.height / 2, @@ -504,7 +504,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawCircle(ui.Offset c, double radius, ui.PaintData paint) { + void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { _applyPaint(paint); ctx.beginPath(); ctx.ellipse(c.dx, c.dy, radius, radius, 0, 0, 2.0 * math.pi, false); @@ -512,7 +512,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawPath(ui.Path path, ui.PaintData paint) { + void drawPath(ui.Path path, SurfacePaintData paint) { _applyPaint(paint); _runPath(path); _strokeOrFill(paint); @@ -534,14 +534,14 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { // paint the shadow without the path itself, but if we use a non-zero // alpha for the paint the path is painted in addition to the shadow, // which is undesirable. - final ui.Paint paint = ui.Paint() + final SurfacePaint paint = SurfacePaint() ..color = shadow.color ..style = ui.PaintingStyle.fill ..strokeWidth = 0.0 ..maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blur); _ctx.save(); _ctx.translate(shadow.offsetX, shadow.offsetY); - final ui.PaintData paintData = paint.webOnlyPaintData; + final SurfacePaintData paintData = paint.paintData; _applyPaint(paintData); _runPath(path); _strokeOrFill(paintData, resetPaint: false); @@ -555,12 +555,12 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { // the opaque occluder. For that reason, we fill with the shadow color, // and set the shadow color to fully opaque. This way, the visible // pixels are less opaque and less noticeable. - final ui.Paint paint = ui.Paint() + final SurfacePaint paint = SurfacePaint() ..color = shadow.color ..style = ui.PaintingStyle.fill ..strokeWidth = 0.0; _ctx.save(); - final ui.PaintData paintData = paint.webOnlyPaintData; + final SurfacePaintData paintData = paint.paintData; _applyPaint(paintData); _ctx.shadowBlur = shadow.blur; _ctx.shadowColor = shadow.color.withAlpha(0xff).toCssString(); @@ -576,7 +576,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } @override - void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) { + void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { _applyPaint(paint); final HtmlImage htmlImage = image; final html.ImageElement imgElement = htmlImage.cloneImageElement(); @@ -607,7 +607,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { @override void drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) { + ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { final HtmlImage htmlImage = image; final bool requiresClipping = src.left != 0 || src.top != 0 || @@ -698,8 +698,8 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { if (paragraph._drawOnCanvas && _childOverdraw == false) { final List lines = paragraph._measurementResult.lines; - final ui.PaintData backgroundPaint = - paragraph._background?.webOnlyPaintData; + final SurfacePaintData backgroundPaint = + paragraph._background?.paintData; if (backgroundPaint != null) { final ui.Rect rect = ui.Rect.fromLTWH( offset.dx, offset.dy, paragraph.width, paragraph.height); @@ -710,7 +710,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { ctx.font = style.cssFontString; _cachedLastStyle = style; } - _applyPaint(paragraph._paint.webOnlyPaintData); + _applyPaint(paragraph._paint.paintData); final double x = offset.dx + paragraph._alignOffset; double y = offset.dy + paragraph.alphabeticBaseline; @@ -764,7 +764,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { /// and use a SkTriColorShader to render. @override void drawVertices( - ui.Vertices vertices, ui.BlendMode blendMode, ui.PaintData paint) { + ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { // TODO(flutter_web): Implement shaders for [Paint.shader] and // blendMode. https://github.com/flutter/flutter/issues/40096 // Move rendering to OffscreenCanvas so that transform is preserved @@ -790,7 +790,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { } /// 'Runs' the given [path] by applying all of its commands to the canvas. - void _runPath(ui.Path path) { + void _runPath(SurfacePath path) { ctx.beginPath(); for (Subpath subpath in path.subpaths) { for (PathCommand command in subpath.commands) { diff --git a/lib/web_ui/lib/src/engine/compositor/canvas.dart b/lib/web_ui/lib/src/engine/compositor/canvas.dart index 81210d35756dc..9a71ba282539a 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvas.dart @@ -55,7 +55,7 @@ class SkCanvas { double startAngle, double sweepAngle, bool useCenter, - ui.Paint paint, + SkPaint paint, ) { const double toDegrees = 180 / math.pi; skCanvas.callMethod('drawArc', [ @@ -63,12 +63,12 @@ class SkCanvas { startAngle * toDegrees, sweepAngle * toDegrees, useCenter, - makeSkPaint(paint), + paint.makeSkPaint(), ]); } void drawAtlasRaw( - ui.Paint paint, + SkPaint paint, ui.Image atlas, Float32List rstTransforms, Float32List rects, @@ -80,18 +80,18 @@ class SkCanvas { skAtlas.skImage, rects, rstTransforms, - makeSkPaint(paint), + paint.makeSkPaint(), makeSkBlendMode(blendMode), colors, ]); } - void drawCircle(ui.Offset c, double radius, ui.Paint paint) { + void drawCircle(ui.Offset c, double radius, SkPaint paint) { skCanvas.callMethod('drawCircle', [ c.dx, c.dy, radius, - makeSkPaint(paint), + paint.makeSkPaint(), ]); } @@ -102,65 +102,65 @@ class SkCanvas { ]); } - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) { + void drawDRRect(ui.RRect outer, ui.RRect inner, SkPaint paint) { skCanvas.callMethod('drawDRRect', [ makeSkRRect(outer), makeSkRRect(inner), - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) { + void drawImage(ui.Image image, ui.Offset offset, SkPaint paint) { final SkImage skImage = image; skCanvas.callMethod('drawImage', [ skImage.skImage, offset.dx, offset.dy, - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, ui.Paint paint) { + void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, SkPaint paint) { final SkImage skImage = image; skCanvas.callMethod('drawImageRect', [ skImage.skImage, makeSkRect(src), makeSkRect(dst), - makeSkPaint(paint), + paint.makeSkPaint(), false, ]); } void drawImageNine( - ui.Image image, ui.Rect center, ui.Rect dst, ui.Paint paint) { + ui.Image image, ui.Rect center, ui.Rect dst, SkPaint paint) { final SkImage skImage = image; skCanvas.callMethod('drawImageNine', [ skImage.skImage, makeSkRect(center), makeSkRect(dst), - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawLine(ui.Offset p1, ui.Offset p2, ui.Paint paint) { + void drawLine(ui.Offset p1, ui.Offset p2, SkPaint paint) { skCanvas.callMethod('drawLine', [ p1.dx, p1.dy, p2.dx, p2.dy, - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawOval(ui.Rect rect, ui.Paint paint) { + void drawOval(ui.Rect rect, SkPaint paint) { skCanvas.callMethod('drawOval', [ makeSkRect(rect), - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawPaint(ui.Paint paint) { - skCanvas.callMethod('drawPaint', [makeSkPaint(paint)]); + void drawPaint(SkPaint paint) { + skCanvas.callMethod('drawPaint', [paint.makeSkPaint()]); } void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { @@ -172,8 +172,8 @@ class SkCanvas { ]); } - void drawPath(ui.Path path, ui.Paint paint) { - final js.JsObject skPaint = makeSkPaint(paint); + void drawPath(ui.Path path, SkPaint paint) { + final js.JsObject skPaint = paint.makeSkPaint(); final SkPath enginePath = path; final js.JsObject skPath = enginePath._skPath; skCanvas.callMethod('drawPath', [skPath, skPaint]); @@ -184,24 +184,24 @@ class SkCanvas { skCanvas.callMethod('drawPicture', [skPicture.skPicture]); } - void drawPoints(ui.Paint paint, ui.PointMode pointMode, Float32List points) { + void drawPoints(SkPaint paint, ui.PointMode pointMode, Float32List points) { skCanvas.callMethod('drawPoints', [ makeSkPointMode(pointMode), points, - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawRRect(ui.RRect rrect, ui.Paint paint) { + void drawRRect(ui.RRect rrect, SkPaint paint) { skCanvas.callMethod('drawRRect', [ makeSkRRect(rrect), - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void drawRect(ui.Rect rect, ui.Paint paint) { + void drawRect(ui.Rect rect, SkPaint paint) { final js.JsObject skRect = makeSkRect(rect); - final js.JsObject skPaint = makeSkPaint(paint); + final js.JsObject skPaint = paint.makeSkPaint(); skCanvas.callMethod('drawRect', [skRect, skPaint]); } @@ -212,12 +212,12 @@ class SkCanvas { } void drawVertices( - ui.Vertices vertices, ui.BlendMode blendMode, ui.Paint paint) { + ui.Vertices vertices, ui.BlendMode blendMode, SkPaint paint) { SkVertices skVertices = vertices; skCanvas.callMethod('drawVertices', [ skVertices.skVertices, makeSkBlendMode(blendMode), - makeSkPaint(paint) + paint.makeSkPaint() ]); } @@ -238,15 +238,15 @@ class SkCanvas { return skCanvas.callMethod('save'); } - void saveLayer(ui.Rect bounds, ui.Paint paint) { + void saveLayer(ui.Rect bounds, SkPaint paint) { skCanvas.callMethod('saveLayer', [ makeSkRect(bounds), - makeSkPaint(paint), + paint.makeSkPaint(), ]); } - void saveLayerWithoutBounds(ui.Paint paint) { - skCanvas.callMethod('saveLayer', [null, makeSkPaint(paint)]); + void saveLayerWithoutBounds(SkPaint paint) { + skCanvas.callMethod('saveLayer', [null, paint.makeSkPaint()]); } void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { diff --git a/lib/web_ui/lib/src/engine/compositor/painting.dart b/lib/web_ui/lib/src/engine/compositor/painting.dart new file mode 100644 index 0000000000000..fdc0ab8eac42e --- /dev/null +++ b/lib/web_ui/lib/src/engine/compositor/painting.dart @@ -0,0 +1,199 @@ +// 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. + +part of engine; + +/// The implementation of [ui.Paint] used by the CanvasKit backend. +class SkPaint implements ui.Paint { + SkPaint(); + + static const ui.Color _defaultPaintColor = ui.Color(0xFF000000); + + @override + ui.BlendMode get blendMode => _blendMode; + @override + set blendMode(ui.BlendMode value) { + _blendMode = value; + } + ui.BlendMode _blendMode = ui.BlendMode.srcOver; + + @override + ui.PaintingStyle get style => _style; + @override + set style(ui.PaintingStyle value) { + _style = value; + } + ui.PaintingStyle _style = ui.PaintingStyle.fill; + + @override + double get strokeWidth => _strokeWidth; + @override + set strokeWidth(double value) { + _strokeWidth = value; + } + double _strokeWidth = 0.0; + + @override + ui.StrokeCap get strokeCap => _strokeCap; + @override + set strokeCap(ui.StrokeCap value) { + _strokeCap = value; + } + ui.StrokeCap _strokeCap = ui.StrokeCap.butt; + + @override + ui.StrokeJoin get strokeJoin => _strokeJoin; + @override + set strokeJoin(ui.StrokeJoin value) { + _strokeJoin = value; + } + ui.StrokeJoin _strokeJoin = ui.StrokeJoin.miter; + + @override + bool get isAntiAlias => _isAntiAlias; + @override + set isAntiAlias(bool value) { + _isAntiAlias = value; + } + bool _isAntiAlias = true; + + @override + ui.Color get color => _color; + @override + set color(ui.Color value) { + _color = value; + } + ui.Color _color = _defaultPaintColor; + + @override + bool get invertColors => _invertColors; + @override + set invertColors(bool value) { + _invertColors = value; + } + bool _invertColors = false; + + @override + ui.Shader get shader => _shader; + @override + set shader(ui.Shader value) { + _shader = value; + } + ui.Shader _shader; + + @override + ui.MaskFilter get maskFilter => _maskFilter; + @override + set maskFilter(ui.MaskFilter value) { + _maskFilter = value; + } + ui.MaskFilter _maskFilter; + + @override + ui.FilterQuality get filterQuality => _filterQuality; + @override + set filterQuality(ui.FilterQuality value) { + _filterQuality = value; + } + ui.FilterQuality _filterQuality = ui.FilterQuality.none; + + @override + ui.ColorFilter get colorFilter => _colorFilter; + @override + set colorFilter(ui.ColorFilter value) { + _colorFilter = value; + } + ui.ColorFilter _colorFilter; + + @override + double get strokeMiterLimit => _strokeMiterLimit; + @override + set strokeMiterLimit(double value) { + _strokeMiterLimit = value; + } + double _strokeMiterLimit = 0.0; + + @override + ui.ImageFilter get imageFilter => _imageFilter; + @override + set imageFilter(ui.ImageFilter value) { + _imageFilter = value; + } + ui.ImageFilter _imageFilter; + + js.JsObject makeSkPaint() { + final js.JsObject skPaint = js.JsObject(canvasKit['SkPaint']); + + if (shader != null) { + final EngineGradient engineShader = shader; + skPaint.callMethod( + 'setShader', [engineShader.createSkiaShader()]); + } + + if (color != null) { + skPaint.callMethod('setColor', [color.value]); + } + + js.JsObject skPaintStyle; + switch (style) { + case ui.PaintingStyle.stroke: + skPaintStyle = canvasKit['PaintStyle']['Stroke']; + break; + case ui.PaintingStyle.fill: + skPaintStyle = canvasKit['PaintStyle']['Fill']; + break; + } + skPaint.callMethod('setStyle', [skPaintStyle]); + + js.JsObject skBlendMode = makeSkBlendMode(blendMode); + if (skBlendMode != null) { + skPaint.callMethod('setBlendMode', [skBlendMode]); + } + + skPaint.callMethod('setAntiAlias', [isAntiAlias]); + + if (strokeWidth > 0.0) { + skPaint.callMethod('setStrokeWidth', [strokeWidth]); + } + + if (maskFilter != null) { + final ui.BlurStyle blurStyle = maskFilter.webOnlyBlurStyle; + final double sigma = maskFilter.webOnlySigma; + + js.JsObject skBlurStyle; + switch (blurStyle) { + case ui.BlurStyle.normal: + skBlurStyle = canvasKit['BlurStyle']['Normal']; + break; + case ui.BlurStyle.solid: + skBlurStyle = canvasKit['BlurStyle']['Solid']; + break; + case ui.BlurStyle.outer: + skBlurStyle = canvasKit['BlurStyle']['Outer']; + break; + case ui.BlurStyle.inner: + skBlurStyle = canvasKit['BlurStyle']['Inner']; + break; + } + + final js.JsObject skMaskFilter = canvasKit + .callMethod('MakeBlurMaskFilter', [skBlurStyle, sigma, true]); + skPaint.callMethod('setMaskFilter', [skMaskFilter]); + } + + if (imageFilter != null) { + final SkImageFilter skImageFilter = imageFilter; + skPaint.callMethod( + 'setImageFilter', [skImageFilter.skImageFilter]); + } + + if (colorFilter != null) { + EngineColorFilter engineFilter = colorFilter; + SkColorFilter skFilter = engineFilter._toSkColorFilter(); + skPaint.callMethod('setColorFilter', [skFilter.skColorFilter]); + } + + return skPaint; + } +} diff --git a/lib/web_ui/lib/src/engine/compositor/path.dart b/lib/web_ui/lib/src/engine/compositor/path.dart index 9995ffc5d3cdc..56d9883589823 100644 --- a/lib/web_ui/lib/src/engine/compositor/path.dart +++ b/lib/web_ui/lib/src/engine/compositor/path.dart @@ -15,6 +15,11 @@ class SkPath implements ui.Path { fillType = ui.PathFillType.nonZero; } + // TODO(yjbanov): implement: https://github.com/flutter/flutter/issues/46812 + SkPath.from(SkPath other) { + throw UnimplementedError('SkPath.from is not implemented in the CanvasKit backend'); + } + SkPath._fromSkPath(js.JsObject skPath) : _skPath = skPath; ui.PathFillType _fillType; @@ -307,12 +312,6 @@ class SkPath implements ui.Path { return SkPath._fromSkPath(newPath); } - @override - List get subpaths { - throw UnimplementedError( - 'Path.subpaths is not used in the CanvasKit backend.'); - } - @override ui.Path transform(Float64List matrix4) { final js.JsObject newPath = _skPath.callMethod('copy'); @@ -320,30 +319,6 @@ class SkPath implements ui.Path { return SkPath._fromSkPath(newPath); } - @override - Ellipse get webOnlyPathAsCircle { - throw new UnimplementedError( - 'webOnlyPathAsCircle is not used in the CanvasKit backend.'); - } - - @override - ui.Rect get webOnlyPathAsRect { - throw new UnimplementedError( - 'webOnlyPathAsRect is not used in the CanvasKit backend.'); - } - - @override - ui.RRect get webOnlyPathAsRoundedRect { - throw new UnimplementedError( - 'webOnlyPathAsRoundedRect is not used in the CanvasKit backend.'); - } - - @override - List webOnlySerializeToCssPaint() { - throw new UnimplementedError( - 'webOnlySerializeToCssPaint is not used in the CanvasKit backend.'); - } - String toSvgString() { return _skPath.callMethod('toSVGString'); } diff --git a/lib/web_ui/lib/src/engine/compositor/text.dart b/lib/web_ui/lib/src/engine/compositor/text.dart index 051c6ce06d6c4..4a8c836497592 100644 --- a/lib/web_ui/lib/src/engine/compositor/text.dart +++ b/lib/web_ui/lib/src/engine/compositor/text.dart @@ -164,15 +164,15 @@ class SkTextStyle implements ui.TextStyle { double wordSpacing, double height, ui.Locale locale, - ui.Paint background, - ui.Paint foreground, + SkPaint background, + SkPaint foreground, List shadows, List fontFeatures, }) { final Map style = {}; if (background != null) { - style['backgroundColor'] = makeSkPaint(background); + style['backgroundColor'] = background.makeSkPaint(); } if (color != null) { @@ -221,7 +221,7 @@ class SkTextStyle implements ui.TextStyle { } if (foreground != null) { - style['foreground'] = makeSkPaint(foreground); + style['foreground'] = foreground.makeSkPaint(); } // TODO(hterkelsen): Add support for diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/compositor/util.dart index a62655d10c8b4..9256d395b88ba 100644 --- a/lib/web_ui/lib/src/engine/compositor/util.dart +++ b/lib/web_ui/lib/src/engine/compositor/util.dart @@ -150,83 +150,6 @@ js.JsObject makeSkBlendMode(ui.BlendMode blendMode) { } } -js.JsObject makeSkPaint(ui.Paint paint) { - if (paint == null) return null; - - final dynamic skPaint = js.JsObject(canvasKit['SkPaint']); - - if (paint.shader != null) { - final EngineGradient engineShader = paint.shader; - skPaint.callMethod( - 'setShader', [engineShader.createSkiaShader()]); - } - - if (paint.color != null) { - skPaint.callMethod('setColor', [paint.color.value]); - } - - js.JsObject skPaintStyle; - switch (paint.style) { - case ui.PaintingStyle.stroke: - skPaintStyle = canvasKit['PaintStyle']['Stroke']; - break; - case ui.PaintingStyle.fill: - skPaintStyle = canvasKit['PaintStyle']['Fill']; - break; - } - skPaint.callMethod('setStyle', [skPaintStyle]); - - js.JsObject skBlendMode = makeSkBlendMode(paint.blendMode); - if (skBlendMode != null) { - skPaint.callMethod('setBlendMode', [skBlendMode]); - } - - skPaint.callMethod('setAntiAlias', [paint.isAntiAlias]); - - if (paint.strokeWidth != 0.0) { - skPaint.callMethod('setStrokeWidth', [paint.strokeWidth]); - } - - if (paint.maskFilter != null) { - final ui.BlurStyle blurStyle = paint.maskFilter.webOnlyBlurStyle; - final double sigma = paint.maskFilter.webOnlySigma; - - js.JsObject skBlurStyle; - switch (blurStyle) { - case ui.BlurStyle.normal: - skBlurStyle = canvasKit['BlurStyle']['Normal']; - break; - case ui.BlurStyle.solid: - skBlurStyle = canvasKit['BlurStyle']['Solid']; - break; - case ui.BlurStyle.outer: - skBlurStyle = canvasKit['BlurStyle']['Outer']; - break; - case ui.BlurStyle.inner: - skBlurStyle = canvasKit['BlurStyle']['Inner']; - break; - } - - final js.JsObject skMaskFilter = canvasKit - .callMethod('MakeBlurMaskFilter', [skBlurStyle, sigma, true]); - skPaint.callMethod('setMaskFilter', [skMaskFilter]); - } - - if (paint.imageFilter != null) { - final SkImageFilter skImageFilter = paint.imageFilter; - skPaint.callMethod( - 'setImageFilter', [skImageFilter.skImageFilter]); - } - - if (paint.colorFilter != null) { - EngineColorFilter engineFilter = paint.colorFilter; - SkColorFilter skFilter = engineFilter._toSkColorFilter(); - skPaint.callMethod('setColorFilter', [skFilter.skColorFilter]); - } - - return skPaint; -} - // Mappings from SkMatrix-index to input-index. const List _skMatrixIndexToMatrix4Index = [ 0, 4, 12, // Row 1 diff --git a/lib/web_ui/lib/src/engine/dom_canvas.dart b/lib/web_ui/lib/src/engine/dom_canvas.dart index 5d207f5a28b39..8618b35d6ae82 100644 --- a/lib/web_ui/lib/src/engine/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/dom_canvas.dart @@ -56,17 +56,17 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawLine(ui.Offset p1, ui.Offset p2, ui.PaintData paint) { + void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { throw UnimplementedError(); } @override - void drawPaint(ui.PaintData paint) { + void drawPaint(SurfacePaintData paint) { throw UnimplementedError(); } @override - void drawRect(ui.Rect rect, ui.PaintData paint) { + void drawRect(ui.Rect rect, SurfacePaintData paint) { assert(paint.shader == null); final html.Element rectangle = html.Element.tag('draw-rect'); assert(() { @@ -126,27 +126,27 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawRRect(ui.RRect rrect, ui.PaintData paint) { + void drawRRect(ui.RRect rrect, SurfacePaintData paint) { throw UnimplementedError(); } @override - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint) { + void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { throw UnimplementedError(); } @override - void drawOval(ui.Rect rect, ui.PaintData paint) { + void drawOval(ui.Rect rect, SurfacePaintData paint) { throw UnimplementedError(); } @override - void drawCircle(ui.Offset c, double radius, ui.PaintData paint) { + void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { throw UnimplementedError(); } @override - void drawPath(ui.Path path, ui.PaintData paint) { + void drawPath(ui.Path path, SurfacePaintData paint) { throw UnimplementedError(); } @@ -157,13 +157,13 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) { + void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { throw UnimplementedError(); } @override void drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) { + ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { throw UnimplementedError(); } @@ -176,7 +176,7 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { @override void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - ui.PaintData paint) { + SurfacePaintData paint) { throw UnimplementedError(); } } diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index b1e29ff2f7f50..222a4e3f41996 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -40,34 +40,34 @@ abstract class EngineCanvas { void drawColor(ui.Color color, ui.BlendMode blendMode); - void drawLine(ui.Offset p1, ui.Offset p2, ui.PaintData paint); + void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint); - void drawPaint(ui.PaintData paint); + void drawPaint(SurfacePaintData paint); - void drawRect(ui.Rect rect, ui.PaintData paint); + void drawRect(ui.Rect rect, SurfacePaintData paint); - void drawRRect(ui.RRect rrect, ui.PaintData paint); + void drawRRect(ui.RRect rrect, SurfacePaintData paint); - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint); + void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint); - void drawOval(ui.Rect rect, ui.PaintData paint); + void drawOval(ui.Rect rect, SurfacePaintData paint); - void drawCircle(ui.Offset c, double radius, ui.PaintData paint); + void drawCircle(ui.Offset c, double radius, SurfacePaintData paint); - void drawPath(ui.Path path, ui.PaintData paint); + void drawPath(ui.Path path, SurfacePaintData paint); void drawShadow( ui.Path path, ui.Color color, double elevation, bool transparentOccluder); - void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint); + void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint); void drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint); + ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint); void drawParagraph(EngineParagraph paragraph, ui.Offset offset); void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - ui.PaintData paint); + SurfacePaintData paint); } /// Adds an [offset] transformation to a [transform] matrix and returns the diff --git a/lib/web_ui/lib/src/engine/houdini_canvas.dart b/lib/web_ui/lib/src/engine/houdini_canvas.dart index 2eb83b9c76269..5ab3c34d5358d 100644 --- a/lib/web_ui/lib/src/engine/houdini_canvas.dart +++ b/lib/web_ui/lib/src/engine/houdini_canvas.dart @@ -151,42 +151,42 @@ class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawLine(ui.Offset p1, ui.Offset p2, ui.PaintData paint) { + void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawPaint(ui.PaintData paint) { + void drawPaint(SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawRect(ui.Rect rect, ui.PaintData paint) { + void drawRect(ui.Rect rect, SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawRRect(ui.RRect rrect, ui.PaintData paint) { + void drawRRect(ui.RRect rrect, SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint) { + void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawOval(ui.Rect rect, ui.PaintData paint) { + void drawOval(ui.Rect rect, SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawCircle(ui.Offset c, double radius, ui.PaintData paint) { + void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { // Drawn using CSS Paint. } @override - void drawPath(ui.Path path, ui.PaintData paint) { + void drawPath(ui.Path path, SurfacePaintData paint) { // Drawn using CSS Paint. } @@ -197,13 +197,13 @@ class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) { + void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { // TODO(yjbanov): implement. } @override void drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) { + ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaintData paint) { // TODO(yjbanov): implement src rectangle final HtmlImage htmlImage = image; final html.Element imageBox = html.Element.tag('flt-img'); @@ -230,7 +230,7 @@ class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking { @override void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - ui.PaintData paint) { + SurfacePaintData paint) { // TODO(flutter_web): implement. } } diff --git a/lib/web_ui/lib/src/engine/path_to_svg.dart b/lib/web_ui/lib/src/engine/path_to_svg.dart index 24f0fcf93893f..0e138044ef074 100644 --- a/lib/web_ui/lib/src/engine/path_to_svg.dart +++ b/lib/web_ui/lib/src/engine/path_to_svg.dart @@ -6,7 +6,7 @@ part of engine; /// Converts [path] to SVG path syntax to be used as "d" attribute in path /// element. -void pathToSvg(ui.Path path, StringBuffer sb, +void pathToSvg(SurfacePath path, StringBuffer sb, {double offsetX = 0, double offsetY = 0}) { for (Subpath subPath in path.subpaths) { for (PathCommand command in subPath.commands) { diff --git a/lib/web_ui/lib/src/engine/render_vertices.dart b/lib/web_ui/lib/src/engine/render_vertices.dart index a64022a0c6632..d3648c4b6fcc7 100644 --- a/lib/web_ui/lib/src/engine/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/render_vertices.dart @@ -23,7 +23,7 @@ abstract class _GlRenderer { Matrix4 transform, ui.Vertices vertices, ui.BlendMode blendMode, - ui.PaintData paint); + SurfacePaintData paint); void drawHairline(html.CanvasRenderingContext2D _ctx, Float32List positions); } @@ -86,7 +86,7 @@ class _WebGlRenderer implements _GlRenderer { Matrix4 transform, ui.Vertices vertices, ui.BlendMode blendMode, - ui.PaintData paint) { + SurfacePaintData paint) { // Compute bounds of vertices. final Float32List positions = vertices.positions; ui.Rect bounds = _computeVerticesBounds(positions, transform); diff --git a/lib/web_ui/lib/src/engine/surface/clip.dart b/lib/web_ui/lib/src/engine/surface/clip.dart index 66db0c1cb0331..d22d74c6576ba 100644 --- a/lib/web_ui/lib/src/engine/surface/clip.dart +++ b/lib/web_ui/lib/src/engine/surface/clip.dart @@ -167,7 +167,7 @@ class PersistedPhysicalShape extends PersistedContainerSurface shadowColor = ui.Color(shadowColor), super(oldLayer); - final ui.Path path; + final SurfacePath path; final double elevation; final ui.Color color; final ui.Color shadowColor; diff --git a/lib/web_ui/lib/src/engine/surface/painting.dart b/lib/web_ui/lib/src/engine/surface/painting.dart new file mode 100644 index 0000000000000..b6df1b15bdf1a --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/painting.dart @@ -0,0 +1,1371 @@ +// 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. + +part of engine; + +/// Implementation of [ui.Paint] used by the HTML rendering backend. +class SurfacePaint implements ui.Paint { + SurfacePaintData _paintData = SurfacePaintData(); + + @override + ui.BlendMode get blendMode => _paintData.blendMode ?? ui.BlendMode.srcOver; + + @override + set blendMode(ui.BlendMode value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.blendMode = value; + } + + ui.BlendMode _blendMode; + + @override + ui.PaintingStyle get style => _paintData.style ?? ui.PaintingStyle.fill; + + @override + set style(ui.PaintingStyle value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.style = value; + } + + @override + double get strokeWidth => _paintData.strokeWidth ?? 0.0; + + @override + set strokeWidth(double value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.strokeWidth = value; + } + + @override + ui.StrokeCap get strokeCap => _paintData.strokeCap; + + @override + set strokeCap(ui.StrokeCap value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.strokeCap = value; + } + + @override + ui.StrokeJoin get strokeJoin => _paintData.strokeJoin; + + @override + set strokeJoin(ui.StrokeJoin value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.strokeJoin = value; + } + + @override + bool get isAntiAlias => _paintData.isAntiAlias; + + @override + set isAntiAlias(bool value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.isAntiAlias = value; + } + + @override + ui.Color get color => _paintData.color; + + @override + set color(ui.Color value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.color = value.runtimeType == ui.Color ? value : ui.Color(value.value); + } + + @override + bool get invertColors { + return false; + } + + @override + set invertColors(bool value) {} + + @override + ui.Color _color = _defaultPaintColor; + + static const ui.Color _defaultPaintColor = ui.Color(0xFF000000); + + @override + ui.Shader get shader => _paintData.shader; + + @override + set shader(ui.Shader value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.shader = value; + } + + @override + ui.MaskFilter get maskFilter => _paintData.maskFilter; + + @override + set maskFilter(ui.MaskFilter value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.maskFilter = value; + } + + @override + ui.FilterQuality get filterQuality => _paintData.filterQuality; + + @override + set filterQuality(ui.FilterQuality value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.filterQuality = value; + } + + @override + ui.ColorFilter get colorFilter => _paintData.colorFilter; + + @override + set colorFilter(ui.ColorFilter value) { + if (_frozen) { + _paintData = _paintData.clone(); + _frozen = false; + } + _paintData.colorFilter = value; + } + + // TODO(flutter_web): see https://github.com/flutter/flutter/issues/33605 + @override + double get strokeMiterLimit { + return null; + } + + @override + set strokeMiterLimit(double value) { + assert(value != null); + } + + @override + ui.ImageFilter get imageFilter { + // TODO(flutter/flutter#35156): Implement ImageFilter. + return null; + } + + @override + set imageFilter(ui.ImageFilter value) { + // TODO(flutter/flutter#35156): Implement ImageFilter. + } + + // True if Paint instance has used in RecordingCanvas. + bool _frozen = false; + + // Marks this paint object as previously used. + SurfacePaintData get paintData { + // Flip bit so next time object gets mutated we create a clone of + // current paint data. + _frozen = true; + return _paintData; + } + + @override + String toString() { + final StringBuffer result = StringBuffer(); + String semicolon = ''; + result.write('Paint('); + if (style == ui.PaintingStyle.stroke) { + result.write('$style'); + if (strokeWidth != null && strokeWidth != 0.0) + result.write(' $strokeWidth'); + else + result.write(' hairline'); + if (strokeCap != null && strokeCap != ui.StrokeCap.butt) + result.write(' $strokeCap'); + semicolon = '; '; + } + if (isAntiAlias != true) { + result.write('${semicolon}antialias off'); + semicolon = '; '; + } + if (color != _defaultPaintColor) { + if (color != null) + result.write('$semicolon$color'); + else + result.write('${semicolon}no color'); + semicolon = '; '; + } + result.write(')'); + return result.toString(); + } +} + +/// Private Paint context data used for recording canvas commands allowing +/// Paint to be mutated post canvas draw operations. +class SurfacePaintData { + ui.BlendMode blendMode; + ui.PaintingStyle style; + double strokeWidth; + ui.StrokeCap strokeCap; + ui.StrokeJoin strokeJoin; + bool isAntiAlias = true; + ui.Color color; + ui.Shader shader; + ui.MaskFilter maskFilter; + ui.FilterQuality filterQuality; + ui.ColorFilter colorFilter; + + // Internal for recording canvas use. + SurfacePaintData clone() { + return SurfacePaintData() + ..blendMode = blendMode + ..filterQuality = filterQuality + ..maskFilter = maskFilter + ..shader = shader + ..isAntiAlias = isAntiAlias + ..color = color + ..colorFilter = colorFilter + ..strokeWidth = strokeWidth + ..style = style + ..strokeJoin = strokeJoin + ..strokeCap = strokeCap; + } +} + +/// A complex, one-dimensional subset of a plane. +/// +/// A path consists of a number of subpaths, and a _current point_. +/// +/// Subpaths consist of segments of various types, such as lines, +/// arcs, or beziers. Subpaths can be open or closed, and can +/// self-intersect. +/// +/// Closed subpaths enclose a (possibly discontiguous) region of the +/// plane based on the current [fillType]. +/// +/// The _current point_ is initially at the origin. After each +/// operation adding a segment to a subpath, the current point is +/// updated to the end of that segment. +/// +/// Paths can be drawn on canvases using [Canvas.drawPath], and can +/// used to create clip regions using [Canvas.clipPath]. +class SurfacePath implements ui.Path { + final List subpaths; + ui.PathFillType _fillType = ui.PathFillType.nonZero; + + Subpath get _currentSubpath => subpaths.isEmpty ? null : subpaths.last; + + List get _commands => _currentSubpath?.commands; + + /// The current x-coordinate for this path. + double get _currentX => _currentSubpath?.currentX ?? 0.0; + + /// The current y-coordinate for this path. + double get _currentY => _currentSubpath?.currentY ?? 0.0; + + /// Recorder used for hit testing paths. + static ui.RawRecordingCanvas _rawRecorder; + + SurfacePath() : subpaths = []; + + /// Creates a copy of another [Path]. + /// + /// This copy is fast and does not require additional memory unless either + /// the `source` path or the path returned by this constructor are modified. + SurfacePath.from(SurfacePath source) + : subpaths = List.from(source.subpaths); + + SurfacePath._clone(this.subpaths, this._fillType); + + /// Determines how the interior of this path is calculated. + /// + /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. + @override + ui.PathFillType get fillType => _fillType; + @override + set fillType(ui.PathFillType value) { + _fillType = value; + } + + /// Opens a new subpath with starting point (x, y). + void _openNewSubpath(double x, double y) { + subpaths.add(Subpath(x, y)); + _setCurrentPoint(x, y); + } + + /// Sets the current point to (x, y). + void _setCurrentPoint(double x, double y) { + _currentSubpath.currentX = x; + _currentSubpath.currentY = y; + } + + /// Starts a new subpath at the given coordinate. + @override + void moveTo(double x, double y) { + _openNewSubpath(x, y); + _commands.add(MoveTo(x, y)); + } + + /// Starts a new subpath at the given offset from the current point. + @override + void relativeMoveTo(double dx, double dy) { + final double newX = _currentX + dx; + final double newY = _currentY + dy; + _openNewSubpath(newX, newY); + _commands.add(MoveTo(newX, newY)); + } + + /// Adds a straight line segment from the current point to the given + /// point. + @override + void lineTo(double x, double y) { + if (subpaths.isEmpty) { + moveTo(0.0, 0.0); + } + _commands.add(LineTo(x, y)); + _setCurrentPoint(x, y); + } + + /// Adds a straight line segment from the current point to the point + /// at the given offset from the current point. + @override + void relativeLineTo(double dx, double dy) { + final double newX = _currentX + dx; + final double newY = _currentY + dy; + if (subpaths.isEmpty) { + moveTo(0.0, 0.0); + } + _commands.add(LineTo(newX, newY)); + _setCurrentPoint(newX, newY); + } + + void _ensurePathStarted() { + if (subpaths.isEmpty) { + subpaths.add(Subpath(0.0, 0.0)); + } + } + + /// Adds a quadratic bezier segment that curves from the current + /// point to the given point (x2,y2), using the control point + /// (x1,y1). + @override + void quadraticBezierTo(double x1, double y1, double x2, double y2) { + _ensurePathStarted(); + _commands.add(QuadraticCurveTo(x1, y1, x2, y2)); + _setCurrentPoint(x2, y2); + } + + /// Adds a quadratic bezier segment that curves from the current + /// point to the point at the offset (x2,y2) from the current point, + /// using the control point at the offset (x1,y1) from the current + /// point. + @override + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { + _ensurePathStarted(); + _commands.add(QuadraticCurveTo( + x1 + _currentX, y1 + _currentY, x2 + _currentX, y2 + _currentY)); + _setCurrentPoint(x2 + _currentX, y2 + _currentY); + } + + /// Adds a cubic bezier segment that curves from the current point + /// to the given point (x3,y3), using the control points (x1,y1) and + /// (x2,y2). + @override + void cubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _ensurePathStarted(); + _commands.add(BezierCurveTo(x1, y1, x2, y2, x3, y3)); + _setCurrentPoint(x3, y3); + } + + /// Adds a cubic bezier segment that curves from the current point + /// to the point at the offset (x3,y3) from the current point, using + /// the control points at the offsets (x1,y1) and (x2,y2) from the + /// current point. + @override + void relativeCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _ensurePathStarted(); + _commands.add(BezierCurveTo(x1 + _currentX, y1 + _currentY, + x2 + _currentX, y2 + _currentY, x3 + _currentX, y3 + _currentY)); + _setCurrentPoint(x3 + _currentX, y3 + _currentY); + } + + /// Adds a bezier segment that curves from the current point to the + /// given point (x2,y2), using the control points (x1,y1) and the + /// weight w. If the weight is greater than 1, then the curve is a + /// hyperbola; if the weight equals 1, it's a parabola; and if it is + /// less than 1, it is an ellipse. + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + final List quads = + Conic(_currentX, _currentY, x1, y1, x2, y2, w).toQuads(); + final int len = quads.length; + for (int i = 1; i < len; i += 2) { + quadraticBezierTo( + quads[i].dx, quads[i].dy, quads[i + 1].dx, quads[i + 1].dy); + } + } + + /// Adds a bezier segment that curves from the current point to the + /// point at the offset (x2,y2) from the current point, using the + /// control point at the offset (x1,y1) from the current point and + /// the weight w. If the weight is greater than 1, then the curve is + /// a hyperbola; if the weight equals 1, it's a parabola; and if it + /// is less than 1, it is an ellipse. + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); + } + + /// If the `forceMoveTo` argument is false, adds a straight line + /// segment and an arc segment. + /// + /// If the `forceMoveTo` argument is true, starts a new subpath + /// consisting of an arc segment. + /// + /// In either case, the arc segment consists of the arc that follows + /// the edge of the oval bounded by the given rectangle, from + /// startAngle radians around the oval up to startAngle + sweepAngle + /// radians around the oval, with zero radians being the point on + /// the right hand side of the oval that crosses the horizontal line + /// that intersects the center of the rectangle and with positive + /// angles going clockwise around the oval. + /// + /// The line segment added if `forceMoveTo` is false starts at the + /// current point and ends at the start of the arc. + @override + void arcTo( + ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { + assert(rectIsValid(rect)); + final ui.Offset center = rect.center; + final double radiusX = rect.width / 2; + final double radiusY = rect.height / 2; + final double startX = radiusX * math.cos(startAngle) + center.dx; + final double startY = radiusY * math.sin(startAngle) + center.dy; + if (forceMoveTo) { + _openNewSubpath(startX, startY); + } else { + lineTo(startX, startY); + } + _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, + startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); + + _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, + radiusY * math.sin(startAngle + sweepAngle) + center.dy); + } + + /// Appends up to four conic curves weighted to describe an oval of `radius` + /// and rotated by `rotation`. + /// + /// The first curve begins from the last point in the path and the last ends + /// at `arcEnd`. The curves follow a path in a direction determined by + /// `clockwise` and `largeArc` in such a way that the sweep angle + /// is always less than 360 degrees. + /// + /// A simple line is appended if either either radii are zero or the last + /// point in the path is `arcEnd`. The radii are scaled to fit the last path + /// point if both are greater than zero but too small to describe an arc. + /// + /// See Conversion from endpoint to center parametrization described in + /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter + /// as reference for implementation. + @override + void arcToPoint( + ui.Offset arcEnd, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + assert(offsetIsValid(arcEnd)); + assert(radiusIsValid(radius)); + // _currentX, _currentY are the coordinates of start point on path, + // arcEnd is final point of arc. + // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) + // largeArc is false if arc is spanning less than or equal to 180 degrees. + // clockwise is false if arc sweeps through decreasing angles or true + // if sweeping through increasing angles. + // rotation is the angle from the x-axis of the current coordinate + // system to the x-axis of the eclipse. + + double rx = radius.x.abs(); + double ry = radius.y.abs(); + + // If the current point and target point for the arc are identical, it + // should be treated as a zero length path. This ensures continuity in + // animations. + final bool isSamePoint = _currentX == arcEnd.dx && _currentY == arcEnd.dy; + + // If rx = 0 or ry = 0 then this arc is treated as a straight line segment + // (a "lineto") joining the endpoints. + // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters + if (isSamePoint || rx.toInt() == 0 || ry.toInt() == 0) { + _commands.add(LineTo(arcEnd.dx, arcEnd.dy)); + _setCurrentPoint(arcEnd.dx, arcEnd.dy); + return; + } + + // As an intermediate point to finding center parametrization, place the + // origin on the midpoint between start/end points and rotate to align + // coordinate axis with axes of the ellipse. + final double midPointX = (_currentX - arcEnd.dx) / 2.0; + final double midPointY = (_currentY - arcEnd.dy) / 2.0; + + // Convert rotation or radians. + final double xAxisRotation = math.pi * rotation / 180.0; + + // Cache cos/sin value. + final double cosXAxisRotation = math.cos(xAxisRotation); + final double sinXAxisRotation = math.sin(xAxisRotation); + + // Calculate rotate midpoint as x/yPrime. + final double xPrime = + (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); + final double yPrime = + (-sinXAxisRotation * midPointX) + (cosXAxisRotation * midPointY); + + // Check if the radii are big enough to draw the arc, scale radii if not. + // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii + double rxSquare = rx * rx; + double rySquare = ry * ry; + final double xPrimeSquare = xPrime * xPrime; + final double yPrimeSquare = yPrime * yPrime; + + double radiiScale = (xPrimeSquare / rxSquare) + (yPrimeSquare / rySquare); + if (radiiScale > 1) { + radiiScale = math.sqrt(radiiScale); + rx *= radiiScale; + ry *= radiiScale; + rxSquare = rx * rx; + rySquare = ry * ry; + } + + // Compute transformed center. eq. 5.2 + final double distanceSquare = + (rxSquare * yPrimeSquare) + rySquare * xPrimeSquare; + final double cNumerator = (rxSquare * rySquare) - distanceSquare; + double scaleFactor = math.sqrt(math.max(cNumerator / distanceSquare, 0.0)); + if (largeArc == clockwise) { + scaleFactor = -scaleFactor; + } + // Ready to compute transformed center. + final double cxPrime = scaleFactor * ((rx * yPrime) / ry); + final double cyPrime = scaleFactor * (-(ry * xPrime) / rx); + + // Rotate to find actual center. + final double cx = cosXAxisRotation * cxPrime - + sinXAxisRotation * cyPrime + + ((_currentX + arcEnd.dx) / 2.0); + final double cy = sinXAxisRotation * cxPrime + + cosXAxisRotation * cyPrime + + ((_currentY + arcEnd.dy) / 2.0); + + // Calculate start angle and sweep. + // Start vector is from midpoint of start/end points to transformed center. + final double startVectorX = (xPrime - cxPrime) / rx; + final double startVectorY = (yPrime - cyPrime) / ry; + + final double startAngle = math.atan2(startVectorY, startVectorX); + final double endVectorX = (-xPrime - cxPrime) / rx; + final double endVectorY = (-yPrime - cyPrime) / ry; + double sweepAngle = math.atan2(endVectorY, endVectorX) - startAngle; + + if (clockwise && sweepAngle < 0) { + sweepAngle += math.pi * 2.0; + } else if (!clockwise && sweepAngle > 0) { + sweepAngle -= math.pi * 2.0; + } + + _commands.add(Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, + startAngle + sweepAngle, sweepAngle.isNegative)); + + _setCurrentPoint(arcEnd.dx, arcEnd.dy); + } + + /// Appends up to four conic curves weighted to describe an oval of `radius` + /// and rotated by `rotation`. + /// + /// The last path point is described by (px, py). + /// + /// The first curve begins from the last point in the path and the last ends + /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a + /// path in a direction determined by `clockwise` and `largeArc` + /// in such a way that the sweep angle is always less than 360 degrees. + /// + /// A simple line is appended if either either radii are zero, or, both + /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to + /// fit the last path point if both are greater than zero but too small to + /// describe an arc. + @override + void relativeArcToPoint( + ui.Offset arcEndDelta, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + assert(offsetIsValid(arcEndDelta)); + assert(radiusIsValid(radius)); + arcToPoint(ui.Offset(_currentX + arcEndDelta.dx, _currentY + arcEndDelta.dy), + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise); + } + + /// Adds a new subpath that consists of four lines that outline the + /// given rectangle. + @override + void addRect(ui.Rect rect) { + assert(rectIsValid(rect)); + _openNewSubpath(rect.left, rect.top); + _commands + .add(RectCommand(rect.left, rect.top, rect.width, rect.height)); + } + + /// Adds a new subpath that consists of a curve that forms the + /// ellipse that fills the given rectangle. + /// + /// To add a circle, pass an appropriate rectangle as `oval`. + /// [Rect.fromCircle] can be used to easily describe the circle's center + /// [Offset] and radius. + @override + void addOval(ui.Rect oval) { + assert(rectIsValid(oval)); + final ui.Offset center = oval.center; + final double radiusX = oval.width / 2; + final double radiusY = oval.height / 2; + + /// At startAngle = 0, the path will begin at center + cos(0) * radius. + _openNewSubpath(center.dx + radiusX, center.dy); + _commands.add(Ellipse( + center.dx, center.dy, radiusX, radiusY, 0.0, 0.0, 2 * math.pi, false)); + } + + /// Adds a new subpath with one arc segment that consists of the arc + /// that follows the edge of the oval bounded by the given + /// rectangle, from startAngle radians around the oval up to + /// startAngle + sweepAngle radians around the oval, with zero + /// radians being the point on the right hand side of the oval that + /// crosses the horizontal line that intersects the center of the + /// rectangle and with positive angles going clockwise around the + /// oval. + @override + void addArc(ui.Rect oval, double startAngle, double sweepAngle) { + assert(rectIsValid(oval)); + final ui.Offset center = oval.center; + final double radiusX = oval.width / 2; + final double radiusY = oval.height / 2; + _openNewSubpath(radiusX * math.cos(startAngle) + center.dx, + radiusY * math.sin(startAngle) + center.dy); + _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, + startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); + + _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, + radiusY * math.sin(startAngle + sweepAngle) + center.dy); + } + + /// Adds a new subpath with a sequence of line segments that connect the given + /// points. + /// + /// If `close` is true, a final line segment will be added that connects the + /// last point to the first point. + /// + /// The `points` argument is interpreted as offsets from the origin. + @override + void addPolygon(List points, bool close) { + assert(points != null); + if (points.isEmpty) { + return; + } + + moveTo(points.first.dx, points.first.dy); + for (int i = 1; i < points.length; i++) { + final ui.Offset point = points[i]; + lineTo(point.dx, point.dy); + } + if (close) { + this.close(); + } else { + _setCurrentPoint(points.last.dx, points.last.dy); + } + } + + /// Adds a new subpath that consists of the straight lines and + /// curves needed to form the rounded rectangle described by the + /// argument. + @override + void addRRect(ui.RRect rrect) { + assert(rrectIsValid(rrect)); + + // Set the current point to the top left corner of the rectangle (the + // point on the top of the rectangle farthest to the left that isn't in + // the rounded corner). + // TODO(het): Is this the current point in Flutter? + _openNewSubpath(rrect.tallMiddleRect.left, rrect.top); + _commands.add(RRectCommand(rrect)); + } + + /// Adds a new subpath that consists of the given `path` offset by the given + /// `offset`. + /// + /// If `matrix4` is specified, the path will be transformed by this matrix + /// after the matrix is translated by the given offset. The matrix is a 4x4 + /// matrix stored in column major order. + @override + void addPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { + assert(path != null); // path is checked on the engine side + assert(offsetIsValid(offset)); + if (matrix4 != null) { + assert(matrix4IsValid(matrix4)); + _addPathWithMatrix(path, offset.dx, offset.dy, matrix4); + } else { + _addPath(path, offset.dx, offset.dy); + } + } + + void _addPath(SurfacePath path, double dx, double dy) { + if (dx == 0.0 && dy == 0.0) { + subpaths.addAll(path.subpaths); + } else { + subpaths.addAll(path + .transform(Matrix4.translationValues(dx, dy, 0.0).storage) + .subpaths); + } + } + + void _addPathWithMatrix(SurfacePath path, double dx, double dy, Float64List matrix) { + final Matrix4 transform = Matrix4.fromFloat64List(matrix); + transform.translate(dx, dy); + subpaths.addAll(path.transform(transform.storage).subpaths); + } + + /// Adds the given path to this path by extending the current segment of this + /// path with the the first segment of the given path. + /// + /// If `matrix4` is specified, the path will be transformed by this matrix + /// after the matrix is translated by the given `offset`. The matrix is a 4x4 + /// matrix stored in column major order. + @override + void extendWithPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { + assert(path != null); // path is checked on the engine side + assert(offsetIsValid(offset)); + if (matrix4 != null) { + assert(matrix4IsValid(matrix4)); + _extendWithPathAndMatrix(path, offset.dx, offset.dy, matrix4); + } else { + _extendWithPath(path, offset.dx, offset.dy); + } + } + + void _extendWithPath(SurfacePath path, double dx, double dy) { + if (dx == 0.0 && dy == 0.0) { + assert(path.subpaths.length == 1); + _ensurePathStarted(); + _commands.addAll(path.subpaths.single.commands); + _setCurrentPoint( + path.subpaths.single.currentX, path.subpaths.single.currentY); + } else { + throw UnimplementedError('Cannot extend path with non-zero offset'); + } + } + + void _extendWithPathAndMatrix( + SurfacePath path, double dx, double dy, Float64List matrix) { + throw UnimplementedError('Cannot extend path with transform matrix'); + } + + /// Closes the last subpath, as if a straight line had been drawn + /// from the current point to the first point of the subpath. + @override + void close() { + _ensurePathStarted(); + _commands.add(const CloseCommand()); + _setCurrentPoint(_currentSubpath.startX, _currentSubpath.startY); + } + + /// Clears the [Path] object of all subpaths, returning it to the + /// same state it had when it was created. The _current point_ is + /// reset to the origin. + @override + void reset() { + subpaths.clear(); + } + + /// Tests to see if the given point is within the path. (That is, whether the + /// point would be in the visible portion of the path if the path was used + /// with [Canvas.clipPath].) + /// + /// The `point` argument is interpreted as an offset from the origin. + /// + /// Returns true if the point is in the path, and false otherwise. + /// + /// Note: Not very efficient, it creates a canvas, plays path and calls + /// Context2D isPointInPath. If performance becomes issue, retaining + /// RawRecordingCanvas can remove create/remove rootElement cost. + @override + bool contains(ui.Offset point) { + assert(offsetIsValid(point)); + final int subPathCount = subpaths.length; + if (subPathCount == 0) { + return false; + } + final double pointX = point.dx; + final double pointY = point.dy; + if (subPathCount == 1) { + // Optimize for rect/roundrect checks. + final Subpath subPath = subpaths[0]; + if (subPath.commands.length == 1) { + final PathCommand cmd = subPath.commands[0]; + if (cmd is RectCommand) { + if (pointY < cmd.y || pointY > (cmd.y + cmd.height)) { + return false; + } + if (pointX < cmd.x || pointX > (cmd.x + cmd.width)) { + return false; + } + return true; + } else if (cmd is RRectCommand) { + final ui.RRect rRect = cmd.rrect; + if (pointY < rRect.top || pointY > rRect.bottom) { + return false; + } + if (pointX < rRect.left || pointX > rRect.right) { + return false; + } + if (pointX < (rRect.left + rRect.tlRadiusX) && + pointY < (rRect.top + rRect.tlRadiusY)) { + // Top left corner + return _ellipseContains( + pointX, + pointY, + rRect.left + rRect.tlRadiusX, + rRect.top + rRect.tlRadiusY, + rRect.tlRadiusX, + rRect.tlRadiusY); + } else if (pointX >= (rRect.right - rRect.trRadiusX) && + pointY < (rRect.top + rRect.trRadiusY)) { + // Top right corner + return _ellipseContains( + pointX, + pointY, + rRect.right - rRect.trRadiusX, + rRect.top + rRect.trRadiusY, + rRect.trRadiusX, + rRect.trRadiusY); + } else if (pointX >= (rRect.right - rRect.brRadiusX) && + pointY >= (rRect.bottom - rRect.brRadiusY)) { + // Bottom right corner + return _ellipseContains( + pointX, + pointY, + rRect.right - rRect.brRadiusX, + rRect.bottom - rRect.brRadiusY, + rRect.trRadiusX, + rRect.trRadiusY); + } else if (pointX < (rRect.left + rRect.blRadiusX) && + pointY >= (rRect.bottom - rRect.blRadiusY)) { + // Bottom left corner + return _ellipseContains( + pointX, + pointY, + rRect.left + rRect.blRadiusX, + rRect.bottom - rRect.blRadiusY, + rRect.trRadiusX, + rRect.trRadiusY); + } + return true; + } + } + } + final ui.Size size = window.physicalSize / window.devicePixelRatio; + _rawRecorder ??= ui.RawRecordingCanvas(size); + // Account for the shift due to padding. + _rawRecorder.translate(-BitmapCanvas.kPaddingPixels.toDouble(), + -BitmapCanvas.kPaddingPixels.toDouble()); + _rawRecorder.drawPath( + this, (SurfacePaint()..color = const ui.Color(0xFF000000)).paintData); + final bool result = _rawRecorder.ctx.isPointInPath(pointX, pointY); + _rawRecorder.dispose(); + return result; + } + + /// Returns a copy of the path with all the segments of every + /// subpath translated by the given offset. + @override + SurfacePath shift(ui.Offset offset) { + assert(offsetIsValid(offset)); + final List shiftedSubPaths = []; + for (final Subpath subPath in subpaths) { + shiftedSubPaths.add(subPath.shift(offset)); + } + return SurfacePath._clone(shiftedSubPaths, fillType); + } + + /// Returns a copy of the path with all the segments of every + /// sub path transformed by the given matrix. + @override + SurfacePath transform(Float64List matrix4) { + assert(matrix4IsValid(matrix4)); + final SurfacePath transformedPath = SurfacePath(); + for (final Subpath subPath in subpaths) { + for (final PathCommand cmd in subPath.commands) { + cmd.transform(matrix4, transformedPath); + } + } + return transformedPath; + } + + /// Computes the bounding rectangle for this path. + /// + /// A path containing only axis-aligned points on the same straight line will + /// have no area, and therefore `Rect.isEmpty` will return true for such a + /// path. Consider checking `rect.width + rect.height > 0.0` instead, or + /// using the [computeMetrics] API to check the path length. + /// + /// For many more elaborate paths, the bounds may be inaccurate. For example, + /// when a path contains a circle, the points used to compute the bounds are + /// the circle's implied control points, which form a square around the + /// circle; if the circle has a transformation applied using [transform] then + /// that square is rotated, and the (axis-aligned, non-rotated) bounding box + /// therefore ends up grossly overestimating the actual area covered by the + /// circle. + // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds + @override + ui.Rect getBounds() { + // Sufficiently small number for curve eq. + const double epsilon = 0.000000001; + bool ltrbInitialized = false; + double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; + double curX = 0.0; + double curY = 0.0; + double minX = 0.0, maxX = 0.0, minY = 0.0, maxY = 0.0; + for (Subpath subpath in subpaths) { + for (PathCommand op in subpath.commands) { + bool skipBounds = false; + switch (op.type) { + case PathCommandTypes.moveTo: + final MoveTo cmd = op; + curX = minX = maxX = cmd.x; + curY = minY = maxY = cmd.y; + break; + case PathCommandTypes.lineTo: + final LineTo cmd = op; + curX = minX = maxX = cmd.x; + curY = minY = maxY = cmd.y; + break; + case PathCommandTypes.ellipse: + final Ellipse cmd = op; + // Rotate 4 corners of bounding box. + final double rx = cmd.radiusX; + final double ry = cmd.radiusY; + final double cosVal = math.cos(cmd.rotation); + final double sinVal = math.sin(cmd.rotation); + final double rxCos = rx * cosVal; + final double ryCos = ry * cosVal; + final double rxSin = rx * sinVal; + final double rySin = ry * sinVal; + + final double leftDeltaX = rxCos - rySin; + final double rightDeltaX = -rxCos - rySin; + final double topDeltaY = ryCos + rxSin; + final double bottomDeltaY = ryCos - rxSin; + + final double centerX = cmd.x; + final double centerY = cmd.y; + + double rotatedX = centerX + leftDeltaX; + double rotatedY = centerY + topDeltaY; + minX = maxX = rotatedX; + minY = maxY = rotatedY; + + rotatedX = centerX + rightDeltaX; + rotatedY = centerY + bottomDeltaY; + minX = math.min(minX, rotatedX); + maxX = math.max(maxX, rotatedX); + minY = math.min(minY, rotatedY); + maxY = math.max(maxY, rotatedY); + + rotatedX = centerX - leftDeltaX; + rotatedY = centerY - topDeltaY; + minX = math.min(minX, rotatedX); + maxX = math.max(maxX, rotatedX); + minY = math.min(minY, rotatedY); + maxY = math.max(maxY, rotatedY); + + rotatedX = centerX - rightDeltaX; + rotatedY = centerY - bottomDeltaY; + minX = math.min(minX, rotatedX); + maxX = math.max(maxX, rotatedX); + minY = math.min(minY, rotatedY); + maxY = math.max(maxY, rotatedY); + + curX = centerX + cmd.radiusX; + curY = centerY; + break; + case PathCommandTypes.quadraticCurveTo: + final QuadraticCurveTo cmd = op; + final double x1 = curX; + final double y1 = curY; + final double cpX = cmd.x1; + final double cpY = cmd.y1; + final double x2 = cmd.x2; + final double y2 = cmd.y2; + + minX = math.min(x1, x2); + minY = math.min(y1, y2); + maxX = math.max(x1, x2); + maxY = math.max(y1, y2); + + // Curve equation : (1-t)(1-t)P1 + 2t(1-t)CP + t*t*P2. + // At extrema's derivative = 0. + // Solve for + // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 + // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 + // t = (x1 - cpX) / (x1 - 2cpX + x2) + + double denom = x1 - (2 * cpX) + x2; + if (denom.abs() > epsilon) { + final num t1 = (x1 - cpX) / denom; + if ((t1 >= 0) && (t1 <= 1.0)) { + // Solve (x,y) for curve at t = tx to find extrema + final num tprime = 1.0 - t1; + final num extremaX = (tprime * tprime * x1) + + (2 * t1 * tprime * cpX) + + (t1 * t1 * x2); + final num extremaY = (tprime * tprime * y1) + + (2 * t1 * tprime * cpY) + + (t1 * t1 * y2); + // Expand bounds. + minX = math.min(minX, extremaX); + maxX = math.max(maxX, extremaX); + minY = math.min(minY, extremaY); + maxY = math.max(maxY, extremaY); + } + } + // Now calculate dy/dt = 0 + denom = y1 - (2 * cpY) + y2; + if (denom.abs() > epsilon) { + final num t2 = (y1 - cpY) / denom; + if ((t2 >= 0) && (t2 <= 1.0)) { + final num tprime2 = 1.0 - t2; + final num extrema2X = (tprime2 * tprime2 * x1) + + (2 * t2 * tprime2 * cpX) + + (t2 * t2 * x2); + final num extrema2Y = (tprime2 * tprime2 * y1) + + (2 * t2 * tprime2 * cpY) + + (t2 * t2 * y2); + // Expand bounds. + minX = math.min(minX, extrema2X); + maxX = math.max(maxX, extrema2X); + minY = math.min(minY, extrema2Y); + maxY = math.max(maxY, extrema2Y); + } + } + curX = x2; + curY = y2; + break; + case PathCommandTypes.bezierCurveTo: + final BezierCurveTo cmd = op; + final double startX = curX; + final double startY = curY; + final double cpX1 = cmd.x1; + final double cpY1 = cmd.y1; + final double cpX2 = cmd.x2; + final double cpY2 = cmd.y2; + final double endX = cmd.x3; + final double endY = cmd.y3; + // Bounding box is defined by all points on the curve where + // monotonicity changes. + minX = math.min(startX, endX); + minY = math.min(startY, endY); + maxX = math.max(startX, endX); + maxY = math.max(startY, endY); + + double extremaX; + double extremaY; + double a, b, c; + + // Check for simple case of strong ordering before calculating + // extrema + if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || + ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { + // The extrema point is dx/dt B(t) = 0 + // The derivative of B(t) for cubic bezier is a quadratic equation + // with multiple roots + // B'(t) = a*t*t + b*t + c*t + a = -startX + (3 * (cpX1 - cpX2)) + endX; + b = 2 * (startX - (2 * cpX1) + cpX2); + c = -startX + cpX1; + + // Now find roots for quadratic equation with known coefficients + // a,b,c + // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a + num s = (b * b) - (4 * a * c); + // If s is negative, we have no real roots + if ((s >= 0.0) && (a.abs() > epsilon)) { + if (s == 0.0) { + // we have only 1 root + final num t = -b / (2 * a); + final num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + } else { + // we have 2 roots + s = math.sqrt(s); + num t = (-b - s) / (2 * a); + num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + // check 2nd root + t = (-b + s) / (2 * a); + tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + } + } + } + + // Now calc extremes for dy/dt = 0 just like above + if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || + ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { + // The extrema point is dy/dt B(t) = 0 + // The derivative of B(t) for cubic bezier is a quadratic equation + // with multiple roots + // B'(t) = a*t*t + b*t + c*t + a = -startY + (3 * (cpY1 - cpY2)) + endY; + b = 2 * (startY - (2 * cpY1) + cpY2); + c = -startY + cpY1; + + // Now find roots for quadratic equation with known coefficients + // a,b,c + // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a + num s = (b * b) - (4 * a * c); + // If s is negative, we have no real roots + if ((s >= 0.0) && (a.abs() > epsilon)) { + if (s == 0.0) { + // we have only 1 root + final num t = -b / (2 * a); + final num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaY = ((tprime * tprime * tprime) * startY) + + ((3 * tprime * tprime * t) * cpY1) + + ((3 * tprime * t * t) * cpY2) + + (t * t * t * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + } else { + // we have 2 roots + s = math.sqrt(s); + final num t = (-b - s) / (2 * a); + final num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaY = ((tprime * tprime * tprime) * startY) + + ((3 * tprime * tprime * t) * cpY1) + + ((3 * tprime * t * t) * cpY2) + + (t * t * t * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + // check 2nd root + final num t2 = (-b + s) / (2 * a); + final num tprime2 = 1.0 - t2; + if ((t2 >= 0.0) && (t2 <= 1.0)) { + extremaY = ((tprime2 * tprime2 * tprime2) * startY) + + ((3 * tprime2 * tprime2 * t2) * cpY1) + + ((3 * tprime2 * t2 * t2) * cpY2) + + (t2 * t2 * t2 * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + } + } + } + break; + case PathCommandTypes.rect: + final RectCommand cmd = op; + left = cmd.x; + double width = cmd.width; + if (cmd.width < 0) { + left -= width; + width = -width; + } + double top = cmd.y; + double height = cmd.height; + if (cmd.height < 0) { + top -= height; + height = -height; + } + curX = minX = left; + maxX = left + width; + curY = minY = top; + maxY = top + height; + break; + case PathCommandTypes.rRect: + final RRectCommand cmd = op; + final ui.RRect rRect = cmd.rrect; + curX = minX = rRect.left; + maxX = rRect.left + rRect.width; + curY = minY = rRect.top; + maxY = rRect.top + rRect.height; + break; + case PathCommandTypes.close: + default: + skipBounds = false; + break; + } + if (!skipBounds) { + if (!ltrbInitialized) { + left = minX; + right = maxX; + top = minY; + bottom = maxY; + ltrbInitialized = true; + } else { + left = math.min(left, minX); + right = math.max(right, maxX); + top = math.min(top, minY); + bottom = math.max(bottom, maxY); + } + } + } + } + return ltrbInitialized + ? ui.Rect.fromLTRB(left, top, right, bottom) + : ui.Rect.zero; + } + + /// Creates a [PathMetrics] object for this path. + /// + /// If `forceClosed` is set to true, the contours of the path will be measured + /// as if they had been closed, even if they were not explicitly closed. + @override + SurfacePathMetrics computeMetrics({bool forceClosed = false}) { + return SurfacePathMetrics._(this, forceClosed); + } + + /// Detects if path is rounded rectangle and returns rounded rectangle or + /// null. + /// + /// Used for web optimization of physical shape represented as + /// a persistent div. + ui.RRect get webOnlyPathAsRoundedRect { + if (subpaths.length != 1) { + return null; + } + final Subpath subPath = subpaths[0]; + if (subPath.commands.length != 1) { + return null; + } + final PathCommand command = subPath.commands[0]; + return (command is RRectCommand) ? command.rrect : null; + } + + /// Detects if path is simple rectangle and returns rectangle or null. + /// + /// Used for web optimization of physical shape represented as + /// a persistent div. + ui.Rect get webOnlyPathAsRect { + if (subpaths.length != 1) { + return null; + } + final Subpath subPath = subpaths[0]; + if (subPath.commands.length != 1) { + return null; + } + final PathCommand command = subPath.commands[0]; + return (command is RectCommand) + ? ui.Rect.fromLTWH(command.x, command.y, command.width, command.height) + : null; + } + + /// Detects if path is simple oval and returns [Ellipse] or null. + /// + /// Used for web optimization of physical shape represented as + /// a persistent div. + Ellipse get webOnlyPathAsCircle { + if (subpaths.length != 1) { + return null; + } + final Subpath subPath = subpaths[0]; + if (subPath.commands.length != 1) { + return null; + } + final PathCommand command = subPath.commands[0]; + if (command is Ellipse) { + final Ellipse ellipse = command; + if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { + return ellipse; + } + } + return null; + } + + /// Serializes this path to a value that's sent to a CSS custom painter for + /// painting. + List webOnlySerializeToCssPaint() { + final List serializedSubpaths = []; + for (int i = 0; i < subpaths.length; i++) { + serializedSubpaths.add(subpaths[i].serializeToCssPaint()); + } + return serializedSubpaths; + } + + @override + String toString() { + if (assertionsEnabled) { + return 'Path(${subpaths.join(', ')})'; + } else { + return super.toString(); + } + } +} + +// Returns true if point is inside ellipse. +bool _ellipseContains(double px, double py, double centerX, double centerY, + double radiusX, double radiusY) { + final double dx = px - centerX; + final double dy = py - centerY; + return ((dx * dx) / (radiusX * radiusX)) + ((dy * dy) / (radiusY * radiusY)) < + 1.0; +} diff --git a/lib/web_ui/lib/src/engine/surface/path_metrics.dart b/lib/web_ui/lib/src/engine/surface/path_metrics.dart new file mode 100644 index 0000000000000..817f7243cfbbc --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/path_metrics.dart @@ -0,0 +1,533 @@ +// 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. + +part of engine; + +/// An iterable collection of [PathMetric] objects describing a [Path]. +/// +/// A [PathMetrics] object is created by using the [Path.computeMetrics] method, +/// and represents the path as it stood at the time of the call. Subsequent +/// modifications of the path do not affect the [PathMetrics] object. +/// +/// Each path metric corresponds to a segment, or contour, of a path. +/// +/// For example, a path consisting of a [Path.lineTo], a [Path.moveTo], and +/// another [Path.lineTo] will contain two contours and thus be represented by +/// two [PathMetric] objects. +/// +/// When iterating across a [PathMetrics]' contours, the [PathMetric] objects +/// are only valid until the next one is obtained. +class SurfacePathMetrics extends IterableBase implements ui.PathMetrics { + SurfacePathMetrics._(SurfacePath path, bool forceClosed) + : _iterator = SurfacePathMetricIterator._(SurfacePathMetric._(path, forceClosed)); + + final SurfacePathMetricIterator _iterator; + + @override + Iterator get iterator => _iterator; +} + +/// Tracks iteration from one segment of a path to the next for measurement. +class SurfacePathMetricIterator implements Iterator { + SurfacePathMetricIterator._(this._pathMetric); + + SurfacePathMetric _pathMetric; + bool _firstTime = true; + + @override + SurfacePathMetric get current => + _firstTime ? null : _pathMetric._segments.isEmpty ? null : _pathMetric; + + @override + bool moveNext() { + // PathMetric isn't a normal iterable - it's already initialized to its + // first Path. Should only call _moveNext when done with the first one. + if (_firstTime == true) { + _firstTime = false; + return _pathMetric._segments.isNotEmpty; + } else if (_pathMetric?._moveNext() == true) { + return true; + } + _pathMetric = null; + return false; + } +} + +// Maximum range value used in curve subdivision using Casteljau algorithm. +const int _kMaxTValue = 0x3FFFFFFF; +// Distance at which we stop subdividing cubic and quadratic curves. +const double _fTolerance = 0.5; + +/// Utilities for measuring a [Path] and extracting subpaths. +/// +/// Iterate over the object returned by [Path.computeMetrics] to obtain +/// [PathMetric] objects. +/// +/// Once created, metrics will only be valid while the iterator is at the given +/// contour. When the next contour's [PathMetric] is obtained, this object +/// becomes invalid. +/// +/// Implementation is based on +/// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp +/// to maintain consistency with native platforms. +class SurfacePathMetric implements ui.PathMetric { + final SurfacePath _path; + final bool _forceClosed; + + // If the contour ends with a call to [Path.close] (which may + // have been implied when using [Path.addRect]) + bool _isClosed; + // Iterator index into [Path.subPaths] + int _subPathIndex = 0; + List<_PathSegment> _segments; + double _contourLength; + + /// Create a new empty [Path] object. + SurfacePathMetric._(this._path, this._forceClosed) { + _buildSegments(); + } + + @override + int get contourIndex { + throw UnimplementedError('contourIndex is not implemented in the HTML backend'); + } + + /// Return the total length of the current contour. + @override + double get length => _contourLength; + + /// Computes the position of hte current contour at the given offset, and the + /// angle of the path at that point. + /// + /// For example, calling this method with a distance of 1.41 for a line from + /// 0.0,0.0 to 2.0,2.0 would give a point 1.0,1.0 and the angle 45 degrees + /// (but in radians). + /// + /// Returns null if the contour has zero [length]. + /// + /// The distance is clamped to the [length] of the current contour. + @override + ui.Tangent getTangentForOffset(double distance) { + final Float32List posTan = _getPosTan(distance); + // first entry == 0 indicates that Skia returned false + if (posTan[0] == 0.0) { + return null; + } else { + return ui.Tangent( + ui.Offset(posTan[1], posTan[2]), ui.Offset(posTan[3], posTan[4])); + } + } + + Float32List _getPosTan(double distance) => throw UnimplementedError(); + + /// Given a start and stop distance, return the intervening segment(s). + /// + /// `start` and `end` are pinned to legal values (0..[length]) + /// Returns null if the segment is 0 length or `start` > `stop`. + /// Begin the segment with a moveTo if `startWithMoveTo` is true. + @override + SurfacePath extractPath(double start, double end, {bool startWithMoveTo = true}) => + throw UnimplementedError(); + + /// Whether the contour is closed. + /// + /// Returns true if the contour ends with a call to [Path.close] (which may + /// have been implied when using [Path.addRect]) or if `forceClosed` was + /// specified as true in the call to [Path.computeMetrics]. Returns false + /// otherwise. + @override + bool get isClosed { + return _isClosed; + } + + // Move to the next contour in the path. + // + // A path can have a next contour if [Path.moveTo] was called after drawing + // began. Return true if one exists, or false. + // + // This is not exactly congruent with a regular [Iterator.moveNext]. + // Typically, [Iterator.moveNext] should be called before accessing the + // [Iterator.current]. In this case, the [PathMetric] is valid before + // calling `_moveNext` - `_moveNext` should be called after the first + // iteration is done instead of before. + bool _moveNext() { + if (_subPathIndex == (_path.subpaths.length - 1)) { + return false; + } + ++_subPathIndex; + _buildSegments(); + return true; + } + + void _buildSegments() { + _segments = <_PathSegment>[]; + _isClosed = false; + double distance = 0.0; + bool haveSeenMoveTo = false; + + if (_path.subpaths.isEmpty) { + _contourLength = 0; + return; + } + final Subpath subpath = _path.subpaths[_subPathIndex]; + final List commands = subpath.commands; + double currentX = 0.0, currentY = 0.0; + final Function lineToHandler = (double x, double y) { + final double dx = currentX - x; + final double dy = currentY - y; + final double prevDistance = distance; + distance += math.sqrt(dx * dx + dy * dy); + // As we accumulate distance, we have to check that the result of += + // actually made it larger, since a very small delta might be > 0, but + // still have no effect on distance (if distance >>> delta). + if (distance > prevDistance) { + _segments.add(_PathSegment(PathCommandTypes.lineTo, distance, + [currentX, currentY, x, y])); + } + currentX = x; + currentY = y; + }; + _EllipseSegmentResult ellipseResult; + for (PathCommand command in commands) { + switch (command.type) { + case PathCommandTypes.moveTo: + final MoveTo moveTo = command; + currentX = moveTo.x; + currentY = moveTo.y; + haveSeenMoveTo = true; + break; + case PathCommandTypes.lineTo: + assert(haveSeenMoveTo); + final LineTo lineTo = command; + lineToHandler(lineTo.x, lineTo.y); + break; + case PathCommandTypes.bezierCurveTo: + assert(haveSeenMoveTo); + final BezierCurveTo curve = command; + // Compute cubic curve distance. + distance = _computeCubicSegments( + currentX, + currentY, + curve.x1, + curve.y1, + curve.x2, + curve.y2, + curve.x3, + curve.y3, + distance, + 0, + _kMaxTValue, + _segments); + break; + case PathCommandTypes.quadraticCurveTo: + assert(haveSeenMoveTo); + final QuadraticCurveTo quadraticCurveTo = command; + // Compute quad curve distance. + distance = _computeQuadSegments( + currentX, + currentY, + quadraticCurveTo.x1, + quadraticCurveTo.y1, + quadraticCurveTo.x2, + quadraticCurveTo.y2, + distance, + 0, + _kMaxTValue); + break; + case PathCommandTypes.close: + break; + case PathCommandTypes.ellipse: + final Ellipse ellipse = command; + ellipseResult ??= _EllipseSegmentResult(); + _computeEllipseSegments( + currentX, + currentY, + distance, + ellipse.x, + ellipse.y, + ellipse.startAngle, + ellipse.endAngle, + ellipse.rotation, + ellipse.radiusX, + ellipse.radiusY, + ellipse.anticlockwise, + ellipseResult, + _segments); + distance = ellipseResult.distance; + currentX = ellipseResult.endPointX; + currentY = ellipseResult.endPointY; + _isClosed = true; + break; + case PathCommandTypes.rRect: + final RRectCommand rrectCommand = command; + final ui.RRect rrect = rrectCommand.rrect; + RRectMetricsRenderer(moveToCallback: (double x, double y) { + currentX = x; + currentY = y; + _isClosed = true; + haveSeenMoveTo = true; + }, lineToCallback: (double x, double y) { + lineToHandler(x, y); + }, ellipseCallback: (double centerX, + double centerY, + double radiusX, + double radiusY, + double rotation, + double startAngle, + double endAngle, + bool antiClockwise) { + ellipseResult ??= _EllipseSegmentResult(); + _computeEllipseSegments( + currentX, + currentY, + distance, + centerX, + centerY, + startAngle, + endAngle, + rotation, + radiusX, + radiusY, + antiClockwise, + ellipseResult, + _segments); + distance = ellipseResult.distance; + currentX = ellipseResult.endPointX; + currentY = ellipseResult.endPointY; + }).render(rrect); + _isClosed = true; + break; + case PathCommandTypes.rect: + final RectCommand rectCommand = command; + final double x = rectCommand.x; + final double y = rectCommand.y; + final double width = rectCommand.width; + final double height = rectCommand.height; + currentX = x; + currentY = y; + lineToHandler(x + width, y); + lineToHandler(x + width, y + height); + lineToHandler(x, y + height); + lineToHandler(x, y); + _isClosed = true; + break; + default: + throw UnimplementedError('Unknown path command $command'); + } + } + if (!_isClosed && _forceClosed && _segments.isNotEmpty) { + _PathSegment firstSegment = _segments.first; + lineToHandler(firstSegment.points[0], firstSegment.points[1]); + } + _contourLength = distance; + } + + static bool _tspanBigEnough(int tSpan) => (tSpan >> 10) != 0; + + static bool _cubicTooCurvy(double x0, double y0, double x1, double y1, + double x2, double y2, double x3, double y3) { + // Measure distance from start-end line at 1/3 and 2/3rds to control + // points. If distance is less than _fTolerance we should continue + // subdividing curve. Uses approx distance for speed. + // + // p1 = point 1/3rd between start,end points. + final double p1x = (x0 * 2 / 3) + (x3 / 3); + final double p1y = (y0 * 2 / 3) + (y3 / 3); + if ((p1x - x1).abs() > _fTolerance) { + return true; + } + if ((p1y - y1).abs() > _fTolerance) { + return true; + } + // p2 = point 2/3rd between start,end points. + final double p2x = (x0 / 3) + (x3 * 2 / 3); + final double p2y = (y0 / 3) + (y3 * 2 / 3); + if ((p2x - x2).abs() > _fTolerance) { + return true; + } + if ((p2y - y2).abs() > _fTolerance) { + return true; + } + return false; + } + + // Recursively subdivides cubic and adds segments. + static double _computeCubicSegments( + double x0, + double y0, + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + double distance, + int tMin, + int tMax, + List<_PathSegment> segments) { + if (_tspanBigEnough(tMax - tMin) && + _cubicTooCurvy(x0, y0, x1, y1, x2, y2, x3, y3)) { + // Chop cubic into two halves (De Cateljau's algorithm) + // See https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm + final double abX = (x0 + x1) / 2; + final double abY = (y0 + y1) / 2; + final double bcX = (x1 + x2) / 2; + final double bcY = (y1 + y2) / 2; + final double cdX = (x2 + x3) / 2; + final double cdY = (y2 + y3) / 2; + final double abcX = (abX + bcX) / 2; + final double abcY = (abY + bcY) / 2; + final double bcdX = (bcX + cdX) / 2; + final double bcdY = (bcY + cdY) / 2; + final double abcdX = (abcX + bcdX) / 2; + final double abcdY = (abcY + bcdY) / 2; + final int tHalf = (tMin + tMax) >> 1; + distance = _computeCubicSegments( + x0, y0, abX, abY, abcX, abcY, abcdX, abcdY, distance, tMin, tHalf, segments); + distance = _computeCubicSegments( + abcdX, abcdY, bcdX, bcdY, cdX, cdY, x3, y3, distance, tHalf, tMax, segments); + } else { + final double dx = x0 - x3; + final double dy = y0 - y3; + final double startToEndDistance = math.sqrt(dx * dx + dy * dy); + final double prevDistance = distance; + distance += startToEndDistance; + if (distance > prevDistance) { + segments.add(_PathSegment(PathCommandTypes.bezierCurveTo, + distance, [x0, y0, x1, y1, x2, y2, x3, y3])); + } + } + return distance; + } + + static bool _quadTooCurvy( + double x0, double y0, double x1, double y1, double x2, double y2) { + // (a/4 + b/2 + c/4) - (a/2 + c/2) = -a/4 + b/2 - c/4 + final double dx = (x1 / 2) - (x0 + x2) / 4; + if (dx.abs() > _fTolerance) { + return true; + } + final double dy = (y1 / 2) - (y0 + y2) / 4; + if (dy.abs() > _fTolerance) { + return true; + } + return false; + } + + double _computeQuadSegments(double x0, double y0, double x1, double y1, + double x2, double y2, double distance, int tMin, int tMax) { + if (_tspanBigEnough(tMax - tMin) && _quadTooCurvy(x0, y0, x1, y1, x2, y2)) { + final double p01x = (x0 + x1) / 2; + final double p01y = (y0 + y1) / 2; + final double p12x = (x1 + x2) / 2; + final double p12y = (y1 + y2) / 2; + final double p012x = (p01x + p12x) / 2; + final double p012y = (p01y + p12y) / 2; + final int tHalf = (tMin + tMax) >> 1; + distance = _computeQuadSegments( + x0, y0, p01x, p01y, p012x, p012y, distance, tMin, tHalf); + distance = _computeQuadSegments( + p012x, p012y, p12x, p12y, x2, y2, distance, tMin, tHalf); + } else { + final double dx = x0 - x2; + final double dy = y0 - y2; + final double startToEndDistance = math.sqrt(dx * dx + dy * dy); + final double prevDistance = distance; + distance += startToEndDistance; + if (distance > prevDistance) { + _segments.add(_PathSegment(PathCommandTypes.quadraticCurveTo, + distance, [x0, y0, x1, y1, x2, y2])); + } + } + return distance; + } + + // Create segments by converting arc to cubics. + // See http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter. + static void _computeEllipseSegments( + double startX, + double startY, + double distance, + double cx, + double cy, + double startAngle, + double endAngle, + double rotation, + double radiusX, + double radiusY, + bool anticlockwise, + _EllipseSegmentResult result, + List<_PathSegment> segments) { + final double endX = cx + (radiusX * math.cos(endAngle)); + final double endY = cy + (radiusY * math.sin(endAngle)); + result.endPointX = endX; + result.endPointY = endY; + // Check for http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters + // Treat as line segment from start to end if arc has zero radii. + // If start and end point are the same treat as zero length path. + if ((radiusX == 0 || radiusY == 0) || (startX == endX && startY == endY)) { + result.distance = distance; + return; + } + final double rxAbs = radiusX.abs(); + final double ryAbs = radiusY.abs(); + + final double theta1 = startAngle; + final double theta2 = endAngle; + final double thetaArc = theta2 - theta1; + + // Add 0.01f to make sure we have enough segments when thetaArc is close + // to pi/2. + final int numSegments = (thetaArc / ((math.pi / 2.0) + 0.01)).abs().ceil(); + double x0 = startX; + double y0 = startY; + for (int segmentIndex = 0; segmentIndex < numSegments; segmentIndex++) { + final double startTheta = + theta1 + (segmentIndex * thetaArc / numSegments); + final double endTheta = + theta1 + ((segmentIndex + 1) * thetaArc / numSegments); + final double t = (4.0 / 3.0) * math.tan((endTheta - startTheta) / 4); + if (!t.isFinite) { + result.distance = distance; + return; + } + final double sinStartTheta = math.sin(startTheta); + final double cosStartTheta = math.cos(startTheta); + final double sinEndTheta = math.sin(endTheta); + final double cosEndTheta = math.cos(endTheta); + + // Compute cubic segment start, control point and end (target). + final double p1x = rxAbs * (cosStartTheta - t * sinStartTheta) + cx; + final double p1y = ryAbs * (sinStartTheta + t * cosStartTheta) + cy; + final double targetPointX = rxAbs * cosEndTheta + cx; + final double targetPointY = ryAbs * sinEndTheta + cy; + final double p2x = targetPointX + rxAbs * (t * sinEndTheta); + final double p2y = targetPointY + ryAbs * (-t * cosEndTheta); + + distance = _computeCubicSegments(x0, y0, p1x, p1y, p2x, p2y, targetPointX, + targetPointY, distance, 0, _kMaxTValue, segments); + x0 = targetPointX; + y0 = targetPointY; + } + result.distance = distance; + } + + @override + String toString() => 'PathMetric'; +} + +class _EllipseSegmentResult { + double endPointX; + double endPointY; + double distance; + _EllipseSegmentResult(); +} + +class _PathSegment { + _PathSegment(this.segmentType, this.distance, this.points); + + final int segmentType; + final double distance; + final List points; +} diff --git a/lib/web_ui/lib/src/engine/recording_canvas.dart b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart similarity index 96% rename from lib/web_ui/lib/src/engine/recording_canvas.dart rename to lib/web_ui/lib/src/engine/surface/recording_canvas.dart index 891390fca4464..07031bcb89890 100644 --- a/lib/web_ui/lib/src/engine/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart @@ -106,7 +106,7 @@ class RecordingCanvas { _saveCount++; } - void saveLayerWithoutBounds(ui.Paint paint) { + void saveLayerWithoutBounds(SurfacePaint paint) { _hasArbitraryPaint = true; // TODO(het): Implement this correctly using another canvas. _commands.add(const PaintSave()); @@ -114,7 +114,7 @@ class RecordingCanvas { _saveCount++; } - void saveLayer(ui.Rect bounds, ui.Paint paint) { + void saveLayer(ui.Rect bounds, SurfacePaint paint) { _hasArbitraryPaint = true; // TODO(het): Implement this correctly using another canvas. _commands.add(const PaintSave()); @@ -187,7 +187,7 @@ class RecordingCanvas { _commands.add(PaintDrawColor(color, blendMode)); } - void drawLine(ui.Offset p1, ui.Offset p2, ui.Paint paint) { + void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaint paint) { final double strokeWidth = math.max(paint.strokeWidth, 1.0); // TODO(yjbanov): This can be optimized. Currently we create a box around // the line and then apply the transform on the box to get @@ -203,17 +203,17 @@ class RecordingCanvas { math.max(p1.dy, p2.dy) + strokeWidth); _hasArbitraryPaint = true; _didDraw = true; - _commands.add(PaintDrawLine(p1, p2, paint.webOnlyPaintData)); + _commands.add(PaintDrawLine(p1, p2, paint.paintData)); } - void drawPaint(ui.Paint paint) { + void drawPaint(SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; _paintBounds.grow(_paintBounds.maxPaintBounds); - _commands.add(PaintDrawPaint(paint.webOnlyPaintData)); + _commands.add(PaintDrawPaint(paint.paintData)); } - void drawRect(ui.Rect rect, ui.Paint paint) { + void drawRect(ui.Rect rect, SurfacePaint paint) { if (paint.shader != null) { _hasArbitraryPaint = true; } @@ -223,10 +223,10 @@ class RecordingCanvas { } else { _paintBounds.grow(rect); } - _commands.add(PaintDrawRect(rect, paint.webOnlyPaintData)); + _commands.add(PaintDrawRect(rect, paint.paintData)); } - void drawRRect(ui.RRect rrect, ui.Paint paint) { + void drawRRect(ui.RRect rrect, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; final double strokeWidth = @@ -236,10 +236,10 @@ class RecordingCanvas { final double top = math.min(rrect.top, rrect.bottom) - strokeWidth; final double bottom = math.max(rrect.top, rrect.bottom) + strokeWidth; _paintBounds.growLTRB(left, top, right, bottom); - _commands.add(PaintDrawRRect(rrect, paint.webOnlyPaintData)); + _commands.add(PaintDrawRRect(rrect, paint.paintData)); } - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) { + void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaint paint) { // Check the inner bounds are contained within the outer bounds // see: https://cs.chromium.org/chromium/src/third_party/skia/src/core/SkCanvas.cpp?l=1787-1789 ui.Rect innerRect = inner.outerRect; @@ -272,10 +272,10 @@ class RecordingCanvas { paint.strokeWidth == null ? 0 : paint.strokeWidth; _paintBounds.growLTRB(outer.left - strokeWidth, outer.top - strokeWidth, outer.right + strokeWidth, outer.bottom + strokeWidth); - _commands.add(PaintDrawDRRect(outer, inner, paint.webOnlyPaintData)); + _commands.add(PaintDrawDRRect(outer, inner, paint.paintData)); } - void drawOval(ui.Rect rect, ui.Paint paint) { + void drawOval(ui.Rect rect, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; if (paint.strokeWidth != null) { @@ -283,10 +283,10 @@ class RecordingCanvas { } else { _paintBounds.grow(rect); } - _commands.add(PaintDrawOval(rect, paint.webOnlyPaintData)); + _commands.add(PaintDrawOval(rect, paint.paintData)); } - void drawCircle(ui.Offset c, double radius, ui.Paint paint) { + void drawCircle(ui.Offset c, double radius, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; final double strokeWidth = @@ -296,10 +296,10 @@ class RecordingCanvas { c.dy - radius - strokeWidth, c.dx + radius + strokeWidth, c.dy + radius + strokeWidth); - _commands.add(PaintDrawCircle(c, radius, paint.webOnlyPaintData)); + _commands.add(PaintDrawCircle(c, radius, paint.paintData)); } - void drawPath(ui.Path path, ui.Paint paint) { + void drawPath(ui.Path path, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; ui.Rect pathBounds = path.getBounds(); @@ -310,23 +310,23 @@ class RecordingCanvas { // Clone path so it can be reused for subsequent draw calls. final ui.Path clone = ui.Path.from(path); clone.fillType = path.fillType; - _commands.add(PaintDrawPath(clone, paint.webOnlyPaintData)); + _commands.add(PaintDrawPath(clone, paint.paintData)); } - void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) { + void drawImage(ui.Image image, ui.Offset offset, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; final double left = offset.dx; final double top = offset.dy; _paintBounds.growLTRB(left, top, left + image.width, top + image.height); - _commands.add(PaintDrawImage(image, offset, paint.webOnlyPaintData)); + _commands.add(PaintDrawImage(image, offset, paint.paintData)); } - void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, ui.Paint paint) { + void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; _paintBounds.grow(dst); - _commands.add(PaintDrawImageRect(image, src, dst, paint.webOnlyPaintData)); + _commands.add(PaintDrawImageRect(image, src, dst, paint.paintData)); } void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { @@ -358,7 +358,7 @@ class RecordingCanvas { } void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - ui.Paint paint) { + SurfacePaint paint) { _hasArbitraryPaint = true; _didDraw = true; final Float32List positions = vertices.positions; @@ -380,7 +380,7 @@ class RecordingCanvas { maxValueY = math.max(maxValueY, y); } _paintBounds.growLTRB(minValueX, minValueY, maxValueX, maxValueY); - _commands.add(PaintVertices(vertices, blendMode, paint.webOnlyPaintData)); + _commands.add(PaintVertices(vertices, blendMode, paint.paintData)); } int _saveCount = 1; @@ -630,7 +630,7 @@ class PaintClipRRect extends PaintCommand { } class PaintClipPath extends PaintCommand { - final ui.Path path; + final SurfacePath path; PaintClipPath(this.path); @@ -683,7 +683,7 @@ class PaintDrawColor extends PaintCommand { class PaintDrawLine extends PaintCommand { final ui.Offset p1; final ui.Offset p2; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawLine(this.p1, this.p2, this.paint); @@ -715,7 +715,7 @@ class PaintDrawLine extends PaintCommand { } class PaintDrawPaint extends PaintCommand { - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawPaint(this.paint); @@ -742,7 +742,7 @@ class PaintDrawPaint extends PaintCommand { class PaintVertices extends PaintCommand { final ui.Vertices vertices; final ui.BlendMode blendMode; - final ui.PaintData paint; + final SurfacePaintData paint; PaintVertices(this.vertices, this.blendMode, this.paint); @override @@ -767,7 +767,7 @@ class PaintVertices extends PaintCommand { class PaintDrawRect extends PaintCommand { final ui.Rect rect; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawRect(this.rect, this.paint); @@ -797,7 +797,7 @@ class PaintDrawRect extends PaintCommand { class PaintDrawRRect extends PaintCommand { final ui.RRect rrect; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawRRect(this.rrect, this.paint); @@ -828,7 +828,7 @@ class PaintDrawRRect extends PaintCommand { class PaintDrawDRRect extends PaintCommand { final ui.RRect outer; final ui.RRect inner; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawDRRect(this.outer, this.inner, this.paint); @@ -859,7 +859,7 @@ class PaintDrawDRRect extends PaintCommand { class PaintDrawOval extends PaintCommand { final ui.Rect rect; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawOval(this.rect, this.paint); @@ -890,7 +890,7 @@ class PaintDrawOval extends PaintCommand { class PaintDrawCircle extends PaintCommand { final ui.Offset c; final double radius; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawCircle(this.c, this.radius, this.paint); @@ -921,8 +921,8 @@ class PaintDrawCircle extends PaintCommand { } class PaintDrawPath extends PaintCommand { - final ui.Path path; - final ui.PaintData paint; + final SurfacePath path; + final SurfacePaintData paint; PaintDrawPath(this.path, this.paint); @@ -954,7 +954,7 @@ class PaintDrawShadow extends PaintCommand { PaintDrawShadow( this.path, this.color, this.elevation, this.transparentOccluder); - final ui.Path path; + final SurfacePath path; final ui.Color color; final double elevation; final bool transparentOccluder; @@ -993,7 +993,7 @@ class PaintDrawShadow extends PaintCommand { class PaintDrawImage extends PaintCommand { final ui.Image image; final ui.Offset offset; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawImage(this.image, this.offset, this.paint); @@ -1023,7 +1023,7 @@ class PaintDrawImageRect extends PaintCommand { final ui.Image image; final ui.Rect src; final ui.Rect dst; - final ui.PaintData paint; + final SurfacePaintData paint; PaintDrawImageRect(this.image, this.src, this.dst, this.paint); @@ -1077,7 +1077,7 @@ class PaintDrawParagraph extends PaintCommand { } } -List _serializePaintToCssPaint(ui.PaintData paint) { +List _serializePaintToCssPaint(SurfacePaintData paint) { final EngineGradient engineShader = paint.shader; return [ paint.blendMode?.index, diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index c4d7ab135e9fb..f275a22900e73 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -127,10 +127,10 @@ class EngineParagraph implements ui.Paragraph { final html.HtmlElement _paragraphElement; final ParagraphGeometricStyle _geometricStyle; final String _plainText; - final ui.Paint _paint; + final SurfacePaint _paint; final ui.TextAlign _textAlign; final ui.TextDirection _textDirection; - final ui.Paint _background; + final SurfacePaint _background; @visibleForTesting String get plainText => _plainText; diff --git a/lib/web_ui/lib/src/ui/canvas.dart b/lib/web_ui/lib/src/ui/canvas.dart index c340e443ee24a..7662d531c756f 100644 --- a/lib/web_ui/lib/src/ui/canvas.dart +++ b/lib/web_ui/lib/src/ui/canvas.dart @@ -1175,12 +1175,3 @@ class RawRecordingCanvas extends engine.BitmapCanvas @override Rect cullRect; } - -// Returns true if point is inside ellipse. -bool _ellipseContains(double px, double py, double centerX, double centerY, - double radiusX, double radiusY) { - final double dx = px - centerX; - final double dy = py - centerY; - return ((dx * dx) / (radiusX * radiusX)) + ((dy * dy) / (radiusY * radiusY)) < - 1.0; -} diff --git a/lib/web_ui/lib/src/ui/painting.dart b/lib/web_ui/lib/src/ui/painting.dart index 1460e1b08f2cd..7a41c55d724d6 100644 --- a/lib/web_ui/lib/src/ui/painting.dart +++ b/lib/web_ui/lib/src/ui/painting.dart @@ -922,48 +922,33 @@ enum Clip { antiAliasWithSaveLayer, } -/// Private Paint context data used for recording canvas commands allowing -/// Paint to be mutated post canvas draw operations. -class PaintData { - BlendMode blendMode; - PaintingStyle style; - double strokeWidth; - StrokeCap strokeCap; - StrokeJoin strokeJoin; - bool isAntiAlias = true; - Color color; - Shader shader; - MaskFilter maskFilter; - FilterQuality filterQuality; - ColorFilter colorFilter; - - // Internal for recording canvas use. - PaintData clone() { - return PaintData() - ..blendMode = blendMode - ..filterQuality = filterQuality - ..maskFilter = maskFilter - ..shader = shader - ..isAntiAlias = isAntiAlias - ..color = color - ..colorFilter = colorFilter - ..strokeWidth = strokeWidth - ..style = style - ..strokeJoin = strokeJoin - ..strokeCap = strokeCap; - } -} - /// A description of the style to use when drawing on a [Canvas]. /// /// Most APIs on [Canvas] take a [Paint] object to describe the style /// to use for that operation. -class Paint { - PaintData _paintData = PaintData(); - +abstract class Paint { /// Constructs an empty [Paint] object with all fields initialized to /// their defaults. - Paint(); + factory Paint() => engine.experimentalUseSkia + ? engine.SkPaint() + : engine.SurfacePaint(); + + /// Whether to dither the output when drawing images. + /// + /// If false, the default value, dithering will be enabled when the input + /// color depth is higher than the output color depth. For example, + /// drawing an RGB8 image onto an RGB565 canvas. + /// + /// This value also controls dithering of [shader]s, which can make + /// gradients appear smoother. + /// + /// Whether or not dithering affects the output is implementation defined. + /// Some implementations may choose to ignore this completely, if they're + /// unable to control dithering. + /// + /// To ensure that dithering is consistently enabled for your entire + /// application, set this to true before invoking any drawing related code. + static bool enableDithering = false; /// A blend mode to apply when a shape is drawn or a layer is composited. /// @@ -982,28 +967,14 @@ class Paint { /// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite /// the layer when [restore] is called. /// * [BlendMode], which discusses the user of [saveLayer] with [blendMode]. - BlendMode get blendMode => _paintData.blendMode ?? BlendMode.srcOver; - set blendMode(BlendMode value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.blendMode = value; - } - - BlendMode _blendMode; + BlendMode get blendMode; + set blendMode(BlendMode value); /// Whether to paint inside shapes, the edges of shapes, or both. /// /// If null, defaults to [PaintingStyle.fill]. - PaintingStyle get style => _paintData.style ?? PaintingStyle.fill; - set style(PaintingStyle value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.style = value; - } + PaintingStyle get style; + set style(PaintingStyle value); /// How wide to make edges drawn when [style] is set to /// [PaintingStyle.stroke] or [PaintingStyle.strokeAndFill]. The @@ -1011,78 +982,43 @@ class Paint { /// orthogonal to the direction of the path. /// /// The values null and 0.0 correspond to a hairline width. - double get strokeWidth => _paintData.strokeWidth ?? 0.0; - set strokeWidth(double value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.strokeWidth = value; - } + double get strokeWidth; + set strokeWidth(double value); /// The kind of finish to place on the end of lines drawn when /// [style] is set to [PaintingStyle.stroke] or /// [PaintingStyle.strokeAndFill]. /// /// If null, defaults to [StrokeCap.butt], i.e. no caps. - StrokeCap get strokeCap => _paintData.strokeCap; - set strokeCap(StrokeCap value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.strokeCap = value; - } + StrokeCap get strokeCap; + set strokeCap(StrokeCap value); /// The kind of finish to use for line segment joins. /// [style] is set to [PaintingStyle.stroke] or /// [PaintingStyle.strokeAndFill]. Only applies to drawPath not drawPoints. /// /// If null, defaults to [StrokeCap.butt], i.e. no caps. - StrokeJoin get strokeJoin => _paintData.strokeJoin; - set strokeJoin(StrokeJoin value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.strokeJoin = value; - } + StrokeJoin get strokeJoin; + set strokeJoin(StrokeJoin value); /// Whether to apply anti-aliasing to lines and images drawn on the /// canvas. /// /// Defaults to true. The value null is treated as false. - bool get isAntiAlias => _paintData.isAntiAlias; - set isAntiAlias(bool value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.isAntiAlias = value; - } + bool get isAntiAlias; + set isAntiAlias(bool value); - Color get color => _paintData.color; - set color(Color value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.color = value.runtimeType == Color ? value : Color(value.value); - } + Color get color; + set color(Color value); /// Whether the colors of the image are inverted when drawn. /// /// Inverting the colors of an image applies a new color filter that will /// be composed with any user provided color filters. This is primarily /// used for implementing smart invert on iOS. - bool get invertColors { - return false; - } - - set invertColors(bool value) {} + bool get invertColors; - Color _color = _defaultPaintColor; - static const Color _defaultPaintColor = Color(0xFF000000); + set invertColors(bool value); /// The shader to use when stroking or filling a shape. /// @@ -1094,27 +1030,15 @@ class Paint { /// * [ImageShader], a shader that tiles an [Image]. /// * [colorFilter], which overrides [shader]. /// * [color], which is used if [shader] and [colorFilter] are null. - Shader get shader => _paintData.shader; - set shader(Shader value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.shader = value; - } + Shader get shader; + set shader(Shader value); /// A mask filter (for example, a blur) to apply to a shape after it has been /// drawn but before it has been composited into the image. /// /// See [MaskFilter] for details. - MaskFilter get maskFilter => _paintData.maskFilter; - set maskFilter(MaskFilter value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.maskFilter = value; - } + MaskFilter get maskFilter; + set maskFilter(MaskFilter value); /// Controls the performance vs quality trade-off to use when applying /// filters, such as [maskFilter], or when drawing images, as with @@ -1122,14 +1046,8 @@ class Paint { /// /// Defaults to [FilterQuality.none]. // TODO(ianh): verify that the image drawing methods actually respect this - FilterQuality get filterQuality => _paintData.filterQuality; - set filterQuality(FilterQuality value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.filterQuality = value; - } + FilterQuality get filterQuality; + set filterQuality(FilterQuality value); /// A color filter to apply when a shape is drawn or when a layer is /// composited. @@ -1137,23 +1055,11 @@ class Paint { /// See [ColorFilter] for details. /// /// When a shape is being drawn, [colorFilter] overrides [color] and [shader]. - ColorFilter get colorFilter => _paintData.colorFilter; - set colorFilter(ColorFilter value) { - if (_frozen) { - _paintData = _paintData.clone(); - _frozen = false; - } - _paintData.colorFilter = value; - } + ColorFilter get colorFilter; + set colorFilter(ColorFilter value); - // TODO(flutter_web): see https://github.com/flutter/flutter/issues/33605 - double get strokeMiterLimit { - return null; - } - - set strokeMiterLimit(double value) { - assert(value != null); - } + double get strokeMiterLimit; + set strokeMiterLimit(double value); /// The [ImageFilter] to use when drawing raster images. /// @@ -1177,72 +1083,8 @@ class Paint { /// See also: /// /// * [MaskFilter], which is used for drawing geometry. - ImageFilter get imageFilter { - // TODO(flutter/flutter#35156): Implement ImageFilter. - return null; - } - - set imageFilter(ImageFilter value) { - // TODO(flutter/flutter#35156): Implement ImageFilter. - } - - /// Whether to dither the output when drawing images. - /// - /// If false, the default value, dithering will be enabled when the input - /// color depth is higher than the output color depth. For example, - /// drawing an RGB8 image onto an RGB565 canvas. - /// - /// This value also controls dithering of [shader]s, which can make - /// gradients appear smoother. - /// - /// Whether or not dithering affects the output is implementation defined. - /// Some implementations may choose to ignore this completely, if they're - /// unable to control dithering. - /// - /// To ensure that dithering is consistently enabled for your entire - /// application, set this to true before invoking any drawing related code. - static bool enableDithering = false; - - // True if Paint instance has used in RecordingCanvas. - bool _frozen = false; - - // Marks this paint object as previously used. - PaintData get webOnlyPaintData { - // Flip bit so next time object gets mutated we create a clone of - // current paint data. - _frozen = true; - return _paintData; - } - - @override - String toString() { - final StringBuffer result = StringBuffer(); - String semicolon = ''; - result.write('Paint('); - if (style == PaintingStyle.stroke) { - result.write('$style'); - if (strokeWidth != null && strokeWidth != 0.0) - result.write(' $strokeWidth'); - else - result.write(' hairline'); - if (strokeCap != null && strokeCap != StrokeCap.butt) - result.write(' $strokeCap'); - semicolon = '; '; - } - if (isAntiAlias != true) { - result.write('${semicolon}antialias off'); - semicolon = '; '; - } - if (color != _defaultPaintColor) { - if (color != null) - result.write('$semicolon$color'); - else - result.write('${semicolon}no color'); - semicolon = '; '; - } - result.write(')'); - return result.toString(); - } + ImageFilter get imageFilter; + set imageFilter(ImageFilter value); } /// Base class for objects such as [Gradient] and [ImageShader] which diff --git a/lib/web_ui/lib/src/ui/path.dart b/lib/web_ui/lib/src/ui/path.dart index 8774ffc984883..3ad50786b8db9 100644 --- a/lib/web_ui/lib/src/ui/path.dart +++ b/lib/web_ui/lib/src/ui/path.dart @@ -21,161 +21,78 @@ part of ui; /// /// Paths can be drawn on canvases using [Canvas.drawPath], and can /// used to create clip regions using [Canvas.clipPath]. -class Path { - final List subpaths; - PathFillType _fillType = PathFillType.nonZero; - - engine.Subpath get _currentSubpath => subpaths.isEmpty ? null : subpaths.last; - - List get _commands => _currentSubpath?.commands; - - /// The current x-coordinate for this path. - double get _currentX => _currentSubpath?.currentX ?? 0.0; - - /// The current y-coordinate for this path. - double get _currentY => _currentSubpath?.currentY ?? 0.0; - - /// Recorder used for hit testing paths. - static RawRecordingCanvas _rawRecorder; - +abstract class Path { /// Create a new empty [Path] object. factory Path() { if (engine.experimentalUseSkia) { return engine.SkPath(); } else { - return Path._(); + return engine.SurfacePath(); } } - Path._() : subpaths = []; - /// Creates a copy of another [Path]. /// /// This copy is fast and does not require additional memory unless either /// the `source` path or the path returned by this constructor are modified. - Path.from(Path source) - : subpaths = List.from(source.subpaths); - - Path._clone(this.subpaths, this._fillType); + factory Path.from(Path source) { + if (engine.experimentalUseSkia) { + return engine.SkPath.from(source); + } else { + return engine.SurfacePath.from(source); + } + } /// Determines how the interior of this path is calculated. /// /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. - PathFillType get fillType => _fillType; - set fillType(PathFillType value) { - _fillType = value; - } - - /// Opens a new subpath with starting point (x, y). - void _openNewSubpath(double x, double y) { - subpaths.add(engine.Subpath(x, y)); - _setCurrentPoint(x, y); - } - - /// Sets the current point to (x, y). - void _setCurrentPoint(double x, double y) { - _currentSubpath.currentX = x; - _currentSubpath.currentY = y; - } + PathFillType get fillType; + set fillType(PathFillType value); /// Starts a new subpath at the given coordinate. - void moveTo(double x, double y) { - _openNewSubpath(x, y); - _commands.add(engine.MoveTo(x, y)); - } + void moveTo(double x, double y); /// Starts a new subpath at the given offset from the current point. - void relativeMoveTo(double dx, double dy) { - final double newX = _currentX + dx; - final double newY = _currentY + dy; - _openNewSubpath(newX, newY); - _commands.add(engine.MoveTo(newX, newY)); - } + void relativeMoveTo(double dx, double dy); /// Adds a straight line segment from the current point to the given /// point. - void lineTo(double x, double y) { - if (subpaths.isEmpty) { - moveTo(0.0, 0.0); - } - _commands.add(engine.LineTo(x, y)); - _setCurrentPoint(x, y); - } + void lineTo(double x, double y); /// Adds a straight line segment from the current point to the point /// at the given offset from the current point. - void relativeLineTo(double dx, double dy) { - final double newX = _currentX + dx; - final double newY = _currentY + dy; - if (subpaths.isEmpty) { - moveTo(0.0, 0.0); - } - _commands.add(engine.LineTo(newX, newY)); - _setCurrentPoint(newX, newY); - } - - void _ensurePathStarted() { - if (subpaths.isEmpty) { - subpaths.add(engine.Subpath(0.0, 0.0)); - } - } + void relativeLineTo(double dx, double dy); /// Adds a quadratic bezier segment that curves from the current /// point to the given point (x2,y2), using the control point /// (x1,y1). - void quadraticBezierTo(double x1, double y1, double x2, double y2) { - _ensurePathStarted(); - _commands.add(engine.QuadraticCurveTo(x1, y1, x2, y2)); - _setCurrentPoint(x2, y2); - } + void quadraticBezierTo(double x1, double y1, double x2, double y2); /// Adds a quadratic bezier segment that curves from the current /// point to the point at the offset (x2,y2) from the current point, /// using the control point at the offset (x1,y1) from the current /// point. - void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { - _ensurePathStarted(); - _commands.add(engine.QuadraticCurveTo( - x1 + _currentX, y1 + _currentY, x2 + _currentX, y2 + _currentY)); - _setCurrentPoint(x2 + _currentX, y2 + _currentY); - } + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2); /// Adds a cubic bezier segment that curves from the current point /// to the given point (x3,y3), using the control points (x1,y1) and /// (x2,y2). void cubicTo( - double x1, double y1, double x2, double y2, double x3, double y3) { - _ensurePathStarted(); - _commands.add(engine.BezierCurveTo(x1, y1, x2, y2, x3, y3)); - _setCurrentPoint(x3, y3); - } + double x1, double y1, double x2, double y2, double x3, double y3); /// Adds a cubic bezier segment that curves from the current point /// to the point at the offset (x3,y3) from the current point, using /// the control points at the offsets (x1,y1) and (x2,y2) from the /// current point. void relativeCubicTo( - double x1, double y1, double x2, double y2, double x3, double y3) { - _ensurePathStarted(); - _commands.add(engine.BezierCurveTo(x1 + _currentX, y1 + _currentY, - x2 + _currentX, y2 + _currentY, x3 + _currentX, y3 + _currentY)); - _setCurrentPoint(x3 + _currentX, y3 + _currentY); - } + double x1, double y1, double x2, double y2, double x3, double y3); /// Adds a bezier segment that curves from the current point to the /// given point (x2,y2), using the control points (x1,y1) and the /// weight w. If the weight is greater than 1, then the curve is a /// hyperbola; if the weight equals 1, it's a parabola; and if it is /// less than 1, it is an ellipse. - void conicTo(double x1, double y1, double x2, double y2, double w) { - final List quads = - engine.Conic(_currentX, _currentY, x1, y1, x2, y2, w).toQuads(); - final int len = quads.length; - for (int i = 1; i < len; i += 2) { - quadraticBezierTo( - quads[i].dx, quads[i].dy, quads[i + 1].dx, quads[i + 1].dy); - } - } + void conicTo(double x1, double y1, double x2, double y2, double w); /// Adds a bezier segment that curves from the current point to the /// point at the offset (x2,y2) from the current point, using the @@ -183,9 +100,7 @@ class Path { /// the weight w. If the weight is greater than 1, then the curve is /// a hyperbola; if the weight equals 1, it's a parabola; and if it /// is less than 1, it is an ellipse. - void relativeConicTo(double x1, double y1, double x2, double y2, double w) { - conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); - } + void relativeConicTo(double x1, double y1, double x2, double y2, double w); /// If the `forceMoveTo` argument is false, adds a straight line /// segment and an arc segment. @@ -204,24 +119,7 @@ class Path { /// The line segment added if `forceMoveTo` is false starts at the /// current point and ends at the start of the arc. void arcTo( - Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { - assert(engine.rectIsValid(rect)); - final Offset center = rect.center; - final double radiusX = rect.width / 2; - final double radiusY = rect.height / 2; - final double startX = radiusX * math.cos(startAngle) + center.dx; - final double startY = radiusY * math.sin(startAngle) + center.dy; - if (forceMoveTo) { - _openNewSubpath(startX, startY); - } else { - lineTo(startX, startY); - } - _commands.add(engine.Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, - startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, - radiusY * math.sin(startAngle + sweepAngle) + center.dy); - } + Rect rect, double startAngle, double sweepAngle, bool forceMoveTo); /// Appends up to four conic curves weighted to describe an oval of `radius` /// and rotated by `rotation`. @@ -244,111 +142,7 @@ class Path { double rotation = 0.0, bool largeArc = false, bool clockwise = true, - }) { - assert(engine.offsetIsValid(arcEnd)); - assert(engine.radiusIsValid(radius)); - // _currentX, _currentY are the coordinates of start point on path, - // arcEnd is final point of arc. - // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) - // largeArc is false if arc is spanning less than or equal to 180 degrees. - // clockwise is false if arc sweeps through decreasing angles or true - // if sweeping through increasing angles. - // rotation is the angle from the x-axis of the current coordinate - // system to the x-axis of the eclipse. - - double rx = radius.x.abs(); - double ry = radius.y.abs(); - - // If the current point and target point for the arc are identical, it - // should be treated as a zero length path. This ensures continuity in - // animations. - final bool isSamePoint = _currentX == arcEnd.dx && _currentY == arcEnd.dy; - - // If rx = 0 or ry = 0 then this arc is treated as a straight line segment - // (a "lineto") joining the endpoints. - // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters - if (isSamePoint || rx.toInt() == 0 || ry.toInt() == 0) { - _commands.add(engine.LineTo(arcEnd.dx, arcEnd.dy)); - _setCurrentPoint(arcEnd.dx, arcEnd.dy); - return; - } - - // As an intermediate point to finding center parametrization, place the - // origin on the midpoint between start/end points and rotate to align - // coordinate axis with axes of the ellipse. - final double midPointX = (_currentX - arcEnd.dx) / 2.0; - final double midPointY = (_currentY - arcEnd.dy) / 2.0; - - // Convert rotation or radians. - final double xAxisRotation = math.pi * rotation / 180.0; - - // Cache cos/sin value. - final double cosXAxisRotation = math.cos(xAxisRotation); - final double sinXAxisRotation = math.sin(xAxisRotation); - - // Calculate rotate midpoint as x/yPrime. - final double xPrime = - (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); - final double yPrime = - (-sinXAxisRotation * midPointX) + (cosXAxisRotation * midPointY); - - // Check if the radii are big enough to draw the arc, scale radii if not. - // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii - double rxSquare = rx * rx; - double rySquare = ry * ry; - final double xPrimeSquare = xPrime * xPrime; - final double yPrimeSquare = yPrime * yPrime; - - double radiiScale = (xPrimeSquare / rxSquare) + (yPrimeSquare / rySquare); - if (radiiScale > 1) { - radiiScale = math.sqrt(radiiScale); - rx *= radiiScale; - ry *= radiiScale; - rxSquare = rx * rx; - rySquare = ry * ry; - } - - // Compute transformed center. eq. 5.2 - final double distanceSquare = - (rxSquare * yPrimeSquare) + rySquare * xPrimeSquare; - final double cNumerator = (rxSquare * rySquare) - distanceSquare; - double scaleFactor = math.sqrt(math.max(cNumerator / distanceSquare, 0.0)); - if (largeArc == clockwise) { - scaleFactor = -scaleFactor; - } - // Ready to compute transformed center. - final double cxPrime = scaleFactor * ((rx * yPrime) / ry); - final double cyPrime = scaleFactor * (-(ry * xPrime) / rx); - - // Rotate to find actual center. - final double cx = cosXAxisRotation * cxPrime - - sinXAxisRotation * cyPrime + - ((_currentX + arcEnd.dx) / 2.0); - final double cy = sinXAxisRotation * cxPrime + - cosXAxisRotation * cyPrime + - ((_currentY + arcEnd.dy) / 2.0); - - // Calculate start angle and sweep. - // Start vector is from midpoint of start/end points to transformed center. - final double startVectorX = (xPrime - cxPrime) / rx; - final double startVectorY = (yPrime - cyPrime) / ry; - - final double startAngle = math.atan2(startVectorY, startVectorX); - final double endVectorX = (-xPrime - cxPrime) / rx; - final double endVectorY = (-yPrime - cyPrime) / ry; - double sweepAngle = math.atan2(endVectorY, endVectorX) - startAngle; - - if (clockwise && sweepAngle < 0) { - sweepAngle += math.pi * 2.0; - } else if (!clockwise && sweepAngle > 0) { - sweepAngle -= math.pi * 2.0; - } - - _commands.add(engine.Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, - startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(arcEnd.dx, arcEnd.dy); - } + }); /// Appends up to four conic curves weighted to describe an oval of `radius` /// and rotated by `rotation`. @@ -370,24 +164,11 @@ class Path { double rotation = 0.0, bool largeArc = false, bool clockwise = true, - }) { - assert(engine.offsetIsValid(arcEndDelta)); - assert(engine.radiusIsValid(radius)); - arcToPoint(Offset(_currentX + arcEndDelta.dx, _currentY + arcEndDelta.dy), - radius: radius, - rotation: rotation, - largeArc: largeArc, - clockwise: clockwise); - } + }); /// Adds a new subpath that consists of four lines that outline the /// given rectangle. - void addRect(Rect rect) { - assert(engine.rectIsValid(rect)); - _openNewSubpath(rect.left, rect.top); - _commands - .add(engine.RectCommand(rect.left, rect.top, rect.width, rect.height)); - } + void addRect(Rect rect); /// Adds a new subpath that consists of a curve that forms the /// ellipse that fills the given rectangle. @@ -395,17 +176,7 @@ class Path { /// To add a circle, pass an appropriate rectangle as `oval`. /// [Rect.fromCircle] can be used to easily describe the circle's center /// [Offset] and radius. - void addOval(Rect oval) { - assert(engine.rectIsValid(oval)); - final Offset center = oval.center; - final double radiusX = oval.width / 2; - final double radiusY = oval.height / 2; - - /// At startAngle = 0, the path will begin at center + cos(0) * radius. - _openNewSubpath(center.dx + radiusX, center.dy); - _commands.add(engine.Ellipse( - center.dx, center.dy, radiusX, radiusY, 0.0, 0.0, 2 * math.pi, false)); - } + void addOval(Rect oval); /// Adds a new subpath with one arc segment that consists of the arc /// that follows the edge of the oval bounded by the given @@ -415,19 +186,7 @@ class Path { /// crosses the horizontal line that intersects the center of the /// rectangle and with positive angles going clockwise around the /// oval. - void addArc(Rect oval, double startAngle, double sweepAngle) { - assert(engine.rectIsValid(oval)); - final Offset center = oval.center; - final double radiusX = oval.width / 2; - final double radiusY = oval.height / 2; - _openNewSubpath(radiusX * math.cos(startAngle) + center.dx, - radiusY * math.sin(startAngle) + center.dy); - _commands.add(engine.Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, - startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, - radiusY * math.sin(startAngle + sweepAngle) + center.dy); - } + void addArc(Rect oval, double startAngle, double sweepAngle); /// Adds a new subpath with a sequence of line segments that connect the given /// points. @@ -436,37 +195,12 @@ class Path { /// last point to the first point. /// /// The `points` argument is interpreted as offsets from the origin. - void addPolygon(List points, bool close) { - assert(points != null); - if (points.isEmpty) { - return; - } - - moveTo(points.first.dx, points.first.dy); - for (int i = 1; i < points.length; i++) { - final Offset point = points[i]; - lineTo(point.dx, point.dy); - } - if (close) { - this.close(); - } else { - _setCurrentPoint(points.last.dx, points.last.dy); - } - } + void addPolygon(List points, bool close); /// Adds a new subpath that consists of the straight lines and /// curves needed to form the rounded rectangle described by the /// argument. - void addRRect(RRect rrect) { - assert(engine.rrectIsValid(rrect)); - - // Set the current point to the top left corner of the rectangle (the - // point on the top of the rectangle farthest to the left that isn't in - // the rounded corner). - // TODO(het): Is this the current point in Flutter? - _openNewSubpath(rrect.tallMiddleRect.left, rrect.top); - _commands.add(engine.RRectCommand(rrect)); - } + void addRRect(RRect rrect); /// Adds a new subpath that consists of the given `path` offset by the given /// `offset`. @@ -474,32 +208,7 @@ class Path { /// If `matrix4` is specified, the path will be transformed by this matrix /// after the matrix is translated by the given offset. The matrix is a 4x4 /// matrix stored in column major order. - void addPath(Path path, Offset offset, {Float64List matrix4}) { - assert(path != null); // path is checked on the engine side - assert(engine.offsetIsValid(offset)); - if (matrix4 != null) { - assert(engine.matrix4IsValid(matrix4)); - _addPathWithMatrix(path, offset.dx, offset.dy, matrix4); - } else { - _addPath(path, offset.dx, offset.dy); - } - } - - void _addPath(Path path, double dx, double dy) { - if (dx == 0.0 && dy == 0.0) { - subpaths.addAll(path.subpaths); - } else { - subpaths.addAll(path - .transform(engine.Matrix4.translationValues(dx, dy, 0.0).storage) - .subpaths); - } - } - - void _addPathWithMatrix(Path path, double dx, double dy, Float64List matrix) { - final engine.Matrix4 transform = engine.Matrix4.fromFloat64List(matrix); - transform.translate(dx, dy); - subpaths.addAll(path.transform(transform.storage).subpaths); - } + void addPath(Path path, Offset offset, {Float64List matrix4}); /// Adds the given path to this path by extending the current segment of this /// path with the the first segment of the given path. @@ -507,48 +216,16 @@ class Path { /// If `matrix4` is specified, the path will be transformed by this matrix /// after the matrix is translated by the given `offset`. The matrix is a 4x4 /// matrix stored in column major order. - void extendWithPath(Path path, Offset offset, {Float64List matrix4}) { - assert(path != null); // path is checked on the engine side - assert(engine.offsetIsValid(offset)); - if (matrix4 != null) { - assert(engine.matrix4IsValid(matrix4)); - _extendWithPathAndMatrix(path, offset.dx, offset.dy, matrix4); - } else { - _extendWithPath(path, offset.dx, offset.dy); - } - } - - void _extendWithPath(Path path, double dx, double dy) { - if (dx == 0.0 && dy == 0.0) { - assert(path.subpaths.length == 1); - _ensurePathStarted(); - _commands.addAll(path.subpaths.single.commands); - _setCurrentPoint( - path.subpaths.single.currentX, path.subpaths.single.currentY); - } else { - throw UnimplementedError('Cannot extend path with non-zero offset'); - } - } - - void _extendWithPathAndMatrix( - Path path, double dx, double dy, Float64List matrix) { - throw UnimplementedError('Cannot extend path with transform matrix'); - } + void extendWithPath(Path path, Offset offset, {Float64List matrix4}); /// Closes the last subpath, as if a straight line had been drawn /// from the current point to the first point of the subpath. - void close() { - _ensurePathStarted(); - _commands.add(const engine.CloseCommand()); - _setCurrentPoint(_currentSubpath.startX, _currentSubpath.startY); - } + void close(); /// Clears the [Path] object of all subpaths, returning it to the /// same state it had when it was created. The _current point_ is /// reset to the origin. - void reset() { - subpaths.clear(); - } + void reset(); /// Tests to see if the given point is within the path. (That is, whether the /// point would be in the visible portion of the path if the path was used @@ -561,115 +238,15 @@ class Path { /// Note: Not very efficient, it creates a canvas, plays path and calls /// Context2D isPointInPath. If performance becomes issue, retaining /// RawRecordingCanvas can remove create/remove rootElement cost. - bool contains(Offset point) { - assert(engine.offsetIsValid(point)); - final int subPathCount = subpaths.length; - if (subPathCount == 0) { - return false; - } - final double pointX = point.dx; - final double pointY = point.dy; - if (subPathCount == 1) { - // Optimize for rect/roundrect checks. - final engine.Subpath subPath = subpaths[0]; - if (subPath.commands.length == 1) { - final engine.PathCommand cmd = subPath.commands[0]; - if (cmd is engine.RectCommand) { - if (pointY < cmd.y || pointY > (cmd.y + cmd.height)) { - return false; - } - if (pointX < cmd.x || pointX > (cmd.x + cmd.width)) { - return false; - } - return true; - } else if (cmd is engine.RRectCommand) { - final RRect rRect = cmd.rrect; - if (pointY < rRect.top || pointY > rRect.bottom) { - return false; - } - if (pointX < rRect.left || pointX > rRect.right) { - return false; - } - if (pointX < (rRect.left + rRect.tlRadiusX) && - pointY < (rRect.top + rRect.tlRadiusY)) { - // Top left corner - return _ellipseContains( - pointX, - pointY, - rRect.left + rRect.tlRadiusX, - rRect.top + rRect.tlRadiusY, - rRect.tlRadiusX, - rRect.tlRadiusY); - } else if (pointX >= (rRect.right - rRect.trRadiusX) && - pointY < (rRect.top + rRect.trRadiusY)) { - // Top right corner - return _ellipseContains( - pointX, - pointY, - rRect.right - rRect.trRadiusX, - rRect.top + rRect.trRadiusY, - rRect.trRadiusX, - rRect.trRadiusY); - } else if (pointX >= (rRect.right - rRect.brRadiusX) && - pointY >= (rRect.bottom - rRect.brRadiusY)) { - // Bottom right corner - return _ellipseContains( - pointX, - pointY, - rRect.right - rRect.brRadiusX, - rRect.bottom - rRect.brRadiusY, - rRect.trRadiusX, - rRect.trRadiusY); - } else if (pointX < (rRect.left + rRect.blRadiusX) && - pointY >= (rRect.bottom - rRect.blRadiusY)) { - // Bottom left corner - return _ellipseContains( - pointX, - pointY, - rRect.left + rRect.blRadiusX, - rRect.bottom - rRect.blRadiusY, - rRect.trRadiusX, - rRect.trRadiusY); - } - return true; - } - } - } - final Size size = window.physicalSize / window.devicePixelRatio; - _rawRecorder ??= RawRecordingCanvas(size); - // Account for the shift due to padding. - _rawRecorder.translate(-engine.BitmapCanvas.kPaddingPixels.toDouble(), - -engine.BitmapCanvas.kPaddingPixels.toDouble()); - _rawRecorder.drawPath( - this, (Paint()..color = const Color(0xFF000000)).webOnlyPaintData); - final bool result = _rawRecorder.ctx.isPointInPath(pointX, pointY); - _rawRecorder.dispose(); - return result; - } + bool contains(Offset point); /// Returns a copy of the path with all the segments of every /// subpath translated by the given offset. - Path shift(Offset offset) { - assert(engine.offsetIsValid(offset)); - final List shiftedSubPaths = []; - for (final engine.Subpath subPath in subpaths) { - shiftedSubPaths.add(subPath.shift(offset)); - } - return Path._clone(shiftedSubPaths, fillType); - } + Path shift(Offset offset); /// Returns a copy of the path with all the segments of every /// sub path transformed by the given matrix. - Path transform(Float64List matrix4) { - assert(engine.matrix4IsValid(matrix4)); - final Path transformedPath = Path(); - for (final engine.Subpath subPath in subpaths) { - for (final engine.PathCommand cmd in subPath.commands) { - cmd.transform(matrix4, transformedPath); - } - } - return transformedPath; - } + Path transform(Float64List matrix4); /// Computes the bounding rectangle for this path. /// @@ -686,328 +263,7 @@ class Path { /// therefore ends up grossly overestimating the actual area covered by the /// circle. // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds - Rect getBounds() { - // Sufficiently small number for curve eq. - const double epsilon = 0.000000001; - bool ltrbInitialized = false; - double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; - double curX = 0.0; - double curY = 0.0; - double minX = 0.0, maxX = 0.0, minY = 0.0, maxY = 0.0; - for (engine.Subpath subpath in subpaths) { - for (engine.PathCommand op in subpath.commands) { - bool skipBounds = false; - switch (op.type) { - case engine.PathCommandTypes.moveTo: - final engine.MoveTo cmd = op; - curX = minX = maxX = cmd.x; - curY = minY = maxY = cmd.y; - break; - case engine.PathCommandTypes.lineTo: - final engine.LineTo cmd = op; - curX = minX = maxX = cmd.x; - curY = minY = maxY = cmd.y; - break; - case engine.PathCommandTypes.ellipse: - final engine.Ellipse cmd = op; - // Rotate 4 corners of bounding box. - final double rx = cmd.radiusX; - final double ry = cmd.radiusY; - final double cosVal = math.cos(cmd.rotation); - final double sinVal = math.sin(cmd.rotation); - final double rxCos = rx * cosVal; - final double ryCos = ry * cosVal; - final double rxSin = rx * sinVal; - final double rySin = ry * sinVal; - - final double leftDeltaX = rxCos - rySin; - final double rightDeltaX = -rxCos - rySin; - final double topDeltaY = ryCos + rxSin; - final double bottomDeltaY = ryCos - rxSin; - - final double centerX = cmd.x; - final double centerY = cmd.y; - - double rotatedX = centerX + leftDeltaX; - double rotatedY = centerY + topDeltaY; - minX = maxX = rotatedX; - minY = maxY = rotatedY; - - rotatedX = centerX + rightDeltaX; - rotatedY = centerY + bottomDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - rotatedX = centerX - leftDeltaX; - rotatedY = centerY - topDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - rotatedX = centerX - rightDeltaX; - rotatedY = centerY - bottomDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - curX = centerX + cmd.radiusX; - curY = centerY; - break; - case engine.PathCommandTypes.quadraticCurveTo: - final engine.QuadraticCurveTo cmd = op; - final double x1 = curX; - final double y1 = curY; - final double cpX = cmd.x1; - final double cpY = cmd.y1; - final double x2 = cmd.x2; - final double y2 = cmd.y2; - - minX = math.min(x1, x2); - minY = math.min(y1, y2); - maxX = math.max(x1, x2); - maxY = math.max(y1, y2); - - // Curve equation : (1-t)(1-t)P1 + 2t(1-t)CP + t*t*P2. - // At extrema's derivative = 0. - // Solve for - // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 - // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 - // t = (x1 - cpX) / (x1 - 2cpX + x2) - - double denom = x1 - (2 * cpX) + x2; - if (denom.abs() > epsilon) { - final num t1 = (x1 - cpX) / denom; - if ((t1 >= 0) && (t1 <= 1.0)) { - // Solve (x,y) for curve at t = tx to find extrema - final num tprime = 1.0 - t1; - final num extremaX = (tprime * tprime * x1) + - (2 * t1 * tprime * cpX) + - (t1 * t1 * x2); - final num extremaY = (tprime * tprime * y1) + - (2 * t1 * tprime * cpY) + - (t1 * t1 * y2); - // Expand bounds. - minX = math.min(minX, extremaX); - maxX = math.max(maxX, extremaX); - minY = math.min(minY, extremaY); - maxY = math.max(maxY, extremaY); - } - } - // Now calculate dy/dt = 0 - denom = y1 - (2 * cpY) + y2; - if (denom.abs() > epsilon) { - final num t2 = (y1 - cpY) / denom; - if ((t2 >= 0) && (t2 <= 1.0)) { - final num tprime2 = 1.0 - t2; - final num extrema2X = (tprime2 * tprime2 * x1) + - (2 * t2 * tprime2 * cpX) + - (t2 * t2 * x2); - final num extrema2Y = (tprime2 * tprime2 * y1) + - (2 * t2 * tprime2 * cpY) + - (t2 * t2 * y2); - // Expand bounds. - minX = math.min(minX, extrema2X); - maxX = math.max(maxX, extrema2X); - minY = math.min(minY, extrema2Y); - maxY = math.max(maxY, extrema2Y); - } - } - curX = x2; - curY = y2; - break; - case engine.PathCommandTypes.bezierCurveTo: - final engine.BezierCurveTo cmd = op; - final double startX = curX; - final double startY = curY; - final double cpX1 = cmd.x1; - final double cpY1 = cmd.y1; - final double cpX2 = cmd.x2; - final double cpY2 = cmd.y2; - final double endX = cmd.x3; - final double endY = cmd.y3; - // Bounding box is defined by all points on the curve where - // monotonicity changes. - minX = math.min(startX, endX); - minY = math.min(startY, endY); - maxX = math.max(startX, endX); - maxY = math.max(startY, endY); - - double extremaX; - double extremaY; - double a, b, c; - - // Check for simple case of strong ordering before calculating - // extrema - if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || - ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { - // The extrema point is dx/dt B(t) = 0 - // The derivative of B(t) for cubic bezier is a quadratic equation - // with multiple roots - // B'(t) = a*t*t + b*t + c*t - a = -startX + (3 * (cpX1 - cpX2)) + endX; - b = 2 * (startX - (2 * cpX1) + cpX2); - c = -startX + cpX1; - - // Now find roots for quadratic equation with known coefficients - // a,b,c - // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a - num s = (b * b) - (4 * a * c); - // If s is negative, we have no real roots - if ((s >= 0.0) && (a.abs() > epsilon)) { - if (s == 0.0) { - // we have only 1 root - final num t = -b / (2 * a); - final num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - } else { - // we have 2 roots - s = math.sqrt(s); - num t = (-b - s) / (2 * a); - num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - // check 2nd root - t = (-b + s) / (2 * a); - tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - } - } - } - - // Now calc extremes for dy/dt = 0 just like above - if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || - ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { - // The extrema point is dy/dt B(t) = 0 - // The derivative of B(t) for cubic bezier is a quadratic equation - // with multiple roots - // B'(t) = a*t*t + b*t + c*t - a = -startY + (3 * (cpY1 - cpY2)) + endY; - b = 2 * (startY - (2 * cpY1) + cpY2); - c = -startY + cpY1; - - // Now find roots for quadratic equation with known coefficients - // a,b,c - // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a - num s = (b * b) - (4 * a * c); - // If s is negative, we have no real roots - if ((s >= 0.0) && (a.abs() > epsilon)) { - if (s == 0.0) { - // we have only 1 root - final num t = -b / (2 * a); - final num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaY = ((tprime * tprime * tprime) * startY) + - ((3 * tprime * tprime * t) * cpY1) + - ((3 * tprime * t * t) * cpY2) + - (t * t * t * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - } else { - // we have 2 roots - s = math.sqrt(s); - final num t = (-b - s) / (2 * a); - final num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaY = ((tprime * tprime * tprime) * startY) + - ((3 * tprime * tprime * t) * cpY1) + - ((3 * tprime * t * t) * cpY2) + - (t * t * t * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - // check 2nd root - final num t2 = (-b + s) / (2 * a); - final num tprime2 = 1.0 - t2; - if ((t2 >= 0.0) && (t2 <= 1.0)) { - extremaY = ((tprime2 * tprime2 * tprime2) * startY) + - ((3 * tprime2 * tprime2 * t2) * cpY1) + - ((3 * tprime2 * t2 * t2) * cpY2) + - (t2 * t2 * t2 * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - } - } - } - break; - case engine.PathCommandTypes.rect: - final engine.RectCommand cmd = op; - left = cmd.x; - double width = cmd.width; - if (cmd.width < 0) { - left -= width; - width = -width; - } - double top = cmd.y; - double height = cmd.height; - if (cmd.height < 0) { - top -= height; - height = -height; - } - curX = minX = left; - maxX = left + width; - curY = minY = top; - maxY = top + height; - break; - case engine.PathCommandTypes.rRect: - final engine.RRectCommand cmd = op; - final RRect rRect = cmd.rrect; - curX = minX = rRect.left; - maxX = rRect.left + rRect.width; - curY = minY = rRect.top; - maxY = rRect.top + rRect.height; - break; - case engine.PathCommandTypes.close: - default: - skipBounds = false; - break; - } - if (!skipBounds) { - if (!ltrbInitialized) { - left = minX; - right = maxX; - top = minY; - bottom = maxY; - ltrbInitialized = true; - } else { - left = math.min(left, minX); - right = math.max(right, maxX); - top = math.min(top, minY); - bottom = math.max(bottom, maxY); - } - } - } - } - return ltrbInitialized - ? Rect.fromLTRB(left, top, right, bottom) - : Rect.zero; - } + Rect getBounds(); /// Combines the two paths according to the manner specified by the given /// `operation`. @@ -1028,83 +284,5 @@ class Path { /// /// If `forceClosed` is set to true, the contours of the path will be measured /// as if they had been closed, even if they were not explicitly closed. - PathMetrics computeMetrics({bool forceClosed = false}) { - return PathMetrics._(this, forceClosed); - } - - /// Detects if path is rounded rectangle and returns rounded rectangle or - /// null. - /// - /// Used for web optimization of physical shape represented as - /// a persistent div. - RRect get webOnlyPathAsRoundedRect { - if (subpaths.length != 1) { - return null; - } - final engine.Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final engine.PathCommand command = subPath.commands[0]; - return (command is engine.RRectCommand) ? command.rrect : null; - } - - /// Detects if path is simple rectangle and returns rectangle or null. - /// - /// Used for web optimization of physical shape represented as - /// a persistent div. - Rect get webOnlyPathAsRect { - if (subpaths.length != 1) { - return null; - } - final engine.Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final engine.PathCommand command = subPath.commands[0]; - return (command is engine.RectCommand) - ? Rect.fromLTWH(command.x, command.y, command.width, command.height) - : null; - } - - /// Detects if path is simple oval and returns [engine.Ellipse] or null. - /// - /// Used for web optimization of physical shape represented as - /// a persistent div. - engine.Ellipse get webOnlyPathAsCircle { - if (subpaths.length != 1) { - return null; - } - final engine.Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final engine.PathCommand command = subPath.commands[0]; - if (command is engine.Ellipse) { - final engine.Ellipse ellipse = command; - if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { - return ellipse; - } - } - return null; - } - - /// Serializes this path to a value that's sent to a CSS custom painter for - /// painting. - List webOnlySerializeToCssPaint() { - final List serializedSubpaths = []; - for (int i = 0; i < subpaths.length; i++) { - serializedSubpaths.add(subpaths[i].serializeToCssPaint()); - } - return serializedSubpaths; - } - - @override - String toString() { - if (engine.assertionsEnabled) { - return 'Path(${subpaths.join(', ')})'; - } else { - return super.toString(); - } - } + PathMetrics computeMetrics({bool forceClosed = false}); } diff --git a/lib/web_ui/lib/src/ui/path_metrics.dart b/lib/web_ui/lib/src/ui/path_metrics.dart index ffb99dff0a5a5..d7e57c37fe39d 100644 --- a/lib/web_ui/lib/src/ui/path_metrics.dart +++ b/lib/web_ui/lib/src/ui/path_metrics.dart @@ -18,47 +18,20 @@ part of ui; /// /// When iterating across a [PathMetrics]' contours, the [PathMetric] objects /// are only valid until the next one is obtained. -class PathMetrics extends collection.IterableBase { - PathMetrics._(Path path, bool forceClosed) - : _iterator = PathMetricIterator._(PathMetric._(path, forceClosed)); - - final Iterator _iterator; - +abstract class PathMetrics extends collection.IterableBase { @override - Iterator get iterator => _iterator; + Iterator get iterator; } /// Tracks iteration from one segment of a path to the next for measurement. -class PathMetricIterator implements Iterator { - PathMetricIterator._(this._pathMetric); - - PathMetric _pathMetric; - bool _firstTime = true; - +abstract class PathMetricIterator implements Iterator { @override - PathMetric get current => - _firstTime ? null : _pathMetric._segments.isEmpty ? null : _pathMetric; + PathMetric get current; @override - bool moveNext() { - // PathMetric isn't a normal iterable - it's already initialized to its - // first Path. Should only call _moveNext when done with the first one. - if (_firstTime == true) { - _firstTime = false; - return _pathMetric._segments.isNotEmpty; - } else if (_pathMetric?._moveNext() == true) { - return true; - } - _pathMetric = null; - return false; - } + bool moveNext(); } -// Maximum range value used in curve subdivision using Casteljau algorithm. -const int _kMaxTValue = 0x3FFFFFFF; -// Distance at which we stop subdividing cubic and quadratic curves. -const double _fTolerance = 0.5; - /// Utilities for measuring a [Path] and extracting subpaths. /// /// Iterate over the object returned by [Path.computeMetrics] to obtain @@ -71,25 +44,23 @@ const double _fTolerance = 0.5; /// Implementation is based on /// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp /// to maintain consistency with native platforms. -class PathMetric { - final Path _path; - final bool _forceClosed; - - // If the contour ends with a call to [Path.close] (which may - // have been implied when using [Path.addRect]) - bool _isClosed; - // Iterator index into [Path.subPaths] - int _subPathIndex = 0; - List<_PathSegment> _segments; - double _contourLength; - - /// Create a new empty [Path] object. - PathMetric._(this._path, this._forceClosed) { - _buildSegments(); - } - +abstract class PathMetric { /// Return the total length of the current contour. - double get length => _contourLength; + double get length; + + /// The zero-based index of the contour. + /// + /// [Path] objects are made up of zero or more contours. The first contour is + /// created once a drawing command (e.g. [Path.lineTo]) is issued. A + /// [Path.moveTo] command after a drawing command may create a new contour, + /// although it may not if optimizations are applied that determine the move + /// command did not actually result in moving the pen. + /// + /// This property is only valid with reference to its original iterator and + /// the contours of the path at the time the path's metrics were computed. If + /// additional contours were added or existing contours updated, this metric + /// will be invalid for the current state of the path. + int get contourIndex; /// Computes the position of hte current contour at the given offset, and the /// angle of the path at that point. @@ -101,26 +72,14 @@ class PathMetric { /// Returns null if the contour has zero [length]. /// /// The distance is clamped to the [length] of the current contour. - Tangent getTangentForOffset(double distance) { - final Float32List posTan = _getPosTan(distance); - // first entry == 0 indicates that Skia returned false - if (posTan[0] == 0.0) { - return null; - } else { - return Tangent( - Offset(posTan[1], posTan[2]), Offset(posTan[3], posTan[4])); - } - } - - Float32List _getPosTan(double distance) => throw UnimplementedError(); + Tangent getTangentForOffset(double distance); /// Given a start and stop distance, return the intervening segment(s). /// /// `start` and `end` are pinned to legal values (0..[length]) /// Returns null if the segment is 0 length or `start` > `stop`. /// Begin the segment with a moveTo if `startWithMoveTo` is true. - Path extractPath(double start, double end, {bool startWithMoveTo = true}) => - throw UnimplementedError(); + Path extractPath(double start, double end, {bool startWithMoveTo = true}); /// Whether the contour is closed. /// @@ -128,402 +87,7 @@ class PathMetric { /// have been implied when using [Path.addRect]) or if `forceClosed` was /// specified as true in the call to [Path.computeMetrics]. Returns false /// otherwise. - bool get isClosed { - return _isClosed; - } - - // Move to the next contour in the path. - // - // A path can have a next contour if [Path.moveTo] was called after drawing - // began. Return true if one exists, or false. - // - // This is not exactly congruent with a regular [Iterator.moveNext]. - // Typically, [Iterator.moveNext] should be called before accessing the - // [Iterator.current]. In this case, the [PathMetric] is valid before - // calling `_moveNext` - `_moveNext` should be called after the first - // iteration is done instead of before. - bool _moveNext() { - if (_subPathIndex == (_path.subpaths.length - 1)) { - return false; - } - ++_subPathIndex; - _buildSegments(); - return true; - } - - void _buildSegments() { - _segments = <_PathSegment>[]; - _isClosed = false; - double distance = 0.0; - int pointIndex = -1; - bool haveSeenMoveTo = false; - bool haveSeenClose = false; - - if (_path.subpaths.isEmpty) { - _contourLength = 0; - return; - } - final engine.Subpath subpath = _path.subpaths[_subPathIndex]; - final List commands = subpath.commands; - double currentX = 0.0, currentY = 0.0; - final Function lineToHandler = (double x, double y) { - final double dx = currentX - x; - final double dy = currentY - y; - final double prevDistance = distance; - distance += math.sqrt(dx * dx + dy * dy); - // As we accumulate distance, we have to check that the result of += - // actually made it larger, since a very small delta might be > 0, but - // still have no effect on distance (if distance >>> delta). - if (distance > prevDistance) { - _segments.add(_PathSegment(engine.PathCommandTypes.lineTo, distance, - [currentX, currentY, x, y])); - } - currentX = x; - currentY = y; - }; - _EllipseSegmentResult ellipseResult; - for (engine.PathCommand command in commands) { - switch (command.type) { - case engine.PathCommandTypes.moveTo: - final engine.MoveTo moveTo = command; - currentX = moveTo.x; - currentY = moveTo.y; - haveSeenMoveTo = true; - break; - case engine.PathCommandTypes.lineTo: - assert(haveSeenMoveTo); - final engine.LineTo lineTo = command; - lineToHandler(lineTo.x, lineTo.y); - break; - case engine.PathCommandTypes.bezierCurveTo: - assert(haveSeenMoveTo); - final engine.BezierCurveTo curve = command; - // Compute cubic curve distance. - distance = _computeCubicSegments( - currentX, - currentY, - curve.x1, - curve.y1, - curve.x2, - curve.y2, - curve.x3, - curve.y3, - distance, - 0, - _kMaxTValue, - _segments); - break; - case engine.PathCommandTypes.quadraticCurveTo: - assert(haveSeenMoveTo); - final engine.QuadraticCurveTo quadraticCurveTo = command; - // Compute quad curve distance. - distance = _computeQuadSegments( - currentX, - currentY, - quadraticCurveTo.x1, - quadraticCurveTo.y1, - quadraticCurveTo.x2, - quadraticCurveTo.y2, - distance, - 0, - _kMaxTValue); - break; - case engine.PathCommandTypes.close: - haveSeenClose = true; - break; - case engine.PathCommandTypes.ellipse: - final engine.Ellipse ellipse = command; - ellipseResult ??= _EllipseSegmentResult(); - _computeEllipseSegments( - currentX, - currentY, - distance, - ellipse.x, - ellipse.y, - ellipse.startAngle, - ellipse.endAngle, - ellipse.rotation, - ellipse.radiusX, - ellipse.radiusY, - ellipse.anticlockwise, - ellipseResult, - _segments); - distance = ellipseResult.distance; - currentX = ellipseResult.endPointX; - currentY = ellipseResult.endPointY; - _isClosed = true; - break; - case engine.PathCommandTypes.rRect: - final engine.RRectCommand rrectCommand = command; - final RRect rrect = rrectCommand.rrect; - engine.RRectMetricsRenderer(moveToCallback: (double x, double y) { - currentX = x; - currentY = y; - _isClosed = true; - haveSeenMoveTo = true; - }, lineToCallback: (double x, double y) { - lineToHandler(x, y); - }, ellipseCallback: (double centerX, - double centerY, - double radiusX, - double radiusY, - double rotation, - double startAngle, - double endAngle, - bool antiClockwise) { - ellipseResult ??= _EllipseSegmentResult(); - _computeEllipseSegments( - currentX, - currentY, - distance, - centerX, - centerY, - startAngle, - endAngle, - rotation, - radiusX, - radiusY, - antiClockwise, - ellipseResult, - _segments); - distance = ellipseResult.distance; - currentX = ellipseResult.endPointX; - currentY = ellipseResult.endPointY; - }).render(rrect); - _isClosed = true; - break; - case engine.PathCommandTypes.rect: - final engine.RectCommand rectCommand = command; - final double x = rectCommand.x; - final double y = rectCommand.y; - final double width = rectCommand.width; - final double height = rectCommand.height; - currentX = x; - currentY = y; - lineToHandler(x + width, y); - lineToHandler(x + width, y + height); - lineToHandler(x, y + height); - lineToHandler(x, y); - _isClosed = true; - break; - default: - throw UnimplementedError('Unknown path command $command'); - } - } - if (!_isClosed && _forceClosed && _segments.isNotEmpty) { - _PathSegment firstSegment = _segments.first; - lineToHandler(firstSegment.points[0], firstSegment.points[1]); - } - _contourLength = distance; - } - - static bool _tspanBigEnough(int tSpan) => (tSpan >> 10) != 0; - - static bool _cubicTooCurvy(double x0, double y0, double x1, double y1, - double x2, double y2, double x3, double y3) { - // Measure distance from start-end line at 1/3 and 2/3rds to control - // points. If distance is less than _fTolerance we should continue - // subdividing curve. Uses approx distance for speed. - // - // p1 = point 1/3rd between start,end points. - final double p1x = (x0 * 2 / 3) + (x3 / 3); - final double p1y = (y0 * 2 / 3) + (y3 / 3); - if ((p1x - x1).abs() > _fTolerance) { - return true; - } - if ((p1y - y1).abs() > _fTolerance) { - return true; - } - // p2 = point 2/3rd between start,end points. - final double p2x = (x0 / 3) + (x3 * 2 / 3); - final double p2y = (y0 / 3) + (y3 * 2 / 3); - if ((p2x - x2).abs() > _fTolerance) { - return true; - } - if ((p2y - y2).abs() > _fTolerance) { - return true; - } - return false; - } - - // Recursively subdivides cubic and adds segments. - static double _computeCubicSegments( - double x0, - double y0, - double x1, - double y1, - double x2, - double y2, - double x3, - double y3, - double distance, - int tMin, - int tMax, - List<_PathSegment> segments) { - if (_tspanBigEnough(tMax - tMin) && - _cubicTooCurvy(x0, y0, x1, y1, x2, y2, x3, y3)) { - // Chop cubic into two halves (De Cateljau's algorithm) - // See https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm - final double abX = (x0 + x1) / 2; - final double abY = (y0 + y1) / 2; - final double bcX = (x1 + x2) / 2; - final double bcY = (y1 + y2) / 2; - final double cdX = (x2 + x3) / 2; - final double cdY = (y2 + y3) / 2; - final double abcX = (abX + bcX) / 2; - final double abcY = (abY + bcY) / 2; - final double bcdX = (bcX + cdX) / 2; - final double bcdY = (bcY + cdY) / 2; - final double abcdX = (abcX + bcdX) / 2; - final double abcdY = (abcY + bcdY) / 2; - final int tHalf = (tMin + tMax) >> 1; - distance = _computeCubicSegments( - x0, y0, abX, abY, abcX, abcY, abcdX, abcdY, distance, tMin, tHalf, segments); - distance = _computeCubicSegments( - abcdX, abcdY, bcdX, bcdY, cdX, cdY, x3, y3, distance, tHalf, tMax, segments); - } else { - final double dx = x0 - x3; - final double dy = y0 - y3; - final double startToEndDistance = math.sqrt(dx * dx + dy * dy); - final double prevDistance = distance; - distance += startToEndDistance; - if (distance > prevDistance) { - segments.add(_PathSegment(engine.PathCommandTypes.bezierCurveTo, - distance, [x0, y0, x1, y1, x2, y2, x3, y3])); - } - } - return distance; - } - - static bool _quadTooCurvy( - double x0, double y0, double x1, double y1, double x2, double y2) { - // (a/4 + b/2 + c/4) - (a/2 + c/2) = -a/4 + b/2 - c/4 - final double dx = (x1 / 2) - (x0 + x2) / 4; - if (dx.abs() > _fTolerance) { - return true; - } - final double dy = (y1 / 2) - (y0 + y2) / 4; - if (dy.abs() > _fTolerance) { - return true; - } - return false; - } - - double _computeQuadSegments(double x0, double y0, double x1, double y1, - double x2, double y2, double distance, int tMin, int tMax) { - if (_tspanBigEnough(tMax - tMin) && _quadTooCurvy(x0, y0, x1, y1, x2, y2)) { - final double p01x = (x0 + x1) / 2; - final double p01y = (y0 + y1) / 2; - final double p12x = (x1 + x2) / 2; - final double p12y = (y1 + y2) / 2; - final double p012x = (p01x + p12x) / 2; - final double p012y = (p01y + p12y) / 2; - final int tHalf = (tMin + tMax) >> 1; - distance = _computeQuadSegments( - x0, y0, p01x, p01y, p012x, p012y, distance, tMin, tHalf); - distance = _computeQuadSegments( - p012x, p012y, p12x, p12y, x2, y2, distance, tMin, tHalf); - } else { - final double dx = x0 - x2; - final double dy = y0 - y2; - final double startToEndDistance = math.sqrt(dx * dx + dy * dy); - final double prevDistance = distance; - distance += startToEndDistance; - if (distance > prevDistance) { - _segments.add(_PathSegment(engine.PathCommandTypes.quadraticCurveTo, - distance, [x0, y0, x1, y1, x2, y2])); - } - } - return distance; - } - - // Create segments by converting arc to cubics. - // See http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter. - static void _computeEllipseSegments( - double startX, - double startY, - double distance, - double cx, - double cy, - double startAngle, - double endAngle, - double rotation, - double radiusX, - double radiusY, - bool anticlockwise, - _EllipseSegmentResult result, - List<_PathSegment> segments) { - final double endX = cx + (radiusX * math.cos(endAngle)); - final double endY = cy + (radiusY * math.sin(endAngle)); - result.endPointX = endX; - result.endPointY = endY; - // Check for http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters - // Treat as line segment from start to end if arc has zero radii. - // If start and end point are the same treat as zero length path. - if ((radiusX == 0 || radiusY == 0) || (startX == endX && startY == endY)) { - result.distance = distance; - return; - } - final double rxAbs = radiusX.abs(); - final double ryAbs = radiusY.abs(); - - final double theta1 = startAngle; - final double theta2 = endAngle; - final double thetaArc = theta2 - theta1; - - // Add 0.01f to make sure we have enough segments when thetaArc is close - // to pi/2. - final int numSegments = (thetaArc / ((math.pi / 2.0) + 0.01)).abs().ceil(); - double x0 = startX; - double y0 = startY; - for (int segmentIndex = 0; segmentIndex < numSegments; segmentIndex++) { - final double startTheta = - theta1 + (segmentIndex * thetaArc / numSegments); - final double endTheta = - theta1 + ((segmentIndex + 1) * thetaArc / numSegments); - final double t = (4.0 / 3.0) * math.tan((endTheta - startTheta) / 4); - if (!t.isFinite) { - result.distance = distance; - return; - } - final double sinStartTheta = math.sin(startTheta); - final double cosStartTheta = math.cos(startTheta); - final double sinEndTheta = math.sin(endTheta); - final double cosEndTheta = math.cos(endTheta); - - // Compute cubic segment start, control point and end (target). - final double p1x = rxAbs * (cosStartTheta - t * sinStartTheta) + cx; - final double p1y = ryAbs * (sinStartTheta + t * cosStartTheta) + cy; - final double targetPointX = rxAbs * cosEndTheta + cx; - final double targetPointY = ryAbs * sinEndTheta + cy; - final double p2x = targetPointX + rxAbs * (t * sinEndTheta); - final double p2y = targetPointY + ryAbs * (-t * cosEndTheta); - - distance = _computeCubicSegments(x0, y0, p1x, p1y, p2x, p2y, targetPointX, - targetPointY, distance, 0, _kMaxTValue, segments); - x0 = targetPointX; - y0 = targetPointY; - } - result.distance = distance; - } - - @override - String toString() => 'PathMetric'; -} - -class _EllipseSegmentResult { - double endPointX; - double endPointY; - double distance; - _EllipseSegmentResult(); -} - -class _PathSegment { - _PathSegment(this.segmentType, this.distance, this.points); - - final int segmentType; - final double distance; - final List points; + bool get isClosed; } /// The geometric description of a tangent: the angle at a point. diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart index 402a93e71f570..ed8c7ac5beb04 100644 --- a/lib/web_ui/test/engine/recording_canvas_test.dart +++ b/lib/web_ui/test/engine/recording_canvas_test.dart @@ -20,7 +20,7 @@ void main() { group('drawDRRect', () { final RRect rrect = RRect.fromLTRBR(10, 10, 50, 50, Radius.circular(3)); - final Paint somePaint = Paint()..color = const Color(0xFFFF0000); + final SurfacePaint somePaint = SurfacePaint()..color = const Color(0xFFFF0000); test('Happy case', () { underTest.drawDRRect(rrect, rrect.deflate(1), somePaint); @@ -29,7 +29,7 @@ void main() { _expectDrawCall(mockCanvas, { 'outer': rrect, 'inner': rrect.deflate(1), - 'paint': somePaint.webOnlyPaintData, + 'paint': somePaint.paintData, }); }); @@ -71,7 +71,7 @@ void main() { _expectDrawCall(mockCanvas, { 'outer': outer, 'inner': inner, - 'paint': somePaint.webOnlyPaintData, + 'paint': somePaint.paintData, }); }); @@ -85,7 +85,7 @@ void main() { _expectDrawCall(mockCanvas, { 'outer': outer, 'inner': inner, - 'paint': somePaint.webOnlyPaintData, + 'paint': somePaint.paintData, }); }); }); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart index 64d83a694d1e6..29ab14b028a8f 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_arc_golden_test.dart @@ -57,7 +57,7 @@ void paintArc(BitmapCanvas canvas, Offset offset, Offset(75.0 + distance + offset.dx, 75.0 + distance + offset.dy); canvas.drawRect( Rect.fromLTRB(startP.dx, startP.dy, endP.dx, endP.dy), - PaintData() + SurfacePaintData() ..strokeWidth = 1 ..color = Color(0xFFFF9800) // orange ..style = PaintingStyle.stroke); @@ -70,7 +70,7 @@ void paintArc(BitmapCanvas canvas, Offset offset, clockwise: clockwise); canvas.drawPath( path, - PaintData() + SurfacePaintData() ..strokeWidth = 2 ..color = Color(0x61000000) // black38 ..style = PaintingStyle.stroke); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart index 4c7986e2bbe71..09d08e4b50d51 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart @@ -32,13 +32,13 @@ void main() async { /// be seen depending on pixel alignment and whether antialiasing happens /// before or after rasterization. void drawMisalignedLines(BitmapCanvas canvas) { - final PaintData linePaint = (Paint() + final SurfacePaintData linePaint = (SurfacePaint() ..style = PaintingStyle.stroke ..strokeWidth = 1) - .webOnlyPaintData; + .paintData; - final PaintData fillPaint = - (Paint()..style = PaintingStyle.fill).webOnlyPaintData; + final SurfacePaintData fillPaint = + (SurfacePaint()..style = PaintingStyle.fill).paintData; canvas.drawRect( const Rect.fromLTWH(0, 0, 40, 40), @@ -101,7 +101,7 @@ void main() async { canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50)); canvas.translate(25, 25); - canvas.drawPaint(PaintData() + canvas.drawPaint(SurfacePaintData() ..color = const Color.fromRGBO(0, 255, 0, 1.0) ..style = PaintingStyle.fill); diff --git a/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart index 6890d38654d55..5ba106129c44b 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_lines_golden_test.dart @@ -34,15 +34,15 @@ void main() async { } void paintLines(BitmapCanvas canvas) { - final PaintData paint1 = PaintData() + final SurfacePaintData paint1 = SurfacePaintData() ..color = Color(0xFF9E9E9E) // Colors.grey ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; - final PaintData paint2 = PaintData() + final SurfacePaintData paint2 = SurfacePaintData() ..color = Color(0x7fff0000) ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; - final PaintData paint3 = PaintData() + final SurfacePaintData paint3 = SurfacePaintData() ..color = Color(0xFF4CAF50) //Colors.green ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart index b226952f3c3a8..12f6c36e58268 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_rect_golden_test.dart @@ -36,27 +36,27 @@ void main() async { void paintRects(BitmapCanvas canvas) { canvas.drawRect(Rect.fromLTRB(30, 40, 100, 50), - PaintData() + SurfacePaintData() ..color = Color(0xFF4CAF50) //Colors.green ..strokeWidth = 1.0 ..style = PaintingStyle.stroke); // swap left and right. canvas.drawRect(Rect.fromLTRB(100, 150, 30, 140), - PaintData() + SurfacePaintData() ..color = Color(0xFFF44336) //Colors.red ..strokeWidth = 1.0 ..style = PaintingStyle.stroke); // Repeat above for fill canvas.drawRect(Rect.fromLTRB(30, 240, 100, 250), - PaintData() + SurfacePaintData() ..color = Color(0xFF4CAF50) //Colors.green ..style = PaintingStyle.fill); // swap left and right. canvas.drawRect(Rect.fromLTRB(100, 350, 30, 340), - PaintData() + SurfacePaintData() ..color = Color(0xFFF44336) //Colors.red ..style = PaintingStyle.fill); } diff --git a/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart index 78e2ddd50808d..d382c8d66f031 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_rrect_golden_test.dart @@ -15,7 +15,7 @@ void main() async { BitmapCanvas canvas; - final PaintData niceRRectPaint = PaintData() + final SurfacePaintData niceRRectPaint = SurfacePaintData() ..color = const Color.fromRGBO(250, 186, 218, 1.0) // #fabada ..style = PaintingStyle.fill; diff --git a/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart index 4d06d6b6b141b..84042603e28e0 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_stroke_joins_golden_test.dart @@ -35,7 +35,7 @@ void main() async { void paintStrokeJoins(BitmapCanvas canvas) { canvas.drawRect(Rect.fromLTRB(0, 0, 300, 300), - PaintData() + SurfacePaintData() ..color = Color(0xFFFFFFFF) ..style = PaintingStyle.fill); // white @@ -55,7 +55,7 @@ void paintStrokeJoins(BitmapCanvas canvas) { path.moveTo(start.dx, start.dy); path.lineTo(mid.dx, mid.dy); path.lineTo(end.dx, end.dy); - canvas.drawPath(path, PaintData() + canvas.drawPath(path, SurfacePaintData() ..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = color diff --git a/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart index 9712e234e7df3..6f10ca0d0d893 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_stroke_rects_golden_test.dart @@ -36,16 +36,16 @@ void main() async { void paintSideBySideRects(BitmapCanvas canvas) { canvas.drawRect(Rect.fromLTRB(0, 0, 300, 300), - PaintData() + SurfacePaintData() ..color = Color(0xFFFFFFFF) ..style = PaintingStyle.fill); // white canvas.drawRect(Rect.fromLTRB(0, 20, 40, 60), - PaintData() + SurfacePaintData() ..style = PaintingStyle.fill ..color = Color(0x7f0000ff)); canvas.drawRect(Rect.fromLTRB(40, 20, 80, 60), - PaintData() + SurfacePaintData() ..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Color(0x7fff0000)); @@ -54,11 +54,11 @@ void paintSideBySideRects(BitmapCanvas canvas) { canvas.transform(new Matrix4.rotationZ(30.0 * math.pi / 180.0).storage); canvas.drawRect(Rect.fromLTRB(100, 60, 140, 100), - PaintData() + SurfacePaintData() ..style = PaintingStyle.fill ..color = Color(0x7fff00ff)); canvas.drawRect(Rect.fromLTRB(140, 60, 180, 100), - PaintData() + SurfacePaintData() ..style = PaintingStyle.stroke ..strokeWidth = 4 ..color = Color(0x7fffff00)); diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index b529acadc6078..fd0f2c52e9d35 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -30,7 +30,7 @@ void main() async { ..save() ..drawRect( rc.computePaintBounds(), - PaintData() + SurfacePaintData() ..color = const Color.fromRGBO(0, 0, 255, 1.0) ..style = PaintingStyle.stroke ..strokeWidth = 1.0, diff --git a/lib/web_ui/test/mock_engine_canvas.dart b/lib/web_ui/test/mock_engine_canvas.dart index d5e8fade6d275..827d4e519d201 100644 --- a/lib/web_ui/test/mock_engine_canvas.dart +++ b/lib/web_ui/test/mock_engine_canvas.dart @@ -121,7 +121,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawLine(Offset p1, Offset p2, PaintData paint) { + void drawLine(Offset p1, Offset p2, SurfacePaintData paint) { _called('drawLine', arguments: { 'p1': p1, 'p2': p2, @@ -130,17 +130,17 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawPaint(PaintData paint) { + void drawPaint(SurfacePaintData paint) { _called('drawPaint', arguments: paint); } @override - void drawRect(Rect rect, PaintData paint) { + void drawRect(Rect rect, SurfacePaintData paint) { _called('drawRect', arguments: paint); } @override - void drawRRect(RRect rrect, PaintData paint) { + void drawRRect(RRect rrect, SurfacePaintData paint) { _called('drawRRect', arguments: { 'rrect': rrect, 'paint': paint, @@ -148,7 +148,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawDRRect(RRect outer, RRect inner, PaintData paint) { + void drawDRRect(RRect outer, RRect inner, SurfacePaintData paint) { _called('drawDRRect', arguments: { 'outer': outer, 'inner': inner, @@ -157,7 +157,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawOval(Rect rect, PaintData paint) { + void drawOval(Rect rect, SurfacePaintData paint) { _called('drawOval', arguments: { 'rect': rect, 'paint': paint, @@ -165,7 +165,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawCircle(Offset c, double radius, PaintData paint) { + void drawCircle(Offset c, double radius, SurfacePaintData paint) { _called('drawCircle', arguments: { 'c': c, 'radius': radius, @@ -174,7 +174,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawPath(Path path, PaintData paint) { + void drawPath(Path path, SurfacePaintData paint) { _called('drawPath', arguments: { 'path': path, 'paint': paint, @@ -193,7 +193,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawImage(Image image, Offset p, PaintData paint) { + void drawImage(Image image, Offset p, SurfacePaintData paint) { _called('drawImage', arguments: { 'image': image, 'p': p, @@ -202,7 +202,7 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawImageRect(Image image, Rect src, Rect dst, PaintData paint) { + void drawImageRect(Image image, Rect src, Rect dst, SurfacePaintData paint) { _called('drawImageRect', arguments: { 'image': image, 'src': src, @@ -221,7 +221,7 @@ class MockEngineCanvas implements EngineCanvas { @override void drawVertices(Vertices vertices, BlendMode blendMode, - PaintData paint) { + SurfacePaintData paint) { _called('drawVertices', arguments: { 'vertices': vertices, 'blendMode': blendMode, diff --git a/lib/web_ui/test/path_test.dart b/lib/web_ui/test/path_test.dart index 1deadc0663991..7b4a062752538 100644 --- a/lib/web_ui/test/path_test.dart +++ b/lib/web_ui/test/path_test.dart @@ -4,17 +4,18 @@ import 'package:test/test.dart'; import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; import 'matchers.dart'; void main() { test('Should have no subpaths when created', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); expect(path.subpaths.length, 0); }); test('LineTo should add command', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.moveTo(5.0, 10.0); path.lineTo(20.0, 40.0); path.lineTo(30.0, 50.0); @@ -24,7 +25,7 @@ void main() { }); test('LineTo should add moveTo 0,0 when first call to Path API', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.lineTo(20.0, 40.0); expect(path.subpaths.length, 1); expect(path.subpaths[0].currentX, 20.0); @@ -32,7 +33,7 @@ void main() { }); test('relativeLineTo should increments currentX', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.moveTo(5.0, 10.0); path.lineTo(20.0, 40.0); path.relativeLineTo(5.0, 5.0); @@ -42,7 +43,7 @@ void main() { }); test('Should allow calling relativeLineTo before moveTo', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.relativeLineTo(5.0, 5.0); path.moveTo(5.0, 10.0); expect(path.subpaths.length, 2); @@ -53,7 +54,7 @@ void main() { }); test('Should allow relativeLineTo after reset', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); final Path subPath = Path(); subPath.moveTo(50.0, 60.0); subPath.lineTo(200.0, 200.0); @@ -66,25 +67,25 @@ void main() { }); test('Should detect rectangular path', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); expect(path.webOnlyPathAsRect, const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); }); test('Should detect non rectangular path if empty', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); expect(path.webOnlyPathAsRect, null); }); test('Should detect non rectangular path if there are multiple subpaths', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); path.addRect(const Rect.fromLTWH(5.0, 6.0, 7.0, 8.0)); expect(path.webOnlyPathAsRect, null); }); test('Should detect rounded rectangular path', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.addRRect(RRect.fromRectAndRadius( const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); expect( @@ -94,12 +95,12 @@ void main() { }); test('Should detect non rounded rectangular path if empty', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); expect(path.webOnlyPathAsRoundedRect, null); }); test('Should detect rectangular path is not round', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); expect(path.webOnlyPathAsRoundedRect, null); }); @@ -107,7 +108,7 @@ void main() { test( 'Should detect non rounded rectangular path if there are ' 'multiple subpaths', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.addRRect(RRect.fromRectAndRadius( const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); path.addRRect(RRect.fromRectAndRadius( @@ -119,25 +120,25 @@ void main() { final Path emptyPath = Path(); expect(emptyPath.getBounds(), Rect.zero); - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.moveTo(50, 60); expect(path.getBounds(), const Rect.fromLTRB(50, 60, 50, 60)); }); test('Should compute bounds for lines', () { - final Path path = Path(); + final SurfacePath path = SurfacePath(); path.moveTo(25, 30); path.lineTo(100, 200); expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); - final Path path2 = Path(); + final SurfacePath path2 = SurfacePath(); path2.moveTo(250, 300); path2.lineTo(50, 60); expect(path2.getBounds(), const Rect.fromLTRB(50, 60, 250, 300)); }); test('Should compute bounds for quadraticBezierTo', () { - final Path path1 = Path(); + final SurfacePath path1 = SurfacePath(); path1.moveTo(285.2, 682.1); path1.quadraticBezierTo(432.0, 431.4, 594.9, 681.2); expect( @@ -147,7 +148,7 @@ void main() { from: const Rect.fromLTRB(285.2, 556.5, 594.9, 682.1))); // Control point below start , end. - final Path path2 = Path(); + final SurfacePath path2 = SurfacePath(); path2.moveTo(285.2, 682.1); path2.quadraticBezierTo(447.4, 946.8, 594.9, 681.2); expect( @@ -157,7 +158,7 @@ void main() { from: const Rect.fromLTRB(285.2, 681.2, 594.9, 814.2))); // Control point to the right of end point. - final Path path3 = Path(); + final SurfacePath path3 = SurfacePath(); path3.moveTo(468.3, 685.6); path3.quadraticBezierTo(644.7, 555.2, 594.9, 681.2); expect( @@ -168,7 +169,7 @@ void main() { }); test('Should compute bounds for cubicTo', () { - final Path path1 = Path(); + final SurfacePath path1 = SurfacePath(); path1.moveTo(220, 300); path1.cubicTo(230, 120, 400, 125, 410, 280); expect( @@ -178,7 +179,7 @@ void main() { from: const Rect.fromLTRB(220.0, 164.3, 410.0, 300.0))); // control point 1 to the right of control point 2 - final Path path2 = Path(); + final SurfacePath path2 = SurfacePath(); path2.moveTo(220, 300); path2.cubicTo(564.2, 13.7, 400.0, 125.0, 410.0, 280.0); expect( @@ -188,7 +189,7 @@ void main() { from: const Rect.fromLTRB(220.0, 122.8, 440.5, 300.0))); // control point 1 to the right of control point 2 inflection - final Path path3 = Path(); + final SurfacePath path3 = SurfacePath(); path3.moveTo(220, 300); path3.cubicTo(839.8, 67.9, 400.0, 125.0, 410.0, 280.0); expect( @@ -198,7 +199,7 @@ void main() { from: const Rect.fromLTRB(220.0, 144.5, 552.1, 300.0))); // control point 1 below and between start and end points - final Path path4 = Path(); + final SurfacePath path4 = SurfacePath(); path4.moveTo(220.0, 300.0); path4.cubicTo(354.8, 388.3, 400.0, 125.0, 410.0, 280.0); expect( @@ -208,7 +209,7 @@ void main() { from: const Rect.fromLTRB(220.0, 230.0, 410.0, 318.6))); // control points inverted below - final Path path5 = Path(); + final SurfacePath path5 = SurfacePath(); path5.moveTo(220.0, 300.0); path5.cubicTo(366.5, 487.3, 256.4, 489.9, 410.0, 280.0); expect( @@ -218,7 +219,7 @@ void main() { from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0))); // control points inverted below wide - final Path path6 = Path(); + final SurfacePath path6 = SurfacePath(); path6.moveTo(220.0, 300.0); path6.cubicTo(496.1, 485.5, 121.4, 491.6, 410.0, 280.0); expect( @@ -228,7 +229,7 @@ void main() { from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0))); // control point 2 and end point swapped - final Path path7 = Path(); + final SurfacePath path7 = SurfacePath(); path7.moveTo(220.0, 300.0); path7.cubicTo(230.0, 120.0, 394.5, 296.1, 382.3, 124.1); expect(