| // 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) { |
| |
| // See https://stackoverflow.com/a/31090240 |
| // This contraption keeps closure from minifying away the check |
| // if btoa is defined *and* prevents runtime "btoa" or "window" is not defined. |
| var isNode = !(new Function("try {return this===window;}catch(e){ return false;}")()); |
| |
| function argsAreFinite(args) { |
| for (var i = 0; i < args.length; i++) { |
| if (!Number.isFinite(args[0])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| function toBase64String(bytes) { |
| if (isNode) { |
| return Buffer.from(bytes).toString('base64'); |
| } else { |
| return btoa(String.fromCharCode.apply(null, bytes)); |
| } |
| } |
| |
| CanvasKit._testing = {}; |
| |
| function HTMLCanvas(skSurface) { |
| this._surface = skSurface; |
| this._context = new CanvasRenderingContext2D(skSurface.getCanvas()); |
| |
| // A normal <canvas> requires that clients call getContext |
| this.getContext = function(type) { |
| if (type === '2d') { |
| return this._context; |
| } |
| return null; |
| } |
| |
| this.toDataURL = function(codec) { |
| // TODO(kjlubick): maybe support other codecs? |
| // For now, just to png |
| this._surface.flush(); |
| |
| var img = this._surface.makeImageSnapshot(); |
| if (!img) { |
| SkDebug('no snapshot'); |
| return; |
| } |
| var png = img.encodeToData(); |
| if (!png) { |
| SkDebug('encoding failure'); |
| return |
| } |
| // TODO(kjlubick): clean this up a bit - maybe better naming? |
| var pngBytes = CanvasKit.getSkDataBytes(png); |
| return 'data:image/png;base64,' + toBase64String(pngBytes); |
| } |
| |
| this.dispose = function() { |
| this._context._dispose(); |
| this._surface.dispose(); |
| } |
| } |
| |
| function CanvasRenderingContext2D(skcanvas) { |
| this._canvas = skcanvas; |
| this._paint = new CanvasKit.SkPaint(); |
| this._paint.setAntiAlias(true); |
| this._paint.setStrokeWidth(2); |
| this._currentSubPath = null; |
| this._paths = []; |
| this._pathStarted = false; |
| |
| this._dispose = function() { |
| this._paths.forEach(function(path) { |
| path.delete(); |
| }); |
| this._paint.delete(); |
| // Don't delete this._canvas as it will be disposed |
| // by the surface of which it is based. |
| } |
| |
| Object.defineProperty(this, 'font', { |
| enumerable: true, |
| set: function(newStyle) { |
| var size = parseFontSize(newStyle); |
| // TODO styles |
| this._paint.setTextSize(size); |
| } |
| }); |
| |
| Object.defineProperty(this, 'lineWidth', { |
| enumerable: true, |
| set: function(newWidth) { |
| if (newWidth <= 0 || !newWidth) { |
| // Spec says to ignore NaN/Inf/0/negative values |
| return; |
| } |
| this._paint.setStrokeWidth(newWidth); |
| } |
| }); |
| |
| Object.defineProperty(this, 'strokeStyle', { |
| enumerable: true, |
| set: function(newStyle) { |
| this._paint.setColor(parseColor(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 (!argsAreFinite(arguments)) { |
| return; |
| } |
| this._ensureSubpath(x1, y1); |
| if (radius < 0) { |
| throw 'radii cannot be negative'; |
| } |
| this._currentSubPath.arcTo(x1, y1, x2, y2, radius); |
| } |
| |
| this.beginPath = function() { |
| this._currentSubPath = new CanvasKit.SkPath(); |
| this._paths.push(this._currentSubPath); |
| this._pathStarted = false; |
| } |
| |
| this.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { |
| if (!argsAreFinite(arguments)) { |
| return; |
| } |
| this._ensureSubpath(cp1x, cp1y); |
| this._currentSubPath.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y); |
| } |
| |
| this.closePath = function() { |
| if (this._currentSubPath) { |
| this._currentSubPath.close(); |
| this._currentSubPath = null; |
| this._pathStarted = false; |
| } |
| } |
| |
| this.ellipse = function(x, y, radiusX, radiusY, rotation, |
| startAngle, endAngle, ccw) { |
| if (!argsAreFinite(arguments)) { |
| return; |
| } |
| if (radiusX < 0 || radiusY < 0) { |
| throw 'radii cannot be negative'; |
| } |
| this._pathStarted = true; |
| |
| var bounds = CanvasKit.LTRBRect(x-radiusX, y-radiusY, x+radiusX, y+radiusY); |
| var sweep = radiansToDegrees(endAngle - startAngle) - (360 * !!ccw); |
| var temp = new CanvasKit.SkPath(); |
| temp.addArc(bounds, radiansToDegrees(startAngle), sweep); |
| var m = CanvasKit.SkMatrix.rotated(radiansToDegrees(rotation), x, y); |
| this._currentSubPath.addPath(temp, m, true); |
| temp.delete(); |
| } |
| |
| // ensureSubpath makes SkPath behave like the browser's path object |
| // in that the first lineTo really acts like a moveTo. |
| // ensureSubpath is the term used in the canvas spec: |
| // https://html.spec.whatwg.org/multipage/canvas.html#ensure-there-is-a-subpath |
| // ensureSubpath returns true if the drawing command can proceed, |
| // false otherwise (i.e. it was the first command and may be replaced |
| // with a moveTo). |
| this._ensureSubpath = function(x, y) { |
| if (!this._currentSubPath) { |
| this.beginPath(); |
| } |
| if (!this._pathStarted) { |
| this._pathStarted = true; |
| this.moveTo(x, y); |
| return false; |
| } |
| return true; |
| } |
| |
| this.fillText = function(text, x, y, maxWidth) { |
| // TODO do something with maxWidth, probably involving measure |
| this._canvas.drawText(text, x, y, this._paint); |
| } |
| |
| this.lineTo = function(x, y) { |
| if (!argsAreFinite(arguments)) { |
| return; |
| } |
| // lineTo is the odd-ball in the sense that a line-to without |
| // a previous subpath is the same as a moveTo. |
| if (this._ensureSubpath(x, y)) { |
| this._currentSubPath.lineTo(x, y); |
| } |
| } |
| |
| this.measureText = function(text) { |
| return { |
| width: this._paint.measureText(text), |
| // TODO other measurements? |
| } |
| } |
| |
| this.moveTo = function(x, y) { |
| if (!argsAreFinite(arguments)) { |
| return; |
| } |
| if (this._ensureSubpath(x, y)) { |
| this._currentSubPath.moveTo(x, y); |
| } |
| } |
| |
| this.quadraticCurveTo = function(cpx, cpy, x, y) { |
| if (!argsAreFinite(arguments)) { |
| return; |
| } |
| this._ensureSubpath(cpx, cpy); |
| this._currentSubPath.quadTo(cpx, cpy, x, y); |
| } |
| |
| this.rect = function(x, y, width, height) { |
| if (!argsAreFinite(arguments)) { |
| return; |
| } |
| // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-rect |
| this.beginPath(); |
| this._currentSubPath.addRect(x, y, x+width, y+height); |
| this.beginPath(); |
| this._currentSubPath.moveTo(x, y); |
| } |
| |
| this.resetTransform = function() { |
| this.setTransform(1, 0, 0, 1, 0, 0); |
| } |
| |
| this.rotate = function(radians, px, py) { |
| // bindings can't turn undefined into floats |
| this._canvas.rotate(radians * 180/Math.PI, px || 0, py || 0); |
| } |
| |
| this.scale = function(sx, sy) { |
| this._canvas.scale(sx, sy); |
| } |
| |
| this.scale = function(sx, sy) { |
| this._canvas.scale(sx, sy); |
| } |
| |
| this.setTransform = function(a, b, c, d, e, f) { |
| this._canvas.setMatrix([a, c, e, |
| b, d, f, |
| 0, 0, 1]); |
| } |
| |
| this.skew = function(sx, sy) { |
| this._canvas.skew(sx, sy); |
| } |
| |
| this.stroke = function() { |
| if (this._currentSubPath) { |
| this._paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| this._paint.setLine |
| for (var i = 0; i < this._paths.length; i++) { |
| this._canvas.drawPath(this._paths[i], this._paint); |
| } |
| } |
| } |
| |
| this.strokeText = function(text, x, y, maxWidth) { |
| // TODO do something with maxWidth, probably involving measure |
| this._paint.setStyle(CanvasKit.PaintStyle.Stroke); |
| this._canvas.drawText(text, x, y, this._paint); |
| } |
| |
| this.translate = function(dx, dy) { |
| this._canvas.translate(dx, dy); |
| } |
| |
| |
| // 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 = 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 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; |
| |
| // 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"; |