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