diff --git a/experimental/canvaskit/canvaskit/example.html b/experimental/canvaskit/canvaskit/example.html index 8b22bf4178..84bde8a3e2 100644 --- a/experimental/canvaskit/canvaskit/example.html +++ b/experimental/canvaskit/canvaskit/example.html @@ -17,6 +17,14 @@ +

Drop in replacement for HTML Canvas (e.g. node.js)

+ + + + + + +

CanvasKit draws Paths to the browser

@@ -38,11 +46,7 @@ -

Drop in replacement for HTML Canvas (e.g. node.js)

- - - - + @@ -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; diff --git a/experimental/canvaskit/canvaskit_bindings.cpp b/experimental/canvaskit/canvaskit_bindings.cpp index 5ddefeb809..04432f8051 100644 --- a/experimental/canvaskit/canvaskit_bindings.cpp +++ b/experimental/canvaskit/canvaskit_bindings.cpp @@ -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(&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(&SkPath::dump)) + .function("dumpHex", select_overload(&SkPath::dumpHex)) +#endif + ; class_("SkShader") .smart_ptr>("sk_sp"); @@ -617,6 +634,22 @@ EMSCRIPTEN_BINDINGS(Skia) { .value("XOR", SkPathOp::kXOR_SkPathOp) .value("ReverseDifference", SkPathOp::kReverseDifference_SkPathOp); + enum_("StrokeCap") + .value("Butt", SkPaint::Cap::kButt_Cap) + .value("Round", SkPaint::Cap::kRound_Cap) + .value("Square", SkPaint::Cap::kSquare_Cap); + + enum_("StrokeJoin") + .value("Miter", SkPaint::Join::kMiter_Join) + .value("Round", SkPaint::Join::kRound_Join) + .value("Bevel", SkPaint::Join::kBevel_Join); + + value_object("StrokeOpts") + .field("width", &StrokeOpts::width) + .field("miter_limit", &StrokeOpts::miter_limit) + .field("join", &StrokeOpts::join) + .field("cap", &StrokeOpts::cap); + enum_("TileMode") .value("Clamp", SkShader::TileMode::kClamp_TileMode) .value("Repeat", SkShader::TileMode::kRepeat_TileMode) @@ -625,10 +658,10 @@ EMSCRIPTEN_BINDINGS(Skia) { enum_("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_("ImageFormat") - .value("PNG", SkEncodedImageFormat::kPNG) + .value("PNG", SkEncodedImageFormat::kPNG) .value("JPEG", SkEncodedImageFormat::kJPEG); diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js index 2c60c37dc3..c2758e4a62 100644 --- a/experimental/canvaskit/externs.js +++ b/experimental/canvaskit/externs.js @@ -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() {}; diff --git a/experimental/canvaskit/htmlcanvas/canvas2d.js b/experimental/canvaskit/htmlcanvas/canvas2d.js index 7b7e1a5f7c..0325b34e37 100644 --- a/experimental/canvaskit/htmlcanvas/canvas2d.js +++ b/experimental/canvaskit/htmlcanvas/canvas2d.js @@ -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': diff --git a/experimental/canvaskit/interface.js b/experimental/canvaskit/interface.js index febdffceb5..248189650c 100644 --- a/experimental/canvaskit/interface.js +++ b/experimental/canvaskit/interface.js @@ -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; } diff --git a/experimental/canvaskit/tests/canvas2d.spec.js b/experimental/canvaskit/tests/canvas2d.spec.js index d2b3f6601b..03e744b275 100644 --- a/experimental/canvaskit/tests/canvas2d.spec.js +++ b/experimental/canvaskit/tests/canvas2d.spec.js @@ -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') diff --git a/experimental/canvaskit/tests/matrix.spec.js b/experimental/canvaskit/tests/matrix.spec.js new file mode 100644 index 0000000000..eff2d4ee6c --- /dev/null +++ b/experimental/canvaskit/tests/matrix.spec.js @@ -0,0 +1 @@ +//TODO test mapPoints and such \ No newline at end of file