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(CanvasKit.Typeface.GetDefault(), 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 if (img instanceof HTMLImage) { 576 img = img.getSkImage(); 577 } 578 var iPaint = this._fillPaint(); 579 if (arguments.length === 3 || arguments.length === 5) { 580 var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2], 581 arguments[3] || img.width(), arguments[4] || img.height()); 582 var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height()); 583 } else if (arguments.length === 9){ 584 var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6], 585 arguments[7], arguments[8]); 586 var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2], 587 arguments[3], arguments[4]); 588 } else { 589 throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length; 590 } 591 this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false); 592 593 iPaint.dispose(); 594 }; 595 596 this.ellipse = function(x, y, radiusX, radiusY, rotation, 597 startAngle, endAngle, ccw) { 598 ellipse(this._currentPath, x, y, radiusX, radiusY, rotation, 599 startAngle, endAngle, ccw); 600 }; 601 602 // A helper to copy the current paint, ready for filling 603 // This applies the global alpha. 604 // Call dispose() after to clean up. 605 this._fillPaint = function() { 606 var paint = this._paint.copy(); 607 paint.setStyle(CanvasKit.PaintStyle.Fill); 608 if (isCanvasKitColor(this._fillStyle)) { 609 var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha); 610 paint.setColor(alphaColor); 611 } else { 612 var shader = this._fillStyle._getShader(this._currentTransform); 613 paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha)); 614 paint.setShader(shader); 615 } 616 617 paint.dispose = function() { 618 // If there are some helper effects in the future, clean them up 619 // here. In any case, we have .dispose() to make _fillPaint behave 620 // like _strokePaint and _shadowPaint. 621 this.delete(); 622 }; 623 return paint; 624 }; 625 626 this.fill = function(path, fillRule) { 627 if (typeof path === 'string') { 628 // shift the args if a Path2D is supplied 629 fillRule = path; 630 path = this._currentPath; 631 } else if (path && path._getPath) { 632 path = path._getPath(); 633 } 634 if (fillRule === 'evenodd') { 635 this._currentPath.setFillType(CanvasKit.FillType.EvenOdd); 636 } else if (fillRule === 'nonzero' || !fillRule) { 637 this._currentPath.setFillType(CanvasKit.FillType.Winding); 638 } else { 639 throw 'invalid fill rule'; 640 } 641 if (!path) { 642 path = this._currentPath; 643 } 644 645 var fillPaint = this._fillPaint(); 646 647 var shadowPaint = this._shadowPaint(fillPaint); 648 if (shadowPaint) { 649 this._canvas.save(); 650 this._applyShadowOffsetMatrix(); 651 this._canvas.drawPath(path, shadowPaint); 652 this._canvas.restore(); 653 shadowPaint.dispose(); 654 } 655 this._canvas.drawPath(path, fillPaint); 656 fillPaint.dispose(); 657 }; 658 659 this.fillRect = function(x, y, width, height) { 660 var fillPaint = this._fillPaint(); 661 662 var shadowPaint = this._shadowPaint(fillPaint); 663 if (shadowPaint) { 664 this._canvas.save(); 665 this._applyShadowOffsetMatrix(); 666 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint); 667 this._canvas.restore(); 668 shadowPaint.dispose(); 669 } 670 671 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint); 672 fillPaint.dispose(); 673 }; 674 675 this.fillText = function(text, x, y, maxWidth) { 676 // TODO do something with maxWidth, probably involving measure 677 var fillPaint = this._fillPaint(); 678 var blob = CanvasKit.TextBlob.MakeFromText(text, this._font); 679 680 var shadowPaint = this._shadowPaint(fillPaint); 681 if (shadowPaint) { 682 this._canvas.save(); 683 this._applyShadowOffsetMatrix(); 684 this._canvas.drawTextBlob(blob, x, y, shadowPaint); 685 this._canvas.restore(); 686 shadowPaint.dispose(); 687 } 688 this._canvas.drawTextBlob(blob, x, y, fillPaint); 689 blob.delete(); 690 fillPaint.dispose(); 691 }; 692 693 this.getImageData = function(x, y, w, h) { 694 var pixels = this._canvas.readPixels(x, y, { 695 'width': w, 696 'height': h, 697 'colorType': CanvasKit.ColorType.RGBA_8888, 698 'alphaType': CanvasKit.AlphaType.Unpremul, 699 'colorSpace': CanvasKit.ColorSpace.SRGB, 700 }); 701 if (!pixels) { 702 return null; 703 } 704 // This essentially re-wraps the pixels from a Uint8Array to 705 // a Uint8ClampedArray (without making a copy of pixels). 706 return new ImageData( 707 new Uint8ClampedArray(pixels.buffer), 708 w, h); 709 }; 710 711 this.getLineDash = function() { 712 return this._lineDashList.slice(); 713 }; 714 715 this._mapToLocalCoordinates = function(pts) { 716 var inverted = CanvasKit.Matrix.invert(this._currentTransform); 717 CanvasKit.Matrix.mapPoints(inverted, pts); 718 return pts; 719 }; 720 721 this.isPointInPath = function(x, y, fillmode) { 722 var args = arguments; 723 if (args.length === 3) { 724 var path = this._currentPath; 725 } else if (args.length === 4) { 726 var path = args[0]; 727 x = args[1]; 728 y = args[2]; 729 fillmode = args[3]; 730 } else { 731 throw 'invalid arg count, need 3 or 4, got ' + args.length; 732 } 733 if (!isFinite(x) || !isFinite(y)) { 734 return false; 735 } 736 fillmode = fillmode || 'nonzero'; 737 if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) { 738 return false; 739 } 740 // x and y are in canvas coordinates (i.e. unaffected by CTM) 741 var pts = this._mapToLocalCoordinates([x, y]); 742 x = pts[0]; 743 y = pts[1]; 744 path.setFillType(fillmode === 'nonzero' ? 745 CanvasKit.FillType.Winding : 746 CanvasKit.FillType.EvenOdd); 747 return path.contains(x, y); 748 }; 749 750 this.isPointInStroke = function(x, y) { 751 var args = arguments; 752 if (args.length === 2) { 753 var path = this._currentPath; 754 } else if (args.length === 3) { 755 var path = args[0]; 756 x = args[1]; 757 y = args[2]; 758 } else { 759 throw 'invalid arg count, need 2 or 3, got ' + args.length; 760 } 761 if (!isFinite(x) || !isFinite(y)) { 762 return false; 763 } 764 var pts = this._mapToLocalCoordinates([x, y]); 765 x = pts[0]; 766 y = pts[1]; 767 var temp = path.copy(); 768 // fillmode is always nonzero 769 temp.setFillType(CanvasKit.FillType.Winding); 770 temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit, 771 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(), 772 'precision': 0.3, // this is what Chrome uses to compute this 773 }); 774 var retVal = temp.contains(x, y); 775 temp.delete(); 776 return retVal; 777 }; 778 779 this.lineTo = function(x, y) { 780 lineTo(this._currentPath, x, y); 781 }; 782 783 this.measureText = function(text) { 784 const ids = this._font.getGlyphIDs(text); 785 const widths = this._font.getGlyphWidths(ids); 786 let totalWidth = 0; 787 for (const w of widths) { 788 totalWidth += w; 789 } 790 return { 791 "width": totalWidth, 792 }; 793 }; 794 795 this.moveTo = function(x, y) { 796 moveTo(this._currentPath, x, y); 797 }; 798 799 this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) { 800 if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) { 801 return; 802 } 803 if (dirtyX === undefined) { 804 // fast, simple path for basic call 805 this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y); 806 return; 807 } 808 dirtyX = dirtyX || 0; 809 dirtyY = dirtyY || 0; 810 dirtyWidth = dirtyWidth || imageData.width; 811 dirtyHeight = dirtyHeight || imageData.height; 812 813 // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata 814 if (dirtyWidth < 0) { 815 dirtyX = dirtyX+dirtyWidth; 816 dirtyWidth = Math.abs(dirtyWidth); 817 } 818 if (dirtyHeight < 0) { 819 dirtyY = dirtyY+dirtyHeight; 820 dirtyHeight = Math.abs(dirtyHeight); 821 } 822 if (dirtyX < 0) { 823 dirtyWidth = dirtyWidth + dirtyX; 824 dirtyX = 0; 825 } 826 if (dirtyY < 0) { 827 dirtyHeight = dirtyHeight + dirtyY; 828 dirtyY = 0; 829 } 830 if (dirtyWidth <= 0 || dirtyHeight <= 0) { 831 return; 832 } 833 var img = CanvasKit.MakeImage({ 834 'width': imageData.width, 835 'height': imageData.height, 836 'alphaType': CanvasKit.AlphaType.Unpremul, 837 'colorType': CanvasKit.ColorType.RGBA_8888, 838 'colorSpace': CanvasKit.ColorSpace.SRGB 839 }, imageData.data, 4 * imageData.width); 840 var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight); 841 var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight); 842 var inverted = CanvasKit.Matrix.invert(this._currentTransform); 843 this._canvas.save(); 844 // putImageData() operates in device space. 845 this._canvas.concat(inverted); 846 this._canvas.drawImageRect(img, src, dst, null, false); 847 this._canvas.restore(); 848 img.delete(); 849 }; 850 851 this.quadraticCurveTo = function(cpx, cpy, x, y) { 852 quadraticCurveTo(this._currentPath, cpx, cpy, x, y); 853 }; 854 855 this.rect = function(x, y, width, height) { 856 rect(this._currentPath, x, y, width, height); 857 }; 858 859 this.resetTransform = function() { 860 // Apply the current transform to the path and then reset 861 // to the identity. Essentially "commit" the transform. 862 this._currentPath.transform(this._currentTransform); 863 var inverted = CanvasKit.Matrix.invert(this._currentTransform); 864 this._canvas.concat(inverted); 865 // This should be identity, modulo floating point drift. 866 this._currentTransform = this._canvas.getTotalMatrix(); 867 }; 868 869 this.restore = function() { 870 var newState = this._canvasStateStack.pop(); 871 if (!newState) { 872 return; 873 } 874 // "commit" the current transform. We pop, then apply the inverse of the 875 // popped state, which has the effect of applying just the delta of 876 // transforms between old and new. 877 var combined = CanvasKit.Matrix.multiply( 878 this._currentTransform, 879 CanvasKit.Matrix.invert(newState.ctm) 880 ); 881 this._currentPath.transform(combined); 882 this._paint.delete(); 883 this._paint = newState.paint; 884 885 this._lineDashList = newState.ldl; 886 this._strokeWidth = newState.sw; 887 this._strokeStyle = newState.ss; 888 this._fillStyle = newState.fs; 889 this._shadowOffsetX = newState.sox; 890 this._shadowOffsetY = newState.soy; 891 this._shadowBlur = newState.sb; 892 this._shadowColor = newState.shc; 893 this._globalAlpha = newState.ga; 894 this._globalCompositeOperation = newState.gco; 895 this._lineDashOffset = newState.ldo; 896 this._fontString = newState.fontstr; 897 898 //TODO: textAlign, textBaseline 899 900 // restores the clip and ctm 901 this._canvas.restore(); 902 this._currentTransform = this._canvas.getTotalMatrix(); 903 }; 904 905 this.rotate = function(radians) { 906 if (!isFinite(radians)) { 907 return; 908 } 909 // retroactively apply the inverse of this transform to the previous 910 // path so it cancels out when we apply the transform at draw time. 911 var inverted = CanvasKit.Matrix.rotated(-radians); 912 this._currentPath.transform(inverted); 913 this._canvas.rotate(radiansToDegrees(radians), 0, 0); 914 this._currentTransform = this._canvas.getTotalMatrix(); 915 }; 916 917 this.save = function() { 918 if (this._fillStyle._copy) { 919 var fs = this._fillStyle._copy(); 920 this._toCleanUp.push(fs); 921 } else { 922 var fs = this._fillStyle; 923 } 924 925 if (this._strokeStyle._copy) { 926 var ss = this._strokeStyle._copy(); 927 this._toCleanUp.push(ss); 928 } else { 929 var ss = this._strokeStyle; 930 } 931 932 this._canvasStateStack.push({ 933 ctm: this._currentTransform.slice(), 934 ldl: this._lineDashList.slice(), 935 sw: this._strokeWidth, 936 ss: ss, 937 fs: fs, 938 sox: this._shadowOffsetX, 939 soy: this._shadowOffsetY, 940 sb: this._shadowBlur, 941 shc: this._shadowColor, 942 ga: this._globalAlpha, 943 ldo: this._lineDashOffset, 944 gco: this._globalCompositeOperation, 945 paint: this._paint.copy(), 946 fontstr: this._fontString, 947 //TODO: textAlign, textBaseline 948 }); 949 // Saves the clip 950 this._canvas.save(); 951 }; 952 953 this.scale = function(sx, sy) { 954 if (!allAreFinite(arguments)) { 955 return; 956 } 957 // retroactively apply the inverse of this transform to the previous 958 // path so it cancels out when we apply the transform at draw time. 959 var inverted = CanvasKit.Matrix.scaled(1/sx, 1/sy); 960 this._currentPath.transform(inverted); 961 this._canvas.scale(sx, sy); 962 this._currentTransform = this._canvas.getTotalMatrix(); 963 }; 964 965 this.setLineDash = function(dashes) { 966 for (var i = 0; i < dashes.length; i++) { 967 if (!isFinite(dashes[i]) || dashes[i] < 0) { 968 Debug('dash list must have positive, finite values'); 969 return; 970 } 971 } 972 if (dashes.length % 2 === 1) { 973 // as per the spec, concatenate 2 copies of dashes 974 // to give it an even number of elements. 975 Array.prototype.push.apply(dashes, dashes); 976 } 977 this._lineDashList = dashes; 978 }; 979 980 this.setTransform = function(a, b, c, d, e, f) { 981 if (!(allAreFinite(arguments))) { 982 return; 983 } 984 this.resetTransform(); 985 this.transform(a, b, c, d, e, f); 986 }; 987 988 // We need to apply the shadowOffsets on the device coordinates, so we undo 989 // the CTM, apply the offsets, then re-apply the CTM. 990 this._applyShadowOffsetMatrix = function() { 991 var inverted = CanvasKit.Matrix.invert(this._currentTransform); 992 this._canvas.concat(inverted); 993 this._canvas.concat(CanvasKit.Matrix.translated(this._shadowOffsetX, this._shadowOffsetY)); 994 this._canvas.concat(this._currentTransform); 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.MaskFilter.MakeBlur(CanvasKit.BlurStyle.Normal, 1015 BlurRadiusToSigma(this._shadowBlur), 1016 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 (isCanvasKitColor(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.PathEffect.MakeDash(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 1057 this.stroke = function(path) { 1058 path = path ? path._getPath() : this._currentPath; 1059 var strokePaint = this._strokePaint(); 1060 1061 var shadowPaint = this._shadowPaint(strokePaint); 1062 if (shadowPaint) { 1063 this._canvas.save(); 1064 this._applyShadowOffsetMatrix(); 1065 this._canvas.drawPath(path, shadowPaint); 1066 this._canvas.restore(); 1067 shadowPaint.dispose(); 1068 } 1069 1070 this._canvas.drawPath(path, strokePaint); 1071 strokePaint.dispose(); 1072 }; 1073 1074 this.strokeRect = function(x, y, width, height) { 1075 var strokePaint = this._strokePaint(); 1076 1077 var shadowPaint = this._shadowPaint(strokePaint); 1078 if (shadowPaint) { 1079 this._canvas.save(); 1080 this._applyShadowOffsetMatrix(); 1081 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), shadowPaint); 1082 this._canvas.restore(); 1083 shadowPaint.dispose(); 1084 } 1085 this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint); 1086 strokePaint.dispose(); 1087 }; 1088 1089 this.strokeText = function(text, x, y, maxWidth) { 1090 // TODO do something with maxWidth, probably involving measure 1091 var strokePaint = this._strokePaint(); 1092 var blob = CanvasKit.TextBlob.MakeFromText(text, this._font); 1093 1094 var shadowPaint = this._shadowPaint(strokePaint); 1095 if (shadowPaint) { 1096 this._canvas.save(); 1097 this._applyShadowOffsetMatrix(); 1098 this._canvas.drawTextBlob(blob, x, y, shadowPaint); 1099 this._canvas.restore(); 1100 shadowPaint.dispose(); 1101 } 1102 this._canvas.drawTextBlob(blob, x, y, strokePaint); 1103 blob.delete(); 1104 strokePaint.dispose(); 1105 }; 1106 1107 this.translate = function(dx, dy) { 1108 if (!allAreFinite(arguments)) { 1109 return; 1110 } 1111 // retroactively apply the inverse of this transform to the previous 1112 // path so it cancels out when we apply the transform at draw time. 1113 var inverted = CanvasKit.Matrix.translated(-dx, -dy); 1114 this._currentPath.transform(inverted); 1115 this._canvas.translate(dx, dy); 1116 this._currentTransform = this._canvas.getTotalMatrix(); 1117 }; 1118 1119 this.transform = function(a, b, c, d, e, f) { 1120 var newTransform = [a, c, e, 1121 b, d, f, 1122 0, 0, 1]; 1123 // retroactively apply the inverse of this transform to the previous 1124 // path so it cancels out when we apply the transform at draw time. 1125 var inverted = CanvasKit.Matrix.invert(newTransform); 1126 this._currentPath.transform(inverted); 1127 this._canvas.concat(newTransform); 1128 this._currentTransform = this._canvas.getTotalMatrix(); 1129 }; 1130 1131 // Not supported operations (e.g. for Web only) 1132 this.addHitRegion = function() {}; 1133 this.clearHitRegions = function() {}; 1134 this.drawFocusIfNeeded = function() {}; 1135 this.removeHitRegion = function() {}; 1136 this.scrollPathIntoView = function() {}; 1137 1138 Object.defineProperty(this, 'canvas', { 1139 value: null, 1140 writable: false 1141 }); 1142} 1143 1144function BlurRadiusToSigma(radius) { 1145 // Blink (Chrome) does the following, for legacy reasons, even though it 1146 // is against the spec. https://bugs.chromium.org/p/chromium/issues/detail?id=179006 1147 // This may change in future releases. 1148 // This code is staying here in case any clients are interested in using it 1149 // to match Blink "exactly". 1150 // if (radius <= 0) 1151 // return 0; 1152 // return 0.288675 * radius + 0.5; 1153 // 1154 // This is what the spec says, which is how Firefox and others operate. 1155 return radius/2; 1156} 1157