function CanvasRenderingContext2D(skcanvas) { this._canvas = skcanvas; this._paint = new CanvasKit.SkPaint(); this._paint.setAntiAlias(true); this._paint.setStrokeMiter(10); this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt); this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter); this._strokeStyle = CanvasKit.BLACK; this._fillStyle = CanvasKit.BLACK; this._shadowBlur = 0; this._shadowColor = CanvasKit.TRANSPARENT; this._shadowOffsetX = 0; this._shadowOffsetY = 0; this._globalAlpha = 1; this._strokeWidth = 1; this._lineDashOffset = 0; this._lineDashList = []; // aka SkBlendMode this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver; this._imageFilterQuality = CanvasKit.FilterQuality.Low; this._imageSmoothingEnabled = true; this._paint.setStrokeWidth(this._strokeWidth); this._paint.setBlendMode(this._globalCompositeOperation); this._currentPath = new CanvasKit.SkPath(); this._currentTransform = CanvasKit.SkMatrix.identity(); // Use this for save/restore this._canvasStateStack = []; // Keep a reference to all the effects (e.g. gradients, patterns) // that were allocated for cleanup in _dispose. this._toCleanUp = []; this._dispose = function() { this._currentPath.delete(); this._paint.delete(); this._toCleanUp.forEach(function(c) { c._dispose(); }); // Don't delete this._canvas as it will be disposed // by the surface of which it is based. } // This always accepts DOMMatrix/SVGMatrix or any other // object that has properties a,b,c,d,e,f defined. // Returns a DOM-Matrix like dictionary Object.defineProperty(this, 'currentTransform', { enumerable: true, get: function() { return { 'a' : this._currentTransform[0], 'c' : this._currentTransform[1], 'e' : this._currentTransform[2], 'b' : this._currentTransform[3], 'd' : this._currentTransform[4], 'f' : this._currentTransform[5], }; }, // @param {DOMMatrix} matrix set: function(matrix) { if (matrix.a) { // if we see a property named 'a', guess that b-f will // also be there. this.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f); } } }); Object.defineProperty(this, 'fillStyle', { enumerable: true, get: function() { if (Number.isInteger(this._fillStyle)) { return colorToString(this._fillStyle); } return this._fillStyle; }, set: function(newStyle) { if (typeof newStyle === 'string') { this._fillStyle = parseColor(newStyle); } else if (newStyle._getShader) { // It's an effect that has a shader. this._fillStyle = newStyle } } }); Object.defineProperty(this, 'font', { enumerable: true, get: function(newStyle) { // TODO generate this return '10px sans-serif'; }, set: function(newStyle) { var size = parseFontSize(newStyle); // TODO(kjlubick) styles, font name this._paint.setTextSize(size); } }); Object.defineProperty(this, 'globalAlpha', { enumerable: true, get: function() { return this._globalAlpha; }, set: function(newAlpha) { // ignore invalid values, as per the spec if (!isFinite(newAlpha) || newAlpha < 0 || newAlpha > 1) { return; } this._globalAlpha = newAlpha; } }); Object.defineProperty(this, 'globalCompositeOperation', { enumerable: true, get: function() { switch (this._globalCompositeOperation) { // composite-mode case CanvasKit.BlendMode.SrcOver: return 'source-over'; case CanvasKit.BlendMode.DstOver: return 'destination-over'; case CanvasKit.BlendMode.Src: return 'copy'; case CanvasKit.BlendMode.Dst: return 'destination'; case CanvasKit.BlendMode.Clear: return 'clear'; case CanvasKit.BlendMode.SrcIn: return 'source-in'; case CanvasKit.BlendMode.DstIn: return 'destination-in'; case CanvasKit.BlendMode.SrcOut: return 'source-out'; case CanvasKit.BlendMode.DstOut: return 'destination-out'; case CanvasKit.BlendMode.SrcATop: return 'source-atop'; case CanvasKit.BlendMode.DstATop: return 'destination-atop'; case CanvasKit.BlendMode.Xor: return 'xor'; case CanvasKit.BlendMode.Plus: return 'lighter'; case CanvasKit.BlendMode.Multiply: return 'multiply'; case CanvasKit.BlendMode.Screen: return 'screen'; case CanvasKit.BlendMode.Overlay: return 'overlay'; case CanvasKit.BlendMode.Darken: return 'darken'; case CanvasKit.BlendMode.Lighten: return 'lighten'; case CanvasKit.BlendMode.ColorDodge: return 'color-dodge'; case CanvasKit.BlendMode.ColorBurn: return 'color-burn'; case CanvasKit.BlendMode.HardLight: return 'hard-light'; case CanvasKit.BlendMode.SoftLight: return 'soft-light'; case CanvasKit.BlendMode.Difference: return 'difference'; case CanvasKit.BlendMode.Exclusion: return 'exclusion'; case CanvasKit.BlendMode.Hue: return 'hue'; case CanvasKit.BlendMode.Saturation: return 'saturation'; case CanvasKit.BlendMode.Color: return 'color'; case CanvasKit.BlendMode.Luminosity: return 'luminosity'; } }, set: function(newMode) { switch (newMode) { // composite-mode case 'source-over': this._globalCompositeOperation = CanvasKit.BlendMode.SrcOver; break; case 'destination-over': this._globalCompositeOperation = CanvasKit.BlendMode.DstOver; break; case 'copy': this._globalCompositeOperation = CanvasKit.BlendMode.Src; break; case 'destination': this._globalCompositeOperation = CanvasKit.BlendMode.Dst; break; case 'clear': this._globalCompositeOperation = CanvasKit.BlendMode.Clear; break; case 'source-in': this._globalCompositeOperation = CanvasKit.BlendMode.SrcIn; break; case 'destination-in': this._globalCompositeOperation = CanvasKit.BlendMode.DstIn; break; case 'source-out': this._globalCompositeOperation = CanvasKit.BlendMode.SrcOut; break; case 'destination-out': this._globalCompositeOperation = CanvasKit.BlendMode.DstOut; break; case 'source-atop': this._globalCompositeOperation = CanvasKit.BlendMode.SrcATop; break; case 'destination-atop': this._globalCompositeOperation = CanvasKit.BlendMode.DstATop; break; case 'xor': this._globalCompositeOperation = CanvasKit.BlendMode.Xor; break; case 'lighter': this._globalCompositeOperation = CanvasKit.BlendMode.Plus; break; case 'plus-lighter': this._globalCompositeOperation = CanvasKit.BlendMode.Plus; break; case 'plus-darker': throw 'plus-darker is not supported'; // blend-mode case 'multiply': this._globalCompositeOperation = CanvasKit.BlendMode.Multiply; break; case 'screen': this._globalCompositeOperation = CanvasKit.BlendMode.Screen; break; case 'overlay': this._globalCompositeOperation = CanvasKit.BlendMode.Overlay; break; case 'darken': this._globalCompositeOperation = CanvasKit.BlendMode.Darken; break; case 'lighten': this._globalCompositeOperation = CanvasKit.BlendMode.Lighten; break; case 'color-dodge': this._globalCompositeOperation = CanvasKit.BlendMode.ColorDodge; break; case 'color-burn': this._globalCompositeOperation = CanvasKit.BlendMode.ColorBurn; break; case 'hard-light': this._globalCompositeOperation = CanvasKit.BlendMode.HardLight; break; case 'soft-light': this._globalCompositeOperation = CanvasKit.BlendMode.SoftLight; break; case 'difference': this._globalCompositeOperation = CanvasKit.BlendMode.Difference; break; case 'exclusion': this._globalCompositeOperation = CanvasKit.BlendMode.Exclusion; break; case 'hue': this._globalCompositeOperation = CanvasKit.BlendMode.Hue; break; case 'saturation': this._globalCompositeOperation = CanvasKit.BlendMode.Saturation; break; case 'color': this._globalCompositeOperation = CanvasKit.BlendMode.Color; break; case 'luminosity': this._globalCompositeOperation = CanvasKit.BlendMode.Luminosity; break; default: return; } this._paint.setBlendMode(this._globalCompositeOperation); } }); Object.defineProperty(this, 'imageSmoothingEnabled', { enumerable: true, get: function() { return this._imageSmoothingEnabled; }, set: function(newVal) { this._imageSmoothingEnabled = !!newVal; } }); Object.defineProperty(this, 'imageSmoothingQuality', { enumerable: true, get: function() { switch (this._imageFilterQuality) { case CanvasKit.FilterQuality.Low: return 'low'; case CanvasKit.FilterQuality.Medium: return 'medium'; case CanvasKit.FilterQuality.High: return 'high'; } }, set: function(newQuality) { switch (newQuality) { case 'low': this._imageFilterQuality = CanvasKit.FilterQuality.Low; return; case 'medium': this._imageFilterQuality = CanvasKit.FilterQuality.Medium; return; case 'high': this._imageFilterQuality = CanvasKit.FilterQuality.High; return; } } }); Object.defineProperty(this, 'lineCap', { enumerable: true, get: function() { switch (this._paint.getStrokeCap()) { case CanvasKit.StrokeCap.Butt: return 'butt'; case CanvasKit.StrokeCap.Round: return 'round'; case CanvasKit.StrokeCap.Square: return 'square'; } }, set: function(newCap) { switch (newCap) { case 'butt': this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt); return; case 'round': this._paint.setStrokeCap(CanvasKit.StrokeCap.Round); return; case 'square': this._paint.setStrokeCap(CanvasKit.StrokeCap.Square); return; } } }); Object.defineProperty(this, 'lineDashOffset', { enumerable: true, get: function() { return this._lineDashOffset; }, set: function(newOffset) { if (!isFinite(newOffset)) { return; } this._lineDashOffset = newOffset; } }); Object.defineProperty(this, 'lineJoin', { enumerable: true, get: function() { switch (this._paint.getStrokeJoin()) { case CanvasKit.StrokeJoin.Miter: return 'miter'; case CanvasKit.StrokeJoin.Round: return 'round'; case CanvasKit.StrokeJoin.Bevel: return 'bevel'; } }, set: function(newJoin) { switch (newJoin) { case 'miter': this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter); return; case 'round': this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Round); return; case 'bevel': this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Bevel); return; } } }); Object.defineProperty(this, 'lineWidth', { enumerable: true, get: function() { return this._paint.getStrokeWidth(); }, set: function(newWidth) { if (newWidth <= 0 || !newWidth) { // Spec says to ignore NaN/Inf/0/negative values return; } this._strokeWidth = newWidth; this._paint.setStrokeWidth(newWidth); } }); Object.defineProperty(this, 'miterLimit', { enumerable: true, get: function() { return this._paint.getStrokeMiter(); }, set: function(newLimit) { if (newLimit <= 0 || !newLimit) { // Spec says to ignore NaN/Inf/0/negative values return; } this._paint.setStrokeMiter(newLimit); } }); Object.defineProperty(this, 'shadowBlur', { enumerable: true, get: function() { return this._shadowBlur; }, set: function(newBlur) { // ignore negative, inf and NAN (but not 0) as per the spec. if (newBlur < 0 || !isFinite(newBlur)) { return; } this._shadowBlur = newBlur; } }); Object.defineProperty(this, 'shadowColor', { enumerable: true, get: function() { return colorToString(this._shadowColor); }, set: function(newColor) { this._shadowColor = parseColor(newColor); } }); Object.defineProperty(this, 'shadowOffsetX', { enumerable: true, get: function() { return this._shadowOffsetX; }, set: function(newOffset) { if (!isFinite(newOffset)) { return; } this._shadowOffsetX = newOffset; } }); Object.defineProperty(this, 'shadowOffsetY', { enumerable: true, get: function() { return this._shadowOffsetY; }, set: function(newOffset) { if (!isFinite(newOffset)) { return; } this._shadowOffsetY = newOffset; } }); Object.defineProperty(this, 'strokeStyle', { enumerable: true, get: function() { return colorToString(this._strokeStyle); }, set: function(newStyle) { if (typeof newStyle === 'string') { this._strokeStyle = parseColor(newStyle); } else if (newStyle._getShader) { // It's probably an effect. this._strokeStyle = newStyle } } }); this.arc = function(x, y, radius, startAngle, endAngle, ccw) { // As per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-arc // arc is essentially a simpler version of ellipse. this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, ccw); } this.arcTo = function(x1, y1, x2, y2, radius) { if (!allAreFinite(arguments)) { return; } if (radius < 0) { throw 'radii cannot be negative'; } if (this._currentPath.isEmpty()) { this.moveTo(x1, y1); } this._currentPath.arcTo(x1, y1, x2, y2, radius); } // As per the spec this doesn't begin any paths, it only // clears out any previous paths. this.beginPath = function() { this._currentPath.delete(); this._currentPath = new CanvasKit.SkPath(); } this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { if (!allAreFinite(arguments)) { return; } if (this._currentPath.isEmpty()) { this.moveTo(cp1x, cp1y); } this._currentPath.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y); } this.clearRect = function(x, y, width, height) { this._paint.setStyle(CanvasKit.PaintStyle.Fill); this._paint.setBlendMode(CanvasKit.BlendMode.Clear); this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), this._paint); this._paint.setBlendMode(this._globalCompositeOperation); } this.clip = function(fillRule) { var clip = this._currentPath.copy(); if (fillRule && fillRule.toLowerCase() === 'evenodd') { clip.setFillType(CanvasKit.FillType.EvenOdd); } else { clip.setFillType(CanvasKit.FillType.Winding); } this._canvas.clipPath(clip, CanvasKit.ClipOp.Intersect, true); } this.closePath = function() { if (this._currentPath.isEmpty()) { return; } // Check to see if we are not just a single point var bounds = this._currentPath.getBounds(); if ((bounds.fBottom - bounds.fTop) || (bounds.fRight - bounds.fLeft)) { this._currentPath.close(); } } this.createImageData = function() { // either takes in 1 or 2 arguments: // - imagedata on which to copy *width* and *height* only // - width, height if (arguments.length === 1) { var oldData = arguments[0]; var byteLength = 4 * oldData.width * oldData.height; return new ImageData(new Uint8ClampedArray(byteLength), oldData.width, oldData.height); } else if (arguments.length === 2) { var width = arguments[0]; var height = arguments[1]; var byteLength = 4 * width * height; return new ImageData(new Uint8ClampedArray(byteLength), width, height); } else { throw 'createImageData expects 1 or 2 arguments, got '+arguments.length; } } this.createLinearGradient = function(x1, y1, x2, y2) { if (!allAreFinite(arguments)) { return; } var lcg = new LinearCanvasGradient(x1, y1, x2, y2); this._toCleanUp.push(lcg); return lcg; } this.createPattern = function(image, repetition) { var cp = new CanvasPattern(image, repetition); this._toCleanUp.push(cp); return cp; } this.createRadialGradient = function(x1, y1, r1, x2, y2, r2) { if (!allAreFinite(arguments)) { return; } var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2); this._toCleanUp.push(rcg); return rcg; } this._imagePaint = function() { var iPaint = this._fillPaint(); if (!this._imageSmoothingEnabled) { iPaint.setFilterQuality(CanvasKit.FilterQuality.None); } else { iPaint.setFilterQuality(this._imageFilterQuality); } return iPaint; } this.drawImage = function(img) { // 3 potential sets of arguments // - image, dx, dy // - image, dx, dy, dWidth, dHeight // - image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight // use the fillPaint, which has the globalAlpha in it // which drawImageRect will use. var iPaint = this._imagePaint(); if (arguments.length === 3 || arguments.length === 5) { var destRect = CanvasKit.XYWHRect(arguments[1], arguments[2], arguments[3] || img.width(), arguments[4] || img.height()); var srcRect = CanvasKit.XYWHRect(0, 0, img.width(), img.height()); } else if (arguments.length === 9){ var destRect = CanvasKit.XYWHRect(arguments[5], arguments[6], arguments[7], arguments[8]); var srcRect = CanvasKit.XYWHRect(arguments[1], arguments[2], arguments[3], arguments[4]); } else { throw 'invalid number of args for drawImage, need 3, 5, or 9; got '+ arguments.length; } this._canvas.drawImageRect(img, srcRect, destRect, iPaint, false); iPaint.dispose(); } this._ellipseHelper = function(x, y, radiusX, radiusY, startAngle, endAngle) { var sweepDegrees = radiansToDegrees(endAngle - startAngle); var startDegrees = radiansToDegrees(startAngle); var oval = CanvasKit.LTRBRect(x - radiusX, y - radiusY, x + radiusX, y + radiusY); // draw in 2 180 degree segments because trying to draw all 360 degrees at once // draws nothing. if (almostEqual(Math.abs(sweepDegrees), 360)) { var halfSweep = sweepDegrees/2; this._currentPath.arcTo(oval, startDegrees, halfSweep, false); this._currentPath.arcTo(oval, startDegrees + halfSweep, halfSweep, false); return; } this._currentPath.arcTo(oval, startDegrees, sweepDegrees, false); } this.ellipse = function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, ccw) { if (!allAreFinite([x, y, radiusX, radiusY, rotation, startAngle, endAngle])) { return; } if (radiusX < 0 || radiusY < 0) { throw 'radii cannot be negative'; } // based off of CanonicalizeAngle in Chrome var tao = 2 * Math.PI; var newStartAngle = startAngle % tao; if (newStartAngle < 0) { newStartAngle += tao; } var delta = newStartAngle - startAngle; startAngle = newStartAngle; endAngle += delta; // Based off of AdjustEndAngle in Chrome. if (!ccw && (endAngle - startAngle) >= tao) { // Draw complete ellipse endAngle = startAngle + tao; } else if (ccw && (startAngle - endAngle) >= tao) { // Draw complete ellipse endAngle = startAngle - tao; } else if (!ccw && startAngle > endAngle) { endAngle = startAngle + (tao - (startAngle - endAngle) % tao); } else if (ccw && startAngle < endAngle) { endAngle = startAngle - (tao - (endAngle - startAngle) % tao); } // Based off of Chrome's implementation in // https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/graphics/path.cc // of note, can't use addArc or addOval because they close the arc, which // the spec says not to do (unless the user explicitly calls closePath). // This throws off points being in/out of the arc. if (!rotation) { this._ellipseHelper(x, y, radiusX, radiusY, startAngle, endAngle); return; } var rotated = CanvasKit.SkMatrix.rotated(rotation, x, y); this._currentPath.transform(CanvasKit.SkMatrix.invert(rotated)); this._ellipseHelper(x, y, radiusX, radiusY, startAngle, endAngle); this._currentPath.transform(rotated); } // A helper to copy the current paint, ready for filling // This applies the global alpha. // Call dispose() after to clean up. this._fillPaint = function() { var paint = this._paint.copy(); paint.setStyle(CanvasKit.PaintStyle.Fill); if (Number.isInteger(this._fillStyle)) { var alphaColor = CanvasKit.multiplyByAlpha(this._fillStyle, this._globalAlpha); paint.setColor(alphaColor); } else { var shader = this._fillStyle._getShader(this._currentTransform); paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha)); paint.setShader(shader); } paint.dispose = function() { // If there are some helper effects in the future, clean them up // here. In any case, we have .dispose() to make _fillPaint behave // like _strokePaint and _shadowPaint. this.delete(); } return paint; } this.fill = function(fillRule) { if (fillRule === 'evenodd') { this._currentPath.setFillType(CanvasKit.FillType.EvenOdd); } else if (fillRule === 'nonzero' || !fillRule) { this._currentPath.setFillType(CanvasKit.FillType.Winding); } else { throw 'invalid fill rule'; } var fillPaint = this._fillPaint(); var shadowPaint = this._shadowPaint(fillPaint); if (shadowPaint) { this._canvas.save(); this._canvas.concat(this._shadowOffsetMatrix()); this._canvas.drawPath(this._currentPath, shadowPaint); this._canvas.restore(); shadowPaint.dispose(); } this._canvas.drawPath(this._currentPath, fillPaint); fillPaint.dispose(); } this.fillRect = function(x, y, width, height) { var fillPaint = this._fillPaint(); this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), fillPaint); fillPaint.dispose(); } this.fillText = function(text, x, y, maxWidth) { // TODO do something with maxWidth, probably involving measure var fillPaint = this._fillPaint() var shadowPaint = this._shadowPaint(fillPaint); if (shadowPaint) { this._canvas.save(); this._canvas.concat(this._shadowOffsetMatrix()); this._canvas.drawText(text, x, y, shadowPaint); this._canvas.restore(); shadowPaint.dispose(); } this._canvas.drawText(text, x, y, fillPaint); fillPaint.dispose(); } this.getImageData = function(x, y, w, h) { var pixels = this._canvas.readPixels(x, y, w, h); if (!pixels) { return null; } // This essentially re-wraps the pixels from a Uint8Array to // a Uint8ClampedArray (without making a copy of pixels). return new ImageData( new Uint8ClampedArray(pixels.buffer), w, h); } this.getLineDash = function() { return this._lineDashList.slice(); } this._mapToLocalCoordinates = function(pts) { var inverted = CanvasKit.SkMatrix.invert(this._currentTransform); CanvasKit.SkMatrix.mapPoints(inverted, pts); return pts; } this.isPointInPath = function(x, y, fillmode) { if (!isFinite(x) || !isFinite(y)) { return false; } fillmode = fillmode || 'nonzero'; if (!(fillmode === 'nonzero' || fillmode === 'evenodd')) { return false; } // x and y are in canvas coordinates (i.e. unaffected by CTM) var pts = this._mapToLocalCoordinates([x, y]); x = pts[0]; y = pts[1]; this._currentPath.setFillType(fillmode === 'nonzero' ? CanvasKit.FillType.Winding : CanvasKit.FillType.EvenOdd); return this._currentPath.contains(x, y); } this.isPointInStroke = function(x, y) { if (!isFinite(x) || !isFinite(y)) { return false; } var pts = this._mapToLocalCoordinates([x, y]); x = pts[0]; y = pts[1]; var temp = this._currentPath.copy(); // fillmode is always nonzero temp.setFillType(CanvasKit.FillType.Winding); temp.stroke({'width': this.lineWidth, 'miter_limit': this.miterLimit, 'cap': this._paint.getStrokeCap(), 'join': this._paint.getStrokeJoin(), 'precision': 0.3, // this is what Chrome uses to compute this }); var retVal = temp.contains(x, y); temp.delete(); return retVal; } this.lineTo = function(x, y) { if (!allAreFinite(arguments)) { return; } // A lineTo without a previous point has a moveTo inserted before it if (this._currentPath.isEmpty()) { this._currentPath.moveTo(x, y); } this._currentPath.lineTo(x, y); } this.measureText = function(text) { return { width: this._paint.measureText(text), // TODO other measurements? } } this.moveTo = function(x, y) { if (!allAreFinite(arguments)) { return; } this._currentPath.moveTo(x, y); } this.putImageData = function(imageData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) { if (!allAreFinite([x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight])) { return; } if (dirtyX === undefined) { // fast, simple path for basic call this._canvas.writePixels(imageData.data, imageData.width, imageData.height, x, y); return; } dirtyX = dirtyX || 0; dirtyY = dirtyY || 0; dirtyWidth = dirtyWidth || imageData.width; dirtyHeight = dirtyHeight || imageData.height; // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata if (dirtyWidth < 0) { dirtyX = dirtyX+dirtyWidth; dirtyWidth = Math.abs(dirtyWidth); } if (dirtyHeight < 0) { dirtyY = dirtyY+dirtyHeight; dirtyHeight = Math.abs(dirtyHeight); } if (dirtyX < 0) { dirtyWidth = dirtyWidth + dirtyX; dirtyX = 0; } if (dirtyY < 0) { dirtyHeight = dirtyHeight + dirtyY; dirtyY = 0; } if (dirtyWidth <= 0 || dirtyHeight <= 0) { return; } var img = CanvasKit.MakeImage(imageData.data, imageData.width, imageData.height, CanvasKit.AlphaType.Unpremul, CanvasKit.ColorType.RGBA_8888); var src = CanvasKit.XYWHRect(dirtyX, dirtyY, dirtyWidth, dirtyHeight); var dst = CanvasKit.XYWHRect(x+dirtyX, y+dirtyY, dirtyWidth, dirtyHeight); var inverted = CanvasKit.SkMatrix.invert(this._currentTransform); this._canvas.save(); // putImageData() operates in device space. this._canvas.concat(inverted); this._canvas.drawImageRect(img, src, dst, null, false); this._canvas.restore(); img.delete(); } this.quadraticCurveTo = function(cpx, cpy, x, y) { if (!allAreFinite(arguments)) { return; } if (this._currentPath.isEmpty()) { this._currentPath.moveTo(cpx, cpy); } this._currentPath.quadTo(cpx, cpy, x, y); } this.rect = function(x, y, width, height) { if (!allAreFinite(arguments)) { return; } // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-rect this._currentPath.addRect(x, y, x+width, y+height); } this.resetTransform = function() { // Apply the current transform to the path and then reset // to the identity. Essentially "commit" the transform. this._currentPath.transform(this._currentTransform); var inverted = CanvasKit.SkMatrix.invert(this._currentTransform); this._canvas.concat(inverted); // This should be identity, modulo floating point drift. this._currentTransform = this._canvas.getTotalMatrix(); } this.restore = function() { var newState = this._canvasStateStack.pop(); if (!newState) { return; } // "commit" the current transform. We pop, then apply the inverse of the // popped state, which has the effect of applying just the delta of // transforms between old and new. var combined = CanvasKit.SkMatrix.multiply( this._currentTransform, CanvasKit.SkMatrix.invert(newState.ctm) ); this._currentPath.transform(combined); this._lineDashList = newState.ldl; this._strokeWidth = newState.sw; this._paint.setStrokeWidth(this._strokeWidth); this._strokeStyle = newState.ss; this._fillStyle = newState.fs; this._paint.setStrokeCap(newState.cap); this._paint.setStrokeJoin(newState.jn); this._paint.setStrokeMiter(newState.mtr); this._shadowOffsetX = newState.sox; this._shadowOffsetY = newState.soy; this._shadowBlur = newState.sb; this._shadowColor = newState.shc; this._globalAlpha = newState.ga; this._globalCompositeOperation = newState.gco; this._paint.setBlendMode(this._globalCompositeOperation); this._lineDashOffset = newState.ldo; this._imageSmoothingEnabled = newState.ise; this._imageFilterQuality = newState.isq; //TODO: font, textAlign, textBaseline, direction // restores the clip and ctm this._canvas.restore(); this._currentTransform = this._canvas.getTotalMatrix(); } this.rotate = function(radians) { if (!isFinite(radians)) { return; } // retroactively apply the inverse of this transform to the previous // path so it cancels out when we apply the transform at draw time. var inverted = CanvasKit.SkMatrix.rotated(-radians); this._currentPath.transform(inverted); this._canvas.rotate(radiansToDegrees(radians), 0, 0); this._currentTransform = this._canvas.getTotalMatrix(); } this.save = function() { if (this._fillStyle._copy) { var fs = this._fillStyle._copy(); this._toCleanUp.push(fs); } else { var fs = this._fillStyle; } if (this._strokeStyle._copy) { var ss = this._strokeStyle._copy(); this._toCleanUp.push(ss); } else { var ss = this._strokeStyle; } this._canvasStateStack.push({ ctm: this._currentTransform.slice(), ldl: this._lineDashList.slice(), sw: this._strokeWidth, ss: ss, fs: fs, cap: this._paint.getStrokeCap(), jn: this._paint.getStrokeJoin(), mtr: this._paint.getStrokeMiter(), sox: this._shadowOffsetX, soy: this._shadowOffsetY, sb: this._shadowBlur, shc: this._shadowColor, ga: this._globalAlpha, ldo: this._lineDashOffset, gco: this._globalCompositeOperation, ise: this._imageSmoothingEnabled, isq: this._imageFilterQuality, //TODO: font, textAlign, textBaseline, direction }); // Saves the clip this._canvas.save(); } this.scale = function(sx, sy) { if (!allAreFinite(arguments)) { return; } // retroactively apply the inverse of this transform to the previous // path so it cancels out when we apply the transform at draw time. var inverted = CanvasKit.SkMatrix.scaled(1/sx, 1/sy); this._currentPath.transform(inverted); this._canvas.scale(sx, sy); this._currentTransform = this._canvas.getTotalMatrix(); } this.setLineDash = function(dashes) { for (var i = 0; i < dashes.length; i++) { if (!isFinite(dashes[i]) || dashes[i] < 0) { SkDebug('dash list must have positive, finite values'); return; } } if (dashes.length % 2 === 1) { // as per the spec, concatenate 2 copies of dashes // to give it an even number of elements. Array.prototype.push.apply(dashes, dashes); } this._lineDashList = dashes; } this.setTransform = function(a, b, c, d, e, f) { if (!(allAreFinite(arguments))) { return; } this.resetTransform(); this.transform(a, b, c, d, e, f); } // Returns the matrix representing the offset of the shadows. This unapplies // the effects of the scale, which should not affect the shadow offsets. this._shadowOffsetMatrix = function() { var sx = this._currentTransform[0]; var sy = this._currentTransform[4]; return CanvasKit.SkMatrix.translated(this._shadowOffsetX/sx, this._shadowOffsetY/sy); } // Returns the shadow paint for the current settings or null if there // should be no shadow. This ends up being a copy of the given // paint with a blur maskfilter and the correct color. this._shadowPaint = function(basePaint) { // multiply first to see if the alpha channel goes to 0 after multiplication. var alphaColor = CanvasKit.multiplyByAlpha(this._shadowColor, this._globalAlpha); // if alpha is zero, no shadows if (!CanvasKit.getColorComponents(alphaColor)[3]) { return null; } // one of these must also be non-zero (otherwise the shadow is // completely hidden. And the spec says so). if (!(this._shadowBlur || this._shadowOffsetY || this._shadowOffsetX)) { return null; } var shadowPaint = basePaint.copy(); shadowPaint.setColor(alphaColor); var blurEffect = CanvasKit.MakeBlurMaskFilter(CanvasKit.BlurStyle.Normal, SkBlurRadiusToSigma(this._shadowBlur), false); shadowPaint.setMaskFilter(blurEffect); // hack up a "destructor" which also cleans up the blurEffect. Otherwise, // we leak the blurEffect (since smart pointers don't help us in JS land). shadowPaint.dispose = function() { blurEffect.delete(); this.delete(); }; return shadowPaint; } // A helper to get a copy of the current paint, ready for stroking. // This applies the global alpha and the dashedness. // Call dispose() after to clean up. this._strokePaint = function() { var paint = this._paint.copy(); paint.setStyle(CanvasKit.PaintStyle.Stroke); if (Number.isInteger(this._strokeStyle)) { var alphaColor = CanvasKit.multiplyByAlpha(this._strokeStyle, this._globalAlpha); paint.setColor(alphaColor); } else { var shader = this._strokeStyle._getShader(this._currentTransform); paint.setColor(CanvasKit.Color(0,0,0, this._globalAlpha)); paint.setShader(shader); } paint.setStrokeWidth(this._strokeWidth); if (this._lineDashList.length) { var dashedEffect = CanvasKit.MakeSkDashPathEffect(this._lineDashList, this._lineDashOffset); paint.setPathEffect(dashedEffect); } paint.dispose = function() { dashedEffect && dashedEffect.delete(); this.delete(); } return paint; } this.stroke = function() { var strokePaint = this._strokePaint(); var shadowPaint = this._shadowPaint(strokePaint); if (shadowPaint) { this._canvas.save(); this._canvas.concat(this._shadowOffsetMatrix()); this._canvas.drawPath(this._currentPath, shadowPaint); this._canvas.restore(); shadowPaint.dispose(); } this._canvas.drawPath(this._currentPath, strokePaint); strokePaint.dispose(); } this.strokeRect = function(x, y, width, height) { var strokePaint = this._strokePaint(); this._canvas.drawRect(CanvasKit.XYWHRect(x, y, width, height), strokePaint); strokePaint.dispose(); } this.strokeText = function(text, x, y, maxWidth) { // TODO do something with maxWidth, probably involving measure var strokePaint = this._strokePaint(); var shadowPaint = this._shadowPaint(strokePaint); if (shadowPaint) { this._canvas.save(); this._canvas.concat(this._shadowOffsetMatrix()); this._canvas.drawText(text, x, y, shadowPaint); this._canvas.restore(); shadowPaint.dispose(); } this._canvas.drawText(text, x, y, strokePaint); strokePaint.dispose(); } this.translate = function(dx, dy) { if (!allAreFinite(arguments)) { return; } // retroactively apply the inverse of this transform to the previous // path so it cancels out when we apply the transform at draw time. var inverted = CanvasKit.SkMatrix.translated(-dx, -dy); this._currentPath.transform(inverted); this._canvas.translate(dx, dy); this._currentTransform = this._canvas.getTotalMatrix(); } this.transform = function(a, b, c, d, e, f) { var newTransform = [a, c, e, b, d, f, 0, 0, 1]; // retroactively apply the inverse of this transform to the previous // path so it cancels out when we apply the transform at draw time. var inverted = CanvasKit.SkMatrix.invert(newTransform); this._currentPath.transform(inverted); this._canvas.concat(newTransform); this._currentTransform = this._canvas.getTotalMatrix(); } // Not supported operations (e.g. for Web only) this.addHitRegion = function() {}; this.clearHitRegions = function() {}; this.drawFocusIfNeeded = function() {}; this.removeHitRegion = function() {}; this.scrollPathIntoView = function() {}; Object.defineProperty(this, 'canvas', { value: null, writable: false }); } function SkBlurRadiusToSigma(radius) { // Blink (Chrome) does the following, for legacy reasons, even though it // is against the spec. https://bugs.chromium.org/p/chromium/issues/detail?id=179006 // This may change in future releases. // This code is staying here in case any clients are interested in using it // to match Blink "exactly". // if (radius <= 0) // return 0; // return 0.288675 * radius + 0.5; // // This is what the spec says, which is how Firefox and others operate. return radius/2; }