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