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