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