• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1function CanvasRenderingContext2D(skcanvas) {
2  this._canvas = skcanvas;
3  this._paint = new CanvasKit.Paint();
4  this._paint.setAntiAlias(true);
5
6  this._paint.setStrokeMiter(10);
7  this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
8  this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
9  this._fontString = '10px monospace';
10
11  this._font = new CanvasKit.Font(null, 10);
12  this._font.setSubpixel(true);
13
14  this._strokeStyle    = CanvasKit.BLACK;
15  this._fillStyle      = CanvasKit.BLACK;
16  this._shadowBlur     = 0;
17  this._shadowColor    = CanvasKit.TRANSPARENT;
18  this._shadowOffsetX  = 0;
19  this._shadowOffsetY  = 0;
20  this._globalAlpha    = 1;
21  this._strokeWidth    = 1;
22  this._lineDashOffset = 0;
23  this._lineDashList   = [];
24  // aka BlendMode
25  this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
26
27  this._paint.setStrokeWidth(this._strokeWidth);
28  this._paint.setBlendMode(this._globalCompositeOperation);
29
30  this._currentPath = new CanvasKit.Path();
31  this._currentTransform = CanvasKit.Matrix.identity();
32
33  // Use this for save/restore
34  this._canvasStateStack = [];
35  // Keep a reference to all the effects (e.g. gradients, patterns)
36  // that were allocated for cleanup in _dispose.
37  this._toCleanUp = [];
38
39  this._dispose = function() {
40    this._currentPath.delete();
41    this._paint.delete();
42    this._font.delete();
43    this._toCleanUp.forEach(function(c) {
44      c._dispose();
45    });
46    // Don't delete this._canvas as it will be disposed
47    // by the surface of which it is based.
48  };
49
50  // This always accepts DOMMatrix/SVGMatrix or any other
51  // object that has properties a,b,c,d,e,f defined.
52  // Returns a DOM-Matrix like dictionary
53  Object.defineProperty(this, 'currentTransform', {
54    enumerable: true,
55    get: function() {
56      return {
57        'a' : this._currentTransform[0],
58        'c' : this._currentTransform[1],
59        'e' : this._currentTransform[2],
60        'b' : this._currentTransform[3],
61        'd' : this._currentTransform[4],
62        'f' : this._currentTransform[5],
63      };
64    },
65    // @param {DOMMatrix} matrix
66    set: function(matrix) {
67      if (matrix.a) {
68        // if we see a property named 'a', guess that b-f will
69        // also be there.
70        this.setTransform(matrix.a, matrix.b, matrix.c,
71                          matrix.d, matrix.e, matrix.f);
72      }
73    }
74  });
75
76  Object.defineProperty(this, 'fillStyle', {
77    enumerable: true,
78    get: function() {
79      if (isCanvasKitColor(this._fillStyle)) {
80        return colorToString(this._fillStyle);
81      }
82      return this._fillStyle;
83    },
84    set: function(newStyle) {
85      if (typeof newStyle === 'string') {
86        this._fillStyle = parseColor(newStyle);
87      } else if (newStyle._getShader) {
88        // It's an effect that has a shader.
89        this._fillStyle = newStyle
90      }
91    }
92  });
93
94  Object.defineProperty(this, 'font', {
95    enumerable: true,
96    get: function() {
97      return this._fontString;
98    },
99    set: function(newFont) {
100      var tf = getTypeface(newFont);
101      if (tf) {
102        // tf is a "dict" according to closure, that is, the field
103        // names are not minified. Thus, we need to access it via
104        // bracket notation to tell closure not to minify these names.
105        this._font.setSize(tf['sizePx']);
106        this._font.setTypeface(tf['typeface']);
107        this._fontString = newFont;
108      }
109    }
110  });
111
112  Object.defineProperty(this, 'globalAlpha', {
113    enumerable: true,
114    get: function() {
115      return this._globalAlpha;
116    },
117    set: function(newAlpha) {
118      // ignore invalid values, as per the spec
119      if (!isFinite(newAlpha) || newAlpha < 0 || newAlpha > 1) {
120        return;
121      }
122      this._globalAlpha = newAlpha;
123    }
124  });
125
126  Object.defineProperty(this, 'globalCompositeOperation', {
127    enumerable: true,
128    get: function() {
129      switch (this._globalCompositeOperation) {
130        // composite-mode
131        case CanvasKit.BlendMode.SrcOver:
132          return 'source-over';
133        case CanvasKit.BlendMode.DstOver:
134          return 'destination-over';
135        case CanvasKit.BlendMode.Src:
136          return 'copy';
137        case CanvasKit.BlendMode.Dst:
138          return 'destination';
139        case CanvasKit.BlendMode.Clear:
140          return 'clear';
141        case CanvasKit.BlendMode.SrcIn:
142          return 'source-in';
143        case CanvasKit.BlendMode.DstIn:
144          return 'destination-in';
145        case CanvasKit.BlendMode.SrcOut:
146          return 'source-out';
147        case CanvasKit.BlendMode.DstOut:
148          return 'destination-out';
149        case CanvasKit.BlendMode.SrcATop:
150          return 'source-atop';
151        case CanvasKit.BlendMode.DstATop:
152          return 'destination-atop';
153        case CanvasKit.BlendMode.Xor:
154          return 'xor';
155        case CanvasKit.BlendMode.Plus:
156          return 'lighter';
157
158        case CanvasKit.BlendMode.Multiply:
159          return 'multiply';
160        case CanvasKit.BlendMode.Screen:
161          return 'screen';
162        case CanvasKit.BlendMode.Overlay:
163          return 'overlay';
164        case CanvasKit.BlendMode.Darken:
165          return 'darken';
166        case CanvasKit.BlendMode.Lighten:
167          return 'lighten';
168        case CanvasKit.BlendMode.ColorDodge:
169          return 'color-dodge';
170        case CanvasKit.BlendMode.ColorBurn:
171          return 'color-burn';
172        case CanvasKit.BlendMode.HardLight:
173          return 'hard-light';
174        case CanvasKit.BlendMode.SoftLight:
175          return 'soft-light';
176        case CanvasKit.BlendMode.Difference:
177          return 'difference';
178        case CanvasKit.BlendMode.Exclusion:
179          return 'exclusion';
180        case CanvasKit.BlendMode.Hue:
181          return 'hue';
182        case CanvasKit.BlendMode.Saturation:
183          return 'saturation';
184        case CanvasKit.BlendMode.Color:
185          return 'color';
186        case CanvasKit.BlendMode.Luminosity:
187          return 'luminosity';
188      }
189    },
190    set: function(newMode) {
191      switch (newMode) {
192        // composite-mode
193        case 'source-over':
194          this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver;
195          break;
196        case 'destination-over':
197          this._globalCompositeOperation = CanvasKit.BlendMode.DstOver;
198          break;
199        case 'copy':
200          this._globalCompositeOperation = CanvasKit.BlendMode.Src;
201          break;
202        case 'destination':
203          this._globalCompositeOperation = CanvasKit.BlendMode.Dst;
204          break;
205        case 'clear':
206          this._globalCompositeOperation = CanvasKit.BlendMode.Clear;
207          break;
208        case 'source-in':
209          this._globalCompositeOperation = CanvasKit.BlendMode.SrcIn;
210          break;
211        case 'destination-in':
212          this._globalCompositeOperation = CanvasKit.BlendMode.DstIn;
213          break;
214        case 'source-out':
215          this._globalCompositeOperation = CanvasKit.BlendMode.SrcOut;
216          break;
217        case 'destination-out':
218          this._globalCompositeOperation = CanvasKit.BlendMode.DstOut;
219          break;
220        case 'source-atop':
221          this._globalCompositeOperation = CanvasKit.BlendMode.SrcATop;
222          break;
223        case 'destination-atop':
224          this._globalCompositeOperation = CanvasKit.BlendMode.DstATop;
225          break;
226        case 'xor':
227          this._globalCompositeOperation = CanvasKit.BlendMode.Xor;
228          break;
229        case 'lighter':
230          this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
231          break;
232        case 'plus-lighter':
233          this._globalCompositeOperation = CanvasKit.BlendMode.Plus;
234          break;
235        case 'plus-darker':
236          throw 'plus-darker is not supported';
237
238        // blend-mode
239        case 'multiply':
240          this._globalCompositeOperation = CanvasKit.BlendMode.Multiply;
241          break;
242        case 'screen':
243          this._globalCompositeOperation = CanvasKit.BlendMode.Screen;
244          break;
245        case 'overlay':
246          this._globalCompositeOperation = CanvasKit.BlendMode.Overlay;
247          break;
248        case 'darken':
249          this._globalCompositeOperation = CanvasKit.BlendMode.Darken;
250          break;
251        case 'lighten':
252          this._globalCompositeOperation = CanvasKit.BlendMode.Lighten;
253          break;
254        case 'color-dodge':
255          this._globalCompositeOperation = CanvasKit.BlendMode.ColorDodge;
256          break;
257        case 'color-burn':
258          this._globalCompositeOperation = CanvasKit.BlendMode.ColorBurn;
259          break;
260        case 'hard-light':
261          this._globalCompositeOperation = CanvasKit.BlendMode.HardLight;
262          break;
263        case 'soft-light':
264          this._globalCompositeOperation = CanvasKit.BlendMode.SoftLight;
265          break;
266        case 'difference':
267          this._globalCompositeOperation = CanvasKit.BlendMode.Difference;
268          break;
269        case 'exclusion':
270          this._globalCompositeOperation = CanvasKit.BlendMode.Exclusion;
271          break;
272        case 'hue':
273          this._globalCompositeOperation = CanvasKit.BlendMode.Hue;
274          break;
275        case 'saturation':
276          this._globalCompositeOperation = CanvasKit.BlendMode.Saturation;
277          break;
278        case 'color':
279          this._globalCompositeOperation = CanvasKit.BlendMode.Color;
280          break;
281        case 'luminosity':
282          this._globalCompositeOperation = CanvasKit.BlendMode.Luminosity;
283          break;
284        default:
285          return;
286      }
287      this._paint.setBlendMode(this._globalCompositeOperation);
288    }
289  });
290
291  Object.defineProperty(this, 'imageSmoothingEnabled', {
292    enumerable: true,
293    get: function() {
294      return true;
295    },
296    set: function(a) {
297      // ignored, we always use high quality image smoothing.
298    }
299  });
300
301  Object.defineProperty(this, 'imageSmoothingQuality', {
302    enumerable: true,
303    get: function() {
304          return 'high';
305    },
306    set: function(a) {
307      // ignored, we always use high quality image smoothing.
308    }
309  });
310
311  Object.defineProperty(this, 'lineCap', {
312    enumerable: true,
313    get: function() {
314      switch (this._paint.getStrokeCap()) {
315        case CanvasKit.StrokeCap.Butt:
316          return 'butt';
317        case CanvasKit.StrokeCap.Round:
318          return 'round';
319        case CanvasKit.StrokeCap.Square:
320          return 'square';
321      }
322    },
323    set: function(newCap) {
324      switch (newCap) {
325        case 'butt':
326          this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
327          return;
328        case 'round':
329          this._paint.setStrokeCap(CanvasKit.StrokeCap.Round);
330          return;
331        case 'square':
332          this._paint.setStrokeCap(CanvasKit.StrokeCap.Square);
333          return;
334      }
335    }
336  });
337
338  Object.defineProperty(this, 'lineDashOffset', {
339    enumerable: true,
340    get: function() {
341      return this._lineDashOffset;
342    },
343    set: function(newOffset) {
344      if (!isFinite(newOffset)) {
345        return;
346      }
347      this._lineDashOffset = newOffset;
348    }
349  });
350
351  Object.defineProperty(this, 'lineJoin', {
352    enumerable: true,
353    get: function() {
354      switch (this._paint.getStrokeJoin()) {
355        case CanvasKit.StrokeJoin.Miter:
356          return 'miter';
357        case CanvasKit.StrokeJoin.Round:
358          return 'round';
359        case CanvasKit.StrokeJoin.Bevel:
360          return 'bevel';
361      }
362    },
363    set: function(newJoin) {
364      switch (newJoin) {
365        case 'miter':
366          this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
367          return;
368        case 'round':
369          this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Round);
370          return;
371        case 'bevel':
372          this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Bevel);
373          return;
374      }
375    }
376  });
377
378  Object.defineProperty(this, 'lineWidth', {
379    enumerable: true,
380    get: function() {
381      return this._paint.getStrokeWidth();
382    },
383    set: function(newWidth) {
384      if (newWidth <= 0 || !newWidth) {
385        // Spec says to ignore NaN/Inf/0/negative values
386        return;
387      }
388      this._strokeWidth = newWidth;
389      this._paint.setStrokeWidth(newWidth);
390    }
391  });
392
393  Object.defineProperty(this, 'miterLimit', {
394    enumerable: true,
395    get: function() {
396      return this._paint.getStrokeMiter();
397    },
398    set: function(newLimit) {
399      if (newLimit <= 0 || !newLimit) {
400        // Spec says to ignore NaN/Inf/0/negative values
401        return;
402      }
403      this._paint.setStrokeMiter(newLimit);
404    }
405  });
406
407  Object.defineProperty(this, 'shadowBlur', {
408    enumerable: true,
409    get: function() {
410      return this._shadowBlur;
411    },
412    set: function(newBlur) {
413      // ignore negative, inf and NAN (but not 0) as per the spec.
414      if (newBlur < 0 || !isFinite(newBlur)) {
415        return;
416      }
417      this._shadowBlur = newBlur;
418    }
419  });
420
421  Object.defineProperty(this, 'shadowColor', {
422    enumerable: true,
423    get: function() {
424      return colorToString(this._shadowColor);
425    },
426    set: function(newColor) {
427      this._shadowColor = parseColor(newColor);
428    }
429  });
430
431  Object.defineProperty(this, 'shadowOffsetX', {
432    enumerable: true,
433    get: function() {
434      return this._shadowOffsetX;
435    },
436    set: function(newOffset) {
437      if (!isFinite(newOffset)) {
438        return;
439      }
440      this._shadowOffsetX = newOffset;
441    }
442  });
443
444  Object.defineProperty(this, 'shadowOffsetY', {
445    enumerable: true,
446    get: function() {
447      return this._shadowOffsetY;
448    },
449    set: function(newOffset) {
450      if (!isFinite(newOffset)) {
451        return;
452      }
453      this._shadowOffsetY = newOffset;
454    }
455  });
456
457  Object.defineProperty(this, 'strokeStyle', {
458    enumerable: true,
459    get: function() {
460      return colorToString(this._strokeStyle);
461    },
462    set: function(newStyle) {
463      if (typeof newStyle === 'string') {
464        this._strokeStyle = parseColor(newStyle);
465      } else if (newStyle._getShader) {
466        // It's probably an effect.
467        this._strokeStyle = newStyle
468      }
469    }
470  });
471
472  this.arc = function(x, y, radius, startAngle, endAngle, ccw) {
473    arc(this._currentPath, x, y, radius, startAngle, endAngle, ccw);
474  };
475
476  this.arcTo = function(x1, y1, x2, y2, radius) {
477    arcTo(this._currentPath, x1, y1, x2, y2, radius);
478  };
479
480  // As per the spec this doesn't begin any paths, it only
481  // clears out any previous paths.
482  this.beginPath = function() {
483    this._currentPath.delete();
484    this._currentPath = new CanvasKit.Path();
485  };
486
487  this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
488    bezierCurveTo(this._currentPath, cp1x, cp1y, cp2x, cp2y, x, y);
489  };
490
491  this.clearRect = function(x, y, width, height) {
492    this._paint.setStyle(CanvasKit.PaintStyle.Fill);
493    this._paint.setBlendMode(CanvasKit.BlendMode.Clear);
494    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), this._paint);
495    this._paint.setBlendMode(this._globalCompositeOperation);
496  };
497
498  this.clip = function(path, fillRule) {
499    if (typeof path === 'string') {
500      // shift the args if a Path2D is supplied
501      fillRule = path;
502      path = this._currentPath;
503    } else if (path && path._getPath) {
504      path = path._getPath();
505    }
506    if (!path) {
507      path = this._currentPath;
508    }
509
510    var clip = path.copy();
511    if (fillRule && fillRule.toLowerCase() === 'evenodd') {
512      clip.setFillType(CanvasKit.FillType.EvenOdd);
513    } else {
514      clip.setFillType(CanvasKit.FillType.Winding);
515    }
516    this._canvas.clipPath(clip, CanvasKit.ClipOp.Intersect, true);
517    clip.delete();
518  };
519
520  this.closePath = function() {
521    closePath(this._currentPath);
522  };
523
524  this.createImageData = function() {
525    // either takes in 1 or 2 arguments:
526    //  - imagedata on which to copy *width* and *height* only
527    //  - width, height
528    if (arguments.length === 1) {
529      var oldData = arguments[0];
530      var byteLength = 4 * oldData.width * oldData.height;
531      return new ImageData(new Uint8ClampedArray(byteLength),
532                           oldData.width, oldData.height);
533    } else if (arguments.length === 2) {
534      var width = arguments[0];
535      var height = arguments[1];
536      var byteLength = 4 * width * height;
537      return new ImageData(new Uint8ClampedArray(byteLength),
538                           width, height);
539    } else {
540      throw 'createImageData expects 1 or 2 arguments, got '+arguments.length;
541    }
542  };
543
544  this.createLinearGradient = function(x1, y1, x2, y2) {
545    if (!allAreFinite(arguments)) {
546      return;
547    }
548    var lcg = new LinearCanvasGradient(x1, y1, x2, y2);
549    this._toCleanUp.push(lcg);
550    return lcg;
551  };
552
553  this.createPattern = function(image, repetition) {
554    var cp = new CanvasPattern(image, repetition);
555    this._toCleanUp.push(cp);
556    return cp;
557  };
558
559  this.createRadialGradient = function(x1, y1, r1, x2, y2, r2) {
560    if (!allAreFinite(arguments)) {
561      return;
562    }
563    var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2);
564    this._toCleanUp.push(rcg);
565    return rcg;
566  };
567
568  this.drawImage = function(img) {
569    // 3 potential sets of arguments
570    // - image, dx, dy
571    // - image, dx, dy, dWidth, dHeight
572    // - image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
573    // use the fillPaint, which has the globalAlpha in it
574    // which drawImageRect will use.
575    var iPaint = this._fillPaint();
576    if (arguments.length === 3 || arguments.length === 5) {
577      var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
578                        arguments[3] || img.width(), arguments[4] || img.height());
579      var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height());
580    } else if (arguments.length === 9){
581      var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6],
582                                        arguments[7], arguments[8]);
583      var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
584                                       arguments[3], arguments[4]);
585    } else {
586      throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length;
587    }
588    this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false);
589
590    iPaint.dispose();
591  };
592
593  this.ellipse = function(x, y, radiusX, radiusY, rotation,
594                          startAngle, endAngle, ccw) {
595    ellipse(this._currentPath, x, y, radiusX, radiusY, rotation,
596            startAngle, endAngle, ccw);
597  };
598
599  // A helper to copy the current paint, ready for filling
600  // This applies the global alpha.
601  // Call dispose() after to clean up.
602  this._fillPaint = function() {
603    var paint = this._paint.copy();
604    paint.setStyle(CanvasKit.PaintStyle.Fill);
605    if (isCanvasKitColor(this._fillStyle)) {
606      var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha);
607      paint.setColor(alphaColor);
608    } else {
609      var shader = this._fillStyle._getShader(this._currentTransform);
610      paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
611      paint.setShader(shader);
612    }
613
614    paint.dispose = function() {
615      // If there are some helper effects in the future, clean them up
616      // here. In any case, we have .dispose() to make _fillPaint behave
617      // like _strokePaint and _shadowPaint.
618      this.delete();
619    };
620    return paint;
621  };
622
623  this.fill = function(path, fillRule) {
624    if (typeof path === 'string') {
625      // shift the args if a Path2D is supplied
626      fillRule = path;
627      path = this._currentPath;
628    } else if (path && path._getPath) {
629      path = path._getPath();
630    }
631    if (fillRule === 'evenodd') {
632      this._currentPath.setFillType(CanvasKit.FillType.EvenOdd);
633    } else if (fillRule === 'nonzero' || !fillRule) {
634      this._currentPath.setFillType(CanvasKit.FillType.Winding);
635    } else {
636      throw 'invalid fill rule';
637    }
638    if (!path) {
639      path = this._currentPath;
640    }
641
642    var fillPaint = this._fillPaint();
643
644    var shadowPaint = this._shadowPaint(fillPaint);
645    if (shadowPaint) {
646      this._canvas.save();
647      this._applyShadowOffsetMatrix();
648      this._canvas.drawPath(path, shadowPaint);
649      this._canvas.restore();
650      shadowPaint.dispose();
651    }
652    this._canvas.drawPath(path, fillPaint);
653    fillPaint.dispose();
654  };
655
656  this.fillRect = function(x, y, width, height) {
657    var fillPaint = this._fillPaint();
658
659    var shadowPaint = this._shadowPaint(fillPaint);
660    if (shadowPaint) {
661      this._canvas.save();
662      this._applyShadowOffsetMatrix();
663      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint);
664      this._canvas.restore();
665      shadowPaint.dispose();
666    }
667
668    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint);
669    fillPaint.dispose();
670  };
671
672  this.fillText = function(text, x, y, maxWidth) {
673    // TODO do something with maxWidth, probably involving measure
674    var fillPaint = this._fillPaint();
675    var blob = CanvasKit.TextBlob.MakeFromText(text, this._font);
676
677    var shadowPaint = this._shadowPaint(fillPaint);
678    if (shadowPaint) {
679      this._canvas.save();
680      this._applyShadowOffsetMatrix();
681      this._canvas.drawTextBlob(blob, x, y, shadowPaint);
682      this._canvas.restore();
683      shadowPaint.dispose();
684    }
685    this._canvas.drawTextBlob(blob, x, y, fillPaint);
686    blob.delete();
687    fillPaint.dispose();
688  };
689
690  this.getImageData = function(x, y, w, h) {
691    var pixels = this._canvas.readPixels(x, y, {
692        'width': w,
693        'height': h,
694        'colorType': CanvasKit.ColorType.RGBA_8888,
695        'alphaType': CanvasKit.AlphaType.Unpremul,
696        'colorSpace': CanvasKit.ColorSpace.SRGB,
697    });
698    if (!pixels) {
699      return null;
700    }
701    // This essentially re-wraps the pixels from a Uint8Array to
702    // a Uint8ClampedArray (without making a copy of pixels).
703    return new ImageData(
704      new Uint8ClampedArray(pixels.buffer),
705      w, h);
706  };
707
708  this.getLineDash = function() {
709    return this._lineDashList.slice();
710  };
711
712  this._mapToLocalCoordinates = function(pts) {
713    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
714    CanvasKit.Matrix.mapPoints(inverted, pts);
715    return pts;
716  };
717
718  this.isPointInPath = function(x, y, fillmode) {
719    var args = arguments;
720    if (args.length === 3) {
721      var path = this._currentPath;
722    } else if (args.length === 4) {
723      var path = args[0];
724      x = args[1];
725      y = args[2];
726      fillmode = args[3];
727    } else {
728      throw 'invalid arg count, need 3 or 4, got ' + args.length;
729    }
730    if (!isFinite(x) || !isFinite(y)) {
731      return false;
732    }
733    fillmode = fillmode || 'nonzero';
734    if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) {
735      return false;
736    }
737    // x and y are in canvas coordinates (i.e. unaffected by CTM)
738    var pts = this._mapToLocalCoordinates([x, y]);
739    x = pts[0];
740    y = pts[1];
741    path.setFillType(fillmode === 'nonzero' ?
742                                  CanvasKit.FillType.Winding :
743                                  CanvasKit.FillType.EvenOdd);
744    return path.contains(x, y);
745  };
746
747  this.isPointInStroke = function(x, y) {
748    var args = arguments;
749    if (args.length === 2) {
750      var path = this._currentPath;
751    } else if (args.length === 3) {
752      var path = args[0];
753      x = args[1];
754      y = args[2];
755    } else {
756      throw 'invalid arg count, need 2 or 3, got ' + args.length;
757    }
758    if (!isFinite(x) || !isFinite(y)) {
759      return false;
760    }
761    var pts = this._mapToLocalCoordinates([x, y]);
762    x = pts[0];
763    y = pts[1];
764    var temp = path.copy();
765    // fillmode is always nonzero
766    temp.setFillType(CanvasKit.FillType.Winding);
767    temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit,
768                 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(),
769                 'precision': 0.3, // this is what Chrome uses to compute this
770                });
771    var retVal = temp.contains(x, y);
772    temp.delete();
773    return retVal;
774  };
775
776  this.lineTo = function(x, y) {
777    lineTo(this._currentPath, x, y);
778  };
779
780  this.measureText = function(text) {
781    throw new Error('Clients wishing to properly measure text should use the Paragraph API');
782  };
783
784  this.moveTo = function(x, y) {
785    moveTo(this._currentPath, x, y);
786  };
787
788  this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
789    if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) {
790      return;
791    }
792    if (dirtyX === undefined) {
793      // fast, simple path for basic call
794      this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y);
795      return;
796    }
797    dirtyX = dirtyX || 0;
798    dirtyY = dirtyY || 0;
799    dirtyWidth = dirtyWidth || imageData.width;
800    dirtyHeight = dirtyHeight || imageData.height;
801
802    // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata
803    if (dirtyWidth < 0) {
804      dirtyX = dirtyX+dirtyWidth;
805      dirtyWidth = Math.abs(dirtyWidth);
806    }
807    if (dirtyHeight < 0) {
808      dirtyY = dirtyY+dirtyHeight;
809      dirtyHeight = Math.abs(dirtyHeight);
810    }
811    if (dirtyX < 0) {
812      dirtyWidth = dirtyWidth + dirtyX;
813      dirtyX = 0;
814    }
815    if (dirtyY < 0) {
816      dirtyHeight = dirtyHeight + dirtyY;
817      dirtyY = 0;
818    }
819    if (dirtyWidth <= 0 || dirtyHeight <= 0) {
820      return;
821    }
822    var img = CanvasKit.MakeImage({
823      'width': imageData.width,
824      'height': imageData.height,
825      'alphaType': CanvasKit.AlphaType.Unpremul,
826      'colorType': CanvasKit.ColorType.RGBA_8888,
827      'colorSpace': CanvasKit.ColorSpace.SRGB
828    }, imageData.data, 4 * imageData.width);
829    var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
830    var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight);
831    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
832    this._canvas.save();
833    // putImageData() operates in device space.
834    this._canvas.concat(inverted);
835    this._canvas.drawImageRect(img, src, dst, null, false);
836    this._canvas.restore();
837    img.delete();
838  };
839
840  this.quadraticCurveTo = function(cpx, cpy, x, y) {
841    quadraticCurveTo(this._currentPath, cpx, cpy, x, y);
842  };
843
844  this.rect = function(x, y, width, height) {
845    rect(this._currentPath, x, y, width, height);
846  };
847
848  this.resetTransform = function() {
849    // Apply the current transform to the path and then reset
850    // to the identity. Essentially "commit" the transform.
851    this._currentPath.transform(this._currentTransform);
852    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
853    this._canvas.concat(inverted);
854    // This should be identity, modulo floating point drift.
855    this._currentTransform = this._canvas.getTotalMatrix();
856  };
857
858  this.restore = function() {
859    var newState = this._canvasStateStack.pop();
860    if (!newState) {
861      return;
862    }
863    // "commit" the current transform. We pop, then apply the inverse of the
864    // popped state, which has the effect of applying just the delta of
865    // transforms between old and new.
866    var combined = CanvasKit.Matrix.multiply(
867      this._currentTransform,
868      CanvasKit.Matrix.invert(newState.ctm)
869    );
870    this._currentPath.transform(combined);
871    this._paint.delete();
872    this._paint = newState.paint;
873
874    this._lineDashList = newState.ldl;
875    this._strokeWidth = newState.sw;
876    this._strokeStyle = newState.ss;
877    this._fillStyle = newState.fs;
878    this._shadowOffsetX = newState.sox;
879    this._shadowOffsetY = newState.soy;
880    this._shadowBlur = newState.sb;
881    this._shadowColor = newState.shc;
882    this._globalAlpha = newState.ga;
883    this._globalCompositeOperation = newState.gco;
884    this._lineDashOffset = newState.ldo;
885    this._fontString = newState.fontstr;
886
887    //TODO: textAlign, textBaseline
888
889    // restores the clip and ctm
890    this._canvas.restore();
891    this._currentTransform = this._canvas.getTotalMatrix();
892  };
893
894  this.rotate = function(radians) {
895    if (!isFinite(radians)) {
896      return;
897    }
898    // retroactively apply the inverse of this transform to the previous
899    // path so it cancels out when we apply the transform at draw time.
900    var inverted = CanvasKit.Matrix.rotated(-radians);
901    this._currentPath.transform(inverted);
902    this._canvas.rotate(radiansToDegrees(radians), 0, 0);
903    this._currentTransform = this._canvas.getTotalMatrix();
904  };
905
906  this.save = function() {
907    if (this._fillStyle._copy) {
908      var fs = this._fillStyle._copy();
909      this._toCleanUp.push(fs);
910    } else {
911      var fs = this._fillStyle;
912    }
913
914    if (this._strokeStyle._copy) {
915      var ss = this._strokeStyle._copy();
916      this._toCleanUp.push(ss);
917    } else {
918      var ss = this._strokeStyle;
919    }
920
921    this._canvasStateStack.push({
922      ctm:     this._currentTransform.slice(),
923      ldl:     this._lineDashList.slice(),
924      sw:      this._strokeWidth,
925      ss:      ss,
926      fs:      fs,
927      sox:     this._shadowOffsetX,
928      soy:     this._shadowOffsetY,
929      sb:      this._shadowBlur,
930      shc:     this._shadowColor,
931      ga:      this._globalAlpha,
932      ldo:     this._lineDashOffset,
933      gco:     this._globalCompositeOperation,
934      paint:   this._paint.copy(),
935      fontstr: this._fontString,
936      //TODO: textAlign, textBaseline
937    });
938    // Saves the clip
939    this._canvas.save();
940  };
941
942  this.scale = function(sx, sy) {
943    if (!allAreFinite(arguments)) {
944      return;
945    }
946    // retroactively apply the inverse of this transform to the previous
947    // path so it cancels out when we apply the transform at draw time.
948    var inverted = CanvasKit.Matrix.scaled(1/sx, 1/sy);
949    this._currentPath.transform(inverted);
950    this._canvas.scale(sx, sy);
951    this._currentTransform = this._canvas.getTotalMatrix();
952  };
953
954  this.setLineDash = function(dashes) {
955    for (var i = 0; i < dashes.length; i++) {
956      if (!isFinite(dashes[i]) || dashes[i] < 0) {
957        Debug('dash list must have positive, finite values');
958        return;
959      }
960    }
961    if (dashes.length % 2 === 1) {
962      // as per the spec, concatenate 2 copies of dashes
963      // to give it an even number of elements.
964      Array.prototype.push.apply(dashes, dashes);
965    }
966    this._lineDashList = dashes;
967  };
968
969  this.setTransform = function(a, b, c, d, e, f) {
970    if (!(allAreFinite(arguments))) {
971      return;
972    }
973    this.resetTransform();
974    this.transform(a, b, c, d, e, f);
975  };
976
977  // We need to apply the shadowOffsets on the device coordinates, so we undo
978  // the CTM, apply the offsets, then re-apply the CTM.
979  this._applyShadowOffsetMatrix = function() {
980    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
981    this._canvas.concat(inverted);
982    this._canvas.concat(CanvasKit.Matrix.translated(this._shadowOffsetX, this._shadowOffsetY));
983    this._canvas.concat(this._currentTransform);
984  };
985
986  // Returns the shadow paint for the current settings or null if there
987  // should be no shadow. This ends up being a copy of the given
988  // paint with a blur maskfilter and the correct color.
989  this._shadowPaint = function(basePaint) {
990    // multiply first to see if the alpha channel goes to 0 after multiplication.
991    var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha);
992    // if alpha is zero, no shadows
993    if (!CanvasKit.getColorComponents(alphaColor)[3]) {
994      return null;
995    }
996    // one of these must also be non-zero (otherwise the shadow is
997    // completely hidden.  And the spec says so).
998    if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
999      return null;
1000    }
1001    var shadowPaint = basePaint.copy();
1002    shadowPaint.setColor(alphaColor);
1003    var blurEffect = CanvasKit.MaskFilter.MakeBlur(CanvasKit.BlurStyle.Normal,
1004      BlurRadiusToSigma(this._shadowBlur),
1005      false);
1006    shadowPaint.setMaskFilter(blurEffect);
1007
1008    // hack up a "destructor" which also cleans up the blurEffect. Otherwise,
1009    // we leak the blurEffect (since smart pointers don't help us in JS land).
1010    shadowPaint.dispose = function() {
1011      blurEffect.delete();
1012      this.delete();
1013    };
1014    return shadowPaint;
1015  };
1016
1017  // A helper to get a copy of the current paint, ready for stroking.
1018  // This applies the global alpha and the dashedness.
1019  // Call dispose() after to clean up.
1020  this._strokePaint = function() {
1021    var paint = this._paint.copy();
1022    paint.setStyle(CanvasKit.PaintStyle.Stroke);
1023    if (isCanvasKitColor(this._strokeStyle)) {
1024      var alphaColor = CanvasKit.multiplyByAlpha(this._strokeStyle, this._globalAlpha);
1025      paint.setColor(alphaColor);
1026    } else {
1027      var shader = this._strokeStyle._getShader(this._currentTransform);
1028      paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
1029      paint.setShader(shader);
1030    }
1031
1032    paint.setStrokeWidth(this._strokeWidth);
1033
1034    if (this._lineDashList.length) {
1035      var dashedEffect = CanvasKit.PathEffect.MakeDash(this._lineDashList, this._lineDashOffset);
1036      paint.setPathEffect(dashedEffect);
1037    }
1038
1039    paint.dispose = function() {
1040      dashedEffect && dashedEffect.delete();
1041      this.delete();
1042    };
1043    return paint;
1044  };
1045
1046  this.stroke = function(path) {
1047    path = path ? path._getPath() : this._currentPath;
1048    var strokePaint = this._strokePaint();
1049
1050    var shadowPaint = this._shadowPaint(strokePaint);
1051    if (shadowPaint) {
1052      this._canvas.save();
1053      this._applyShadowOffsetMatrix();
1054      this._canvas.drawPath(path, shadowPaint);
1055      this._canvas.restore();
1056      shadowPaint.dispose();
1057    }
1058
1059    this._canvas.drawPath(path, strokePaint);
1060    strokePaint.dispose();
1061  };
1062
1063  this.strokeRect = function(x, y, width, height) {
1064    var strokePaint = this._strokePaint();
1065
1066    var shadowPaint = this._shadowPaint(strokePaint);
1067    if (shadowPaint) {
1068      this._canvas.save();
1069      this._applyShadowOffsetMatrix();
1070      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint);
1071      this._canvas.restore();
1072      shadowPaint.dispose();
1073    }
1074    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint);
1075    strokePaint.dispose();
1076  };
1077
1078  this.strokeText = function(text, x, y, maxWidth) {
1079    // TODO do something with maxWidth, probably involving measure
1080    var strokePaint = this._strokePaint();
1081    var blob = CanvasKit.TextBlob.MakeFromText(text, this._font);
1082
1083    var shadowPaint = this._shadowPaint(strokePaint);
1084    if (shadowPaint) {
1085      this._canvas.save();
1086      this._applyShadowOffsetMatrix();
1087      this._canvas.drawTextBlob(blob, x, y, shadowPaint);
1088      this._canvas.restore();
1089      shadowPaint.dispose();
1090    }
1091    this._canvas.drawTextBlob(blob, x, y, strokePaint);
1092    blob.delete();
1093    strokePaint.dispose();
1094  };
1095
1096  this.translate = function(dx, dy) {
1097    if (!allAreFinite(arguments)) {
1098      return;
1099    }
1100    // retroactively apply the inverse of this transform to the previous
1101    // path so it cancels out when we apply the transform at draw time.
1102    var inverted = CanvasKit.Matrix.translated(-dx, -dy);
1103    this._currentPath.transform(inverted);
1104    this._canvas.translate(dx, dy);
1105    this._currentTransform = this._canvas.getTotalMatrix();
1106  };
1107
1108  this.transform = function(a, b, c, d, e, f) {
1109    var newTransform = [a, c, e,
1110                        b, d, f,
1111                        0, 0, 1];
1112    // retroactively apply the inverse of this transform to the previous
1113    // path so it cancels out when we apply the transform at draw time.
1114    var inverted = CanvasKit.Matrix.invert(newTransform);
1115    this._currentPath.transform(inverted);
1116    this._canvas.concat(newTransform);
1117    this._currentTransform = this._canvas.getTotalMatrix();
1118  };
1119
1120  // Not supported operations (e.g. for Web only)
1121  this.addHitRegion = function() {};
1122  this.clearHitRegions = function() {};
1123  this.drawFocusIfNeeded = function() {};
1124  this.removeHitRegion = function() {};
1125  this.scrollPathIntoView = function() {};
1126
1127  Object.defineProperty(this, 'canvas', {
1128    value: null,
1129    writable: false
1130  });
1131}
1132
1133function BlurRadiusToSigma(radius) {
1134  // Blink (Chrome) does the following, for legacy reasons, even though it
1135  // is against the spec. https://bugs.chromium.org/p/chromium/issues/detail?id=179006
1136  // This may change in future releases.
1137  // This code is staying here in case any clients are interested in using it
1138  // to match Blink "exactly".
1139  // if (radius <= 0)
1140  //   return 0;
1141  // return 0.288675 * radius + 0.5;
1142  //
1143  // This is what the spec says, which is how Firefox and others operate.
1144  return radius/2;
1145}
1146