Add CPU backend for CanvasKit

This also makes the GPU and Skottie portions optional
at build time.

Bug: skia:
Change-Id: I34f494caf0e2ca35dc4767d57f79ba92b24e818f
Reviewed-on: https://skia-review.googlesource.com/c/159146
Reviewed-by: Mike Klein <mtklein@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
diff --git a/experimental/canvaskit/canvas-kit/cpu_example.html b/experimental/canvaskit/canvas-kit/cpu_example.html
new file mode 100644
index 0000000..f4bf85b
--- /dev/null
+++ b/experimental/canvaskit/canvas-kit/cpu_example.html
@@ -0,0 +1,320 @@
+<!DOCTYPE html>
+<title>CanvasKit (Skia via Web Assembly)</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<style>
+  svg, canvas {
+    border: 1px dashed #AAA;
+  }
+
+  #patheffect,#paths,#sk_drinks,#sk_party, #sk_legos, #sk_onboarding {
+    width: 300px;
+    height: 300px;
+  }
+
+</style>
+
+<h2> CanvasKit draws Paths to Canvas using a CPU backend</h2>
+<canvas id=patheffect width=300 height=300></canvas>
+<img id=output>
+<canvas id=paths width=200 height=200></canvas>
+<canvas id=ink width=300 height=300></canvas>
+
+<h2> Skottie </h2>
+<canvas id=sk_legos width=300 height=300></canvas>
+<canvas id=sk_drinks width=500 height=500></canvas>
+<canvas id=sk_party width=500 height=500></canvas>
+<canvas id=sk_onboarding width=500 height=500></canvas>
+
+<script type="text/javascript" src="/node_modules/canvas-kit/bin/canvaskit.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+  var CanvasKit = null;
+  var legoJSON = null;
+  var drinksJSON = null;
+  var confettiJSON = null;
+  var onboardingJSON = null;
+  var fullBounds = {fLeft: 0, fTop: 0, fRight: 500, fBottom: 500};
+  CanvasKitInit({
+    locateFile: (file) => '/node_modules/canvas-kit/bin/'+file,
+  }).then((CK) => {
+    CK.initFonts();
+    CanvasKit = CK;
+    DrawingExample(CanvasKit);
+    PathExample(CanvasKit);
+    InkExample(CanvasKit);
+    // Set bounds to fix the 4:3 resolution of the legos
+    SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
+    // Re-size to fit
+    SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
+    SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
+    SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
+  });
+
+  fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
+    resp.text().then((str) => {
+      legoJSON = str;
+      SkottieExample(CanvasKit, 'sk_legos', legoJSON, {fLeft: -50, fTop: 0, fRight: 350, fBottom: 300});
+    });
+  });
+
+  fetch('https://storage.googleapis.com/skia-cdn/misc/drinks.json').then((resp) => {
+    resp.text().then((str) => {
+      drinksJSON = str;
+      SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
+    });
+  });
+
+  fetch('https://storage.googleapis.com/skia-cdn/misc/confetti.json').then((resp) => {
+    resp.text().then((str) => {
+      confettiJSON = str;
+      SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
+    });
+  });
+
+  fetch('https://storage.googleapis.com/skia-cdn/misc/onboarding.json').then((resp) => {
+    resp.text().then((str) => {
+      onboardingJSON = str;
+      SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
+    });
+  });
+
+  function DrawingExample(CanvasKit) {
+    const surface = CanvasKit.getRasterN32PremulSurface('patheffect');
+    if (!surface) {
+      console.log('Could not make surface');
+      return;
+    }
+
+    const canvas = surface.getCanvas();
+
+    const paint = new CanvasKit.SkPaint();
+
+    const textPaint = new CanvasKit.SkPaint();
+    textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
+    textPaint.setTextSize(30);
+    textPaint.setAntiAlias(true);
+
+    let i = 0;
+
+    let X = 128;
+    let Y = 128;
+
+    function drawFrame() {
+      const path = starPath(CanvasKit, X, Y);
+      const dpe = CanvasKit.MakeSkDashPathEffect([15, 5, 5, 10], i/5);
+      i++;
+
+      paint.setPathEffect(dpe);
+      paint.setStyle(CanvasKit.PaintStyle.STROKE);
+      paint.setStrokeWidth(5.0 + -3 * Math.cos(i/30));
+      paint.setAntiAlias(true);
+      paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
+
+      canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
+
+      canvas.drawPath(path, paint);
+      canvas.drawText('Try Clicking!', 10, 280, textPaint);
+
+      surface.flush();
+
+      dpe.delete();
+      path.delete();
+      window.requestAnimationFrame(drawFrame);
+    }
+    window.requestAnimationFrame(drawFrame);
+
+    // Make animation interactive
+    let interact = (e) => {
+      if (!e.pressure) {
+        return;
+      }
+      X = e.offsetX;
+      Y = e.offsetY;
+    };
+    document.getElementById('patheffect').addEventListener('pointermove', interact);
+    document.getElementById('patheffect').addEventListener('pointerdown', interact);
+    preventScrolling(document.getElementById('patheffect'));
+    // A client would need to delete this if it didn't go on for ever.
+    //paint.delete();
+  }
+
+  function PathExample(CanvasKit) {
+    const surface = CanvasKit.getRasterN32PremulSurface('paths');
+    if (!surface) {
+      console.log('Could not make surface');
+    }
+    const canvas = surface.getCanvas();
+
+    function drawFrame() {
+      const paint = new CanvasKit.SkPaint();
+      paint.setStrokeWidth(1.0);
+      paint.setAntiAlias(true);
+      paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
+      paint.setStyle(CanvasKit.PaintStyle.STROKE);
+
+      const path = new CanvasKit.SkPath();
+      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.cubicTo(90, 10, 160, 150, 190, 10);
+
+      path.moveTo(36, 148);
+      path.quadTo(66, 188, 120, 136);
+      path.lineTo(36, 148);
+
+      path.moveTo(150, 180);
+      path.arcTo(150, 100, 50, 200, 20);
+      path.lineTo(160, 160);
+
+      path.moveTo(20, 120);
+      path.lineTo(20, 120);
+
+      canvas.drawPath(path, paint);
+
+      surface.flush();
+
+      path.delete();
+      paint.delete();
+      // Intentionally just draw frame once
+    }
+    window.requestAnimationFrame(drawFrame);
+  }
+
+  function preventScrolling(canvas) {
+    canvas.addEventListener('touchmove', (e) => {
+      // Prevents touch events in the canvas from scrolling the canvas.
+      e.preventDefault();
+      e.stopPropagation();
+    });
+  }
+
+  function InkExample(CanvasKit) {
+    const surface = CanvasKit.getRasterN32PremulSurface('ink');
+    if (!surface) {
+      console.log('Could not make surface');
+    }
+    const canvas = surface.getCanvas();
+
+    let paint = new CanvasKit.SkPaint();
+    paint.setAntiAlias(true);
+    paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
+    paint.setStyle(CanvasKit.PaintStyle.STROKE);
+    paint.setStrokeWidth(4.0);
+    paint.setPathEffect(CanvasKit.MakeSkCornerPathEffect(50));
+
+    // Draw I N K
+    let path = new CanvasKit.SkPath();
+    path.moveTo(80, 30);
+    path.lineTo(80, 80);
+
+    path.moveTo(100, 80);
+    path.lineTo(100, 15);
+    path.lineTo(130, 95);
+    path.lineTo(130, 30);
+
+    path.moveTo(150, 30);
+    path.lineTo(150, 80);
+    path.moveTo(170, 30);
+    path.lineTo(150, 55);
+    path.lineTo(170, 80);
+
+    let paths = [path];
+    let paints = [paint];
+
+    function drawFrame() {
+      canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
+      for (let i = 0; i < paints.length && i < paths.length; i++) {
+        canvas.drawPath(paths[i], paints[i]);
+      }
+      surface.flush();
+
+      window.requestAnimationFrame(drawFrame);
+    }
+
+    let hold = false;
+    let interact = (e) => {
+      let type = e.type;
+      if (type === 'lostpointercapture' || type === 'pointerup' || !e.pressure ) {
+        hold = false;
+        return;
+      }
+      if (hold) {
+        path.lineTo(e.offsetX, e.offsetY);
+      } else {
+        paint = paint.copy();
+        paint.setColor(CanvasKit.Color(Math.random() * 255, Math.random() * 255, Math.random() * 255, Math.random() + .2));
+        paints.push(paint);
+        path = new CanvasKit.SkPath();
+        paths.push(path);
+        path.moveTo(e.offsetX, e.offsetY);
+      }
+      hold = true;
+    };
+    document.getElementById('ink').addEventListener('pointermove', interact);
+    document.getElementById('ink').addEventListener('pointerdown', interact);
+    document.getElementById('ink').addEventListener('lostpointercapture', interact);
+    document.getElementById('ink').addEventListener('pointerup', interact);
+    preventScrolling(document.getElementById('ink'));
+    window.requestAnimationFrame(drawFrame);
+  }
+
+  function starPath(CanvasKit, X=128, Y=128, R=116) {
+    let p = new CanvasKit.SkPath();
+    p.moveTo(X + R, Y);
+    for (let i = 1; i < 8; i++) {
+      let a = 2.6927937 * i;
+      p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
+    }
+    return p;
+  }
+
+  function fps(frameTimes) {
+    let total = 0;
+    for (let ft of frameTimes) {
+      total += ft;
+    }
+    return frameTimes.length / total;
+  }
+
+  function SkottieExample(CanvasKit, id, jsonStr, bounds) {
+    if (!CanvasKit || !jsonStr) {
+      return;
+    }
+    const animation = CanvasKit.MakeAnimation(jsonStr);
+    const duration = animation.duration() * 1000;
+    const size = animation.size();
+    let c = document.getElementById(id);
+    bounds = bounds || {fLeft: 0, fTop: 0, fRight: size.w, fBottom: size.h};
+
+    const surface = CanvasKit.getRasterN32PremulSurface(id);
+    if (!surface) {
+      console.log('Could not make surface');
+    }
+    const canvas = surface.getCanvas();
+
+    let firstFrame = new Date().getTime();
+
+    function drawFrame() {
+      let now = new Date().getTime();
+      let seek = ((now - firstFrame) / duration) % 1.0;
+      animation.seek(seek);
+      canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
+      animation.render(canvas, bounds);
+      surface.flush();
+      window.requestAnimationFrame(drawFrame);
+    }
+    window.requestAnimationFrame(drawFrame);
+
+    //animation.delete();
+  }
+
+</script>
diff --git a/experimental/canvaskit/canvaskit_bindings.cpp b/experimental/canvaskit/canvaskit_bindings.cpp
index 004a069..82e65c7 100644
--- a/experimental/canvaskit/canvaskit_bindings.cpp
+++ b/experimental/canvaskit/canvaskit_bindings.cpp
@@ -5,10 +5,13 @@
  * found in the LICENSE file.
  */
 
+#if SK_SUPPORT_GPU
 #include "GrBackendSurface.h"
 #include "GrContext.h"
 #include "GrGLInterface.h"
 #include "GrGLTypes.h"
+#endif
+
 #include "SkCanvas.h"
 #include "SkCanvas.h"
 #include "SkDashPathEffect.h"
@@ -23,15 +26,19 @@
 #include "SkSurface.h"
 #include "SkSurfaceProps.h"
 #include "SkTestFontMgr.h"
+#if SK_INCLUDE_SKOTTIE
 #include "Skottie.h"
+#endif
 
 #include <iostream>
 #include <string>
-#include <GL/gl.h>
 
 #include <emscripten.h>
 #include <emscripten/bind.h>
+#if SK_SUPPORT_GPU
+#include <GL/gl.h>
 #include <emscripten/html5.h>
+#endif
 
 using namespace emscripten;
 
@@ -42,6 +49,7 @@
     gSkFontMgr_DefaultFactory = &sk_tool_utils::MakePortableFontMgr;
 }
 
+#if SK_SUPPORT_GPU
 // Wraps the WebGL context in an SkSurface and returns it.
 sk_sp<SkSurface> getWebGLSurface(std::string id, int width, int height) {
     // Context configurations
@@ -91,10 +99,13 @@
                                                                     colorType, nullptr, nullptr));
     return surface;
 }
+#endif
 
+#if SK_INCLUDE_SKOTTIE
 sk_sp<skottie::Animation> MakeAnimation(std::string json) {
     return skottie::Animation::Make(json.c_str(), json.length());
 }
+#endif
 
 //========================================================================================
 // Path things
@@ -196,7 +207,14 @@
 // the compiler is happy.
 EMSCRIPTEN_BINDINGS(Skia) {
     function("initFonts", &initFonts);
+#if SK_SUPPORT_GPU
     function("_getWebGLSurface", &getWebGLSurface, allow_raw_pointers());
+    function("currentContext", &emscripten_webgl_get_current_context);
+    function("setCurrentContext", &emscripten_webgl_make_context_current);
+#endif
+    function("_getRasterN32PremulSurface", optional_override([](int width, int height)->sk_sp<SkSurface> {
+        return SkSurface::MakeRasterN32Premul(width, height, nullptr);
+    }), allow_raw_pointers());
     function("MakeSkCornerPathEffect", &SkCornerPathEffect::Make, allow_raw_pointers());
     function("MakeSkDiscretePathEffect", &SkDiscretePathEffect::Make, allow_raw_pointers());
     // Won't be called directly, there's a JS helper to deal with typed arrays.
@@ -280,6 +298,11 @@
         .function("width", &SkSurface::width)
         .function("height", &SkSurface::height)
         .function("makeImageSnapshot", &SkSurface::makeImageSnapshot)
+        .function("_readPixels", optional_override([](SkSurface& self, int width, int height, uintptr_t /* uint8_t* */ cptr)->bool {
+            auto* dst = reinterpret_cast<uint8_t*>(cptr);
+            auto dstInfo = SkImageInfo::Make(width, height, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType);
+            return self.readPixels(dstInfo, dst, width*4, 0, 0);
+        }))
         .function("getCanvas", &SkSurface::getCanvas, allow_raw_pointers());
 
 
@@ -317,6 +340,7 @@
         .field("w",   &SkISize::fWidth)
         .field("h",   &SkISize::fHeight);
 
+#if SK_INCLUDE_SKOTTIE
     // Animation things (may eventually go in own library)
     class_<skottie::Animation>("Animation")
         .smart_ptr<sk_sp<skottie::Animation>>("sk_sp<Animation>")
@@ -334,7 +358,5 @@
         }), allow_raw_pointers());
 
     function("MakeAnimation", &MakeAnimation);
-
-    function("currentContext", &emscripten_webgl_get_current_context);
-    function("setCurrentContext", &emscripten_webgl_make_context_current);
+#endif
 }
diff --git a/experimental/canvaskit/compile.sh b/experimental/canvaskit/compile.sh
index ff225ed..0fc2a8f 100755
--- a/experimental/canvaskit/compile.sh
+++ b/experimental/canvaskit/compile.sh
@@ -30,6 +30,37 @@
   RELEASE_CONF="-O0 --js-opts 0 -s SAFE_HEAP=1 -s ASSERTIONS=1 -s GL_ASSERTIONS=1 -g3 -DPATHKIT_TESTING -DSK_DEBUG"
 fi
 
+GN_GPU="skia_enable_gpu=true"
+WASM_GPU="-lEGL -lGLESv2 -DSK_SUPPORT_GPU=1"
+if [[ $@ == *no_gpu* ]]; then
+  echo "Omitting the GPU backend"
+  GN_GPU="skia_enable_gpu=false"
+  WASM_GPU="-DSK_SUPPORT_GPU=0"
+fi
+
+WASM_SKOTTIE="-DSK_INCLUDE_SKOTTIE=1 \
+  modules/skottie/src/Skottie.cpp \
+  modules/skottie/src/SkottieAdapter.cpp \
+  modules/skottie/src/SkottieAnimator.cpp \
+  modules/skottie/src/SkottieJson.cpp \
+  modules/skottie/src/SkottieLayer.cpp \
+  modules/skottie/src/SkottieLayerEffect.cpp \
+  modules/skottie/src/SkottiePrecompLayer.cpp \
+  modules/skottie/src/SkottieProperty.cpp \
+  modules/skottie/src/SkottieShapeLayer.cpp \
+  modules/skottie/src/SkottieTextLayer.cpp \
+  modules/skottie/src/SkottieValue.cpp \
+  modules/sksg/src/*.cpp \
+  src/core/SkCubicMap.cpp \
+  src/core/SkTime.cpp \
+  src/pathops/SkOpBuilder.cpp \
+  src/utils/SkJSON.cpp \
+  src/utils/SkParse.cpp "
+if [[ $@ == *no_skottie* ]]; then
+  echo "Omitting Skottie"
+  WASM_SKOTTIE="-DSK_INCLUDE_SKOTTIE=0"
+fi
+
 # Turn off exiting while we check for ninja (which may not be on PATH)
 set +e
 NINJA=`which ninja`
@@ -73,7 +104,7 @@
   skia_use_zlib=true \
   \
   skia_enable_ccpr=false \
-  skia_enable_gpu=true \
+  ${GN_GPU} \
   skia_enable_fontmgr_empty=false \
   skia_enable_pdf=false"
 
@@ -104,33 +135,16 @@
     -Isrc/sfnt/ \
     -Itools/fonts \
     -Itools \
-    -lEGL \
-    -lGLESv2 \
+    $WASM_GPU \
     -std=c++14 \
     --bind \
     --pre-js $BASE_DIR/helper.js \
     --pre-js $BASE_DIR/interface.js \
     $BASE_DIR/canvaskit_bindings.cpp \
     $BUILD_DIR/libskia.a \
-    modules/skottie/src/Skottie.cpp \
-    modules/skottie/src/SkottieAdapter.cpp \
-    modules/skottie/src/SkottieAnimator.cpp \
-    modules/skottie/src/SkottieJson.cpp \
-    modules/skottie/src/SkottieLayer.cpp \
-    modules/skottie/src/SkottieLayerEffect.cpp \
-    modules/skottie/src/SkottiePrecompLayer.cpp \
-    modules/skottie/src/SkottieProperty.cpp \
-    modules/skottie/src/SkottieShapeLayer.cpp \
-    modules/skottie/src/SkottieTextLayer.cpp \
-    modules/skottie/src/SkottieValue.cpp \
-    modules/sksg/src/*.cpp \
-    src/core/SkCubicMap.cpp \
-    src/core/SkTime.cpp \
-    src/pathops/SkOpBuilder.cpp \
     tools/fonts/SkTestFontMgr.cpp \
     tools/fonts/SkTestTypeface.cpp \
-    src/utils/SkJSON.cpp \
-    src/utils/SkParse.cpp \
+    $WASM_SKOTTIE \
     -s ALLOW_MEMORY_GROWTH=1 \
     -s EXPORT_NAME="CanvasKitInit" \
     -s FORCE_FILESYSTEM=0 \
diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js
index 5d07f0d..c0f6c66 100644
--- a/experimental/canvaskit/externs.js
+++ b/experimental/canvaskit/externs.js
@@ -27,6 +27,7 @@
 	Color: function(r, g, b, a) {},
 	currentContext: function() {},
 	getWebGLSurface: function(htmlID) {},
+	getRasterN32PremulSurface: function(htmlID) {},
 	MakeSkDashPathEffect: function(intervals, phase) {},
 	setCurrentContext: function() {},
 	LTRBRect: function(l, t, r, b) {},
@@ -34,13 +35,26 @@
 	// private API (i.e. things declared in the bindings that we use
 	// in the pre-js file)
 	_getWebGLSurface: function(htmlID, w, h) {},
+	_getRasterN32PremulSurface: function(w, h) {},
 	_malloc: function(size) {},
 	onRuntimeInitialized: function() {},
 	_MakeSkDashPathEffect: function(ptr, len, phase) {},
 
 	// Objects and properties on CanvasKit
 
+	/** Represents the heap of the WASM code
+	 * @type {ArrayBuffer}
+	 */
+	buffer: {},
+	/**
+	 * @type {Float32Array}
+	 */
 	HEAPF32: {}, // only needed for TypedArray mallocs
+	/**
+	 * @type {Uint8Array}
+	 */
+	HEAPU8: {},
+
 
 	SkPath: {
 		// public API should go below because closure still will
@@ -59,6 +73,14 @@
 		_rect: function(x, y, w, h) {},
 		_simplify: function() {},
 		_transform: function(scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2) {},
+	},
+
+	SkSurface: {
+		// public API should go below because closure still will
+		// remove things declared here and not on the prototype.
+
+		// private API
+		_readPixels: function(w, h, ptr) {},
 	}
 }
 
@@ -76,5 +98,7 @@
 CanvasKit.SkPath.prototype.simplify = function() {};
 CanvasKit.SkPath.prototype.transform = function() {};
 
+CanvasKit.SkSurface.prototype.flush = function() {};
+
 // Not sure why this is needed - might be a bug in emsdk that this isn't properly declared.
 function loadWebAssemblyModule() {}
diff --git a/experimental/canvaskit/interface.js b/experimental/canvaskit/interface.js
index f0e5b0c..98be5ef 100644
--- a/experimental/canvaskit/interface.js
+++ b/experimental/canvaskit/interface.js
@@ -106,6 +106,20 @@
       }
       return this;
     };
+
+    CanvasKit.SkSurface.prototype.flush = function() {
+      var success = this._readPixels(this._width, this._height, this._pixelPtr);
+      if (!success) {
+        console.err('could not read pixels');
+        return;
+      }
+
+      var pixels = new Uint8ClampedArray(CanvasKit.buffer, this._pixelPtr, this._pixelLen);
+      var imageData = new ImageData(pixels, this._width, this._height);
+
+      this.canvas.getContext('2d').putImageData(imageData, 0, 0);
+
+    };
   }
 
   CanvasKit.getWebGLSurface = function(htmlID) {
@@ -118,6 +132,25 @@
     return this._getWebGLSurface(htmlID, canvas.width, canvas.height);
   }
 
+  CanvasKit.getRasterN32PremulSurface = function(htmlID) {
+    var canvas = document.getElementById(htmlID);
+    if (!canvas) {
+      throw 'Canvas with id ' + htmlID + ' was not found';
+    }
+    // Maybe better to use clientWidth/height.  See:
+    // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
+    var surface = this._getRasterN32PremulSurface(canvas.width, canvas.height);
+    if (surface) {
+      surface.canvas = canvas;
+      surface._width = canvas.width;
+      surface._height = canvas.height;
+      surface._pixelLen = surface._width * surface._height * 4; // it's 8888
+      // Allocate the buffer of pixels to be used to draw back and forth.
+      surface._pixelPtr = CanvasKit._malloc(surface._pixelLen);
+    }
+    return surface;
+  }
+
   // Likely only used for tests.
   CanvasKit.LTRBRect = function(l, t, r, b) {
     return {