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