1 /* 2 Copyright 2008-2025 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 if (el.evalVisProp('evaluateOnlyOnce') !== true || !el.renderedOnce) { 809 el.rendNode.innerHTML = el.content; 810 el.renderedOnce = true; 811 } 812 this._updateVisual(el, { stroke: true, dash: true }, true); 813 }, 814 815 /* ********* Render primitive objects *********** */ 816 817 // Already documented in JXG.AbstractRenderer 818 appendChildPrim: function (node, level) { 819 if (!Type.exists(level)) { 820 // trace nodes have level not set 821 level = 0; 822 } else if (level >= Options.layer.numlayers) { 823 level = Options.layer.numlayers - 1; 824 } 825 this.layer[level].appendChild(node); 826 827 return node; 828 }, 829 830 // Already documented in JXG.AbstractRenderer 831 createPrim: function (type, id) { 832 var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type); 833 node.setAttributeNS(null, "id", this.uniqName(id)); 834 node.style.position = "absolute"; 835 if (type === "path") { 836 node.setAttributeNS(null, "stroke-linecap", "round"); 837 node.setAttributeNS(null, "stroke-linejoin", "round"); 838 node.setAttributeNS(null, "fill-rule", "evenodd"); 839 } 840 841 return node; 842 }, 843 844 // Already documented in JXG.AbstractRenderer 845 remove: function (shape) { 846 if (Type.exists(shape) && Type.exists(shape.parentNode)) { 847 shape.parentNode.removeChild(shape); 848 } 849 }, 850 851 // Already documented in JXG.AbstractRenderer 852 setLayer: function (el, level) { 853 if (!Type.exists(level)) { 854 level = 0; 855 } else if (level >= Options.layer.numlayers) { 856 level = Options.layer.numlayers - 1; 857 } 858 859 this.layer[level].appendChild(el.rendNode); 860 }, 861 862 // Already documented in JXG.AbstractRenderer 863 makeArrows: function (el, a) { 864 var node2, str, 865 ev_fa = a.evFirst, 866 ev_la = a.evLast; 867 868 if (this.isIE && el.visPropCalc.visible && (ev_fa || ev_la)) { 869 // Necessary, since Safari is the new IE (11.2024) 870 el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode); 871 return; 872 } 873 874 // We can not compare against visPropOld if there is need for a new arrow head, 875 // since here visPropOld and ev_fa / ev_la already have the same value. 876 // This has been set in _updateVisual. 877 // 878 node2 = el.rendNodeTriangleStart; 879 if (ev_fa) { 880 str = this.toStr(this.container.id, '_', el.id, 'TriangleStart', a.typeFirst); 881 882 // If we try to set the same arrow head as is already set, we can bail out now 883 if (!Type.exists(node2) || node2.id !== str) { 884 node2 = this.container.ownerDocument.getElementById(str); 885 // Check if the marker already exists. 886 // If not, create a new marker 887 if (node2 === null) { 888 node2 = this._createArrowHead(el, "Start", a.typeFirst); 889 this.defs.appendChild(node2); 890 } 891 el.rendNodeTriangleStart = node2; 892 el.rendNode.setAttributeNS(null, "marker-start", this.toURL(str)); 893 } 894 } else { 895 if (Type.exists(node2)) { 896 this.remove(node2); 897 el.rendNodeTriangleStart = null; 898 } 899 el.rendNode.setAttributeNS(null, "marker-start", null); 900 } 901 902 node2 = el.rendNodeTriangleEnd; 903 if (ev_la) { 904 str = this.toStr(this.container.id, '_', el.id, 'TriangleEnd', a.typeLast); 905 906 // If we try to set the same arrow head as is already set, we can bail out now 907 if (!Type.exists(node2) || node2.id !== str) { 908 node2 = this.container.ownerDocument.getElementById(str); 909 // Check if the marker already exists. 910 // If not, create a new marker 911 if (node2 === null) { 912 node2 = this._createArrowHead(el, "End", a.typeLast); 913 this.defs.appendChild(node2); 914 } 915 el.rendNodeTriangleEnd = node2; 916 el.rendNode.setAttributeNS(null, "marker-end", this.toURL(str)); 917 } 918 } else { 919 if (Type.exists(node2)) { 920 this.remove(node2); 921 el.rendNodeTriangleEnd = null; 922 } 923 el.rendNode.setAttributeNS(null, "marker-end", null); 924 } 925 }, 926 927 // Already documented in JXG.AbstractRenderer 928 updateEllipsePrim: function (node, x, y, rx, ry) { 929 var huge = 1000000; 930 931 huge = 200000; // IE 932 // webkit does not like huge values if the object is dashed 933 // iE doesn't like huge values above 216000 934 x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x); 935 y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y); 936 rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx); 937 ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry); 938 939 node.setAttributeNS(null, "cx", x); 940 node.setAttributeNS(null, "cy", y); 941 node.setAttributeNS(null, "rx", Math.abs(rx)); 942 node.setAttributeNS(null, "ry", Math.abs(ry)); 943 }, 944 945 // Already documented in JXG.AbstractRenderer 946 updateLinePrim: function (node, p1x, p1y, p2x, p2y) { 947 var huge = 1000000; 948 949 huge = 200000; //IE 950 if (!isNaN(p1x + p1y + p2x + p2y)) { 951 // webkit does not like huge values if the object is dashed 952 // IE doesn't like huge values above 216000 953 p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x); 954 p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y); 955 p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x); 956 p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y); 957 958 node.setAttributeNS(null, "x1", p1x); 959 node.setAttributeNS(null, "y1", p1y); 960 node.setAttributeNS(null, "x2", p2x); 961 node.setAttributeNS(null, "y2", p2y); 962 } 963 }, 964 965 // Already documented in JXG.AbstractRenderer 966 updatePathPrim: function (node, pointString) { 967 if (pointString === "") { 968 pointString = "M 0 0"; 969 } 970 node.setAttributeNS(null, "d", pointString); 971 }, 972 973 // Already documented in JXG.AbstractRenderer 974 updatePathStringPoint: function (el, size, type) { 975 var s = "", 976 scr = el.coords.scrCoords, 977 sqrt32 = size * Math.sqrt(3) * 0.5, 978 s05 = size * 0.5; 979 980 if (type === "x") { 981 s = 982 " M " + 983 (scr[1] - size) + 984 " " + 985 (scr[2] - size) + 986 " L " + 987 (scr[1] + size) + 988 " " + 989 (scr[2] + size) + 990 " M " + 991 (scr[1] + size) + 992 " " + 993 (scr[2] - size) + 994 " L " + 995 (scr[1] - size) + 996 " " + 997 (scr[2] + size); 998 } else if (type === "+") { 999 s = 1000 " M " + 1001 (scr[1] - size) + 1002 " " + 1003 scr[2] + 1004 " L " + 1005 (scr[1] + size) + 1006 " " + 1007 scr[2] + 1008 " M " + 1009 scr[1] + 1010 " " + 1011 (scr[2] - size) + 1012 " L " + 1013 scr[1] + 1014 " " + 1015 (scr[2] + size); 1016 } else if (type === "|") { 1017 s = 1018 " M " + 1019 scr[1] + 1020 " " + 1021 (scr[2] - size) + 1022 " L " + 1023 scr[1] + 1024 " " + 1025 (scr[2] + size); 1026 } else if (type === "-") { 1027 s = 1028 " M " + 1029 (scr[1] - size) + 1030 " " + 1031 scr[2] + 1032 " L " + 1033 (scr[1] + size) + 1034 " " + 1035 scr[2]; 1036 } else if (type === "<>" || type === "<<>>") { 1037 if (type === "<<>>") { 1038 size *= 1.41; 1039 } 1040 s = 1041 " M " + 1042 (scr[1] - size) + 1043 " " + 1044 scr[2] + 1045 " L " + 1046 scr[1] + 1047 " " + 1048 (scr[2] + size) + 1049 " L " + 1050 (scr[1] + size) + 1051 " " + 1052 scr[2] + 1053 " L " + 1054 scr[1] + 1055 " " + 1056 (scr[2] - size) + 1057 " Z "; 1058 } else if (type === "^") { 1059 s = 1060 " M " + 1061 scr[1] + 1062 " " + 1063 (scr[2] - size) + 1064 " L " + 1065 (scr[1] - sqrt32) + 1066 " " + 1067 (scr[2] + s05) + 1068 " L " + 1069 (scr[1] + sqrt32) + 1070 " " + 1071 (scr[2] + s05) + 1072 " Z "; // close path 1073 } else if (type === "v") { 1074 s = 1075 " M " + 1076 scr[1] + 1077 " " + 1078 (scr[2] + size) + 1079 " L " + 1080 (scr[1] - sqrt32) + 1081 " " + 1082 (scr[2] - s05) + 1083 " L " + 1084 (scr[1] + sqrt32) + 1085 " " + 1086 (scr[2] - s05) + 1087 " Z "; 1088 } else if (type === ">") { 1089 s = 1090 " M " + 1091 (scr[1] + size) + 1092 " " + 1093 scr[2] + 1094 " L " + 1095 (scr[1] - s05) + 1096 " " + 1097 (scr[2] - sqrt32) + 1098 " L " + 1099 (scr[1] - s05) + 1100 " " + 1101 (scr[2] + sqrt32) + 1102 " Z "; 1103 } else if (type === "<") { 1104 s = 1105 " M " + 1106 (scr[1] - size) + 1107 " " + 1108 scr[2] + 1109 " L " + 1110 (scr[1] + s05) + 1111 " " + 1112 (scr[2] - sqrt32) + 1113 " L " + 1114 (scr[1] + s05) + 1115 " " + 1116 (scr[2] + sqrt32) + 1117 " Z "; 1118 } 1119 return s; 1120 }, 1121 1122 // Already documented in JXG.AbstractRenderer 1123 updatePathStringPrim: function (el) { 1124 var i, 1125 scr, 1126 len, 1127 symbm = " M ", 1128 symbl = " L ", 1129 symbc = " C ", 1130 nextSymb = symbm, 1131 maxSize = 5000.0, 1132 pStr = ""; 1133 1134 if (el.numberPoints <= 0) { 1135 return ""; 1136 } 1137 1138 len = Math.min(el.points.length, el.numberPoints); 1139 1140 if (el.bezierDegree === 1) { 1141 for (i = 0; i < len; i++) { 1142 scr = el.points[i].scrCoords; 1143 if (isNaN(scr[1]) || isNaN(scr[2])) { 1144 // PenUp 1145 nextSymb = symbm; 1146 } else { 1147 // Chrome has problems with values being too far away. 1148 scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize); 1149 scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize); 1150 1151 // Attention: first coordinate may be inaccurate if far way 1152 //pStr += [nextSymb, scr[1], ' ', scr[2]].join(''); 1153 pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox) 1154 nextSymb = symbl; 1155 } 1156 } 1157 } else if (el.bezierDegree === 3) { 1158 i = 0; 1159 while (i < len) { 1160 scr = el.points[i].scrCoords; 1161 if (isNaN(scr[1]) || isNaN(scr[2])) { 1162 // PenUp 1163 nextSymb = symbm; 1164 } else { 1165 pStr += nextSymb + scr[1] + " " + scr[2]; 1166 if (nextSymb === symbc) { 1167 i += 1; 1168 scr = el.points[i].scrCoords; 1169 pStr += " " + scr[1] + " " + scr[2]; 1170 i += 1; 1171 scr = el.points[i].scrCoords; 1172 pStr += " " + scr[1] + " " + scr[2]; 1173 } 1174 nextSymb = symbc; 1175 } 1176 i += 1; 1177 } 1178 } 1179 return pStr; 1180 }, 1181 1182 // Already documented in JXG.AbstractRenderer 1183 updatePathStringBezierPrim: function (el) { 1184 var i, j, k, 1185 scr, 1186 lx, ly, 1187 len, 1188 symbm = " M ", 1189 symbl = " C ", 1190 nextSymb = symbm, 1191 maxSize = 5000.0, 1192 pStr = "", 1193 f = el.evalVisProp('strokewidth'), 1194 isNoPlot = el.evalVisProp('curvetype') !== "plot"; 1195 1196 if (el.numberPoints <= 0) { 1197 return ""; 1198 } 1199 1200 if (isNoPlot && el.board.options.curve.RDPsmoothing) { 1201 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5); 1202 } 1203 1204 len = Math.min(el.points.length, el.numberPoints); 1205 for (j = 1; j < 3; j++) { 1206 nextSymb = symbm; 1207 for (i = 0; i < len; i++) { 1208 scr = el.points[i].scrCoords; 1209 1210 if (isNaN(scr[1]) || isNaN(scr[2])) { 1211 // PenUp 1212 nextSymb = symbm; 1213 } else { 1214 // Chrome has problems with values being too far away. 1215 scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize); 1216 scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize); 1217 1218 // Attention: first coordinate may be inaccurate if far way 1219 if (nextSymb === symbm) { 1220 //pStr += [nextSymb, scr[1], ' ', scr[2]].join(''); 1221 pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox) 1222 } else { 1223 k = 2 * j; 1224 pStr += [ 1225 nextSymb, 1226 lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j), 1227 " ", 1228 ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j), 1229 " ", 1230 lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j), 1231 " ", 1232 ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j), 1233 " ", 1234 scr[1], 1235 " ", 1236 scr[2] 1237 ].join(""); 1238 } 1239 1240 nextSymb = symbl; 1241 lx = scr[1]; 1242 ly = scr[2]; 1243 } 1244 } 1245 } 1246 return pStr; 1247 }, 1248 1249 // Already documented in JXG.AbstractRenderer 1250 updatePolygonPrim: function (node, el) { 1251 var i, 1252 pStr = "", 1253 scrCoords, 1254 len = el.vertices.length; 1255 1256 node.setAttributeNS(null, "stroke", "none"); 1257 node.setAttributeNS(null, "fill-rule", "evenodd"); 1258 if (el.elType === "polygonalchain") { 1259 len++; 1260 } 1261 1262 for (i = 0; i < len - 1; i++) { 1263 if (el.vertices[i].isReal) { 1264 scrCoords = el.vertices[i].coords.scrCoords; 1265 pStr = pStr + scrCoords[1] + "," + scrCoords[2]; 1266 } else { 1267 node.setAttributeNS(null, "points", ""); 1268 return; 1269 } 1270 1271 if (i < len - 2) { 1272 pStr += " "; 1273 } 1274 } 1275 if (pStr.indexOf("NaN") === -1) { 1276 node.setAttributeNS(null, "points", pStr); 1277 } 1278 }, 1279 1280 // Already documented in JXG.AbstractRenderer 1281 updateRectPrim: function (node, x, y, w, h) { 1282 node.setAttributeNS(null, "x", x); 1283 node.setAttributeNS(null, "y", y); 1284 node.setAttributeNS(null, "width", w); 1285 node.setAttributeNS(null, "height", h); 1286 }, 1287 1288 /* ********* Set attributes *********** */ 1289 1290 /** 1291 * Call user-defined function to set visual attributes. 1292 * If "testAttribute" is the empty string, the function 1293 * is called immediately, otherwise it is called in a timeOut. 1294 * 1295 * This is necessary to realize smooth transitions but avoid transitions 1296 * when first creating the objects. 1297 * 1298 * Usually, the string in testAttribute is the visPropOld attribute 1299 * of the values which are set. 1300 * 1301 * @param {Function} setFunc Some function which usually sets some attributes 1302 * @param {String} testAttribute If this string is the empty string the function is called immediately, 1303 * otherwise it is called in a setImeout. 1304 * @see JXG.SVGRenderer#setObjectFillColor 1305 * @see JXG.SVGRenderer#setObjectStrokeColor 1306 * @see JXG.SVGRenderer#_setArrowColor 1307 * @private 1308 */ 1309 _setAttribute: function (setFunc, testAttribute) { 1310 if (testAttribute === "") { 1311 setFunc(); 1312 } else { 1313 window.setTimeout(setFunc, 1); 1314 } 1315 }, 1316 1317 display: function (el, val) { 1318 var node; 1319 1320 if (el && el.rendNode) { 1321 el.visPropOld.visible = val; 1322 node = el.rendNode; 1323 if (val) { 1324 node.setAttributeNS(null, "display", "inline"); 1325 node.style.visibility = "inherit"; 1326 } else { 1327 node.setAttributeNS(null, "display", "none"); 1328 node.style.visibility = "hidden"; 1329 } 1330 } 1331 }, 1332 1333 // documented in JXG.AbstractRenderer 1334 hide: function (el) { 1335 JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()"); 1336 this.display(el, false); 1337 }, 1338 1339 // documented in JXG.AbstractRenderer 1340 setARIA: function(el) { 1341 // This method is only called in abstractRenderer._updateVisual() if aria.enabled == true. 1342 var key, k, v; 1343 1344 // this.setPropertyPrim(el.rendNode, 'aria-label', el.evalVisProp('aria.label')); 1345 // this.setPropertyPrim(el.rendNode, 'aria-live', el.evalVisProp('aria.live')); 1346 for (key in el.visProp.aria) { 1347 if (el.visProp.aria.hasOwnProperty(key) && key !== 'enabled') { 1348 k = 'aria.' + key; 1349 v = el.evalVisProp('aria.' + key); 1350 if (el.visPropOld[k] !== v) { 1351 this.setPropertyPrim(el.rendNode, 'aria-' + key, v); 1352 el.visPropOld[k] = v; 1353 } 1354 } 1355 } 1356 }, 1357 1358 // documented in JXG.AbstractRenderer 1359 setBuffering: function (el, type) { 1360 el.rendNode.setAttribute("buffered-rendering", type); 1361 }, 1362 1363 // documented in JXG.AbstractRenderer 1364 setCssClass(el, cssClass) { 1365 1366 if (el.visPropOld.cssclass !== cssClass) { 1367 this.setPropertyPrim(el.rendNode, 'class', cssClass); 1368 el.visPropOld.cssclass = cssClass; 1369 } 1370 }, 1371 1372 // documented in JXG.AbstractRenderer 1373 setDashStyle: function (el) { 1374 var dashStyle = el.evalVisProp('dash'), 1375 ds = el.evalVisProp('dashscale'), 1376 sw = ds ? 0.5 * el.evalVisProp('strokewidth') : 1, 1377 node = el.rendNode; 1378 1379 if (dashStyle > 0) { 1380 node.setAttributeNS(null, "stroke-dasharray", 1381 // sw could distinguish highlighting or not. 1382 // But it seems to preferable to ignore this. 1383 this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',') 1384 ); 1385 } else { 1386 if (node.hasAttributeNS(null, "stroke-dasharray")) { 1387 node.removeAttributeNS(null, "stroke-dasharray"); 1388 } 1389 } 1390 }, 1391 1392 // documented in JXG.AbstractRenderer 1393 setGradient: function (el) { 1394 var fillNode = el.rendNode, 1395 node, node2, node3, 1396 ev_g = el.evalVisProp('gradient'); 1397 1398 if (ev_g === "linear" || ev_g === "radial") { 1399 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient"); 1400 node2 = this.createPrim("stop", el.id + "_gradient1"); 1401 node3 = this.createPrim("stop", el.id + "_gradient2"); 1402 node.appendChild(node2); 1403 node.appendChild(node3); 1404 this.defs.appendChild(node); 1405 fillNode.setAttributeNS( 1406 null, 1407 'style', 1408 // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)" 1409 'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient') 1410 ); 1411 el.gradNode1 = node2; 1412 el.gradNode2 = node3; 1413 el.gradNode = node; 1414 } else { 1415 fillNode.removeAttributeNS(null, "style"); 1416 } 1417 }, 1418 1419 // documented in JXG.AbstractRenderer 1420 setLineCap: function (el) { 1421 var capStyle = el.evalVisProp('linecap'); 1422 1423 if ( 1424 capStyle === undefined || 1425 capStyle === "" || 1426 el.visPropOld.linecap === capStyle || 1427 !Type.exists(el.rendNode) 1428 ) { 1429 return; 1430 } 1431 1432 this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle); 1433 el.visPropOld.linecap = capStyle; 1434 }, 1435 1436 // documented in JXG.AbstractRenderer 1437 setObjectFillColor: function (el, color, opacity, rendNode) { 1438 var node, c, rgbo, oo, 1439 rgba = color, 1440 o = opacity, 1441 grad = el.evalVisProp('gradient'); 1442 1443 o = o > 0 ? o : 0; 1444 1445 // TODO save gradient and gradientangle 1446 if ( 1447 el.visPropOld.fillcolor === rgba && 1448 el.visPropOld.fillopacity === o && 1449 grad === null 1450 ) { 1451 return; 1452 } 1453 if (Type.exists(rgba) && rgba !== false) { 1454 if (rgba.length !== 9) { 1455 // RGB, not RGBA 1456 c = rgba; 1457 oo = o; 1458 } else { 1459 // True RGBA, not RGB 1460 rgbo = Color.rgba2rgbo(rgba); 1461 c = rgbo[0]; 1462 oo = o * rgbo[1]; 1463 } 1464 1465 if (rendNode === undefined) { 1466 node = el.rendNode; 1467 } else { 1468 node = rendNode; 1469 } 1470 1471 if (c !== "none") { 1472 this._setAttribute(function () { 1473 node.setAttributeNS(null, "fill", c); 1474 }, el.visPropOld.fillcolor); 1475 } 1476 1477 if (el.type === JXG.OBJECT_TYPE_IMAGE) { 1478 this._setAttribute(function () { 1479 node.setAttributeNS(null, "opacity", oo); 1480 }, el.visPropOld.fillopacity); 1481 //node.style['opacity'] = oo; // This would overwrite values set by CSS class. 1482 } else { 1483 if (c === "none") { 1484 // This is done only for non-images 1485 // because images have no fill color. 1486 oo = 0; 1487 // This is necessary if there is a foreignObject below. 1488 node.setAttributeNS(null, "pointer-events", "visibleStroke"); 1489 } else { 1490 // This is the default 1491 node.setAttributeNS(null, "pointer-events", "visiblePainted"); 1492 } 1493 this._setAttribute(function () { 1494 node.setAttributeNS(null, "fill-opacity", oo); 1495 }, el.visPropOld.fillopacity); 1496 } 1497 1498 if (grad === "linear" || grad === "radial") { 1499 this.updateGradient(el); 1500 } 1501 } 1502 el.visPropOld.fillcolor = rgba; 1503 el.visPropOld.fillopacity = o; 1504 }, 1505 1506 // documented in JXG.AbstractRenderer 1507 setObjectStrokeColor: function (el, color, opacity) { 1508 var rgba = color, 1509 c, rgbo, 1510 o = opacity, 1511 oo, node; 1512 1513 o = o > 0 ? o : 0; 1514 1515 if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) { 1516 return; 1517 } 1518 1519 if (Type.exists(rgba) && rgba !== false) { 1520 if (rgba.length !== 9) { 1521 // RGB, not RGBA 1522 c = rgba; 1523 oo = o; 1524 } else { 1525 // True RGBA, not RGB 1526 rgbo = Color.rgba2rgbo(rgba); 1527 c = rgbo[0]; 1528 oo = o * rgbo[1]; 1529 } 1530 1531 node = el.rendNode; 1532 1533 if (el.elementClass === Const.OBJECT_CLASS_TEXT) { 1534 if (el.evalVisProp('display') === "html") { 1535 this._setAttribute(function () { 1536 node.style.color = c; 1537 node.style.opacity = oo; 1538 }, el.visPropOld.strokecolor); 1539 } else { 1540 this._setAttribute(function () { 1541 node.setAttributeNS(null, "style", "fill:" + c); 1542 node.setAttributeNS(null, "style", "fill-opacity:" + oo); 1543 }, el.visPropOld.strokecolor); 1544 } 1545 } else { 1546 this._setAttribute(function () { 1547 node.setAttributeNS(null, "stroke", c); 1548 node.setAttributeNS(null, "stroke-opacity", oo); 1549 }, el.visPropOld.strokecolor); 1550 } 1551 1552 if ( 1553 el.elementClass === Const.OBJECT_CLASS_CURVE || 1554 el.elementClass === Const.OBJECT_CLASS_LINE 1555 ) { 1556 if (el.evalVisProp('firstarrow')) { 1557 this._setArrowColor( 1558 el.rendNodeTriangleStart, 1559 c, oo, el, 1560 el.visPropCalc.typeFirst 1561 ); 1562 } 1563 1564 if (el.evalVisProp('lastarrow')) { 1565 this._setArrowColor( 1566 el.rendNodeTriangleEnd, 1567 c, oo, el, 1568 el.visPropCalc.typeLast 1569 ); 1570 } 1571 } 1572 } 1573 1574 el.visPropOld.strokecolor = rgba; 1575 el.visPropOld.strokeopacity = o; 1576 }, 1577 1578 // documented in JXG.AbstractRenderer 1579 setObjectStrokeWidth: function (el, width) { 1580 var node, 1581 w = width; 1582 1583 if (isNaN(w) || el.visPropOld.strokewidth === w) { 1584 return; 1585 } 1586 1587 node = el.rendNode; 1588 this.setPropertyPrim(node, "stroked", "true"); 1589 if (Type.exists(w)) { 1590 this.setPropertyPrim(node, "stroke-width", w + "px"); 1591 1592 // if (el.elementClass === Const.OBJECT_CLASS_CURVE || 1593 // el.elementClass === Const.OBJECT_CLASS_LINE) { 1594 // if (el.evalVisProp('firstarrow')) { 1595 // this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode); 1596 // } 1597 // 1598 // if (el.evalVisProp('lastarrow')) { 1599 // this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode); 1600 // } 1601 // } 1602 } 1603 el.visPropOld.strokewidth = w; 1604 }, 1605 1606 // documented in JXG.AbstractRenderer 1607 setObjectTransition: function (el, duration) { 1608 var node, props, 1609 transitionArr = [], 1610 transitionStr, 1611 i, 1612 len = 0, 1613 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"]; 1614 1615 if (duration === undefined) { 1616 duration = el.evalVisProp('transitionduration'); 1617 } 1618 1619 props = el.evalVisProp('transitionproperties'); 1620 if (duration === el.visPropOld.transitionduration && 1621 props === el.visPropOld.transitionproperties) { 1622 return; 1623 } 1624 1625 // if ( 1626 // el.elementClass === Const.OBJECT_CLASS_TEXT && 1627 // el.evalVisProp('display') === "html" 1628 // ) { 1629 // // transitionStr = " color " + duration + "ms," + 1630 // // " opacity " + duration + "ms"; 1631 // transitionStr = " all " + duration + "ms ease"; 1632 // } else { 1633 // transitionStr = 1634 // " fill " + duration + "ms," + 1635 // " fill-opacity " + duration + "ms," + 1636 // " stroke " + duration + "ms," + 1637 // " stroke-opacity " + duration + "ms," + 1638 // " stroke-width " + duration + "ms," + 1639 // " width " + duration + "ms," + 1640 // " height " + duration + "ms," + 1641 // " rx " + duration + "ms," + 1642 // " ry " + duration + "ms"; 1643 // } 1644 1645 if (Type.exists(props)) { 1646 len = props.length; 1647 } 1648 for (i = 0; i < len; i++) { 1649 transitionArr.push(props[i] + ' ' + duration + 'ms'); 1650 } 1651 transitionStr = transitionArr.join(', '); 1652 1653 len = nodes.length; 1654 for (i = 0; i < len; ++i) { 1655 if (el[nodes[i]]) { 1656 node = el[nodes[i]]; 1657 node.style.transition = transitionStr; 1658 } 1659 } 1660 1661 el.visPropOld.transitionduration = duration; 1662 el.visPropOld.transitionproperties = props; 1663 }, 1664 1665 // documented in JXG.AbstractRenderer 1666 setShadow: function (el) { 1667 var ev_s = el.evalVisProp('shadow'), 1668 ev_s_json, c, b, bl, o, op, id, node, 1669 use_board_filter = true, 1670 show = false; 1671 1672 ev_s_json = JSON.stringify(ev_s); 1673 if (ev_s_json === el.visPropOld.shadow) { 1674 return; 1675 } 1676 1677 if (typeof ev_s === 'boolean') { 1678 use_board_filter = true; 1679 show = ev_s; 1680 c = 'none'; 1681 b = 3; 1682 bl = 0.1; 1683 o = [5, 5]; 1684 op = 1; 1685 } else { 1686 if (el.evalVisProp('shadow.enabled')) { 1687 use_board_filter = false; 1688 show = true; 1689 c = JXG.rgbParser(el.evalVisProp('shadow.color')); 1690 b = el.evalVisProp('shadow.blur'); 1691 bl = el.evalVisProp('shadow.blend'); 1692 o = el.evalVisProp('shadow.offset'); 1693 op = el.evalVisProp('shadow.opacity'); 1694 } else { 1695 show = false; 1696 } 1697 } 1698 1699 if (Type.exists(el.rendNode)) { 1700 if (show) { 1701 if (use_board_filter) { 1702 el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1')); 1703 // 'url(#' + this.container.id + '_' + 'f1)'); 1704 } else { 1705 node = this.container.ownerDocument.getElementById(id); 1706 if (node) { 1707 this.defs.removeChild(node); 1708 } 1709 id = el.rendNode.id + '_' + 'f1'; 1710 this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o)); 1711 el.rendNode.setAttributeNS(null, 'filter', this.toURL(id)); 1712 // 'url(#' + id + ')'); 1713 } 1714 } else { 1715 el.rendNode.removeAttributeNS(null, 'filter'); 1716 } 1717 } 1718 1719 el.visPropOld.shadow = ev_s_json; 1720 }, 1721 1722 // documented in JXG.AbstractRenderer 1723 setTabindex: function (el) { 1724 var val; 1725 if (el.board.attr.keyboard.enabled && Type.exists(el.rendNode)) { 1726 val = el.evalVisProp('tabindex'); 1727 if (!el.visPropCalc.visible /* || el.evalVisProp('fixed') */) { 1728 val = null; 1729 } 1730 if (val !== el.visPropOld.tabindex) { 1731 el.rendNode.setAttribute("tabindex", val); 1732 el.visPropOld.tabindex = val; 1733 } 1734 } 1735 }, 1736 1737 // documented in JXG.AbstractRenderer 1738 setPropertyPrim: function (node, key, val) { 1739 if (key === "stroked") { 1740 return; 1741 } 1742 node.setAttributeNS(null, key, val); 1743 }, 1744 1745 // documented in JXG.AbstractRenderer 1746 show: function (el) { 1747 JXG.deprecated("Board.renderer.show()", "Board.renderer.display()"); 1748 this.display(el, true); 1749 // var node; 1750 // 1751 // if (el && el.rendNode) { 1752 // node = el.rendNode; 1753 // node.setAttributeNS(null, 'display', 'inline'); 1754 // node.style.visibility = "inherit"; 1755 // } 1756 }, 1757 1758 // documented in JXG.AbstractRenderer 1759 updateGradient: function (el) { 1760 var col, 1761 op, 1762 node2 = el.gradNode1, 1763 node3 = el.gradNode2, 1764 ev_g = el.evalVisProp('gradient'); 1765 1766 if (!Type.exists(node2) || !Type.exists(node3)) { 1767 return; 1768 } 1769 1770 op = el.evalVisProp('fillopacity'); 1771 op = op > 0 ? op : 0; 1772 col = el.evalVisProp('fillcolor'); 1773 1774 node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op); 1775 node3.setAttributeNS( 1776 null, 1777 "style", 1778 "stop-color:" + 1779 el.evalVisProp('gradientsecondcolor') + 1780 ";stop-opacity:" + 1781 el.evalVisProp('gradientsecondopacity') 1782 ); 1783 node2.setAttributeNS( 1784 null, 1785 "offset", 1786 el.evalVisProp('gradientstartoffset') * 100 + "%" 1787 ); 1788 node3.setAttributeNS( 1789 null, 1790 "offset", 1791 el.evalVisProp('gradientendoffset') * 100 + "%" 1792 ); 1793 if (ev_g === "linear") { 1794 this.updateGradientAngle(el.gradNode, el.evalVisProp('gradientangle')); 1795 } else if (ev_g === "radial") { 1796 this.updateGradientCircle( 1797 el.gradNode, 1798 el.evalVisProp('gradientcx'), 1799 el.evalVisProp('gradientcy'), 1800 el.evalVisProp('gradientr'), 1801 el.evalVisProp('gradientfx'), 1802 el.evalVisProp('gradientfy'), 1803 el.evalVisProp('gradientfr') 1804 ); 1805 } 1806 }, 1807 1808 /** 1809 * Set the gradient angle for linear color gradients. 1810 * 1811 * @private 1812 * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element. 1813 * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom. 1814 */ 1815 updateGradientAngle: function (node, radians) { 1816 // Angles: 1817 // 0: -> 1818 // 90: down 1819 // 180: <- 1820 // 90: up 1821 var f = 1.0, 1822 co = Math.cos(radians), 1823 si = Math.sin(radians); 1824 1825 if (Math.abs(co) > Math.abs(si)) { 1826 f /= Math.abs(co); 1827 } else { 1828 f /= Math.abs(si); 1829 } 1830 1831 if (co >= 0) { 1832 node.setAttributeNS(null, "x1", 0); 1833 node.setAttributeNS(null, "x2", co * f); 1834 } else { 1835 node.setAttributeNS(null, "x1", -co * f); 1836 node.setAttributeNS(null, "x2", 0); 1837 } 1838 if (si >= 0) { 1839 node.setAttributeNS(null, "y1", 0); 1840 node.setAttributeNS(null, "y2", si * f); 1841 } else { 1842 node.setAttributeNS(null, "y1", -si * f); 1843 node.setAttributeNS(null, "y2", 0); 1844 } 1845 }, 1846 1847 /** 1848 * Set circles for radial color gradients. 1849 * 1850 * @private 1851 * @param {SVGnode} node SVG gradient node 1852 * @param {Number} cx SVG value cx (value between 0 and 1) 1853 * @param {Number} cy SVG value cy (value between 0 and 1) 1854 * @param {Number} r SVG value r (value between 0 and 1) 1855 * @param {Number} fx SVG value fx (value between 0 and 1) 1856 * @param {Number} fy SVG value fy (value between 0 and 1) 1857 * @param {Number} fr SVG value fr (value between 0 and 1) 1858 */ 1859 updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) { 1860 node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color 1861 node.setAttributeNS(null, "cy", cy * 100 + "%"); 1862 node.setAttributeNS(null, "r", r * 100 + "%"); 1863 node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point 1864 node.setAttributeNS(null, "fy", fy * 100 + "%"); 1865 node.setAttributeNS(null, "fr", fr * 100 + "%"); 1866 }, 1867 1868 /* ********* Renderer control *********** */ 1869 1870 // documented in JXG.AbstractRenderer 1871 suspendRedraw: function () { 1872 // It seems to be important for the Linux version of firefox 1873 this.suspendHandle = this.svgRoot.suspendRedraw(10000); 1874 }, 1875 1876 // documented in JXG.AbstractRenderer 1877 unsuspendRedraw: function () { 1878 this.svgRoot.unsuspendRedraw(this.suspendHandle); 1879 // this.svgRoot.unsuspendRedrawAll(); 1880 //this.svgRoot.forceRedraw(); 1881 }, 1882 1883 // documented in AbstractRenderer 1884 resize: function (w, h) { 1885 this.svgRoot.setAttribute("width", parseFloat(w)); 1886 this.svgRoot.setAttribute("height", parseFloat(h)); 1887 }, 1888 1889 // documented in JXG.AbstractRenderer 1890 createTouchpoints: function (n) { 1891 var i, na1, na2, node; 1892 this.touchpoints = []; 1893 for (i = 0; i < n; i++) { 1894 na1 = "touchpoint1_" + i; 1895 node = this.createPrim("path", na1); 1896 this.appendChildPrim(node, 19); 1897 node.setAttributeNS(null, "d", "M 0 0"); 1898 this.touchpoints.push(node); 1899 1900 this.setPropertyPrim(node, "stroked", "true"); 1901 this.setPropertyPrim(node, "stroke-width", "1px"); 1902 node.setAttributeNS(null, "stroke", "#000000"); 1903 node.setAttributeNS(null, "stroke-opacity", 1.0); 1904 node.setAttributeNS(null, "display", "none"); 1905 1906 na2 = "touchpoint2_" + i; 1907 node = this.createPrim("ellipse", na2); 1908 this.appendChildPrim(node, 19); 1909 this.updateEllipsePrim(node, 0, 0, 0, 0); 1910 this.touchpoints.push(node); 1911 1912 this.setPropertyPrim(node, "stroked", "true"); 1913 this.setPropertyPrim(node, "stroke-width", "1px"); 1914 node.setAttributeNS(null, "stroke", "#000000"); 1915 node.setAttributeNS(null, "stroke-opacity", 1.0); 1916 node.setAttributeNS(null, "fill", "#ffffff"); 1917 node.setAttributeNS(null, "fill-opacity", 0.0); 1918 1919 node.setAttributeNS(null, "display", "none"); 1920 } 1921 }, 1922 1923 // documented in JXG.AbstractRenderer 1924 showTouchpoint: function (i) { 1925 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1926 this.touchpoints[2 * i].setAttributeNS(null, "display", "inline"); 1927 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "inline"); 1928 } 1929 }, 1930 1931 // documented in JXG.AbstractRenderer 1932 hideTouchpoint: function (i) { 1933 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1934 this.touchpoints[2 * i].setAttributeNS(null, "display", "none"); 1935 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "none"); 1936 } 1937 }, 1938 1939 // documented in JXG.AbstractRenderer 1940 updateTouchpoint: function (i, pos) { 1941 var x, 1942 y, 1943 d = 37; 1944 1945 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1946 x = pos[0]; 1947 y = pos[1]; 1948 1949 this.touchpoints[2 * i].setAttributeNS( 1950 null, 1951 "d", 1952 "M " + 1953 (x - d) + 1954 " " + 1955 y + 1956 " " + 1957 "L " + 1958 (x + d) + 1959 " " + 1960 y + 1961 " " + 1962 "M " + 1963 x + 1964 " " + 1965 (y - d) + 1966 " " + 1967 "L " + 1968 x + 1969 " " + 1970 (y + d) 1971 ); 1972 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25); 1973 } 1974 }, 1975 1976 /* ********* Dump related stuff *********** */ 1977 1978 /** 1979 * Walk recursively through the DOM subtree of a node and collect all 1980 * value attributes together with the id of that node. 1981 * <b>Attention:</b> Only values of nodes having a valid id are taken. 1982 * @param {Node} node root node of DOM subtree that will be searched recursively. 1983 * @return {Array} Array with entries of the form [id, value] 1984 * @private 1985 */ 1986 _getValuesOfDOMElements: function (node) { 1987 var values = []; 1988 if (node.nodeType === 1) { 1989 node = node.firstChild; 1990 while (node) { 1991 if (node.id !== undefined && node.value !== undefined) { 1992 values.push([node.id, node.value]); 1993 } 1994 Type.concat(values, this._getValuesOfDOMElements(node)); 1995 node = node.nextSibling; 1996 } 1997 } 1998 return values; 1999 }, 2000 2001 // _getDataUri: function (url, callback) { 2002 // var image = new Image(); 2003 // image.onload = function () { 2004 // var canvas = document.createElement("canvas"); 2005 // canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size 2006 // canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size 2007 // canvas.getContext("2d").drawImage(this, 0, 0); 2008 // callback(canvas.toDataURL("image/png")); 2009 // canvas.remove(); 2010 // }; 2011 // image.src = url; 2012 // }, 2013 2014 _getImgDataURL: function (svgRoot) { 2015 var images, len, canvas, ctx, ur, i; 2016 2017 images = svgRoot.getElementsByTagName("image"); 2018 len = images.length; 2019 if (len > 0) { 2020 canvas = document.createElement("canvas"); 2021 //img = new Image(); 2022 for (i = 0; i < len; i++) { 2023 images[i].setAttribute("crossorigin", "anonymous"); 2024 //img.src = images[i].href; 2025 //img.onload = function() { 2026 // img.crossOrigin = "anonymous"; 2027 ctx = canvas.getContext("2d"); 2028 canvas.width = images[i].getAttribute("width"); 2029 canvas.height = images[i].getAttribute("height"); 2030 try { 2031 ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height); 2032 2033 // If the image is not png, the format must be specified here 2034 ur = canvas.toDataURL(); 2035 images[i].setAttribute("xlink:href", ur); 2036 } catch (err) { 2037 console.log("CORS problem! Image can not be used", err); 2038 } 2039 } 2040 //canvas.remove(); 2041 } 2042 return true; 2043 }, 2044 2045 /** 2046 * Return a data URI of the SVG code representing the construction. 2047 * The SVG code of the construction is base64 encoded. The return string starts 2048 * with "data:image/svg+xml;base64,...". 2049 * 2050 * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none. 2051 * This is necessary for older versions of Safari. Default: false 2052 * @returns {String} data URI string 2053 * 2054 * @example 2055 * var A = board.create('point', [2, 2]); 2056 * 2057 * var txt = board.renderer.dumpToDataURI(false); 2058 * // txt consists of a string of the form 2059 * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+ 2060 * // Behind the comma, there is the base64 encoded SVG code 2061 * // which is decoded with atob(). 2062 * // The call of decodeURIComponent(escape(...)) is necessary 2063 * // to handle unicode strings correctly. 2064 * var ar = txt.split(','); 2065 * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1]))); 2066 * 2067 * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div> 2068 * <textarea id="output2023" rows="5" cols="50"></textarea> 2069 * <script type="text/javascript"> 2070 * (function() { 2071 * var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d', 2072 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2073 * var A = board.create('point', [2, 2]); 2074 * 2075 * var txt = board.renderer.dumpToDataURI(false); 2076 * // txt consists of a string of the form 2077 * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+ 2078 * // Behind the comma, there is the base64 encoded SVG code 2079 * // which is decoded with atob(). 2080 * // The call of decodeURIComponent(escape(...)) is necessary 2081 * // to handle unicode strings correctly. 2082 * var ar = txt.split(','); 2083 * document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1]))); 2084 * 2085 * })(); 2086 * 2087 * </script><pre> 2088 * 2089 */ 2090 dumpToDataURI: function (ignoreTexts) { 2091 var svgRoot = this.svgRoot, 2092 btoa = window.btoa || Base64.encode, 2093 svg, i, len, 2094 values = []; 2095 2096 // Move all HTML tags (beside the SVG root) of the container 2097 // to the foreignObject element inside of the svgRoot node 2098 // Problem: 2099 // input values are not copied. This can be verified by looking at an innerHTML output 2100 // of an input element. Therefore, we do it "by hand". 2101 if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) { 2102 if (!ignoreTexts) { 2103 this.foreignObjLayer.setAttribute("display", "inline"); 2104 } 2105 while (svgRoot.nextSibling) { 2106 // Copy all value attributes 2107 Type.concat(values, this._getValuesOfDOMElements(svgRoot.nextSibling)); 2108 this.foreignObjLayer.appendChild(svgRoot.nextSibling); 2109 } 2110 } 2111 2112 this._getImgDataURL(svgRoot); 2113 2114 // Convert the SVG graphic into a string containing SVG code 2115 svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 2116 svg = new XMLSerializer().serializeToString(svgRoot); 2117 2118 if (ignoreTexts !== true) { 2119 // Handle SVG texts 2120 // Insert all value attributes back into the svg string 2121 len = values.length; 2122 for (i = 0; i < len; i++) { 2123 svg = svg.replace( 2124 'id="' + values[i][0] + '"', 2125 'id="' + values[i][0] + '" value="' + values[i][1] + '"' 2126 ); 2127 } 2128 } 2129 2130 // if (false) { 2131 // // Debug: use example svg image 2132 // 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>'; 2133 // } 2134 2135 // In IE we have to remove the namespace again. 2136 // Since 2024 we have to check if the namespace attribute appears twice in one tag, because 2137 // there might by a svg inside of the svg, e.g. the screenshot icon. 2138 if (this.isIE && 2139 (svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1 2140 ) { 2141 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g, ""); 2142 } 2143 2144 // Safari fails if the svg string contains a " " 2145 // Obsolete with Safari 12+ 2146 svg = svg.replace(/ /g, " "); 2147 // Replacing "s might be necessary for older Safari versions 2148 // svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); // Bug: does not replace matching "s 2149 // svg = svg.replace(/"/g, ""); 2150 2151 // Move all HTML tags back from 2152 // the foreignObject element to the container 2153 if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) { 2154 // Restore all HTML elements 2155 while (this.foreignObjLayer.firstChild) { 2156 this.container.appendChild(this.foreignObjLayer.firstChild); 2157 } 2158 this.foreignObjLayer.setAttribute("display", "none"); 2159 } 2160 2161 return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); 2162 }, 2163 2164 /** 2165 * Convert the SVG construction into an HTML canvas image. 2166 * This works for all SVG supporting browsers. Implemented as Promise. 2167 * <p> 2168 * Might fail if any text element or foreign object element contains SVG. This 2169 * is the case e.g. for the default fullscreen symbol. 2170 * <p> 2171 * For IE, it is realized as function. 2172 * It works from version 9, with the exception that HTML texts 2173 * are ignored on IE. The drawing is done with a delay of 2174 * 200 ms. Otherwise there would be problems with IE. 2175 * 2176 * @param {String} canvasId Id of an HTML canvas element 2177 * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag. 2178 * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag. 2179 * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root. 2180 * This is necessary for older versions of Safari. Default: false 2181 * @returns {Promise} Promise object 2182 * 2183 * @example 2184 * board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); }); 2185 * 2186 * @example 2187 * // IE 11 example: 2188 * board.renderer.dumpToCanvas('canvas'); 2189 * setTimeout(function() { console.log('done'); }, 400); 2190 */ 2191 dumpToCanvas: function (canvasId, w, h, ignoreTexts) { 2192 var svg, tmpImg, 2193 cv, ctx, 2194 doc = this.container.ownerDocument; 2195 2196 // Prepare the canvas element 2197 cv = doc.getElementById(canvasId); 2198 2199 // Clear the canvas 2200 /* eslint-disable no-self-assign */ 2201 cv.width = cv.width; 2202 /* eslint-enable no-self-assign */ 2203 2204 ctx = cv.getContext("2d"); 2205 if (w !== undefined && h !== undefined) { 2206 cv.style.width = parseFloat(w) + "px"; 2207 cv.style.height = parseFloat(h) + "px"; 2208 // Scale twice the CSS size to make the image crisp 2209 // cv.setAttribute('width', 2 * parseFloat(wOrg)); 2210 // cv.setAttribute('height', 2 * parseFloat(hOrg)); 2211 // ctx.scale(2 * wOrg / w, 2 * hOrg / h); 2212 cv.setAttribute("width", parseFloat(w)); 2213 cv.setAttribute("height", parseFloat(h)); 2214 } 2215 2216 // Display the SVG string as data-uri in an HTML img. 2217 /** 2218 * @type {Image} 2219 * @ignore 2220 * {ignore} 2221 */ 2222 tmpImg = new Image(); 2223 svg = this.dumpToDataURI(ignoreTexts); 2224 tmpImg.src = svg; 2225 2226 // Finally, draw the HTML img in the canvas. 2227 if (!("Promise" in window)) { 2228 /** 2229 * @function 2230 * @ignore 2231 */ 2232 tmpImg.onload = function () { 2233 // IE needs a pause... 2234 // Seems to be broken 2235 window.setTimeout(function () { 2236 try { 2237 ctx.drawImage(tmpImg, 0, 0, w, h); 2238 } catch (err) { 2239 console.log("screenshots not longer supported on IE"); 2240 } 2241 }, 200); 2242 }; 2243 return this; 2244 } 2245 2246 return new Promise(function (resolve, reject) { 2247 try { 2248 tmpImg.onload = function () { 2249 ctx.drawImage(tmpImg, 0, 0, w, h); 2250 resolve(); 2251 }; 2252 } catch (e) { 2253 reject(e); 2254 } 2255 }); 2256 }, 2257 2258 /** 2259 * Display SVG image in html img-tag which enables 2260 * easy download for the user. 2261 * 2262 * Support: 2263 * <ul> 2264 * <li> IE: No 2265 * <li> Edge: full 2266 * <li> Firefox: full 2267 * <li> Chrome: full 2268 * <li> Safari: full (No text support in versions prior to 12). 2269 * </ul> 2270 * 2271 * @param {JXG.Board} board Link to the board. 2272 * @param {String} imgId Optional id of an img object. If given and different from the empty string, 2273 * the screenshot is copied to this img object. The width and height will be set to the values of the 2274 * JSXGraph container. 2275 * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the 2276 * SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false 2277 * @return {Object} the svg renderer object 2278 */ 2279 screenshot: function (board, imgId, ignoreTexts) { 2280 var node, 2281 doc = this.container.ownerDocument, 2282 parent = this.container.parentNode, 2283 // cPos, 2284 // cssTxt, 2285 canvas, id, img, 2286 button, buttonText, 2287 w, h, 2288 bas = board.attr.screenshot, 2289 navbar, navbarDisplay, insert, 2290 newImg = false, 2291 _copyCanvasToImg, 2292 isDebug = false; 2293 2294 if (this.type === "no") { 2295 return this; 2296 } 2297 2298 w = bas.scale * this.container.getBoundingClientRect().width; 2299 h = bas.scale * this.container.getBoundingClientRect().height; 2300 2301 if (imgId === undefined || imgId === "") { 2302 newImg = true; 2303 img = new Image(); //doc.createElement('img'); 2304 img.style.width = w + "px"; 2305 img.style.height = h + "px"; 2306 } else { 2307 newImg = false; 2308 img = doc.getElementById(imgId); 2309 } 2310 // img.crossOrigin = 'anonymous'; 2311 2312 // Create div which contains canvas element and close button 2313 if (newImg) { 2314 node = doc.createElement("div"); 2315 node.style.cssText = bas.css; 2316 node.style.width = w + "px"; 2317 node.style.height = h + "px"; 2318 node.style.zIndex = this.container.style.zIndex + 120; 2319 2320 // Try to position the div exactly over the JSXGraph board 2321 node.style.position = "absolute"; 2322 node.style.top = this.container.offsetTop + "px"; 2323 node.style.left = this.container.offsetLeft + "px"; 2324 } 2325 2326 if (!isDebug) { 2327 // Create canvas element and add it to the DOM 2328 // It will be removed after the image has been stored. 2329 canvas = doc.createElement("canvas"); 2330 id = Math.random().toString(36).slice(2, 7); 2331 canvas.setAttribute("id", id); 2332 canvas.setAttribute("width", w); 2333 canvas.setAttribute("height", h); 2334 canvas.style.width = w + "px"; 2335 canvas.style.height = w + "px"; 2336 canvas.style.display = "none"; 2337 parent.appendChild(canvas); 2338 } else { 2339 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html 2340 id = "jxgbox_canvas"; 2341 canvas = doc.getElementById(id); 2342 } 2343 2344 if (newImg) { 2345 // Create close button 2346 button = doc.createElement("span"); 2347 buttonText = doc.createTextNode("\u2716"); 2348 button.style.cssText = bas.cssButton; 2349 button.appendChild(buttonText); 2350 button.onclick = function () { 2351 node.parentNode.removeChild(node); 2352 }; 2353 2354 // Add all nodes 2355 node.appendChild(img); 2356 node.appendChild(button); 2357 parent.insertBefore(node, this.container.nextSibling); 2358 } 2359 2360 // Hide navigation bar in board 2361 navbar = doc.getElementById(this.uniqName('navigationbar')); 2362 if (Type.exists(navbar)) { 2363 navbarDisplay = navbar.style.display; 2364 navbar.style.display = "none"; 2365 insert = this.removeToInsertLater(navbar); 2366 } 2367 2368 _copyCanvasToImg = function () { 2369 // Show image in img tag 2370 img.src = canvas.toDataURL("image/png"); 2371 2372 // Remove canvas node 2373 if (!isDebug) { 2374 parent.removeChild(canvas); 2375 } 2376 }; 2377 2378 // Create screenshot in image element 2379 if ("Promise" in window) { 2380 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg); 2381 } else { 2382 // IE 2383 this.dumpToCanvas(id, w, h, ignoreTexts); 2384 window.setTimeout(_copyCanvasToImg, 200); 2385 } 2386 2387 // Reinsert navigation bar in board 2388 if (Type.exists(navbar)) { 2389 navbar.style.display = navbarDisplay; 2390 insert(); 2391 } 2392 2393 return this; 2394 } 2395 } 2396 ); 2397 2398 export default JXG.SVGRenderer; 2399