[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
diff --git a/site/user/modules/pathkit.md b/site/user/modules/pathkit.md
index 4d6ec11..007ae9a 100644
--- a/site/user/modules/pathkit.md
+++ b/site/user/modules/pathkit.md
@@ -327,6 +327,33 @@
     // Users can also do pathOne.op(pathTwo, PathKit.PathOp.UNION);
     // to have the resulting path be stored to pathOne and avoid allocating another object.
 
+#### `cubicYFromX(cpx1, cpy1, cpx2, cpy2, X)` ####
+**cpx1, cpy1, cpx2, cpy2** - `Number`, coordinates for control points. <br>
+**X** - `Number`, The X coordinate for which to find the corresponding Y coordinate.
+
+Fast evaluation of a cubic ease-in / ease-out curve. This is defined as a parametric cubic
+curve inside the unit square. Makes the following assumptions:
+
+  - pt[0] is implicitly { 0, 0 }
+  - pt[3] is implicitly { 1, 1 }
+  - pts[1, 2] are inside the unit square
+
+This returns the Y coordinate for the given X coordinate.
+
+#### `cubicPtFromT(cpx1, cpy1, cpx2, cpy2, T)` ####
+**cpx1, cpy1, cpx2, cpy2** - `Number`, coordinates for control points. <br>
+**T** - `Number`, The T param for which to find the corresponding (X, Y) coordinates.
+
+Fast evaluation of a cubic ease-in / ease-out curve. This is defined as a parametric cubic
+curve inside the unit square. Makes the following assumptions:
+
+  - pt[0] is implicitly { 0, 0 }
+  - pt[3] is implicitly { 1, 1 }
+  - pts[1, 2] are inside the unit square
+
+This returns the (X, Y) coordinate for the given T value as a length 2 array.
+
+
 ### SkPath (object) ###
 
 #### `addPath(otherPath)` ####