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