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