[canvaskit] Refactor Canvas2D JS into own files
Rather than one monolithic file, we now have one monolithic
file (canvascontext2d) and several smaller files (one per class,
and some helpers).
This should make the code navigation a little easier.
Bug: skia:
Change-Id: Ia191c2db778591af21d2a6126f053c17c4f677f1
Reviewed-on: https://skia-review.googlesource.com/c/175996
Reviewed-by: Florin Malita <fmalita@chromium.org>
diff --git a/experimental/canvaskit/htmlcanvas/canvas2dcontext.js b/experimental/canvaskit/htmlcanvas/canvas2dcontext.js
new file mode 100644
index 0000000..72a44fa
--- /dev/null
+++ b/experimental/canvaskit/htmlcanvas/canvas2dcontext.js
@@ -0,0 +1,1182 @@
+function CanvasRenderingContext2D(skcanvas) {
+ this._canvas = skcanvas;
+ this._paint = new CanvasKit.SkPaint();
+ this._paint.setAntiAlias(true);
+
+ this._paint.setStrokeMiter(10);
+ this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
+ this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
+
+ this._strokeStyle = CanvasKit.BLACK;
+ this._fillStyle = CanvasKit.BLACK;
+ this._shadowBlur = 0;
+ this._shadowColor = CanvasKit.TRANSPARENT;
+ this._shadowOffsetX = 0;
+ this._shadowOffsetY = 0;
+ this._globalAlpha = 1;
+ this._strokeWidth = 1;
+ this._lineDashOffset = 0;
+ this._lineDashList = [];
+ // aka SkBlendMode
+ this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
+ this._imageFilterQuality = CanvasKit.FilterQuality.Low;
+ this._imageSmoothingEnabled = true;
+
+ this._paint.setStrokeWidth(this._strokeWidth);
+ this._paint.setBlendMode(this._globalCompositeOperation);
+
+ this._currentPath = new CanvasKit.SkPath();
+ this._currentTransform = CanvasKit.SkMatrix.identity();
+
+ // Use this for save/restore
+ this._canvasStateStack = [];
+ // Keep a reference to all the effects (e.g. gradients, patterns)
+ // that were allocated for cleanup in _dispose.
+ this._toCleanUp = [];
+
+ this._dispose = function() {
+ this._currentPath.delete();
+ this._paint.delete();
+ this._toCleanUp.forEach(function(c) {
+ c._dispose();
+ });
+ // Don't delete this._canvas as it will be disposed
+ // by the surface of which it is based.
+ }
+
+ // This always accepts DOMMatrix/SVGMatrix or any other
+ // object that has properties a,b,c,d,e,f defined.
+ // Returns a DOM-Matrix like dictionary
+ Object.defineProperty(this, 'currentTransform', {
+ enumerable: true,
+ get: function() {
+ return {
+ 'a' : this._currentTransform[0],
+ 'c' : this._currentTransform[1],
+ 'e' : this._currentTransform[2],
+ 'b' : this._currentTransform[3],
+ 'd' : this._currentTransform[4],
+ 'f' : this._currentTransform[5],
+ };
+ },
+ // @param {DOMMatrix} matrix
+ set: function(matrix) {
+ if (matrix.a) {
+ // if we see a property named 'a', guess that b-f will
+ // also be there.
+ this.setTransform(matrix.a, matrix.b, matrix.c,
+ matrix.d, matrix.e, matrix.f);
+ }
+ }
+ });
+
+ Object.defineProperty(this, 'fillStyle', {
+ enumerable: true,
+ get: function() {
+ if (Number.isInteger(this._fillStyle)) {
+ return colorToString(this._fillStyle);
+ }
+ return this._fillStyle;
+ },
+ set: function(newStyle) {
+ if (typeof newStyle === 'string') {
+ this._fillStyle = parseColor(newStyle);
+ } else if (newStyle._getShader) {
+ // It's an effect that has a shader.
+ this._fillStyle = newStyle
+ }
+ }
+ });
+
+ Object.defineProperty(this, 'font', {
+ enumerable: true,
+ get: function(newStyle) {
+ // TODO generate this
+ return '10px sans-serif';
+ },
+ set: function(newStyle) {
+ var size = parseFontSize(newStyle);
+ // TODO(kjlubick) styles, font name
+ this._paint.setTextSize(size);
+ }
+ });
+
+ Object.defineProperty(this, 'globalAlpha', {
+ enumerable: true,
+ get: function() {
+ return this._globalAlpha;
+ },
+ set: function(newAlpha) {
+ // ignore invalid values, as per the spec
+ if (!isFinite(newAlpha) || newAlpha < 0 || newAlpha > 1) {
+ return;
+ }
+ this._globalAlpha = newAlpha;
+ }
+ });
+
+ Object.defineProperty(this, 'globalCompositeOperation', {
+ enumerable: true,
+ get: function() {
+ switch (this._globalCompositeOperation) {
+ // composite-mode
+ case CanvasKit.BlendMode.SrcOver:
+ return 'source-over';
+ case CanvasKit.BlendMode.DstOver:
+ return 'destination-over';
+ case CanvasKit.BlendMode.Src:
+ return 'copy';
+ case CanvasKit.BlendMode.Dst:
+ return 'destination';
+ case CanvasKit.BlendMode.Clear:
+ return 'clear';
+ case CanvasKit.BlendMode.SrcIn:
+ return 'source-in';
+ case CanvasKit.BlendMode.DstIn:
+ return 'destination-in';
+ case CanvasKit.BlendMode.SrcOut:
+ return 'source-out';
+ case CanvasKit.BlendMode.DstOut:
+ return 'destination-out';
+ case CanvasKit.BlendMode.SrcATop:
+ return 'source-atop';
+ case CanvasKit.BlendMode.DstATop:
+ return 'destination-atop';
+ case CanvasKit.BlendMode.Xor:
+ return 'xor';
+ case CanvasKit.BlendMode.Plus:
+ return 'lighter';
+
+ case CanvasKit.BlendMode.Multiply:
+ return 'multiply';
+ case CanvasKit.BlendMode.Screen:
+ return 'screen';
+ case CanvasKit.BlendMode.Overlay:
+ return 'overlay';
+ case CanvasKit.BlendMode.Darken:
+ return 'darken';
+ case CanvasKit.BlendMode.Lighten:
+ return 'lighten';
+ case CanvasKit.BlendMode.ColorDodge:
+ return 'color-dodge';
+ case CanvasKit.BlendMode.ColorBurn:
+ return 'color-burn';
+ case CanvasKit.BlendMode.HardLight:
+ return 'hard-light';
+ case CanvasKit.BlendMode.SoftLight:
+ return 'soft-light';
+ case CanvasKit.BlendMode.Difference:
+ return 'difference';
+ case CanvasKit.BlendMode.Exclusion:
+ return 'exclusion';
+ case CanvasKit.BlendMode.Hue:
+ return 'hue';
+ case CanvasKit.BlendMode.Saturation:
+ return 'saturation';
+ case CanvasKit.BlendMode.Color:
+ return 'color';
+ case CanvasKit.BlendMode.Luminosity:
+ return 'luminosity';
+ }
+ },
+ set: function(newMode) {
+ switch (newMode) {
+ // composite-mode
+ case 'source-over':
+ this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
+ break;
+ case 'destination-over':
+ this._globalCompositeOperation = CanvasKit.BlendMode.DstOver;
+ break;
+ case 'copy':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Src;
+ break;
+ case 'destination':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Dst;
+ break;
+ case 'clear':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Clear;
+ break;
+ case 'source-in':
+ this._globalCompositeOperation = CanvasKit.BlendMode.SrcIn;
+ break;
+ case 'destination-in':
+ this._globalCompositeOperation = CanvasKit.BlendMode.DstIn;
+ break;
+ case 'source-out':
+ this._globalCompositeOperation = CanvasKit.BlendMode.SrcOut;
+ break;
+ case 'destination-out':
+ this._globalCompositeOperation = CanvasKit.BlendMode.DstOut;
+ break;
+ case 'source-atop':
+ this._globalCompositeOperation = CanvasKit.BlendMode.SrcATop;
+ break;
+ case 'destination-atop':
+ this._globalCompositeOperation = CanvasKit.BlendMode.DstATop;
+ break;
+ case 'xor':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Xor;
+ break;
+ case 'lighter':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
+ break;
+ case 'plus-lighter':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
+ break;
+ case 'plus-darker':
+ throw 'plus-darker is not supported';
+
+ // blend-mode
+ case 'multiply':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Multiply;
+ break;
+ case 'screen':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Screen;
+ break;
+ case 'overlay':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Overlay;
+ break;
+ case 'darken':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Darken;
+ break;
+ case 'lighten':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Lighten;
+ break;
+ case 'color-dodge':
+ this._globalCompositeOperation = CanvasKit.BlendMode.ColorDodge;
+ break;
+ case 'color-burn':
+ this._globalCompositeOperation = CanvasKit.BlendMode.ColorBurn;
+ break;
+ case 'hard-light':
+ this._globalCompositeOperation = CanvasKit.BlendMode.HardLight;
+ break;
+ case 'soft-light':
+ this._globalCompositeOperation = CanvasKit.BlendMode.SoftLight;
+ break;
+ case 'difference':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Difference;
+ break;
+ case 'exclusion':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Exclusion;
+ break;
+ case 'hue':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Hue;
+ break;
+ case 'saturation':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Saturation;
+ break;
+ case 'color':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Color;
+ break;
+ case 'luminosity':
+ this._globalCompositeOperation = CanvasKit.BlendMode.Luminosity;
+ break;
+ default:
+ return;
+ }
+ this._paint.setBlendMode(this._globalCompositeOperation);
+ }
+ });
+
+ Object.defineProperty(this, 'imageSmoothingEnabled', {
+ enumerable: true,
+ get: function() {
+ return this._imageSmoothingEnabled;
+ },
+ set: function(newVal) {
+ this._imageSmoothingEnabled = !!newVal;
+ }
+ });
+
+ Object.defineProperty(this, 'imageSmoothingQuality', {
+ enumerable: true,
+ get: function() {
+ switch (this._imageFilterQuality) {
+ case CanvasKit.FilterQuality.Low:
+ return 'low';
+ case CanvasKit.FilterQuality.Medium:
+ return 'medium';
+ case CanvasKit.FilterQuality.High:
+ return 'high';
+ }
+ },
+ set: function(newQuality) {
+ switch (newQuality) {
+ case 'low':
+ this._imageFilterQuality = CanvasKit.FilterQuality.Low;
+ return;
+ case 'medium':
+ this._imageFilterQuality = CanvasKit.FilterQuality.Medium;
+ return;
+ case 'high':
+ this._imageFilterQuality = CanvasKit.FilterQuality.High;
+ return;
+ }
+ }
+ });
+
+ Object.defineProperty(this, 'lineCap', {
+ enumerable: true,
+ get: function() {
+ switch (this._paint.getStrokeCap()) {
+ case CanvasKit.StrokeCap.Butt:
+ return 'butt';
+ case CanvasKit.StrokeCap.Round:
+ return 'round';
+ case CanvasKit.StrokeCap.Square:
+ return 'square';
+ }
+ },
+ set: function(newCap) {
+ switch (newCap) {
+ case 'butt':
+ this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
+ return;
+ case 'round':
+ this._paint.setStrokeCap(CanvasKit.StrokeCap.Round);
+ return;
+ case 'square':
+ this._paint.setStrokeCap(CanvasKit.StrokeCap.Square);
+ return;
+ }
+ }
+ });
+
+ Object.defineProperty(this, 'lineDashOffset', {
+ enumerable: true,
+ get: function() {
+ return this._lineDashOffset;
+ },
+ set: function(newOffset) {
+ if (!isFinite(newOffset)) {
+ return;
+ }
+ this._lineDashOffset = newOffset;
+ }
+ });
+
+ Object.defineProperty(this, 'lineJoin', {
+ enumerable: true,
+ get: function() {
+ switch (this._paint.getStrokeJoin()) {
+ case CanvasKit.StrokeJoin.Miter:
+ return 'miter';
+ case CanvasKit.StrokeJoin.Round:
+ return 'round';
+ case CanvasKit.StrokeJoin.Bevel:
+ return 'bevel';
+ }
+ },
+ set: function(newJoin) {
+ switch (newJoin) {
+ case 'miter':
+ this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
+ return;
+ case 'round':
+ this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Round);
+ return;
+ case 'bevel':
+ this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Bevel);
+ return;
+ }
+ }
+ });
+
+ Object.defineProperty(this, 'lineWidth', {
+ enumerable: true,
+ get: function() {
+ return this._paint.getStrokeWidth();
+ },
+ set: function(newWidth) {
+ if (newWidth <= 0 || !newWidth) {
+ // Spec says to ignore NaN/Inf/0/negative values
+ return;
+ }
+ this._strokeWidth = newWidth;
+ this._paint.setStrokeWidth(newWidth);
+ }
+ });
+
+ Object.defineProperty(this, 'miterLimit', {
+ enumerable: true,
+ get: function() {
+ return this._paint.getStrokeMiter();
+ },
+ set: function(newLimit) {
+ if (newLimit <= 0 || !newLimit) {
+ // Spec says to ignore NaN/Inf/0/negative values
+ return;
+ }
+ this._paint.setStrokeMiter(newLimit);
+ }
+ });
+
+ Object.defineProperty(this, 'shadowBlur', {
+ enumerable: true,
+ get: function() {
+ return this._shadowBlur;
+ },
+ set: function(newBlur) {
+ // ignore negative, inf and NAN (but not 0) as per the spec.
+ if (newBlur < 0 || !isFinite(newBlur)) {
+ return;
+ }
+ this._shadowBlur = newBlur;
+ }
+ });
+
+ Object.defineProperty(this, 'shadowColor', {
+ enumerable: true,
+ get: function() {
+ return colorToString(this._shadowColor);
+ },
+ set: function(newColor) {
+ this._shadowColor = parseColor(newColor);
+ }
+ });
+
+ Object.defineProperty(this, 'shadowOffsetX', {
+ enumerable: true,
+ get: function() {
+ return this._shadowOffsetX;
+ },
+ set: function(newOffset) {
+ if (!isFinite(newOffset)) {
+ return;
+ }
+ this._shadowOffsetX = newOffset;
+ }
+ });
+
+ Object.defineProperty(this, 'shadowOffsetY', {
+ enumerable: true,
+ get: function() {
+ return this._shadowOffsetY;
+ },
+ set: function(newOffset) {
+ if (!isFinite(newOffset)) {
+ return;
+ }
+ this._shadowOffsetY = newOffset;
+ }
+ });
+
+ Object.defineProperty(this, 'strokeStyle', {
+ enumerable: true,
+ get: function() {
+ return colorToString(this._strokeStyle);
+ },
+ set: function(newStyle) {
+ if (typeof newStyle === 'string') {
+ this._strokeStyle = parseColor(newStyle);
+ } else if (newStyle._getShader) {
+ // It's probably an effect.
+ this._strokeStyle = newStyle
+ }
+ }
+ });
+
+ this.arc = function(x, y, radius, startAngle, endAngle, ccw) {
+ // As per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-arc
+ // arc is essentially a simpler version of ellipse.
+ this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, ccw);
+ }
+
+ this.arcTo = function(x1, y1, x2, y2, radius) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ if (radius < 0) {
+ throw 'radii cannot be negative';
+ }
+ if (this._currentPath.isEmpty()) {
+ this.moveTo(x1, y1);
+ }
+ this._currentPath.arcTo(x1, y1, x2, y2, radius);
+ }
+
+ // As per the spec this doesn't begin any paths, it only
+ // clears out any previous paths.
+ this.beginPath = function() {
+ this._currentPath.delete();
+ this._currentPath = new CanvasKit.SkPath();
+ }
+
+ this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ if (this._currentPath.isEmpty()) {
+ this.moveTo(cp1x, cp1y);
+ }
+ this._currentPath.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y);
+ }
+
+ this.clearRect = function(x, y, width, height) {
+ this._paint.setStyle(CanvasKit.PaintStyle.Fill);
+ this._paint.setBlendMode(CanvasKit.BlendMode.Clear);
+ this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), this._paint);
+ this._paint.setBlendMode(this._globalCompositeOperation);
+ }
+
+ this.clip = function(fillRule) {
+ var clip = this._currentPath.copy();
+ if (fillRule && fillRule.toLowerCase() === 'evenodd') {
+ clip.setFillType(CanvasKit.FillType.EvenOdd);
+ } else {
+ clip.setFillType(CanvasKit.FillType.Winding);
+ }
+ this._canvas.clipPath(clip, CanvasKit.ClipOp.Intersect, true);
+ }
+
+ this.closePath = function() {
+ if (this._currentPath.isEmpty()) {
+ return;
+ }
+ // Check to see if we are not just a single point
+ var bounds = this._currentPath.getBounds();
+ if ((bounds.fBottom - bounds.fTop) || (bounds.fRight - bounds.fLeft)) {
+ this._currentPath.close();
+ }
+ }
+
+ this.createImageData = function() {
+ // either takes in 1 or 2 arguments:
+ // - imagedata on which to copy *width* and *height* only
+ // - width, height
+ if (arguments.length === 1) {
+ var oldData = arguments[0];
+ var byteLength = 4 * oldData.width * oldData.height;
+ return new ImageData(new Uint8ClampedArray(byteLength),
+ oldData.width, oldData.height);
+ } else if (arguments.length === 2) {
+ var width = arguments[0];
+ var height = arguments[1];
+ var byteLength = 4 * width * height;
+ return new ImageData(new Uint8ClampedArray(byteLength),
+ width, height);
+ } else {
+ throw 'createImageData expects 1 or 2 arguments, got '+arguments.length;
+ }
+ }
+
+ this.createLinearGradient = function(x1, y1, x2, y2) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ var lcg = new LinearCanvasGradient(x1, y1, x2, y2);
+ this._toCleanUp.push(lcg);
+ return lcg;
+ }
+
+ this.createPattern = function(image, repetition) {
+ var cp = new CanvasPattern(image, repetition);
+ this._toCleanUp.push(cp);
+ return cp;
+ }
+
+ this.createRadialGradient = function(x1, y1, r1, x2, y2, r2) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2);
+ this._toCleanUp.push(rcg);
+ return rcg;
+ }
+
+ this._imagePaint = function() {
+ var iPaint = this._fillPaint();
+ if (!this._imageSmoothingEnabled) {
+ iPaint.setFilterQuality(CanvasKit.FilterQuality.None);
+ } else {
+ iPaint.setFilterQuality(this._imageFilterQuality);
+ }
+ return iPaint;
+ }
+
+ this.drawImage = function(img) {
+ // 3 potential sets of arguments
+ // - image, dx, dy
+ // - image, dx, dy, dWidth, dHeight
+ // - image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
+ // use the fillPaint, which has the globalAlpha in it
+ // which drawImageRect will use.
+ var iPaint = this._imagePaint();
+ if (arguments.length === 3 || arguments.length === 5) {
+ var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
+ arguments[3] || img.width(), arguments[4] || img.height());
+ var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height());
+ } else if (arguments.length === 9){
+ var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6],
+ arguments[7], arguments[8]);
+ var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
+ arguments[3], arguments[4]);
+ } else {
+ throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length;
+ }
+ this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false);
+
+ iPaint.dispose();
+ }
+
+ this._ellipseHelper = function(x, y, radiusX, radiusY, startAngle, endAngle) {
+ var sweepDegrees = radiansToDegrees(endAngle - startAngle);
+ var startDegrees = radiansToDegrees(startAngle);
+
+ var oval = CanvasKit.LTRBRect(x - radiusX, y - radiusY, x + radiusX, y + radiusY);
+
+ // draw in 2 180 degree segments because trying to draw all 360 degrees at once
+ // draws nothing.
+ if (almostEqual(Math.abs(sweepDegrees), 360)) {
+ var halfSweep = sweepDegrees/2;
+ this._currentPath.arcTo(oval, startDegrees, halfSweep, false);
+ this._currentPath.arcTo(oval, startDegrees + halfSweep, halfSweep, false);
+ return;
+ }
+ this._currentPath.arcTo(oval, startDegrees, sweepDegrees, false);
+ }
+
+ this.ellipse = function(x, y, radiusX, radiusY, rotation,
+ startAngle, endAngle, ccw) {
+ if (!allAreFinite([x, y, radiusX, radiusY, rotation, startAngle, endAngle])) {
+ return;
+ }
+ if (radiusX < 0 || radiusY < 0) {
+ throw 'radii cannot be negative';
+ }
+
+ // based off of CanonicalizeAngle in Chrome
+ var tao = 2 * Math.PI;
+ var newStartAngle = startAngle % tao;
+ if (newStartAngle < 0) {
+ newStartAngle += tao;
+ }
+ var delta = newStartAngle - startAngle;
+ startAngle = newStartAngle;
+ endAngle += delta;
+
+ // Based off of AdjustEndAngle in Chrome.
+ if (!ccw && (endAngle - startAngle) >= tao) {
+ // Draw complete ellipse
+ endAngle = startAngle + tao;
+ } else if (ccw && (startAngle - endAngle) >= tao) {
+ // Draw complete ellipse
+ endAngle = startAngle - tao;
+ } else if (!ccw && startAngle > endAngle) {
+ endAngle = startAngle + (tao - (startAngle - endAngle) % tao);
+ } else if (ccw && startAngle < endAngle) {
+ endAngle = startAngle - (tao - (endAngle - startAngle) % tao);
+ }
+
+
+ // Based off of Chrome's implementation in
+ // https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/graphics/path.cc
+ // of note, can't use addArc or addOval because they close the arc, which
+ // the spec says not to do (unless the user explicitly calls closePath).
+ // This throws off points being in/out of the arc.
+ if (!rotation) {
+ this._ellipseHelper(x, y, radiusX, radiusY, startAngle, endAngle);
+ return;
+ }
+ var rotated = CanvasKit.SkMatrix.rotated(rotation, x, y);
+ this._currentPath.transform(CanvasKit.SkMatrix.invert(rotated));
+ this._ellipseHelper(x, y, radiusX, radiusY, startAngle, endAngle);
+ this._currentPath.transform(rotated);
+ }
+
+ // A helper to copy the current paint, ready for filling
+ // This applies the global alpha.
+ // Call dispose() after to clean up.
+ this._fillPaint = function() {
+ var paint = this._paint.copy();
+ paint.setStyle(CanvasKit.PaintStyle.Fill);
+ if (Number.isInteger(this._fillStyle)) {
+ var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha);
+ paint.setColor(alphaColor);
+ } else {
+ var shader = this._fillStyle._getShader(this._currentTransform);
+ paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
+ paint.setShader(shader);
+ }
+
+ paint.dispose = function() {
+ // If there are some helper effects in the future, clean them up
+ // here. In any case, we have .dispose() to make _fillPaint behave
+ // like _strokePaint and _shadowPaint.
+ this.delete();
+ }
+ return paint;
+ }
+
+ this.fill = function(fillRule) {
+ if (fillRule === 'evenodd') {
+ this._currentPath.setFillType(CanvasKit.FillType.EvenOdd);
+ } else if (fillRule === 'nonzero' || !fillRule) {
+ this._currentPath.setFillType(CanvasKit.FillType.Winding);
+ } else {
+ throw 'invalid fill rule';
+ }
+ var fillPaint = this._fillPaint();
+
+ var shadowPaint = this._shadowPaint(fillPaint);
+ if (shadowPaint) {
+ this._canvas.save();
+ this._canvas.concat(this._shadowOffsetMatrix());
+ this._canvas.drawPath(this._currentPath, shadowPaint);
+ this._canvas.restore();
+ shadowPaint.dispose();
+ }
+ this._canvas.drawPath(this._currentPath, fillPaint);
+ fillPaint.dispose();
+ }
+
+ this.fillRect = function(x, y, width, height) {
+ var fillPaint = this._fillPaint();
+ this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint);
+ fillPaint.dispose();
+ }
+
+ this.fillText = function(text, x, y, maxWidth) {
+ // TODO do something with maxWidth, probably involving measure
+ var fillPaint = this._fillPaint()
+ var shadowPaint = this._shadowPaint(fillPaint);
+ if (shadowPaint) {
+ this._canvas.save();
+ this._canvas.concat(this._shadowOffsetMatrix());
+ this._canvas.drawText(text, x, y, shadowPaint);
+ this._canvas.restore();
+ shadowPaint.dispose();
+ }
+ this._canvas.drawText(text, x, y, fillPaint);
+ fillPaint.dispose();
+ }
+
+ this.getImageData = function(x, y, w, h) {
+ var pixels = this._canvas.readPixels(x, y, w, h);
+ if (!pixels) {
+ return null;
+ }
+ // This essentially re-wraps the pixels from a Uint8Array to
+ // a Uint8ClampedArray (without making a copy of pixels).
+ return new ImageData(
+ new Uint8ClampedArray(pixels.buffer),
+ w, h);
+ }
+
+ this.getLineDash = function() {
+ return this._lineDashList.slice();
+ }
+
+ this._mapToLocalCoordinates = function(pts) {
+ var inverted = CanvasKit.SkMatrix.invert(this._currentTransform);
+ CanvasKit.SkMatrix.mapPoints(inverted, pts);
+ return pts;
+ }
+
+ this.isPointInPath = function(x, y, fillmode) {
+ if (!isFinite(x) || !isFinite(y)) {
+ return false;
+ }
+ fillmode = fillmode || 'nonzero';
+ if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) {
+ return false;
+ }
+ // x and y are in canvas coordinates (i.e. unaffected by CTM)
+ var pts = this._mapToLocalCoordinates([x, y]);
+ x = pts[0];
+ y = pts[1];
+ this._currentPath.setFillType(fillmode === 'nonzero' ?
+ CanvasKit.FillType.Winding :
+ CanvasKit.FillType.EvenOdd);
+ return this._currentPath.contains(x, y);
+ }
+
+ this.isPointInStroke = function(x, y) {
+ if (!isFinite(x) || !isFinite(y)) {
+ return false;
+ }
+ var pts = this._mapToLocalCoordinates([x, y]);
+ x = pts[0];
+ y = pts[1];
+ var temp = this._currentPath.copy();
+ // fillmode is always nonzero
+ temp.setFillType(CanvasKit.FillType.Winding);
+ temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit,
+ 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(),
+ 'precision': 0.3, // this is what Chrome uses to compute this
+ });
+ var retVal = temp.contains(x, y);
+ temp.delete();
+ return retVal;
+ }
+
+ this.lineTo = function(x, y) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ // A lineTo without a previous point has a moveTo inserted before it
+ if (this._currentPath.isEmpty()) {
+ this._currentPath.moveTo(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 (!allAreFinite(arguments)) {
+ return;
+ }
+ this._currentPath.moveTo(x, y);
+ }
+
+ this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
+ if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) {
+ return;
+ }
+ if (dirtyX === undefined) {
+ // fast, simple path for basic call
+ this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y);
+ return;
+ }
+ dirtyX = dirtyX || 0;
+ dirtyY = dirtyY || 0;
+ dirtyWidth = dirtyWidth || imageData.width;
+ dirtyHeight = dirtyHeight || imageData.height;
+
+ // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata
+ if (dirtyWidth < 0) {
+ dirtyX = dirtyX+dirtyWidth;
+ dirtyWidth = Math.abs(dirtyWidth);
+ }
+ if (dirtyHeight < 0) {
+ dirtyY = dirtyY+dirtyHeight;
+ dirtyHeight = Math.abs(dirtyHeight);
+ }
+ if (dirtyX < 0) {
+ dirtyWidth = dirtyWidth + dirtyX;
+ dirtyX = 0;
+ }
+ if (dirtyY < 0) {
+ dirtyHeight = dirtyHeight + dirtyY;
+ dirtyY = 0;
+ }
+ if (dirtyWidth <= 0 || dirtyHeight <= 0) {
+ return;
+ }
+ var img = CanvasKit.MakeImage(imageData.data, imageData.width, imageData.height,
+ CanvasKit.AlphaType.Unpremul,
+ CanvasKit.ColorType.RGBA_8888);
+ var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
+ var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight);
+ var inverted = CanvasKit.SkMatrix.invert(this._currentTransform);
+ this._canvas.save();
+ // putImageData() operates in device space.
+ this._canvas.concat(inverted);
+ this._canvas.drawImageRect(img, src, dst, null, false);
+ this._canvas.restore();
+ img.delete();
+ }
+
+ this.quadraticCurveTo = function(cpx, cpy, x, y) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ if (this._currentPath.isEmpty()) {
+ this._currentPath.moveTo(cpx, cpy);
+ }
+ this._currentPath.quadTo(cpx, cpy, x, y);
+ }
+
+ this.rect = function(x, y, width, height) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-rect
+ this._currentPath.addRect(x, y, x+width, y+height);
+ }
+
+ this.resetTransform = function() {
+ // Apply the current transform to the path and then reset
+ // to the identity. Essentially "commit" the transform.
+ this._currentPath.transform(this._currentTransform);
+ var inverted = CanvasKit.SkMatrix.invert(this._currentTransform);
+ this._canvas.concat(inverted);
+ // This should be identity, modulo floating point drift.
+ this._currentTransform = this._canvas.getTotalMatrix();
+ }
+
+ this.restore = function() {
+ var newState = this._canvasStateStack.pop();
+ if (!newState) {
+ return;
+ }
+ // "commit" the current transform. We pop, then apply the inverse of the
+ // popped state, which has the effect of applying just the delta of
+ // transforms between old and new.
+ var combined = CanvasKit.SkMatrix.multiply(
+ this._currentTransform,
+ CanvasKit.SkMatrix.invert(newState.ctm)
+ );
+ this._currentPath.transform(combined);
+
+ this._lineDashList = newState.ldl;
+ this._strokeWidth = newState.sw;
+ this._paint.setStrokeWidth(this._strokeWidth);
+ this._strokeStyle = newState.ss;
+ this._fillStyle = newState.fs;
+ this._paint.setStrokeCap(newState.cap);
+ this._paint.setStrokeJoin(newState.jn);
+ this._paint.setStrokeMiter(newState.mtr);
+ this._shadowOffsetX = newState.sox;
+ this._shadowOffsetY = newState.soy;
+ this._shadowBlur = newState.sb;
+ this._shadowColor = newState.shc;
+ this._globalAlpha = newState.ga;
+ this._globalCompositeOperation = newState.gco;
+ this._paint.setBlendMode(this._globalCompositeOperation);
+ this._lineDashOffset = newState.ldo;
+ this._imageSmoothingEnabled = newState.ise;
+ this._imageFilterQuality = newState.isq;
+ //TODO: font, textAlign, textBaseline, direction
+
+ // restores the clip and ctm
+ this._canvas.restore();
+ this._currentTransform = this._canvas.getTotalMatrix();
+ }
+
+ this.rotate = function(radians) {
+ if (!isFinite(radians)) {
+ return;
+ }
+ // retroactively apply the inverse of this transform to the previous
+ // path so it cancels out when we apply the transform at draw time.
+ var inverted = CanvasKit.SkMatrix.rotated(-radians);
+ this._currentPath.transform(inverted);
+ this._canvas.rotate(radiansToDegrees(radians), 0, 0);
+ this._currentTransform = this._canvas.getTotalMatrix();
+ }
+
+ this.save = function() {
+ if (this._fillStyle._copy) {
+ var fs = this._fillStyle._copy();
+ this._toCleanUp.push(fs);
+ } else {
+ var fs = this._fillStyle;
+ }
+
+ if (this._strokeStyle._copy) {
+ var ss = this._strokeStyle._copy();
+ this._toCleanUp.push(ss);
+ } else {
+ var ss = this._strokeStyle;
+ }
+
+ this._canvasStateStack.push({
+ ctm: this._currentTransform.slice(),
+ ldl: this._lineDashList.slice(),
+ sw: this._strokeWidth,
+ ss: ss,
+ fs: fs,
+ cap: this._paint.getStrokeCap(),
+ jn: this._paint.getStrokeJoin(),
+ mtr: this._paint.getStrokeMiter(),
+ sox: this._shadowOffsetX,
+ soy: this._shadowOffsetY,
+ sb: this._shadowBlur,
+ shc: this._shadowColor,
+ ga: this._globalAlpha,
+ ldo: this._lineDashOffset,
+ gco: this._globalCompositeOperation,
+ ise: this._imageSmoothingEnabled,
+ isq: this._imageFilterQuality,
+ //TODO: font, textAlign, textBaseline, direction
+ });
+ // Saves the clip
+ this._canvas.save();
+ }
+
+ this.scale = function(sx, sy) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ // retroactively apply the inverse of this transform to the previous
+ // path so it cancels out when we apply the transform at draw time.
+ var inverted = CanvasKit.SkMatrix.scaled(1/sx, 1/sy);
+ this._currentPath.transform(inverted);
+ this._canvas.scale(sx, sy);
+ this._currentTransform = this._canvas.getTotalMatrix();
+ }
+
+ this.setLineDash = function(dashes) {
+ for (var i = 0; i < dashes.length; i++) {
+ if (!isFinite(dashes[i]) || dashes[i] < 0) {
+ SkDebug('dash list must have positive, finite values');
+ return;
+ }
+ }
+ if (dashes.length % 2 === 1) {
+ // as per the spec, concatenate 2 copies of dashes
+ // to give it an even number of elements.
+ Array.prototype.push.apply(dashes, dashes);
+ }
+ this._lineDashList = dashes;
+ }
+
+ this.setTransform = function(a, b, c, d, e, f) {
+ if (!(allAreFinite(arguments))) {
+ return;
+ }
+ this.resetTransform();
+ this.transform(a, b, c, d, e, f);
+ }
+
+ // Returns the matrix representing the offset of the shadows. This unapplies
+ // the effects of the scale, which should not affect the shadow offsets.
+ this._shadowOffsetMatrix = function() {
+ var sx = this._currentTransform[0];
+ var sy = this._currentTransform[4];
+ return CanvasKit.SkMatrix.translated(this._shadowOffsetX/sx, this._shadowOffsetY/sy);
+ }
+
+ // Returns the shadow paint for the current settings or null if there
+ // should be no shadow. This ends up being a copy of the given
+ // paint with a blur maskfilter and the correct color.
+ this._shadowPaint = function(basePaint) {
+ // multiply first to see if the alpha channel goes to 0 after multiplication.
+ var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha);
+ // if alpha is zero, no shadows
+ if (!CanvasKit.getColorComponents(alphaColor)[3]) {
+ return null;
+ }
+ // one of these must also be non-zero (otherwise the shadow is
+ // completely hidden. And the spec says so).
+ if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
+ return null;
+ }
+ var shadowPaint = basePaint.copy();
+ shadowPaint.setColor(alphaColor);
+ var blurEffect = CanvasKit.MakeBlurMaskFilter(CanvasKit.BlurStyle.Normal,
+ Math.max(1, this._shadowBlur/2), // very little blur when < 1
+ false);
+ shadowPaint.setMaskFilter(blurEffect);
+
+ // hack up a "destructor" which also cleans up the blurEffect. Otherwise,
+ // we leak the blurEffect (since smart pointers don't help us in JS land).
+ shadowPaint.dispose = function() {
+ blurEffect.delete();
+ this.delete();
+ };
+ return shadowPaint;
+ }
+
+ // A helper to get a copy of the current paint, ready for stroking.
+ // This applies the global alpha and the dashedness.
+ // Call dispose() after to clean up.
+ this._strokePaint = function() {
+ var paint = this._paint.copy();
+ paint.setStyle(CanvasKit.PaintStyle.Stroke);
+ if (Number.isInteger(this._strokeStyle)) {
+ var alphaColor = CanvasKit.multiplyByAlpha(this._strokeStyle, this._globalAlpha);
+ paint.setColor(alphaColor);
+ } else {
+ var shader = this._strokeStyle._getShader(this._currentTransform);
+ paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
+ paint.setShader(shader);
+ }
+
+ paint.setStrokeWidth(this._strokeWidth);
+
+ if (this._lineDashList.length) {
+ var dashedEffect = CanvasKit.MakeSkDashPathEffect(this._lineDashList, this._lineDashOffset);
+ paint.setPathEffect(dashedEffect);
+ }
+
+ paint.dispose = function() {
+ dashedEffect && dashedEffect.delete();
+ this.delete();
+ }
+ return paint;
+ }
+
+ this.stroke = function() {
+ var strokePaint = this._strokePaint();
+
+ var shadowPaint = this._shadowPaint(strokePaint);
+ if (shadowPaint) {
+ this._canvas.save();
+ this._canvas.concat(this._shadowOffsetMatrix());
+ this._canvas.drawPath(this._currentPath, shadowPaint);
+ this._canvas.restore();
+ shadowPaint.dispose();
+ }
+
+ this._canvas.drawPath(this._currentPath, strokePaint);
+ strokePaint.dispose();
+ }
+
+ this.strokeRect = function(x, y, width, height) {
+ var strokePaint = this._strokePaint();
+ this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint);
+ strokePaint.dispose();
+ }
+
+ this.strokeText = function(text, x, y, maxWidth) {
+ // TODO do something with maxWidth, probably involving measure
+ var strokePaint = this._strokePaint();
+
+ var shadowPaint = this._shadowPaint(strokePaint);
+ if (shadowPaint) {
+ this._canvas.save();
+ this._canvas.concat(this._shadowOffsetMatrix());
+ this._canvas.drawText(text, x, y, shadowPaint);
+ this._canvas.restore();
+ shadowPaint.dispose();
+ }
+ this._canvas.drawText(text, x, y, strokePaint);
+ strokePaint.dispose();
+ }
+
+ this.translate = function(dx, dy) {
+ if (!allAreFinite(arguments)) {
+ return;
+ }
+ // retroactively apply the inverse of this transform to the previous
+ // path so it cancels out when we apply the transform at draw time.
+ var inverted = CanvasKit.SkMatrix.translated(-dx, -dy);
+ this._currentPath.transform(inverted);
+ this._canvas.translate(dx, dy);
+ this._currentTransform = this._canvas.getTotalMatrix();
+ }
+
+ this.transform = function(a, b, c, d, e, f) {
+ var newTransform = [a, c, e,
+ b, d, f,
+ 0, 0, 1];
+ // retroactively apply the inverse of this transform to the previous
+ // path so it cancels out when we apply the transform at draw time.
+ var inverted = CanvasKit.SkMatrix.invert(newTransform);
+ this._currentPath.transform(inverted);
+ this._canvas.concat(newTransform);
+ this._currentTransform = this._canvas.getTotalMatrix();
+ }
+
+ // Not supported operations (e.g. for Web only)
+ this.addHitRegion = function() {};
+ this.clearHitRegions = function() {};
+ this.drawFocusIfNeeded = function() {};
+ this.removeHitRegion = function() {};
+ this.scrollPathIntoView = function() {};
+
+ Object.defineProperty(this, 'canvas', {
+ value: null,
+ writable: false
+ });
+}