1 /* 2 Copyright 2008-2017 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/> 29 and <http://opensource.org/licenses/MIT/>. 30 */ 31 32 33 /*global JXG: true, define: true, AMprocessNode: true, document: true, Image: true, module: true, require: true */ 34 /*jslint nomen: true, plusplus: true, newcap:true*/ 35 36 /* depends: 37 jxg 38 renderer/abstract 39 base/constants 40 utils/env 41 utils/type 42 utils/uuid 43 utils/color 44 base/coords 45 math/math 46 math/geometry 47 math/numerics 48 */ 49 50 define([ 51 'jxg', 'renderer/abstract', 'base/constants', 'utils/env', 'utils/type', 'utils/uuid', 'utils/color', 52 'base/coords', 'math/math', 'math/geometry', 'math/numerics' 53 ], function (JXG, AbstractRenderer, Const, Env, Type, UUID, Color, Coords, Mat, Geometry, Numerics) { 54 55 "use strict"; 56 57 /** 58 * Uses HTML Canvas to implement the rendering methods defined in {@link JXG.AbstractRenderer}. 59 * @class JXG.AbstractRenderer 60 * @augments JXG.AbstractRenderer 61 * @param {Node} container Reference to a DOM node containing the board. 62 * @param {Object} dim The dimensions of the board 63 * @param {Number} dim.width 64 * @param {Number} dim.height 65 * @see JXG.AbstractRenderer 66 */ 67 JXG.CanvasRenderer = function (container, dim) { 68 var i; 69 70 this.type = 'canvas'; 71 72 this.canvasRoot = null; 73 this.suspendHandle = null; 74 this.canvasId = UUID.genUUID(); 75 76 this.canvasNamespace = null; 77 78 if (Env.isBrowser) { 79 this.container = container; 80 this.container.style.MozUserSelect = 'none'; 81 this.container.style.userSelect = 'none'; 82 83 this.container.style.overflow = 'hidden'; 84 if (this.container.style.position === '') { 85 this.container.style.position = 'relative'; 86 } 87 88 this.container.innerHTML = ['<canvas id="', this.canvasId, 89 '" width="', dim.width, 90 'px" height="', dim.height, 91 'px"><', '/canvas>'].join(''); 92 this.canvasRoot = this.container.ownerDocument.getElementById(this.canvasId); 93 this.context = this.canvasRoot.getContext('2d'); 94 } else if (Env.isNode()) { 95 this.canvasId = (typeof module === 'object' ? module.require('canvas') : require('canvas')); 96 this.canvasRoot = new this.canvasId(500, 500); 97 this.context = this.canvasRoot.getContext('2d'); 98 } 99 100 this.dashArray = [[2, 2], [5, 5], [10, 10], [20, 20], [20, 10, 10, 10], [20, 5, 10, 5]]; 101 }; 102 103 JXG.CanvasRenderer.prototype = new AbstractRenderer(); 104 105 JXG.extend(JXG.CanvasRenderer.prototype, /** @lends JXG.CanvasRenderer.prototype */ { 106 107 /* ************************** 108 * private methods only used 109 * in this renderer. Should 110 * not be called from outside. 111 * **************************/ 112 113 /** 114 * Draws a filled polygon. 115 * @param {Array} shape A matrix presented by a two dimensional array of numbers. 116 * @see JXG.AbstractRenderer#makeArrows 117 * @private 118 */ 119 _drawFilledPolygon: function (shape) { 120 var i, len = shape.length, 121 context = this.context; 122 123 if (len > 0) { 124 context.beginPath(); 125 context.moveTo(shape[0][0], shape[0][1]); 126 for (i = 0; i < len; i++) { 127 if (i > 0) { 128 context.lineTo(shape[i][0], shape[i][1]); 129 } 130 } 131 context.lineTo(shape[0][0], shape[0][1]); 132 context.fill(); 133 } 134 }, 135 136 /** 137 * Sets the fill color and fills an area. 138 * @param {JXG.GeometryElement} el An arbitrary JSXGraph element, preferably one with an area. 139 * @private 140 */ 141 _fill: function (el) { 142 var context = this.context; 143 144 context.save(); 145 if (this._setColor(el, 'fill')) { 146 context.fill(); 147 } 148 context.restore(); 149 }, 150 151 /** 152 * Rotates a point around <tt>(0, 0)</tt> by a given angle. 153 * @param {Number} angle An angle, given in rad. 154 * @param {Number} x X coordinate of the point. 155 * @param {Number} y Y coordinate of the point. 156 * @returns {Array} An array containing the x and y coordinate of the rotated point. 157 * @private 158 */ 159 _rotatePoint: function (angle, x, y) { 160 return [ 161 (x * Math.cos(angle)) - (y * Math.sin(angle)), 162 (x * Math.sin(angle)) + (y * Math.cos(angle)) 163 ]; 164 }, 165 166 /** 167 * Rotates an array of points around <tt>(0, 0)</tt>. 168 * @param {Array} shape An array of array of point coordinates. 169 * @param {Number} angle The angle in rad the points are rotated by. 170 * @returns {Array} Array of array of two dimensional point coordinates. 171 * @private 172 */ 173 _rotateShape: function (shape, angle) { 174 var i, rv = [], len = shape.length; 175 176 if (len <= 0) { 177 return shape; 178 } 179 180 for (i = 0; i < len; i++) { 181 rv.push(this._rotatePoint(angle, shape[i][0], shape[i][1])); 182 } 183 184 return rv; 185 }, 186 187 /** 188 * Sets color and opacity for filling and stroking. 189 * type is the attribute from visProp and targetType the context[targetTypeStyle]. 190 * This is necessary, because the fill style of a text is set by the stroke attributes of the text element. 191 * @param {JXG.GeometryElement} el Any JSXGraph element. 192 * @param {String} [type='stroke'] Either <em>fill</em> or <em>stroke</em>. 193 * @param {String} [targetType=type] (optional) Either <em>fill</em> or <em>stroke</em>. 194 * @returns {Boolean} If the color could be set, <tt>true</tt> is returned. 195 * @private 196 */ 197 _setColor: function (el, type, targetType) { 198 var hasColor = true, isTrace = false, 199 ev = el.visProp, hl, sw, 200 rgba, rgbo, c, o, oo; 201 202 type = type || 'stroke'; 203 targetType = targetType || type; 204 205 if (!Type.exists(el.board) || !Type.exists(el.board.highlightedObjects)) { 206 // This case handles trace elements. 207 // To make them work, we simply neglect highlighting. 208 isTrace = true; 209 } 210 211 if (!isTrace && Type.exists(el.board.highlightedObjects[el.id])) { 212 hl = 'highlight'; 213 } else { 214 hl = ''; 215 } 216 217 // type is equal to 'fill' or 'stroke' 218 rgba = Type.evaluate(ev[hl + type + 'color']); 219 if (rgba !== 'none' && rgba !== false) { 220 o = Type.evaluate(ev[hl + type + 'opacity']); 221 o = (o > 0) ? o : 0; 222 223 // RGB, not RGBA 224 if (rgba.length !== 9) { 225 c = rgba; 226 oo = o; 227 // True RGBA, not RGB 228 } else { 229 rgbo = Color.rgba2rgbo(rgba); 230 c = rgbo[0]; 231 oo = o * rgbo[1]; 232 } 233 this.context.globalAlpha = oo; 234 235 this.context[targetType + 'Style'] = c; 236 237 } else { 238 hasColor = false; 239 } 240 241 sw = parseFloat(Type.evaluate(ev.strokewidth)); 242 if (type === 'stroke' && !isNaN(sw)) { 243 if (sw === 0) { 244 this.context.globalAlpha = 0; 245 } else { 246 this.context.lineWidth = sw; 247 } 248 } 249 250 if (type === 'stroke' && ev.linecap !== undefined && ev.linecap !== '') { 251 this.context.lineCap = ev.linecap; 252 } 253 254 return hasColor; 255 }, 256 257 /** 258 * Sets color and opacity for drawing paths and lines and draws the paths and lines. 259 * @param {JXG.GeometryElement} el An JSXGraph element with a stroke. 260 * @private 261 */ 262 _stroke: function (el) { 263 var context = this.context, 264 ev_dash = Type.evaluate(el.visProp.dash); 265 266 context.save(); 267 268 if (ev_dash > 0) { 269 if (context.setLineDash) { 270 context.setLineDash(this.dashArray[ev_dash]); 271 } 272 } else { 273 this.context.lineDashArray = []; 274 } 275 276 if (this._setColor(el, 'stroke')) { 277 context.stroke(); 278 } 279 280 context.restore(); 281 }, 282 283 /** 284 * Translates a set of points. 285 * @param {Array} shape An array of point coordinates. 286 * @param {Number} x Translation in X direction. 287 * @param {Number} y Translation in Y direction. 288 * @returns {Array} An array of translated point coordinates. 289 * @private 290 */ 291 _translateShape: function (shape, x, y) { 292 var i, rv = [], len = shape.length; 293 294 if (len <= 0) { 295 return shape; 296 } 297 298 for (i = 0; i < len; i++) { 299 rv.push([ shape[i][0] + x, shape[i][1] + y ]); 300 } 301 302 return rv; 303 }, 304 305 /* ******************************** * 306 * Point drawing and updating * 307 * ******************************** */ 308 309 // documented in AbstractRenderer 310 drawPoint: function (el) { 311 var f = Type.evaluate(el.visProp.face), 312 size = Type.evaluate(el.visProp.size), 313 scr = el.coords.scrCoords, 314 sqrt32 = size * Math.sqrt(3) * 0.5, 315 s05 = size * 0.5, 316 stroke05 = parseFloat(Type.evaluate(el.visProp.strokewidth)) / 2.0, 317 context = this.context; 318 319 if (!el.visPropCalc.visible) { 320 return; 321 } 322 323 switch (f) { 324 case 'cross': // x 325 case 'x': 326 context.beginPath(); 327 context.moveTo(scr[1] - size, scr[2] - size); 328 context.lineTo(scr[1] + size, scr[2] + size); 329 context.moveTo(scr[1] + size, scr[2] - size); 330 context.lineTo(scr[1] - size, scr[2] + size); 331 context.lineCap = 'round'; 332 context.lineJoin = 'round'; 333 context.closePath(); 334 this._stroke(el); 335 break; 336 case 'circle': // dot 337 case 'o': 338 context.beginPath(); 339 context.arc(scr[1], scr[2], size + 1 + stroke05, 0, 2 * Math.PI, false); 340 context.closePath(); 341 this._fill(el); 342 this._stroke(el); 343 break; 344 case 'square': // rectangle 345 case '[]': 346 if (size <= 0) { 347 break; 348 } 349 350 context.save(); 351 if (this._setColor(el, 'stroke', 'fill')) { 352 context.fillRect(scr[1] - size - stroke05, scr[2] - size - stroke05, size * 2 + 3 * stroke05, size * 2 + 3 * stroke05); 353 } 354 context.restore(); 355 context.save(); 356 this._setColor(el, 'fill'); 357 context.fillRect(scr[1] - size + stroke05, scr[2] - size + stroke05, size * 2 - stroke05, size * 2 - stroke05); 358 context.restore(); 359 break; 360 case 'plus': // + 361 case '+': 362 context.beginPath(); 363 context.moveTo(scr[1] - size, scr[2]); 364 context.lineTo(scr[1] + size, scr[2]); 365 context.moveTo(scr[1], scr[2] - size); 366 context.lineTo(scr[1], scr[2] + size); 367 context.lineCap = 'round'; 368 context.lineJoin = 'round'; 369 context.closePath(); 370 this._stroke(el); 371 break; 372 case 'diamond': // <> 373 case '<>': 374 context.beginPath(); 375 context.moveTo(scr[1] - size, scr[2]); 376 context.lineTo(scr[1], scr[2] + size); 377 context.lineTo(scr[1] + size, scr[2]); 378 context.lineTo(scr[1], scr[2] - size); 379 context.closePath(); 380 this._fill(el); 381 this._stroke(el); 382 break; 383 case 'triangleup': 384 case 'a': 385 case '^': 386 context.beginPath(); 387 context.moveTo(scr[1], scr[2] - size); 388 context.lineTo(scr[1] - sqrt32, scr[2] + s05); 389 context.lineTo(scr[1] + sqrt32, scr[2] + s05); 390 context.closePath(); 391 this._fill(el); 392 this._stroke(el); 393 break; 394 case 'triangledown': 395 case 'v': 396 context.beginPath(); 397 context.moveTo(scr[1], scr[2] + size); 398 context.lineTo(scr[1] - sqrt32, scr[2] - s05); 399 context.lineTo(scr[1] + sqrt32, scr[2] - s05); 400 context.closePath(); 401 this._fill(el); 402 this._stroke(el); 403 break; 404 case 'triangleleft': 405 case '<': 406 context.beginPath(); 407 context.moveTo(scr[1] - size, scr[2]); 408 context.lineTo(scr[1] + s05, scr[2] - sqrt32); 409 context.lineTo(scr[1] + s05, scr[2] + sqrt32); 410 context.closePath(); 411 this.fill(el); 412 this._stroke(el); 413 break; 414 case 'triangleright': 415 case '>': 416 context.beginPath(); 417 context.moveTo(scr[1] + size, scr[2]); 418 context.lineTo(scr[1] - s05, scr[2] - sqrt32); 419 context.lineTo(scr[1] - s05, scr[2] + sqrt32); 420 context.closePath(); 421 this._fill(el); 422 this._stroke(el); 423 break; 424 } 425 }, 426 427 // documented in AbstractRenderer 428 updatePoint: function (el) { 429 this.drawPoint(el); 430 }, 431 432 /* ******************************** * 433 * Lines * 434 * ******************************** */ 435 436 // documented in AbstractRenderer 437 drawLine: function (el) { 438 var obj, 439 c1 = new Coords(Const.COORDS_BY_USER, el.point1.coords.usrCoords, el.board), 440 c2 = new Coords(Const.COORDS_BY_USER, el.point2.coords.usrCoords, el.board), 441 margin = null, 442 ev_fa = Type.evaluate(el.visProp.firstarrow), 443 ev_la = Type.evaluate(el.visProp.lastarrow); 444 445 if (!el.visPropCalc.visible) { 446 return; 447 } 448 449 if (ev_fa || ev_la) { 450 margin = -4; 451 } 452 Geometry.calcStraight(el, c1, c2, margin); 453 454 obj = this.getPositionArrowHead(el, c1, c2); 455 456 this.context.beginPath(); 457 this.context.moveTo(obj.c1.scrCoords[1] + obj.d1x, obj.c1.scrCoords[2] + obj.d1y); 458 this.context.lineTo(obj.c2.scrCoords[1] - obj.d2x, obj.c2.scrCoords[2] - obj.d2y); 459 this._stroke(el); 460 461 if ((ev_fa && obj.sFirst > 0) || 462 (ev_la && obj.sLast > 0)) { 463 this.makeArrows(el, obj.c1, obj.c2); 464 } 465 }, 466 467 // documented in AbstractRenderer 468 updateLine: function (el) { 469 this.drawLine(el); 470 }, 471 472 // documented in AbstractRenderer 473 drawTicks: function () { 474 // this function is supposed to initialize the svg/vml nodes in the SVG/VMLRenderer. 475 // but in canvas there are no such nodes, hence we just do nothing and wait until 476 // updateTicks is called. 477 }, 478 479 // documented in AbstractRenderer 480 updateTicks: function (ticks) { 481 var i, c, x, y, 482 len = ticks.ticks.length, 483 context = this.context; 484 485 context.beginPath(); 486 for (i = 0; i < len; i++) { 487 c = ticks.ticks[i]; 488 x = c[0]; 489 y = c[1]; 490 context.moveTo(x[0], y[0]); 491 context.lineTo(x[1], y[1]); 492 } 493 // Labels 494 for (i = 0; i < len; i++) { 495 c = ticks.ticks[i].scrCoords; 496 if (ticks.ticks[i].major && 497 (ticks.board.needsFullUpdate || ticks.needsRegularUpdate) && 498 ticks.labels[i] && 499 ticks.labels[i].visPropCalc.visible) { 500 this.updateText(ticks.labels[i]); 501 } 502 } 503 context.lineCap = 'round'; 504 this._stroke(ticks); 505 }, 506 507 /* ************************** 508 * Curves 509 * **************************/ 510 511 // documented in AbstractRenderer 512 drawCurve: function (el) { 513 if (Type.evaluate(el.visProp.handdrawing)) { 514 this.updatePathStringBezierPrim(el); 515 } else { 516 this.updatePathStringPrim(el); 517 } 518 if (el.numberPoints > 1) { 519 this.makeArrows(el); 520 } 521 }, 522 523 // documented in AbstractRenderer 524 updateCurve: function (el) { 525 this.drawCurve(el); 526 }, 527 528 /* ************************** 529 * Circle related stuff 530 * **************************/ 531 532 // documented in AbstractRenderer 533 drawEllipse: function (el) { 534 var m1 = el.center.coords.scrCoords[1], 535 m2 = el.center.coords.scrCoords[2], 536 sX = el.board.unitX, 537 sY = el.board.unitY, 538 rX = 2 * el.Radius(), 539 rY = 2 * el.Radius(), 540 aWidth = rX * sX, 541 aHeight = rY * sY, 542 aX = m1 - aWidth / 2, 543 aY = m2 - aHeight / 2, 544 hB = (aWidth / 2) * 0.5522848, 545 vB = (aHeight / 2) * 0.5522848, 546 eX = aX + aWidth, 547 eY = aY + aHeight, 548 mX = aX + aWidth / 2, 549 mY = aY + aHeight / 2, 550 context = this.context; 551 552 if (rX > 0.0 && rY > 0.0 && !isNaN(m1 + m2)) { 553 context.beginPath(); 554 context.moveTo(aX, mY); 555 context.bezierCurveTo(aX, mY - vB, mX - hB, aY, mX, aY); 556 context.bezierCurveTo(mX + hB, aY, eX, mY - vB, eX, mY); 557 context.bezierCurveTo(eX, mY + vB, mX + hB, eY, mX, eY); 558 context.bezierCurveTo(mX - hB, eY, aX, mY + vB, aX, mY); 559 context.closePath(); 560 this._fill(el); 561 this._stroke(el); 562 } 563 }, 564 565 // documented in AbstractRenderer 566 updateEllipse: function (el) { 567 return this.drawEllipse(el); 568 }, 569 570 /* ************************** 571 * Polygon 572 * **************************/ 573 574 // nothing here, using AbstractRenderer implementations 575 576 /* ************************** 577 * Text related stuff 578 * **************************/ 579 580 // already documented in JXG.AbstractRenderer 581 displayCopyright: function (str, fontSize) { 582 var context = this.context; 583 584 // this should be called on EVERY update, otherwise it won't be shown after the first update 585 context.save(); 586 context.font = fontSize + 'px Arial'; 587 context.fillStyle = '#aaa'; 588 context.lineWidth = 0.5; 589 context.fillText(str, 10, 2 + fontSize); 590 context.restore(); 591 }, 592 593 // already documented in JXG.AbstractRenderer 594 drawInternalText: function (el) { 595 var ev_fs = Type.evaluate(el.visProp.fontsize), 596 ev_ax = Type.evaluate(el.visProp.anchorx), 597 ev_ay = Type.evaluate(el.visProp.anchory), 598 context = this.context; 599 600 context.save(); 601 if (this._setColor(el, 'stroke', 'fill') && 602 !isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) { 603 context.font = (ev_fs > 0 ? ev_fs : 0) + 'px Arial'; 604 605 this.transformImage(el, el.transformations); 606 if (ev_ax === 'left') { 607 context.textAlign = 'left'; 608 } else if (ev_ax === 'right') { 609 context.textAlign = 'right'; 610 } else if (ev_ax === 'middle') { 611 context.textAlign = 'center'; 612 } 613 if (ev_ay === 'bottom') { 614 context.textBaseline = 'bottom'; 615 } else if (ev_ay === 'top') { 616 context.textBaseline = 'top'; 617 } else if (ev_ay === 'middle') { 618 context.textBaseline = 'middle'; 619 } 620 context.fillText(el.plaintext, el.coords.scrCoords[1], el.coords.scrCoords[2]); 621 } 622 context.restore(); 623 return null; 624 }, 625 626 // already documented in JXG.AbstractRenderer 627 updateInternalText: function (el) { 628 this.drawInternalText(el); 629 }, 630 631 // documented in JXG.AbstractRenderer 632 // Only necessary for texts 633 setObjectStrokeColor: function (el, color, opacity) { 634 var rgba = Type.evaluate(color), c, rgbo, 635 o = Type.evaluate(opacity), oo, 636 node; 637 638 o = (o > 0) ? o : 0; 639 640 if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) { 641 return; 642 } 643 644 // Check if this could be merged with _setColor 645 646 if (Type.exists(rgba) && rgba !== false) { 647 // RGB, not RGBA 648 if (rgba.length !== 9) { 649 c = rgba; 650 oo = o; 651 // True RGBA, not RGB 652 } else { 653 rgbo = Color.rgba2rgbo(rgba); 654 c = rgbo[0]; 655 oo = o * rgbo[1]; 656 } 657 node = el.rendNode; 658 if (el.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(el.visProp.display) === 'html') { 659 node.style.color = c; 660 node.style.opacity = oo; 661 } 662 } 663 664 el.visPropOld.strokecolor = rgba; 665 el.visPropOld.strokeopacity = o; 666 }, 667 668 /* ************************** 669 * Image related stuff 670 * **************************/ 671 672 // already documented in JXG.AbstractRenderer 673 drawImage: function (el) { 674 el.rendNode = new Image(); 675 // Store the file name of the image. 676 // Before, this was done in el.rendNode.src 677 // But there, the file name is expanded to 678 // the full url. This may be different from 679 // the url computed in updateImageURL(). 680 el._src = ''; 681 this.updateImage(el); 682 }, 683 684 // already documented in JXG.AbstractRenderer 685 updateImage: function (el) { 686 var context = this.context, 687 o = Type.evaluate(el.visProp.fillopacity), 688 paintImg = Type.bind(function () { 689 el.imgIsLoaded = true; 690 if (el.size[0] <= 0 || el.size[1] <= 0) { 691 return; 692 } 693 context.save(); 694 context.globalAlpha = o; 695 // If det(el.transformations)=0, FireFox 3.6. breaks down. 696 // This is tested in transformImage 697 this.transformImage(el, el.transformations); 698 context.drawImage(el.rendNode, 699 el.coords.scrCoords[1], 700 el.coords.scrCoords[2] - el.size[1], 701 el.size[0], 702 el.size[1]); 703 context.restore(); 704 }, this); 705 706 if (this.updateImageURL(el)) { 707 el.rendNode.onload = paintImg; 708 } else { 709 if (el.imgIsLoaded) { 710 paintImg(); 711 } 712 } 713 }, 714 715 // already documented in JXG.AbstractRenderer 716 transformImage: function (el, t) { 717 var m, len = t.length, 718 ctx = this.context; 719 720 if (len > 0) { 721 m = this.joinTransforms(el, t); 722 if (Math.abs(Numerics.det(m)) >= Mat.eps) { 723 ctx.transform(m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]); 724 } 725 } 726 }, 727 728 // already documented in JXG.AbstractRenderer 729 updateImageURL: function (el) { 730 var url; 731 732 url = Type.evaluate(el.url); 733 if (el._src !== url) { 734 el.imgIsLoaded = false; 735 el.rendNode.src = url; 736 el._src = url; 737 return true; 738 } 739 740 return false; 741 }, 742 743 /* ************************** 744 * Render primitive objects 745 * **************************/ 746 747 // documented in AbstractRenderer 748 remove: function (shape) { 749 // sounds odd for a pixel based renderer but we need this for html texts 750 if (Type.exists(shape) && Type.exists(shape.parentNode)) { 751 shape.parentNode.removeChild(shape); 752 } 753 }, 754 755 // documented in AbstractRenderer 756 makeArrows: function (el, scr1, scr2) { 757 // not done yet for curves and arcs. 758 var x1, y1, x2, y2, ang, 759 size, 760 w = Type.evaluate(el.visProp.strokewidth), 761 arrowHead, 762 arrowTail, 763 context = this.context, 764 type, 765 ev_fa = Type.evaluate(el.visProp.firstarrow), 766 ev_la = Type.evaluate(el.visProp.lastarrow); 767 768 if (Type.evaluate(el.visProp.strokecolor) !== 'none' && 769 (ev_fa || ev_la)) { 770 if (el.elementClass === Const.OBJECT_CLASS_LINE) { 771 x1 = scr1.scrCoords[1]; 772 y1 = scr1.scrCoords[2]; 773 x2 = scr2.scrCoords[1]; 774 y2 = scr2.scrCoords[2]; 775 } else { 776 x1 = el.points[0].scrCoords[1]; 777 y1 = el.points[0].scrCoords[2]; 778 x2 = el.points[el.points.length - 1].scrCoords[1]; 779 y2 = el.points[el.points.length - 1].scrCoords[2]; 780 return; 781 } 782 783 if (ev_fa && 784 Type.exists(ev_fa.type)) { 785 786 if (Type.exists(ev_fa.size)) { 787 size = Type.evaluate(ev_fa.size); 788 } else { 789 size = 3; 790 } 791 w *= size; 792 793 type = Type.evaluate(ev_fa.type); 794 if (type === 2) { 795 arrowTail = [ 796 [ w, -w * 0.5], 797 [ 0.0, 0.0], 798 [ w, w * 0.5], 799 [ w * 0.5, 0.0], 800 ]; 801 } else if (type === 3) { 802 arrowTail = [ 803 [ w / 3.0, -w * 0.5], 804 [ 0.0, -w * 0.5], 805 [ 0.0, w * 0.5], 806 [ w / 3.0, w * 0.5] 807 ]; 808 } else { 809 arrowTail = [ 810 [ w, -w * 0.5], 811 [ 0.0, 0.0], 812 [ w, w * 0.5] 813 ]; 814 } 815 816 } 817 if (ev_la && 818 Type.exists(ev_la.type)) { 819 820 if (Type.exists(ev_la.size)) { 821 size = Type.evaluate(ev_la.size); 822 } else { 823 size = 3; 824 } 825 w *= size; 826 827 type = Type.evaluate(ev_la.type); 828 if (type === 2) { 829 arrowHead = [ 830 [ -w, -w * 0.5], 831 [ 0.0, 0.0], 832 [ -w, w * 0.5], 833 [ -w * 0.5, 0.0] 834 ]; 835 } else if (type === 3) { 836 arrowHead = [ 837 [-w / 3.0, -w * 0.5], 838 [ 0.0, -w * 0.5], 839 [ 0.0, w * 0.5], 840 [-w / 3.0, w * 0.5] 841 ]; 842 } else { 843 arrowHead = [ 844 [ -w, -w * 0.5], 845 [ 0.0, 0.0], 846 [ -w, w * 0.5] 847 ]; 848 } 849 } 850 851 context.save(); 852 if (this._setColor(el, 'stroke', 'fill')) { 853 ang = Math.atan2(y2 - y1, x2 - x1); 854 if (ev_la) { 855 this._drawFilledPolygon(this._translateShape(this._rotateShape(arrowHead, ang), x2, y2)); 856 } 857 858 if (ev_fa) { 859 this._drawFilledPolygon(this._translateShape(this._rotateShape(arrowTail, ang), x1, y1)); 860 } 861 } 862 context.restore(); 863 } 864 }, 865 866 // documented in AbstractRenderer 867 updatePathStringPrim: function (el) { 868 var i, scr, scr1, scr2, len, 869 symbm = 'M', 870 symbl = 'L', 871 symbc = 'C', 872 nextSymb = symbm, 873 maxSize = 5000.0, 874 context = this.context; 875 876 if (el.numberPoints <= 0) { 877 return; 878 } 879 880 len = Math.min(el.points.length, el.numberPoints); 881 context.beginPath(); 882 883 if (el.bezierDegree === 1) { 884 /* 885 if (isNotPlot && el.board.options.curve.RDPsmoothing) { 886 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5); 887 } 888 */ 889 890 for (i = 0; i < len; i++) { 891 scr = el.points[i].scrCoords; 892 893 if (isNaN(scr[1]) || isNaN(scr[2])) { // PenUp 894 nextSymb = symbm; 895 } else { 896 // Chrome has problems with values being too far away. 897 if (scr[1] > maxSize) { 898 scr[1] = maxSize; 899 } else if (scr[1] < -maxSize) { 900 scr[1] = -maxSize; 901 } 902 903 if (scr[2] > maxSize) { 904 scr[2] = maxSize; 905 } else if (scr[2] < -maxSize) { 906 scr[2] = -maxSize; 907 } 908 909 if (nextSymb === symbm) { 910 context.moveTo(scr[1], scr[2]); 911 } else { 912 context.lineTo(scr[1], scr[2]); 913 } 914 nextSymb = symbl; 915 } 916 } 917 } else if (el.bezierDegree === 3) { 918 i = 0; 919 while (i < len) { 920 scr = el.points[i].scrCoords; 921 if (isNaN(scr[1]) || isNaN(scr[2])) { // PenUp 922 nextSymb = symbm; 923 } else { 924 if (nextSymb === symbm) { 925 context.moveTo(scr[1], scr[2]); 926 } else { 927 i += 1; 928 scr1 = el.points[i].scrCoords; 929 i += 1; 930 scr2 = el.points[i].scrCoords; 931 context.bezierCurveTo(scr[1], scr[2], scr1[1], scr1[2], scr2[1], scr2[2]); 932 } 933 nextSymb = symbc; 934 } 935 i += 1; 936 } 937 } 938 context.lineCap = 'round'; 939 this._fill(el); 940 this._stroke(el); 941 }, 942 943 // already documented in JXG.AbstractRenderer 944 updatePathStringBezierPrim: function (el) { 945 var i, j, k, scr, lx, ly, len, 946 symbm = 'M', 947 symbl = 'C', 948 nextSymb = symbm, 949 maxSize = 5000.0, 950 f = Type.evaluate(el.visProp.strokewidth), 951 isNoPlot = (Type.evaluate(el.visProp.curvetype) !== 'plot'), 952 context = this.context; 953 954 if (el.numberPoints <= 0) { 955 return; 956 } 957 958 if (isNoPlot && el.board.options.curve.RDPsmoothing) { 959 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5); 960 } 961 962 len = Math.min(el.points.length, el.numberPoints); 963 context.beginPath(); 964 965 for (j = 1; j < 3; j++) { 966 nextSymb = symbm; 967 for (i = 0; i < len; i++) { 968 scr = el.points[i].scrCoords; 969 970 if (isNaN(scr[1]) || isNaN(scr[2])) { // PenUp 971 nextSymb = symbm; 972 } else { 973 // Chrome has problems with values being too far away. 974 if (scr[1] > maxSize) { 975 scr[1] = maxSize; 976 } else if (scr[1] < -maxSize) { 977 scr[1] = -maxSize; 978 } 979 980 if (scr[2] > maxSize) { 981 scr[2] = maxSize; 982 } else if (scr[2] < -maxSize) { 983 scr[2] = -maxSize; 984 } 985 986 if (nextSymb === symbm) { 987 context.moveTo(scr[1], scr[2]); 988 } else { 989 k = 2 * j; 990 context.bezierCurveTo( 991 (lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j)), 992 (ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j)), 993 (lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j)), 994 (ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j)), 995 scr[1], 996 scr[2] 997 ); 998 } 999 nextSymb = symbl; 1000 lx = scr[1]; 1001 ly = scr[2]; 1002 } 1003 } 1004 } 1005 context.lineCap = 'round'; 1006 this._fill(el); 1007 this._stroke(el); 1008 }, 1009 1010 // documented in AbstractRenderer 1011 updatePolygonPrim: function (node, el) { 1012 var scrCoords, i, j, 1013 len = el.vertices.length, 1014 context = this.context, 1015 isReal = true; 1016 1017 if (len <= 0 || !el.visPropCalc.visible) { 1018 return; 1019 } 1020 1021 context.beginPath(); 1022 i = 0; 1023 while (!el.vertices[i].isReal && i < len - 1) { 1024 i++; 1025 isReal = false; 1026 } 1027 scrCoords = el.vertices[i].coords.scrCoords; 1028 context.moveTo(scrCoords[1], scrCoords[2]); 1029 1030 for (j = i; j < len - 1; j++) { 1031 if (!el.vertices[j].isReal) { 1032 isReal = false; 1033 } 1034 scrCoords = el.vertices[j].coords.scrCoords; 1035 context.lineTo(scrCoords[1], scrCoords[2]); 1036 } 1037 context.closePath(); 1038 1039 if (isReal) { 1040 this._fill(el); // The edges of a polygon are displayed separately (as segments). 1041 } 1042 }, 1043 1044 /*************************** 1045 * Set Attributes 1046 ************************** 1047 */ 1048 1049 // already documented in JXG.AbstractRenderer 1050 display: function(el, val) { 1051 if (el && el.rendNode) { 1052 el.visPropOld.visible = val; 1053 if (val) { 1054 el.rendNode.style.visibility = "inherit"; 1055 } else { 1056 el.rendNode.style.visibility = "hidden"; 1057 } 1058 } 1059 }, 1060 1061 // documented in AbstractRenderer 1062 show: function (el) { 1063 JXG.deprecated('Board.renderer.show()', 'Board.renderer.display()'); 1064 1065 if (Type.exists(el.rendNode)) { 1066 el.rendNode.style.visibility = "inherit"; 1067 } 1068 }, 1069 1070 // documented in AbstractRenderer 1071 hide: function (el) { 1072 JXG.deprecated('Board.renderer.hide()', 'Board.renderer.display()'); 1073 1074 if (Type.exists(el.rendNode)) { 1075 el.rendNode.style.visibility = "hidden"; 1076 } 1077 }, 1078 1079 // documented in AbstractRenderer 1080 setGradient: function (el) { 1081 var col, op; 1082 1083 op = Type.evaluate(el.visProp.fillopacity); 1084 op = (op > 0) ? op : 0; 1085 1086 col = Type.evaluate(el.visProp.fillcolor); 1087 }, 1088 1089 // documented in AbstractRenderer 1090 setShadow: function (el) { 1091 if (el.visPropOld.shadow === el.visProp.shadow) { 1092 return; 1093 } 1094 1095 // not implemented yet 1096 // we simply have to redraw the element 1097 // probably the best way to do so would be to call el.updateRenderer(), i think. 1098 1099 el.visPropOld.shadow = el.visProp.shadow; 1100 }, 1101 1102 // documented in AbstractRenderer 1103 highlight: function (obj) { 1104 if (obj.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(obj.visProp.display) === 'html') { 1105 this.updateTextStyle(obj, true); 1106 } else { 1107 obj.board.prepareUpdate(); 1108 obj.board.renderer.suspendRedraw(obj.board); 1109 obj.board.updateRenderer(); 1110 obj.board.renderer.unsuspendRedraw(); 1111 } 1112 return this; 1113 }, 1114 1115 // documented in AbstractRenderer 1116 noHighlight: function (obj) { 1117 if (obj.elementClass === Const.OBJECT_CLASS_TEXT && Type.evaluate(obj.visProp.display) === 'html') { 1118 this.updateTextStyle(obj, false); 1119 } else { 1120 obj.board.prepareUpdate(); 1121 obj.board.renderer.suspendRedraw(obj.board); 1122 obj.board.updateRenderer(); 1123 obj.board.renderer.unsuspendRedraw(); 1124 } 1125 return this; 1126 }, 1127 1128 /* ************************** 1129 * renderer control 1130 * **************************/ 1131 1132 // documented in AbstractRenderer 1133 suspendRedraw: function (board) { 1134 this.context.save(); 1135 this.context.clearRect(0, 0, this.canvasRoot.width, this.canvasRoot.height); 1136 1137 if (board && board.attr.showcopyright) { 1138 this.displayCopyright(JXG.licenseText, 12); 1139 } 1140 }, 1141 1142 // documented in AbstractRenderer 1143 unsuspendRedraw: function () { 1144 this.context.restore(); 1145 }, 1146 1147 // document in AbstractRenderer 1148 resize: function (w, h) { 1149 if (this.container) { 1150 this.canvasRoot.style.width = parseFloat(w) + 'px'; 1151 this.canvasRoot.style.height = parseFloat(h) + 'px'; 1152 1153 this.canvasRoot.setAttribute('width', (2 * parseFloat(w)) + 'px'); 1154 this.canvasRoot.setAttribute('height',(2 * parseFloat(h)) + 'px'); 1155 } else { 1156 this.canvasRoot.width = 2 * parseFloat(w); 1157 this.canvasRoot.height = 2 * parseFloat(h); 1158 } 1159 this.context = this.canvasRoot.getContext('2d'); 1160 // The width and height of the canvas is set to twice the CSS values, 1161 // followed by an appropiate scaling. 1162 // See http://stackoverflow.com/questions/22416462/canvas-element-with-blurred-lines 1163 this.context.scale(2, 2); 1164 }, 1165 1166 removeToInsertLater: function () { 1167 return function () {}; 1168 } 1169 }); 1170 1171 return JXG.CanvasRenderer; 1172 }); 1173