[canvaskit] Make transforms work properly

This re-works the subpath model and does transforming
of ports in JS rather than in C++; the latter was not easy
to conform to the Canvas Spec, especially for changing
transforms in the middle of a path.

Additionally adds jpeg to the available images it can produce.

Bug: skia:
Change-Id: I5a52ec341d4060198c8680aa4d3b85a26f77b6b9
Reviewed-on: https://skia-review.googlesource.com/c/172500
Reviewed-by: Kevin Lubick <kjlubick@google.com>
This commit is contained in:
Kevin Lubick 2018-11-26 11:47:54 -05:00
parent 3387f2b6ac
commit b9db3906d5
7 changed files with 486 additions and 128 deletions

View File

@ -17,6 +17,14 @@
</style>
<h2>Drop in replacement for HTML Canvas (e.g. node.js)</h2>
<img id=api1 width=300 height=300>
<canvas id=api1_c width=300 height=300></canvas>
<img id=api2 width=300 height=300>
<canvas id=api2_c width=300 height=300></canvas>
<img id=api3 width=300 height=300>
<canvas id=api3_c width=300 height=300></canvas>
<h2> CanvasKit draws Paths to the browser</h2>
<canvas id=vertex1 width=300 height=300></canvas>
<canvas id=vertex2 width=300 height=300></canvas>
@ -38,11 +46,7 @@
<canvas id=nima_example width=300 height=300></canvas>
<h2>Drop in replacement for HTML Canvas (e.g. node.js)</h2>
<img id=api1 width=300 height=300>
<canvas id=api1_c width=300 height=300></canvas>
<img id=api2 width=300 height=300>
<canvas id=api2_c width=300 height=300></canvas>
<script type="text/javascript" src="/node_modules/canvaskit/bin/canvaskit.js"></script>
@ -86,6 +90,7 @@
CanvasAPI1(CanvasKit);
CanvasAPI2(CanvasKit);
CanvasAPI3(CanvasKit);
VertexAPI1(CanvasKit);
VertexAPI2(CanvasKit, bonesImage);
@ -453,13 +458,14 @@
}
function CanvasAPI2(CanvasKit) {
let skcanvas = CanvasKit.MakeCanvas(200, 200);
let skcanvas = CanvasKit.MakeCanvas(300, 300);
let realCanvas = document.getElementById('api2_c');
realCanvas.width = 200;
realCanvas.height = 200;
realCanvas.width = 300;
realCanvas.height = 300;
for (let canvas of [skcanvas, realCanvas]) {
let ctx = canvas.getContext('2d');
ctx.scale(1.5, 1.5);
ctx.moveTo(20, 5);
ctx.lineTo(30, 20);
ctx.lineTo(40, 10);
@ -488,12 +494,68 @@
ctx.ellipse(130, 25, 30, 10, -1*Math.PI/8, Math.PI/6, 1.5*Math.PI);
ctx.lineWidth = 4/3;
ctx.stroke();
}
document.getElementById('api2').src = skcanvas.toDataURL();
}
function CanvasAPI3(CanvasKit) {
let skcanvas = CanvasKit.MakeCanvas(300, 300);
let realCanvas = document.getElementById('api3_c');
realCanvas.width = 300;
realCanvas.height = 300;
for (let canvas of [skcanvas, realCanvas]) {
let ctx = canvas.getContext('2d');
ctx.rect(10, 10, 20, 20);
ctx.scale(2.0, 4.0);
ctx.rect(30, 10, 20, 20);
ctx.resetTransform();
ctx.rotate(Math.PI / 3);
ctx.rect(50, 10, 20, 20);
ctx.resetTransform();
ctx.translate(30, -2);
ctx.rect(70, 10, 20, 20);
ctx.resetTransform();
ctx.translate(60, 0);
ctx.rotate(Math.PI / 6);
ctx.transform(1.5, 0, 0, 0.5, 0, 0, 0); // effectively scale
ctx.rect(90, 10, 20, 20);
ctx.resetTransform();
ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
ctx.rect(110, 10, 20, 20);
ctx.lineTo(110, 0);
ctx.resetTransform();
ctx.lineTo(220, 120);
ctx.scale(3.0, 3.0);
ctx.font = '6pt Arial';
ctx.fillText('This text should be huge', 10, 80);
ctx.resetTransform();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250, 30);
ctx.lineTo(250, 80);
ctx.scale(3.0, 3.0);
ctx.lineTo(280/3, 90/3);
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 5;
ctx.stroke();
}
document.getElementById('api3').src = skcanvas.toDataURL();
}
function NimaExample(CanvasKit, nimaFile, nimaTexture) {
if (!CanvasKit || !nimaFile || !nimaTexture) {
return;

View File

@ -281,7 +281,7 @@ bool ApplyTrim(SkPath& path, SkScalar startT, SkScalar stopT, bool isComplement)
}
struct StrokeOpts {
// Default values are set in chaining.js which allows clients
// Default values are set in interface.js which allows clients
// to set any number of them. Otherwise, the binding code complains if
// any are omitted.
SkScalar width;
@ -456,7 +456,9 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("rotate", select_overload<void (SkScalar, SkScalar, SkScalar)>(&SkCanvas::rotate))
.function("save", &SkCanvas::save)
.function("scale", &SkCanvas::scale)
.function("setMatrix", &SkCanvas::setMatrix)
.function("setMatrix", optional_override([](SkCanvas& self, const SimpleMatrix& m) {
self.setMatrix(toSkMatrix(m));
}))
.function("skew", &SkCanvas::skew)
.function("translate", &SkCanvas::translate);
@ -475,6 +477,11 @@ EMSCRIPTEN_BINDINGS(Skia) {
SkPaint p(self);
return p;
}))
.function("getStrokeWidth", &SkPaint::getStrokeWidth)
.function("getStrokeMiter", &SkPaint::getStrokeMiter)
.function("getStrokeCap", &SkPaint::getStrokeCap)
.function("getStrokeJoin", &SkPaint::getStrokeJoin)
.function("getTextSize", &SkPaint::getTextSize)
.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
@ -490,6 +497,9 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("setPathEffect", &SkPaint::setPathEffect)
.function("setShader", &SkPaint::setShader)
.function("setStrokeWidth", &SkPaint::setStrokeWidth)
.function("setStrokeMiter", &SkPaint::setStrokeMiter)
.function("setStrokeCap", &SkPaint::setStrokeCap)
.function("setStrokeJoin", &SkPaint::setStrokeJoin)
.function("setStyle", &SkPaint::setStyle)
.function("setTextSize", &SkPaint::setTextSize);
@ -507,7 +517,9 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("_arcTo", &ApplyArcTo)
.function("_close", &ApplyClose)
.function("_conicTo", &ApplyConicTo)
.function("countPoints", &SkPath::countPoints)
.function("_cubicTo", &ApplyCubicTo)
.function("getPoint", &SkPath::getPoint)
.function("_lineTo", &ApplyLineTo)
.function("_moveTo", &ApplyMoveTo)
.function("_quadTo", &ApplyQuadTo)
@ -530,7 +542,12 @@ EMSCRIPTEN_BINDINGS(Skia) {
.function("getBounds", &SkPath::getBounds)
.function("computeTightBounds", &SkPath::computeTightBounds)
.function("equals", &Equals)
.function("copy", &CopyPath);
.function("copy", &CopyPath)
#ifdef SK_DEBUG
.function("dump", select_overload<void() const>(&SkPath::dump))
.function("dumpHex", select_overload<void() const>(&SkPath::dumpHex))
#endif
;
class_<SkShader>("SkShader")
.smart_ptr<sk_sp<SkShader>>("sk_sp<SkShader>");
@ -617,6 +634,22 @@ EMSCRIPTEN_BINDINGS(Skia) {
.value("XOR", SkPathOp::kXOR_SkPathOp)
.value("ReverseDifference", SkPathOp::kReverseDifference_SkPathOp);
enum_<SkPaint::Cap>("StrokeCap")
.value("Butt", SkPaint::Cap::kButt_Cap)
.value("Round", SkPaint::Cap::kRound_Cap)
.value("Square", SkPaint::Cap::kSquare_Cap);
enum_<SkPaint::Join>("StrokeJoin")
.value("Miter", SkPaint::Join::kMiter_Join)
.value("Round", SkPaint::Join::kRound_Join)
.value("Bevel", SkPaint::Join::kBevel_Join);
value_object<StrokeOpts>("StrokeOpts")
.field("width", &StrokeOpts::width)
.field("miter_limit", &StrokeOpts::miter_limit)
.field("join", &StrokeOpts::join)
.field("cap", &StrokeOpts::cap);
enum_<SkShader::TileMode>("TileMode")
.value("Clamp", SkShader::TileMode::kClamp_TileMode)
.value("Repeat", SkShader::TileMode::kRepeat_TileMode)
@ -625,10 +658,10 @@ EMSCRIPTEN_BINDINGS(Skia) {
enum_<SkVertices::VertexMode>("VertexMode")
.value("Triangles", SkVertices::VertexMode::kTriangles_VertexMode)
.value("TrianglesStrip", SkVertices::VertexMode::kTriangleStrip_VertexMode)
.value("TriangleFan", SkVertices::VertexMode::kTriangleFan_VertexMode);
.value("TriangleFan", SkVertices::VertexMode::kTriangleFan_VertexMode);
enum_<SkEncodedImageFormat>("ImageFormat")
.value("PNG", SkEncodedImageFormat::kPNG)
.value("PNG", SkEncodedImageFormat::kPNG)
.value("JPEG", SkEncodedImageFormat::kJPEG);

View File

@ -97,11 +97,52 @@ var CanvasKit = {
},
SkMatrix: {
identity: function() {},
mapPoints: function() {},
multiply: function() {},
rotated: function() {},
scaled: function() {},
skewed: function() {},
translated: function() {},
},
SkPaint: {
// public API (from C++ bindings)
/** @return {CanvasKit.SkPaint} */
copy: function() {},
getStrokeCap: function() {},
getStrokeJoin: function() {},
getStrokeMiter: function() {},
getStrokeWidth: function() {},
getTextSize: function() {},
measureText: function() {},
setAntiAlias: function() {},
setColor: function() {},
setPathEffect: function() {},
setShader: function() {},
setStrokeCap: function() {},
setStrokeJoin: function() {},
setStrokeMiter: function() {},
setStrokeWidth: function() {},
setStyle: function() {},
setTextSize: function() {},
//private API
delete: function() {},
},
SkPath: {
// public API (from C++ bindings)
computeTightBounds: function() {},
/** @return {CanvasKit.SkPath} */
copy: function() {},
countPoints: function() {},
equals: function() {},
getBounds: function() {},
getFillType: function() {},
getPoint: function() {},
setFillType: function() {},
toSVGString: function() {},
// private API
_addArc: function() {},
@ -123,23 +164,8 @@ var CanvasKit = {
_transform: function() {},
_trim: 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() {},
dump: function() {},
dumpHex: function() {},
},
SkRect: {
@ -176,11 +202,6 @@ var CanvasKit = {
// Constants and Enums
gpu: {},
skottie: {},
PaintStyle: {
Fill: {},
Stroke: {},
StrokeAndFill: {},
},
FillType: {
Winding: {},
@ -194,6 +215,24 @@ var CanvasKit = {
JPEG: {},
},
PaintStyle: {
Fill: {},
Stroke: {},
StrokeAndFill: {},
},
StrokeCap: {
Butt: {},
Round: {},
Square: {},
},
StrokeJoin: {
Miter: {},
Round: {},
Bevel: {},
},
// Things Enscriptem adds for us
/** Represents the heap of the WASM code
@ -258,5 +297,38 @@ StrokeOpts.prototype.miter_limit;
StrokeOpts.prototype.cap;
StrokeOpts.prototype.join;
var HTMLCanvas = {};
HTMLCanvas.prototype.getContext = function() {};
HTMLCanvas.prototype.toDataURL = function() {};
HTMLCanvas.prototype.dispose = function() {};
var CanvasRenderingContext2D = {};
CanvasRenderingContext2D.prototype.addHitRegion = function() {};
CanvasRenderingContext2D.prototype.arc = function() {};
CanvasRenderingContext2D.prototype.arcTo = function() {};
CanvasRenderingContext2D.prototype.beginPath = function() {};
CanvasRenderingContext2D.prototype.bezierCurveTo = function() {};
CanvasRenderingContext2D.prototype.clearHitRegions = function() {};
CanvasRenderingContext2D.prototype.closePath = function() {};
CanvasRenderingContext2D.prototype.drawFocusIfNeeded = function() {};
CanvasRenderingContext2D.prototype.ellipse = function() {};
CanvasRenderingContext2D.prototype.fillText = function() {};
CanvasRenderingContext2D.prototype.lineTo = function() {};
CanvasRenderingContext2D.prototype.measureText = function() {};
CanvasRenderingContext2D.prototype.moveTo = function() {};
CanvasRenderingContext2D.prototype.quadraticCurveTo = function() {};
CanvasRenderingContext2D.prototype.rect = function() {};
CanvasRenderingContext2D.prototype.removeHitRegion = function() {};
CanvasRenderingContext2D.prototype.resetTransform = function() {};
CanvasRenderingContext2D.prototype.rotate = function() {};
CanvasRenderingContext2D.prototype.scale = function() {};
CanvasRenderingContext2D.prototype.scrollPathIntoView = function() {};
CanvasRenderingContext2D.prototype.setTransform = function() {};
CanvasRenderingContext2D.prototype.stroke = function() {};
CanvasRenderingContext2D.prototype.strokeText = function() {};
CanvasRenderingContext2D.prototype.transform = function() {};
CanvasRenderingContext2D.prototype.translate = function() {};
// Not sure why this is needed - might be a bug in emsdk that this isn't properly declared.
function loadWebAssemblyModule() {};

View File

@ -40,9 +40,9 @@
return null;
}
this.toDataURL = function(codec) {
// TODO(kjlubick): maybe support other codecs?
// For now, just to png
this.toDataURL = function(codec, quality) {
// TODO(kjlubick): maybe support other codecs (webp?)
// For now, just to png and jpeg
this._surface.flush();
var img = this._surface.makeImageSnapshot();
@ -50,14 +50,19 @@
SkDebug('no snapshot');
return;
}
var png = img.encodeToData();
if (!png) {
var codec = codec || 'image/png';
var format = CanvasKit.ImageFormat.PNG;
if (codec === 'image/jpeg') {
format = CanvasKit.ImageFormat.JPEG;
}
var quality = quality || 0.92;
var skimg = img.encodeToData(format, quality);
if (!skimg) {
SkDebug('encoding failure');
return
}
// TODO(kjlubick): clean this up a bit - maybe better naming?
var pngBytes = CanvasKit.getSkDataBytes(png);
return 'data:image/png;base64,' + toBase64String(pngBytes);
var imgBytes = CanvasKit.getSkDataBytes(skimg);
return 'data:' + codec + ';base64,' + toBase64String(imgBytes);
}
this.dispose = function() {
@ -70,15 +75,18 @@
this._canvas = skcanvas;
this._paint = new CanvasKit.SkPaint();
this._paint.setAntiAlias(true);
this._paint.setStrokeWidth(2);
this._currentSubPath = null;
this._paths = [];
this._pathStarted = false;
this._paint.setStrokeWidth(1);
this._paint.setStrokeMiter(10);
this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
this._currentPath = new CanvasKit.SkPath();
this._currentSubpath = null;
this._currentTransform = CanvasKit.SkMatrix.identity();
this._dispose = function() {
this._paths.forEach(function(path) {
path.delete();
});
this._currentPath.delete();
this._currentSubpath && this._currentSubpath.delete();
this._paint.delete();
// Don't delete this._canvas as it will be disposed
// by the surface of which it is based.
@ -121,32 +129,61 @@
if (!argsAreFinite(arguments)) {
return;
}
this._ensureSubpath(x1, y1);
if (radius < 0) {
throw 'radii cannot be negative';
}
this._currentSubPath.arcTo(x1, y1, x2, y2, radius);
var pts = [x1, y1, x2, y2];
CanvasKit.SkMatrix.mapPoints(this._currentTransform, pts);
x1 = pts[0];
y1 = pts[1];
x2 = pts[2];
y2 = pts[3];
if (!this._currentSubpath) {
this._newSubpath(x1, y1);
}
this._currentSubpath.arcTo(x1, y1, x2, y2, radius * this._scalefactor());
}
// As per the spec this doesn't begin any paths, it only
// clears out any previous subpaths.
this.beginPath = function() {
this._currentSubPath = new CanvasKit.SkPath();
this._paths.push(this._currentSubPath);
this._pathStarted = false;
this._currentPath.delete();
this._currentPath = new CanvasKit.SkPath();
this._currentSubpath && this._currentSubpath.delete();
this._currentSubpath = null;
}
this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
if (!argsAreFinite(arguments)) {
return;
}
this._ensureSubpath(cp1x, cp1y);
this._currentSubPath.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y);
var pts = [cp1x, cp1y, cp2x, cp2y, x, y];
CanvasKit.SkMatrix.mapPoints(this._currentTransform, pts);
cp1x = pts[0];
cp1y = pts[1];
cp2x = pts[2];
cp2y = pts[3];
x = pts[4];
y = pts[5];
if (!this._currentSubpath) {
this._newSubpath(cp1x, cp1y);
}
this._currentSubpath.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y);
}
this.closePath = function() {
if (this._currentSubPath) {
this._currentSubPath.close();
this._currentSubPath = null;
this._pathStarted = false;
if (this._currentSubpath) {
this._currentSubpath.close();
var lastPt = this._currentSubpath.getPoint(0);
this._newSubpath(lastPt[0], lastPt[1]);
}
}
this._commitSubpath = function () {
if (this._currentSubpath) {
this._currentPath.addPath(this._currentSubpath, false);
this._currentSubpath.delete();
this._currentSubpath = null;
}
}
@ -158,49 +195,46 @@
if (radiusX < 0 || radiusY < 0) {
throw 'radii cannot be negative';
}
this._pathStarted = true;
if (!this._currentSubpath) {
// Don't use newSubpath here because calculating the starting
// point in the arc is non-trivial. Just make a new, empty
// subpath to append to.
this._currentSubpath = new CanvasKit.SkPath();
}
var bounds = CanvasKit.LTRBRect(x-radiusX, y-radiusY, x+radiusX, y+radiusY);
var sweep = radiansToDegrees(endAngle - startAngle) - (360 * !!ccw);
var temp = new CanvasKit.SkPath();
// Skia takes degrees. JS tends to be radians.
temp.addArc(bounds, radiansToDegrees(startAngle), sweep);
var m = CanvasKit.SkMatrix.rotated(radiansToDegrees(rotation), x, y);
this._currentSubPath.addPath(temp, m, true);
temp.delete();
}
var m = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.rotated(rotation, x, y));
// ensureSubpath makes SkPath behave like the browser's path object
// in that the first lineTo 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 may be replaced
// with a moveTo).
this._ensureSubpath = function(x, y) {
if (!this._currentSubPath) {
this.beginPath();
}
if (!this._pathStarted) {
this._pathStarted = true;
this.moveTo(x, y);
return false;
}
return true;
this._currentSubpath.addPath(temp, m, true);
temp.delete();
}
this.fillText = function(text, x, y, maxWidth) {
// TODO do something with maxWidth, probably involving measure
this._canvas.setMatrix(this._currentTransform);
this._canvas.drawText(text, x, y, this._paint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
}
this.lineTo = function(x, y) {
if (!argsAreFinite(arguments)) {
return;
}
// lineTo is the odd-ball in the sense that a line-to without
// a previous subpath is the same as a moveTo.
if (this._ensureSubpath(x, y)) {
this._currentSubPath.lineTo(x, y);
var pts = [x, y];
CanvasKit.SkMatrix.mapPoints(this._currentTransform, pts);
x = pts[0];
y = pts[1];
// A lineTo without a previous subpath is turned into a moveTo
if (!this._currentSubpath) {
this._newSubpath(x, y);
} else {
this._currentSubpath.lineTo(x, y);
}
}
@ -215,77 +249,116 @@
if (!argsAreFinite(arguments)) {
return;
}
if (this._ensureSubpath(x, y)) {
this._currentSubPath.moveTo(x, y);
}
var pts = [x, y];
CanvasKit.SkMatrix.mapPoints(this._currentTransform, pts);
x = pts[0];
y = pts[1];
this._newSubpath(x, y);
}
this._newSubpath = function(x, y) {
this._commitSubpath();
this._currentSubpath = new CanvasKit.SkPath();
this._currentSubpath.moveTo(x, y);
}
this.quadraticCurveTo = function(cpx, cpy, x, y) {
if (!argsAreFinite(arguments)) {
return;
}
this._ensureSubpath(cpx, cpy);
this._currentSubPath.quadTo(cpx, cpy, x, y);
var pts = [cpx, cpy, x, y];
CanvasKit.SkMatrix.mapPoints(this._currentTransform, pts);
cpx = pts[0];
cpy = pts[1];
x = pts[2];
y = pts[3];
if (!this._currentSubpath) {
this._newSubpath(cpx, cpy);
}
this._currentSubpath.quadTo(cpx, cpy, x, y);
}
this.rect = function(x, y, width, height) {
if (!argsAreFinite(arguments)) {
return;
}
var pts = [x, y];
CanvasKit.SkMatrix.mapPoints(this._currentTransform, pts);
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-rect
this.beginPath();
this._currentSubPath.addRect(x, y, x+width, y+height);
this.beginPath();
this._currentSubPath.moveTo(x, y);
this._newSubpath(x, y);
var scale = this._scalefactor();
this._currentSubpath.addRect(x, y, x+width, y+height);
this._currentSubpath.transform(this._currentTransform);
this._newSubpath(pts[0], pts[1]);
}
this.resetTransform = function() {
this.setTransform(1, 0, 0, 1, 0, 0);
this._currentTransform = CanvasKit.SkMatrix.identity();
}
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._currentTransform = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.rotated(radians, px, py));
}
this.scale = function(sx, sy) {
this._canvas.scale(sx, sy);
this._currentTransform = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.scaled(sx, sy));
}
this.scale = function(sx, sy) {
this._canvas.scale(sx, sy);
this._scalefactor = function() {
// This is an approximation of what Chrome does when scaling up
// line width.
var m = this._currentTransform;
var sx = m[0];
var sy = m[4];
return (Math.abs(sx) + Math.abs(sy))/2;
}
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._currentTransform = [a, c, e,
b, d, f,
0, 0, 1];
}
this.stroke = function() {
if (this._currentSubPath) {
if (this._currentSubpath) {
this._commitSubpath();
this._paint.setStyle(CanvasKit.PaintStyle.Stroke);
this._paint.setLine
for (var i = 0; i < this._paths.length; i++) {
this._canvas.drawPath(this._paths[i], this._paint);
}
var orig = this._paint.getStrokeWidth();
// This is not in the spec, but it appears Chrome scales up
// the line width by some amount when stroking (and filling?).
var scaledWidth = orig * this._scalefactor();
this._paint.setStrokeWidth(scaledWidth);
this._canvas.drawPath(this._currentPath, this._paint);
// set stroke width back to original size:
this._paint.setStrokeWidth(orig);
}
}
this.strokeText = function(text, x, y, maxWidth) {
// TODO do something with maxWidth, probably involving measure
this._paint.setStyle(CanvasKit.PaintStyle.Stroke);
this._canvas.setMatrix(this._currentTransform);
this._canvas.drawText(text, x, y, this._paint);
this._canvas.setMatrix(CanvasKit.SkMatrix.identity());
}
this.translate = function(dx, dy) {
this._canvas.translate(dx, dy);
this._currentTransform = CanvasKit.SkMatrix.multiply(
this._currentTransform,
CanvasKit.SkMatrix.translated(dx, dy));
}
this.transform = function(a, b, c, d, e, f) {
this._currentTransform = CanvasKit.SkMatrix.multiply(
this._currentTransform,
[a, c, e,
b, d, f,
0, 0, 1]);
}
// Not supported operations (e.g. for Web only)
this.addHitRegion = function() {};
@ -323,7 +396,7 @@
SkDebug('Could not parse font size' + fontStr);
return 16;
}
var size = fontSize[1];
var size = parseFloat(fontSize[1]);
var unit = fontSize[2];
switch (unit) {
case 'pt':

View File

@ -19,23 +19,90 @@
return a * b + c * d + e * f;
}
// Return a matrix representing a rotation by n degrees.
CanvasKit.SkMatrix.identity = function() {
return [
1, 0, 0,
0, 1, 0,
0, 0, 1,
];
};
// Maps the given points according to the passed in matrix.
// Results are done in place.
// See SkMatrix.h::mapPoints for the docs on the math.
CanvasKit.SkMatrix.mapPoints = function(matrix, ptArr) {
if (ptArr.length % 2) {
throw 'mapPoints requires an even length arr';
}
for (var i = 0; i < ptArr.length; i+=2) {
var x = ptArr[i], y = ptArr[i+1];
// Gx+Hy+I
var denom = matrix[6]*x + matrix[7]*y + matrix[8];
// Ax+By+C
var xTrans = matrix[0]*x + matrix[1]*y + matrix[2];
// Dx+Ey+F
var yTrans = matrix[3]*x + matrix[4]*y + matrix[5];
ptArr[i] = xTrans/denom;
ptArr[i+1] = yTrans/denom;
}
return ptArr;
};
CanvasKit.SkMatrix.multiply = function(m1, m2) {
var result = [0,0,0, 0,0,0, 0,0,0];
for (var r = 0; r < 3; r++) {
for (var c = 0; c < 3; c++) {
// m1 and m2 are 1D arrays pretending to be 2D arrays
result[3*r + c] = sdot(m1[3*r + 0], m2[3*0 + c],
m1[3*r + 1], m2[3*1 + c],
m1[3*r + 2], m2[3*2 + c]);
}
}
return result;
}
// Return a matrix representing a rotation by n radians.
// px, py optionally say which point the rotation should be around
// with the default being (0, 0);
CanvasKit.SkMatrix.rotated = function(degrees, px, py) {
CanvasKit.SkMatrix.rotated = function(radians, px, py) {
px = px || 0;
py = py || 0;
var rad = degreesToRadians(degrees);
var sinV = Math.sin(rad);
var cosV = Math.cos(rad);
var sinV = Math.sin(radians);
var cosV = Math.cos(radians);
return [
cosV, -sinV, sdot( sinV, py, 1 - cosV, px),
sinV, cosV, sdot(-sinV, px, 1 - cosV, py),
0, 0, 1,
];
};
// TODO(kjlubick): translated, scaled
CanvasKit.SkMatrix.scaled = function(sx, sy, px, py) {
px = px || 0;
py = py || 0;
return [
sx, 0, px - sx * px,
0, sy, py - sy * py,
0, 0, 1,
];
};
CanvasKit.SkMatrix.skewed = function(kx, ky, px, py) {
px = px || 0;
py = py || 0;
return [
1, kx, -kx * px,
ky, 1, -ky * py,
0, 0, 1,
];
};
CanvasKit.SkMatrix.translated = function(dx, dy) {
return [
1, 0, dx,
0, 1, dy,
0, 0, 1,
];
};
CanvasKit.SkPath.prototype.addArc = function(oval, startAngle, sweepAngle) {
// see arc() for the HTMLCanvas version
@ -183,8 +250,8 @@
opts = opts || {};
opts.width = opts.width || 1;
opts.miter_limit = opts.miter_limit || 4;
opts.cap = opts.cap || CanvasKit.StrokeCap.BUTT;
opts.join = opts.join || CanvasKit.StrokeJoin.MITER;
opts.cap = opts.cap || CanvasKit.StrokeCap.Butt;
opts.join = opts.join || CanvasKit.StrokeJoin.Miter;
if (this._stroke(opts)) {
return this;
}

View File

@ -94,7 +94,7 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
test(canvas);
// canvas has .toDataURL (even though skcanvas is not a real Canvas)
// so this will work.
promises.push(reportCanvas(canvas, 'all_path_operations', canvas._config));
promises.push(reportCanvas(canvas, testname, canvas._config));
}
Promise.all(promises).then(() => {
skcanvas.dispose();
@ -107,8 +107,6 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
LoadCanvasKit.then(catchException(done, () => {
multipleCanvasTest('all_line_drawing_operations', done, (canvas) => {
let ctx = canvas.getContext('2d');
// TODO(kjlubick): scale doesn't work quite right yet, but putting
// it before all draws makes it consistent for now.
ctx.scale(3.0, 3.0);
ctx.moveTo(20, 5);
ctx.lineTo(30, 20);
@ -142,6 +140,58 @@ describe('CanvasKit\'s Canvas 2d Behavior', function() {
});
}));
});
it('handles all the transforms as specified', function(done) {
LoadCanvasKit.then(catchException(done, () => {
multipleCanvasTest('all_matrix_operations', done, (canvas) => {
let ctx = canvas.getContext('2d');
ctx.rect(10, 10, 20, 20);
ctx.scale(2.0, 4.0);
ctx.rect(30, 10, 20, 20);
ctx.resetTransform();
ctx.rotate(Math.PI / 3);
ctx.rect(50, 10, 20, 20);
ctx.resetTransform();
ctx.translate(30, -2);
ctx.rect(70, 10, 20, 20);
ctx.resetTransform();
ctx.translate(60, 0);
ctx.rotate(Math.PI / 6);
ctx.transform(1.5, 0, 0, 0.5, 0, 0, 0); // effectively scale
ctx.rect(90, 10, 20, 20);
ctx.resetTransform();
ctx.setTransform(2, 0, -.5, 2.5, -40, 120);
ctx.rect(110, 10, 20, 20);
ctx.lineTo(110, 0);
ctx.resetTransform();
ctx.lineTo(220, 120);
ctx.scale(3.0, 3.0);
ctx.font = '6pt Arial';
ctx.fillText('This text should be huge', 10, 80);
ctx.resetTransform();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(250, 30);
ctx.lineTo(250, 80);
ctx.scale(3.0, 3.0);
ctx.lineTo(280/3, 90/3);
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 5;
ctx.stroke();
});
}));
});
}); // end describe('Path drawing API')

View File

@ -0,0 +1 @@
//TODO test mapPoints and such