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