blob: 350ff3069c3e6bd2646d9cd575996f657e6ddfff [file] [log] [blame]
Kevin Lubick53eabf62018-12-10 12:41:26 -05001function 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);
Kevin Lubick8e4a3312018-12-14 15:03:41 -05009 this._fontString = '10px monospace';
Kevin Lubick53eabf62018-12-10 12:41:26 -050010
Kevin Lubickec4903d2019-01-14 08:36:08 -050011 this._font = new CanvasKit.SkFont(null, 10);
Kevin Lubick35ac0382019-01-02 15:13:57 -050012
Kevin Lubick53eabf62018-12-10 12:41:26 -050013 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
Kevin Lubick8e4a3312018-12-14 15:03:41 -050028
Kevin Lubick53eabf62018-12-10 12:41:26 -050029 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();
Kevin Lubick35ac0382019-01-02 15:13:57 -050044 this._font.delete();
Kevin Lubick53eabf62018-12-10 12:41:26 -050045 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,
Kevin Lubick8e4a3312018-12-14 15:03:41 -050098 get: function() {
99 return this._fontString;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500100 },
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500101 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.
Kevin Lubick35ac0382019-01-02 15:13:57 -0500107 this._font.setSize(tf['sizePx']);
108 this._font.setTypeface(tf['typeface']);
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500109 this._fontString = newFont;
110 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500111 }
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) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500492 arc(this._currentPath, x, y, radius, startAngle, endAngle, ccw);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500493 }
494
495 this.arcTo = function(x1, y1, x2, y2, radius) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500496 arcTo(this._currentPath, x1, y1, x2, y2, radius);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500497 }
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) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500507 bezierCurveTo(this._currentPath, cp1x, cp1y, cp2x, cp2y, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500508 }
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
Kevin Lubicka40f8322018-12-17 16:01:36 -0500517 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();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500530 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);
Kevin Lubicka40f8322018-12-17 16:01:36 -0500536 clip.delete();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500537 }
538
539 this.closePath = function() {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500540 closePath(this._currentPath);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500541 }
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
Kevin Lubick53eabf62018-12-10 12:41:26 -0500622 this.ellipse = function(x, y, radiusX, radiusY, rotation,
623 startAngle, endAngle, ccw) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500624 ellipse(this._currentPath, x, y, radiusX, radiusY, rotation,
625 startAngle, endAngle, ccw);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500626 }
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
Kevin Lubicka40f8322018-12-17 16:01:36 -0500652 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 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500660 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 }
Kevin Lubicka40f8322018-12-17 16:01:36 -0500667 if (!path) {
668 path = this._currentPath;
669 }
670
Kevin Lubick53eabf62018-12-10 12:41:26 -0500671 var fillPaint = this._fillPaint();
672
673 var shadowPaint = this._shadowPaint(fillPaint);
674 if (shadowPaint) {
675 this._canvas.save();
676 this._canvas.concat(this._shadowOffsetMatrix());
Kevin Lubicka40f8322018-12-17 16:01:36 -0500677 this._canvas.drawPath(path, shadowPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500678 this._canvas.restore();
679 shadowPaint.dispose();
680 }
Kevin Lubicka40f8322018-12-17 16:01:36 -0500681 this._canvas.drawPath(path, fillPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500682 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
Kevin Lubickec4903d2019-01-14 08:36:08 -0500693 var fillPaint = this._fillPaint();
694 var blob = CanvasKit.SkTextBlob.MakeFromText(text, this._font);
695
Kevin Lubick53eabf62018-12-10 12:41:26 -0500696 var shadowPaint = this._shadowPaint(fillPaint);
697 if (shadowPaint) {
698 this._canvas.save();
699 this._canvas.concat(this._shadowOffsetMatrix());
Kevin Lubickec4903d2019-01-14 08:36:08 -0500700 this._canvas.drawTextBlob(blob, x, y, shadowPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500701 this._canvas.restore();
702 shadowPaint.dispose();
703 }
Kevin Lubickec4903d2019-01-14 08:36:08 -0500704 this._canvas.drawTextBlob(blob, x, y, fillPaint);
705 blob.delete();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500706 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) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500732 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 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500743 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];
Kevin Lubicka40f8322018-12-17 16:01:36 -0500754 path.setFillType(fillmode === 'nonzero' ?
Kevin Lubick53eabf62018-12-10 12:41:26 -0500755 CanvasKit.FillType.Winding :
756 CanvasKit.FillType.EvenOdd);
Kevin Lubicka40f8322018-12-17 16:01:36 -0500757 return path.contains(x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500758 }
759
760 this.isPointInStroke = function(x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500761 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 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500771 if (!isFinite(x) || !isFinite(y)) {
772 return false;
773 }
774 var pts = this._mapToLocalCoordinates([x, y]);
775 x = pts[0];
776 y = pts[1];
Kevin Lubicka40f8322018-12-17 16:01:36 -0500777 var temp = path.copy();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500778 // 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) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500790 lineTo(this._currentPath, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500791 }
792
793 this.measureText = function(text) {
794 return {
Kevin Lubick35ac0382019-01-02 15:13:57 -0500795 width: this._font.measureText(text),
Kevin Lubick53eabf62018-12-10 12:41:26 -0500796 // TODO other measurements?
797 }
798 }
799
800 this.moveTo = function(x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500801 moveTo(this._currentPath, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500802 }
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) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500853 quadraticCurveTo(this._currentPath, cpx, cpy, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500854 }
855
856 this.rect = function(x, y, width, height) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500857 rect(this._currentPath, x, y, width, height);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500858 }
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);
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500883 this._paint.delete();
884 this._paint = newState.paint;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500885
886 this._lineDashList = newState.ldl;
887 this._strokeWidth = newState.sw;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500888 this._strokeStyle = newState.ss;
889 this._fillStyle = newState.fs;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500890 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;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500896 this._lineDashOffset = newState.ldo;
897 this._imageSmoothingEnabled = newState.ise;
898 this._imageFilterQuality = newState.isq;
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500899 this._fontString = newState.fontstr;
900
901 //TODO: textAlign, textBaseline
Kevin Lubick53eabf62018-12-10 12:41:26 -0500902
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({
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500936 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
Kevin Lubick53eabf62018-12-10 12:41:26 -0500953 });
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,
Kevin Lubickd090a702018-12-12 10:34:02 -05001019 SkBlurRadiusToSigma(this._shadowBlur),
Kevin Lubick53eabf62018-12-10 12:41:26 -05001020 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
Kevin Lubicka40f8322018-12-17 16:01:36 -05001061 this.stroke = function(path) {
1062 path = path ? path._getPath() : this._currentPath;
Kevin Lubick53eabf62018-12-10 12:41:26 -05001063 var strokePaint = this._strokePaint();
1064
1065 var shadowPaint = this._shadowPaint(strokePaint);
1066 if (shadowPaint) {
1067 this._canvas.save();
1068 this._canvas.concat(this._shadowOffsetMatrix());
Kevin Lubicka40f8322018-12-17 16:01:36 -05001069 this._canvas.drawPath(path, shadowPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -05001070 this._canvas.restore();
1071 shadowPaint.dispose();
1072 }
1073
Kevin Lubicka40f8322018-12-17 16:01:36 -05001074 this._canvas.drawPath(path, strokePaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -05001075 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();
Kevin Lubickec4903d2019-01-14 08:36:08 -05001087 var blob = CanvasKit.SkTextBlob.MakeFromText(text, this._font);
Kevin Lubick53eabf62018-12-10 12:41:26 -05001088
1089 var shadowPaint = this._shadowPaint(strokePaint);
1090 if (shadowPaint) {
1091 this._canvas.save();
1092 this._canvas.concat(this._shadowOffsetMatrix());
Kevin Lubickec4903d2019-01-14 08:36:08 -05001093 this._canvas.drawTextBlob(blob, x, y, shadowPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -05001094 this._canvas.restore();
1095 shadowPaint.dispose();
1096 }
Kevin Lubickec4903d2019-01-14 08:36:08 -05001097 this._canvas.drawTextBlob(blob, x, y, strokePaint);
1098 blob.delete();
Kevin Lubick53eabf62018-12-10 12:41:26 -05001099 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}
Kevin Lubickd090a702018-12-12 10:34:02 -05001138
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}