| // 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. |
| |
| // @dart = 2.6 |
| part of engine; |
| |
| /// An object backed by a [js.JsObject] mapped onto a Skia C++ object in the |
| /// WebAssembly heap. |
| /// |
| /// These objects are automatically deleted when no longer used. |
| /// |
| /// Because there is no feedback from JavaScript's GC (no destructors or |
| /// finalizers), we pessimistically delete the underlying C++ object before |
| /// the Dart object is garbage-collected. The current algorithm deletes objects |
| /// at the end of every frame. This allows reusing the C++ objects within the |
| /// frame. In the future we may add smarter strategies that will allow us to |
| /// reuse C++ objects across frames. |
| /// |
| /// The lifecycle of a C++ object is as follows: |
| /// |
| /// - Create default: when instantiating a C++ object for a Dart object for the |
| /// first time, the C++ object is populated with default data (the defaults are |
| /// defined by Flutter; Skia defaults are corrected if necessary). The |
| /// default object is created by [createDefault]. |
| /// - Zero or more cycles of delete + resurrect: when a Dart object is reused |
| /// after its C++ object is deleted we create a new C++ object populated with |
| /// data from the current state of the Dart object. This is done using the |
| /// [resurrect] method. |
| /// - Final delete: if a Dart object is never reused, it is GC'd after its |
| /// underlying C++ object is deleted. This is implemented by [SkiaObjects]. |
| abstract class SkiaObject { |
| SkiaObject() { |
| _skiaObject = createDefault(); |
| SkiaObjects.manage(this); |
| } |
| |
| /// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap. |
| js.JsObject get skiaObject { |
| if (_skiaObject == null) { |
| _skiaObject = resurrect(); |
| SkiaObjects.manage(this); |
| } |
| return _skiaObject; |
| } |
| |
| /// Do not use this field outside this class. Use [skiaObject] instead. |
| js.JsObject _skiaObject; |
| |
| /// Instantiates a new Skia-backed JavaScript object containing default |
| /// values. |
| /// |
| /// The object is expected to represent Flutter's defaults. If Skia uses |
| /// different defaults from those used by Flutter, this method is expected |
| /// initialize the object to Flutter's defaults. |
| js.JsObject createDefault(); |
| |
| /// Creates a new Skia-backed JavaScript object containing data representing |
| /// the current state of the Dart object. |
| js.JsObject resurrect(); |
| } |
| |
| /// Singleton that manages the lifecycles of [SkiaObject] instances. |
| class SkiaObjects { |
| // TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects |
| // beyond a single frame. |
| @visibleForTesting |
| static final List<SkiaObject> managedObjects = () { |
| window.rasterizer.addPostFrameCallback(postFrameCleanUp); |
| return <SkiaObject>[]; |
| }(); |
| |
| /// Starts managing the lifecycle of [object]. |
| /// |
| /// The object's underlying WASM object is deleted by calling the |
| /// "delete" method when it goes out of scope. |
| /// |
| /// The current implementation deletes objects at the end of every frame. |
| static void manage(SkiaObject object) { |
| managedObjects.add(object); |
| } |
| |
| /// Deletes all C++ objects created this frame. |
| static void postFrameCleanUp() { |
| if (managedObjects.isEmpty) { |
| return; |
| } |
| |
| for (int i = 0; i < managedObjects.length; i++) { |
| final SkiaObject object = managedObjects[i]; |
| object._skiaObject.callMethod('delete'); |
| object._skiaObject = null; |
| } |
| |
| managedObjects.clear(); |
| } |
| } |
| |
| js.JsObject makeSkRect(ui.Rect rect) { |
| return js.JsObject(canvasKit['LTRBRect'], |
| <double>[rect.left, rect.top, rect.right, rect.bottom]); |
| } |
| |
| js.JsObject makeSkRRect(ui.RRect rrect) { |
| return js.JsObject.jsify({ |
| 'rect': makeSkRect(rrect.outerRect), |
| 'rx1': rrect.tlRadiusX, |
| 'ry1': rrect.tlRadiusY, |
| 'rx2': rrect.trRadiusX, |
| 'ry2': rrect.trRadiusY, |
| 'rx3': rrect.brRadiusX, |
| 'ry3': rrect.brRadiusY, |
| 'rx4': rrect.blRadiusX, |
| 'ry4': rrect.blRadiusY, |
| }); |
| } |
| |
| ui.Rect fromSkRect(js.JsObject skRect) { |
| return ui.Rect.fromLTRB( |
| skRect['fLeft'], |
| skRect['fTop'], |
| skRect['fRight'], |
| skRect['fBottom'], |
| ); |
| } |
| |
| ui.TextPosition fromPositionWithAffinity(js.JsObject positionWithAffinity) { |
| if (positionWithAffinity['affinity'] == canvasKit['Affinity']['Upstream']) { |
| return ui.TextPosition( |
| offset: positionWithAffinity['pos'], |
| affinity: ui.TextAffinity.upstream, |
| ); |
| } else { |
| assert(positionWithAffinity['affinity'] == |
| canvasKit['Affinity']['Downstream']); |
| return ui.TextPosition( |
| offset: positionWithAffinity['pos'], |
| affinity: ui.TextAffinity.downstream, |
| ); |
| } |
| } |
| |
| js.JsArray<double> makeSkPoint(ui.Offset point) { |
| final js.JsArray<double> skPoint = js.JsArray<double>(); |
| skPoint.length = 2; |
| skPoint[0] = point.dx; |
| skPoint[1] = point.dy; |
| return skPoint; |
| } |
| |
| /// Creates a point list using a typed buffer created by CanvasKit.Malloc. |
| Float32List encodePointList(List<ui.Offset> points) { |
| assert(points != null); |
| final int pointCount = points.length; |
| final Float32List result = canvasKit.callMethod('Malloc', <dynamic>[js.context['Float32Array'], pointCount * 2]); |
| for (int i = 0; i < pointCount; ++i) { |
| final int xIndex = i * 2; |
| final int yIndex = xIndex + 1; |
| final ui.Offset point = points[i]; |
| assert(_offsetIsValid(point)); |
| result[xIndex] = point.dx; |
| result[yIndex] = point.dy; |
| } |
| return result; |
| } |
| |
| js.JsObject makeSkPointMode(ui.PointMode pointMode) { |
| switch (pointMode) { |
| case ui.PointMode.points: |
| return canvasKit['PointMode']['Points']; |
| case ui.PointMode.lines: |
| return canvasKit['PointMode']['Lines']; |
| case ui.PointMode.polygon: |
| return canvasKit['PointMode']['Polygon']; |
| default: |
| throw StateError('Unrecognized point mode $pointMode'); |
| } |
| } |
| |
| js.JsObject makeSkBlendMode(ui.BlendMode blendMode) { |
| switch (blendMode) { |
| case ui.BlendMode.clear: |
| return canvasKit['BlendMode']['Clear']; |
| case ui.BlendMode.src: |
| return canvasKit['BlendMode']['Src']; |
| case ui.BlendMode.dst: |
| return canvasKit['BlendMode']['Dst']; |
| case ui.BlendMode.srcOver: |
| return canvasKit['BlendMode']['SrcOver']; |
| case ui.BlendMode.dstOver: |
| return canvasKit['BlendMode']['DstOver']; |
| case ui.BlendMode.srcIn: |
| return canvasKit['BlendMode']['SrcIn']; |
| case ui.BlendMode.dstIn: |
| return canvasKit['BlendMode']['DstIn']; |
| case ui.BlendMode.srcOut: |
| return canvasKit['BlendMode']['SrcOut']; |
| case ui.BlendMode.dstOut: |
| return canvasKit['BlendMode']['DstOut']; |
| case ui.BlendMode.srcATop: |
| return canvasKit['BlendMode']['SrcATop']; |
| case ui.BlendMode.dstATop: |
| return canvasKit['BlendMode']['DstATop']; |
| case ui.BlendMode.xor: |
| return canvasKit['BlendMode']['Xor']; |
| case ui.BlendMode.plus: |
| return canvasKit['BlendMode']['Plus']; |
| case ui.BlendMode.modulate: |
| return canvasKit['BlendMode']['Modulate']; |
| case ui.BlendMode.screen: |
| return canvasKit['BlendMode']['Screen']; |
| case ui.BlendMode.overlay: |
| return canvasKit['BlendMode']['Overlay']; |
| case ui.BlendMode.darken: |
| return canvasKit['BlendMode']['Darken']; |
| case ui.BlendMode.lighten: |
| return canvasKit['BlendMode']['Lighten']; |
| case ui.BlendMode.colorDodge: |
| return canvasKit['BlendMode']['ColorDodge']; |
| case ui.BlendMode.colorBurn: |
| return canvasKit['BlendMode']['ColorBurn']; |
| case ui.BlendMode.hardLight: |
| return canvasKit['BlendMode']['HardLight']; |
| case ui.BlendMode.softLight: |
| return canvasKit['BlendMode']['SoftLight']; |
| case ui.BlendMode.difference: |
| return canvasKit['BlendMode']['Difference']; |
| case ui.BlendMode.exclusion: |
| return canvasKit['BlendMode']['Exclusion']; |
| case ui.BlendMode.multiply: |
| return canvasKit['BlendMode']['Multiply']; |
| case ui.BlendMode.hue: |
| return canvasKit['BlendMode']['Hue']; |
| case ui.BlendMode.saturation: |
| return canvasKit['BlendMode']['Saturation']; |
| case ui.BlendMode.color: |
| return canvasKit['BlendMode']['Color']; |
| case ui.BlendMode.luminosity: |
| return canvasKit['BlendMode']['Luminosity']; |
| default: |
| return null; |
| } |
| } |
| |
| // Mappings from SkMatrix-index to input-index. |
| const List<int> _skMatrixIndexToMatrix4Index = <int>[ |
| 0, 4, 12, // Row 1 |
| 1, 5, 13, // Row 2 |
| 3, 7, 15, // Row 3 |
| ]; |
| |
| /// Converts a 4x4 Flutter matrix (represented as a [Float32List]) to an |
| /// SkMatrix, which is a 3x3 transform matrix. |
| js.JsArray<double> makeSkMatrixFromFloat32(Float32List matrix4) { |
| final js.JsArray<double> skMatrix = js.JsArray<double>(); |
| skMatrix.length = 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; |
| } |
| |
| js.JsArray<double> makeSkMatrixFromFloat64(Float64List matrix4) { |
| final js.JsArray<double> skMatrix = js.JsArray<double>(); |
| skMatrix.length = 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; |
| } |
| |
| /// Color stops used when the framework specifies `null`. |
| final js.JsArray<double> _kDefaultColorStops = () { |
| final js.JsArray<double> jsColorStops = js.JsArray<double>(); |
| jsColorStops.length = 2; |
| jsColorStops[0] = 0; |
| jsColorStops[1] = 1; |
| return jsColorStops; |
| }(); |
| |
| /// Converts a list of color stops into a Skia-compatible JS array or color stops. |
| /// |
| /// In Flutter `null` means two color stops `[0, 1]` that in Skia must be specified explicitly. |
| js.JsArray<double> makeSkiaColorStops(List<double> colorStops) { |
| if (colorStops == null) { |
| return _kDefaultColorStops; |
| } |
| |
| final js.JsArray<double> jsColorStops = js.JsArray<double>.from(colorStops); |
| jsColorStops.length = colorStops.length; |
| return jsColorStops; |
| } |
| |
| void drawSkShadow( |
| js.JsObject skCanvas, |
| SkPath path, |
| ui.Color color, |
| double elevation, |
| bool transparentOccluder, |
| double devicePixelRatio, |
| ) { |
| const double ambientAlpha = 0.039; |
| const double spotAlpha = 0.25; |
| |
| final int flags = transparentOccluder ? 0x01 : 0x00; |
| |
| final ui.Rect bounds = path.getBounds(); |
| final double shadowX = (bounds.left + bounds.right) / 2.0; |
| final double shadowY = bounds.top - 600.0; |
| |
| ui.Color inAmbient = color.withAlpha((color.alpha * ambientAlpha).round()); |
| ui.Color inSpot = color.withAlpha((color.alpha * spotAlpha).round()); |
| |
| final js.JsObject inTonalColors = js.JsObject.jsify(<String, int>{ |
| 'ambient': inAmbient.value, |
| 'spot': inSpot.value, |
| }); |
| |
| final js.JsObject tonalColors = |
| canvasKit.callMethod('computeTonalColors', <js.JsObject>[inTonalColors]); |
| |
| skCanvas.callMethod('drawShadow', <dynamic>[ |
| path._skPath, |
| js.JsArray<double>.from(<double>[0, 0, devicePixelRatio * elevation]), |
| js.JsArray<double>.from( |
| <double>[shadowX, shadowY, devicePixelRatio * kLightHeight]), |
| devicePixelRatio * kLightRadius, |
| tonalColors['ambient'], |
| tonalColors['spot'], |
| flags, |
| ]); |
| } |