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