aboutsummaryrefslogtreecommitdiff
path: root/experimental
diff options
context:
space:
mode:
authorKevin Lubick <kjlubick@google.com>2018-08-17 13:52:56 -0400
committerKevin Lubick <kjlubick@google.com>2018-08-17 18:30:32 +0000
commit11194ab930962f21ad5b5cba4680d80aeee00866 (patch)
treec8a87d51815838ecdfab05b3aa2b036770c026e8 /experimental
parent5eebe87a0c7f705585d8add1ca07fee855061a00 (diff)
downloadskqp-11194ab930962f21ad5b5cba4680d80aeee00866.tar.gz
[PathKit] Rework API to avoid extra copies unless explicitly called for.
Breaking Changes: - All method calls that mutate a path now return the same JS path object to allow chaining (moveTo, lineTo, trim, op, simplify, etc). Pre-existing code likely will need to have some delete() methods removed because the path will be deleted multiple times. See chaining.js for this code (basically, we wrote our own binding code since the default code wasn't quite flexible enough) - GetCanvasFillType -> GetFillTypeString (Was in https://skia-review.googlesource.com/c/skia/+/147209) Since Canvas and SVG use the same strings, it seemed logical to make them share. - stroke() now takes a single object instead of 3 params. This object currently can have up to 4 params, cap, join, width, miter_limit. This object can be expanded on in future versions as more configuration options are added. As per custom with v0 software, we bump the minor version to 0.2.X to indicate breaking changes in a pre-release software package. Other changes of note: - Simple tests added for effects (see effects.specs.js) A follow up CL will handle the Gold (correctness tests) - Simple tests added for equals and copy constructors (from https://skia-review.googlesource.com/c/skia/+/147209) - Added transform() to allow for arbitrary matrix transforms - Added SimpleMatrix as a value_array, which means users can provide a 9 element array which will be converted to SimpleMatrix and then SkMatrix on the C++ side. - Renamed helpers_externs.js to externs.js and expanded it greatly. This was necessitated by the code written in chaining.js - Fixed a few bugs in previous tests (svg gold test race condition, uncaught exception in svg reporting) See also https://skia-review.googlesource.com/c/skia/+/147209 which allows .moveTo .lineTo, etc to chain on the C++ SkPath. Bug: skia:8216 Change-Id: I7450cd8b7b5377cf15c962b02d161677b62d7e15 Reviewed-on: https://skia-review.googlesource.com/147115 Reviewed-by: Mike Reed <reed@google.com>
Diffstat (limited to 'experimental')
-rw-r--r--experimental/pathkit/Makefile6
-rw-r--r--experimental/pathkit/chaining.js186
-rwxr-xr-xexperimental/pathkit/compile.sh3
-rw-r--r--experimental/pathkit/externs.js136
-rw-r--r--experimental/pathkit/helper_externs.js5
-rw-r--r--experimental/pathkit/npm-wasm/example.html310
-rw-r--r--experimental/pathkit/npm-wasm/package.json2
-rw-r--r--experimental/pathkit/package.json2
-rw-r--r--experimental/pathkit/pathkit_wasm_bindings.cpp252
-rw-r--r--experimental/pathkit/tests/effects.spec.js136
-rw-r--r--experimental/pathkit/tests/path.spec.js172
-rw-r--r--experimental/pathkit/tests/path2d.spec.js77
-rw-r--r--experimental/pathkit/tests/pathops.spec.js4
-rw-r--r--experimental/pathkit/tests/svg.spec.js1
-rw-r--r--experimental/pathkit/tests/testReporter.js2
15 files changed, 987 insertions, 307 deletions
diff --git a/experimental/pathkit/Makefile b/experimental/pathkit/Makefile
index dee9c1d746..d66dbb121e 100644
--- a/experimental/pathkit/Makefile
+++ b/experimental/pathkit/Makefile
@@ -84,21 +84,21 @@ example:
python serve.py
local-example:
- rm -rf node_modules
+ rm -rf node_modules/experimental-pathkit-wasm
mkdir -p node_modules
ln -s -T ../npm-wasm node_modules/experimental-pathkit-wasm
echo "Go check out localhost:8000/npm-wasm/example.html"
python serve.py
local-example-test:
- rm -rf node_modules
+ rm -rf node_modules/experimental-pathkit-wasm
mkdir -p node_modules/experimental-pathkit-wasm
ln -s -T ../../npm-wasm/bin/test node_modules/experimental-pathkit-wasm/bin
echo "Go check out localhost:8000/npm-wasm/example.html"
python serve.py
local-example-debug:
- rm -rf node_modules
+ rm -rf node_modules/experimental-pathkit-wasm
mkdir -p node_modules/experimental-pathkit-wasm
ln -s -T ../../npm-wasm/bin/debug node_modules/experimental-pathkit-wasm/bin
echo "Go check out localhost:8000/npm-wasm/example.html"
diff --git a/experimental/pathkit/chaining.js b/experimental/pathkit/chaining.js
new file mode 100644
index 0000000000..d4902c6069
--- /dev/null
+++ b/experimental/pathkit/chaining.js
@@ -0,0 +1,186 @@
+// Adds JS functions to allow for chaining w/o leaking by re-using 'this' path.
+(function(PathKit){
+ // PathKit.onRuntimeInitialized is called after the WASM library has loaded.
+ // when onRuntimeInitialized is called, PathKit.SkPath is defined with many
+ // functions on it (see pathkit_wasm_bindings.cpp@EMSCRIPTEN_BINDINGS)
+ PathKit.onRuntimeInitialized = function() {
+ // All calls to 'this' need to go in externs.js so closure doesn't minify them away.
+
+ PathKit.SkPath.prototype.addPath = function() {
+ // Takes 1, 2, 7 or 10 args, where the first arg is always the path.
+ // The options for the remaining args are:
+ // - an SVGMatrix
+ // - the 6 parameters of an SVG Matrix
+ // - the 9 parameters of a full Matrix
+ if (arguments.length === 1) {
+ // Add path, unchanged. Use identify matrix
+ this._addPath(arguments[0], 1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 1);
+ } else if (arguments.length === 2) {
+ // Takes SVGMatrix, which has its args in a counter-intuitive order
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#Transform_functions
+ /**
+ * @type {SVGMatrix}
+ */
+ var sm = arguments[1];
+ this._addPath(arguments[0], sm.a, sm.c, sm.e,
+ sm.b, sm.d, sm.f,
+ 0, 0, 1);
+ } else if (arguments.length === 7) {
+ // User provided the 6 params for an SVGMatrix directly.
+ var a = arguments;
+ this._addPath(arguments[0], a[1], a[3], a[5],
+ a[2], a[4], a[6],
+ 0, 0, 1);
+ } else if (arguments.length === 10) {
+ // User provided the 9 params of a (full) matrix directly.
+ // These are in the same order as what Skia expects.
+ var a = arguments;
+ this._addPath(arguments[0], a[1], a[2], a[3],
+ a[4], a[5], a[6],
+ a[7], a[8], a[9]);
+ } else {
+ console.err('addPath expected to take 1, 2, 7, or 10 args. Got ' + arguments.length);
+ return null;
+ }
+ return this;
+ };
+
+ // ccw (counter clock wise) is optional and defaults to false.
+ PathKit.SkPath.prototype.arc = function(x, y, radius, startAngle, endAngle, ccw) {
+ this._arc(x, y, radius, startAngle, endAngle, !!ccw);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.arcTo = function(x1, y1, x2, y2, radius) {
+ this._arcTo(x1, y1, x2, y2, radius);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
+ this._cubicTo(cp1x, cp1y, cp2x, cp2y, x, y);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.close = function() {
+ this._close();
+ return this;
+ };
+
+ // Reminder, we have some duplicate definitions because we want to be a
+ // superset of Path2D and also work like the original SkPath C++ object.
+ PathKit.SkPath.prototype.closePath = function() {
+ this._close();
+ return this;
+ };
+
+ PathKit.SkPath.prototype.conicTo = function(x1, y1, x2, y2, w) {
+ this._conicTo(x1, y1, x2, y2, w);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.cubicTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
+ this._cubicTo(cp1x, cp1y, cp2x, cp2y, x, y);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.dash = function(on, off, phase) {
+ if (this._dash(on, off, phase)) {
+ return this;
+ }
+ return null;
+ };
+
+ // ccw (counter clock wise) is optional and defaults to false.
+ PathKit.SkPath.prototype.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, ccw) {
+ this._ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, !!ccw);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.lineTo = function(x, y) {
+ this._lineTo(x, y);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.moveTo = function(x, y) {
+ this._moveTo(x, y);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.op = function(otherPath, op) {
+ if (this._op(otherPath, op)) {
+ return this;
+ }
+ return null;
+ };
+
+ PathKit.SkPath.prototype.quadraticCurveTo = function(x1, y1, x2, y2) {
+ this._quadTo(x1, y1, x2, y2);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.quadTo = function(x1, y1, x2, y2) {
+ this._quadTo(x1, y1, x2, y2);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.rect = function(x, y, w, h) {
+ this._rect(x, y, w, h);
+ return this;
+ };
+
+ PathKit.SkPath.prototype.simplify = function() {
+ if (this._simplify()) {
+ return this;
+ }
+ return null;
+ };
+
+ PathKit.SkPath.prototype.stroke = function(opts) {
+ // Fill out any missing values with the default values.
+ /**
+ * See externs.js for this definition
+ * @type {StrokeOpts}
+ */
+ opts = opts || {};
+ opts.width = opts.width || 1;
+ opts.miter_limit = opts.miter_limit || 4;
+ opts.cap = opts.cap || PathKit.StrokeCap.BUTT;
+ opts.join = opts.join || PathKit.StrokeJoin.MITER;
+ if (this._stroke(opts)) {
+ return this;
+ }
+ return null;
+ };
+
+ PathKit.SkPath.prototype.transform = function() {
+ // Takes 1 or 9 args
+ if (arguments.length === 1) {
+ // argument 1 should be a 9 element array (which is transformed on the C++ side
+ // to a SimpleMatrix)
+ this._transform(arguments[0]);
+ } else if (arguments.length === 9) {
+ // these arguments are the 9 members of the matrix
+ var a = arguments;
+ this._transform(a[0], a[1], a[2],
+ a[3], a[4], a[5],
+ a[6], a[7], a[8]);
+ } else {
+ console.err('transform expected to take 1 or 9 arguments. Got ' + arguments.length);
+ return null;
+ }
+ return this;
+ };
+
+ // isComplement is optional, defaults to false
+ PathKit.SkPath.prototype.trim = function(startT, stopT, isComplement) {
+ if (this._trim(startT, stopT, !!isComplement)) {
+ return this;
+ }
+ return null;
+ };
+ };
+
+}(Module)); // When this file is loaded in, the high level object is "Module";
+
diff --git a/experimental/pathkit/compile.sh b/experimental/pathkit/compile.sh
index fc43301d9a..9507f809bf 100755
--- a/experimental/pathkit/compile.sh
+++ b/experimental/pathkit/compile.sh
@@ -43,7 +43,7 @@ fi
# Use -O0 for larger builds (but generally quicker)
# Use -Oz for (much slower, but smaller/faster) production builds
-export EMCC_CLOSURE_ARGS="--externs $BASE_DIR/helper_externs.js "
+export EMCC_CLOSURE_ARGS="--externs $BASE_DIR/externs.js "
RELEASE_CONF="-Oz --closure 1 -s EVAL_CTORS=1 --llvm-lto 3"
if [[ $@ == *test* ]]; then
echo "Building a Testing/Profiling build"
@@ -85,6 +85,7 @@ em++ $RELEASE_CONF -std=c++14 \
-Isrc/utils \
--bind \
--pre-js $BASE_DIR/helper.js \
+--pre-js $BASE_DIR/chaining.js \
-DWEB_ASSEMBLY=1 \
-fno-rtti -fno-exceptions -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 \
$WASM_CONF \
diff --git a/experimental/pathkit/externs.js b/experimental/pathkit/externs.js
new file mode 100644
index 0000000000..b68a3a21d4
--- /dev/null
+++ b/experimental/pathkit/externs.js
@@ -0,0 +1,136 @@
+/*
+ * This externs file prevents the Closure JS compiler from minifying away
+ * names of objects created by Emscripten.
+ * Basically, by defining empty objects and functions here, Closure will
+ * know not to rename them. This is needed because of our pre-js files,
+ * that is, the JS we hand-write to bundle into PathKit.
+ *
+ * Emscripten does not support automatically generating an externs file, so we
+ * do it by hand. The general process is to write some JS code, and then put any
+ * calls to PathKit or related things in here. Running ./compile.sh and then
+ * looking at the minified results or running the Release-PathKit trybot should
+ * verify nothing was missed. Optionally, looking directly at the minified
+ * pathkit.js can be useful when developing locally.
+ *
+ * Docs:
+ * https://github.com/cljsjs/packages/wiki/Creating-Externs
+ * https://github.com/google/closure-compiler/wiki/Types-in-the-Closure-Type-System
+ *
+ * Example externs:
+ * https://github.com/google/closure-compiler/tree/master/externs
+ */
+
+var PathKit = {
+ SkBits2FloatUnsigned: function(num) {},
+ _malloc: function(size) {},
+ onRuntimeInitialized: function() {},
+
+ HEAPF32: {},
+
+ SkPath: {
+ _addPath: function(path, scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2) {},
+ _arc: function(x, y, radius, startAngle, endAngle, ccw) {},
+ _arcTo: function(x1, y1, x2, y2, radius) {},
+ _dash: function(on, off, phase) {},
+ _close: function() {},
+ _conicTo: function(x1, y1, x2, y2, w) {},
+ copy: function() {},
+ _cubicTo: function(cp1x, cp1y, cp2x, cp2y, x, y) {},
+ _ellipse: function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, ccw) {},
+ _lineTo: function(x1, y1) {},
+ _moveTo: function(x1, y1) {},
+ _op: function(otherPath, op) {},
+ _quadTo: function(x1, y1, x2, y2) {},
+ _rect: function(x, y, w, h) {},
+ _simplify: function() {},
+ _stroke: function(opts) {},
+ _trim: function(startT, stopT, isComplement) {},
+ _transform: function() {}, // takes 1 or 9 params
+ },
+
+ StrokeCap: {
+ BUTT: {},
+ ROUND: {},
+ SQUARE: {},
+ },
+ StrokeJoin: {
+ MITER: {},
+ ROUND: {},
+ BEVEL: {},
+ }
+};
+
+// Define StrokeOpts object
+var StrokeOpts = {};
+StrokeOpts.prototype.width;
+StrokeOpts.prototype.miter_limit;
+StrokeOpts.prototype.cap;
+StrokeOpts.prototype.join;
+
+
+
+// For whatever reason, the closure compiler thinks it can rename some of our
+// prototype methods. Not entirely sure why.
+// Listing them here prevents that.
+PathKit.SkPath.prototype.addPath = function() {};
+PathKit.SkPath.prototype.arc = function(x, y, radius, startAngle, endAngle, ccw) {};
+PathKit.SkPath.prototype.arcTo = function(x1, y1, x2, y2, radius) {};
+PathKit.SkPath.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {};
+PathKit.SkPath.prototype.close = function() {};
+PathKit.SkPath.prototype.closePath = function() {};
+PathKit.SkPath.prototype.conicTo = function(x1, y1, x2, y2, w) {};
+PathKit.SkPath.prototype.cubicTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {};
+PathKit.SkPath.prototype.dash = function(on, off, phase) {};
+PathKit.SkPath.prototype.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, ccw) {};
+PathKit.SkPath.prototype.lineTo = function(x, y) {};
+PathKit.SkPath.prototype.moveTo = function(x, y) {};
+PathKit.SkPath.prototype.op = function(otherPath, op) {};
+PathKit.SkPath.prototype.quadTo = function(x1, y1, x2, y2) {};
+PathKit.SkPath.prototype.quadraticCurveTo = function(x1, y1, x2, y2) {};
+PathKit.SkPath.prototype.rect = function(x, y, w, h) {};
+PathKit.SkPath.prototype.simplify = function() {};
+PathKit.SkPath.prototype.stroke = function(opts) {};
+PathKit.SkPath.prototype.transform = function() {};
+PathKit.SkPath.prototype.trim = function(startT, stopT, isComplement) {};
+// The following was taken from https://github.com/google/closure-compiler/blob/master/contrib/externs/svg.js
+
+/**
+ * @constructor
+ */
+function SVGMatrix(){}
+
+
+/**
+ * @type {number}
+ */
+SVGMatrix.prototype.a;
+
+
+/**
+ * @type {number}
+ */
+SVGMatrix.prototype.b;
+
+
+/**
+ * @type {number}
+ */
+SVGMatrix.prototype.c;
+
+
+/**
+ * @type {number}
+ */
+SVGMatrix.prototype.d;
+
+
+/**
+ * @type {number}
+ */
+SVGMatrix.prototype.e;
+
+
+/**
+ * @type {number}
+ */
+SVGMatrix.prototype.f; \ No newline at end of file
diff --git a/experimental/pathkit/helper_externs.js b/experimental/pathkit/helper_externs.js
deleted file mode 100644
index 6718053fa5..0000000000
--- a/experimental/pathkit/helper_externs.js
+++ /dev/null
@@ -1,5 +0,0 @@
-var PathKit = {};
-
-PathKit.SkBits2FloatUnsigned = function(num) {};
-PathKit._malloc = function(size) {};
-PathKit.HEAPF32 = {}; \ No newline at end of file
diff --git a/experimental/pathkit/npm-wasm/example.html b/experimental/pathkit/npm-wasm/example.html
index ec097b947e..60d6981b0a 100644
--- a/experimental/pathkit/npm-wasm/example.html
+++ b/experimental/pathkit/npm-wasm/example.html
@@ -22,7 +22,7 @@
</style>
<h2> Can output to an SVG Path, a Canvas, or a Path2D object </h2>
-<svg id=svg xmlns='http://www.w3.org/2000/svg' width=200 height=200></svg>
+<svg id=svg1 xmlns='http://www.w3.org/2000/svg' width=200 height=200></svg>
<canvas id=canvas1></canvas>
<canvas id=canvas2></canvas>
@@ -37,6 +37,11 @@
<canvas class=big id=canvas8></canvas>
<canvas class=big id=canvas9></canvas>
<canvas class=big id=canvas10></canvas>
+<canvas class=big id=canvasTransform></canvas>
+
+<h2> Supports fill-rules of nonzero and evenodd </h2>
+<svg id=svg2 xmlns='http://www.w3.org/2000/svg' width=200 height=200></svg>
+<svg id=svg3 xmlns='http://www.w3.org/2000/svg' width=200 height=200></svg>
<script type="text/javascript" src="/node_modules/experimental-pathkit-wasm/bin/pathkit.js"></script>
@@ -45,9 +50,12 @@
PathKitInit({
locateFile: (file) => '/node_modules/experimental-pathkit-wasm/bin/'+file,
}).then((PathKit) => {
+ window.PathKit = PathKit;
OutputsExample(PathKit);
Path2DExample(PathKit);
PathEffectsExample(PathKit);
+ MatrixTransformExample(PathKit);
+ FilledSVGExample(PathKit);
});
function setCanvasSize(ctx, width, height) {
@@ -59,21 +67,23 @@
let firstPath = PathKit.FromSVGString('M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z');
let secondPath = PathKit.NewPath();
- // Acts somewhat like the Canvas API
- secondPath.moveTo(1, 1);
- secondPath.lineTo(20, 1);
- secondPath.lineTo(10, 30);
- secondPath.closePath();
+ // Acts somewhat like the Canvas API, except can be chained
+ secondPath.moveTo(1, 1)
+ .lineTo(20, 1)
+ .lineTo(10, 30)
+ .closePath();
+
+ // Join the two paths together (mutating firstPath in the process)
+ firstPath.op(secondPath, PathKit.PathOp.INTERSECT);
- let combinedPath = firstPath.op(secondPath, PathKit.PathOp.INTERSECT);
- let simpleStr = combinedPath.toSVGString();
+ let simpleStr = firstPath.toSVGString();
let newSVG = document.createElementNS('http://www.w3.org/2000/svg', 'path');
newSVG.setAttribute('stroke', 'rgb(0,0,200)');
newSVG.setAttribute('fill', 'white');
newSVG.setAttribute('transform', 'scale(8,8)');
newSVG.setAttribute('d', simpleStr);
- document.getElementById('svg').appendChild(newSVG);
+ document.getElementById('svg1').appendChild(newSVG);
// Draw directly to Canvas
let ctx = document.getElementById('canvas1').getContext('2d');
@@ -82,11 +92,11 @@
ctx.fillStyle = 'white';
ctx.scale(8, 8);
ctx.beginPath();
- combinedPath.toCanvas(ctx);
+ firstPath.toCanvas(ctx);
ctx.stroke();
// create Path2D object and use it in a Canvas.
- let path2D = combinedPath.toPath2D();
+ let path2D = firstPath.toPath2D();
ctx = document.getElementById('canvas2').getContext('2d');
setCanvasSize(ctx, 200, 200);
ctx.canvas.width = 200
@@ -100,67 +110,66 @@
// See http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/embind.html?highlight=memory#memory-management
firstPath.delete();
secondPath.delete();
- combinedPath.delete();
}
function Path2DExample(PathKit) {
let objs = [new Path2D(), PathKit.NewPath(), new Path2D(), PathKit.NewPath()];
let canvases = [
- document.getElementById('canvas3').getContext('2d'),
- document.getElementById('canvas4').getContext('2d')
+ document.getElementById('canvas3').getContext('2d'),
+ document.getElementById('canvas4').getContext('2d')
];
for (i = 0; i <= 1; i++) {
- let path = objs[i];
-
- path.moveTo(20, 5);
- path.lineTo(30, 20);
- path.lineTo(40, 10);
- path.lineTo(50, 20);
- path.lineTo(60, 0);
- path.lineTo(20, 5);
-
- path.moveTo(20, 80);
- path.bezierCurveTo(90, 10, 160, 150, 190, 10);
-
- path.moveTo(36, 148);
- path.quadraticCurveTo(66, 188, 120, 136);
- path.lineTo(36, 148);
-
- path.rect(5, 170, 20, 20);
-
- path.moveTo(150, 180);
- path.arcTo(150, 100, 50, 200, 20);
- path.lineTo(160, 160);
-
- path.moveTo(20, 120);
- path.arc(20, 120, 18, 0, 1.75 * Math.PI);
- path.lineTo(20, 120);
-
- let secondPath = objs[i+2];
- secondPath.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI, false);
-
- path.addPath(secondPath);
-
- let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
- m.a = 1; m.b = 0;
- m.c = 0; m.d = 1;
- m.e = 0; m.f = 20.5;
-
- path.addPath(secondPath, m);
- // With PathKit, one can also just provide the 6 params as floats, to avoid
- // the overhead of making an SVGMatrix
- // path.addPath(secondPath, 1, 0, 0, 1, 0, 20.5);
-
- canvasCtx = canvases[i];
- canvasCtx.fillStyle = 'blue';
- setCanvasSize(canvasCtx, 300, 300);
- canvasCtx.scale(1.5, 1.5);
- if (path.toPath2D) {
- canvasCtx.stroke(path.toPath2D());
- } else {
- canvasCtx.stroke(path);
- }
+ let path = objs[i];
+
+ path.moveTo(20, 5);
+ path.lineTo(30, 20);
+ path.lineTo(40, 10);
+ path.lineTo(50, 20);
+ path.lineTo(60, 0);
+ path.lineTo(20, 5);
+
+ path.moveTo(20, 80);
+ path.bezierCurveTo(90, 10, 160, 150, 190, 10);
+
+ path.moveTo(36, 148);
+ path.quadraticCurveTo(66, 188, 120, 136);
+ path.lineTo(36, 148);
+
+ path.rect(5, 170, 20, 20);
+
+ path.moveTo(150, 180);
+ path.arcTo(150, 100, 50, 200, 20);
+ path.lineTo(160, 160);
+
+ path.moveTo(20, 120);
+ path.arc(20, 120, 18, 0, 1.75 * Math.PI);
+ path.lineTo(20, 120);
+
+ let secondPath = objs[i+2];
+ secondPath.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI, false);
+
+ path.addPath(secondPath);
+
+ let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
+ m.a = 1; m.b = 0;
+ m.c = 0; m.d = 1;
+ m.e = 0; m.f = 20.5;
+
+ path.addPath(secondPath, m);
+ // With PathKit, one can also just provide the 6 params as floats, to avoid
+ // the overhead of making an SVGMatrix
+ // path.addPath(secondPath, 1, 0, 0, 1, 0, 20.5);
+
+ canvasCtx = canvases[i];
+ canvasCtx.fillStyle = 'blue';
+ setCanvasSize(canvasCtx, 300, 300);
+ canvasCtx.scale(1.5, 1.5);
+ if (path.toPath2D) {
+ canvasCtx.stroke(path.toPath2D());
+ } else {
+ canvasCtx.stroke(path);
+ }
}
@@ -172,68 +181,141 @@
let R = 115.2, C = 128.0;
path.moveTo(C + R + 22, C);
for (let i = 1; i < 8; i++) {
- let a = 2.6927937 * i;
- path.lineTo(C + R * Math.cos(a) + 22, C + R * Math.sin(a));
+ let a = 2.6927937 * i;
+ path.lineTo(C + R * Math.cos(a) + 22, C + R * Math.sin(a));
}
path.closePath();
return path;
}
function PathEffectsExample(PathKit) {
- let transforms = [
- // no-op
- (path) => path,
- // dash
- (path) => path.dash(10, 3, 0),
- // trim (takes optional 3rd param for returning the trimmed part
- // or the complement)
- (path) => path.trim(0.25, 0.8, false),
- // simplify
- (path) => path.simplify(),
- // stroke
- (path) => path.stroke(15, PathKit.StrokeJoin.BEVEL, PathKit.StrokeCap.BUTT),
- // "offset effect", that is, making a border around the shape.
- (path) => {
- let temp = path.stroke(10, PathKit.StrokeJoin.ROUND, PathKit.StrokeCap.SQUARE);
- let ret = temp.op(path, PathKit.PathOp.DIFFERENCE);
- temp.delete();
- return ret;
- }
+ let effects = [
+ // no-op
+ (path) => path,
+ // dash
+ (path) => path.dash(10, 3, 0),
+ // trim (takes optional 3rd param for returning the trimmed part
+ // or the complement)
+ (path) => path.trim(0.25, 0.8, false),
+ // simplify
+ (path) => path.simplify(),
+ // stroke
+ (path) => path.stroke({
+ width: 15,
+ join: PathKit.StrokeJoin.BEVEL,
+ cap: PathKit.StrokeCap.BUTT,
+ miter_limit: 1,
+ }),
+ // "offset effect", that is, making a border around the shape.
+ (path) => {
+ let orig = path.copy();
+ path.stroke({
+ width: 10,
+ join: PathKit.StrokeJoin.ROUND,
+ cap: PathKit.StrokeCap.SQUARE,
+ })
+ .op(orig, PathKit.PathOp.DIFFERENCE);
+ orig.delete();
+ }
];
let names = ["(plain)", "Dash", "Trim", "Simplify", "Stroke", "Offset"];
- for (let i = 0; i < transforms.length; i++) {
- let path = PathKit.NewPath();
- drawStar(path);
-
- let transformedPath = transforms[i](path);
-
- let ctx = document.getElementById(`canvas${i+5}`).getContext('2d');
- setCanvasSize(ctx, 300, 300);
- ctx.strokeStyle = '#3c597a';
- ctx.fillStyle = '#3c597a';
- if (i === 4 || i === 5) {
- ctx.fill(transformedPath.toPath2D(), transformedPath.getCanvasFillType());
- if (i === 5) {
- ctx.fillStyle = "#51d9bb";
- ctx.fill(path.toPath2D());
- }
- } else {
- ctx.stroke(transformedPath.toPath2D());
- }
-
- ctx.font = '42px monospace';
-
- let x = 150-ctx.measureText(names[i]).width/2;
- ctx.strokeText(names[i], x, 290);
-
- if (i) {
- transformedPath.delete();
- }
- path.delete();
+ for (let i = 0; i < effects.length; i++) {
+ let path = PathKit.NewPath();
+ drawStar(path);
+
+ // The transforms apply directly to the path.
+ effects[i](path);
+
+ let ctx = document.getElementById(`canvas${i+5}`).getContext('2d');
+ setCanvasSize(ctx, 300, 300);
+ ctx.strokeStyle = '#3c597a';
+ ctx.fillStyle = '#3c597a';
+ if (i === 4 || i === 5) {
+ ctx.fill(path.toPath2D(), path.getFillTypeString());
+ } else {
+ ctx.stroke(path.toPath2D());
+ }
+
+ ctx.font = '42px monospace';
+
+ let x = 150-ctx.measureText(names[i]).width/2;
+ ctx.strokeText(names[i], x, 290);
+
+ path.delete();
}
+ }
+
+ function MatrixTransformExample(PathKit) {
+ // Creates an animated star that twists and moves.
+ let ctx = document.getElementById('canvasTransform').getContext('2d');
+ setCanvasSize(ctx, 300, 300);
+ ctx.strokeStyle = '#3c597a';
+
+ let path = drawStar(PathKit.NewPath());
+ // TODO(kjlubick): Perhaps expose some matrix helper functions to allow
+ // clients to build their own matrices like this?
+ // These matrices represent a 2 degree rotation and a 1% scale factor.
+ let scaleUp = [1.0094, -0.0352, 3.1041,
+ 0.0352, 1.0094, -6.4885,
+ 0 , 0 , 1];
+
+ let scaleDown = [ 0.9895, 0.0346, -2.8473,
+ -0.0346, 0.9895, 6.5276,
+ 0 , 0 , 1];
+
+ let i = 0;
+ function frame(){
+ i++;
+ if (Math.round(i/100) % 2) {
+ path.transform(scaleDown);
+ } else {
+ path.transform(scaleUp);
+ }
+
+ ctx.clearRect(0, 0, 300, 300);
+ ctx.stroke(path.toPath2D());
+
+ ctx.font = '42px monospace';
+ let x = 150-ctx.measureText('Transform').width/2;
+ ctx.strokeText('Transform', x, 290);
+
+ window.requestAnimationFrame(frame);
+ }
+ window.requestAnimationFrame(frame);
+ }
+ function FilledSVGExample(PathKit) {
+ let innerRect = PathKit.NewPath();
+ innerRect.rect(80, 100, 40, 40);
+
+ let outerRect = PathKit.NewPath();
+ outerRect.rect(50, 10, 100, 100)
+ .op(innerRect, PathKit.PathOp.XOR);
+
+ let str = outerRect.toSVGString();
+
+ let diffSVG = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ diffSVG.setAttribute('stroke', 'red');
+ diffSVG.setAttribute('fill', 'black');
+ // force fill-rule to nonzero to demonstrate difference
+ diffSVG.setAttribute('fill-rule', 'nonzero');
+ diffSVG.setAttribute('d', str);
+ document.getElementById('svg2').appendChild(diffSVG);
+
+ let unionSVG = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ unionSVG.setAttribute('stroke', 'red');
+ unionSVG.setAttribute('fill', 'black');
+ // ask what the path thinks fill-rule should be ('evenodd')
+ // SVG and Canvas both use the same keys ('nonzero' and 'evenodd') and both
+ // default to 'nonzero', so one call supports both.
+ unionSVG.setAttribute('fill-rule', outerRect.getFillTypeString());
+ unionSVG.setAttribute('d', str);
+ document.getElementById('svg3').appendChild(unionSVG);
+
+ outerRect.delete();
+ innerRect.delete();
}
</script>
diff --git a/experimental/pathkit/npm-wasm/package.json b/experimental/pathkit/npm-wasm/package.json
index b3e42b439b..c9516250dd 100644
--- a/experimental/pathkit/npm-wasm/package.json
+++ b/experimental/pathkit/npm-wasm/package.json
@@ -1,6 +1,6 @@
{
"name": "experimental-pathkit-wasm",
- "version": "0.1.7",
+ "version": "0.2.0",
"description": "A WASM version of Skia's PathOps toolkit",
"main": "bin/pathkit.js",
"homepage": "https://github.com/google/skia/tree/master/experimental/pathkit",
diff --git a/experimental/pathkit/package.json b/experimental/pathkit/package.json
index 44a344faf4..731afd4ef6 100644
--- a/experimental/pathkit/package.json
+++ b/experimental/pathkit/package.json
@@ -5,7 +5,7 @@
"private": true,
"main": "index.js",
"dependencies": {
- "experimental-pathkit-wasm": "^0.1.2"
+ "experimental-pathkit-wasm": "^0.2.0"
},
"devDependencies": {
"jasmine-core": "^3.1.0",
diff --git a/experimental/pathkit/pathkit_wasm_bindings.cpp b/experimental/pathkit/pathkit_wasm_bindings.cpp
index 4e84c96e97..db540d195b 100644
--- a/experimental/pathkit/pathkit_wasm_bindings.cpp
+++ b/experimental/pathkit/pathkit_wasm_bindings.cpp
@@ -11,9 +11,11 @@
#include "SkMatrix.h"
#include "SkPaint.h"
#include "SkParsePath.h"
+#include "SkStrokeRec.h"
#include "SkPath.h"
#include "SkPathOps.h"
#include "SkRect.h"
+#include "SkPaintDefaults.h"
#include "SkString.h"
#include "SkTrimPathEffect.h"
@@ -163,6 +165,49 @@ bool EMSCRIPTEN_KEEPALIVE Equals(const SkPath& a, const SkPath& b) {
}
//========================================================================================
+// Path things
+//========================================================================================
+
+// All these Apply* methods are simple wrappers to avoid returning an object.
+// The default WASM bindings produce code that will leak if a return value
+// isn't assigned to a JS variable and has delete() called on it.
+// These Apply methods, combined with the smarter binding code allow for chainable
+// commands that don't leak if the return value is ignored (i.e. when used intuitively).
+
+void ApplyArcTo(SkPath& p, SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2,
+ SkScalar radius) {
+ p.arcTo(x1, y1, x2, y2, radius);
+}
+
+void ApplyClose(SkPath& p) {
+ p.close();
+}
+
+void ApplyConicTo(SkPath& p, SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2,
+ SkScalar w) {
+ p.conicTo(x1, y1, x2, y2, w);
+}
+
+void ApplyCubicTo(SkPath& p, SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2,
+ SkScalar x3, SkScalar y3) {
+ p.cubicTo(x1, y1, x2, y2, x3, y3);
+}
+
+void ApplyLineTo(SkPath& p, SkScalar x, SkScalar y) {
+ p.lineTo(x, y);
+}
+
+void ApplyMoveTo(SkPath& p, SkScalar x, SkScalar y) {
+ p.moveTo(x, y);
+}
+
+void ApplyQuadTo(SkPath& p, SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2) {
+ p.quadTo(x1, y1, x2, y2);
+}
+
+
+
+//========================================================================================
// SVG things
//========================================================================================
@@ -189,20 +234,12 @@ SkPathOrNull EMSCRIPTEN_KEEPALIVE FromSVGString(std::string str) {
// PATHOP things
//========================================================================================
-SkPathOrNull EMSCRIPTEN_KEEPALIVE SimplifyPath(const SkPath& path) {
- SkPath simple;
- if (Simplify(path, &simple)) {
- return emscripten::val(simple);
- }
- return emscripten::val::null();
+bool EMSCRIPTEN_KEEPALIVE ApplySimplify(SkPath& path) {
+ return Simplify(path, &path);
}
-SkPathOrNull EMSCRIPTEN_KEEPALIVE ApplyPathOp(const SkPath& pathOne, const SkPath& pathTwo, SkPathOp op) {
- SkPath path;
- if (Op(pathOne, pathTwo, op, &path)) {
- return emscripten::val(path);
- }
- return emscripten::val::null();
+bool EMSCRIPTEN_KEEPALIVE ApplyPathOp(SkPath& pathOne, const SkPath& pathTwo, SkPathOp op) {
+ return Op(pathOne, pathTwo, op, &pathOne);
}
SkPathOrNull EMSCRIPTEN_KEEPALIVE ResolveBuilder(SkOpBuilder& builder) {
@@ -263,12 +300,12 @@ emscripten::val EMSCRIPTEN_KEEPALIVE ToPath2D(const SkPath& path) {
// ======================================================================================
// Path2D API things
// ======================================================================================
-void Path2DAddRect(SkPath& path, SkScalar x, SkScalar y, SkScalar width, SkScalar height) {
+void ApplyAddRect(SkPath& path, SkScalar x, SkScalar y, SkScalar width, SkScalar height) {
path.addRect(x, y, x+width, y+height);
}
-void Path2DAddArc(SkPath& path, SkScalar x, SkScalar y, SkScalar radius,
- SkScalar startAngle, SkScalar endAngle, bool ccw) {
+void ApplyAddArc(SkPath& path, SkScalar x, SkScalar y, SkScalar radius,
+ SkScalar startAngle, SkScalar endAngle, bool ccw) {
SkPath temp;
SkRect bounds = SkRect::MakeLTRB(x-radius, y-radius, x+radius, y+radius);
const auto sweep = SkRadiansToDegrees(endAngle - startAngle) - 360 * ccw;
@@ -276,12 +313,7 @@ void Path2DAddArc(SkPath& path, SkScalar x, SkScalar y, SkScalar radius,
path.addPath(temp, SkPath::kExtend_AddPathMode);
}
-void Path2DAddArc(SkPath& path, SkScalar x, SkScalar y, SkScalar radius,
- SkScalar startAngle, SkScalar endAngle) {
- Path2DAddArc(path, x, y, radius, startAngle, endAngle, false);
-}
-
-void Path2DAddEllipse(SkPath& path, SkScalar x, SkScalar y, SkScalar radiusX, SkScalar radiusY,
+void ApplyEllipse(SkPath& path, SkScalar x, SkScalar y, SkScalar radiusX, SkScalar radiusY,
SkScalar rotation, SkScalar startAngle, SkScalar endAngle, bool ccw) {
// This is easiest to do by making a new path and then extending the current path
// (this properly catches the cases of if there's a moveTo before this call or not).
@@ -295,35 +327,8 @@ void Path2DAddEllipse(SkPath& path, SkScalar x, SkScalar y, SkScalar radiusX, Sk
path.addPath(temp, m, SkPath::kExtend_AddPathMode);
}
-void Path2DAddEllipse(SkPath& path, SkScalar x, SkScalar y, SkScalar radiusX, SkScalar radiusY,
- SkScalar rotation, SkScalar startAngle, SkScalar endAngle) {
- Path2DAddEllipse(path, x, y, radiusX, radiusY, rotation, startAngle, endAngle, false);
-}
-
-void Path2DAddPath(SkPath& orig, const SkPath& newPath) {
- orig.addPath(newPath);
-}
-
-void Path2DAddPath(SkPath& orig, const SkPath& newPath, emscripten::val /* SVGMatrix*/ t) {
- SkMatrix m = SkMatrix::MakeAll(
- t["a"].as<SkScalar>(), t["c"].as<SkScalar>(), t["e"].as<SkScalar>(),
- t["b"].as<SkScalar>(), t["d"].as<SkScalar>(), t["f"].as<SkScalar>(),
- 0 , 0 , 1);
- orig.addPath(newPath, m);
-}
-
-// Mimics the order of SVGMatrix, just w/o the SVG Matrix
-// This order is scaleX, skewY, skewX, scaleY, transX, transY
-// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#Transform_functions
-void Path2DAddPath(SkPath& orig, const SkPath& newPath, SkScalar a, SkScalar b, SkScalar c, SkScalar d, SkScalar e, SkScalar f) {
- SkMatrix m = SkMatrix::MakeAll(a, c, e,
- b, d, f,
- 0, 0, 1);
- orig.addPath(newPath, m);
-}
-
// Allows for full matix control.
-void Path2DAddPath(SkPath& orig, const SkPath& newPath,
+void ApplyAddPath(SkPath& orig, const SkPath& newPath,
SkScalar scaleX, SkScalar skewX, SkScalar transX,
SkScalar skewY, SkScalar scaleY, SkScalar transY,
SkScalar pers0, SkScalar pers1, SkScalar pers2) {
@@ -348,53 +353,55 @@ JSString GetFillTypeString(const SkPath& path) {
// Path Effects
//========================================================================================
-SkPathOrNull PathEffectDash(const SkPath& path, SkScalar on, SkScalar off, SkScalar phase) {
- SkPath output;
+bool ApplyDash(SkPath& path, SkScalar on, SkScalar off, SkScalar phase) {
SkScalar intervals[] = { on, off };
auto pe = SkDashPathEffect::Make(intervals, 2, phase);
if (!pe) {
SkDebugf("Invalid args to dash()\n");
- return emscripten::val::null();
+ return false;
}
- if (pe->filterPath(&output, path, nullptr, nullptr)) {
- return emscripten::val(output);
+ SkStrokeRec rec(SkStrokeRec::InitStyle::kHairline_InitStyle);
+ if (pe->filterPath(&path, path, &rec, nullptr)) {
+ return true;
}
SkDebugf("Could not make dashed path\n");
- return emscripten::val::null();
+ return false;
}
-SkPathOrNull PathEffectTrim(const SkPath& path, SkScalar startT, SkScalar stopT, bool isComplement) {
- SkPath output;
+bool ApplyTrim(SkPath& path, SkScalar startT, SkScalar stopT, bool isComplement) {
auto mode = isComplement ? SkTrimPathEffect::Mode::kInverted : SkTrimPathEffect::Mode::kNormal;
auto pe = SkTrimPathEffect::Make(startT, stopT, mode);
if (!pe) {
SkDebugf("Invalid args to trim(): startT and stopT must be in [0,1]\n");
- return emscripten::val::null();
+ return false;
}
- if (pe->filterPath(&output, path, nullptr, nullptr)) {
- return emscripten::val(output);
+ SkStrokeRec rec(SkStrokeRec::InitStyle::kHairline_InitStyle);
+ if (pe->filterPath(&path, path, &rec, nullptr)) {
+ return true;
}
SkDebugf("Could not trim path\n");
- return emscripten::val::null();
+ return false;
}
-SkPathOrNull PathEffectTrim(const SkPath& path, SkScalar startT, SkScalar stopT) {
- return PathEffectTrim(path, startT, stopT, false);
-}
+struct StrokeOpts {
+ // Default values are set in chaining.js which allows clients
+ // to set any number of them. Otherwise, the binding code complains if
+ // any are omitted.
+ SkScalar width;
+ SkScalar miter_limit;
+ SkPaint::Join join;
+ SkPaint::Cap cap;
+};
-SkPathOrNull PathEffectStroke(const SkPath& path, SkScalar width, SkPaint::Join join, SkPaint::Cap cap) {
- SkPath output;
+bool ApplyStroke(SkPath& path, StrokeOpts opts) {
SkPaint p;
p.setStyle(SkPaint::kStroke_Style);
- p.setStrokeCap(cap);
- p.setStrokeJoin(join);
- p.setStrokeWidth(width);
+ p.setStrokeCap(opts.cap);
+ p.setStrokeJoin(opts.join);
+ p.setStrokeWidth(opts.width);
+ p.setStrokeMiter(opts.miter_limit);
- if (p.getFillPath(path, &output)) {
- return emscripten::val(output);
- }
- SkDebugf("Could not stroke path\n");
- return emscripten::val::null();
+ return p.getFillPath(path, &path);
}
//========================================================================================
@@ -413,22 +420,18 @@ SkMatrix toSkMatrix(const SimpleMatrix& sm) {
sm.pers0 , sm.pers1 , sm.pers2);
}
-SkPathOrNull PathTransform(const SkPath& orig, const SimpleMatrix& sm) {
- SkPath output;
- orig.transform(toSkMatrix(sm), &output);
- return emscripten::val(output);
+void ApplyTransform(SkPath& orig, const SimpleMatrix& sm) {
+ orig.transform(toSkMatrix(sm));
}
-SkPathOrNull PathTransform(const SkPath& orig,
- SkScalar scaleX, SkScalar skewX, SkScalar transX,
- SkScalar skewY, SkScalar scaleY, SkScalar transY,
- SkScalar pers0, SkScalar pers1, SkScalar pers2) {
+void ApplyTransform(SkPath& orig,
+ SkScalar scaleX, SkScalar skewX, SkScalar transX,
+ SkScalar skewY, SkScalar scaleY, SkScalar transY,
+ SkScalar pers0, SkScalar pers1, SkScalar pers2) {
SkMatrix m = SkMatrix::MakeAll(scaleX, skewX , transX,
skewY , scaleY, transY,
pers0 , pers1 , pers2);
- SkPath output;
- orig.transform(m, &output);
- return emscripten::val(output);
+ orig.transform(m);
}
//========================================================================================
@@ -453,49 +456,34 @@ float SkBits2FloatUnsigned(uint32_t floatAsBits) {
//
// An important detail for binding non-member functions is that the first argument
// must be SkPath& (the reference part is very important).
+//
+// Note that we can't expose default or optional arguments, but we can have multiple
+// declarations of the same function that take different amounts of arguments.
+// For example, see _transform
+// Additionally, we are perfectly happy to handle default arguments and function
+// overloads in the JS glue code (see chaining.js::addPath() for an example).
EMSCRIPTEN_BINDINGS(skia) {
class_<SkPath>("SkPath")
.constructor<>()
.constructor<const SkPath&>()
// Path2D API
- .function("addPath",
- select_overload<void(SkPath&, const SkPath&)>(&Path2DAddPath))
- .function("addPath",
- select_overload<void(SkPath&, const SkPath&, emscripten::val)>(&Path2DAddPath))
- .function("arc",
- select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddArc))
- .function("arc",
- select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, bool)>(&Path2DAddArc))
- .function("arcTo",
- select_overload<SkPath&(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::arcTo))
- .function("bezierCurveTo",
- select_overload<SkPath&(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::cubicTo))
- .function("closePath", &SkPath::close)
- .function("ellipse",
- select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddEllipse))
- .function("ellipse",
- select_overload<void(SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, bool)>(&Path2DAddEllipse))
- .function("lineTo",
- select_overload<SkPath&(SkScalar, SkScalar)>(&SkPath::lineTo))
- .function("moveTo",
- select_overload<SkPath&(SkScalar, SkScalar)>(&SkPath::moveTo))
- .function("quadraticCurveTo",
- select_overload<SkPath&(SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::quadTo))
- .function("rect", &Path2DAddRect)
-
- // Some shorthand helpers, to mirror SkPath.cpp's API
- .function("addPath",
- select_overload<void(SkPath&, const SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddPath))
- .function("addPath",
- select_overload<void(SkPath&, const SkPath&, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&Path2DAddPath))
- .function("close", &SkPath::close)
- .function("conicTo",
- select_overload<SkPath&(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::conicTo))
- .function("cubicTo",
- select_overload<SkPath&(SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::cubicTo))
- .function("quadTo",
- select_overload<SkPath&(SkScalar, SkScalar, SkScalar, SkScalar)>(&SkPath::quadTo))
+ .function("_addPath", &ApplyAddPath)
+ // 3 additional overloads of addPath are handled in JS bindings
+ .function("_arc", &ApplyAddArc)
+ .function("_arcTo", &ApplyArcTo)
+ //"bezierCurveTo" alias handled in JS bindings
+ .function("_close", &ApplyClose)
+ .function("_conicTo", &ApplyConicTo)
+ .function("_cubicTo", &ApplyCubicTo)
+ //"closePath" alias handled in JS bindings
+
+ .function("_ellipse", &ApplyEllipse)
+ .function("_lineTo", &ApplyLineTo)
+ .function("_moveTo", &ApplyMoveTo)
+ // "quadraticCurveTo" alias handled in JS bindings
+ .function("_quadTo", &ApplyQuadTo)
+ .function("_rect", &ApplyAddRect)
// Extra features
.function("setFillType", &SkPath::setFillType)
@@ -507,18 +495,17 @@ EMSCRIPTEN_BINDINGS(skia) {
.function("copy", &CopyPath)
// PathEffects
- .function("dash", &PathEffectDash)
- .function("trim", select_overload<SkPathOrNull(const SkPath&, SkScalar, SkScalar)>(&PathEffectTrim))
- .function("trim", select_overload<SkPathOrNull(const SkPath&, SkScalar, SkScalar, bool)>(&PathEffectTrim))
- .function("stroke", &PathEffectStroke)
+ .function("_dash", &ApplyDash)
+ .function("_trim", &ApplyTrim)
+ .function("_stroke", &ApplyStroke)
// Matrix
- .function("transform", select_overload<SkPathOrNull(const SkPath& orig, const SimpleMatrix& sm)>(&PathTransform))
- .function("transform", select_overload<SkPathOrNull(const SkPath& orig, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&PathTransform))
+ .function("_transform", select_overload<void(SkPath& orig, const SimpleMatrix& sm)>(&ApplyTransform))
+ .function("_transform", select_overload<void(SkPath& orig, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar, SkScalar)>(&ApplyTransform))
// PathOps
- .function("simplify", &SimplifyPath)
- .function("op", &ApplyPathOp)
+ .function("_simplify", &ApplySimplify)
+ .function("_op", &ApplyPathOp)
// Exporting
.function("toCmds", &ToCmds)
@@ -593,6 +580,11 @@ EMSCRIPTEN_BINDINGS(skia) {
.value("ROUND", SkPaint::Cap::kRound_Cap)
.value("SQUARE", SkPaint::Cap::kSquare_Cap);
+ value_object<StrokeOpts>("StrokeOpts")
+ .field("width", &StrokeOpts::width)
+ .field("miter_limit", &StrokeOpts::miter_limit)
+ .field("join", &StrokeOpts::join)
+ .field("cap", &StrokeOpts::cap);
// Matrix
// Allows clients to supply a 1D array of 9 elements and the bindings
diff --git a/experimental/pathkit/tests/effects.spec.js b/experimental/pathkit/tests/effects.spec.js
new file mode 100644
index 0000000000..a332c6e962
--- /dev/null
+++ b/experimental/pathkit/tests/effects.spec.js
@@ -0,0 +1,136 @@
+
+describe('PathKit\'s Path Behavior', function() {
+ // Note, don't try to print the PathKit object - it can cause Karma/Jasmine to lock up.
+ var PathKit = null;
+ const LoadPathKit = new Promise(function(resolve, reject) {
+ if (PathKit) {
+ resolve();
+ } else {
+ PathKitInit({
+ locateFile: (file) => '/pathkit/'+file,
+ }).then((_PathKit) => {
+ PathKit = _PathKit;
+ resolve();
+ });
+ }
+ });
+
+ // see https://fiddle.skia.org/c/@discrete_path
+ function drawStar() {
+ let path = PathKit.NewPath();
+ let R = 115.2, C = 128.0;
+ path.moveTo(C + R + 22, C);
+ for (let i = 1; i < 8; i++) {
+ let a = 2.6927937 * i;
+ path.lineTo(C + R * Math.cos(a) + 22, C + R * Math.sin(a));
+ }
+ path.closePath();
+ return path;
+ }
+
+ describe('Dash Path Effect', function() {
+ it('performs dash in-place with start, stop, phase', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawStar();
+ let dashed = drawStar();
+ let notACopy = dashed.dash(10, 3, 0);
+ let phased = drawStar().dash(10, 3, 2);
+
+ expect(dashed === notACopy).toBe(true);
+ expect(dashed.equals(phased)).toBe(false);
+ expect(dashed.equals(orig)).toBe(false);
+ // TODO(store to Gold), dashed_no_phase, dashed_with_phase
+
+ orig.delete();
+ dashed.delete();
+ phased.delete();
+ done();
+ });
+ });
+ });
+
+ describe('Trim Path Effect', function() {
+ it('performs trim in-place with start, stop, phase', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawStar();
+ let trimmed = drawStar();
+ let notACopy = trimmed.trim(0.25, .8);
+ let complement = drawStar().trim(.1, .9, true);
+
+ expect(trimmed === notACopy).toBe(true);
+ expect(trimmed.equals(complement)).toBe(false);
+ expect(trimmed.equals(orig)).toBe(false);
+ expect(complement.equals(orig)).toBe(false);
+
+ // TODO(store to Gold), trimmed, trimmed_complement
+
+ orig.delete();
+ trimmed.delete();
+ complement.delete();
+ done();
+ });
+ });
+ });
+
+ describe('Transform Path Effect', function() {
+ it('performs matrix transform in-place', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawStar();
+ let scaled = drawStar();
+ let notACopy = scaled.transform(3, 0, 0,
+ 0, 3, 0,
+ 0, 0, 1);
+
+ let scaled2 = drawStar().transform([3, 0, 0,
+ 0, 3, 0,
+ 0, 0, 1]);
+
+ expect(scaled === notACopy).toBe(true);
+ expect(scaled.equals(scaled2)).toBe(true);
+ expect(scaled.equals(orig)).toBe(false);
+
+ // TODO(store to Gold), transformed, transformed_more
+
+ orig.delete();
+ scaled.delete();
+ scaled2.delete();
+ done();
+ });
+ });
+ });
+
+ describe('Stroke Path Effect', function() {
+ it('creates a stroked path in-place', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawStar();
+ let stroked = drawStar();
+ let notACopy = stroked.stroke({
+ width: 15,
+ join: PathKit.StrokeJoin.BEVEL,
+ cap: PathKit.StrokeCap.BUTT,
+ miter_limit: 2,
+ });
+
+ // Don't have to specify all of the fields, defaults will
+ // be used instead.
+ let rounded = drawStar().stroke({
+ width: 10,
+ join: PathKit.StrokeJoin.ROUND,
+ cap:PathKit.StrokeCap.SQUARE,
+ });
+
+ expect(stroked === notACopy).toBe(true);
+ expect(stroked.equals(rounded)).toBe(false);
+ expect(stroked.equals(orig)).toBe(false);
+
+ // TODO(store to Gold), stroked, rounded
+
+ orig.delete();
+ stroked.delete();
+ rounded.delete();
+ done();
+ });
+ });
+ });
+
+});
diff --git a/experimental/pathkit/tests/path.spec.js b/experimental/pathkit/tests/path.spec.js
index 0cafc384cf..3c55dfac83 100644
--- a/experimental/pathkit/tests/path.spec.js
+++ b/experimental/pathkit/tests/path.spec.js
@@ -15,63 +15,143 @@ describe('PathKit\'s Path Behavior', function() {
}
});
- it('dynamically updates getBounds()', function(done){
- LoadPathKit.then(() => {
- // Based on test_bounds_crbug_513799
+ describe('Basic Path Features', function() {
+ function drawSimplePath() {
let path = PathKit.NewPath();
- expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(0, 0, 0, 0));
- path.moveTo(-5, -8);
- expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, -5, -8));
- path.rect(1, 2, 2, 2);
- expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, 3, 4));
- path.moveTo(1, 2);
- expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, 3, 4));
- path.delete();
- done();
+ path.moveTo(0, 0);
+ path.lineTo(10, 0);
+ path.lineTo(10, 10);
+ path.close();
+ return path;
+ }
+
+ it('supports.equals()', function(done) {
+ LoadPathKit.then(() => {
+ let path = drawSimplePath();
+ let otherPath = drawSimplePath();
+ let blank = PathKit.NewPath();
+
+ expect(path.equals(path)).toBe(true);
+ expect(otherPath.equals(path)).toBe(true);
+ expect(path.equals(otherPath)).toBe(true);
+
+ expect(path.equals(blank)).toBe(false);
+ expect(otherPath.equals(blank)).toBe(false);
+ expect(blank.equals(path)).toBe(false);
+ expect(blank.equals(otherPath)).toBe(false);
+
+ path.delete();
+ otherPath.delete();
+ blank.delete();
+ done();
+ });
+ });
+
+ it('has a copy constructor', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawSimplePath();
+ let copy = new PathKit.SkPath(orig);
+
+ expect(orig.toSVGString()).toEqual(copy.toSVGString());
+ expect(orig.equals(copy)).toBe(true);
+
+ orig.delete();
+ copy.delete();
+ done();
+ });
+ });
+
+ it('has a copy method', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawSimplePath();
+ let copy = orig.copy();
+
+ expect(orig.toSVGString()).toEqual(copy.toSVGString());
+ expect(orig.equals(copy)).toBe(true);
+
+ orig.delete();
+ copy.delete();
+ done();
+ });
+ });
+
+ it('can create a copy with MakePath', function(done) {
+ LoadPathKit.then(() => {
+ let orig = drawSimplePath();
+ let copy = PathKit.NewPath(orig);
+
+ expect(orig.toSVGString()).toEqual(copy.toSVGString());
+ expect(orig.equals(copy)).toBe(true);
+
+ orig.delete();
+ copy.delete();
+ done();
+ });
});
});
+
function bits2float(str) {
return PathKit.SkBits2FloatUnsigned(parseInt(str))
}
- it('has getBounds() and computeTightBounds()', function(done){
- LoadPathKit.then(() => {
- // Based on PathOpsTightBoundsIllBehaved
- let path = PathKit.NewPath();
- path.moveTo(1, 1);
- path.quadraticCurveTo(4, 3, 2, 2);
- expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(1, 1, 4, 3));
- expect(path.computeTightBounds()).toEqual(PathKit.MakeLTRBRect(1, 1,
- bits2float("0x40333334"), // 2.8
- bits2float("0x40155556"))); // 2.3333333
- path.delete();
-
- done();
+ describe('bounds and rect', function(){
+ it('dynamically updates getBounds()', function(done){
+ LoadPathKit.then(() => {
+ // Based on test_bounds_crbug_513799
+ let path = PathKit.NewPath();
+ expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(0, 0, 0, 0));
+ path.moveTo(-5, -8);
+ expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, -5, -8));
+ path.rect(1, 2, 2, 2);
+ expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, 3, 4));
+ path.moveTo(1, 2);
+ expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, 3, 4));
+ path.delete();
+ done();
+ });
+ });
+
+ it('has getBounds() and computeTightBounds()', function(done){
+ LoadPathKit.then(() => {
+ // Based on PathOpsTightBoundsIllBehaved
+ let path = PathKit.NewPath();
+ path.moveTo(1, 1);
+ path.quadraticCurveTo(4, 3, 2, 2);
+ expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(1, 1, 4, 3));
+ expect(path.computeTightBounds()).toEqual(PathKit.MakeLTRBRect(1, 1,
+ bits2float("0x40333334"), // 2.8
+ bits2float("0x40155556"))); // 2.3333333
+ path.delete();
+
+ done();
+ });
});
});
- it('does NOT approximates conics when dumping as toCmds', function(done){
- LoadPathKit.then(() => {
- let path = PathKit.NewPath();
- path.moveTo(20, 120);
- path.arc(20, 120, 18, 0, 1.75 * Math.PI);
- path.lineTo(20, 120);
-
- let expectedCmds = [
- [PathKit.MOVE_VERB, 20, 120],
- [PathKit.LINE_VERB, 38, 120],
- [PathKit.CONIC_VERB, 38, 138, 20, 138, bits2float("0x3f3504f3)")], // 0.707107f
- [PathKit.CONIC_VERB, 2, 138, 2, 120, bits2float("0x3f3504f3)")], // 0.707107f
- [PathKit.CONIC_VERB, 2, 102, 20, 102, bits2float("0x3f3504f3)")], // 0.707107f
- [PathKit.CONIC_VERB, bits2float("0x41dba58e"), 102, bits2float("0x4202e962"), bits2float("0x42d68b4d"), bits2float("0x3f6c8361")], // 27.4558, 102, 32.7279, 107.272, 0.92388
- [PathKit.LINE_VERB, 20, 120],
- ];
- let actual = path.toCmds();
- expect(actual).toEqual(expectedCmds);
-
- path.delete();
- done();
+ describe('Command arrays', function(){
+ it('does NOT approximates conics when dumping as toCmds', function(done){
+ LoadPathKit.then(() => {
+ let path = PathKit.NewPath();
+ path.moveTo(20, 120);
+ path.arc(20, 120, 18, 0, 1.75 * Math.PI);
+ path.lineTo(20, 120);
+
+ let expectedCmds = [
+ [PathKit.MOVE_VERB, 20, 120],
+ [PathKit.LINE_VERB, 38, 120],
+ [PathKit.CONIC_VERB, 38, 138, 20, 138, bits2float("0x3f3504f3)")], // 0.707107f
+ [PathKit.CONIC_VERB, 2, 138, 2, 120, bits2float("0x3f3504f3)")], // 0.707107f
+ [PathKit.CONIC_VERB, 2, 102, 20, 102, bits2float("0x3f3504f3)")], // 0.707107f
+ [PathKit.CONIC_VERB, bits2float("0x41dba58e"), 102, bits2float("0x4202e962"), bits2float("0x42d68b4d"), bits2float("0x3f6c8361")], // 27.4558, 102, 32.7279, 107.272, 0.92388
+ [PathKit.LINE_VERB, 20, 120],
+ ];
+ let actual = path.toCmds();
+ expect(actual).toEqual(expectedCmds);
+
+ path.delete();
+ done();
+ });
});
});
diff --git a/experimental/pathkit/tests/path2d.spec.js b/experimental/pathkit/tests/path2d.spec.js
index bbd76f3443..33fcec92ad 100644
--- a/experimental/pathkit/tests/path2d.spec.js
+++ b/experimental/pathkit/tests/path2d.spec.js
@@ -56,8 +56,6 @@ describe('PathKit\'s Path2D API', function() {
m.e = 0; m.f = 20.5;
path.addPath(secondPath, m);
- // An alternate API that avoids having to make the SVG matrix
- path.addPath(secondPath, 1, 0, 0, 1, 0, 20.5);
let canvas = document.createElement('canvas');
let canvasCtx = canvas.getContext('2d');
@@ -76,6 +74,81 @@ describe('PathKit\'s Path2D API', function() {
});
});
+ it('can chain by returning the same object', function(done) {
+ LoadPathKit.then(() => {
+ let path = PathKit.NewPath();
+
+ let p1 = path.moveTo(20, 5)
+ .lineTo(30, 20)
+ .quadTo(66, 188, 120, 136)
+ .close();
+
+ // these should be the same object
+ expect(path === p1).toBe(true);
+ p1.delete();
+ try {
+ // This should throw an exception because
+ // the underlying path was already deleted.
+ path.delete();
+ expect('should not have gotten here').toBe(false);
+ } catch (e) {
+ // all is well
+ }
+ done();
+ });
+ });
+
+ it('does not leak path objects when chaining', function(done) {
+ LoadPathKit.then(() => {
+ // By default, we have 16 MB of memory assigned to our PathKit
+ // library. This can be configured by -S TOTAL_MEMORY=NN
+ // and defaults to 16MB (we likely don't need to touch this).
+ // If there's a leak in here, we should OOM pretty quick.
+ // Testing showed around 50k is enough to see one if we leak a path,
+ // so run 250k times just to be safe.
+ for(let i = 0; i < 250000; i++) {
+ let path = PathKit.NewPath()
+ .moveTo(20, 5)
+ .lineTo(30, 20)
+ .quadTo(66, 188, 120, 136)
+ .close();
+ path.delete();
+ }
+ done();
+ });
+ });
+
+ function drawTriangle() {
+ let path = PathKit.NewPath();
+ path.moveTo(0, 0);
+ path.lineTo(10, 0);
+ path.lineTo(10, 10);
+ path.close();
+ return path;
+ }
+
+ it('has multiple overloads of addPath', function(done) {
+ LoadPathKit.then(() => {
+ let basePath = PathKit.NewPath();
+ let otherPath = drawTriangle();
+ // These add path call can be chained.
+ // add it unchanged
+ basePath.addPath(otherPath)
+ // providing the 6 params of an SVG matrix to make it appear 20.5 px down
+ .addPath(otherPath, 1, 0, 0, 1, 0, 20.5)
+ // provide the full 9 matrix params to make it appear 30 px to the right
+ // and be 3 times as big.
+ .addPath(otherPath, 3, 0, 30,
+ 0, 3, 0,
+ 0, 0, 1);
+
+ // TODO(kjlubick): Do svg and canvas gm helper function
+ basePath.delete();
+ otherPath.delete();
+ done();
+ });
+ });
+
it('approximates arcs (conics) with quads', function(done) {
LoadPathKit.then(() => {
let path = PathKit.NewPath();
diff --git a/experimental/pathkit/tests/pathops.spec.js b/experimental/pathkit/tests/pathops.spec.js
index a8f0376e9e..4b8c3ef5e9 100644
--- a/experimental/pathkit/tests/pathops.spec.js
+++ b/experimental/pathkit/tests/pathops.spec.js
@@ -169,7 +169,7 @@ describe('PathKit\'s PathOps Behavior', function() {
}
expected.delete();
}
- combined.delete();
+ // combined === path1, so we only have to delete one.
path1.delete();
path2.delete();
}
@@ -222,7 +222,7 @@ describe('PathKit\'s PathOps Behavior', function() {
}
expected.delete();
}
- simplified.delete();
+ // simplified === path, so we only have to delete one.
path.delete();
}
done();
diff --git a/experimental/pathkit/tests/svg.spec.js b/experimental/pathkit/tests/svg.spec.js
index 5d964cc7b4..c28618bf26 100644
--- a/experimental/pathkit/tests/svg.spec.js
+++ b/experimental/pathkit/tests/svg.spec.js
@@ -108,7 +108,6 @@ describe('PathKit\'s SVG Behavior', function() {
reportSVGString(svgStr, 'conics_quads_approx').then(() => {
done();
}).catch(reportError(done));
- done();
});
});
diff --git a/experimental/pathkit/tests/testReporter.js b/experimental/pathkit/tests/testReporter.js
index 81124d4a49..add663426b 100644
--- a/experimental/pathkit/tests/testReporter.js
+++ b/experimental/pathkit/tests/testReporter.js
@@ -26,7 +26,7 @@ function reportSVG(svg, testname) {
let b64 = tempCanvas.toDataURL('image/png');
_report(b64, 'svg', testname).then(() => {
resolve();
- });
+ }).catch((e) => reject(e));
};
tempImg.setAttribute('src', 'data:image/svg+xml;,' + svgStr);
});