[PathKit] Add cubicYFromX
Docs-Preview: https://skia.org/?cl=152385
Bug: skia:8216
Change-Id: I0020e8d2d4e6e7c7de5c71ddf923a618659cde2c
Reviewed-on: https://skia-review.googlesource.com/152385
Reviewed-by: Mike Reed <reed@google.com>
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
diff --git a/modules/pathkit/Makefile b/modules/pathkit/Makefile
index 7694cf1..b99b644 100644
--- a/modules/pathkit/Makefile
+++ b/modules/pathkit/Makefile
@@ -127,4 +127,4 @@
ln -s -T ../../npm-asmjs/bin/debug node_modules/pathkit-asmjs/bin
echo "Go check out localhost:8000/npm-wasm/example.html"
echo "or http://localhost:8000/npm-asmjs/example.html"
- python serve.py
\ No newline at end of file
+ python serve.py
diff --git a/modules/pathkit/compile.sh b/modules/pathkit/compile.sh
index 22b8a49..3d1f33a 100755
--- a/modules/pathkit/compile.sh
+++ b/modules/pathkit/compile.sh
@@ -101,6 +101,7 @@
$BASE_DIR/pathkit_wasm_bindings.cpp \
src/core/SkAnalyticEdge.cpp \
src/core/SkArenaAlloc.cpp \
+src/core/SkCubicMap.cpp \
src/core/SkEdge.cpp \
src/core/SkEdgeBuilder.cpp \
src/core/SkEdgeClipper.cpp \
diff --git a/modules/pathkit/externs.js b/modules/pathkit/externs.js
index 9116254..c6c5fc9 100644
--- a/modules/pathkit/externs.js
+++ b/modules/pathkit/externs.js
@@ -27,6 +27,9 @@
_FromCmds: function(ptr, size) {},
loadCmdsTypedArray: function(arr) {},
FromCmds: function(arr) {},
+ _SkCubicMap: function(cp1, cp2) {},
+ cubicYFromX: function(cpx1, cpy1, cpx2, cpy2, X) {},
+ cubicPtFromT: function(cpx1, cpy1, cpx2, cpy2, T) {},
HEAPF32: {},
@@ -70,6 +73,10 @@
StrokeOpts.prototype.cap;
StrokeOpts.prototype.join;
+// Define CubicMap object
+var CubicMap = {};
+CubicMap.prototype.computeYFromX = function(x) {};
+CubicMap.prototype.computePtFromT = function(t) {};
// For whatever reason, the closure compiler thinks it can rename some of our
diff --git a/modules/pathkit/helper.js b/modules/pathkit/helper.js
index 4f84782..51e1ffd 100644
--- a/modules/pathkit/helper.js
+++ b/modules/pathkit/helper.js
@@ -65,5 +65,41 @@
var ptrLen = PathKit.loadCmdsTypedArray(cmds);
return PathKit._FromCmds(ptrLen[0], ptrLen[1]);
}
+
+ /**
+ * A common pattern is to call this function in sequence with the same
+ * params. We can just remember the last one to speed things up.
+ * Caching in this way is about a 10-15x speed up.
+ * See externs.js for this definition
+ * @type {CubicMap}
+ */
+ var cachedMap;
+ var _cpx1, _cpy1, _cpx2, _cpy2;
+
+ PathKit.cubicYFromX = function(cpx1, cpy1, cpx2, cpy2, X) {
+ if (!cachedMap || _cpx1 !== cpx1 || _cpy1 !== cpy1 ||
+ _cpx2 !== cpx2 || _cpy2 !== cpy2) {
+ if (cachedMap) {
+ // Delete previous cached map to avoid memory leaks.
+ cachedMap.delete()
+ }
+ cachedMap = new PathKit._SkCubicMap([cpx1, cpy1], [cpx2, cpy2]);
+ _cpx1 = cpx1, _cpy1 = cpy1, _cpx2 = cpx2, _cpy2 = cpy2;
+ }
+ return cachedMap.computeYFromX(X);
+ }
+
+ PathKit.cubicPtFromT = function(cpx1, cpy1, cpx2, cpy2, T) {
+ if (!cachedMap || _cpx1 !== cpx1 || _cpy1 !== cpy1 ||
+ _cpx2 !== cpx2 || _cpy2 !== cpy2) {
+ if (cachedMap) {
+ // Delete previous cached map to avoid memory leaks.
+ cachedMap.delete()
+ }
+ cachedMap = new PathKit._SkCubicMap([cpx1, cpy1], [cpx2, cpy2]);
+ _cpx1 = cpx1, _cpy1 = cpy1, _cpx2 = cpx2, _cpy2 = cpy2;
+ }
+ return cachedMap.computePtFromT(T);
+ }
}(Module)); // When this file is loaded in, the high level object is "Module";
diff --git a/modules/pathkit/npm-asmjs/example.html b/modules/pathkit/npm-asmjs/example.html
index 395caa0..f51c628 100644
--- a/modules/pathkit/npm-asmjs/example.html
+++ b/modules/pathkit/npm-asmjs/example.html
@@ -44,6 +44,9 @@
<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>
+<h2> Solves Cubics for Y given X </h2>
+<canvas class=big id=cubics></canvas>
+
<script type="text/javascript" src="/node_modules/pathkit-asmjs/bin/pathkit.js"></script>
<script type="text/javascript" charset="utf-8">
@@ -57,6 +60,7 @@
PathEffectsExample(PathKit);
MatrixTransformExample(PathKit);
FilledSVGExample(PathKit);
+ CubicSolverExample(PathKit);
});
function setCanvasSize(ctx, width, height) {
@@ -335,4 +339,30 @@
innerRect.delete();
}
+ function CubicSolverExample(PathKit) {
+ let ctx = document.getElementById('cubics').getContext('2d');
+ setCanvasSize(ctx, 300, 300);
+ // Draw lines between cp0 (0, 0) and cp1 and then cp2 and cp3 (1, 1)
+ // scaled up to be on a 300x300 grid instead of unit square
+ ctx.strokeStyle = 'black';
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0.1 * 300, 0.5*300);
+
+ ctx.moveTo(0.5 * 300, 0.1*300);
+ ctx.lineTo(300, 300);
+ ctx.stroke();
+
+
+ ctx.strokeStyle = 'green';
+ ctx.beginPath();
+
+ for (let x = 0; x < 300; x++) {
+ // scale X into unit square
+ let y = PathKit.cubicYFromX(0.1, 0.5, 0.5, 0.1, x/300);
+ ctx.arc(x, y*300, 2, 0, 2*Math.PI);
+ }
+ ctx.stroke();
+ }
+
</script>
diff --git a/modules/pathkit/npm-asmjs/package.json b/modules/pathkit/npm-asmjs/package.json
index eb4adda..701e307 100644
--- a/modules/pathkit/npm-asmjs/package.json
+++ b/modules/pathkit/npm-asmjs/package.json
@@ -1,6 +1,6 @@
{
"name": "pathkit-asmjs",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "A asm.js version of Skia's PathOps toolkit",
"main": "bin/pathkit.js",
"homepage": "https://github.com/google/skia/tree/master/modules/pathkit",
diff --git a/modules/pathkit/npm-wasm/example.html b/modules/pathkit/npm-wasm/example.html
index 063454f..09af83b 100644
--- a/modules/pathkit/npm-wasm/example.html
+++ b/modules/pathkit/npm-wasm/example.html
@@ -44,6 +44,9 @@
<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>
+<h2> Solves Cubics for Y given X </h2>
+<canvas class=big id=cubics></canvas>
+
<script type="text/javascript" src="/node_modules/pathkit-wasm/bin/pathkit.js"></script>
<script type="text/javascript" charset="utf-8">
@@ -57,6 +60,7 @@
PathEffectsExample(PathKit);
MatrixTransformExample(PathKit);
FilledSVGExample(PathKit);
+ CubicSolverExample(PathKit);
});
function setCanvasSize(ctx, width, height) {
@@ -335,4 +339,30 @@
innerRect.delete();
}
+ function CubicSolverExample(PathKit) {
+ let ctx = document.getElementById('cubics').getContext('2d');
+ setCanvasSize(ctx, 300, 300);
+ // Draw lines between cp0 (0, 0) and cp1 and then cp2 and cp3 (1, 1)
+ // scaled up to be on a 300x300 grid instead of unit square
+ ctx.strokeStyle = 'black';
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0.1 * 300, 0.5*300);
+
+ ctx.moveTo(0.5 * 300, 0.1*300);
+ ctx.lineTo(300, 300);
+ ctx.stroke();
+
+
+ ctx.strokeStyle = 'green';
+ ctx.beginPath();
+
+ for (let x = 0; x < 300; x++) {
+ // scale X into unit square
+ let y = PathKit.cubicYFromX(0.1, 0.5, 0.5, 0.1, x/300);
+ ctx.arc(x, y*300, 2, 0, 2*Math.PI);
+ }
+ ctx.stroke();
+ }
+
</script>
diff --git a/modules/pathkit/npm-wasm/package.json b/modules/pathkit/npm-wasm/package.json
index f501a89..80cbaec 100644
--- a/modules/pathkit/npm-wasm/package.json
+++ b/modules/pathkit/npm-wasm/package.json
@@ -1,6 +1,6 @@
{
"name": "pathkit-wasm",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "A WASM version of Skia's PathOps toolkit",
"main": "bin/pathkit.js",
"homepage": "https://github.com/google/skia/tree/master/modules/pathkit",
diff --git a/modules/pathkit/pathkit_wasm_bindings.cpp b/modules/pathkit/pathkit_wasm_bindings.cpp
index 40772d8..38af2c6 100644
--- a/modules/pathkit/pathkit_wasm_bindings.cpp
+++ b/modules/pathkit/pathkit_wasm_bindings.cpp
@@ -14,6 +14,7 @@
#include "SkStrokeRec.h"
#include "SkPath.h"
#include "SkPathOps.h"
+#include "SkCubicMap.h"
#include "SkRect.h"
#include "SkPaintDefaults.h"
#include "SkString.h"
@@ -622,6 +623,19 @@
.element(&SimpleMatrix::pers1)
.element(&SimpleMatrix::pers2);
+ value_array<SkPoint>("SkPoint")
+ .element(&SkPoint::fX)
+ .element(&SkPoint::fY);
+
+ // Not intended for external clients to call directly.
+ // See helper.js for the client-facing implementation.
+ class_<SkCubicMap>("_SkCubicMap")
+ .constructor<SkPoint, SkPoint>()
+
+ .function("computeYFromX", &SkCubicMap::computeYFromX)
+ .function("computePtFromT", &SkCubicMap::computeFromT);
+
+
// Test Utils
function("SkBits2FloatUnsigned", &SkBits2FloatUnsigned);
}
diff --git a/modules/pathkit/tests/util.spec.js b/modules/pathkit/tests/util.spec.js
new file mode 100644
index 0000000..2161769
--- /dev/null
+++ b/modules/pathkit/tests/util.spec.js
@@ -0,0 +1,67 @@
+// Tests for util-related things
+
+describe('PathKit\'s CubicMap 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();
+ });
+ }
+ });
+
+ it('computes YFromX correctly', function(done) {
+ LoadPathKit.then(() => {
+ // Spot check a few points
+ const testcases = [
+ // input x, expected y
+ [0.025391, 0.117627],
+ [0.333984, 0.276221],
+ [0.662109, 0.366052],
+ [0.939453, 0.643296],
+ ];
+ for (tc of testcases) {
+ expect(PathKit.cubicYFromX(0, 0.5, 1.0, 0, tc[0])).toBeCloseTo(tc[1], 5);
+ }
+ done();
+ });
+ });
+ it('computes a point from T correctly', function(done) {
+ LoadPathKit.then(() => {
+ // Spot check a few points
+ const testcases = [
+ // input t, expected x, expected y
+ [0.25, [0.128125, 0.240625]],
+ [0.5, [0.35, 0.35]],
+ [0.75, [0.646875, 0.534375]],
+ [1.0, [1.0, 1.0]],
+ ];
+ for (tc of testcases) {
+ let ans = PathKit.cubicPtFromT(0.1, 0.5, 0.5, 0.1, tc[0]);
+ expect(ans).toBeTruthy();
+ expect(ans.length).toBe(2);
+ expect(ans[0]).toBeCloseTo(tc[1][0]);
+ expect(ans[1]).toBeCloseTo(tc[1][1]);
+ }
+ done();
+ });
+ });
+
+ it('does not leak, with or without cache', function(done) {
+ LoadPathKit.then(() => {
+ // Run it a lot to make sure we don't leak.
+ for (let i = 0; i < 300000; i++) {
+ PathKit.cubicYFromX(0.1, 0.5, 0.5, 0.1, 0.1);
+ PathKit.cubicPtFromT(0.1, 0.5, 0.5, 0.1, 0.1);
+ }
+ done();
+ });
+ });
+
+});
\ No newline at end of file