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