aboutsummaryrefslogtreecommitdiff
path: root/experimental
diff options
context:
space:
mode:
authorKevin Lubick <kjlubick@google.com>2018-10-19 14:34:34 -0400
committerKevin Lubick <kjlubick@google.com>2018-10-22 18:02:09 +0000
commit006a6f3b14746ad1e8d71e441364a97e365b193a (patch)
treea2113f45ff6d78bf94acb0bd309cd0e14daebf29 /experimental
parent07afa23bd0fa74d18fb7faee898b2a876536a170 (diff)
downloadskqp-006a6f3b14746ad1e8d71e441364a97e365b193a.tar.gz
[canvaskit] Fleshing out the beginnings of a Canvas API
I can probably write most, if not all, of a Canvas API in JS using the SkCanvas and SkPaint objects. This lets us expose the fancier API and optionally have a more familiar API. This is controlled at compile time, i.e. bring in the extra JS or not. There is still plenty of the API that needs working, but this is meant to outlay the plans of where this is going. Bug: skia: Change-Id: I2e36a33c24c2bacd52811dc85508dba170ab0dd7 Reviewed-on: https://skia-review.googlesource.com/c/163490 Reviewed-by: Mike Reed <reed@google.com>
Diffstat (limited to 'experimental')
-rw-r--r--experimental/canvaskit/canvaskit/example.html28
-rw-r--r--experimental/canvaskit/canvaskit/node.example.js23
-rw-r--r--experimental/canvaskit/canvaskit_bindings.cpp10
-rwxr-xr-xexperimental/canvaskit/compile.sh7
-rw-r--r--experimental/canvaskit/externs.js177
-rw-r--r--experimental/canvaskit/gpu.js2
-rw-r--r--experimental/canvaskit/htmlcanvas/canvas2d.js170
7 files changed, 361 insertions, 56 deletions
diff --git a/experimental/canvaskit/canvaskit/example.html b/experimental/canvaskit/canvaskit/example.html
index 2e41635f03..ec2a18c921 100644
--- a/experimental/canvaskit/canvaskit/example.html
+++ b/experimental/canvaskit/canvaskit/example.html
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
- svg, canvas {
+ svg, canvas, img {
border: 1px dashed #AAA;
}
@@ -30,6 +30,9 @@
<!-- Doesn't work yet. -->
<button id=lego_btn>Take a picture of the legos</button>
+<h2>Drop in replacement for HTML Canvas (e.g. node.js)</h2>
+<img id=api1 width=300 height=300/>
+
<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
<script type="text/javascript" charset="utf-8">
@@ -55,6 +58,8 @@
SkottieExample(CanvasKit, 'sk_drinks', drinksJSON, fullBounds);
SkottieExample(CanvasKit, 'sk_party', confettiJSON, fullBounds);
SkottieExample(CanvasKit, 'sk_onboarding', onboardingJSON, fullBounds);
+
+ CanvasAPI1(CanvasKit);
});
fetch('https://storage.googleapis.com/skia-cdn/misc/lego_loader.json').then((resp) => {
@@ -358,4 +363,25 @@
return surface;
}
+ function CanvasAPI1(CanvasKit) {
+ let canvas = CanvasKit.MakeCanvas(300, 300);
+
+ let ctx = canvas.getContext('2d');
+ ctx.font = '30px Impact'
+ ctx.rotate(.1);
+ let text = ctx.measureText('Awesome');
+ ctx.fillText('Awesome ', 50, 100);
+ ctx.strokeText('Groovy!', 60+text.width, 100);
+
+ // Draw line under Awesome
+ ctx.strokeStyle = 'rgba(125,0,0,0.5)';
+ ctx.beginPath();
+ ctx.lineTo(50, 102);
+ ctx.lineTo(50 + text.width, 102);
+ ctx.stroke();
+
+ // TODO load image
+ document.getElementById('api1').src = canvas.toDataURL();
+ }
+
</script>
diff --git a/experimental/canvaskit/canvaskit/node.example.js b/experimental/canvaskit/canvaskit/node.example.js
index 514900347b..55140e6d51 100644
--- a/experimental/canvaskit/canvaskit/node.example.js
+++ b/experimental/canvaskit/canvaskit/node.example.js
@@ -6,6 +6,27 @@ CanvasKitInit({
locateFile: (file) => __dirname + '/bin/'+file,
}).then((CK) => {
CanvasKit = CK;
+ let canvas = CanvasKit.MakeCanvas(300, 300);
+
+ let ctx = canvas.getContext('2d');
+ ctx.font = '30px Impact'
+ ctx.rotate(.1);
+ let text = ctx.measureText('Awesome');
+ ctx.fillText('Awesome ', 50, 100);
+ ctx.strokeText('Groovy!', 60+text.width, 100);
+
+ // Draw line under Awesome
+ ctx.strokeStyle = 'rgba(125,0,0,0.5)';
+ ctx.beginPath();
+ ctx.lineTo(50, 102);
+ ctx.lineTo(50 + text.width, 102);
+ ctx.stroke();
+
+ // TODO load an image from file
+ console.log('<img src="' + canvas.toDataURL() + '" />');
+});
+
+function fancyAPI() {
CanvasKit.initFonts();
console.log('loaded');
@@ -57,7 +78,7 @@ CanvasKitInit({
paint.delete();
surface.dispose();
-});
+}
function starPath(CanvasKit, X=128, Y=128, R=116) {
let p = new CanvasKit.SkPath();
diff --git a/experimental/canvaskit/canvaskit_bindings.cpp b/experimental/canvaskit/canvaskit_bindings.cpp
index 28832fbe76..1d91667864 100644
--- a/experimental/canvaskit/canvaskit_bindings.cpp
+++ b/experimental/canvaskit/canvaskit_bindings.cpp
@@ -245,7 +245,11 @@ EMSCRIPTEN_BINDINGS(Skia) {
self.drawText(text.c_str(), text.length(), x, y, p);
}))
.function("flush", &SkCanvas::flush)
+ .function("rotate", select_overload<void (SkScalar degrees, SkScalar px, SkScalar py)>(&SkCanvas::rotate))
.function("save", &SkCanvas::save)
+ .function("scale", &SkCanvas::scale)
+ .function("setMatrix", &SkCanvas::setMatrix)
+ .function("skew", &SkCanvas::skew)
.function("translate", &SkCanvas::translate);
class_<SkData>("SkData")
@@ -262,6 +266,12 @@ EMSCRIPTEN_BINDINGS(Skia) {
SkPaint p(self);
return p;
}))
+ .function("measureText", optional_override([](SkPaint& self, std::string text) {
+ // TODO(kjlubick): This does not work well for non-ascii
+ // Need to maybe add a helper in interface.js that supports UTF-8
+ // Otherwise, go with std::wstring and set UTF-32 encoding.
+ return self.measureText(text.c_str(), text.length());
+ }))
.function("setAntiAlias", &SkPaint::setAntiAlias)
.function("setColor", optional_override([](SkPaint& self, JSColor color)->void {
// JS side gives us a signed int instead of an unsigned int for color
diff --git a/experimental/canvaskit/compile.sh b/experimental/canvaskit/compile.sh
index bc905dd363..7ca0dc0ffb 100755
--- a/experimental/canvaskit/compile.sh
+++ b/experimental/canvaskit/compile.sh
@@ -67,6 +67,12 @@ if [[ $@ == *no_skottie* ]]; then
WASM_SKOTTIE="-DSK_INCLUDE_SKOTTIE=0"
fi
+HTML_CANVAS_API="--pre-js $BASE_DIR/htmlcanvas/canvas2d.js"
+if [[ $@ == *no_canvas* ]]; then
+ echo "Omitting bindings for HTML Canvas API"
+ HTML_CANVAS_API=""
+fi
+
# Turn off exiting while we check for ninja (which may not be on PATH)
set +e
NINJA=`which ninja`
@@ -153,6 +159,7 @@ ${EMCXX} \
--bind \
--pre-js $BASE_DIR/helper.js \
--pre-js $BASE_DIR/interface.js \
+ $HTML_CANVAS_API \
$BASE_DIR/canvaskit_bindings.cpp \
tools/fonts/SkTestFontMgr.cpp \
tools/fonts/SkTestTypeface.cpp \
diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js
index 808d478366..4377561a1d 100644
--- a/experimental/canvaskit/externs.js
+++ b/experimental/canvaskit/externs.js
@@ -24,83 +24,154 @@
var CanvasKit = {
// public API (i.e. things we declare in the pre-js file)
- Color: function(r, g, b, a) {},
+ Color: function() {},
+ /** @return {CanvasKit.SkRect} */
+ LTRBRect: function() {},
+ MakeCanvas: function() {},
+ MakeCanvasSurface: function() {},
+ MakeSkDashPathEffect: function() {},
+ MakeSurface: function() {},
currentContext: function() {},
- MakeCanvasSurface: function(htmlID) {},
- MakeSurface: function(w, h) {},
- MakeSkDashPathEffect: function(intervals, phase) {},
- setCurrentContext: function(ctx) {},
- LTRBRect: function(l, t, r, b) {},
- gpu: {},
- skottie: {},
+ initFonts: function() {},
+ setCurrentContext: function() {},
+ getSkDataBytes: function() {},
// 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) {},
- _free: function(ptr) {},
- onRuntimeInitialized: function() {},
- _MakeSkDashPathEffect: function(ptr, len, phase) {},
+ _getWebGLSurface: function() {},
+ _getRasterN32PremulSurface: function() {},
+ _MakeSkDashPathEffect: function() {},
// 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: {},
+ SkCanvas: {
+ // public API (from C++ bindings)
+ clear: function() {},
+ drawPaint: function() {},
+ drawPath: function() {},
+ drawText: function() {},
+ flush: function() {},
+ rotate: function() {},
+ save: function() {},
+ scale: function() {},
+ setMatrix: function() {},
+ skew: function() {},
+ translate: function() {},
+ // private API
+ delete: function() {},
+ },
+
+ SkImage: {
+ encodeToData: function() {},
+ },
SkPath: {
- // public API should go below because closure still will
- // remove things declared here and not on the prototype.
+ // public API (from C++ bindings)
// private API
- _addPath: function(path, scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2) {},
- _arcTo: function(x1, y1, x2, y2, radius) {},
+ _addPath: function() {},
+ _arcTo: function() {},
_close: function() {},
- _conicTo: function(x1, y1, x2, y2, w) {},
- _cubicTo: function(cp1x, cp1y, cp2x, cp2y, x, y) {},
- _lineTo: function(x1, y1) {},
- _moveTo: function(x1, y1) {},
- _op: function(otherPath, op) {},
- _quadTo: function(cpx, cpy, x, y) {},
- _rect: function(x, y, w, h) {},
+ _conicTo: function() {},
+ _cubicTo: function() {},
+ _lineTo: function() {},
+ _moveTo: function() {},
+ _op: function() {},
+ _quadTo: function() {},
+ _rect: function() {},
_simplify: function() {},
- _transform: function(scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2) {},
+ _transform: function() {},
+ delete: function() {},
+ },
+
+ SkPaint: {
+ // public API (from C++ bindings)
+ /** @return {CanvasKit.SkPaint} */
+ copy: function() {},
+ measureText: function() {},
+ setAntiAlias: function() {},
+ setColor: function() {},
+ setPathEffect: function() {},
+ setShader: function() {},
+ setStrokeWidth: function() {},
+ setStyle: function() {},
+ setTextSize: function() {},
+
+ //private API
delete: function() {},
},
+ SkRect: {
+ fLeft: {},
+ fTop: {},
+ fRight: {},
+ fBottom: {},
+ },
+
SkSurface: {
- // public API should go below because closure still will
- // remove things declared here and not on the prototype.
- flush: function() {},
+ // public API (from C++ bindings)
+ /** @return {CanvasKit.SkCanvas} */
+ getCanvas: function() {},
+ /** @return {CanvasKit.SkImage} */
+ makeImageSnapshot: function() {},
+
// private API
- _readPixels: function(w, h, ptr) {},
_flush: function() {},
+ _getRasterN32PremulSurface: function() {},
+ _readPixels: function() {},
delete: function() {},
- }
-}
+ },
+
+ // Constants and Enums
+ gpu: {},
+ skottie: {},
+ PaintStyle: {
+ FILL: {},
+ STROKE: {},
+ STROKE_AND_FILL: {},
+ },
+
+ FillType: {
+ WINDING: {},
+ EVENODD: {},
+ INVERSE_WINDING: {},
+ INVERSE_EVENODD: {},
+ },
+
+ // Things Enscriptem adds for us
+
+ /** Represents the heap of the WASM code
+ * @type {ArrayBuffer}
+ */
+ buffer: {},
+ /**
+ * @type {Float32Array}
+ */
+ HEAPF32: {}, // only needed for TypedArray mallocs
+ /**
+ * @type {Uint8Array}
+ */
+ HEAPU8: {},
+
+ _malloc: function() {},
+ _free: function() {},
+ onRuntimeInitialized: function() {},
+};
-// Path public API
+// Public API things that are newly declared in the JS should go here.
+// It's not enough to declare them above, because closure can still erase them
+// unless they go on the prototype.
CanvasKit.SkPath.prototype.addPath = function() {};
-CanvasKit.SkPath.prototype.arcTo = function(x1, y1, x2, y2, radius) {};
+CanvasKit.SkPath.prototype.arcTo = function() {};
CanvasKit.SkPath.prototype.close = function() {};
-CanvasKit.SkPath.prototype.conicTo = function(x1, y1, x2, y2, w) {};
-CanvasKit.SkPath.prototype.cubicTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {};
-CanvasKit.SkPath.prototype.lineTo = function(x, y) {};
-CanvasKit.SkPath.prototype.moveTo = function(x, y) {};
-CanvasKit.SkPath.prototype.op = function(otherPath, op) {};
-CanvasKit.SkPath.prototype.quadTo = function(x1, y1, x2, y2) {};
-CanvasKit.SkPath.prototype.rect = function(x, y, w, h) {};
+CanvasKit.SkPath.prototype.conicTo = function() {};
+CanvasKit.SkPath.prototype.cubicTo = function() {};
+CanvasKit.SkPath.prototype.lineTo = function() {};
+CanvasKit.SkPath.prototype.moveTo = function() {};
+CanvasKit.SkPath.prototype.op = function() {};
+CanvasKit.SkPath.prototype.quadTo = function() {};
+CanvasKit.SkPath.prototype.rect = function() {};
CanvasKit.SkPath.prototype.simplify = function() {};
CanvasKit.SkPath.prototype.transform = function() {};
diff --git a/experimental/canvaskit/gpu.js b/experimental/canvaskit/gpu.js
index 7472fccd8c..3a26842296 100644
--- a/experimental/canvaskit/gpu.js
+++ b/experimental/canvaskit/gpu.js
@@ -3,7 +3,7 @@
(function(CanvasKit){
CanvasKit._extraInitializations = CanvasKit._extraInitializations || [];
CanvasKit._extraInitializations.push(function() {
- CanvasKit.MakeCanvasSurface = function(htmlID) {
+ CanvasKit.MakeCanvasSurface = function(htmlID) {
var canvas = document.getElementById(htmlID);
if (!canvas) {
throw 'Canvas with id ' + htmlID + ' was not found';
diff --git a/experimental/canvaskit/htmlcanvas/canvas2d.js b/experimental/canvaskit/htmlcanvas/canvas2d.js
new file mode 100644
index 0000000000..4289d87796
--- /dev/null
+++ b/experimental/canvaskit/htmlcanvas/canvas2d.js
@@ -0,0 +1,170 @@
+// Adds compile-time JS functions to augment the CanvasKit interface.
+// Specifically, the code that emulates the HTML Canvas interface
+// (which may be called HTMLCanvas or similar to avoid confusion with
+// SkCanvas).
+(function(CanvasKit) {
+
+ var isNode = typeof btoa === undefined;
+
+ function HTMLCanvas(skSurface) {
+ this._surface = skSurface;
+ this._context = new CanvasRenderingContext2D(skSurface.getCanvas());
+
+ // A normal <canvas> requires that clients call getContext
+ this.getContext = function(type) {
+ if (type === '2d') {
+ return this._context;
+ }
+ return null;
+ }
+
+ this.toDataURL = function() {
+ this._surface.flush();
+
+ var img = this._surface.makeImageSnapshot();
+ if (!img) {
+ console.error('no snapshot');
+ return;
+ }
+ var png = img.encodeToData();
+ if (!png) {
+ console.error('encoding failure');
+ return
+ }
+ // TODO(kjlubick): clean this up a bit - maybe better naming?
+ var pngBytes = CanvasKit.getSkDataBytes(png);
+ if (isNode) {
+ // See https://stackoverflow.com/a/12713326
+ var b64encoded = Buffer.from(pngBytes).toString('base64');
+ } else {
+ var b64encoded = btoa(String.fromCharCode.apply(null, pngBytes));
+ }
+ return 'data:image/png;base64,' + b64encoded;
+ }
+ }
+
+ function CanvasRenderingContext2D(skcanvas) {
+ this._canvas = skcanvas;
+ this._paint = new CanvasKit.SkPaint();
+ this._paint.setAntiAlias(true);
+ this._currentPath = null;
+ this._pathStarted = false;
+
+ Object.defineProperty(this, 'font', {
+ enumerable: true,
+ set: function(newStyle) {
+ //TODO
+ this._paint.setTextSize(30);
+ }
+ });
+
+ Object.defineProperty(this, 'strokeStyle', {
+ enumerable: true,
+ set: function(newStyle) {
+ // TODO
+ }
+ });
+
+ this.beginPath = function() {
+ if (this._currentPath) {
+ this._currentPath.delete();
+ }
+ this._currentPath = new CanvasKit.SkPath();
+ this._pathStarted = false;
+ }
+
+ // ensureSubpath makes SkPath behave like the browser's path object
+ // in that the first lineTo/cubicTo, etc, really acts like a moveTo.
+ // ensureSubpath is the term used in the canvas spec:
+ // https://html.spec.whatwg.org/multipage/canvas.html#ensure-there-is-a-subpath
+ // ensureSubpath returns true if the drawing command can proceed,
+ // false otherwise (i.e. it was the first command and replaced
+ // with a moveTo).
+ this._ensureSubpath = function(x, y) {
+ if (!this._currentPath) {
+ this.beginPath();
+ }
+ if (!this._pathStarted) {
+ this._pathStarted = true;
+ this.moveTo(x, y);
+ return false;
+ }
+ return true;
+ }
+
+ this.fillText = function(text, x, y, maxWidth) {
+ // TODO do something with maxWidth, probably involving measure
+ this._canvas.drawText(text, x, y, this._paint);
+ }
+
+ this.lineTo = function(x, y) {
+ if (this._ensureSubpath(x, y)) {
+ this._currentPath.lineTo(x, y);
+ }
+ }
+
+ this.measureText = function(text) {
+ return {
+ width: this._paint.measureText(text),
+ // TODO other measurements?
+ }
+ }
+
+ this.moveTo = function(x, y) {
+ if (this._ensureSubpath(x, y)) {
+ this._currentPath.moveTo(x, y);
+ }
+ }
+
+ this.resetTransform = function() {
+ this.setTransform(1, 0, 0, 1, 0, 0);
+ }
+
+ this.rotate = function(radians, px, py) {
+ // bindings can't turn undefined into floats
+ this._canvas.rotate(radians * 180/Math.PI, px || 0, py || 0);
+ }
+
+ this.scale = function(sx, sy) {
+ this._canvas.scale(sx, sy);
+ }
+
+ this.setTransform = function(a, b, c, d, e, f) {
+ this._canvas.setMatrix([a, c, e,
+ b, d, f,
+ 0, 0, 1]);
+ }
+
+ this.skew = function(sx, sy) {
+ this._canvas.skew(sx, sy);
+ }
+
+ this.stroke = function() {
+ if (this._currentPath) {
+ this._paint.setStyle(CanvasKit.PaintStyle.STROKE);
+ this._canvas.drawPath(this._currentPath, this._paint);
+ }
+ }
+
+ this.strokeText = function(text, x, y, maxWidth) {
+ // TODO do something with maxWidth, probably involving measure
+ this._paint.setStyle(CanvasKit.PaintStyle.STROKE);
+ this._canvas.drawText(text, x, y, this._paint);
+ }
+
+ this.translate = function(dx, dy) {
+ this._canvas.translate(dx, dy);
+ }
+ }
+
+ CanvasKit.MakeCanvas = function(width, height) {
+ // TODO do fonts the "correct" way
+ CanvasKit.initFonts();
+ var surf = CanvasKit.MakeSurface(width, height);
+ if (surf) {
+ return new HTMLCanvas(surf);
+ }
+ return null;
+ }
+
+}(Module)); // When this file is loaded in, the high level object is "Module";