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