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