xref: /aosp_15_r20/external/skia/modules/canvaskit/htmlcanvas/canvas2dcontext.js (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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(CanvasKit.Typeface.GetDefault(), 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    if (img instanceof HTMLImage) {
576      img = img.getSkImage();
577    }
578    var iPaint = this._fillPaint();
579    if (arguments.length === 3 || arguments.length === 5) {
580      var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
581                        arguments[3] || img.width(), arguments[4] || img.height());
582      var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height());
583    } else if (arguments.length === 9){
584      var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6],
585                                        arguments[7], arguments[8]);
586      var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
587                                       arguments[3], arguments[4]);
588    } else {
589      throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length;
590    }
591    this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false);
592
593    iPaint.dispose();
594  };
595
596  this.ellipse = function(x, y, radiusX, radiusY, rotation,
597                          startAngle, endAngle, ccw) {
598    ellipse(this._currentPath, x, y, radiusX, radiusY, rotation,
599            startAngle, endAngle, ccw);
600  };
601
602  // A helper to copy the current paint, ready for filling
603  // This applies the global alpha.
604  // Call dispose() after to clean up.
605  this._fillPaint = function() {
606    var paint = this._paint.copy();
607    paint.setStyle(CanvasKit.PaintStyle.Fill);
608    if (isCanvasKitColor(this._fillStyle)) {
609      var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha);
610      paint.setColor(alphaColor);
611    } else {
612      var shader = this._fillStyle._getShader(this._currentTransform);
613      paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
614      paint.setShader(shader);
615    }
616
617    paint.dispose = function() {
618      // If there are some helper effects in the future, clean them up
619      // here. In any case, we have .dispose() to make _fillPaint behave
620      // like _strokePaint and _shadowPaint.
621      this.delete();
622    };
623    return paint;
624  };
625
626  this.fill = function(path, fillRule) {
627    if (typeof path === 'string') {
628      // shift the args if a Path2D is supplied
629      fillRule = path;
630      path = this._currentPath;
631    } else if (path && path._getPath) {
632      path = path._getPath();
633    }
634    if (fillRule === 'evenodd') {
635      this._currentPath.setFillType(CanvasKit.FillType.EvenOdd);
636    } else if (fillRule === 'nonzero' || !fillRule) {
637      this._currentPath.setFillType(CanvasKit.FillType.Winding);
638    } else {
639      throw 'invalid fill rule';
640    }
641    if (!path) {
642      path = this._currentPath;
643    }
644
645    var fillPaint = this._fillPaint();
646
647    var shadowPaint = this._shadowPaint(fillPaint);
648    if (shadowPaint) {
649      this._canvas.save();
650      this._applyShadowOffsetMatrix();
651      this._canvas.drawPath(path, shadowPaint);
652      this._canvas.restore();
653      shadowPaint.dispose();
654    }
655    this._canvas.drawPath(path, fillPaint);
656    fillPaint.dispose();
657  };
658
659  this.fillRect = function(x, y, width, height) {
660    var fillPaint = this._fillPaint();
661
662    var shadowPaint = this._shadowPaint(fillPaint);
663    if (shadowPaint) {
664      this._canvas.save();
665      this._applyShadowOffsetMatrix();
666      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint);
667      this._canvas.restore();
668      shadowPaint.dispose();
669    }
670
671    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint);
672    fillPaint.dispose();
673  };
674
675  this.fillText = function(text, x, y, maxWidth) {
676    // TODO do something with maxWidth, probably involving measure
677    var fillPaint = this._fillPaint();
678    var blob = CanvasKit.TextBlob.MakeFromText(text, this._font);
679
680    var shadowPaint = this._shadowPaint(fillPaint);
681    if (shadowPaint) {
682      this._canvas.save();
683      this._applyShadowOffsetMatrix();
684      this._canvas.drawTextBlob(blob, x, y, shadowPaint);
685      this._canvas.restore();
686      shadowPaint.dispose();
687    }
688    this._canvas.drawTextBlob(blob, x, y, fillPaint);
689    blob.delete();
690    fillPaint.dispose();
691  };
692
693  this.getImageData = function(x, y, w, h) {
694    var pixels = this._canvas.readPixels(x, y, {
695        'width': w,
696        'height': h,
697        'colorType': CanvasKit.ColorType.RGBA_8888,
698        'alphaType': CanvasKit.AlphaType.Unpremul,
699        'colorSpace': CanvasKit.ColorSpace.SRGB,
700    });
701    if (!pixels) {
702      return null;
703    }
704    // This essentially re-wraps the pixels from a Uint8Array to
705    // a Uint8ClampedArray (without making a copy of pixels).
706    return new ImageData(
707      new Uint8ClampedArray(pixels.buffer),
708      w, h);
709  };
710
711  this.getLineDash = function() {
712    return this._lineDashList.slice();
713  };
714
715  this._mapToLocalCoordinates = function(pts) {
716    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
717    CanvasKit.Matrix.mapPoints(inverted, pts);
718    return pts;
719  };
720
721  this.isPointInPath = function(x, y, fillmode) {
722    var args = arguments;
723    if (args.length === 3) {
724      var path = this._currentPath;
725    } else if (args.length === 4) {
726      var path = args[0];
727      x = args[1];
728      y = args[2];
729      fillmode = args[3];
730    } else {
731      throw 'invalid arg count, need 3 or 4, got ' + args.length;
732    }
733    if (!isFinite(x) || !isFinite(y)) {
734      return false;
735    }
736    fillmode = fillmode || 'nonzero';
737    if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) {
738      return false;
739    }
740    // x and y are in canvas coordinates (i.e. unaffected by CTM)
741    var pts = this._mapToLocalCoordinates([x, y]);
742    x = pts[0];
743    y = pts[1];
744    path.setFillType(fillmode === 'nonzero' ?
745                                  CanvasKit.FillType.Winding :
746                                  CanvasKit.FillType.EvenOdd);
747    return path.contains(x, y);
748  };
749
750  this.isPointInStroke = function(x, y) {
751    var args = arguments;
752    if (args.length === 2) {
753      var path = this._currentPath;
754    } else if (args.length === 3) {
755      var path = args[0];
756      x = args[1];
757      y = args[2];
758    } else {
759      throw 'invalid arg count, need 2 or 3, got ' + args.length;
760    }
761    if (!isFinite(x) || !isFinite(y)) {
762      return false;
763    }
764    var pts = this._mapToLocalCoordinates([x, y]);
765    x = pts[0];
766    y = pts[1];
767    var temp = path.copy();
768    // fillmode is always nonzero
769    temp.setFillType(CanvasKit.FillType.Winding);
770    temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit,
771                 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(),
772                 'precision': 0.3, // this is what Chrome uses to compute this
773                });
774    var retVal = temp.contains(x, y);
775    temp.delete();
776    return retVal;
777  };
778
779  this.lineTo = function(x, y) {
780    lineTo(this._currentPath, x, y);
781  };
782
783  this.measureText = function(text) {
784    const ids = this._font.getGlyphIDs(text);
785    const widths = this._font.getGlyphWidths(ids);
786    let totalWidth = 0;
787    for (const w of widths) {
788      totalWidth += w;
789    }
790    return {
791      "width": totalWidth,
792    };
793  };
794
795  this.moveTo = function(x, y) {
796    moveTo(this._currentPath, x, y);
797  };
798
799  this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
800    if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) {
801      return;
802    }
803    if (dirtyX === undefined) {
804      // fast, simple path for basic call
805      this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y);
806      return;
807    }
808    dirtyX = dirtyX || 0;
809    dirtyY = dirtyY || 0;
810    dirtyWidth = dirtyWidth || imageData.width;
811    dirtyHeight = dirtyHeight || imageData.height;
812
813    // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata
814    if (dirtyWidth < 0) {
815      dirtyX = dirtyX+dirtyWidth;
816      dirtyWidth = Math.abs(dirtyWidth);
817    }
818    if (dirtyHeight < 0) {
819      dirtyY = dirtyY+dirtyHeight;
820      dirtyHeight = Math.abs(dirtyHeight);
821    }
822    if (dirtyX < 0) {
823      dirtyWidth = dirtyWidth + dirtyX;
824      dirtyX = 0;
825    }
826    if (dirtyY < 0) {
827      dirtyHeight = dirtyHeight + dirtyY;
828      dirtyY = 0;
829    }
830    if (dirtyWidth <= 0 || dirtyHeight <= 0) {
831      return;
832    }
833    var img = CanvasKit.MakeImage({
834      'width': imageData.width,
835      'height': imageData.height,
836      'alphaType': CanvasKit.AlphaType.Unpremul,
837      'colorType': CanvasKit.ColorType.RGBA_8888,
838      'colorSpace': CanvasKit.ColorSpace.SRGB
839    }, imageData.data, 4 * imageData.width);
840    var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
841    var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight);
842    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
843    this._canvas.save();
844    // putImageData() operates in device space.
845    this._canvas.concat(inverted);
846    this._canvas.drawImageRect(img, src, dst, null, false);
847    this._canvas.restore();
848    img.delete();
849  };
850
851  this.quadraticCurveTo = function(cpx, cpy, x, y) {
852    quadraticCurveTo(this._currentPath, cpx, cpy, x, y);
853  };
854
855  this.rect = function(x, y, width, height) {
856    rect(this._currentPath, x, y, width, height);
857  };
858
859  this.resetTransform = function() {
860    // Apply the current transform to the path and then reset
861    // to the identity. Essentially "commit" the transform.
862    this._currentPath.transform(this._currentTransform);
863    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
864    this._canvas.concat(inverted);
865    // This should be identity, modulo floating point drift.
866    this._currentTransform = this._canvas.getTotalMatrix();
867  };
868
869  this.restore = function() {
870    var newState = this._canvasStateStack.pop();
871    if (!newState) {
872      return;
873    }
874    // "commit" the current transform. We pop, then apply the inverse of the
875    // popped state, which has the effect of applying just the delta of
876    // transforms between old and new.
877    var combined = CanvasKit.Matrix.multiply(
878      this._currentTransform,
879      CanvasKit.Matrix.invert(newState.ctm)
880    );
881    this._currentPath.transform(combined);
882    this._paint.delete();
883    this._paint = newState.paint;
884
885    this._lineDashList = newState.ldl;
886    this._strokeWidth = newState.sw;
887    this._strokeStyle = newState.ss;
888    this._fillStyle = newState.fs;
889    this._shadowOffsetX = newState.sox;
890    this._shadowOffsetY = newState.soy;
891    this._shadowBlur = newState.sb;
892    this._shadowColor = newState.shc;
893    this._globalAlpha = newState.ga;
894    this._globalCompositeOperation = newState.gco;
895    this._lineDashOffset = newState.ldo;
896    this._fontString = newState.fontstr;
897
898    //TODO: textAlign, textBaseline
899
900    // restores the clip and ctm
901    this._canvas.restore();
902    this._currentTransform = this._canvas.getTotalMatrix();
903  };
904
905  this.rotate = function(radians) {
906    if (!isFinite(radians)) {
907      return;
908    }
909    // retroactively apply the inverse of this transform to the previous
910    // path so it cancels out when we apply the transform at draw time.
911    var inverted = CanvasKit.Matrix.rotated(-radians);
912    this._currentPath.transform(inverted);
913    this._canvas.rotate(radiansToDegrees(radians), 0, 0);
914    this._currentTransform = this._canvas.getTotalMatrix();
915  };
916
917  this.save = function() {
918    if (this._fillStyle._copy) {
919      var fs = this._fillStyle._copy();
920      this._toCleanUp.push(fs);
921    } else {
922      var fs = this._fillStyle;
923    }
924
925    if (this._strokeStyle._copy) {
926      var ss = this._strokeStyle._copy();
927      this._toCleanUp.push(ss);
928    } else {
929      var ss = this._strokeStyle;
930    }
931
932    this._canvasStateStack.push({
933      ctm:     this._currentTransform.slice(),
934      ldl:     this._lineDashList.slice(),
935      sw:      this._strokeWidth,
936      ss:      ss,
937      fs:      fs,
938      sox:     this._shadowOffsetX,
939      soy:     this._shadowOffsetY,
940      sb:      this._shadowBlur,
941      shc:     this._shadowColor,
942      ga:      this._globalAlpha,
943      ldo:     this._lineDashOffset,
944      gco:     this._globalCompositeOperation,
945      paint:   this._paint.copy(),
946      fontstr: this._fontString,
947      //TODO: textAlign, textBaseline
948    });
949    // Saves the clip
950    this._canvas.save();
951  };
952
953  this.scale = function(sx, sy) {
954    if (!allAreFinite(arguments)) {
955      return;
956    }
957    // retroactively apply the inverse of this transform to the previous
958    // path so it cancels out when we apply the transform at draw time.
959    var inverted = CanvasKit.Matrix.scaled(1/sx, 1/sy);
960    this._currentPath.transform(inverted);
961    this._canvas.scale(sx, sy);
962    this._currentTransform = this._canvas.getTotalMatrix();
963  };
964
965  this.setLineDash = function(dashes) {
966    for (var i = 0; i < dashes.length; i++) {
967      if (!isFinite(dashes[i]) || dashes[i] < 0) {
968        Debug('dash list must have positive, finite values');
969        return;
970      }
971    }
972    if (dashes.length % 2 === 1) {
973      // as per the spec, concatenate 2 copies of dashes
974      // to give it an even number of elements.
975      Array.prototype.push.apply(dashes, dashes);
976    }
977    this._lineDashList = dashes;
978  };
979
980  this.setTransform = function(a, b, c, d, e, f) {
981    if (!(allAreFinite(arguments))) {
982      return;
983    }
984    this.resetTransform();
985    this.transform(a, b, c, d, e, f);
986  };
987
988  // We need to apply the shadowOffsets on the device coordinates, so we undo
989  // the CTM, apply the offsets, then re-apply the CTM.
990  this._applyShadowOffsetMatrix = function() {
991    var inverted = CanvasKit.Matrix.invert(this._currentTransform);
992    this._canvas.concat(inverted);
993    this._canvas.concat(CanvasKit.Matrix.translated(this._shadowOffsetX, this._shadowOffsetY));
994    this._canvas.concat(this._currentTransform);
995  };
996
997  // Returns the shadow paint for the current settings or null if there
998  // should be no shadow. This ends up being a copy of the given
999  // paint with a blur maskfilter and the correct color.
1000  this._shadowPaint = function(basePaint) {
1001    // multiply first to see if the alpha channel goes to 0 after multiplication.
1002    var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha);
1003    // if alpha is zero, no shadows
1004    if (!CanvasKit.getColorComponents(alphaColor)[3]) {
1005      return null;
1006    }
1007    // one of these must also be non-zero (otherwise the shadow is
1008    // completely hidden.  And the spec says so).
1009    if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
1010      return null;
1011    }
1012    var shadowPaint = basePaint.copy();
1013    shadowPaint.setColor(alphaColor);
1014    var blurEffect = CanvasKit.MaskFilter.MakeBlur(CanvasKit.BlurStyle.Normal,
1015      BlurRadiusToSigma(this._shadowBlur),
1016      false);
1017    shadowPaint.setMaskFilter(blurEffect);
1018
1019    // hack up a "destructor" which also cleans up the blurEffect. Otherwise,
1020    // we leak the blurEffect (since smart pointers don't help us in JS land).
1021    shadowPaint.dispose = function() {
1022      blurEffect.delete();
1023      this.delete();
1024    };
1025    return shadowPaint;
1026  };
1027
1028  // A helper to get a copy of the current paint, ready for stroking.
1029  // This applies the global alpha and the dashedness.
1030  // Call dispose() after to clean up.
1031  this._strokePaint = function() {
1032    var paint = this._paint.copy();
1033    paint.setStyle(CanvasKit.PaintStyle.Stroke);
1034    if (isCanvasKitColor(this._strokeStyle)) {
1035      var alphaColor = CanvasKit.multiplyByAlpha(this._strokeStyle, this._globalAlpha);
1036      paint.setColor(alphaColor);
1037    } else {
1038      var shader = this._strokeStyle._getShader(this._currentTransform);
1039      paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
1040      paint.setShader(shader);
1041    }
1042
1043    paint.setStrokeWidth(this._strokeWidth);
1044
1045    if (this._lineDashList.length) {
1046      var dashedEffect = CanvasKit.PathEffect.MakeDash(this._lineDashList, this._lineDashOffset);
1047      paint.setPathEffect(dashedEffect);
1048    }
1049
1050    paint.dispose = function() {
1051      dashedEffect && dashedEffect.delete();
1052      this.delete();
1053    };
1054    return paint;
1055  };
1056
1057  this.stroke = function(path) {
1058    path = path ? path._getPath() : this._currentPath;
1059    var strokePaint = this._strokePaint();
1060
1061    var shadowPaint = this._shadowPaint(strokePaint);
1062    if (shadowPaint) {
1063      this._canvas.save();
1064      this._applyShadowOffsetMatrix();
1065      this._canvas.drawPath(path, shadowPaint);
1066      this._canvas.restore();
1067      shadowPaint.dispose();
1068    }
1069
1070    this._canvas.drawPath(path, strokePaint);
1071    strokePaint.dispose();
1072  };
1073
1074  this.strokeRect = function(x, y, width, height) {
1075    var strokePaint = this._strokePaint();
1076
1077    var shadowPaint = this._shadowPaint(strokePaint);
1078    if (shadowPaint) {
1079      this._canvas.save();
1080      this._applyShadowOffsetMatrix();
1081      this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint);
1082      this._canvas.restore();
1083      shadowPaint.dispose();
1084    }
1085    this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint);
1086    strokePaint.dispose();
1087  };
1088
1089  this.strokeText = function(text, x, y, maxWidth) {
1090    // TODO do something with maxWidth, probably involving measure
1091    var strokePaint = this._strokePaint();
1092    var blob = CanvasKit.TextBlob.MakeFromText(text, this._font);
1093
1094    var shadowPaint = this._shadowPaint(strokePaint);
1095    if (shadowPaint) {
1096      this._canvas.save();
1097      this._applyShadowOffsetMatrix();
1098      this._canvas.drawTextBlob(blob, x, y, shadowPaint);
1099      this._canvas.restore();
1100      shadowPaint.dispose();
1101    }
1102    this._canvas.drawTextBlob(blob, x, y, strokePaint);
1103    blob.delete();
1104    strokePaint.dispose();
1105  };
1106
1107  this.translate = function(dx, dy) {
1108    if (!allAreFinite(arguments)) {
1109      return;
1110    }
1111    // retroactively apply the inverse of this transform to the previous
1112    // path so it cancels out when we apply the transform at draw time.
1113    var inverted = CanvasKit.Matrix.translated(-dx, -dy);
1114    this._currentPath.transform(inverted);
1115    this._canvas.translate(dx, dy);
1116    this._currentTransform = this._canvas.getTotalMatrix();
1117  };
1118
1119  this.transform = function(a, b, c, d, e, f) {
1120    var newTransform = [a, c, e,
1121                        b, d, f,
1122                        0, 0, 1];
1123    // retroactively apply the inverse of this transform to the previous
1124    // path so it cancels out when we apply the transform at draw time.
1125    var inverted = CanvasKit.Matrix.invert(newTransform);
1126    this._currentPath.transform(inverted);
1127    this._canvas.concat(newTransform);
1128    this._currentTransform = this._canvas.getTotalMatrix();
1129  };
1130
1131  // Not supported operations (e.g. for Web only)
1132  this.addHitRegion = function() {};
1133  this.clearHitRegions = function() {};
1134  this.drawFocusIfNeeded = function() {};
1135  this.removeHitRegion = function() {};
1136  this.scrollPathIntoView = function() {};
1137
1138  Object.defineProperty(this, 'canvas', {
1139    value: null,
1140    writable: false
1141  });
1142}
1143
1144function BlurRadiusToSigma(radius) {
1145  // Blink (Chrome) does the following, for legacy reasons, even though it
1146  // is against the spec. https://bugs.chromium.org/p/chromium/issues/detail?id=179006
1147  // This may change in future releases.
1148  // This code is staying here in case any clients are interested in using it
1149  // to match Blink "exactly".
1150  // if (radius <= 0)
1151  //   return 0;
1152  // return 0.288675 * radius + 0.5;
1153  //
1154  // This is what the spec says, which is how Firefox and others operate.
1155  return radius/2;
1156}
1157