/** * mxJsCanvas * * Open Issues: * * - Canvas has no built-in dash-pattern for strokes * - Use AS code for straight lines * - Must use proxy for cross domain images * - Use html2canvas for HTML rendering (Replaces complete page with * canvas currently, needs API call to render elt to canvas) */ /** * Extends mxAbstractCanvas2D */ function mxJsCanvas(canvas) { mxAbstractCanvas2D.call(this); this.ctx = canvas.getContext('2d'); this.ctx.textBaseline = 'top'; this.ctx.fillStyle = 'rgba(255,255,255,0)'; this.ctx.strokeStyle = 'rgba(0, 0, 0, 0)'; //this.ctx.translate(0.5, 0.5); this.M_RAD_PER_DEG = Math.PI / 180; this.images = this.images == null ? [] : this.images; this.subCanvas = this.subCanvas == null ? [] : this.subCanvas; }; /** * Extends mxAbstractCanvas2D */ mxUtils.extend(mxJsCanvas, mxAbstractCanvas2D); /** * Variable: ctx * * Holds the current canvas context */ mxJsCanvas.prototype.ctx = null; /** * Variable: ctx * * Holds the current canvas context */ mxJsCanvas.prototype.waitCounter = 0; /** * Variable: ctx * * Holds the current canvas context */ mxJsCanvas.prototype.onComplete = null; /** * Variable: images * * Ordered array of images used in this canvas */ mxJsCanvas.prototype.images = null; /** * Variable: subCanvas * * Ordered array of sub canvas elements in this canvas */ mxJsCanvas.prototype.subCanvas = null; /** * Variable: canvasIndex * * The current index into the canvas sub-canvas array being processed */ mxJsCanvas.prototype.canvasIndex = 0; mxJsCanvas.prototype.hexToRgb = function(hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function(m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }; mxJsCanvas.prototype.incWaitCounter = function() { this.waitCounter++; }; mxJsCanvas.prototype.decWaitCounter = function() { this.waitCounter--; if (this.waitCounter == 0 && this.onComplete != null) { this.onComplete(); this.onComplete = null; } }; mxJsCanvas.prototype.updateFont = function() { var style = ''; if ((this.state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) { style += 'bold '; } if ((this.state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) { style += 'italic '; } this.ctx.font = style + this.state.fontSize + 'px ' + this.state.fontFamily; }; mxJsCanvas.prototype.save = function() { this.states.push(this.state); this.state = mxUtils.clone(this.state); this.ctx.save(); }; mxJsCanvas.prototype.restore = function() { this.state = this.states.pop(); this.ctx.restore(); }; mxJsCanvas.prototype.scale = function(s) { this.state.scale *= s; this.state.strokeWidth *= s; this.ctx.scale(s, s); }; mxJsCanvas.prototype.translate = function(dx, dy) { this.state.dx += dx; this.state.dy += dy; this.ctx.translate(dx, dy); }; mxJsCanvas.prototype.rotate = function(theta, flipH, flipV, cx, cy) { // This is a special case where the rotation center is scaled so dx/dy, // which are also scaled, must be applied after scaling the center. cx -= this.state.dx; cy -= this.state.dy; this.ctx.translate(cx, cy); if (flipH || flipV) { var sx = (flipH) ? -1 : 1; var sy = (flipV) ? -1 : 1; this.ctx.scale(sx, sy); } this.ctx.rotate(theta * this.M_RAD_PER_DEG); this.ctx.translate(-cx, -cy); }; mxJsCanvas.prototype.setAlpha = function(alpha) { this.state.alpha = alpha; this.ctx.globalAlpha = alpha; }; /** * Function: setFillColor * * Sets the current fill color. */ mxJsCanvas.prototype.setFillColor = function(value) { if (value == mxConstants.NONE) { value = null; } this.state.fillColor = value; this.state.gradientColor = null; this.ctx.fillStyle = value; }; mxJsCanvas.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2) { var gradient = this.ctx.createLinearGradient(0, y, 0, y + h); var s = this.state; s.fillColor = color1; s.fillAlpha = (alpha1 != null) ? alpha1 : 1; s.gradientColor = color2; s.gradientAlpha = (alpha2 != null) ? alpha2 : 1; s.gradientDirection = direction; var rgb1 = this.hexToRgb(color1); var rgb2 = this.hexToRgb(color2); if (rgb1 != null) { gradient.addColorStop(0, 'rgba(' + rgb1.r + ',' + rgb1.g + ',' + rgb1.b + ',' + s.fillAlpha + ')'); } if (rgb2 != null) { gradient.addColorStop(1, 'rgba(' + rgb2.r + ',' + rgb2.g + ',' + rgb2.b + ',' + s.gradientAlpha + ')'); } this.ctx.fillStyle = gradient; }; mxJsCanvas.prototype.setStrokeColor = function(value) { if (value == null) { // null value ignored } else if (value == mxConstants.NONE) { this.state.strokeColor = null; this.ctx.strokeStyle = 'rgba(0, 0, 0, 0)'; } else { this.ctx.strokeStyle = value; this.state.strokeColor = value; } }; mxJsCanvas.prototype.setStrokeWidth = function(value) { this.ctx.lineWidth = value; }; mxJsCanvas.prototype.setDashed = function(value) { this.state.dashed = value; if (value) { var dashArray = this.state.dashPattern.split(" "); for (var i = 0; i < dashArray.length; i++) { dashArray[i] = parseInt(dashArray[i], 10); } this.setLineDash(dashArray); } else { this.setLineDash([0]); } }; mxJsCanvas.prototype.setLineDash = function(value) { try { if (typeof this.ctx.setLineDash === "function") { this.ctx.setLineDash(value); } else { // Line dash not supported IE 10- } } catch (e) { // ignore } }; mxJsCanvas.prototype.setDashPattern = function(value) { this.state.dashPattern = value; if (this.state.dashed) { var dashArray = value.split(" "); for (var i = 0; i < dashArray.length; i++) { dashArray[i] = parseInt(dashArray[i], 10); } this.ctx.setLineDash(dashArray); } }; mxJsCanvas.prototype.setLineCap = function(value) { this.ctx.lineCap = value; }; mxJsCanvas.prototype.setLineJoin = function(value) { this.ctx.lineJoin = value; }; mxJsCanvas.prototype.setMiterLimit = function(value) { this.ctx.lineJoin = value; }; mxJsCanvas.prototype.setFontColor = function(value) { this.ctx.fillStyle = value; }; mxJsCanvas.prototype.setFontBackgroundColor = function(value) { if (value == mxConstants.NONE) { value = null; } this.state.fontBackgroundColor = value; }; mxJsCanvas.prototype.setFontBorderColor = function(value) { if (value == mxConstants.NONE) { value = null; } this.state.fontBorderColor = value; }; mxJsCanvas.prototype.setFontSize = function(value) { this.state.fontSize = value; }; mxJsCanvas.prototype.setFontFamily = function(value) { this.state.fontFamily = value; }; mxJsCanvas.prototype.setFontStyle = function(value) { this.state.fontStyle = value; }; /** * Function: setShadow * * Enables or disables and configures the current shadow. */ mxJsCanvas.prototype.setShadow = function(enabled) { this.state.shadow = enabled; if (enabled) { this.setShadowOffset(this.state.shadowDx, this.state.shadowDy); this.setShadowAlpha(this.state.shadowAlpha); } else { this.ctx.shadowColor = 'transparent'; this.ctx.shadowBlur = 0; this.ctx.shadowOffsetX = 0; this.ctx.shadowOffsetY = 0; } }; /** * Function: setShadowColor * * Enables or disables and configures the current shadow. */ mxJsCanvas.prototype.setShadowColor = function(value) { if (value == null || value == mxConstants.NONE) { value = null; this.ctx.shadowColor = 'transparent'; } this.state.shadowColor = value; if (this.state.shadow && value != null) { var alpha = (this.state.shadowAlpha != null) ? this.state.shadowAlpha : 1; var rgb = this.hexToRgb(value); this.ctx.shadowColor = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + alpha + ')'; } }; /** * Function: setShadowAlpha * * Enables or disables and configures the current shadow. */ mxJsCanvas.prototype.setShadowAlpha = function(value) { this.state.shadowAlpha = value; this.setShadowColor(this.state.shadowColor); }; /** * Function: setShadowOffset * * Enables or disables and configures the current shadow. */ mxJsCanvas.prototype.setShadowOffset = function(dx, dy) { this.state.shadowDx = dx; this.state.shadowDy = dy; if (this.state.shadow) { this.ctx.shadowOffsetX = dx; this.ctx.shadowOffsetY = dy; } }; mxJsCanvas.prototype.moveTo = function(x, y) { this.ctx.moveTo(x, y); this.lastMoveX = x; this.lastMoveY = y; }; mxJsCanvas.prototype.lineTo = function(x, y) { this.ctx.lineTo(x, y); this.lastMoveX = x; this.lastMoveY = y; }; mxJsCanvas.prototype.quadTo = function(x1, y1, x2, y2) { this.ctx.quadraticCurveTo(x1, y1, x2, y2); this.lastMoveX = x2; this.lastMoveY = y2; }; mxJsCanvas.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y) { var curves = mxUtils.arcToCurves(this.lastMoveX, this.lastMoveY, rx, ry, angle, largeArcFlag, sweepFlag, x, y); if (curves != null) { for (var i = 0; i < curves.length; i += 6) { this.curveTo(curves[i], curves[i + 1], curves[i + 2], curves[i + 3], curves[i + 4], curves[i + 5]); } } }; mxJsCanvas.prototype.curveTo = function(x1, y1, x2, y2, x3, y3) { this.ctx.bezierCurveTo(x1, y1, x2, y2 , x3, y3); this.lastMoveX = x3; this.lastMoveY = y3; }; mxJsCanvas.prototype.rect = function(x, y, w, h) { // TODO: Check if fillRect/strokeRect is faster this.begin(); this.moveTo(x, y); this.lineTo(x + w, y); this.lineTo(x + w, y + h); this.lineTo(x, y + h); this.close(); }; mxJsCanvas.prototype.roundrect = function(x, y, w, h, dx, dy) { this.begin(); this.moveTo(x + dx, y); this.lineTo(x + w - dx, y); this.quadTo(x + w, y, x + w, y + dy); this.lineTo(x + w, y + h - dy); this.quadTo(x + w, y + h, x + w - dx, y + h); this.lineTo(x + dx, y + h); this.quadTo(x, y + h, x, y + h - dy); this.lineTo(x, y + dy); this.quadTo(x, y, x + dx, y); }; mxJsCanvas.prototype.ellipse = function(x, y, w, h) { this.ctx.save(); this.ctx.translate((x + w / 2), (y + h / 2)); this.ctx.scale(w / 2, h / 2); this.ctx.beginPath(); this.ctx.arc(0, 0, 1, 0, 2 * Math.PI, false); this.ctx.restore(); }; //Redirect can be implemented via a hook mxJsCanvas.prototype.rewriteImageSource = function(src) { if (src.substring(0, 7) == 'http://' || src.substring(0, 8) == 'https://') { src = '/proxy?url=' + encodeURIComponent(src); } return src; }; mxJsCanvas.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV) { var scale = this.state.scale; // x = this.state.tx + x / scale; // y = this.state.ty + y / scale; // w /= scale; // h /= scale; src = this.rewriteImageSource(src); var image = this.images[src]; function drawImage(ctx, image, x, y, w, h) { ctx.save(); if (aspect) { var iw = image.width; var ih = image.height; var s = Math.min(w / iw, h / ih); var x0 = (w - iw * s) / 2; var y0 = (h - ih * s) / 2; x += x0; y += y0; w = iw * s; h = ih * s; } var s = this.state.scale; if (flipH) { ctx.translate(2 * x + w, 0); ctx.scale(-1, 1); } if (flipV) { ctx.translate(0, 2 * y + h); ctx.scale(1, -1); } ctx.drawImage(image, x, y, w, h); ctx.restore(); }; if (image != null && image.height > 0 && image.width > 0) { drawImage.call(this, this.ctx, image, x, y, w, h); } else { // TODO flag error that image wasn't obtaining in canvas preprocessing } }; mxJsCanvas.prototype.begin = function() { this.ctx.beginPath(); }; mxJsCanvas.prototype.close = function() { this.ctx.closePath(); }; mxJsCanvas.prototype.fill = function() { this.ctx.fill(); }; mxJsCanvas.prototype.stroke = function() { this.ctx.stroke(); }; mxJsCanvas.prototype.fillAndStroke = function() { // If you fill then stroke, the shadow of the stroke appears over the fill // So stroke, fill, disable shadow, stroke, restore previous shadow if (!this.state.shadow) { this.ctx.fill(); this.ctx.stroke(); } else { this.ctx.stroke(); this.ctx.fill(); var shadowColor = this.ctx.shadowColor; var shadowOffsetX = this.ctx.shadowOffsetX; var shadowOffsetY = this.ctx.shadowOffsetY; this.ctx.shadowColor = 'transparent'; this.ctx.shadowOffsetX = 0; this.ctx.shadowOffsetY = 0; this.ctx.stroke(); this.ctx.shadowColor = shadowColor; this.ctx.shadowOffsetX = shadowOffsetX; this.ctx.shadowOffsetY = shadowOffsetY; } }; mxJsCanvas.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation) { if (str == null || str.length == 0) { return; } var sc = this.state.scale; w *= sc; h *= sc; if (rotation != 0) { this.ctx.translate(Math.round(x), Math.round(y)); this.ctx.rotate(rotation * Math.PI / 180); this.ctx.translate(Math.round(-x), Math.round(-y)); } if (format == 'html') { var subCanvas = this.subCanvas[this.canvasIndex++]; var cavHeight = subCanvas.height; var cavWidth = subCanvas.width; switch (valign) { case mxConstants.ALIGN_MIDDLE: y -= cavHeight / 2 /sc; break; case mxConstants.ALIGN_BOTTOM: y -= cavHeight / sc; break; } switch (align) { case mxConstants.ALIGN_CENTER: x -= cavWidth / 2 / sc; break; case mxConstants.ALIGN_RIGHT: x -= cavWidth / sc; break; } this.ctx.save(); if (this.state.fontBackgroundColor != null || this.state.fontBorderColor != null) { if (this.state.fontBackgroundColor != null) { this.ctx.fillStyle = this.state.fontBackgroundColor; this.ctx.fillRect(Math.round(x) - 0.5, Math.round(y) - 0.5, Math.round(subCanvas.width / sc), Math.round(subCanvas.height / sc)); } if (this.state.fontBorderColor != null) { this.ctx.strokeStyle = this.state.fontBorderColor; this.ctx.lineWidth = 1; this.ctx.strokeRect(Math.round(x) - 0.5, Math.round(y) - 0.5, Math.round(subCanvas.width / sc), Math.round(subCanvas.height / sc)); } } //if (sc < 1) //{ this.ctx.scale(1/sc, 1/sc); //} this.ctx.drawImage(subCanvas, Math.round(x * sc) ,Math.round(y * sc)); this.ctx.restore(); } else { this.ctx.save(); this.updateFont(); var div = document.createElement("div"); div.innerHTML = str; div.style.position = 'absolute'; div.style.top = '-9999px'; div.style.left = '-9999px'; div.style.fontFamily = this.state.fontFamily; div.style.fontWeight = 'bold'; div.style.fontSize = this.state.fontSize + 'pt'; document.body.appendChild(div); var measuredFont = [div.offsetWidth, div.offsetHeight]; document.body.removeChild(div); var lines = str.split('\n'); var lineHeight = measuredFont[1]; this.ctx.textBaseline = 'top'; var backgroundY = y; switch (valign) { case mxConstants.ALIGN_MIDDLE: this.ctx.textBaseline = 'middle'; y -= (lines.length-1) * lineHeight / 2; backgroundY = y - this.state.fontSize / 2; break; case mxConstants.ALIGN_BOTTOM: this.ctx.textBaseline = 'alphabetic'; y -= lineHeight * (lines.length-1); backgroundY = y - this.state.fontSize; break; } var lineWidth = []; var lineX = []; for (var i = 0; i < lines.length; i++) { lineX[i] = x; lineWidth[i] = this.ctx.measureText(lines[i]).width; if (align != null && align != mxConstants.ALIGN_LEFT) { lineX[i] -= lineWidth[i]; if (align == mxConstants.ALIGN_CENTER) { lineX[i] += lineWidth[i] / 2; } } } if (this.state.fontBackgroundColor != null || this.state.fontBorderColor != null) { var startMostX = lineX[0]; var maxWidth = lineWidth[0]; for (var i = 1; i < lines.length; i++) { startMostX = Math.min(startMostX, lineX[i]); maxWidth = Math.max(maxWidth, lineWidth[i]); } this.ctx.save(); startMostX = Math.round(startMostX) - 0.5; backgroundY = Math.round(backgroundY) - 0.5; if (this.state.fontBackgroundColor != null) { this.ctx.fillStyle = this.state.fontBackgroundColor; this.ctx.fillRect(startMostX, backgroundY, maxWidth, this.state.fontSize * mxConstants.LINE_HEIGHT * lines.length); } if (this.state.fontBorderColor != null) { this.ctx.strokeStyle = this.state.fontBorderColor; this.ctx.lineWidth = 1; this.ctx.strokeRect(startMostX, backgroundY, maxWidth, this.state.fontSize * mxConstants.LINE_HEIGHT * lines.length); } this.ctx.restore(); } for (var i = 0; i < lines.length; i++) { this.ctx.fillText(lines[i], lineX[i], y); y += this.state.fontSize * mxConstants.LINE_HEIGHT; } this.ctx.restore(); } }; mxJsCanvas.prototype.getCanvas = function() { return canvas; }; mxJsCanvas.prototype.finish = function(handler) { // TODO: Check if waitCounter updates need a monitor. Question is // if image load-handler can be executed in parallel leading to // race conditions when updating the "shared" waitCounter. if (this.waitCounter == 0) { handler(); } else { this.onComplete = handler; } };