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