| // Adds compile-time JS functions to augment the CanvasKit interface. |
| // Specifically, the code that emulates the HTML Canvas interface |
| // (which may be called HTMLCanvas or similar to avoid confusion with |
| // SkCanvas). |
| (function(CanvasKit) { |
| |
| function allAreFinite(args) { |
| for (var i = 0; i < args.length; i++) { |
| if (args[i] !== undefined && !Number.isFinite(args[i])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| function toBase64String(bytes) { |
| if (isNode) { |
| return Buffer.from(bytes).toString('base64'); |
| } else { |
| // From https://stackoverflow.com/a/25644409 |
| // because the naive solution of |
| // btoa(String.fromCharCode.apply(null, bytes)); |
| // would occasionally throw "Maximum call stack size exceeded" |
| var CHUNK_SIZE = 0x8000; //arbitrary number |
| var index = 0; |
| var length = bytes.length; |
| var result = ''; |
| var slice; |
| while (index < length) { |
| slice = bytes.slice(index, Math.min(index + CHUNK_SIZE, length)); |
| result += String.fromCharCode.apply(null, slice); |
| index += CHUNK_SIZE; |
| } |
| return btoa(result); |
| } |
| } |
| |
| CanvasKit._testing = {}; |
| |
| function HTMLCanvas(skSurface) { |
| this._surface = skSurface; |
| this._context = new CanvasRenderingContext2D(skSurface.getCanvas()); |
| this._imgs = []; |
| |
| // Data is either an ArrayBuffer, a TypedArray, or a Node Buffer |
| this.decodeImage = function(data) { |
| var img = CanvasKit.MakeImageFromEncoded(data); |
| if (!img) { |
| throw 'Invalid input'; |
| } |
| this._imgs.push(img); |
| return img; |
| } |
| |
| // A normal <canvas> requires that clients call getContext |
| this.getContext = function(type) { |
| if (type === '2d') { |
| return this._context; |
| } |
| return null; |
| } |
| |
| this.toDataURL = function(codec, quality) { |
| // TODO(kjlubick): maybe support other codecs (webp?) |
| // For now, just to png and jpeg |
| this._surface.flush(); |
| |
| var img = this._surface.makeImageSnapshot(); |
| if (!img) { |
| SkDebug('no snapshot'); |
| return; |
| } |
| var codec = codec || 'image/png'; |
| var format = CanvasKit.ImageFormat.PNG; |
| if (codec === 'image/jpeg') { |
| format = CanvasKit.ImageFormat.JPEG; |
| } |
| var quality = quality || 0.92; |
| var skimg = img.encodeToData(format, quality); |
| if (!skimg) { |
| SkDebug('encoding failure'); |
| return |
| } |
| var imgBytes = CanvasKit.getSkDataBytes(skimg); |
| return 'data:' + codec + ';base64,' + toBase64String(imgBytes); |
| } |
| |
| this.dispose = function() { |
| this._context._dispose(); |
| this._imgs.forEach(function(i) { |
| i.delete(); |
| }); |
| this._surface.dispose(); |
| } |
| } |
| |
| function ImageData(arr, width, height) { |
| if (!width || height === 0) { |
| throw 'invalid dimensions, width and height must be non-zero'; |
| } |
| if (arr.length % 4) { |
| throw 'arr must be a multiple of 4'; |
| } |
| height = height || arr.length/(4*width); |
| |
| Object.defineProperty(this, 'data', { |
| value: arr, |
| writable: false |
| }); |
| Object.defineProperty(this, 'height', { |
| value: height, |
| writable: false |
| }); |
| Object.defineProperty(this, 'width', { |
| value: width, |
| writable: false |
| }); |
| } |
| |
| CanvasKit.ImageData = function() { |
| 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 if (arguments.length === 3) { |
| var arr = arguments[0]; |
| if (arr.prototype.constructor !== Uint8ClampedArray ) { |
| throw 'bytes must be given as a Uint8ClampedArray'; |
| } |
| var width = arguments[1]; |
| var height = arguments[2]; |
| if (arr % 4) { |
| throw 'bytes must be given in a multiple of 4'; |
| } |
| if (arr % width) { |
| throw 'bytes must divide evenly by width'; |
| } |
| if (height && (height !== (arr / (width * 4)))) { |
| throw 'invalid height given'; |
| } |
| height = arr / (width * 4); |
| return new ImageData(arr, width, height); |
| } else { |
| throw 'invalid number of arguments - takes 2 or 3, saw ' + arguments.length; |
| } |
| } |
| |
| function LinearCanvasGradient(x1, y1, x2, y2) { |
| this._shader = null; |
| this._colors = []; |
| this._pos = []; |
| |
| this.addColorStop = function(offset, color) { |
| if (offset < 0 || offset > 1 || !isFinite(offset)) { |
| throw 'offset must be between 0 and 1 inclusively'; |
| } |
| |
| color = parseColor(color); |
| // From the spec: If multiple stops are added at the same offset on a |
| // gradient, then they must be placed in the order added, with the first |
| // one closest to the start of the gradient, and each subsequent one |
| // infinitesimally further along towards the end point (in effect |
| // causing all but the first and last stop added at each point to be |
| // ignored). |
| // To implement that, if an offset is already in the list, |
| // we just overwrite its color (since the user can't remove Color stops |
| // after the fact). |
| var idx = this._pos.indexOf(offset); |
| if (idx !== -1) { |
| this._colors[idx] = color; |
| } else { |
| // insert it in sorted order |
| for (idx = 0; idx < this._pos.length; idx++) { |
| if (this._pos[idx] > offset) { |
| break; |
| } |
| } |
| this._pos .splice(idx, 0, offset); |
| this._colors.splice(idx, 0, color); |
| } |
| } |
| |
| this._copy = function() { |
| var lcg = new LinearCanvasGradient(x1, y1, x2, y2); |
| lcg._colors = this._colors.slice(); |
| lcg._pos = this._pos.slice(); |
| return lcg; |
| } |
| |
| this._dispose = function() { |
| if (this._shader) { |
| this._shader.delete(); |
| this._shader = null; |
| } |
| } |
| |
| this._getShader = function(currentTransform, globalAlpha) { |
| // From the spec: "The points in the linear gradient must be transformed |
| // as described by the current transformation matrix when rendering." |
| var pts = [x1, y1, x2, y2]; |
| CanvasKit.SkMatrix.mapPoints(currentTransform, pts); |
| var sx1 = pts[0]; |
| var sy1 = pts[1]; |
| var sx2 = pts[2]; |
| var sy2 = pts[3]; |
| |
| this._dispose(); |
| var colors = this._colors.map(function(c) { |
| return CanvasKit.multiplyByAlpha(c, globalAlpha); |
| }); |
| this._shader = CanvasKit.MakeLinearGradientShader([sx1, sy1], [sx2, sy2], |
| colors, this._pos, CanvasKit.TileMode.Clamp); |
| return this._shader; |
| } |
| } |
| |
| // Note, Skia has a different notion of a "radial" gradient. |
| // Skia has a twoPointConical gradient that is the same as the |
| // canvas's RadialGradient. |
| function RadialCanvasGradient(x1, y1, r1, x2, y2, r2) { |
| this._shader = null; |
| this._colors = []; |
| this._pos = []; |
| |
| this.addColorStop = function(offset, color) { |
| if (offset < 0 || offset > 1 || !isFinite(offset)) { |
| throw 'offset must be between 0 and 1 inclusively'; |
| } |
| |
| color = parseColor(color); |
| // From the spec: If multiple stops are added at the same offset on a |
| // gradient, then they must be placed in the order added, with the first |
| // one closest to the start of the gradient, and each subsequent one |
| // infinitesimally further along towards the end point (in effect |
| // causing all but the first and last stop added at each point to be |
| // ignored). |
| // To implement that, if an offset is already in the list, |
| // we just overwrite its color (since the user can't remove Color stops |
| // after the fact). |
| var idx = this._pos.indexOf(offset); |
| if (idx !== -1) { |
| this._colors[idx] = color; |
| } else { |
| // insert it in sorted order |
| for (idx = 0; idx < this._pos.length; idx++) { |
| if (this._pos[idx] > offset) { |
| break; |
| } |
| } |
| this._pos .splice(idx, 0, offset); |
| this._colors.splice(idx, 0, color); |
| } |
| } |
| |
| this._copy = function() { |
| var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2); |
| rcg._colors = this._colors.slice(); |
| rcg._pos = this._pos.slice(); |
| return rcg; |
| } |
| |
| this._dispose = function() { |
| if (this._shader) { |
| this._shader.delete(); |
| this._shader = null; |
| } |
| } |
| |
| this._getShader = function(currentTransform, globalAlpha) { |
| // From the spec: "The points in the linear gradient must be transformed |
| // as described by the current transformation matrix when rendering." |
| var pts = [x1, y1, x2, y2]; |
| CanvasKit.SkMatrix.mapPoints(currentTransform, pts); |
| var sx1 = pts[0]; |
| var sy1 = pts[1]; |
| var sx2 = pts[2]; |
| var sy2 = pts[3]; |
| |
| // Maybe refactor _scalefactor() on which this is taken? |
| var sx = currentTransform[0]; |
| var sy = currentTransform[4]; |
| var scaleFactor = (Math.abs(sx) + Math.abs(sy))/2; |
| |
| var sr1 = r1 * scaleFactor; |
| var sr2 = r2 * scaleFactor; |
| |
| this._dispose(); |
| var colors = this._colors.map(function(c) { |
| return CanvasKit.multiplyByAlpha(c, globalAlpha); |
| }); |
| this._shader = CanvasKit.MakeTwoPointConicalGradientShader( |
| [sx1, sy1], sr1, [sx2, sy2], sr2, colors, this._pos, |
| CanvasKit.TileMode.Clamp); |
| return this._shader; |
| } |
| } |
| |
| 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._currentSubpath = null; |
| this._currentTransform = CanvasKit.SkMatrix.identity(); |
| |
| // Use this for save/restore |
| this._canvasStateStack = []; |
| // Keep a reference to all the gradients that were allocated |
| // for cleanup in _dispose; |
| this._gradients = []; |
| |
| this._dispose = function() { |
| this._currentPath.delete(); |
| this._currentSubpath && this._currentSubpath.delete(); |
| this._paint.delete(); |
| this._gradients.forEach(function(gradient) { |
| gradient._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], |
| }; |
| }, |
| set: function(matrix) { |
| if (matrix.a) { |
| // if we see a property named 'a', guess that b-f will |
| // also be there. |
| this._currentTransform = [matrix.a, matrix.c, matrix.e, |
| matrix.b, matrix.d, matrix.f, |
| 0, 0, 1]; |
| } |
| } |
| }); |
| |
| 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.addColorStop) { |
| // It's probably a gradient. |
| 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.addColorStop) { |
| // It's probably a gradient. |
| 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._currentSubpath) { |
| this._newSubpath(x1, y1); |
| } |
| this._currentSubpath.arcTo(x1, y1, x2, y2, radius); |
| } |
| |
| // As per the spec this doesn't begin any paths, it only |
| // clears out any previous subpaths. |
| this.beginPath = function() { |
| this._currentPath.delete(); |
| this._currentPath = new CanvasKit.SkPath(); |
| this._currentSubpath && this._currentSubpath.delete(); |
| this._currentSubpath = null; |
| } |
| |
| this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { |
| if (!allAreFinite(arguments)) { |
| return; |
| } |
| if (!this._currentSubpath) { |
| this._newSubpath(cp1x, cp1y); |
| } |
| this._currentSubpath.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) { |
| this._commitSubpath(); |
| 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._currentSubpath) { |
| this._currentSubpath.close(); |
| var lastPt = this._currentSubpath.getPoint(0); |
| this._newSubpath(lastPt[0], lastPt[1]); |
| } |
| } |
| |
| 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._gradients.push(lcg); |
| return lcg; |
| } |
| |
| this.createRadialGradient = function(x1, y1, r1, x2, y2, r2) { |
| if (!allAreFinite(arguments)) { |
| return; |
| } |
| var rcg = new RadialCanvasGradient(x1, y1, r1, x2, y2, r2); |
| this._gradients.push(rcg); |
| return rcg; |
| } |
| |
| this._commitSubpath = function() { |
| if (this._currentSubpath) { |
| this._currentPath.addPath(this._currentSubpath, false); |
| this._currentSubpath.delete(); |
| this._currentSubpath = null; |
| } |
| } |
| |
| 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.ellipse = function(x, y, radiusX, radiusY, rotation, |
| startAngle, endAngle, ccw) { |
| if (!allAreFinite(arguments)) { |
| return; |
| } |
| if (radiusX < 0 || radiusY < 0) { |
| throw 'radii cannot be negative'; |
| } |
| |
| if (!this._currentSubpath) { |
| // Don't use newSubpath here because calculating the starting |
| // point in the arc is non-trivial. Just make a new, empty |
| // subpath to append to. |
| this._currentSubpath = new CanvasKit.SkPath(); |
| } |
| var bounds = CanvasKit.LTRBRect(x-radiusX, y-radiusY, x+radiusX, y+radiusY); |
| var sweep = radiansToDegrees(endAngle - startAngle) - (360 * !!ccw); |
| var temp = new CanvasKit.SkPath(); |
| // Skia takes degrees. JS tends to be radians. |
| temp.addArc(bounds, radiansToDegrees(startAngle), sweep); |
| var m = CanvasKit.SkMatrix.rotated(rotation, x, y); |
| this._currentSubpath.addPath(temp, m, true); |
| temp.delete(); |
| } |
| |
| // 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 gradient = this._fillStyle._getShader(this._currentTransform, this._globalAlpha); |
| paint.setShader(gradient); |
| } |
| |
| 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() { |
| this._commitSubpath(); |
| 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.lineTo = function(x, y) { |
| if (!allAreFinite(arguments)) { |
| return; |
| } |
| // A lineTo without a previous subpath is turned into a moveTo |
| if (!this._currentSubpath) { |
| this._newSubpath(x, y); |
| } else { |
| this._currentSubpath.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._newSubpath(x, y); |
| } |
| |
| this._newSubpath = function(x, y) { |
| this._commitSubpath(); |
| this._currentSubpath = new CanvasKit.SkPath(); |
| this._currentSubpath.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._currentSubpath) { |
| this._newSubpath(cpx, cpy); |
| } |
| this._currentSubpath.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._newSubpath(x, y); |
| this._currentSubpath.addRect(x, y, x+width, y+height); |
| this._newSubpath(x, y); |
| } |
| |
| 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); |
| this._currentSubpath && this._currentSubpath.transform(this._currentTransform); |
| var inverted = CanvasKit.SkMatrix.invert(this._currentTransform); |
| this._canvas.concat(inverted); |
| this._currentTransform = CanvasKit.SkMatrix.identity(); |
| } |
| |
| 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._currentSubpath && this._currentSubpath.transform(combined); |
| |
| this._currentTransform = newState.ctm; |
| 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 |
| this._canvas.restore(); |
| } |
| |
| 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._currentSubpath && this._currentSubpath.transform(inverted); |
| this._currentTransform = CanvasKit.SkMatrix.multiply( |
| this._currentTransform, |
| CanvasKit.SkMatrix.rotated(radians)); |
| this._canvas.rotate(radiansToDegrees(radians), 0, 0); |
| } |
| |
| this.save = function() { |
| if (this._fillStyle._copy) { |
| var fs = this._fillStyle._copy(); |
| this._gradients.push(fs); |
| } else { |
| var fs = this._fillStyle; |
| } |
| |
| if (this._strokeStyle._copy) { |
| var ss = this._strokeStyle._copy(); |
| this._gradients.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._currentSubpath && this._currentSubpath.transform(inverted); |
| this._currentTransform = CanvasKit.SkMatrix.multiply( |
| this._currentTransform, |
| CanvasKit.SkMatrix.scaled(sx, sy)); |
| this._canvas.scale(sx, sy); |
| } |
| |
| 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 gradient = this._strokeStyle._getShader(this._currentTransform, this._globalAlpha); |
| paint.setShader(gradient); |
| } |
| |
| 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() { |
| this._commitSubpath(); |
| 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._currentSubpath && this._currentSubpath.transform(inverted); |
| this._currentTransform = CanvasKit.SkMatrix.multiply( |
| this._currentTransform, |
| CanvasKit.SkMatrix.translated(dx, dy)); |
| this._canvas.translate(dx, dy); |
| } |
| |
| 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._currentSubpath && this._currentSubpath.transform(inverted); |
| this._canvas.concat(newTransform); |
| this._currentTransform = CanvasKit.SkMatrix.multiply( |
| this._currentTransform, |
| newTransform); |
| } |
| |
| // 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 |
| }); |
| } |
| |
| CanvasKit.MakeCanvas = function(width, height) { |
| // TODO(kjlubick) do fonts the "correct" way |
| CanvasKit.initFonts(); |
| var surf = CanvasKit.MakeSurface(width, height); |
| if (surf) { |
| return new HTMLCanvas(surf); |
| } |
| return null; |
| } |
| |
| var units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q'; |
| var fontSizeRegex = new RegExp('([\\d\\.]+)(' + units + ')'); |
| var defaultHeight = 12; |
| // Based off of node-canvas's parseFont |
| // returns font size in *points* (original impl was in px); |
| function parseFontSize(fontStr) { |
| // This is naive and doesn't account for line-height yet |
| // (but neither does node-canvas's?) |
| var fontSize = fontSizeRegex.exec(fontStr); |
| if (!fontSize) { |
| SkDebug('Could not parse font size' + fontStr); |
| return 16; |
| } |
| var size = parseFloat(fontSize[1]); |
| var unit = fontSize[2]; |
| switch (unit) { |
| case 'pt': |
| return size; |
| case 'px': |
| return size * 3/4; |
| case 'pc': |
| return size * 12; |
| case 'in': |
| return size * 72; |
| case 'cm': |
| return size * 72.0 / 2.54; |
| case 'mm': |
| return size * (72.0 / 25.4); |
| case '%': |
| return size * (defaultHeight / 100); |
| case 'em': |
| case 'rem': |
| return size * defaultHeight; |
| case 'q': |
| return size * (96 / 25.4 / 3); |
| } |
| } |
| |
| function colorToString(skcolor) { |
| // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color |
| var components = CanvasKit.getColorComponents(skcolor); |
| var r = components[0]; |
| var g = components[1]; |
| var b = components[2]; |
| var a = components[3]; |
| if (a === 1.0) { |
| // hex |
| r = r.toString(16).toLowerCase(); |
| g = g.toString(16).toLowerCase(); |
| b = b.toString(16).toLowerCase(); |
| r = (r.length === 1 ? '0'+r: r); |
| g = (g.length === 1 ? '0'+g: g); |
| b = (b.length === 1 ? '0'+b: b); |
| return '#'+r+g+b; |
| } else { |
| a = (a === 0 || a === 1) ? a : a.toFixed(8); |
| return 'rgba('+r+', '+g+', '+b+', '+a+')'; |
| } |
| } |
| |
| function valueOrPercent(aStr) { |
| var a = parseFloat(aStr) || 1; |
| if (aStr && aStr.indexOf('%') !== -1) { |
| return a / 100; |
| } |
| return a; |
| } |
| |
| function parseColor(colorStr) { |
| colorStr = colorStr.toLowerCase(); |
| // See https://drafts.csswg.org/css-color/#typedef-hex-color |
| if (colorStr.startsWith('#')) { |
| var r, g, b, a = 255; |
| switch (colorStr.length) { |
| case 9: // 8 hex chars #RRGGBBAA |
| a = parseInt(colorStr.slice(7, 9), 16); |
| case 7: // 6 hex chars #RRGGBB |
| r = parseInt(colorStr.slice(1, 3), 16); |
| g = parseInt(colorStr.slice(3, 5), 16); |
| b = parseInt(colorStr.slice(5, 7), 16); |
| break; |
| case 5: // 4 hex chars #RGBA |
| // multiplying by 17 is the same effect as |
| // appending another character of the same value |
| // e.g. e => ee == 14 => 238 |
| a = parseInt(colorStr.slice(4, 5), 16) * 17; |
| case 4: // 6 hex chars #RGB |
| r = parseInt(colorStr.slice(1, 2), 16) * 17; |
| g = parseInt(colorStr.slice(2, 3), 16) * 17; |
| b = parseInt(colorStr.slice(3, 4), 16) * 17; |
| break; |
| } |
| return CanvasKit.Color(r, g, b, a/255); |
| |
| } else if (colorStr.startsWith('rgba')) { |
| // Trim off rgba( and the closing ) |
| colorStr = colorStr.slice(5, -1); |
| var nums = colorStr.split(','); |
| return CanvasKit.Color(+nums[0], +nums[1], +nums[2], |
| valueOrPercent(nums[3])); |
| } else if (colorStr.startsWith('rgb')) { |
| // Trim off rgba( and the closing ) |
| colorStr = colorStr.slice(4, -1); |
| var nums = colorStr.split(','); |
| // rgb can take 3 or 4 arguments |
| return CanvasKit.Color(+nums[0], +nums[1], +nums[2], |
| valueOrPercent(nums[3])); |
| } else if (colorStr.startsWith('gray(')) { |
| // TODO |
| } else if (colorStr.startsWith('hsl')) { |
| // TODO |
| } else { |
| // Try for named color |
| var nc = colorMap[colorStr]; |
| if (nc !== undefined) { |
| return nc; |
| } |
| } |
| SkDebug('unrecognized color ' + colorStr); |
| return CanvasKit.BLACK; |
| } |
| |
| CanvasKit._testing['parseColor'] = parseColor; |
| CanvasKit._testing['colorToString'] = colorToString; |
| |
| // Create the following with |
| // node ./htmlcanvas/_namedcolors.js --expose-wasm |
| // JS/closure doesn't have a constexpr like thing which |
| // would really help here. Since we don't, we pre-compute |
| // the map, which saves (a tiny amount of) startup time |
| // and (a small amount of) code size. |
| /* @dict */ |
| var colorMap = {"aliceblue":-984833,"antiquewhite":-332841,"aqua":-16711681,"aquamarine":-8388652,"azure":-983041,"beige":-657956,"bisque":-6972,"black":-16777216,"blanchedalmond":-5171,"blue":-16776961,"blueviolet":-7722014,"brown":-5952982,"burlywood":-2180985,"cadetblue":-10510688,"chartreuse":-8388864,"chocolate":-2987746,"coral":-32944,"cornflowerblue":-10185235,"cornsilk":-1828,"crimson":-2354116,"cyan":-16711681,"darkblue":-16777077,"darkcyan":-16741493,"darkgoldenrod":-4684277,"darkgray":-5658199,"darkgreen":-16751616,"darkgrey":-5658199,"darkkhaki":-4343957,"darkmagenta":-7667573,"darkolivegreen":-11179217,"darkorange":-29696,"darkorchid":-6737204,"darkred":-7667712,"darksalmon":-1468806,"darkseagreen":-7357297,"darkslateblue":-12042869,"darkslategray":-13676721,"darkslategrey":-13676721,"darkturquoise":-16724271,"darkviolet":-7077677,"deeppink":-60269,"deepskyblue":-16728065,"dimgray":-9868951,"dimgrey":-9868951,"dodgerblue":-14774017,"firebrick":-5103070,"floralwhite":-1296,"forestgreen":-14513374,"fuchsia":-65281,"gainsboro":-2302756,"ghostwhite":-460545,"gold":-10496,"goldenrod":-2448096,"gray":-8355712,"green":-16744448,"greenyellow":-5374161,"grey":-8355712,"honeydew":-983056,"hotpink":-38476,"indianred":-3318692,"indigo":-11861886,"ivory":-16,"khaki":-989556,"lavender":-1644806,"lavenderblush":-3851,"lawngreen":-8586240,"lemonchiffon":-1331,"lightblue":-5383962,"lightcoral":-1015680,"lightcyan":-2031617,"lightgoldenrodyellow":-329006,"lightgray":-2894893,"lightgreen":-7278960,"lightgrey":-2894893,"lightpink":-18751,"lightsalmon":-24454,"lightseagreen":-14634326,"lightskyblue":-7876870,"lightslategray":-8943463,"lightslategrey":-8943463,"lightsteelblue":-5192482,"lightyellow":-32,"lime":-16711936,"limegreen":-13447886,"linen":-331546,"magenta":-65281,"maroon":-8388608,"mediumaquamarine":-10039894,"mediumblue":-16777011,"mediumorchid":-4565549,"mediumpurple":-7114533,"mediumseagreen":-12799119,"mediumslateblue":-8689426,"mediumspringgreen":-16713062,"mediumturquoise":-12004916,"mediumvioletred":-3730043,"midnightblue":-15132304,"mintcream":-655366,"mistyrose":-6943,"moccasin":-6987,"navajowhite":-8531,"navy":-16777088,"oldlace":-133658,"olive":-8355840,"olivedrab":-9728477,"orange":-23296,"orangered":-47872,"orchid":-2461482,"palegoldenrod":-1120086,"palegreen":-6751336,"paleturquoise":-5247250,"palevioletred":-2396013,"papayawhip":-4139,"peachpuff":-9543,"peru":-3308225,"pink":-16181,"plum":-2252579,"powderblue":-5185306,"purple":-8388480,"rebeccapurple":-10079335,"red":-65536,"rosybrown":-4419697,"royalblue":-12490271,"saddlebrown":-7650029,"salmon":-360334,"sandybrown":-744352,"seagreen":-13726889,"seashell":-2578,"sienna":-6270419,"silver":-4144960,"skyblue":-7876885,"slateblue":-9807155,"slategray":-9404272,"slategrey":-9404272,"snow":-1286,"springgreen":-16711809,"steelblue":-12156236,"tan":-2968436,"teal":-16744320,"thistle":-2572328,"transparent":0,"tomato":-40121,"turquoise":-12525360,"violet":-1146130,"wheat":-663885,"white":-1,"whitesmoke":-657931,"yellow":-256,"yellowgreen":-6632142}; |
| |
| }(Module)); // When this file is loaded in, the high level object is "Module"; |