blob: 06e47dca23b0dbac050ec90f686e6d4a69373a73 [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) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500491 arc(this._currentPath, x, y, radius, startAngle, endAngle, ccw);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500492 }
493
494 this.arcTo = function(x1, y1, x2, y2, radius) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500495 arcTo(this._currentPath, x1, y1, x2, y2, radius);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500496 }
497
498 // As per the spec this doesn't begin any paths, it only
499 // clears out any previous paths.
500 this.beginPath = function() {
501 this._currentPath.delete();
502 this._currentPath = new CanvasKit.SkPath();
503 }
504
505 this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500506 bezierCurveTo(this._currentPath, cp1x, cp1y, cp2x, cp2y, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500507 }
508
509 this.clearRect = function(x, y, width, height) {
510 this._paint.setStyle(CanvasKit.PaintStyle.Fill);
511 this._paint.setBlendMode(CanvasKit.BlendMode.Clear);
512 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), this._paint);
513 this._paint.setBlendMode(this._globalCompositeOperation);
514 }
515
Kevin Lubicka40f8322018-12-17 16:01:36 -0500516 this.clip = function(path, fillRule) {
517 if (typeof path === 'string') {
518 // shift the args if a Path2D is supplied
519 fillRule = path;
520 path = this._currentPath;
521 } else if (path && path._getPath) {
522 path = path._getPath();
523 }
524 if (!path) {
525 path = this._currentPath;
526 }
527
528 var clip = path.copy();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500529 if (fillRule && fillRule.toLowerCase() === 'evenodd') {
530 clip.setFillType(CanvasKit.FillType.EvenOdd);
531 } else {
532 clip.setFillType(CanvasKit.FillType.Winding);
533 }
534 this._canvas.clipPath(clip, CanvasKit.ClipOp.Intersect, true);
Kevin Lubicka40f8322018-12-17 16:01:36 -0500535 clip.delete();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500536 }
537
538 this.closePath = function() {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500539 closePath(this._currentPath);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500540 }
541
542 this.createImageData = function() {
543 // either takes in 1 or 2 arguments:
544 // - imagedata on which to copy *width* and *height* only
545 // - width, height
546 if (arguments.length === 1) {
547 var oldData = arguments[0];
548 var byteLength = 4 * oldData.width * oldData.height;
549 return new ImageData(new Uint8ClampedArray(byteLength),
550 oldData.width, oldData.height);
551 } else if (arguments.length === 2) {
552 var width = arguments[0];
553 var height = arguments[1];
554 var byteLength = 4 * width * height;
555 return new ImageData(new Uint8ClampedArray(byteLength),
556 width, height);
557 } else {
558 throw 'createImageData expects 1 or 2 arguments, got '+arguments.length;
559 }
560 }
561
562 this.createLinearGradient = function(x1, y1, x2, y2) {
563 if (!allAreFinite(arguments)) {
564 return;
565 }
566 var lcg = new LinearCanvasGradient(x1, y1, x2, y2);
567 this._toCleanUp.push(lcg);
568 return lcg;
569 }
570
571 this.createPattern = function(image, repetition) {
572 var cp = new CanvasPattern(image, repetition);
573 this._toCleanUp.push(cp);
574 return cp;
575 }
576
577 this.createRadialGradient = function(x1, y1, r1, x2, y2, r2) {
578 if (!allAreFinite(arguments)) {
579 return;
580 }
581 var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2);
582 this._toCleanUp.push(rcg);
583 return rcg;
584 }
585
586 this._imagePaint = function() {
587 var iPaint = this._fillPaint();
588 if (!this._imageSmoothingEnabled) {
589 iPaint.setFilterQuality(CanvasKit.FilterQuality.None);
590 } else {
591 iPaint.setFilterQuality(this._imageFilterQuality);
592 }
593 return iPaint;
594 }
595
596 this.drawImage = function(img) {
597 // 3 potential sets of arguments
598 // - image, dx, dy
599 // - image, dx, dy, dWidth, dHeight
600 // - image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
601 // use the fillPaint, which has the globalAlpha in it
602 // which drawImageRect will use.
603 var iPaint = this._imagePaint();
604 if (arguments.length === 3 || arguments.length === 5) {
605 var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
606 arguments[3] || img.width(), arguments[4] || img.height());
607 var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height());
608 } else if (arguments.length === 9){
609 var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6],
610 arguments[7], arguments[8]);
611 var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2],
612 arguments[3], arguments[4]);
613 } else {
614 throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length;
615 }
616 this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false);
617
618 iPaint.dispose();
619 }
620
Kevin Lubick53eabf62018-12-10 12:41:26 -0500621 this.ellipse = function(x, y, radiusX, radiusY, rotation,
622 startAngle, endAngle, ccw) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500623 ellipse(this._currentPath, x, y, radiusX, radiusY, rotation,
624 startAngle, endAngle, ccw);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500625 }
626
627 // A helper to copy the current paint, ready for filling
628 // This applies the global alpha.
629 // Call dispose() after to clean up.
630 this._fillPaint = function() {
631 var paint = this._paint.copy();
632 paint.setStyle(CanvasKit.PaintStyle.Fill);
633 if (Number.isInteger(this._fillStyle)) {
634 var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha);
635 paint.setColor(alphaColor);
636 } else {
637 var shader = this._fillStyle._getShader(this._currentTransform);
638 paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
639 paint.setShader(shader);
640 }
641
642 paint.dispose = function() {
643 // If there are some helper effects in the future, clean them up
644 // here. In any case, we have .dispose() to make _fillPaint behave
645 // like _strokePaint and _shadowPaint.
646 this.delete();
647 }
648 return paint;
649 }
650
Kevin Lubicka40f8322018-12-17 16:01:36 -0500651 this.fill = function(path, fillRule) {
652 if (typeof path === 'string') {
653 // shift the args if a Path2D is supplied
654 fillRule = path;
655 path = this._currentPath;
656 } else if (path && path._getPath) {
657 path = path._getPath();
658 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500659 if (fillRule === 'evenodd') {
660 this._currentPath.setFillType(CanvasKit.FillType.EvenOdd);
661 } else if (fillRule === 'nonzero' || !fillRule) {
662 this._currentPath.setFillType(CanvasKit.FillType.Winding);
663 } else {
664 throw 'invalid fill rule';
665 }
Kevin Lubicka40f8322018-12-17 16:01:36 -0500666 if (!path) {
667 path = this._currentPath;
668 }
669
Kevin Lubick53eabf62018-12-10 12:41:26 -0500670 var fillPaint = this._fillPaint();
671
672 var shadowPaint = this._shadowPaint(fillPaint);
673 if (shadowPaint) {
674 this._canvas.save();
675 this._canvas.concat(this._shadowOffsetMatrix());
Kevin Lubicka40f8322018-12-17 16:01:36 -0500676 this._canvas.drawPath(path, shadowPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500677 this._canvas.restore();
678 shadowPaint.dispose();
679 }
Kevin Lubicka40f8322018-12-17 16:01:36 -0500680 this._canvas.drawPath(path, fillPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500681 fillPaint.dispose();
682 }
683
684 this.fillRect = function(x, y, width, height) {
685 var fillPaint = this._fillPaint();
686 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint);
687 fillPaint.dispose();
688 }
689
690 this.fillText = function(text, x, y, maxWidth) {
691 // TODO do something with maxWidth, probably involving measure
692 var fillPaint = this._fillPaint()
693 var shadowPaint = this._shadowPaint(fillPaint);
694 if (shadowPaint) {
695 this._canvas.save();
696 this._canvas.concat(this._shadowOffsetMatrix());
697 this._canvas.drawText(text, x, y, shadowPaint);
698 this._canvas.restore();
699 shadowPaint.dispose();
700 }
701 this._canvas.drawText(text, x, y, fillPaint);
702 fillPaint.dispose();
703 }
704
705 this.getImageData = function(x, y, w, h) {
706 var pixels = this._canvas.readPixels(x, y, w, h);
707 if (!pixels) {
708 return null;
709 }
710 // This essentially re-wraps the pixels from a Uint8Array to
711 // a Uint8ClampedArray (without making a copy of pixels).
712 return new ImageData(
713 new Uint8ClampedArray(pixels.buffer),
714 w, h);
715 }
716
717 this.getLineDash = function() {
718 return this._lineDashList.slice();
719 }
720
721 this._mapToLocalCoordinates = function(pts) {
722 var inverted = CanvasKit.SkMatrix.invert(this._currentTransform);
723 CanvasKit.SkMatrix.mapPoints(inverted, pts);
724 return pts;
725 }
726
727 this.isPointInPath = function(x, y, fillmode) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500728 var args = arguments;
729 if (args.length === 3) {
730 var path = this._currentPath;
731 } else if (args.length === 4) {
732 var path = args[0];
733 x = args[1];
734 y = args[2];
735 fillmode = args[3];
736 } else {
737 throw 'invalid arg count, need 3 or 4, got ' + args.length;
738 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500739 if (!isFinite(x) || !isFinite(y)) {
740 return false;
741 }
742 fillmode = fillmode || 'nonzero';
743 if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) {
744 return false;
745 }
746 // x and y are in canvas coordinates (i.e. unaffected by CTM)
747 var pts = this._mapToLocalCoordinates([x, y]);
748 x = pts[0];
749 y = pts[1];
Kevin Lubicka40f8322018-12-17 16:01:36 -0500750 path.setFillType(fillmode === 'nonzero' ?
Kevin Lubick53eabf62018-12-10 12:41:26 -0500751 CanvasKit.FillType.Winding :
752 CanvasKit.FillType.EvenOdd);
Kevin Lubicka40f8322018-12-17 16:01:36 -0500753 return path.contains(x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500754 }
755
756 this.isPointInStroke = function(x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500757 var args = arguments;
758 if (args.length === 2) {
759 var path = this._currentPath;
760 } else if (args.length === 3) {
761 var path = args[0];
762 x = args[1];
763 y = args[2];
764 } else {
765 throw 'invalid arg count, need 2 or 3, got ' + args.length;
766 }
Kevin Lubick53eabf62018-12-10 12:41:26 -0500767 if (!isFinite(x) || !isFinite(y)) {
768 return false;
769 }
770 var pts = this._mapToLocalCoordinates([x, y]);
771 x = pts[0];
772 y = pts[1];
Kevin Lubicka40f8322018-12-17 16:01:36 -0500773 var temp = path.copy();
Kevin Lubick53eabf62018-12-10 12:41:26 -0500774 // fillmode is always nonzero
775 temp.setFillType(CanvasKit.FillType.Winding);
776 temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit,
777 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(),
778 'precision': 0.3, // this is what Chrome uses to compute this
779 });
780 var retVal = temp.contains(x, y);
781 temp.delete();
782 return retVal;
783 }
784
785 this.lineTo = function(x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500786 lineTo(this._currentPath, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500787 }
788
789 this.measureText = function(text) {
790 return {
791 width: this._paint.measureText(text),
792 // TODO other measurements?
793 }
794 }
795
796 this.moveTo = function(x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500797 moveTo(this._currentPath, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500798 }
799
800 this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
801 if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) {
802 return;
803 }
804 if (dirtyX === undefined) {
805 // fast, simple path for basic call
806 this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y);
807 return;
808 }
809 dirtyX = dirtyX || 0;
810 dirtyY = dirtyY || 0;
811 dirtyWidth = dirtyWidth || imageData.width;
812 dirtyHeight = dirtyHeight || imageData.height;
813
814 // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata
815 if (dirtyWidth < 0) {
816 dirtyX = dirtyX+dirtyWidth;
817 dirtyWidth = Math.abs(dirtyWidth);
818 }
819 if (dirtyHeight < 0) {
820 dirtyY = dirtyY+dirtyHeight;
821 dirtyHeight = Math.abs(dirtyHeight);
822 }
823 if (dirtyX < 0) {
824 dirtyWidth = dirtyWidth + dirtyX;
825 dirtyX = 0;
826 }
827 if (dirtyY < 0) {
828 dirtyHeight = dirtyHeight + dirtyY;
829 dirtyY = 0;
830 }
831 if (dirtyWidth <= 0 || dirtyHeight <= 0) {
832 return;
833 }
834 var img = CanvasKit.MakeImage(imageData.data, imageData.width, imageData.height,
835 CanvasKit.AlphaType.Unpremul,
836 CanvasKit.ColorType.RGBA_8888);
837 var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
838 var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight);
839 var inverted = CanvasKit.SkMatrix.invert(this._currentTransform);
840 this._canvas.save();
841 // putImageData() operates in device space.
842 this._canvas.concat(inverted);
843 this._canvas.drawImageRect(img, src, dst, null, false);
844 this._canvas.restore();
845 img.delete();
846 }
847
848 this.quadraticCurveTo = function(cpx, cpy, x, y) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500849 quadraticCurveTo(this._currentPath, cpx, cpy, x, y);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500850 }
851
852 this.rect = function(x, y, width, height) {
Kevin Lubicka40f8322018-12-17 16:01:36 -0500853 rect(this._currentPath, x, y, width, height);
Kevin Lubick53eabf62018-12-10 12:41:26 -0500854 }
855
856 this.resetTransform = function() {
857 // Apply the current transform to the path and then reset
858 // to the identity. Essentially "commit" the transform.
859 this._currentPath.transform(this._currentTransform);
860 var inverted = CanvasKit.SkMatrix.invert(this._currentTransform);
861 this._canvas.concat(inverted);
862 // This should be identity, modulo floating point drift.
863 this._currentTransform = this._canvas.getTotalMatrix();
864 }
865
866 this.restore = function() {
867 var newState = this._canvasStateStack.pop();
868 if (!newState) {
869 return;
870 }
871 // "commit" the current transform. We pop, then apply the inverse of the
872 // popped state, which has the effect of applying just the delta of
873 // transforms between old and new.
874 var combined = CanvasKit.SkMatrix.multiply(
875 this._currentTransform,
876 CanvasKit.SkMatrix.invert(newState.ctm)
877 );
878 this._currentPath.transform(combined);
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500879 this._paint.delete();
880 this._paint = newState.paint;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500881
882 this._lineDashList = newState.ldl;
883 this._strokeWidth = newState.sw;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500884 this._strokeStyle = newState.ss;
885 this._fillStyle = newState.fs;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500886 this._shadowOffsetX = newState.sox;
887 this._shadowOffsetY = newState.soy;
888 this._shadowBlur = newState.sb;
889 this._shadowColor = newState.shc;
890 this._globalAlpha = newState.ga;
891 this._globalCompositeOperation = newState.gco;
Kevin Lubick53eabf62018-12-10 12:41:26 -0500892 this._lineDashOffset = newState.ldo;
893 this._imageSmoothingEnabled = newState.ise;
894 this._imageFilterQuality = newState.isq;
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500895 this._fontString = newState.fontstr;
896
897 //TODO: textAlign, textBaseline
Kevin Lubick53eabf62018-12-10 12:41:26 -0500898
899 // restores the clip and ctm
900 this._canvas.restore();
901 this._currentTransform = this._canvas.getTotalMatrix();
902 }
903
904 this.rotate = function(radians) {
905 if (!isFinite(radians)) {
906 return;
907 }
908 // retroactively apply the inverse of this transform to the previous
909 // path so it cancels out when we apply the transform at draw time.
910 var inverted = CanvasKit.SkMatrix.rotated(-radians);
911 this._currentPath.transform(inverted);
912 this._canvas.rotate(radiansToDegrees(radians), 0, 0);
913 this._currentTransform = this._canvas.getTotalMatrix();
914 }
915
916 this.save = function() {
917 if (this._fillStyle._copy) {
918 var fs = this._fillStyle._copy();
919 this._toCleanUp.push(fs);
920 } else {
921 var fs = this._fillStyle;
922 }
923
924 if (this._strokeStyle._copy) {
925 var ss = this._strokeStyle._copy();
926 this._toCleanUp.push(ss);
927 } else {
928 var ss = this._strokeStyle;
929 }
930
931 this._canvasStateStack.push({
Kevin Lubick8e4a3312018-12-14 15:03:41 -0500932 ctm: this._currentTransform.slice(),
933 ldl: this._lineDashList.slice(),
934 sw: this._strokeWidth,
935 ss: ss,
936 fs: fs,
937 sox: this._shadowOffsetX,
938 soy: this._shadowOffsetY,
939 sb: this._shadowBlur,
940 shc: this._shadowColor,
941 ga: this._globalAlpha,
942 ldo: this._lineDashOffset,
943 gco: this._globalCompositeOperation,
944 ise: this._imageSmoothingEnabled,
945 isq: this._imageFilterQuality,
946 paint: this._paint.copy(),
947 fontstr: this._fontString,
948 //TODO: textAlign, textBaseline
Kevin Lubick53eabf62018-12-10 12:41:26 -0500949 });
950 // Saves the clip
951 this._canvas.save();
952 }
953
954 this.scale = function(sx, sy) {
955 if (!allAreFinite(arguments)) {
956 return;
957 }
958 // retroactively apply the inverse of this transform to the previous
959 // path so it cancels out when we apply the transform at draw time.
960 var inverted = CanvasKit.SkMatrix.scaled(1/sx, 1/sy);
961 this._currentPath.transform(inverted);
962 this._canvas.scale(sx, sy);
963 this._currentTransform = this._canvas.getTotalMatrix();
964 }
965
966 this.setLineDash = function(dashes) {
967 for (var i = 0; i < dashes.length; i++) {
968 if (!isFinite(dashes[i]) || dashes[i] < 0) {
969 SkDebug('dash list must have positive, finite values');
970 return;
971 }
972 }
973 if (dashes.length % 2 === 1) {
974 // as per the spec, concatenate 2 copies of dashes
975 // to give it an even number of elements.
976 Array.prototype.push.apply(dashes, dashes);
977 }
978 this._lineDashList = dashes;
979 }
980
981 this.setTransform = function(a, b, c, d, e, f) {
982 if (!(allAreFinite(arguments))) {
983 return;
984 }
985 this.resetTransform();
986 this.transform(a, b, c, d, e, f);
987 }
988
989 // Returns the matrix representing the offset of the shadows. This unapplies
990 // the effects of the scale, which should not affect the shadow offsets.
991 this._shadowOffsetMatrix = function() {
992 var sx = this._currentTransform[0];
993 var sy = this._currentTransform[4];
994 return CanvasKit.SkMatrix.translated(this._shadowOffsetX/sx, this._shadowOffsetY/sy);
995 }
996
997 // Returns the shadow paint for the current settings or null if there
998 // should be no shadow. This ends up being a copy of the given
999 // paint with a blur maskfilter and the correct color.
1000 this._shadowPaint = function(basePaint) {
1001 // multiply first to see if the alpha channel goes to 0 after multiplication.
1002 var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha);
1003 // if alpha is zero, no shadows
1004 if (!CanvasKit.getColorComponents(alphaColor)[3]) {
1005 return null;
1006 }
1007 // one of these must also be non-zero (otherwise the shadow is
1008 // completely hidden. And the spec says so).
1009 if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) {
1010 return null;
1011 }
1012 var shadowPaint = basePaint.copy();
1013 shadowPaint.setColor(alphaColor);
1014 var blurEffect = CanvasKit.MakeBlurMaskFilter(CanvasKit.BlurStyle.Normal,
Kevin Lubickd090a702018-12-12 10:34:02 -05001015 SkBlurRadiusToSigma(this._shadowBlur),
Kevin Lubick53eabf62018-12-10 12:41:26 -05001016 false);
1017 shadowPaint.setMaskFilter(blurEffect);
1018
1019 // hack up a "destructor" which also cleans up the blurEffect. Otherwise,
1020 // we leak the blurEffect (since smart pointers don't help us in JS land).
1021 shadowPaint.dispose = function() {
1022 blurEffect.delete();
1023 this.delete();
1024 };
1025 return shadowPaint;
1026 }
1027
1028 // A helper to get a copy of the current paint, ready for stroking.
1029 // This applies the global alpha and the dashedness.
1030 // Call dispose() after to clean up.
1031 this._strokePaint = function() {
1032 var paint = this._paint.copy();
1033 paint.setStyle(CanvasKit.PaintStyle.Stroke);
1034 if (Number.isInteger(this._strokeStyle)) {
1035 var alphaColor = CanvasKit.multiplyByAlpha(this._strokeStyle, this._globalAlpha);
1036 paint.setColor(alphaColor);
1037 } else {
1038 var shader = this._strokeStyle._getShader(this._currentTransform);
1039 paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha));
1040 paint.setShader(shader);
1041 }
1042
1043 paint.setStrokeWidth(this._strokeWidth);
1044
1045 if (this._lineDashList.length) {
1046 var dashedEffect = CanvasKit.MakeSkDashPathEffect(this._lineDashList, this._lineDashOffset);
1047 paint.setPathEffect(dashedEffect);
1048 }
1049
1050 paint.dispose = function() {
1051 dashedEffect && dashedEffect.delete();
1052 this.delete();
1053 }
1054 return paint;
1055 }
1056
Kevin Lubicka40f8322018-12-17 16:01:36 -05001057 this.stroke = function(path) {
1058 path = path ? path._getPath() : this._currentPath;
Kevin Lubick53eabf62018-12-10 12:41:26 -05001059 var strokePaint = this._strokePaint();
1060
1061 var shadowPaint = this._shadowPaint(strokePaint);
1062 if (shadowPaint) {
1063 this._canvas.save();
1064 this._canvas.concat(this._shadowOffsetMatrix());
Kevin Lubicka40f8322018-12-17 16:01:36 -05001065 this._canvas.drawPath(path, shadowPaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -05001066 this._canvas.restore();
1067 shadowPaint.dispose();
1068 }
1069
Kevin Lubicka40f8322018-12-17 16:01:36 -05001070 this._canvas.drawPath(path, strokePaint);
Kevin Lubick53eabf62018-12-10 12:41:26 -05001071 strokePaint.dispose();
1072 }
1073
1074 this.strokeRect = function(x, y, width, height) {
1075 var strokePaint = this._strokePaint();
1076 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint);
1077 strokePaint.dispose();
1078 }
1079
1080 this.strokeText = function(text, x, y, maxWidth) {
1081 // TODO do something with maxWidth, probably involving measure
1082 var strokePaint = this._strokePaint();
1083
1084 var shadowPaint = this._shadowPaint(strokePaint);
1085 if (shadowPaint) {
1086 this._canvas.save();
1087 this._canvas.concat(this._shadowOffsetMatrix());
1088 this._canvas.drawText(text, x, y, shadowPaint);
1089 this._canvas.restore();
1090 shadowPaint.dispose();
1091 }
1092 this._canvas.drawText(text, x, y, strokePaint);
1093 strokePaint.dispose();
1094 }
1095
1096 this.translate = function(dx, dy) {
1097 if (!allAreFinite(arguments)) {
1098 return;
1099 }
1100 // retroactively apply the inverse of this transform to the previous
1101 // path so it cancels out when we apply the transform at draw time.
1102 var inverted = CanvasKit.SkMatrix.translated(-dx, -dy);
1103 this._currentPath.transform(inverted);
1104 this._canvas.translate(dx, dy);
1105 this._currentTransform = this._canvas.getTotalMatrix();
1106 }
1107
1108 this.transform = function(a, b, c, d, e, f) {
1109 var newTransform = [a, c, e,
1110 b, d, f,
1111 0, 0, 1];
1112 // retroactively apply the inverse of this transform to the previous
1113 // path so it cancels out when we apply the transform at draw time.
1114 var inverted = CanvasKit.SkMatrix.invert(newTransform);
1115 this._currentPath.transform(inverted);
1116 this._canvas.concat(newTransform);
1117 this._currentTransform = this._canvas.getTotalMatrix();
1118 }
1119
1120 // Not supported operations (e.g. for Web only)
1121 this.addHitRegion = function() {};
1122 this.clearHitRegions = function() {};
1123 this.drawFocusIfNeeded = function() {};
1124 this.removeHitRegion = function() {};
1125 this.scrollPathIntoView = function() {};
1126
1127 Object.defineProperty(this, 'canvas', {
1128 value: null,
1129 writable: false
1130 });
1131}
Kevin Lubickd090a702018-12-12 10:34:02 -05001132
1133function SkBlurRadiusToSigma(radius) {
1134 // Blink (Chrome) does the following, for legacy reasons, even though it
1135 // is against the spec. https://bugs.chromium.org/p/chromium/issues/detail?id=179006
1136 // This may change in future releases.
1137 // This code is staying here in case any clients are interested in using it
1138 // to match Blink "exactly".
1139 // if (radius <= 0)
1140 // return 0;
1141 // return 0.288675 * radius + 0.5;
1142 //
1143 // This is what the spec says, which is how Firefox and others operate.
1144 return radius/2;
1145}