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 {