[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>
This commit is contained in:
Kevin Lubick 2018-10-19 14:34:34 -04:00
parent 07afa23bd0
commit 006a6f3b14
7 changed files with 362 additions and 57 deletions

View File

@ -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>

View File

@ -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();

View File

@ -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

View File

@ -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 \

View File

@ -24,27 +24,123 @@
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
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 (from C++ bindings)
// private API
_addPath: function() {},
_arcTo: function() {},
_close: function() {},
_conicTo: function() {},
_cubicTo: function() {},
_lineTo: function() {},
_moveTo: function() {},
_op: function() {},
_quadTo: function() {},
_rect: function() {},
_simplify: function() {},
_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 (from C++ bindings)
/** @return {CanvasKit.SkCanvas} */
getCanvas: function() {},
/** @return {CanvasKit.SkImage} */
makeImageSnapshot: function() {},
// private API
_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}
*/
@ -58,49 +154,24 @@ var CanvasKit = {
*/
HEAPU8: {},
_malloc: function() {},
_free: function() {},
onRuntimeInitialized: function() {},
};
SkPath: {
// public API should go below because closure still will
// remove things declared here and not on the prototype.
// private API
_addPath: function(path, scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2) {},
_arcTo: function(x1, y1, x2, y2, radius) {},
_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) {},
_simplify: function() {},
_transform: function(scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2) {},
delete: function() {},
},
SkSurface: {
// public API should go below because closure still will
// remove things declared here and not on the prototype.
flush: function() {},
// private API
_readPixels: function(w, h, ptr) {},
_flush: function() {},
delete: 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() {};

View File

@ -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';

View File

@ -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";