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