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