diff --git a/lib/web_ui/lib/src/engine/compositor/canvas.dart b/lib/web_ui/lib/src/engine/compositor/canvas.dart index 95fa4694e18b5..7233331d744b8 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvas.dart @@ -21,7 +21,7 @@ class CkCanvas { final CkPath skPath = path as CkPath; final js.JsObject? intersectClipOp = canvasKit['ClipOp']['Intersect']; skCanvas.callMethod('clipPath', [ - skPath._skPath, + skPath._legacyJsObject, intersectClipOp, doAntiAlias, ]); @@ -78,7 +78,7 @@ class CkCanvas { ) { final CkImage skAtlas = atlas as CkImage; skCanvas.callMethod('drawAtlas', [ - skAtlas.skImage, + skAtlas.legacyJsObject, rects, rstTransforms, paint.skiaObject, @@ -114,7 +114,7 @@ class CkCanvas { void drawImage(ui.Image image, ui.Offset offset, CkPaint paint) { final CkImage skImage = image as CkImage; skCanvas.callMethod('drawImage', [ - skImage.skImage, + skImage.legacyJsObject, offset.dx, offset.dy, paint.skiaObject, @@ -124,7 +124,7 @@ class CkCanvas { void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, CkPaint paint) { final CkImage skImage = image as CkImage; skCanvas.callMethod('drawImageRect', [ - skImage.skImage, + skImage.legacyJsObject, makeSkRect(src), makeSkRect(dst), paint.skiaObject, @@ -136,7 +136,7 @@ class CkCanvas { ui.Image image, ui.Rect center, ui.Rect dst, CkPaint paint) { final CkImage skImage = image as CkImage; skCanvas.callMethod('drawImageNine', [ - skImage.skImage, + skImage.legacyJsObject, makeSkRect(center), makeSkRect(dst), paint.skiaObject, @@ -176,7 +176,7 @@ class CkCanvas { void drawPath(ui.Path path, CkPaint paint) { final js.JsObject? skPaint = paint.skiaObject; final CkPath enginePath = path as CkPath; - final js.JsObject? skPath = enginePath._skPath; + final js.JsObject? skPath = enginePath._legacyJsObject; skCanvas.callMethod('drawPath', [skPath, skPaint]); } diff --git a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart index 91e6f03a2bcbf..3e4b59046b8a9 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart @@ -19,6 +19,9 @@ final js.JsObject _jsObjectWrapperLegacy = js.JsObject(js.context['Object']); @JS('window.flutter_js_object_wrapper') external JsObjectWrapper get _jsObjectWrapper; +@visibleForTesting +JsObjectWrapper get debugJsObjectWrapper => _jsObjectWrapper; + void initializeCanvasKitBindings(js.JsObject canvasKit) { // Because JsObject cannot be cast to a @JS type, we stash CanvasKit into // a global and use the [canvasKitJs] getter to access it. @@ -32,8 +35,14 @@ class JsObjectWrapper { external set skMaskFilter(SkMaskFilter? filter); external set skColorFilter(SkColorFilter? filter); external set skImageFilter(SkImageFilter? filter); + external set skPath(SkPath? path); + external set skImage(SkImage? image); } +/// Reads [JsObjectWrapper.skPath] as [SkPathArcToPointOverload]. +@JS('window.flutter_js_object_wrapper.skPath') +external SkPathArcToPointOverload get _skPathArcToPointOverload; + /// Specific methods that wrap `@JS`-backed objects into a [js.JsObject] /// for use with legacy `dart:js` API. extension JsObjectWrappers on JsObjectWrapper { @@ -64,6 +73,27 @@ extension JsObjectWrappers on JsObjectWrapper { _jsObjectWrapper.skImageFilter = null; return wrapped; } + + js.JsObject wrapSkPath(SkPath path) { + _jsObjectWrapper.skPath = path; + js.JsObject wrapped = _jsObjectWrapperLegacy['skPath']; + _jsObjectWrapper.skPath = null; + return wrapped; + } + + js.JsObject wrapSkImage(SkImage image) { + _jsObjectWrapper.skImage = image; + js.JsObject wrapped = _jsObjectWrapperLegacy['skImage']; + _jsObjectWrapper.skImage = null; + return wrapped; + } + + SkPathArcToPointOverload castToSkPathArcToPointOverload(SkPath path) { + _jsObjectWrapper.skPath = path; + final SkPathArcToPointOverload overload = _skPathArcToPointOverload; + _jsObjectWrapper.skPath = null; + return overload; + } } @JS('window.flutter_canvas_kit') @@ -78,11 +108,60 @@ class CanvasKit { external SkFilterQualityEnum get FilterQuality; external SkBlurStyleEnum get BlurStyle; external SkTileModeEnum get TileMode; + external SkFillTypeEnum get FillType; + external SkPathOpEnum get PathOp; external SkAnimatedImage MakeAnimatedImageFromEncoded(Uint8List imageData); external SkShaderNamespace get SkShader; external SkMaskFilter MakeBlurMaskFilter(SkBlurStyle blurStyle, double sigma, bool respectCTM); external SkColorFilterNamespace get SkColorFilter; external SkImageFilterNamespace get SkImageFilter; + external SkPath MakePathFromOp(SkPath path1, SkPath path2, SkPathOp pathOp); +} + +@JS() +class SkFillTypeEnum { + external SkFillType get Winding; + external SkFillType get EvenOdd; +} + +@JS() +class SkFillType { + external int get value; +} + +final List _skFillTypes = [ + canvasKitJs.FillType.Winding, + canvasKitJs.FillType.EvenOdd, +]; + +SkFillType toSkFillType(ui.PathFillType fillType) { + return _skFillTypes[fillType.index]; +} + +@JS() +class SkPathOpEnum { + external SkPathOp get Difference; + external SkPathOp get Intersect; + external SkPathOp get Union; + external SkPathOp get XOR; + external SkPathOp get ReverseDifference; +} + +@JS() +class SkPathOp { + external int get value; +} + +final List _skPathOps = [ + canvasKitJs.PathOp.Difference, + canvasKitJs.PathOp.Intersect, + canvasKitJs.PathOp.Union, + canvasKitJs.PathOp.XOR, + canvasKitJs.PathOp.ReverseDifference, +]; + +SkPathOp toSkPathOp(ui.PathOperation pathOp) { + return _skPathOps[pathOp.index]; } @JS() @@ -324,7 +403,7 @@ class SkShaderNamespace { external SkShader MakeLinearGradient( Float32List from, // 2-element array Float32List to, // 2-element array - List colors, + Uint32List colors, Float32List colorStops, SkTileMode tileMode, ); @@ -429,6 +508,20 @@ Float32List toSkMatrixFromFloat32(Float32List matrix4) { return skMatrix; } +/// Converts a 4x4 Flutter matrix (represented as a [Float32List]) to an +/// SkMatrix, which is a 3x3 transform matrix. +Float32List toSkMatrixFromFloat64(Float64List matrix4) { + final Float32List skMatrix = Float32List(9); + for (int i = 0; i < 9; ++i) { + final int matrix4Index = _skMatrixIndexToMatrix4Index[i]; + if (matrix4Index < matrix4.length) + skMatrix[i] = matrix4[matrix4Index]; + else + skMatrix[i] = 0.0; + } + return skMatrix; +} + /// Converts an [offset] into an `[x, y]` pair stored in a `Float32List`. /// /// The returned list can be passed to CanvasKit API that take points. @@ -474,10 +567,20 @@ external SkFloat32List _mallocFloat32List( /// Allocates a [Float32List] backed by WASM memory, managed by /// a [SkFloat32List]. +/// +/// To free the allocated array use [freeFloat32List]. SkFloat32List mallocFloat32List(int size) { return _mallocFloat32List(_nativeFloat32ArrayType, size); } +/// Frees the WASM memory occupied by a [SkFloat32List]. +/// +/// The [list] is no longer usable after calling this function. +/// +/// Use this function to free lists owned by the engine. +@JS('window.flutter_canvas_kit.Free') +external void freeFloat32List(SkFloat32List list); + /// Wraps a [Float32List] backed by WASM memory. /// /// This wrapper is necessary because the raw [Float32List] will get detached @@ -534,3 +637,225 @@ Float32List toSharedSkColor3(ui.Color color) { return _populateSkColor(_sharedSkColor3, color); } final SkFloat32List _sharedSkColor3 = mallocFloat32List(4); + +Uint32List toSkIntColorList(List colors) { + final int len = colors.length; + final Uint32List result = Uint32List(len); + for (int i = 0; i < len; i++) { + result[i] = colors[i].value; + } + return result; +} + +@JS('window.flutter_canvas_kit.SkPath') +class SkPath { + external SkPath([SkPath? other]); + external void setFillType(SkFillType fillType); + external void addArc( + SkRect oval, + double startAngleDegrees, + double sweepAngleDegrees, + ); + external void addOval( + SkRect oval, + bool counterClockWise, + int startIndex, + ); + external void addPath( + SkPath other, + double scaleX, + double skewX, + double transX, + double skewY, + double scaleY, + double transY, + double pers0, + double pers1, + double pers2, + bool extendPath, + ); + external void addPoly( + Float32List points, + bool close, + ); + external void addRoundRect( + SkRect outerRect, + Float32List radii, + bool counterClockWise, + ); + external void addRect( + SkRect rect, + ); + external void arcTo( + SkRect oval, + double startAngleDegrees, + double sweepAngleDegrees, + bool forceMoveTo, + ); + external void close(); + external void conicTo( + double x1, + double y1, + double x2, + double y2, + double w, + ); + external bool contains( + double x, + double y, + ); + external void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ); + external SkRect getBounds(); + external void lineTo(double x, double y); + external void moveTo(double x, double y); + external void quadTo( + double x1, + double y1, + double x2, + double y2, + ); + external void rArcTo( + double x, + double y, + double rotation, + bool useSmallArc, + bool counterClockWise, + double deltaX, + double deltaY, + ); + external void rConicTo( + double x1, + double y1, + double x2, + double y2, + double w, + ); + external void rCubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ); + external void rLineTo(double x, double y); + external void rMoveTo(double x, double y); + external void rQuadTo( + double x1, + double y1, + double x2, + double y2, + ); + external void reset(); + external String toSVGString(); + external bool isEmpty(); + external SkPath copy(); + external void transform( + double scaleX, + double skewX, + double transX, + double skewY, + double scaleY, + double transY, + double pers0, + double pers1, + double pers2, + ); +} + +/// A different view on [SkPath] used to overload [SkPath.arcTo]. +// TODO(yjbanov): this is a hack to get around https://github.com/flutter/flutter/issues/61305 +@JS() +class SkPathArcToPointOverload { + external void arcTo( + double radiusX, + double radiusY, + double rotation, + bool useSmallArc, + bool counterClockWise, + double x, + double y, + ); +} + +@JS('window.flutter_canvas_kit.SkContourMeasureIter') +class SkContourMeasureIter { + external SkContourMeasureIter(SkPath path, bool forceClosed, int startIndex); + external SkContourMeasure? next(); +} + +@JS() +class SkContourMeasure { + external SkPath getSegment(double start, double end, bool startWithMoveTo); + external Float32List getPosTan(double distance); + external bool isClosed(); + external double length(); +} + +@JS() +@anonymous +class SkRect { + external factory SkRect({ + required double fLeft, + required double fTop, + required double fRight, + required double fBottom, + }); + external double get fLeft; + external double get fTop; + external double get fRight; + external double get fBottom; +} + +extension SkRectExtensions on SkRect { + ui.Rect toRect() { + return ui.Rect.fromLTRB( + this.fLeft, + this.fTop, + this.fRight, + this.fBottom, + ); + } +} + +SkRect toSkRect(ui.Rect rect) { + return SkRect( + fLeft: rect.left, + fTop: rect.top, + fRight: rect.right, + fBottom: rect.bottom, + ); +} + +SkRect toOuterSkRect(ui.RRect rrect) { + return SkRect( + fLeft: rrect.left, + fTop: rrect.top, + fRight: rrect.right, + fBottom: rrect.bottom, + ); +} + +/// Encodes a list of offsets to CanvasKit-compatible point array. +/// +/// Uses `CanvasKit.Malloc` to allocate storage for the points in the WASM +/// memory to avoid unnecessary copying. Unless CanvasKit takes ownership of +/// the list the returned list must be explicitly freed using +/// [freeMallocedFloat32List]. +SkFloat32List toMallocedSkPoints(List points) { + final int len = points.length; + final SkFloat32List skPoints = mallocFloat32List(len * 2); + final Float32List list = skPoints.toTypedArray(); + for (int i = 0; i < len; i++) { + list[2 * i] = points[i].dx; + list[2 * i + 1] = points[i].dy; + } + return skPoints; +} diff --git a/lib/web_ui/lib/src/engine/compositor/image.dart b/lib/web_ui/lib/src/engine/compositor/image.dart index 55492aeceb363..2f66eefb7c15f 100644 --- a/lib/web_ui/lib/src/engine/compositor/image.dart +++ b/lib/web_ui/lib/src/engine/compositor/image.dart @@ -54,7 +54,8 @@ class CkAnimatedImage implements ui.Image { /// A [ui.Image] backed by an `SkImage` from Skia. class CkImage implements ui.Image { - SkImage skImage; + final SkImage skImage; + late final js.JsObject legacyJsObject = _jsObjectWrapper.wrapSkImage(skImage); CkImage(this.skImage); diff --git a/lib/web_ui/lib/src/engine/compositor/path.dart b/lib/web_ui/lib/src/engine/compositor/path.dart index b46ec502ce504..4cdc5b994b33b 100644 --- a/lib/web_ui/lib/src/engine/compositor/path.dart +++ b/lib/web_ui/lib/src/engine/compositor/path.dart @@ -2,66 +2,56 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - part of engine; /// An implementation of [ui.Path] which is backed by an `SkPath`. /// /// The `SkPath` is required for `CkCanvas` methods which take a path. class CkPath implements ui.Path { - js.JsObject? _skPath; + final SkPath _skPath; - /// Cached constructor function for `SkPath`, so we don't have to look it up - /// every time we construct a new path. - static final js.JsFunction? _skPathConstructor = canvasKit['SkPath']; + // TODO(yjbanov): remove this once we're fully @JS-ified. + late final js.JsObject _legacyJsObject = _jsObjectWrapper.wrapSkPath(_skPath); - CkPath() { - _skPath = js.JsObject(_skPathConstructor!); - fillType = ui.PathFillType.nonZero; + CkPath() : _skPath = SkPath(), _fillType = ui.PathFillType.nonZero { + _skPath.setFillType(toSkFillType(_fillType)); } - CkPath.from(CkPath other) { - _skPath = js.JsObject(_skPathConstructor!, [other._skPath]); - fillType = other.fillType; + CkPath.from(CkPath other) : _skPath = SkPath(other._skPath), _fillType = other.fillType { + _skPath.setFillType(toSkFillType(_fillType)); } - CkPath._fromSkPath(js.JsObject? skPath) : _skPath = skPath; + CkPath._fromSkPath(SkPath skPath, this._fillType) : _skPath = skPath { + _skPath.setFillType(toSkFillType(_fillType)); + } - late ui.PathFillType _fillType; + ui.PathFillType _fillType; @override ui.PathFillType get fillType => _fillType; @override set fillType(ui.PathFillType newFillType) { - _fillType = newFillType; - - js.JsObject? skFillType; - switch (newFillType) { - case ui.PathFillType.nonZero: - skFillType = canvasKit['FillType']['Winding']; - break; - case ui.PathFillType.evenOdd: - skFillType = canvasKit['FillType']['EvenOdd']; - break; + if (_fillType == newFillType) { + return; } - - _skPath!.callMethod('setFillType', [skFillType]); + _fillType = newFillType; + _skPath.setFillType(toSkFillType(newFillType)); } @override void addArc(ui.Rect oval, double startAngle, double sweepAngle) { const double toDegrees = 180.0 / math.pi; - _skPath!.callMethod('addArc', [ - makeSkRect(oval), + _skPath.addArc( + toSkRect(oval), startAngle * toDegrees, sweepAngle * toDegrees, - ]); + ); } @override void addOval(ui.Rect oval) { - _skPath!.callMethod('addOval', [makeSkRect(oval), false, 1]); + _skPath.addOval(toSkRect(oval), false, 1); } @override @@ -76,7 +66,7 @@ class CkPath implements ui.Path { skMatrix[5] += offset.dy; } final CkPath otherPath = path as CkPath; - _skPath!.callMethod('addPath', [ + _skPath.addPath( otherPath._skPath, skMatrix[0], skMatrix[1], @@ -88,49 +78,52 @@ class CkPath implements ui.Path { skMatrix[7], skMatrix[8], false, - ]); + ); } @override void addPolygon(List points, bool close) { assert(points != null); // ignore: unnecessary_null_comparison - // TODO(hterkelsen): https://github.com/flutter/flutter/issues/58824 - final List>? encodedPoints = encodePointList(points); - _skPath!.callMethod('addPoly', [encodedPoints, close]); + final SkFloat32List encodedPoints = toMallocedSkPoints(points); + _skPath.addPoly(encodedPoints.toTypedArray(), close); + freeFloat32List(encodedPoints); } @override void addRRect(ui.RRect rrect) { - final js.JsObject skRect = makeSkRect(rrect.outerRect); - final List radii = [ - rrect.tlRadiusX, - rrect.tlRadiusY, - rrect.trRadiusX, - rrect.trRadiusY, - rrect.brRadiusX, - rrect.brRadiusY, - rrect.blRadiusX, - rrect.blRadiusY, - ]; - _skPath!.callMethod('addRoundRect', - [skRect, js.JsArray.from(radii), false]); + final SkFloat32List skRadii = mallocFloat32List(8); + final Float32List radii = skRadii.toTypedArray(); + radii[0] = rrect.tlRadiusX; + radii[1] = rrect.tlRadiusY; + radii[2] = rrect.trRadiusX; + radii[3] = rrect.trRadiusY; + radii[4] = rrect.brRadiusX; + radii[5] = rrect.brRadiusY; + radii[6] = rrect.blRadiusX; + radii[7] = rrect.blRadiusY; + _skPath.addRoundRect( + toOuterSkRect(rrect), + radii, + false, + ); + freeFloat32List(skRadii); } @override void addRect(ui.Rect rect) { - _skPath!.callMethod('addRect', [makeSkRect(rect)]); + _skPath.addRect(toSkRect(rect)); } @override void arcTo( ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { const double toDegrees = 180.0 / math.pi; - _skPath!.callMethod('arcTo', [ - makeSkRect(rect), + _skPath.arcTo( + toSkRect(rect), startAngle * toDegrees, sweepAngle * toDegrees, forceMoveTo, - ]); + ); } @override @@ -139,7 +132,8 @@ class CkPath implements ui.Path { double rotation = 0.0, bool largeArc = false, bool clockwise = true}) { - _skPath!.callMethod('arcTo', [ + final SkPathArcToPointOverload overload = _jsObjectWrapper.castToSkPathArcToPointOverload(_skPath); + overload.arcTo( radius.x, radius.y, rotation, @@ -147,12 +141,12 @@ class CkPath implements ui.Path { !clockwise, arcEnd.dx, arcEnd.dy, - ]); + ); } @override void close() { - _skPath!.callMethod('close'); + _skPath.close(); } @override @@ -162,18 +156,18 @@ class CkPath implements ui.Path { @override void conicTo(double x1, double y1, double x2, double y2, double w) { - _skPath!.callMethod('conicTo', [x1, y1, x2, y2, w]); + _skPath.conicTo(x1, y1, x2, y2, w); } @override bool contains(ui.Offset point) { - return _skPath!.callMethod('contains', [point.dx, point.dy]); + return _skPath.contains(point.dx, point.dy); } @override void cubicTo( double x1, double y1, double x2, double y2, double x3, double y3) { - _skPath!.callMethod('cubicTo', [x1, y1, x2, y2, x3, y3]); + _skPath.cubicTo(x1, y1, x2, y2, x3, y3); } @override @@ -188,7 +182,7 @@ class CkPath implements ui.Path { skMatrix[5] += offset.dy; } final CkPath otherPath = path as CkPath; - _skPath!.callMethod('addPath', [ + _skPath.addPath( otherPath._skPath, skMatrix[0], skMatrix[1], @@ -200,28 +194,25 @@ class CkPath implements ui.Path { skMatrix[7], skMatrix[8], true, - ]); + ); } @override - ui.Rect getBounds() { - final js.JsObject bounds = _skPath!.callMethod('getBounds'); - return fromSkRect(bounds); - } + ui.Rect getBounds() => _skPath.getBounds().toRect(); @override void lineTo(double x, double y) { - _skPath!.callMethod('lineTo', [x, y]); + _skPath.lineTo(x, y); } @override void moveTo(double x, double y) { - _skPath!.callMethod('moveTo', [x, y]); + _skPath.moveTo(x, y); } @override void quadraticBezierTo(double x1, double y1, double x2, double y2) { - _skPath!.callMethod('quadTo', [x1, y1, x2, y2]); + _skPath.quadTo(x1, y1, x2, y2); } @override @@ -230,7 +221,7 @@ class CkPath implements ui.Path { double rotation = 0.0, bool largeArc = false, bool clockwise = true}) { - _skPath!.callMethod('rArcTo', [ + _skPath.rArcTo( radius.x, radius.y, rotation, @@ -238,48 +229,47 @@ class CkPath implements ui.Path { !clockwise, arcEndDelta.dx, arcEndDelta.dy, - ]); + ); } @override void relativeConicTo(double x1, double y1, double x2, double y2, double w) { - _skPath!.callMethod('rConicTo', [x1, y1, x2, y2, w]); + _skPath.rConicTo(x1, y1, x2, y2, w); } @override void relativeCubicTo( double x1, double y1, double x2, double y2, double x3, double y3) { - _skPath!.callMethod('rCubicTo', [x1, y1, x2, y2, x3, y3]); + _skPath.rCubicTo(x1, y1, x2, y2, x3, y3); } @override void relativeLineTo(double dx, double dy) { - _skPath!.callMethod('rLineTo', [dx, dy]); + _skPath.rLineTo(dx, dy); } @override void relativeMoveTo(double dx, double dy) { - _skPath!.callMethod('rMoveTo', [dx, dy]); + _skPath.rMoveTo(dx, dy); } @override void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { - _skPath!.callMethod('rQuadTo', [x1, y1, x2, y2]); + _skPath.rQuadTo(x1, y1, x2, y2); } @override void reset() { - _skPath!.callMethod('reset'); + _skPath.reset(); } @override ui.Path shift(ui.Offset offset) { // Since CanvasKit does not expose `SkPath.offset`, create a copy of this // path and call `transform` on it. - final js.JsObject newPath = _skPath!.callMethod('copy'); - newPath.callMethod('transform', - [1.0, 0.0, offset.dx, 0.0, 1.0, offset.dy, 0.0, 0.0, 0.0]); - return CkPath._fromSkPath(newPath); + final SkPath newPath = _skPath.copy(); + newPath.transform(1.0, 0.0, offset.dx, 0.0, 1.0, offset.dy, 0.0, 0.0, 0.0); + return CkPath._fromSkPath(newPath, _fillType); } static CkPath combine( @@ -289,48 +279,38 @@ class CkPath implements ui.Path { ) { final CkPath path1 = uiPath1 as CkPath; final CkPath path2 = uiPath2 as CkPath; - js.JsObject? pathOp; - switch (operation) { - case ui.PathOperation.difference: - pathOp = canvasKit['PathOp']['Difference']; - break; - case ui.PathOperation.intersect: - pathOp = canvasKit['PathOp']['Intersect']; - break; - case ui.PathOperation.union: - pathOp = canvasKit['PathOp']['Union']; - break; - case ui.PathOperation.xor: - pathOp = canvasKit['PathOp']['XOR']; - break; - case ui.PathOperation.reverseDifference: - pathOp = canvasKit['PathOp']['ReverseDifference']; - break; - } - final js.JsObject? newPath = canvasKit.callMethod( - 'MakePathFromOp', - [ - path1._skPath, - path2._skPath, - pathOp, - ], + final SkPath newPath = canvasKitJs.MakePathFromOp( + path1._skPath, + path2._skPath, + toSkPathOp(operation), ); - return CkPath._fromSkPath(newPath); + return CkPath._fromSkPath(newPath, path1._fillType); } @override ui.Path transform(Float64List matrix4) { - final js.JsObject newPath = _skPath!.callMethod('copy'); - newPath.callMethod('transform', [makeSkMatrixFromFloat64(matrix4)]); - return CkPath._fromSkPath(newPath); + final SkPath newPath = _skPath.copy(); + final Float32List m = toSkMatrixFromFloat64(matrix4); + newPath.transform( + m[0], + m[1], + m[2], + m[3], + m[4], + m[5], + m[6], + m[7], + m[8], + ); + return CkPath._fromSkPath(newPath, _fillType); } String? toSvgString() { - return _skPath!.callMethod('toSVGString'); + return _skPath.toSVGString(); } /// Return `true` if this path contains no segments. - bool? get isEmpty { - return _skPath!.callMethod('isEmpty'); + bool get isEmpty { + return _skPath.isEmpty(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/path_metrics.dart b/lib/web_ui/lib/src/engine/compositor/path_metrics.dart index 020a790f20415..32a43fa18a329 100644 --- a/lib/web_ui/lib/src/engine/compositor/path_metrics.dart +++ b/lib/web_ui/lib/src/engine/compositor/path_metrics.dart @@ -17,19 +17,16 @@ class CkPathMetrics extends IterableBase } class CkContourMeasureIter implements Iterator { - /// Cached constructor function for `SkContourMeasureIter`, so we don't have to look it - /// up every time we're constructing a new instance. - static final js.JsFunction? _skContourMeasureIterConstructor = canvasKit['SkContourMeasureIter']; - CkContourMeasureIter(CkPath path, bool forceClosed) - : _skObject = js.JsObject(_skContourMeasureIterConstructor!, [ + : _skObject = SkContourMeasureIter( path._skPath, forceClosed, 1, - ]); + ), + _fillType = path._fillType; - /// The JavaScript `SkContourMeasureIter` object. - final js.JsObject _skObject; + final SkContourMeasureIter _skObject; + final ui.PathFillType _fillType; /// A monotonically increasing counter used to generate [ui.PathMetric.contourIndex]. /// @@ -52,36 +49,36 @@ class CkContourMeasureIter implements Iterator { @override bool moveNext() { - final js.JsObject? skContourMeasure = _skObject.callMethod('next'); + final SkContourMeasure? skContourMeasure = _skObject.next(); if (skContourMeasure == null) { _current = null; return false; } - _current = CkContourMeasure(_contourIndexCounter, skContourMeasure); + _current = CkContourMeasure(_contourIndexCounter, skContourMeasure, _fillType); _contourIndexCounter += 1; return true; } } class CkContourMeasure implements ui.PathMetric { - CkContourMeasure(this.contourIndex, this._skObject); + CkContourMeasure(this.contourIndex, this._skObject, this._fillType); - final js.JsObject _skObject; + final SkContourMeasure _skObject; + final ui.PathFillType _fillType; @override final int contourIndex; @override ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { - final js.JsObject? skPath = _skObject - .callMethod('getSegment', [start, end, startWithMoveTo]); - return CkPath._fromSkPath(skPath); + final SkPath skPath = _skObject.getSegment(start, end, startWithMoveTo); + return CkPath._fromSkPath(skPath, _fillType); } @override ui.Tangent getTangentForOffset(double distance) { - final js.JsObject posTan = _skObject.callMethod('getPosTan', [distance]); + final Float32List posTan = _skObject.getPosTan(distance); return ui.Tangent( ui.Offset(posTan[0], posTan[1]), ui.Offset(posTan[2], posTan[3]), @@ -90,12 +87,12 @@ class CkContourMeasure implements ui.PathMetric { @override bool get isClosed { - return _skObject.callMethod('isClosed'); + return _skObject.isClosed(); } @override double get length { - return _skObject.callMethod('length'); + return _skObject.length(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/compositor/util.dart index f8c28fb5122d8..03111a5e407b7 100644 --- a/lib/web_ui/lib/src/engine/compositor/util.dart +++ b/lib/web_ui/lib/src/engine/compositor/util.dart @@ -340,7 +340,7 @@ void drawSkShadow( canvasKit.callMethod('computeTonalColors', [inTonalColors]); skCanvas.callMethod('drawShadow', [ - path._skPath, + path._legacyJsObject, js.JsArray.from([0, 0, devicePixelRatio * elevation]), js.JsArray.from( [shadowX, shadowY, devicePixelRatio * kLightHeight]), diff --git a/lib/web_ui/lib/src/engine/shader.dart b/lib/web_ui/lib/src/engine/shader.dart index 9db1c4e53a652..a8c7a700144dd 100644 --- a/lib/web_ui/lib/src/engine/shader.dart +++ b/lib/web_ui/lib/src/engine/shader.dart @@ -158,11 +158,10 @@ class GradientLinear extends EngineGradient { SkShader createSkiaShader() { assert(experimentalUseSkia); - var jsColors = makeColorList(colors); return canvasKitJs.SkShader.MakeLinearGradient( toSkPoint(from), toSkPoint(to), - jsColors, + toSkIntColorList(colors), toSkColorStops(colorStops), toSkTileMode(tileMode), ); diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 85d3ff7618264..9d80af08203a6 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -38,6 +38,10 @@ void main() { _toSkPointTests(); _toSkColorStopsTests(); _toSkMatrixFromFloat32Tests(); + _skSkRectTests(); + group('SkPath', () { + _pathTests(); + }); }, // This test failed on iOS Safari. // TODO: https://github.com/flutter/flutter/issues/60040 @@ -243,10 +247,7 @@ SkShader _makeTestShader() { return canvasKitJs.SkShader.MakeLinearGradient( Float32List.fromList([0, 0]), Float32List.fromList([1, 1]), - [ - Float32List.fromList([0, 0, 0, 1]), - Float32List.fromList([1, 1, 1, 1]), - ], + Uint32List.fromList([0x000000FF]), Float32List.fromList([0, 1]), canvasKitJs.TileMode.Repeat, ); @@ -443,6 +444,254 @@ void _toSkMatrixFromFloat32Tests() { }); } +void _pathTests() { + SkPath path; + + setUp(() { + path = SkPath(); + }); + + test('setFillType', () { + path.setFillType(canvasKitJs.FillType.Winding); + }); + + test('addArc', () { + path.addArc( + SkRect(fLeft: 10, fTop: 20, fRight: 30, fBottom: 40), + 1, + 5, + ); + }); + + test('addOval', () { + path.addOval( + SkRect(fLeft: 10, fTop: 20, fRight: 30, fBottom: 40), + false, + 1, + ); + }); + + SkPath _testClosedSkPath() { + return SkPath() + ..moveTo(10, 10) + ..lineTo(20, 10) + ..lineTo(20, 20) + ..lineTo(10, 20) + ..close(); + } + + test('addPath', () { + path.addPath(_testClosedSkPath(), 1, 0, 0, 0, 1, 0, 0, 0, 0, false); + }); + + test('addPoly', () { + final SkFloat32List encodedPoints = toMallocedSkPoints(const [ + ui.Offset.zero, + ui.Offset(10, 10), + ]); + path.addPoly(encodedPoints.toTypedArray(), true); + freeFloat32List(encodedPoints); + }); + + test('addRoundRect', () { + final ui.RRect rrect = ui.RRect.fromRectAndRadius( + ui.Rect.fromLTRB(10, 10, 20, 20), + ui.Radius.circular(3), + ); + final SkFloat32List skRadii = mallocFloat32List(8); + final Float32List radii = skRadii.toTypedArray(); + radii[0] = rrect.tlRadiusX; + radii[1] = rrect.tlRadiusY; + radii[2] = rrect.trRadiusX; + radii[3] = rrect.trRadiusY; + radii[4] = rrect.brRadiusX; + radii[5] = rrect.brRadiusY; + radii[6] = rrect.blRadiusX; + radii[7] = rrect.blRadiusY; + path.addRoundRect( + toOuterSkRect(rrect), + radii, + false, + ); + freeFloat32List(skRadii); + }); + + test('addRect', () { + path.addRect(SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4)); + }); + + test('arcTo', () { + path.arcTo( + SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4), + 5, + 40, + false, + ); + }); + + test('overloaded arcTo (used for arcToPoint)', () { + final SkPathArcToPointOverload overload = debugJsObjectWrapper.castToSkPathArcToPointOverload(path); + overload.arcTo( + 1, + 2, + 3, + false, + true, + 4, + 5, + ); + }); + + test('close', () { + _testClosedSkPath(); + }); + + test('conicTo', () { + path.conicTo(1, 2, 3, 4, 5); + }); + + test('contains', () { + final SkPath testPath = _testClosedSkPath(); + expect(testPath.contains(15, 15), true); + expect(testPath.contains(100, 100), false); + }); + + test('cubicTo', () { + path.cubicTo(1, 2, 3, 4, 5, 6); + }); + + test('getBounds', () { + final SkPath testPath = _testClosedSkPath(); + final ui.Rect bounds = testPath.getBounds().toRect(); + expect(bounds, const ui.Rect.fromLTRB(10, 10, 20, 20)); + }); + + test('lineTo', () { + path.lineTo(10, 10); + }); + + test('moveTo', () { + path.moveTo(10, 10); + }); + + test('quadTo', () { + path.quadTo(10, 10, 20, 20); + }); + + test('rArcTo', () { + path.rArcTo( + 10, + 20, + 30, + false, + true, + 40, + 50, + ); + }); + + test('rConicTo', () { + path.rConicTo(1, 2, 3, 4, 5); + }); + + test('rCubicTo', () { + path.rCubicTo(1, 2, 3, 4, 5, 6); + }); + + test('rLineTo', () { + path.rLineTo(10, 10); + }); + + test('rMoveTo', () { + path.rMoveTo(10, 10); + }); + + test('rQuadTo', () { + path.rQuadTo(10, 10, 20, 20); + }); + + test('reset', () { + final SkPath testPath = _testClosedSkPath(); + expect(testPath.getBounds().toRect(), const ui.Rect.fromLTRB(10, 10, 20, 20)); + testPath.reset(); + expect(testPath.getBounds().toRect(), ui.Rect.zero); + }); + + test('toSVGString', () { + expect(_testClosedSkPath().toSVGString(), 'M10 10L20 10L20 20L10 20L10 10Z'); + }); + + test('isEmpty', () { + expect(SkPath().isEmpty(), true); + expect(_testClosedSkPath().isEmpty(), false); + }); + + test('copy', () { + final SkPath original = _testClosedSkPath(); + final SkPath copy = original.copy(); + expect(original.getBounds().toRect(), copy.getBounds().toRect()); + }); + + test('transform', () { + path = _testClosedSkPath(); + path.transform(2, 0, 10, 0, 2, 10, 0, 0, 0); + final ui.Rect transformedBounds = path.getBounds().toRect(); + expect(transformedBounds, ui.Rect.fromLTRB(30, 30, 50, 50)); + }); + + test('SkContourMeasureIter/SkContourMeasure', () { + final SkContourMeasureIter iter = SkContourMeasureIter(_testClosedSkPath(), false, 0); + final SkContourMeasure measure1 = iter.next(); + expect(measure1.length(), 40); + expect(measure1.getPosTan(5), Float32List.fromList([15, 10, 1, 0])); + expect(measure1.getPosTan(15), Float32List.fromList([20, 15, 0, 1])); + expect(measure1.isClosed(), true); + + // Starting with a box path: + // + // 10 20 + // 10 +-----------+ + // | | + // | | + // | | + // | | + // | | + // 20 +-----------+ + // + // Cut out the top-right quadrant: + // + // 10 15 20 + // 10 +-----+=====+ + // | ║+++++║ + // | ║+++++║ + // | +=====+ 15 + // | | + // | | + // 20 +-----------+ + final SkPath segment = measure1.getSegment(5, 15, true); + expect(segment.getBounds().toRect(), ui.Rect.fromLTRB(15, 10, 20, 15)); + + final SkContourMeasure measure2 = iter.next(); + expect(measure2, isNull); + }); +} + +void _skSkRectTests() { + test('SkRect', () { + final SkRect rect = SkRect(fLeft: 1, fTop: 2, fRight: 3, fBottom: 4); + expect(rect.fLeft, 1); + expect(rect.fTop, 2); + expect(rect.fRight, 3); + expect(rect.fBottom, 4); + + final ui.Rect uiRect = rect.toRect(); + expect(uiRect.left, 1); + expect(uiRect.top, 2); + expect(uiRect.right, 3); + expect(uiRect.bottom, 4); + }); +} + final Uint8List kTransparentImage = Uint8List.fromList([ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,