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