1 /* 2 Copyright 2008-2023 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 <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true, AMprocessNode: true, MathJax: true, document: true */ 33 /*jslint nomen: true, plusplus: true, newcap:true*/ 34 35 import JXG from "../jxg"; 36 import Options from "../options"; 37 import AbstractRenderer from "./abstract"; 38 import Const from "../base/constants"; 39 import Type from "../utils/type"; 40 import Color from "../utils/color"; 41 import Base64 from "../utils/base64"; 42 import Numerics from "../math/numerics"; 43 44 /** 45 * Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}. 46 * @class JXG.SVGRenderer 47 * @augments JXG.AbstractRenderer 48 * @param {Node} container Reference to a DOM node containing the board. 49 * @param {Object} dim The dimensions of the board 50 * @param {Number} dim.width 51 * @param {Number} dim.height 52 * @see JXG.AbstractRenderer 53 */ 54 JXG.SVGRenderer = function (container, dim) { 55 var i; 56 57 // docstring in AbstractRenderer 58 this.type = "svg"; 59 60 this.isIE = 61 navigator.appVersion.indexOf("MSIE") !== -1 || navigator.userAgent.match(/Trident\//); 62 63 /** 64 * SVG root node 65 * @type Node 66 */ 67 this.svgRoot = null; 68 69 /** 70 * The SVG Namespace used in JSXGraph. 71 * @see http://www.w3.org/TR/SVG2/ 72 * @type String 73 * @default http://www.w3.org/2000/svg 74 */ 75 this.svgNamespace = "http://www.w3.org/2000/svg"; 76 77 /** 78 * The xlink namespace. This is used for images. 79 * @see http://www.w3.org/TR/xlink/ 80 * @type String 81 * @default http://www.w3.org/1999/xlink 82 */ 83 this.xlinkNamespace = "http://www.w3.org/1999/xlink"; 84 85 // container is documented in AbstractRenderer 86 this.container = container; 87 88 // prepare the div container and the svg root node for use with JSXGraph 89 this.container.style.MozUserSelect = "none"; 90 this.container.style.userSelect = "none"; 91 92 this.container.style.overflow = "hidden"; 93 if (this.container.style.position === "") { 94 this.container.style.position = "relative"; 95 } 96 97 this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, "svg"); 98 this.svgRoot.style.overflow = "hidden"; 99 this.svgRoot.style.display = "block"; 100 101 this.resize(dim.width, dim.height); 102 103 //this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision'); 104 105 this.container.appendChild(this.svgRoot); 106 107 /** 108 * The <tt>defs</tt> element is a container element to reference reusable SVG elements. 109 * @type Node 110 * @see https://www.w3.org/TR/SVG2/struct.html#DefsElement 111 */ 112 this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, "defs"); 113 this.svgRoot.appendChild(this.defs); 114 115 /** 116 * Filters are used to apply shadows. 117 * @type Node 118 * @see https://www.w3.org/TR/SVG2/struct.html#DefsElement 119 */ 120 /** 121 * Create an SVG shadow filter. If the object's RGB color is [r,g,b], it's opacity is op, and 122 * the parameter color is given as [r', g', b'] with opacity op' 123 * the shadow will have RGB color [blend*r + r', blend*g + g', blend*b + b'] and the opacity will be equal to op * op'. 124 * Further, blur and offset can be adjusted. 125 * 126 * The shadow color is [r*ble 127 * @param {String} id Node is of the filter. 128 * @param {Array|String} rgb RGB value for the blend color or the string 'none' for default values. Default 'black'. 129 * @param {Number} opacity Value between 0 and 1, default is 1. 130 * @param {Number} blend Value between 0 and 1, default is 0.1. 131 * @param {Number} blur Default: 3 132 * @param {Array} offset [dx, dy]. Default is [5,5]. 133 * @returns DOM node to be added to this.defs. 134 * @private 135 */ 136 this.createShadowFilter = function (id, rgb, opacity, blend, blur, offset) { 137 var filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter'), 138 feOffset, feColor, feGaussianBlur, feBlend, 139 mat; 140 141 filter.setAttributeNS(null, 'id', id); 142 filter.setAttributeNS(null, 'width', '300%'); 143 filter.setAttributeNS(null, 'height', '300%'); 144 filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse'); 145 146 feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset'); 147 feOffset.setAttributeNS(null, 'in', 'SourceGraphic'); // b/w: SourceAlpha, Color: SourceGraphic 148 feOffset.setAttributeNS(null, 'result', 'offOut'); 149 feOffset.setAttributeNS(null, 'dx', offset[0]); 150 feOffset.setAttributeNS(null, 'dy', offset[1]); 151 filter.appendChild(feOffset); 152 153 feColor = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feColorMatrix'); 154 feColor.setAttributeNS(null, 'in', 'offOut'); 155 feColor.setAttributeNS(null, 'result', 'colorOut'); 156 feColor.setAttributeNS(null, 'type', 'matrix'); 157 // See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix 158 if (rgb === 'none' || !Type.isArray(rgb) || rgb.length < 3) { 159 feColor.setAttributeNS(null, 'values', '0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 ' + opacity + ' 0'); 160 } else { 161 rgb[0] /= 255; 162 rgb[1] /= 255; 163 rgb[2] /= 255; 164 mat = blend + ' 0 0 0 ' + rgb[0] + 165 ' 0 ' + blend + ' 0 0 ' + rgb[1] + 166 ' 0 0 ' + blend + ' 0 ' + rgb[2] + 167 ' 0 0 0 ' + opacity + ' 0'; 168 feColor.setAttributeNS(null, 'values', mat); 169 } 170 filter.appendChild(feColor); 171 172 feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur'); 173 feGaussianBlur.setAttributeNS(null, 'in', 'colorOut'); 174 feGaussianBlur.setAttributeNS(null, 'result', 'blurOut'); 175 feGaussianBlur.setAttributeNS(null, 'stdDeviation', blur); 176 filter.appendChild(feGaussianBlur); 177 178 feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend'); 179 feBlend.setAttributeNS(null, 'in', 'SourceGraphic'); 180 feBlend.setAttributeNS(null, 'in2', 'blurOut'); 181 feBlend.setAttributeNS(null, 'mode', 'normal'); 182 filter.appendChild(feBlend); 183 184 return filter; 185 }; 186 187 /** 188 * Create a "unique" string id from the arguments of the function. 189 * Concatenate all arguments by "_". 190 * "Unique" is achieved by simply prepending the container id. 191 * Do not escape the string. 192 * 193 * If the id is used in an "url()" call it must be eascaped. 194 * 195 * @params {String} one or strings which will be concatenated. 196 * @return {String} 197 * @private 198 */ 199 this.uniqName = function () { 200 return this.container.id + '_' + 201 Array.prototype.slice.call(arguments).join('_'); 202 }; 203 204 /** 205 * Combine arguments to an URL string of the form 206 * url(#...) 207 * Masks the container id. 208 * 209 * @params {Objects} parts of the string 210 * @returns URL string 211 * @private 212 * @example 213 * this.toURL('aaa', '_', 'bbb', 'TriangleEnd') 214 * // Output: 215 * // url(#xxx_bbbTriangleEnd) 216 * 217 */ 218 this.toURL = function () { 219 // ES6 would be [...arguments].join() 220 var str = Array.prototype.slice.call(arguments).join(''); 221 // Mask special symbols like '/' and '\' in id 222 if (Type.exists(CSS) && Type.exists(CSS.escape)) { 223 str = CSS.escape(str); 224 } 225 return 'url(#' + str + ')'; 226 }; 227 228 /* Default shadow filter */ 229 this.defs.appendChild(this.createShadowFilter(this.uniqName('f1'), 'none', 1, 0.1, 3, [5, 5])); 230 231 /** 232 * JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front 233 * of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented 234 * there, too. The higher the number, the "more on top" are the elements on this layer. 235 * @type Array 236 */ 237 this.layer = []; 238 for (i = 0; i < Options.layer.numlayers; i++) { 239 this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g'); 240 this.svgRoot.appendChild(this.layer[i]); 241 } 242 243 try { 244 this.foreignObjLayer = this.container.ownerDocument.createElementNS( 245 this.svgNamespace, 246 "foreignObject" 247 ); 248 this.foreignObjLayer.setAttribute("display", "none"); 249 this.foreignObjLayer.setAttribute("x", 0); 250 this.foreignObjLayer.setAttribute("y", 0); 251 this.foreignObjLayer.setAttribute("width", "100%"); 252 this.foreignObjLayer.setAttribute("height", "100%"); 253 this.foreignObjLayer.setAttribute("id", this.uniqName('foreignObj')); 254 this.svgRoot.appendChild(this.foreignObjLayer); 255 this.supportsForeignObject = true; 256 } catch (e) { 257 this.supportsForeignObject = false; 258 } 259 }; 260 261 JXG.SVGRenderer.prototype = new AbstractRenderer(); 262 263 JXG.extend( 264 JXG.SVGRenderer.prototype, 265 /** @lends JXG.SVGRenderer.prototype */ { 266 /** 267 * Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag. 268 * @private 269 * @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached. 270 * @param {String} [idAppendix=''] A string that is added to the node's id. 271 * @returns {Node} Reference to the node added to the DOM. 272 */ 273 _createArrowHead: function (el, idAppendix, type) { 274 var node2, 275 node3, 276 id = el.id + "Triangle", 277 //type = null, 278 v, 279 h; 280 281 if (Type.exists(idAppendix)) { 282 id += idAppendix; 283 } 284 node2 = this.createPrim("marker", id); 285 286 node2.setAttributeNS(null, "stroke", Type.evaluate(el.visProp.strokecolor)); 287 node2.setAttributeNS( 288 null, 289 "stroke-opacity", 290 Type.evaluate(el.visProp.strokeopacity) 291 ); 292 node2.setAttributeNS(null, "fill", Type.evaluate(el.visProp.strokecolor)); 293 node2.setAttributeNS(null, "fill-opacity", Type.evaluate(el.visProp.strokeopacity)); 294 node2.setAttributeNS(null, "stroke-width", 0); // this is the stroke-width of the arrow head. 295 // Should be zero to simplify the calculations 296 297 node2.setAttributeNS(null, "orient", "auto"); 298 node2.setAttributeNS(null, "markerUnits", "strokeWidth"); // 'strokeWidth' 'userSpaceOnUse'); 299 300 /* 301 Types 1, 2: 302 The arrow head is an isosceles triangle with base length 10 and height 10. 303 304 Type 3: 305 A rectangle 306 307 Types 4, 5, 6: 308 Defined by Bezier curves from mp_arrowheads.html 309 310 In any case but type 3 the arrow head is 10 units long, 311 type 3 is 10 units high. 312 These 10 units are scaled to strokeWidth * arrowSize pixels, see 313 this._setArrowWidth(). 314 315 See also abstractRenderer.updateLine() where the line path is shortened accordingly. 316 317 Changes here are also necessary in setArrowWidth(). 318 319 So far, lines with arrow heads are shortenend to avoid overlapping of 320 arrow head and line. This is not the case for curves, yet. 321 Therefore, the offset refX has to be adapted to the path type. 322 */ 323 node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, "path"); 324 h = 5; 325 if (idAppendix === "End") { 326 // First arrow 327 //type = a.typeFirst; 328 // if (JXG.exists(ev_fa.type)) { 329 // type = Type.evaluate(ev_fa.type); 330 // } 331 332 v = 0; 333 if (type === 2) { 334 node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 L 5,5 z"); 335 } else if (type === 3) { 336 node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z"); 337 } else if (type === 4) { 338 // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0 339 h = 3.31; 340 node3.setAttributeNS( 341 null, 342 "d", 343 "M 0.00,3.31 C 3.53,3.84 7.13,4.50 10.00,6.63 C 9.33,5.52 8.67,4.42 8.00,3.31 C 8.67,2.21 9.33,1.10 10.00,0.00 C 7.13,2.13 3.53,2.79 0.00,3.31" 344 ); 345 } else if (type === 5) { 346 // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15 347 h = 3.28; 348 node3.setAttributeNS( 349 null, 350 "d", 351 "M 0.00,3.28 C 3.39,4.19 6.81,5.07 10.00,6.55 C 9.38,5.56 9.00,4.44 9.00,3.28 C 9.00,2.11 9.38,0.99 10.00,0.00 C 6.81,1.49 3.39,2.37 0.00,3.28" 352 ); 353 } else if (type === 6) { 354 // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0 355 h = 2.84; 356 node3.setAttributeNS( 357 null, 358 "d", 359 "M 0.00,2.84 C 3.39,3.59 6.79,4.35 10.00,5.68 C 9.67,4.73 9.33,3.78 9.00,2.84 C 9.33,1.89 9.67,0.95 10.00,0.00 C 6.79,1.33 3.39,2.09 0.00,2.84" 360 ); 361 } else if (type === 7) { 362 // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0 363 h = 5.2; 364 node3.setAttributeNS( 365 null, 366 "d", 367 "M 0.00,5.20 C 4.04,5.20 7.99,6.92 10.00,10.39 M 10.00,0.00 C 7.99,3.47 4.04,5.20 0.00,5.20" 368 ); 369 } else { 370 // type == 1 or > 6 371 node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 z"); 372 } 373 if ( 374 // !Type.exists(el.rendNode.getTotalLength) && 375 el.elementClass === Const.OBJECT_CLASS_LINE 376 ) { 377 if (type === 2) { 378 v = 4.9; 379 } else if (type === 3) { 380 v = 3.3; 381 } else if (type === 4 || type === 5 || type === 6) { 382 v = 6.66; 383 } else if (type === 7) { 384 v = 0.0; 385 } else { 386 v = 10.0; 387 } 388 } 389 } else { 390 // Last arrow 391 // if (JXG.exists(ev_la.type)) { 392 // type = Type.evaluate(ev_la.type); 393 // } 394 //type = a.typeLast; 395 396 v = 10.0; 397 if (type === 2) { 398 node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 L 5,5 z"); 399 } else if (type === 3) { 400 v = 3.3; 401 node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z"); 402 } else if (type === 4) { 403 // insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0 404 h = 3.31; 405 node3.setAttributeNS( 406 null, 407 "d", 408 "M 10.00,3.31 C 6.47,3.84 2.87,4.50 0.00,6.63 C 0.67,5.52 1.33,4.42 2.00,3.31 C 1.33,2.21 0.67,1.10 0.00,0.00 C 2.87,2.13 6.47,2.79 10.00,3.31" 409 ); 410 } else if (type === 5) { 411 // insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15 412 h = 3.28; 413 node3.setAttributeNS( 414 null, 415 "d", 416 "M 10.00,3.28 C 6.61,4.19 3.19,5.07 0.00,6.55 C 0.62,5.56 1.00,4.44 1.00,3.28 C 1.00,2.11 0.62,0.99 0.00,0.00 C 3.19,1.49 6.61,2.37 10.00,3.28" 417 ); 418 } else if (type === 6) { 419 // insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0 420 h = 2.84; 421 node3.setAttributeNS( 422 null, 423 "d", 424 "M 10.00,2.84 C 6.61,3.59 3.21,4.35 0.00,5.68 C 0.33,4.73 0.67,3.78 1.00,2.84 C 0.67,1.89 0.33,0.95 0.00,0.00 C 3.21,1.33 6.61,2.09 10.00,2.84" 425 ); 426 } else if (type === 7) { 427 // insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0 428 h = 5.2; 429 node3.setAttributeNS( 430 null, 431 "d", 432 "M 10.00,5.20 C 5.96,5.20 2.01,6.92 0.00,10.39 M 0.00,0.00 C 2.01,3.47 5.96,5.20 10.00,5.20" 433 ); 434 } else { 435 // type == 1 or > 6 436 node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 z"); 437 } 438 if ( 439 // !Type.exists(el.rendNode.getTotalLength) && 440 el.elementClass === Const.OBJECT_CLASS_LINE 441 ) { 442 if (type === 2) { 443 v = 5.1; 444 } else if (type === 3) { 445 v = 0.02; 446 } else if (type === 4 || type === 5 || type === 6) { 447 v = 3.33; 448 } else if (type === 7) { 449 v = 10.0; 450 } else { 451 v = 0.05; 452 } 453 } 454 } 455 if (type === 7) { 456 node2.setAttributeNS(null, "fill", "none"); 457 node2.setAttributeNS(null, "stroke-width", 1); // this is the stroke-width of the arrow head. 458 } 459 node2.setAttributeNS(null, "refY", h); 460 node2.setAttributeNS(null, "refX", v); 461 462 node2.appendChild(node3); 463 return node2; 464 }, 465 466 /** 467 * Updates color of an arrow DOM node. 468 * @param {Node} node The arrow node. 469 * @param {String} color Color value in a HTML compatible format, e.g. <tt>#00ff00</tt> or <tt>green</tt> for green. 470 * @param {Number} opacity 471 * @param {JXG.GeometryElement} el The element the arrows are to be attached to 472 */ 473 _setArrowColor: function (node, color, opacity, el, type) { 474 if (node) { 475 if (Type.isString(color)) { 476 if (type !== 7) { 477 this._setAttribute(function () { 478 node.setAttributeNS(null, "stroke", color); 479 node.setAttributeNS(null, "fill", color); 480 node.setAttributeNS(null, "stroke-opacity", opacity); 481 node.setAttributeNS(null, "fill-opacity", opacity); 482 }, el.visPropOld.fillcolor); 483 } else { 484 this._setAttribute(function () { 485 node.setAttributeNS(null, "fill", "none"); 486 node.setAttributeNS(null, "stroke", color); 487 node.setAttributeNS(null, "stroke-opacity", opacity); 488 }, el.visPropOld.fillcolor); 489 } 490 } 491 492 if (this.isIE) { 493 el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode); 494 } 495 } 496 }, 497 498 // Already documented in JXG.AbstractRenderer 499 _setArrowWidth: function (node, width, parentNode, size) { 500 var s, d; 501 502 if (node) { 503 // if (width === 0) { 504 // // display:none does not work well in webkit 505 // node.setAttributeNS(null, 'display', 'none'); 506 // } else { 507 s = width; 508 d = s * size; 509 node.setAttributeNS(null, "viewBox", 0 + " " + 0 + " " + s * 10 + " " + s * 10); 510 node.setAttributeNS(null, "markerHeight", d); 511 node.setAttributeNS(null, "markerWidth", d); 512 node.setAttributeNS(null, "display", "inherit"); 513 // } 514 515 if (this.isIE) { 516 parentNode.parentNode.insertBefore(parentNode, parentNode); 517 } 518 } 519 }, 520 521 /* ******************************** * 522 * This renderer does not need to 523 * override draw/update* methods 524 * since it provides draw/update*Prim 525 * methods except for some cases like 526 * internal texts or images. 527 * ******************************** */ 528 529 /* ************************** 530 * Lines 531 * **************************/ 532 533 // documented in AbstractRenderer 534 updateTicks: function (ticks) { 535 var i, 536 j, 537 c, 538 node, 539 x, 540 y, 541 tickStr = "", 542 len = ticks.ticks.length, 543 len2, 544 str, 545 isReal = true; 546 547 for (i = 0; i < len; i++) { 548 c = ticks.ticks[i]; 549 x = c[0]; 550 y = c[1]; 551 552 len2 = x.length; 553 str = " M " + x[0] + " " + y[0]; 554 if (!Type.isNumber(x[0])) { 555 isReal = false; 556 } 557 for (j = 1; isReal && j < len2; ++j) { 558 if (Type.isNumber(x[j])) { 559 str += " L " + x[j] + " " + y[j]; 560 } else { 561 isReal = false; 562 } 563 } 564 if (isReal) { 565 tickStr += str; 566 } 567 } 568 569 node = ticks.rendNode; 570 571 if (!Type.exists(node)) { 572 node = this.createPrim("path", ticks.id); 573 this.appendChildPrim(node, Type.evaluate(ticks.visProp.layer)); 574 ticks.rendNode = node; 575 } 576 577 node.setAttributeNS(null, "stroke", Type.evaluate(ticks.visProp.strokecolor)); 578 node.setAttributeNS(null, "fill", "none"); 579 // node.setAttributeNS(null, 'fill', Type.evaluate(ticks.visProp.fillcolor)); 580 // node.setAttributeNS(null, 'fill-opacity', Type.evaluate(ticks.visProp.fillopacity)); 581 node.setAttributeNS( 582 null, 583 "stroke-opacity", 584 Type.evaluate(ticks.visProp.strokeopacity) 585 ); 586 node.setAttributeNS(null, "stroke-width", Type.evaluate(ticks.visProp.strokewidth)); 587 this.updatePathPrim(node, tickStr, ticks.board); 588 }, 589 590 /* ************************** 591 * Text related stuff 592 * **************************/ 593 594 // Already documented in JXG.AbstractRenderer 595 displayCopyright: function (str, fontsize) { 596 var node = this.createPrim("text", 'licenseText'), 597 t; 598 node.setAttributeNS(null, 'x', '20px'); 599 node.setAttributeNS(null, 'y', 2 + fontsize + 'px'); 600 node.setAttributeNS(null, 'style', 'font-family:Arial,Helvetica,sans-serif; font-size:' + 601 fontsize + 'px; fill:#356AA0; opacity:0.3;'); 602 t = this.container.ownerDocument.createTextNode(str); 603 node.appendChild(t); 604 this.appendChildPrim(node, 0); 605 }, 606 607 // Already documented in JXG.AbstractRenderer 608 drawInternalText: function (el) { 609 var node = this.createPrim("text", el.id); 610 611 //node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox 612 // Preserve spaces 613 //node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", "preserve"); 614 node.style.whiteSpace = "nowrap"; 615 616 el.rendNodeText = this.container.ownerDocument.createTextNode(""); 617 node.appendChild(el.rendNodeText); 618 this.appendChildPrim(node, Type.evaluate(el.visProp.layer)); 619 620 return node; 621 }, 622 623 // Already documented in JXG.AbstractRenderer 624 updateInternalText: function (el) { 625 var content = el.plaintext, 626 v, 627 ev_ax = el.getAnchorX(), 628 ev_ay = el.getAnchorY(); 629 630 if (el.rendNode.getAttributeNS(null, "class") !== el.visProp.cssclass) { 631 el.rendNode.setAttributeNS(null, "class", Type.evaluate(el.visProp.cssclass)); 632 el.needsSizeUpdate = true; 633 } 634 635 if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) { 636 // Horizontal 637 v = el.coords.scrCoords[1]; 638 if (el.visPropOld.left !== ev_ax + v) { 639 el.rendNode.setAttributeNS(null, "x", v + "px"); 640 641 if (ev_ax === "left") { 642 el.rendNode.setAttributeNS(null, "text-anchor", "start"); 643 } else if (ev_ax === "right") { 644 el.rendNode.setAttributeNS(null, "text-anchor", "end"); 645 } else if (ev_ax === "middle") { 646 el.rendNode.setAttributeNS(null, "text-anchor", "middle"); 647 } 648 el.visPropOld.left = ev_ax + v; 649 } 650 651 // Vertical 652 v = el.coords.scrCoords[2]; 653 if (el.visPropOld.top !== ev_ay + v) { 654 el.rendNode.setAttributeNS(null, "y", v + this.vOffsetText * 0.5 + "px"); 655 656 if (ev_ay === "bottom") { 657 el.rendNode.setAttributeNS( 658 null, 659 "dominant-baseline", 660 "text-after-edge" 661 ); 662 } else if (ev_ay === "top") { 663 el.rendNode.setAttributeNS(null, "dy", "1.6ex"); 664 //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge'); // Not supported by IE, edge 665 } else if (ev_ay === "middle") { 666 //el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle'); 667 el.rendNode.setAttributeNS(null, "dy", "0.6ex"); 668 } 669 el.visPropOld.top = ev_ay + v; 670 } 671 } 672 if (el.htmlStr !== content) { 673 el.rendNodeText.data = content; 674 el.htmlStr = content; 675 } 676 this.transformImage(el, el.transformations); 677 }, 678 679 /** 680 * Set color and opacity of internal texts. 681 * SVG needs its own version. 682 * @private 683 * @see JXG.AbstractRenderer#updateTextStyle 684 * @see JXG.AbstractRenderer#updateInternalTextStyle 685 */ 686 updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) { 687 this.setObjectFillColor(el, strokeColor, strokeOpacity); 688 }, 689 690 /* ************************** 691 * Image related stuff 692 * **************************/ 693 694 // Already documented in JXG.AbstractRenderer 695 drawImage: function (el) { 696 var node = this.createPrim("image", el.id); 697 698 node.setAttributeNS(null, "preserveAspectRatio", "none"); 699 this.appendChildPrim(node, Type.evaluate(el.visProp.layer)); 700 el.rendNode = node; 701 702 this.updateImage(el); 703 }, 704 705 // Already documented in JXG.AbstractRenderer 706 transformImage: function (el, t) { 707 var s, m, 708 node = el.rendNode, 709 str = "", 710 cx, cy, 711 len = t.length; 712 713 if (len > 0) { 714 m = this.joinTransforms(el, t); 715 s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(","); 716 if (s.indexOf('NaN') === -1) { 717 str += " matrix(" + s + ") "; 718 if (el.elementClass === Const.OBJECT_CLASS_TEXT && el.visProp.display === 'html') { 719 node.style.transform = str; 720 cx = -el.coords.scrCoords[1]; 721 cy = -el.coords.scrCoords[2]; 722 switch (Type.evaluate(el.visProp.anchorx)) { 723 case 'right': cx += el.size[0]; break; 724 case 'middle': cx += el.size[0] * 0.5; break; 725 } 726 switch (Type.evaluate(el.visProp.anchory)) { 727 case 'bottom': cy += el.size[1]; break; 728 case 'middle': cy += el.size[1] * 0.5; break; 729 } 730 node.style['transform-origin'] = (cx) + 'px ' + (cy) + 'px'; 731 } else { 732 // Images and texts with display:'internal' 733 node.setAttributeNS(null, "transform", str); 734 } 735 } 736 } 737 }, 738 739 // Already documented in JXG.AbstractRenderer 740 updateImageURL: function (el) { 741 var url = Type.evaluate(el.url); 742 743 if (el._src !== url) { 744 el.imgIsLoaded = false; 745 el.rendNode.setAttributeNS(this.xlinkNamespace, "xlink:href", url); 746 el._src = url; 747 748 return true; 749 } 750 751 return false; 752 }, 753 754 // Already documented in JXG.AbstractRenderer 755 updateImageStyle: function (el, doHighlight) { 756 var css = Type.evaluate( 757 doHighlight ? el.visProp.highlightcssclass : el.visProp.cssclass 758 ); 759 760 el.rendNode.setAttributeNS(null, "class", css); 761 }, 762 763 // Already documented in JXG.AbstractRenderer 764 drawForeignObject: function (el) { 765 el.rendNode = this.appendChildPrim( 766 this.createPrim("foreignObject", el.id), 767 Type.evaluate(el.visProp.layer) 768 ); 769 770 this.appendNodesToElement(el, "foreignObject"); 771 this.updateForeignObject(el); 772 }, 773 774 // Already documented in JXG.AbstractRenderer 775 updateForeignObject: function (el) { 776 if (el._useUserSize) { 777 el.rendNode.style.overflow = "hidden"; 778 } else { 779 el.rendNode.style.overflow = "visible"; 780 } 781 782 this.updateRectPrim( 783 el.rendNode, 784 el.coords.scrCoords[1], 785 el.coords.scrCoords[2] - el.size[1], 786 el.size[0], 787 el.size[1] 788 ); 789 790 el.rendNode.innerHTML = el.content; 791 this._updateVisual(el, { stroke: true, dash: true }, true); 792 }, 793 794 /* ************************** 795 * Render primitive objects 796 * **************************/ 797 798 // Already documented in JXG.AbstractRenderer 799 appendChildPrim: function (node, level) { 800 if (!Type.exists(level)) { 801 // trace nodes have level not set 802 level = 0; 803 } else if (level >= Options.layer.numlayers) { 804 level = Options.layer.numlayers - 1; 805 } 806 807 this.layer[level].appendChild(node); 808 809 return node; 810 }, 811 812 // Already documented in JXG.AbstractRenderer 813 createPrim: function (type, id) { 814 var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type); 815 node.setAttributeNS(null, "id", this.uniqName(id)); 816 node.style.position = "absolute"; 817 if (type === "path") { 818 node.setAttributeNS(null, "stroke-linecap", "round"); 819 node.setAttributeNS(null, "stroke-linejoin", "round"); 820 node.setAttributeNS(null, "fill-rule", "evenodd"); 821 } 822 return node; 823 }, 824 825 // Already documented in JXG.AbstractRenderer 826 remove: function (shape) { 827 if (Type.exists(shape) && Type.exists(shape.parentNode)) { 828 shape.parentNode.removeChild(shape); 829 } 830 }, 831 832 // Already documented in JXG.AbstractRenderer 833 setLayer: function (el, level) { 834 if (!Type.exists(level)) { 835 level = 0; 836 } else if (level >= Options.layer.numlayers) { 837 level = Options.layer.numlayers - 1; 838 } 839 840 this.layer[level].appendChild(el.rendNode); 841 }, 842 843 // Already documented in JXG.AbstractRenderer 844 makeArrows: function (el, a) { 845 var node2, 846 ev_fa = a.evFirst, 847 ev_la = a.evLast; 848 849 // Test if the arrow heads already exist 850 if (el.visPropOld.firstarrow === ev_fa && el.visPropOld.lastarrow === ev_la) { 851 if (this.isIE && el.visPropCalc.visible && (ev_fa || ev_la)) { 852 el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode); 853 } 854 return; 855 } 856 857 if (ev_fa) { 858 node2 = el.rendNodeTriangleStart; 859 if (!Type.exists(node2)) { 860 node2 = this._createArrowHead(el, "End", a.typeFirst); 861 this.defs.appendChild(node2); 862 el.rendNodeTriangleStart = node2; 863 el.rendNode.setAttributeNS( 864 null, 865 "marker-start", 866 // "url(#" + this.container.id + "_" + el.id + "TriangleEnd)" 867 this.toURL(this.container.id, '_', el.id, 'TriangleEnd') 868 ); 869 } else { 870 this.defs.appendChild(node2); 871 } 872 } else { 873 node2 = el.rendNodeTriangleStart; 874 if (Type.exists(node2)) { 875 this.remove(node2); 876 } 877 } 878 if (ev_la) { 879 node2 = el.rendNodeTriangleEnd; 880 if (!Type.exists(node2)) { 881 node2 = this._createArrowHead(el, "Start", a.typeLast); 882 this.defs.appendChild(node2); 883 el.rendNodeTriangleEnd = node2; 884 el.rendNode.setAttributeNS( 885 null, 886 "marker-end", 887 // "url(#" + this.container.id + "_" + el.id + "TriangleStart)" 888 this.toURL(this.container.id, '_', el.id, 'TriangleStart') 889 ); 890 } else { 891 this.defs.appendChild(node2); 892 } 893 } else { 894 node2 = el.rendNodeTriangleEnd; 895 if (Type.exists(node2)) { 896 this.remove(node2); 897 } 898 } 899 el.visPropOld.firstarrow = ev_fa; 900 el.visPropOld.lastarrow = ev_la; 901 }, 902 903 // Already documented in JXG.AbstractRenderer 904 updateEllipsePrim: function (node, x, y, rx, ry) { 905 var huge = 1000000; 906 907 huge = 200000; // IE 908 // webkit does not like huge values if the object is dashed 909 // iE doesn't like huge values above 216000 910 x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x); 911 y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y); 912 rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx); 913 ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry); 914 915 node.setAttributeNS(null, "cx", x); 916 node.setAttributeNS(null, "cy", y); 917 node.setAttributeNS(null, "rx", Math.abs(rx)); 918 node.setAttributeNS(null, "ry", Math.abs(ry)); 919 }, 920 921 // Already documented in JXG.AbstractRenderer 922 updateLinePrim: function (node, p1x, p1y, p2x, p2y) { 923 var huge = 1000000; 924 925 huge = 200000; //IE 926 if (!isNaN(p1x + p1y + p2x + p2y)) { 927 // webkit does not like huge values if the object is dashed 928 // IE doesn't like huge values above 216000 929 p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x); 930 p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y); 931 p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x); 932 p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y); 933 934 node.setAttributeNS(null, "x1", p1x); 935 node.setAttributeNS(null, "y1", p1y); 936 node.setAttributeNS(null, "x2", p2x); 937 node.setAttributeNS(null, "y2", p2y); 938 } 939 }, 940 941 // Already documented in JXG.AbstractRenderer 942 updatePathPrim: function (node, pointString) { 943 if (pointString === "") { 944 pointString = "M 0 0"; 945 } 946 node.setAttributeNS(null, "d", pointString); 947 }, 948 949 // Already documented in JXG.AbstractRenderer 950 updatePathStringPoint: function (el, size, type) { 951 var s = "", 952 scr = el.coords.scrCoords, 953 sqrt32 = size * Math.sqrt(3) * 0.5, 954 s05 = size * 0.5; 955 956 if (type === "x") { 957 s = 958 " M " + 959 (scr[1] - size) + 960 " " + 961 (scr[2] - size) + 962 " L " + 963 (scr[1] + size) + 964 " " + 965 (scr[2] + size) + 966 " M " + 967 (scr[1] + size) + 968 " " + 969 (scr[2] - size) + 970 " L " + 971 (scr[1] - size) + 972 " " + 973 (scr[2] + size); 974 } else if (type === "+") { 975 s = 976 " M " + 977 (scr[1] - size) + 978 " " + 979 scr[2] + 980 " L " + 981 (scr[1] + size) + 982 " " + 983 scr[2] + 984 " M " + 985 scr[1] + 986 " " + 987 (scr[2] - size) + 988 " L " + 989 scr[1] + 990 " " + 991 (scr[2] + size); 992 } else if (type === "|") { 993 s = 994 " M " + 995 scr[1] + 996 " " + 997 (scr[2] - size) + 998 " L " + 999 scr[1] + 1000 " " + 1001 (scr[2] + size); 1002 } else if (type === "-") { 1003 s = 1004 " M " + 1005 (scr[1] - size) + 1006 " " + 1007 scr[2] + 1008 " L " + 1009 (scr[1] + size) + 1010 " " + 1011 scr[2]; 1012 } else if (type === "<>") { 1013 s = 1014 " M " + 1015 (scr[1] - size) + 1016 " " + 1017 scr[2] + 1018 " L " + 1019 scr[1] + 1020 " " + 1021 (scr[2] + size) + 1022 " L " + 1023 (scr[1] + size) + 1024 " " + 1025 scr[2] + 1026 " L " + 1027 scr[1] + 1028 " " + 1029 (scr[2] - size) + 1030 " Z "; 1031 } else if (type === "^") { 1032 s = 1033 " M " + 1034 scr[1] + 1035 " " + 1036 (scr[2] - size) + 1037 " L " + 1038 (scr[1] - sqrt32) + 1039 " " + 1040 (scr[2] + s05) + 1041 " L " + 1042 (scr[1] + sqrt32) + 1043 " " + 1044 (scr[2] + s05) + 1045 " Z "; // close path 1046 } else if (type === "v") { 1047 s = 1048 " M " + 1049 scr[1] + 1050 " " + 1051 (scr[2] + size) + 1052 " L " + 1053 (scr[1] - sqrt32) + 1054 " " + 1055 (scr[2] - s05) + 1056 " L " + 1057 (scr[1] + sqrt32) + 1058 " " + 1059 (scr[2] - s05) + 1060 " Z "; 1061 } else if (type === ">") { 1062 s = 1063 " M " + 1064 (scr[1] + size) + 1065 " " + 1066 scr[2] + 1067 " L " + 1068 (scr[1] - s05) + 1069 " " + 1070 (scr[2] - sqrt32) + 1071 " L " + 1072 (scr[1] - s05) + 1073 " " + 1074 (scr[2] + sqrt32) + 1075 " Z "; 1076 } else if (type === "<") { 1077 s = 1078 " M " + 1079 (scr[1] - size) + 1080 " " + 1081 scr[2] + 1082 " L " + 1083 (scr[1] + s05) + 1084 " " + 1085 (scr[2] - sqrt32) + 1086 " L " + 1087 (scr[1] + s05) + 1088 " " + 1089 (scr[2] + sqrt32) + 1090 " Z "; 1091 } 1092 return s; 1093 }, 1094 1095 // Already documented in JXG.AbstractRenderer 1096 updatePathStringPrim: function (el) { 1097 var i, 1098 scr, 1099 len, 1100 symbm = " M ", 1101 symbl = " L ", 1102 symbc = " C ", 1103 nextSymb = symbm, 1104 maxSize = 5000.0, 1105 pStr = ""; 1106 1107 if (el.numberPoints <= 0) { 1108 return ""; 1109 } 1110 1111 len = Math.min(el.points.length, el.numberPoints); 1112 1113 if (el.bezierDegree === 1) { 1114 for (i = 0; i < len; i++) { 1115 scr = el.points[i].scrCoords; 1116 if (isNaN(scr[1]) || isNaN(scr[2])) { 1117 // PenUp 1118 nextSymb = symbm; 1119 } else { 1120 // Chrome has problems with values being too far away. 1121 scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize); 1122 scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize); 1123 1124 // Attention: first coordinate may be inaccurate if far way 1125 //pStr += [nextSymb, scr[1], ' ', scr[2]].join(''); 1126 pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox) 1127 nextSymb = symbl; 1128 } 1129 } 1130 } else if (el.bezierDegree === 3) { 1131 i = 0; 1132 while (i < len) { 1133 scr = el.points[i].scrCoords; 1134 if (isNaN(scr[1]) || isNaN(scr[2])) { 1135 // PenUp 1136 nextSymb = symbm; 1137 } else { 1138 pStr += nextSymb + scr[1] + " " + scr[2]; 1139 if (nextSymb === symbc) { 1140 i += 1; 1141 scr = el.points[i].scrCoords; 1142 pStr += " " + scr[1] + " " + scr[2]; 1143 i += 1; 1144 scr = el.points[i].scrCoords; 1145 pStr += " " + scr[1] + " " + scr[2]; 1146 } 1147 nextSymb = symbc; 1148 } 1149 i += 1; 1150 } 1151 } 1152 return pStr; 1153 }, 1154 1155 // Already documented in JXG.AbstractRenderer 1156 updatePathStringBezierPrim: function (el) { 1157 var i, 1158 j, 1159 k, 1160 scr, 1161 lx, 1162 ly, 1163 len, 1164 symbm = " M ", 1165 symbl = " C ", 1166 nextSymb = symbm, 1167 maxSize = 5000.0, 1168 pStr = "", 1169 f = Type.evaluate(el.visProp.strokewidth), 1170 isNoPlot = Type.evaluate(el.visProp.curvetype) !== "plot"; 1171 1172 if (el.numberPoints <= 0) { 1173 return ""; 1174 } 1175 1176 if (isNoPlot && el.board.options.curve.RDPsmoothing) { 1177 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5); 1178 } 1179 1180 len = Math.min(el.points.length, el.numberPoints); 1181 for (j = 1; j < 3; j++) { 1182 nextSymb = symbm; 1183 for (i = 0; i < len; i++) { 1184 scr = el.points[i].scrCoords; 1185 1186 if (isNaN(scr[1]) || isNaN(scr[2])) { 1187 // PenUp 1188 nextSymb = symbm; 1189 } else { 1190 // Chrome has problems with values being too far away. 1191 scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize); 1192 scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize); 1193 1194 // Attention: first coordinate may be inaccurate if far way 1195 if (nextSymb === symbm) { 1196 //pStr += [nextSymb, scr[1], ' ', scr[2]].join(''); 1197 pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox) 1198 } else { 1199 k = 2 * j; 1200 pStr += [ 1201 nextSymb, 1202 lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j), 1203 " ", 1204 ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j), 1205 " ", 1206 lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j), 1207 " ", 1208 ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j), 1209 " ", 1210 scr[1], 1211 " ", 1212 scr[2] 1213 ].join(""); 1214 } 1215 1216 nextSymb = symbl; 1217 lx = scr[1]; 1218 ly = scr[2]; 1219 } 1220 } 1221 } 1222 return pStr; 1223 }, 1224 1225 // Already documented in JXG.AbstractRenderer 1226 updatePolygonPrim: function (node, el) { 1227 var i, 1228 pStr = "", 1229 scrCoords, 1230 len = el.vertices.length; 1231 1232 node.setAttributeNS(null, "stroke", "none"); 1233 node.setAttributeNS(null, "fill-rule", "evenodd"); 1234 if (el.elType === "polygonalchain") { 1235 len++; 1236 } 1237 1238 for (i = 0; i < len - 1; i++) { 1239 if (el.vertices[i].isReal) { 1240 scrCoords = el.vertices[i].coords.scrCoords; 1241 pStr = pStr + scrCoords[1] + "," + scrCoords[2]; 1242 } else { 1243 node.setAttributeNS(null, "points", ""); 1244 return; 1245 } 1246 1247 if (i < len - 2) { 1248 pStr += " "; 1249 } 1250 } 1251 if (pStr.indexOf("NaN") === -1) { 1252 node.setAttributeNS(null, "points", pStr); 1253 } 1254 }, 1255 1256 // Already documented in JXG.AbstractRenderer 1257 updateRectPrim: function (node, x, y, w, h) { 1258 node.setAttributeNS(null, "x", x); 1259 node.setAttributeNS(null, "y", y); 1260 node.setAttributeNS(null, "width", w); 1261 node.setAttributeNS(null, "height", h); 1262 }, 1263 1264 /* ************************** 1265 * Set Attributes 1266 * **************************/ 1267 1268 // documented in JXG.AbstractRenderer 1269 setPropertyPrim: function (node, key, val) { 1270 if (key === "stroked") { 1271 return; 1272 } 1273 node.setAttributeNS(null, key, val); 1274 }, 1275 1276 display: function (el, val) { 1277 var node; 1278 1279 if (el && el.rendNode) { 1280 el.visPropOld.visible = val; 1281 node = el.rendNode; 1282 if (val) { 1283 node.setAttributeNS(null, "display", "inline"); 1284 node.style.visibility = "inherit"; 1285 } else { 1286 node.setAttributeNS(null, "display", "none"); 1287 node.style.visibility = "hidden"; 1288 } 1289 } 1290 }, 1291 1292 // documented in JXG.AbstractRenderer 1293 show: function (el) { 1294 JXG.deprecated("Board.renderer.show()", "Board.renderer.display()"); 1295 this.display(el, true); 1296 // var node; 1297 // 1298 // if (el && el.rendNode) { 1299 // node = el.rendNode; 1300 // node.setAttributeNS(null, 'display', 'inline'); 1301 // node.style.visibility = "inherit"; 1302 // } 1303 }, 1304 1305 // documented in JXG.AbstractRenderer 1306 hide: function (el) { 1307 JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()"); 1308 this.display(el, false); 1309 // var node; 1310 // 1311 // if (el && el.rendNode) { 1312 // node = el.rendNode; 1313 // node.setAttributeNS(null, 'display', 'none'); 1314 // node.style.visibility = "hidden"; 1315 // } 1316 }, 1317 1318 // documented in JXG.AbstractRenderer 1319 setBuffering: function (el, type) { 1320 el.rendNode.setAttribute("buffered-rendering", type); 1321 }, 1322 1323 // documented in JXG.AbstractRenderer 1324 setDashStyle: function (el) { 1325 var dashStyle = Type.evaluate(el.visProp.dash), 1326 ds = Type.evaluate(el.visProp.dashscale), 1327 sw = ds ? 0.5 * Type.evaluate(el.visProp.strokewidth) : 1, 1328 node = el.rendNode; 1329 1330 if (dashStyle > 0) { 1331 node.setAttributeNS(null, "stroke-dasharray", 1332 // sw could distinguish highlighting or not. 1333 // But it seems to preferable to ignore this. 1334 this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',') 1335 ); 1336 } else { 1337 if (node.hasAttributeNS(null, "stroke-dasharray")) { 1338 node.removeAttributeNS(null, "stroke-dasharray"); 1339 } 1340 } 1341 }, 1342 1343 // documented in JXG.AbstractRenderer 1344 setGradient: function (el) { 1345 var fillNode = el.rendNode, 1346 node, node2, node3, 1347 ev_g = Type.evaluate(el.visProp.gradient); 1348 1349 if (ev_g === "linear" || ev_g === "radial") { 1350 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient"); 1351 node2 = this.createPrim("stop", el.id + "_gradient1"); 1352 node3 = this.createPrim("stop", el.id + "_gradient2"); 1353 node.appendChild(node2); 1354 node.appendChild(node3); 1355 this.defs.appendChild(node); 1356 fillNode.setAttributeNS( 1357 null, 1358 'style', 1359 // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)" 1360 'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient') 1361 ); 1362 el.gradNode1 = node2; 1363 el.gradNode2 = node3; 1364 el.gradNode = node; 1365 } else { 1366 fillNode.removeAttributeNS(null, "style"); 1367 } 1368 }, 1369 1370 /** 1371 * Set the gradient angle for linear color gradients. 1372 * 1373 * @private 1374 * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element. 1375 * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom. 1376 */ 1377 updateGradientAngle: function (node, radians) { 1378 // Angles: 1379 // 0: -> 1380 // 90: down 1381 // 180: <- 1382 // 90: up 1383 var f = 1.0, 1384 co = Math.cos(radians), 1385 si = Math.sin(radians); 1386 1387 if (Math.abs(co) > Math.abs(si)) { 1388 f /= Math.abs(co); 1389 } else { 1390 f /= Math.abs(si); 1391 } 1392 1393 if (co >= 0) { 1394 node.setAttributeNS(null, "x1", 0); 1395 node.setAttributeNS(null, "x2", co * f); 1396 } else { 1397 node.setAttributeNS(null, "x1", -co * f); 1398 node.setAttributeNS(null, "x2", 0); 1399 } 1400 if (si >= 0) { 1401 node.setAttributeNS(null, "y1", 0); 1402 node.setAttributeNS(null, "y2", si * f); 1403 } else { 1404 node.setAttributeNS(null, "y1", -si * f); 1405 node.setAttributeNS(null, "y2", 0); 1406 } 1407 }, 1408 1409 /** 1410 * Set circles for radial color gradients. 1411 * 1412 * @private 1413 * @param {SVGnode} node SVG gradient node 1414 * @param {Number} cx SVG value cx (value between 0 and 1) 1415 * @param {Number} cy SVG value cy (value between 0 and 1) 1416 * @param {Number} r SVG value r (value between 0 and 1) 1417 * @param {Number} fx SVG value fx (value between 0 and 1) 1418 * @param {Number} fy SVG value fy (value between 0 and 1) 1419 * @param {Number} fr SVG value fr (value between 0 and 1) 1420 */ 1421 updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) { 1422 node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color 1423 node.setAttributeNS(null, "cy", cy * 100 + "%"); 1424 node.setAttributeNS(null, "r", r * 100 + "%"); 1425 node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point 1426 node.setAttributeNS(null, "fy", fy * 100 + "%"); 1427 node.setAttributeNS(null, "fr", fr * 100 + "%"); 1428 }, 1429 1430 // documented in JXG.AbstractRenderer 1431 updateGradient: function (el) { 1432 var col, 1433 op, 1434 node2 = el.gradNode1, 1435 node3 = el.gradNode2, 1436 ev_g = Type.evaluate(el.visProp.gradient); 1437 1438 if (!Type.exists(node2) || !Type.exists(node3)) { 1439 return; 1440 } 1441 1442 op = Type.evaluate(el.visProp.fillopacity); 1443 op = op > 0 ? op : 0; 1444 col = Type.evaluate(el.visProp.fillcolor); 1445 1446 node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op); 1447 node3.setAttributeNS( 1448 null, 1449 "style", 1450 "stop-color:" + 1451 Type.evaluate(el.visProp.gradientsecondcolor) + 1452 ";stop-opacity:" + 1453 Type.evaluate(el.visProp.gradientsecondopacity) 1454 ); 1455 node2.setAttributeNS( 1456 null, 1457 "offset", 1458 Type.evaluate(el.visProp.gradientstartoffset) * 100 + "%" 1459 ); 1460 node3.setAttributeNS( 1461 null, 1462 "offset", 1463 Type.evaluate(el.visProp.gradientendoffset) * 100 + "%" 1464 ); 1465 if (ev_g === "linear") { 1466 this.updateGradientAngle(el.gradNode, Type.evaluate(el.visProp.gradientangle)); 1467 } else if (ev_g === "radial") { 1468 this.updateGradientCircle( 1469 el.gradNode, 1470 Type.evaluate(el.visProp.gradientcx), 1471 Type.evaluate(el.visProp.gradientcy), 1472 Type.evaluate(el.visProp.gradientr), 1473 Type.evaluate(el.visProp.gradientfx), 1474 Type.evaluate(el.visProp.gradientfy), 1475 Type.evaluate(el.visProp.gradientfr) 1476 ); 1477 } 1478 }, 1479 1480 // documented in JXG.AbstractRenderer 1481 setObjectTransition: function (el, duration) { 1482 var node, props, 1483 transitionArr = [], 1484 transitionStr, 1485 i, 1486 len = 0, 1487 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"]; 1488 1489 if (duration === undefined) { 1490 duration = Type.evaluate(el.visProp.transitionduration); 1491 } 1492 1493 props = Type.evaluate(el.visProp.transitionproperties); 1494 if (duration === el.visPropOld.transitionduration && 1495 props === el.visPropOld.transitionproperties) { 1496 return; 1497 } 1498 1499 // if ( 1500 // el.elementClass === Const.OBJECT_CLASS_TEXT && 1501 // Type.evaluate(el.visProp.display) === "html" 1502 // ) { 1503 // // transitionStr = " color " + duration + "ms," + 1504 // // " opacity " + duration + "ms"; 1505 // transitionStr = " all " + duration + "ms ease"; 1506 // } else { 1507 // transitionStr = 1508 // " fill " + duration + "ms," + 1509 // " fill-opacity " + duration + "ms," + 1510 // " stroke " + duration + "ms," + 1511 // " stroke-opacity " + duration + "ms," + 1512 // " stroke-width " + duration + "ms," + 1513 // " width " + duration + "ms," + 1514 // " height " + duration + "ms," + 1515 // " rx " + duration + "ms," + 1516 // " ry " + duration + "ms"; 1517 // } 1518 1519 if (Type.exists(props)) { 1520 len = props.length; 1521 } 1522 for (i = 0; i < len; i++) { 1523 transitionArr.push(props[i] + ' ' + duration + 'ms'); 1524 } 1525 transitionStr = transitionArr.join(', '); 1526 1527 len = nodes.length; 1528 for (i = 0; i < len; ++i) { 1529 if (el[nodes[i]]) { 1530 node = el[nodes[i]]; 1531 node.style.transition = transitionStr; 1532 } 1533 } 1534 1535 el.visPropOld.transitionduration = duration; 1536 el.visPropOld.transitionproperties = props; 1537 }, 1538 1539 /** 1540 * Call user-defined function to set visual attributes. 1541 * If "testAttribute" is the empty string, the function 1542 * is called immediately, otherwise it is called in a timeOut. 1543 * 1544 * This is necessary to realize smooth transitions but avoid transitions 1545 * when first creating the objects. 1546 * 1547 * Usually, the string in testAttribute is the visPropOld attribute 1548 * of the values which are set. 1549 * 1550 * @param {Function} setFunc Some function which usually sets some attributes 1551 * @param {String} testAttribute If this string is the empty string the function is called immediately, 1552 * otherwise it is called in a setImeout. 1553 * @see JXG.SVGRenderer#setObjectFillColor 1554 * @see JXG.SVGRenderer#setObjectStrokeColor 1555 * @see JXG.SVGRenderer#_setArrowColor 1556 * @private 1557 */ 1558 _setAttribute: function (setFunc, testAttribute) { 1559 if (testAttribute === "") { 1560 setFunc(); 1561 } else { 1562 window.setTimeout(setFunc, 1); 1563 } 1564 }, 1565 1566 // documented in JXG.AbstractRenderer 1567 setObjectFillColor: function (el, color, opacity, rendNode) { 1568 var node, c, rgbo, oo, 1569 rgba = Type.evaluate(color), 1570 o = Type.evaluate(opacity), 1571 grad = Type.evaluate(el.visProp.gradient); 1572 1573 o = o > 0 ? o : 0; 1574 1575 // TODO save gradient and gradientangle 1576 if ( 1577 el.visPropOld.fillcolor === rgba && 1578 el.visPropOld.fillopacity === o && 1579 grad === null 1580 ) { 1581 return; 1582 } 1583 1584 if (Type.exists(rgba) && rgba !== false) { 1585 if (rgba.length !== 9) { 1586 // RGB, not RGBA 1587 c = rgba; 1588 oo = o; 1589 } else { 1590 // True RGBA, not RGB 1591 rgbo = Color.rgba2rgbo(rgba); 1592 c = rgbo[0]; 1593 oo = o * rgbo[1]; 1594 } 1595 1596 if (rendNode === undefined) { 1597 node = el.rendNode; 1598 } else { 1599 node = rendNode; 1600 } 1601 1602 if (c !== "none") { 1603 this._setAttribute(function () { 1604 node.setAttributeNS(null, "fill", c); 1605 }, el.visPropOld.fillcolor); 1606 } 1607 1608 if (el.type === JXG.OBJECT_TYPE_IMAGE) { 1609 this._setAttribute(function () { 1610 node.setAttributeNS(null, "opacity", oo); 1611 }, el.visPropOld.fillopacity); 1612 //node.style['opacity'] = oo; // This would overwrite values set by CSS class. 1613 } else { 1614 if (c === "none") { 1615 // This is done only for non-images 1616 // because images have no fill color. 1617 oo = 0; 1618 // This is necessary if there is a foreignObject below. 1619 node.setAttributeNS(null, "pointer-events", "visibleStroke"); 1620 } else { 1621 // This is the default 1622 node.setAttributeNS(null, "pointer-events", "visiblePainted"); 1623 } 1624 this._setAttribute(function () { 1625 node.setAttributeNS(null, "fill-opacity", oo); 1626 }, el.visPropOld.fillopacity); 1627 } 1628 1629 if (grad === "linear" || grad === "radial") { 1630 this.updateGradient(el); 1631 } 1632 } 1633 el.visPropOld.fillcolor = rgba; 1634 el.visPropOld.fillopacity = o; 1635 }, 1636 1637 // documented in JXG.AbstractRenderer 1638 setObjectStrokeColor: function (el, color, opacity) { 1639 var rgba = Type.evaluate(color), 1640 c, rgbo, 1641 o = Type.evaluate(opacity), 1642 oo, node; 1643 1644 o = o > 0 ? o : 0; 1645 1646 if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) { 1647 return; 1648 } 1649 1650 if (Type.exists(rgba) && rgba !== false) { 1651 if (rgba.length !== 9) { 1652 // RGB, not RGBA 1653 c = rgba; 1654 oo = o; 1655 } else { 1656 // True RGBA, not RGB 1657 rgbo = Color.rgba2rgbo(rgba); 1658 c = rgbo[0]; 1659 oo = o * rgbo[1]; 1660 } 1661 1662 node = el.rendNode; 1663 1664 if (el.elementClass === Const.OBJECT_CLASS_TEXT) { 1665 if (Type.evaluate(el.visProp.display) === "html") { 1666 this._setAttribute(function () { 1667 node.style.color = c; 1668 node.style.opacity = oo; 1669 }, el.visPropOld.strokecolor); 1670 } else { 1671 this._setAttribute(function () { 1672 node.setAttributeNS(null, "style", "fill:" + c); 1673 node.setAttributeNS(null, "style", "fill-opacity:" + oo); 1674 }, el.visPropOld.strokecolor); 1675 } 1676 } else { 1677 this._setAttribute(function () { 1678 node.setAttributeNS(null, "stroke", c); 1679 node.setAttributeNS(null, "stroke-opacity", oo); 1680 }, el.visPropOld.strokecolor); 1681 } 1682 1683 if ( 1684 el.elementClass === Const.OBJECT_CLASS_CURVE || 1685 el.elementClass === Const.OBJECT_CLASS_LINE 1686 ) { 1687 if (Type.evaluate(el.visProp.firstarrow)) { 1688 this._setArrowColor( 1689 el.rendNodeTriangleStart, 1690 c, oo, el, 1691 el.visPropCalc.typeFirst 1692 ); 1693 } 1694 1695 if (Type.evaluate(el.visProp.lastarrow)) { 1696 this._setArrowColor( 1697 el.rendNodeTriangleEnd, 1698 c, oo, el, 1699 el.visPropCalc.typeLast 1700 ); 1701 } 1702 } 1703 } 1704 1705 el.visPropOld.strokecolor = rgba; 1706 el.visPropOld.strokeopacity = o; 1707 }, 1708 1709 // documented in JXG.AbstractRenderer 1710 setObjectStrokeWidth: function (el, width) { 1711 var node, 1712 w = Type.evaluate(width); 1713 1714 if (isNaN(w) || el.visPropOld.strokewidth === w) { 1715 return; 1716 } 1717 1718 node = el.rendNode; 1719 this.setPropertyPrim(node, "stroked", "true"); 1720 if (Type.exists(w)) { 1721 this.setPropertyPrim(node, "stroke-width", w + "px"); 1722 1723 // if (el.elementClass === Const.OBJECT_CLASS_CURVE || 1724 // el.elementClass === Const.OBJECT_CLASS_LINE) { 1725 // if (Type.evaluate(el.visProp.firstarrow)) { 1726 // this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode); 1727 // } 1728 // 1729 // if (Type.evaluate(el.visProp.lastarrow)) { 1730 // this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode); 1731 // } 1732 // } 1733 } 1734 el.visPropOld.strokewidth = w; 1735 }, 1736 1737 // documented in JXG.AbstractRenderer 1738 setLineCap: function (el) { 1739 var capStyle = Type.evaluate(el.visProp.linecap); 1740 1741 if ( 1742 capStyle === undefined || 1743 capStyle === "" || 1744 el.visPropOld.linecap === capStyle || 1745 !Type.exists(el.rendNode) 1746 ) { 1747 return; 1748 } 1749 1750 this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle); 1751 el.visPropOld.linecap = capStyle; 1752 }, 1753 1754 // documented in JXG.AbstractRenderer 1755 setShadow: function (el) { 1756 var ev_s = Type.evaluate(el.visProp.shadow), 1757 ev_s_json, c, b, bl, o, op, id, node, 1758 use_board_filter = true, 1759 show = false; 1760 1761 ev_s_json = JSON.stringify(ev_s); 1762 if (ev_s_json === el.visPropOld.shadow) { 1763 return; 1764 } 1765 1766 if (typeof ev_s === 'boolean') { 1767 use_board_filter = true; 1768 show = ev_s; 1769 c = 'none'; 1770 b = 3; 1771 bl = 0.1; 1772 o = [5, 5]; 1773 op = 1; 1774 } else { 1775 if (Type.evaluate(ev_s.enabled)) { 1776 use_board_filter = false; 1777 show = true; 1778 c = JXG.rgbParser(Type.evaluate(ev_s.color)); 1779 b = Type.evaluate(ev_s.blur); 1780 bl = Type.evaluate(ev_s.blend); 1781 o = Type.evaluate(ev_s.offset); 1782 op = Type.evaluate(ev_s.opacity); 1783 } else { 1784 show = false; 1785 } 1786 } 1787 1788 if (Type.exists(el.rendNode)) { 1789 if (show) { 1790 if (use_board_filter) { 1791 el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1')) 1792 // 'url(#' + this.container.id + '_' + 'f1)'); 1793 } else { 1794 node = this.container.ownerDocument.getElementById(id); 1795 if (node) { 1796 this.defs.removeChild(node); 1797 } 1798 id = el.rendNode.id + '_' + 'f1'; 1799 this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o)); 1800 el.rendNode.setAttributeNS(null, 'filter', this.toURL(id)); 1801 // 'url(#' + id + ')'); 1802 } 1803 } else { 1804 el.rendNode.removeAttributeNS(null, 'filter'); 1805 } 1806 } 1807 1808 el.visPropOld.shadow = ev_s_json; 1809 }, 1810 1811 /* ************************** 1812 * renderer control 1813 * **************************/ 1814 1815 // documented in JXG.AbstractRenderer 1816 suspendRedraw: function () { 1817 // It seems to be important for the Linux version of firefox 1818 //this.suspendHandle = this.svgRoot.suspendRedraw(10000); 1819 }, 1820 1821 // documented in JXG.AbstractRenderer 1822 unsuspendRedraw: function () { 1823 //this.svgRoot.unsuspendRedraw(this.suspendHandle); 1824 //this.svgRoot.unsuspendRedrawAll(); 1825 //this.svgRoot.forceRedraw(); 1826 }, 1827 1828 // documented in AbstractRenderer 1829 resize: function (w, h) { 1830 // this.svgRoot.style.width = parseFloat(w) + 'px'; 1831 // this.svgRoot.style.height = parseFloat(h) + 'px'; 1832 1833 this.svgRoot.setAttribute("width", parseFloat(w)); 1834 this.svgRoot.setAttribute("height", parseFloat(h)); 1835 // this.svgRoot.setAttribute('width', '100%'); 1836 // this.svgRoot.setAttribute('height', '100%'); 1837 }, 1838 1839 // documented in JXG.AbstractRenderer 1840 createTouchpoints: function (n) { 1841 var i, na1, na2, node; 1842 this.touchpoints = []; 1843 for (i = 0; i < n; i++) { 1844 na1 = "touchpoint1_" + i; 1845 node = this.createPrim("path", na1); 1846 this.appendChildPrim(node, 19); 1847 node.setAttributeNS(null, "d", "M 0 0"); 1848 this.touchpoints.push(node); 1849 1850 this.setPropertyPrim(node, "stroked", "true"); 1851 this.setPropertyPrim(node, "stroke-width", "1px"); 1852 node.setAttributeNS(null, "stroke", "#000000"); 1853 node.setAttributeNS(null, "stroke-opacity", 1.0); 1854 node.setAttributeNS(null, "display", "none"); 1855 1856 na2 = "touchpoint2_" + i; 1857 node = this.createPrim("ellipse", na2); 1858 this.appendChildPrim(node, 19); 1859 this.updateEllipsePrim(node, 0, 0, 0, 0); 1860 this.touchpoints.push(node); 1861 1862 this.setPropertyPrim(node, "stroked", "true"); 1863 this.setPropertyPrim(node, "stroke-width", "1px"); 1864 node.setAttributeNS(null, "stroke", "#000000"); 1865 node.setAttributeNS(null, "stroke-opacity", 1.0); 1866 node.setAttributeNS(null, "fill", "#ffffff"); 1867 node.setAttributeNS(null, "fill-opacity", 0.0); 1868 1869 node.setAttributeNS(null, "display", "none"); 1870 } 1871 }, 1872 1873 // documented in JXG.AbstractRenderer 1874 showTouchpoint: function (i) { 1875 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1876 this.touchpoints[2 * i].setAttributeNS(null, "display", "inline"); 1877 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "inline"); 1878 } 1879 }, 1880 1881 // documented in JXG.AbstractRenderer 1882 hideTouchpoint: function (i) { 1883 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1884 this.touchpoints[2 * i].setAttributeNS(null, "display", "none"); 1885 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "none"); 1886 } 1887 }, 1888 1889 // documented in JXG.AbstractRenderer 1890 updateTouchpoint: function (i, pos) { 1891 var x, 1892 y, 1893 d = 37; 1894 1895 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1896 x = pos[0]; 1897 y = pos[1]; 1898 1899 this.touchpoints[2 * i].setAttributeNS( 1900 null, 1901 "d", 1902 "M " + 1903 (x - d) + 1904 " " + 1905 y + 1906 " " + 1907 "L " + 1908 (x + d) + 1909 " " + 1910 y + 1911 " " + 1912 "M " + 1913 x + 1914 " " + 1915 (y - d) + 1916 " " + 1917 "L " + 1918 x + 1919 " " + 1920 (y + d) 1921 ); 1922 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25); 1923 } 1924 }, 1925 1926 /** 1927 * Walk recursively through the DOM subtree of a node and collect all 1928 * value attributes together with the id of that node. 1929 * <b>Attention:</b> Only values of nodes having a valid id are taken. 1930 * @param {Node} node root node of DOM subtree that will be searched recursively. 1931 * @return {Array} Array with entries of the form [id, value] 1932 * @private 1933 */ 1934 _getValuesOfDOMElements: function (node) { 1935 var values = []; 1936 if (node.nodeType === 1) { 1937 node = node.firstChild; 1938 while (node) { 1939 if (node.id !== undefined && node.value !== undefined) { 1940 values.push([node.id, node.value]); 1941 } 1942 values = values.concat(this._getValuesOfDOMElements(node)); 1943 node = node.nextSibling; 1944 } 1945 } 1946 return values; 1947 }, 1948 1949 _getDataUri: function (url, callback) { 1950 var image = new Image(); 1951 1952 image.onload = function () { 1953 var canvas = document.createElement("canvas"); 1954 canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size 1955 canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size 1956 1957 canvas.getContext("2d").drawImage(this, 0, 0); 1958 1959 callback(canvas.toDataURL("image/png")); 1960 canvas.remove(); 1961 }; 1962 1963 image.src = url; 1964 }, 1965 1966 _getImgDataURL: function (svgRoot) { 1967 var images, len, canvas, ctx, ur, i; 1968 1969 images = svgRoot.getElementsByTagName("image"); 1970 len = images.length; 1971 if (len > 0) { 1972 canvas = document.createElement("canvas"); 1973 //img = new Image(); 1974 for (i = 0; i < len; i++) { 1975 images[i].setAttribute("crossorigin", "anonymous"); 1976 //img.src = images[i].href; 1977 //img.onload = function() { 1978 // img.crossOrigin = "anonymous"; 1979 ctx = canvas.getContext("2d"); 1980 canvas.width = images[i].getAttribute("width"); 1981 canvas.height = images[i].getAttribute("height"); 1982 try { 1983 ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height); 1984 1985 // If the image is not png, the format must be specified here 1986 ur = canvas.toDataURL(); 1987 images[i].setAttribute("xlink:href", ur); 1988 } catch (err) { 1989 console.log("CORS problem! Image can not be used", err); 1990 } 1991 } 1992 //canvas.remove(); 1993 } 1994 return true; 1995 }, 1996 1997 /** 1998 * Return a data URI of the SVG code representing the construction. 1999 * The SVG code of the construction is base64 encoded. The return string starts 2000 * with "data:image/svg+xml;base64,...". 2001 * 2002 * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none. 2003 * This is necessary for older versions of Safari. Default: false 2004 * @returns {String} data URI string 2005 * 2006 * @example 2007 * var A = board.create('point', [2, 2]); 2008 * 2009 * var txt = board.renderer.dumpToDataURI(false); 2010 * // txt consists of a string of the form 2011 * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+ 2012 * // Behind the comma, there is the base64 encoded SVG code 2013 * // which is decoded with atob(). 2014 * // The call of decodeURIComponent(escape(...)) is necessary 2015 * // to handle unicode strings correctly. 2016 * var ar = txt.split(','); 2017 * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1]))); 2018 * 2019 * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div> 2020 * <textarea id="output2023" rows="5" cols="50"></textarea> 2021 * <script type="text/javascript"> 2022 * (function() { 2023 * var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d', 2024 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2025 * var A = board.create('point', [2, 2]); 2026 * 2027 * var txt = board.renderer.dumpToDataURI(false); 2028 * // txt consists of a string of the form 2029 * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+ 2030 * // Behind the comma, there is the base64 encoded SVG code 2031 * // which is decoded with atob(). 2032 * // The call of decodeURIComponent(escape(...)) is necessary 2033 * // to handle unicode strings correctly. 2034 * var ar = txt.split(','); 2035 * document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1]))); 2036 * 2037 * })(); 2038 * 2039 * </script><pre> 2040 * 2041 */ 2042 dumpToDataURI: function (ignoreTexts) { 2043 var svgRoot = this.svgRoot, 2044 btoa = window.btoa || Base64.encode, 2045 svg, i, len, 2046 values = []; 2047 2048 // Move all HTML tags (beside the SVG root) of the container 2049 // to the foreignObject element inside of the svgRoot node 2050 // Problem: 2051 // input values are not copied. This can be verified by looking at an innerHTML output 2052 // of an input element. Therefore, we do it "by hand". 2053 if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) { 2054 if (!ignoreTexts) { 2055 this.foreignObjLayer.setAttribute("display", "inline"); 2056 while (svgRoot.nextSibling) { 2057 // Copy all value attributes 2058 values = values.concat(this._getValuesOfDOMElements(svgRoot.nextSibling)); 2059 this.foreignObjLayer.appendChild(svgRoot.nextSibling); 2060 } 2061 } 2062 } 2063 2064 this._getImgDataURL(svgRoot); 2065 2066 // Convert the SVG graphic into a string containing SVG code 2067 svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 2068 svg = new XMLSerializer().serializeToString(svgRoot); 2069 2070 if (ignoreTexts !== true) { 2071 // Handle SVG texts 2072 // Insert all value attributes back into the svg string 2073 len = values.length; 2074 for (i = 0; i < len; i++) { 2075 svg = svg.replace( 2076 'id="' + values[i][0] + '"', 2077 'id="' + values[i][0] + '" value="' + values[i][1] + '"' 2078 ); 2079 } 2080 } 2081 2082 // if (false) { 2083 // // Debug: use example svg image 2084 // svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="220" height="220"><rect width="66" height="30" x="21" y="32" stroke="#204a87" stroke-width="2" fill="none" /></svg>'; 2085 // } 2086 2087 // In IE we have to remove the namespace again. 2088 if ((svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1) { 2089 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, ""); 2090 } 2091 2092 // Safari fails if the svg string contains a " " 2093 // Obsolete with Safari 12+ 2094 svg = svg.replace(/ /g, " "); 2095 svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); 2096 2097 // Move all HTML tags back from 2098 // the foreignObject element to the container 2099 if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) { 2100 // Restore all HTML elements 2101 while (this.foreignObjLayer.firstChild) { 2102 this.container.appendChild(this.foreignObjLayer.firstChild); 2103 } 2104 this.foreignObjLayer.setAttribute("display", "none"); 2105 } 2106 2107 return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); 2108 }, 2109 2110 /** 2111 * Convert the SVG construction into an HTML canvas image. 2112 * This works for all SVG supporting browsers. Implemented as Promise. 2113 * <p> 2114 * Might fail if any text element or foreign object element contains SVG. This 2115 * is the case e.g. for the default fullscreen symbol. 2116 * <p> 2117 * For IE, it is realized as function. 2118 * It works from version 9, with the exception that HTML texts 2119 * are ignored on IE. The drawing is done with a delay of 2120 * 200 ms. Otherwise there would be problems with IE. 2121 * 2122 * @param {String} canvasId Id of an HTML canvas element 2123 * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag. 2124 * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag. 2125 * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root. 2126 * This is necessary for older versions of Safari. Default: false 2127 * @returns {Promise} Promise object 2128 * 2129 * @example 2130 * board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); }); 2131 * 2132 * @example 2133 * // IE 11 example: 2134 * board.renderer.dumpToCanvas('canvas'); 2135 * setTimeout(function() { console.log('done'); }, 400); 2136 */ 2137 dumpToCanvas: function (canvasId, w, h, ignoreTexts) { 2138 var svg, tmpImg, 2139 cv, ctx, 2140 doc = this.container.ownerDocument; 2141 2142 // Prepare the canvas element 2143 cv = doc.getElementById(canvasId); 2144 2145 // Clear the canvas 2146 /* eslint-disable no-self-assign */ 2147 cv.width = cv.width; 2148 /* eslint-enable no-self-assign */ 2149 2150 ctx = cv.getContext("2d"); 2151 if (w !== undefined && h !== undefined) { 2152 cv.style.width = parseFloat(w) + "px"; 2153 cv.style.height = parseFloat(h) + "px"; 2154 // Scale twice the CSS size to make the image crisp 2155 // cv.setAttribute('width', 2 * parseFloat(wOrg)); 2156 // cv.setAttribute('height', 2 * parseFloat(hOrg)); 2157 // ctx.scale(2 * wOrg / w, 2 * hOrg / h); 2158 cv.setAttribute("width", parseFloat(w)); 2159 cv.setAttribute("height", parseFloat(h)); 2160 } 2161 2162 // Display the SVG string as data-uri in an HTML img. 2163 tmpImg = new Image(); 2164 svg = this.dumpToDataURI(ignoreTexts); 2165 tmpImg.src = svg; 2166 2167 // Finally, draw the HTML img in the canvas. 2168 if (!("Promise" in window)) { 2169 tmpImg.onload = function () { 2170 // IE needs a pause... 2171 // Seems to be broken 2172 window.setTimeout(function () { 2173 try { 2174 ctx.drawImage(tmpImg, 0, 0, w, h); 2175 } catch (err) { 2176 console.log("screenshots not longer supported on IE"); 2177 } 2178 }, 200); 2179 }; 2180 return this; 2181 } 2182 2183 return new Promise(function (resolve, reject) { 2184 try { 2185 tmpImg.onload = function () { 2186 ctx.drawImage(tmpImg, 0, 0, w, h); 2187 resolve(); 2188 }; 2189 } catch (e) { 2190 reject(e); 2191 } 2192 }); 2193 }, 2194 2195 /** 2196 * Display SVG image in html img-tag which enables 2197 * easy download for the user. 2198 * 2199 * Support: 2200 * <ul> 2201 * <li> IE: No 2202 * <li> Edge: full 2203 * <li> Firefox: full 2204 * <li> Chrome: full 2205 * <li> Safari: full (No text support in versions prior to 12). 2206 * </ul> 2207 * 2208 * @param {JXG.Board} board Link to the board. 2209 * @param {String} imgId Optional id of an img object. If given and different from the empty string, 2210 * the screenshot is copied to this img object. The width and height will be set to the values of the 2211 * JSXGraph container. 2212 * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the 2213 * SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false 2214 * @return {Object} the svg renderer object 2215 */ 2216 screenshot: function (board, imgId, ignoreTexts) { 2217 var node, 2218 doc = this.container.ownerDocument, 2219 parent = this.container.parentNode, 2220 // cPos, 2221 // cssTxt, 2222 canvas, id, img, 2223 button, buttonText, 2224 w, h, 2225 bas = board.attr.screenshot, 2226 navbar, navbarDisplay, insert, 2227 newImg = false, 2228 _copyCanvasToImg, 2229 isDebug = false; 2230 2231 if (this.type === "no") { 2232 return this; 2233 } 2234 2235 w = bas.scale * this.container.getBoundingClientRect().width; 2236 h = bas.scale * this.container.getBoundingClientRect().height; 2237 2238 if (imgId === undefined || imgId === "") { 2239 newImg = true; 2240 img = new Image(); //doc.createElement('img'); 2241 img.style.width = w + "px"; 2242 img.style.height = h + "px"; 2243 } else { 2244 newImg = false; 2245 img = doc.getElementById(imgId); 2246 } 2247 // img.crossOrigin = 'anonymous'; 2248 2249 // Create div which contains canvas element and close button 2250 if (newImg) { 2251 node = doc.createElement("div"); 2252 node.style.cssText = bas.css; 2253 node.style.width = w + "px"; 2254 node.style.height = h + "px"; 2255 node.style.zIndex = this.container.style.zIndex + 120; 2256 2257 // Try to position the div exactly over the JSXGraph board 2258 node.style.position = "absolute"; 2259 node.style.top = this.container.offsetTop + "px"; 2260 node.style.left = this.container.offsetLeft + "px"; 2261 } 2262 2263 if (!isDebug) { 2264 // Create canvas element and add it to the DOM 2265 // It will be removed after the image has been stored. 2266 canvas = doc.createElement("canvas"); 2267 id = Math.random().toString(36).substr(2, 5); 2268 canvas.setAttribute("id", id); 2269 canvas.setAttribute("width", w); 2270 canvas.setAttribute("height", h); 2271 canvas.style.width = w + "px"; 2272 canvas.style.height = w + "px"; 2273 canvas.style.display = "none"; 2274 parent.appendChild(canvas); 2275 } else { 2276 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html 2277 id = "jxgbox_canvas"; 2278 // canvas = document.getElementById(id); 2279 canvas = doc.getElementById(id); 2280 } 2281 2282 if (newImg) { 2283 // Create close button 2284 button = doc.createElement("span"); 2285 buttonText = doc.createTextNode("\u2716"); 2286 button.style.cssText = bas.cssButton; 2287 button.appendChild(buttonText); 2288 button.onclick = function () { 2289 node.parentNode.removeChild(node); 2290 }; 2291 2292 // Add all nodes 2293 node.appendChild(img); 2294 node.appendChild(button); 2295 parent.insertBefore(node, this.container.nextSibling); 2296 } 2297 2298 // Hide navigation bar in board 2299 navbar = doc.getElementById(this.uniqName('navigationbar')); 2300 if (Type.exists(navbar)) { 2301 navbarDisplay = navbar.style.display; 2302 navbar.style.display = "none"; 2303 insert = this.removeToInsertLater(navbar); 2304 } 2305 2306 _copyCanvasToImg = function () { 2307 // Show image in img tag 2308 img.src = canvas.toDataURL("image/png"); 2309 2310 // Remove canvas node 2311 if (!isDebug) { 2312 parent.removeChild(canvas); 2313 } 2314 }; 2315 2316 // Create screenshot in image element 2317 if ("Promise" in window) { 2318 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg); 2319 } else { 2320 // IE 2321 this.dumpToCanvas(id, w, h, ignoreTexts); 2322 window.setTimeout(_copyCanvasToImg, 200); 2323 } 2324 2325 // Reinsert navigation bar in board 2326 if (Type.exists(navbar)) { 2327 navbar.style.display = navbarDisplay; 2328 insert(); 2329 } 2330 2331 return this; 2332 } 2333 } 2334 ); 2335 2336 export default JXG.SVGRenderer; 2337