skia2/experimental/canvaskit/htmlcanvas/canvas2dcontext.js
Kevin Lubick 53eabf6871 [canvaskit] Refactor Canvas2D JS into own files
Rather than one monolithic file, we now have one monolithic
file (canvascontext2d) and several smaller files (one per class,
and some helpers).

This should make the code navigation a little easier.

Bug: skia:
Change-Id: Ia191c2db778591af21d2a6126f053c17c4f677f1
Reviewed-on: https://skia-review.googlesource.com/c/175996
Reviewed-by: Florin Malita <fmalita@chromium.org>
2018-12-11 12:02:27 +00:00

1183 lines
37 KiB
JavaScript

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,
Math.max(1, this._shadowBlur/2), // very little blur when < 1
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
});
}