1/** 2 * mxJsCanvas 3 * 4 * Open Issues: 5 * 6 * - Canvas has no built-in dash-pattern for strokes 7 * - Use AS code for straight lines 8 * - Must use proxy for cross domain images 9 * - Use html2canvas for HTML rendering (Replaces complete page with 10 * canvas currently, needs API call to render elt to canvas) 11 */ 12 13/** 14 * Extends mxAbstractCanvas2D 15 */ 16function mxJsCanvas(canvas) 17{ 18 mxAbstractCanvas2D.call(this); 19 20 this.ctx = canvas.getContext('2d'); 21 this.ctx.textBaseline = 'top'; 22 this.ctx.fillStyle = 'rgba(255,255,255,0)'; 23 this.ctx.strokeStyle = 'rgba(0, 0, 0, 0)'; 24 25 //this.ctx.translate(0.5, 0.5); 26 27 this.M_RAD_PER_DEG = Math.PI / 180; 28 29 this.images = this.images == null ? [] : this.images; 30 this.subCanvas = this.subCanvas == null ? [] : this.subCanvas; 31}; 32 33/** 34 * Extends mxAbstractCanvas2D 35 */ 36mxUtils.extend(mxJsCanvas, mxAbstractCanvas2D); 37 38/** 39 * Variable: ctx 40 * 41 * Holds the current canvas context 42 */ 43mxJsCanvas.prototype.ctx = null; 44 45/** 46 * Variable: ctx 47 * 48 * Holds the current canvas context 49 */ 50mxJsCanvas.prototype.waitCounter = 0; 51 52/** 53 * Variable: ctx 54 * 55 * Holds the current canvas context 56 */ 57mxJsCanvas.prototype.onComplete = null; 58 59/** 60 * Variable: images 61 * 62 * Ordered array of images used in this canvas 63 */ 64mxJsCanvas.prototype.images = null; 65 66/** 67 * Variable: subCanvas 68 * 69 * Ordered array of sub canvas elements in this canvas 70 */ 71mxJsCanvas.prototype.subCanvas = null; 72 73/** 74 * Variable: canvasIndex 75 * 76 * The current index into the canvas sub-canvas array being processed 77 */ 78mxJsCanvas.prototype.canvasIndex = 0; 79 80mxJsCanvas.prototype.hexToRgb = function(hex) { 81 // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 82 var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 83 hex = hex.replace(shorthandRegex, function(m, r, g, b) { 84 return r + r + g + g + b + b; 85 }); 86 87 var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 88 return result ? { 89 r: parseInt(result[1], 16), 90 g: parseInt(result[2], 16), 91 b: parseInt(result[3], 16) 92 } : null; 93}; 94 95mxJsCanvas.prototype.incWaitCounter = function() 96{ 97 this.waitCounter++; 98}; 99 100mxJsCanvas.prototype.decWaitCounter = function() 101{ 102 this.waitCounter--; 103 104 if (this.waitCounter == 0 && this.onComplete != null) 105 { 106 this.onComplete(); 107 this.onComplete = null; 108 } 109}; 110 111mxJsCanvas.prototype.updateFont = function() 112{ 113 var style = ''; 114 115 if ((this.state.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD) 116 { 117 style += 'bold '; 118 } 119 120 if ((this.state.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC) 121 { 122 style += 'italic '; 123 } 124 125 this.ctx.font = style + this.state.fontSize + 'px ' + this.state.fontFamily; 126}; 127 128mxJsCanvas.prototype.save = function() 129{ 130 this.states.push(this.state); 131 this.state = mxUtils.clone(this.state); 132 this.ctx.save(); 133}; 134 135mxJsCanvas.prototype.restore = function() 136{ 137 this.state = this.states.pop(); 138 this.ctx.restore(); 139}; 140 141mxJsCanvas.prototype.scale = function(s) 142{ 143 this.state.scale *= s; 144 this.state.strokeWidth *= s; 145 this.ctx.scale(s, s); 146}; 147 148mxJsCanvas.prototype.translate = function(dx, dy) 149{ 150 this.state.dx += dx; 151 this.state.dy += dy; 152 this.ctx.translate(dx, dy); 153}; 154 155mxJsCanvas.prototype.rotate = function(theta, flipH, flipV, cx, cy) 156{ 157 // This is a special case where the rotation center is scaled so dx/dy, 158 // which are also scaled, must be applied after scaling the center. 159 cx -= this.state.dx; 160 cy -= this.state.dy; 161 162 this.ctx.translate(cx, cy); 163 164 if (flipH || flipV) 165 { 166 var sx = (flipH) ? -1 : 1; 167 var sy = (flipV) ? -1 : 1; 168 169 this.ctx.scale(sx, sy); 170 } 171 172 this.ctx.rotate(theta * this.M_RAD_PER_DEG); 173 this.ctx.translate(-cx, -cy); 174}; 175 176mxJsCanvas.prototype.setAlpha = function(alpha) 177{ 178 this.state.alpha = alpha; 179 this.ctx.globalAlpha = alpha; 180}; 181 182/** 183 * Function: setFillColor 184 * 185 * Sets the current fill color. 186 */ 187mxJsCanvas.prototype.setFillColor = function(value) 188{ 189 if (value == mxConstants.NONE) 190 { 191 value = null; 192 } 193 194 this.state.fillColor = value; 195 this.state.gradientColor = null; 196 this.ctx.fillStyle = value; 197}; 198 199mxJsCanvas.prototype.setGradient = function(color1, color2, x, y, w, h, direction, alpha1, alpha2) 200{ 201 var gradient = this.ctx.createLinearGradient(0, y, 0, y + h); 202 203 var s = this.state; 204 s.fillColor = color1; 205 s.fillAlpha = (alpha1 != null) ? alpha1 : 1; 206 s.gradientColor = color2; 207 s.gradientAlpha = (alpha2 != null) ? alpha2 : 1; 208 s.gradientDirection = direction; 209 210 var rgb1 = this.hexToRgb(color1); 211 var rgb2 = this.hexToRgb(color2); 212 213 if (rgb1 != null) 214 { 215 gradient.addColorStop(0, 'rgba(' + rgb1.r + ',' + rgb1.g + ',' + rgb1.b + ',' + s.fillAlpha + ')'); 216 } 217 218 if (rgb2 != null) 219 { 220 gradient.addColorStop(1, 'rgba(' + rgb2.r + ',' + rgb2.g + ',' + rgb2.b + ',' + s.gradientAlpha + ')'); 221 } 222 223 this.ctx.fillStyle = gradient; 224}; 225 226mxJsCanvas.prototype.setStrokeColor = function(value) 227{ 228 if (value == null) 229 { 230 // null value ignored 231 } 232 else if (value == mxConstants.NONE) 233 { 234 this.state.strokeColor = null; 235 this.ctx.strokeStyle = 'rgba(0, 0, 0, 0)'; 236 } 237 else 238 { 239 this.ctx.strokeStyle = value; 240 this.state.strokeColor = value; 241 } 242}; 243 244mxJsCanvas.prototype.setStrokeWidth = function(value) 245{ 246 this.ctx.lineWidth = value; 247}; 248 249mxJsCanvas.prototype.setDashed = function(value) 250{ 251 this.state.dashed = value; 252 253 if (value) 254 { 255 var dashArray = this.state.dashPattern.split(" "); 256 257 for (var i = 0; i < dashArray.length; i++) 258 { 259 dashArray[i] = parseInt(dashArray[i], 10); 260 } 261 262 this.setLineDash(dashArray); 263 } 264 else 265 { 266 this.setLineDash([0]); 267 } 268}; 269 270mxJsCanvas.prototype.setLineDash = function(value) 271{ 272 try 273 { 274 if (typeof this.ctx.setLineDash === "function") 275 { 276 this.ctx.setLineDash(value); 277 } 278 else 279 { 280 // Line dash not supported IE 10- 281 } 282 } 283 catch (e) 284 { 285 // ignore 286 } 287}; 288 289mxJsCanvas.prototype.setDashPattern = function(value) 290{ 291 this.state.dashPattern = value; 292 293 if (this.state.dashed) 294 { 295 var dashArray = value.split(" "); 296 297 for (var i = 0; i < dashArray.length; i++) 298 { 299 dashArray[i] = parseInt(dashArray[i], 10); 300 } 301 302 this.ctx.setLineDash(dashArray); 303 } 304}; 305 306mxJsCanvas.prototype.setLineCap = function(value) 307{ 308 this.ctx.lineCap = value; 309}; 310 311mxJsCanvas.prototype.setLineJoin = function(value) 312{ 313 this.ctx.lineJoin = value; 314}; 315 316mxJsCanvas.prototype.setMiterLimit = function(value) 317{ 318 this.ctx.lineJoin = value; 319}; 320 321mxJsCanvas.prototype.setFontColor = function(value) 322{ 323 this.ctx.fillStyle = value; 324}; 325 326mxJsCanvas.prototype.setFontBackgroundColor = function(value) 327{ 328 if (value == mxConstants.NONE) 329 { 330 value = null; 331 } 332 333 this.state.fontBackgroundColor = value; 334}; 335 336mxJsCanvas.prototype.setFontBorderColor = function(value) 337{ 338 if (value == mxConstants.NONE) 339 { 340 value = null; 341 } 342 343 this.state.fontBorderColor = value; 344}; 345 346mxJsCanvas.prototype.setFontSize = function(value) 347{ 348 this.state.fontSize = value; 349}; 350 351mxJsCanvas.prototype.setFontFamily = function(value) 352{ 353 this.state.fontFamily = value; 354}; 355 356mxJsCanvas.prototype.setFontStyle = function(value) 357{ 358 this.state.fontStyle = value; 359}; 360 361/** 362* Function: setShadow 363* 364* Enables or disables and configures the current shadow. 365*/ 366mxJsCanvas.prototype.setShadow = function(enabled) 367{ 368 this.state.shadow = enabled; 369 370 if (enabled) 371 { 372 this.setShadowOffset(this.state.shadowDx, this.state.shadowDy); 373 this.setShadowAlpha(this.state.shadowAlpha); 374 } 375 else 376 { 377 this.ctx.shadowColor = 'transparent'; 378 this.ctx.shadowBlur = 0; 379 this.ctx.shadowOffsetX = 0; 380 this.ctx.shadowOffsetY = 0; 381 } 382}; 383 384/** 385* Function: setShadowColor 386* 387* Enables or disables and configures the current shadow. 388*/ 389mxJsCanvas.prototype.setShadowColor = function(value) 390{ 391 if (value == null || value == mxConstants.NONE) 392 { 393 value = null; 394 this.ctx.shadowColor = 'transparent'; 395 } 396 397 this.state.shadowColor = value; 398 399 if (this.state.shadow && value != null) 400 { 401 var alpha = (this.state.shadowAlpha != null) ? this.state.shadowAlpha : 1; 402 var rgb = this.hexToRgb(value); 403 404 this.ctx.shadowColor = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + alpha + ')'; 405 } 406}; 407 408/** 409* Function: setShadowAlpha 410* 411* Enables or disables and configures the current shadow. 412*/ 413mxJsCanvas.prototype.setShadowAlpha = function(value) 414{ 415 this.state.shadowAlpha = value; 416 this.setShadowColor(this.state.shadowColor); 417}; 418 419/** 420* Function: setShadowOffset 421* 422* Enables or disables and configures the current shadow. 423*/ 424 425mxJsCanvas.prototype.setShadowOffset = function(dx, dy) 426{ 427 this.state.shadowDx = dx; 428 this.state.shadowDy = dy; 429 430 if (this.state.shadow) 431 { 432 this.ctx.shadowOffsetX = dx; 433 this.ctx.shadowOffsetY = dy; 434 } 435}; 436 437mxJsCanvas.prototype.moveTo = function(x, y) 438{ 439 this.ctx.moveTo(x, y); 440 this.lastMoveX = x; 441 this.lastMoveY = y; 442}; 443 444mxJsCanvas.prototype.lineTo = function(x, y) 445{ 446 this.ctx.lineTo(x, y); 447 this.lastMoveX = x; 448 this.lastMoveY = y; 449}; 450 451mxJsCanvas.prototype.quadTo = function(x1, y1, x2, y2) 452{ 453 this.ctx.quadraticCurveTo(x1, y1, x2, y2); 454 this.lastMoveX = x2; 455 this.lastMoveY = y2; 456}; 457 458mxJsCanvas.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y) 459{ 460 var curves = mxUtils.arcToCurves(this.lastMoveX, this.lastMoveY, rx, ry, angle, largeArcFlag, sweepFlag, x, y); 461 462 if (curves != null) 463 { 464 for (var i = 0; i < curves.length; i += 6) 465 { 466 this.curveTo(curves[i], curves[i + 1], curves[i + 2], 467 curves[i + 3], curves[i + 4], curves[i + 5]); 468 } 469 } 470}; 471 472mxJsCanvas.prototype.curveTo = function(x1, y1, x2, y2, x3, y3) 473{ 474 this.ctx.bezierCurveTo(x1, y1, x2, y2 , x3, y3); 475 this.lastMoveX = x3; 476 this.lastMoveY = y3; 477}; 478 479mxJsCanvas.prototype.rect = function(x, y, w, h) 480{ 481 // TODO: Check if fillRect/strokeRect is faster 482 this.begin(); 483 this.moveTo(x, y); 484 this.lineTo(x + w, y); 485 this.lineTo(x + w, y + h); 486 this.lineTo(x, y + h); 487 this.close(); 488}; 489 490mxJsCanvas.prototype.roundrect = function(x, y, w, h, dx, dy) 491{ 492 this.begin(); 493 this.moveTo(x + dx, y); 494 this.lineTo(x + w - dx, y); 495 this.quadTo(x + w, y, x + w, y + dy); 496 this.lineTo(x + w, y + h - dy); 497 this.quadTo(x + w, y + h, x + w - dx, y + h); 498 this.lineTo(x + dx, y + h); 499 this.quadTo(x, y + h, x, y + h - dy); 500 this.lineTo(x, y + dy); 501 this.quadTo(x, y, x + dx, y); 502}; 503 504mxJsCanvas.prototype.ellipse = function(x, y, w, h) 505{ 506 this.ctx.save(); 507 this.ctx.translate((x + w / 2), (y + h / 2)); 508 this.ctx.scale(w / 2, h / 2); 509 this.ctx.beginPath(); 510 this.ctx.arc(0, 0, 1, 0, 2 * Math.PI, false); 511 this.ctx.restore(); 512}; 513 514//Redirect can be implemented via a hook 515mxJsCanvas.prototype.rewriteImageSource = function(src) 516{ 517 if (src.substring(0, 7) == 'http://' || src.substring(0, 8) == 'https://') 518 { 519 src = '/proxy?url=' + encodeURIComponent(src); 520 } 521 522 return src; 523}; 524 525mxJsCanvas.prototype.image = function(x, y, w, h, src, aspect, flipH, flipV) 526{ 527 var scale = this.state.scale; 528 529// x = this.state.tx + x / scale; 530// y = this.state.ty + y / scale; 531// w /= scale; 532// h /= scale; 533 534 src = this.rewriteImageSource(src); 535 var image = this.images[src]; 536 537 function drawImage(ctx, image, x, y, w, h) 538 { 539 ctx.save(); 540 541 if (aspect) 542 { 543 var iw = image.width; 544 var ih = image.height; 545 546 var s = Math.min(w / iw, h / ih); 547 var x0 = (w - iw * s) / 2; 548 var y0 = (h - ih * s) / 2; 549 550 x += x0; 551 y += y0; 552 w = iw * s; 553 h = ih * s; 554 } 555 556 var s = this.state.scale; 557 558 if (flipH) 559 { 560 ctx.translate(2 * x + w, 0); 561 ctx.scale(-1, 1); 562 } 563 564 if (flipV) 565 { 566 ctx.translate(0, 2 * y + h); 567 ctx.scale(1, -1); 568 } 569 570 ctx.drawImage(image, x, y, w, h); 571 ctx.restore(); 572 }; 573 574 if (image != null && image.height > 0 && image.width > 0) 575 { 576 drawImage.call(this, this.ctx, image, x, y, w, h); 577 } 578 else 579 { 580 // TODO flag error that image wasn't obtaining in canvas preprocessing 581 } 582}; 583 584mxJsCanvas.prototype.begin = function() 585{ 586 this.ctx.beginPath(); 587}; 588 589mxJsCanvas.prototype.close = function() 590{ 591 this.ctx.closePath(); 592}; 593 594mxJsCanvas.prototype.fill = function() 595{ 596 this.ctx.fill(); 597}; 598 599mxJsCanvas.prototype.stroke = function() 600{ 601 this.ctx.stroke(); 602}; 603 604mxJsCanvas.prototype.fillAndStroke = function() 605{ 606 // If you fill then stroke, the shadow of the stroke appears over the fill 607 // So stroke, fill, disable shadow, stroke, restore previous shadow 608 if (!this.state.shadow) 609 { 610 this.ctx.fill(); 611 this.ctx.stroke(); 612 } 613 else 614 { 615 this.ctx.stroke(); 616 this.ctx.fill(); 617 618 var shadowColor = this.ctx.shadowColor; 619 var shadowOffsetX = this.ctx.shadowOffsetX; 620 var shadowOffsetY = this.ctx.shadowOffsetY; 621 622 this.ctx.shadowColor = 'transparent'; 623 this.ctx.shadowOffsetX = 0; 624 this.ctx.shadowOffsetY = 0; 625 626 this.ctx.stroke(); 627 628 this.ctx.shadowColor = shadowColor; 629 this.ctx.shadowOffsetX = shadowOffsetX; 630 this.ctx.shadowOffsetY = shadowOffsetY; 631 } 632}; 633 634mxJsCanvas.prototype.text = function(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation) 635{ 636 if (str == null || str.length == 0) 637 { 638 return; 639 } 640 641 var sc = this.state.scale; 642 w *= sc; 643 h *= sc; 644 645 if (rotation != 0) 646 { 647 this.ctx.translate(Math.round(x), Math.round(y)); 648 this.ctx.rotate(rotation * Math.PI / 180); 649 this.ctx.translate(Math.round(-x), Math.round(-y)); 650 } 651 652 if (format == 'html') 653 { 654 var subCanvas = this.subCanvas[this.canvasIndex++]; 655 var cavHeight = subCanvas.height; 656 var cavWidth = subCanvas.width; 657 658 switch (valign) 659 { 660 case mxConstants.ALIGN_MIDDLE: 661 y -= cavHeight / 2 /sc; 662 break; 663 case mxConstants.ALIGN_BOTTOM: 664 y -= cavHeight / sc; 665 break; 666 } 667 668 switch (align) 669 { 670 case mxConstants.ALIGN_CENTER: 671 x -= cavWidth / 2 / sc; 672 break; 673 case mxConstants.ALIGN_RIGHT: 674 x -= cavWidth / sc; 675 break; 676 } 677 678 this.ctx.save(); 679 680 if (this.state.fontBackgroundColor != null || this.state.fontBorderColor != null) 681 { 682 683 if (this.state.fontBackgroundColor != null) 684 { 685 this.ctx.fillStyle = this.state.fontBackgroundColor; 686 this.ctx.fillRect(Math.round(x) - 0.5, Math.round(y) - 0.5, Math.round(subCanvas.width / sc), Math.round(subCanvas.height / sc)); 687 } 688 if (this.state.fontBorderColor != null) 689 { 690 this.ctx.strokeStyle = this.state.fontBorderColor; 691 this.ctx.lineWidth = 1; 692 this.ctx.strokeRect(Math.round(x) - 0.5, Math.round(y) - 0.5, Math.round(subCanvas.width / sc), Math.round(subCanvas.height / sc)); 693 } 694 } 695 696 //if (sc < 1) 697 //{ 698 this.ctx.scale(1/sc, 1/sc); 699 //} 700 701 this.ctx.drawImage(subCanvas, Math.round(x * sc) ,Math.round(y * sc)); 702 703 this.ctx.restore(); 704 705 } 706 else 707 { 708 this.ctx.save(); 709 this.updateFont(); 710 711 var div = document.createElement("div"); 712 div.innerHTML = str; 713 div.style.position = 'absolute'; 714 div.style.top = '-9999px'; 715 div.style.left = '-9999px'; 716 div.style.fontFamily = this.state.fontFamily; 717 div.style.fontWeight = 'bold'; 718 div.style.fontSize = this.state.fontSize + 'pt'; 719 document.body.appendChild(div); 720 var measuredFont = [div.offsetWidth, div.offsetHeight]; 721 document.body.removeChild(div); 722 723 var lines = str.split('\n'); 724 var lineHeight = measuredFont[1]; 725 726 this.ctx.textBaseline = 'top'; 727 var backgroundY = y; 728 729 switch (valign) 730 { 731 case mxConstants.ALIGN_MIDDLE: 732 this.ctx.textBaseline = 'middle'; 733 y -= (lines.length-1) * lineHeight / 2; 734 backgroundY = y - this.state.fontSize / 2; 735 break; 736 case mxConstants.ALIGN_BOTTOM: 737 this.ctx.textBaseline = 'alphabetic'; 738 y -= lineHeight * (lines.length-1); 739 backgroundY = y - this.state.fontSize; 740 break; 741 } 742 743 var lineWidth = []; 744 var lineX = []; 745 746 for (var i = 0; i < lines.length; i++) 747 { 748 lineX[i] = x; 749 lineWidth[i] = this.ctx.measureText(lines[i]).width; 750 751 if (align != null && align != mxConstants.ALIGN_LEFT) 752 { 753 lineX[i] -= lineWidth[i]; 754 755 if (align == mxConstants.ALIGN_CENTER) 756 { 757 lineX[i] += lineWidth[i] / 2; 758 } 759 } 760 } 761 762 if (this.state.fontBackgroundColor != null || this.state.fontBorderColor != null) 763 { 764 var startMostX = lineX[0]; 765 var maxWidth = lineWidth[0]; 766 767 for (var i = 1; i < lines.length; i++) 768 { 769 startMostX = Math.min(startMostX, lineX[i]); 770 maxWidth = Math.max(maxWidth, lineWidth[i]); 771 } 772 773 this.ctx.save(); 774 775 startMostX = Math.round(startMostX) - 0.5; 776 backgroundY = Math.round(backgroundY) - 0.5; 777 778 if (this.state.fontBackgroundColor != null) 779 { 780 this.ctx.fillStyle = this.state.fontBackgroundColor; 781 this.ctx.fillRect(startMostX, backgroundY, maxWidth, this.state.fontSize * mxConstants.LINE_HEIGHT * lines.length); 782 } 783 if (this.state.fontBorderColor != null) 784 { 785 this.ctx.strokeStyle = this.state.fontBorderColor; 786 this.ctx.lineWidth = 1; 787 this.ctx.strokeRect(startMostX, backgroundY, maxWidth, this.state.fontSize * mxConstants.LINE_HEIGHT * lines.length); 788 } 789 790 this.ctx.restore(); 791 } 792 793 for (var i = 0; i < lines.length; i++) 794 { 795 this.ctx.fillText(lines[i], lineX[i], y); 796 y += this.state.fontSize * mxConstants.LINE_HEIGHT; 797 } 798 799 this.ctx.restore(); 800 } 801}; 802 803mxJsCanvas.prototype.getCanvas = function() 804{ 805 return canvas; 806}; 807 808mxJsCanvas.prototype.finish = function(handler) 809{ 810 // TODO: Check if waitCounter updates need a monitor. Question is 811 // if image load-handler can be executed in parallel leading to 812 // race conditions when updating the "shared" waitCounter. 813 if (this.waitCounter == 0) 814 { 815 handler(); 816 } 817 else 818 { 819 this.onComplete = handler; 820 } 821};