[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:
parent
07afa23bd0
commit
006a6f3b14
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
@ -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() {};
|
||||
|
||||
|
@ -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';
|
||||
|
170
experimental/canvaskit/htmlcanvas/canvas2d.js
Normal file
170
experimental/canvaskit/htmlcanvas/canvas2d.js
Normal 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";
|
Loading…
Reference in New Issue
Block a user