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