1 /* 2 Copyright 2008-2023 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"; 36 import Options from "../options"; 37 import AbstractRenderer from "./abstract"; 38 import Const from "../base/constants"; 39 import Type from "../utils/type"; 40 import Color from "../utils/color"; 41 import Base64 from "../utils/base64"; 42 import Numerics from "../math/numerics"; 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 if (!Type.exists(node2) || node2.id !== str) { 893 node2 = this.container.ownerDocument.getElementById(str); 894 // Check if the marker already exists. 895 // If not, create a new marker 896 if (node2 === null) { 897 node2 = this._createArrowHead(el, "Start", a.typeFirst); 898 this.defs.appendChild(node2); 899 } 900 el.rendNodeTriangleStart = node2; 901 el.rendNode.setAttributeNS(null, "marker-start", this.toURL(str)); 902 } 903 } else if (Type.exists(node2)) { 904 this.remove(node2); 905 } 906 907 node2 = el.rendNodeTriangleEnd; 908 if (ev_la) { 909 str = this.toStr(this.container.id, '_', el.id, 'TriangleEnd', a.typeLast); 910 if (!Type.exists(node2) || node2.id !== str) { 911 node2 = this.container.ownerDocument.getElementById(str); 912 // Check if the marker already exists. 913 // If not, create a new marker 914 if (node2 === null) { 915 node2 = this._createArrowHead(el, "End", a.typeLast); 916 this.defs.appendChild(node2); 917 } 918 el.rendNodeTriangleEnd = node2; 919 el.rendNode.setAttributeNS(null, "marker-end", this.toURL(str)); 920 } 921 } else if (Type.exists(node2)) { 922 this.remove(node2); 923 } 924 }, 925 926 // Already documented in JXG.AbstractRenderer 927 updateEllipsePrim: function (node, x, y, rx, ry) { 928 var huge = 1000000; 929 930 huge = 200000; // IE 931 // webkit does not like huge values if the object is dashed 932 // iE doesn't like huge values above 216000 933 x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x); 934 y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y); 935 rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx); 936 ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry); 937 938 node.setAttributeNS(null, "cx", x); 939 node.setAttributeNS(null, "cy", y); 940 node.setAttributeNS(null, "rx", Math.abs(rx)); 941 node.setAttributeNS(null, "ry", Math.abs(ry)); 942 }, 943 944 // Already documented in JXG.AbstractRenderer 945 updateLinePrim: function (node, p1x, p1y, p2x, p2y) { 946 var huge = 1000000; 947 948 huge = 200000; //IE 949 if (!isNaN(p1x + p1y + p2x + p2y)) { 950 // webkit does not like huge values if the object is dashed 951 // IE doesn't like huge values above 216000 952 p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x); 953 p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y); 954 p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x); 955 p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y); 956 957 node.setAttributeNS(null, "x1", p1x); 958 node.setAttributeNS(null, "y1", p1y); 959 node.setAttributeNS(null, "x2", p2x); 960 node.setAttributeNS(null, "y2", p2y); 961 } 962 }, 963 964 // Already documented in JXG.AbstractRenderer 965 updatePathPrim: function (node, pointString) { 966 if (pointString === "") { 967 pointString = "M 0 0"; 968 } 969 node.setAttributeNS(null, "d", pointString); 970 }, 971 972 // Already documented in JXG.AbstractRenderer 973 updatePathStringPoint: function (el, size, type) { 974 var s = "", 975 scr = el.coords.scrCoords, 976 sqrt32 = size * Math.sqrt(3) * 0.5, 977 s05 = size * 0.5; 978 979 if (type === "x") { 980 s = 981 " M " + 982 (scr[1] - size) + 983 " " + 984 (scr[2] - size) + 985 " L " + 986 (scr[1] + size) + 987 " " + 988 (scr[2] + size) + 989 " M " + 990 (scr[1] + size) + 991 " " + 992 (scr[2] - size) + 993 " L " + 994 (scr[1] - size) + 995 " " + 996 (scr[2] + size); 997 } else if (type === "+") { 998 s = 999 " M " + 1000 (scr[1] - size) + 1001 " " + 1002 scr[2] + 1003 " L " + 1004 (scr[1] + size) + 1005 " " + 1006 scr[2] + 1007 " M " + 1008 scr[1] + 1009 " " + 1010 (scr[2] - size) + 1011 " L " + 1012 scr[1] + 1013 " " + 1014 (scr[2] + size); 1015 } else if (type === "|") { 1016 s = 1017 " M " + 1018 scr[1] + 1019 " " + 1020 (scr[2] - size) + 1021 " L " + 1022 scr[1] + 1023 " " + 1024 (scr[2] + size); 1025 } else if (type === "-") { 1026 s = 1027 " M " + 1028 (scr[1] - size) + 1029 " " + 1030 scr[2] + 1031 " L " + 1032 (scr[1] + size) + 1033 " " + 1034 scr[2]; 1035 } else if (type === "<>" || type === "<<>>") { 1036 if (type === "<<>>") { 1037 size *= 1.41; 1038 } 1039 s = 1040 " M " + 1041 (scr[1] - size) + 1042 " " + 1043 scr[2] + 1044 " L " + 1045 scr[1] + 1046 " " + 1047 (scr[2] + size) + 1048 " L " + 1049 (scr[1] + size) + 1050 " " + 1051 scr[2] + 1052 " L " + 1053 scr[1] + 1054 " " + 1055 (scr[2] - size) + 1056 " Z "; 1057 } else if (type === "^") { 1058 s = 1059 " M " + 1060 scr[1] + 1061 " " + 1062 (scr[2] - size) + 1063 " L " + 1064 (scr[1] - sqrt32) + 1065 " " + 1066 (scr[2] + s05) + 1067 " L " + 1068 (scr[1] + sqrt32) + 1069 " " + 1070 (scr[2] + s05) + 1071 " Z "; // close path 1072 } else if (type === "v") { 1073 s = 1074 " M " + 1075 scr[1] + 1076 " " + 1077 (scr[2] + size) + 1078 " L " + 1079 (scr[1] - sqrt32) + 1080 " " + 1081 (scr[2] - s05) + 1082 " L " + 1083 (scr[1] + sqrt32) + 1084 " " + 1085 (scr[2] - s05) + 1086 " Z "; 1087 } else if (type === ">") { 1088 s = 1089 " M " + 1090 (scr[1] + size) + 1091 " " + 1092 scr[2] + 1093 " L " + 1094 (scr[1] - s05) + 1095 " " + 1096 (scr[2] - sqrt32) + 1097 " L " + 1098 (scr[1] - s05) + 1099 " " + 1100 (scr[2] + sqrt32) + 1101 " Z "; 1102 } else if (type === "<") { 1103 s = 1104 " M " + 1105 (scr[1] - size) + 1106 " " + 1107 scr[2] + 1108 " L " + 1109 (scr[1] + s05) + 1110 " " + 1111 (scr[2] - sqrt32) + 1112 " L " + 1113 (scr[1] + s05) + 1114 " " + 1115 (scr[2] + sqrt32) + 1116 " Z "; 1117 } 1118 return s; 1119 }, 1120 1121 // Already documented in JXG.AbstractRenderer 1122 updatePathStringPrim: function (el) { 1123 var i, 1124 scr, 1125 len, 1126 symbm = " M ", 1127 symbl = " L ", 1128 symbc = " C ", 1129 nextSymb = symbm, 1130 maxSize = 5000.0, 1131 pStr = ""; 1132 1133 if (el.numberPoints <= 0) { 1134 return ""; 1135 } 1136 1137 len = Math.min(el.points.length, el.numberPoints); 1138 1139 if (el.bezierDegree === 1) { 1140 for (i = 0; i < len; i++) { 1141 scr = el.points[i].scrCoords; 1142 if (isNaN(scr[1]) || isNaN(scr[2])) { 1143 // PenUp 1144 nextSymb = symbm; 1145 } else { 1146 // Chrome has problems with values being too far away. 1147 scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize); 1148 scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize); 1149 1150 // Attention: first coordinate may be inaccurate if far way 1151 //pStr += [nextSymb, scr[1], ' ', scr[2]].join(''); 1152 pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox) 1153 nextSymb = symbl; 1154 } 1155 } 1156 } else if (el.bezierDegree === 3) { 1157 i = 0; 1158 while (i < len) { 1159 scr = el.points[i].scrCoords; 1160 if (isNaN(scr[1]) || isNaN(scr[2])) { 1161 // PenUp 1162 nextSymb = symbm; 1163 } else { 1164 pStr += nextSymb + scr[1] + " " + scr[2]; 1165 if (nextSymb === symbc) { 1166 i += 1; 1167 scr = el.points[i].scrCoords; 1168 pStr += " " + scr[1] + " " + scr[2]; 1169 i += 1; 1170 scr = el.points[i].scrCoords; 1171 pStr += " " + scr[1] + " " + scr[2]; 1172 } 1173 nextSymb = symbc; 1174 } 1175 i += 1; 1176 } 1177 } 1178 return pStr; 1179 }, 1180 1181 // Already documented in JXG.AbstractRenderer 1182 updatePathStringBezierPrim: function (el) { 1183 var i, 1184 j, 1185 k, 1186 scr, 1187 lx, 1188 ly, 1189 len, 1190 symbm = " M ", 1191 symbl = " C ", 1192 nextSymb = symbm, 1193 maxSize = 5000.0, 1194 pStr = "", 1195 f = Type.evaluate(el.visProp.strokewidth), 1196 isNoPlot = Type.evaluate(el.visProp.curvetype) !== "plot"; 1197 1198 if (el.numberPoints <= 0) { 1199 return ""; 1200 } 1201 1202 if (isNoPlot && el.board.options.curve.RDPsmoothing) { 1203 el.points = Numerics.RamerDouglasPeucker(el.points, 0.5); 1204 } 1205 1206 len = Math.min(el.points.length, el.numberPoints); 1207 for (j = 1; j < 3; j++) { 1208 nextSymb = symbm; 1209 for (i = 0; i < len; i++) { 1210 scr = el.points[i].scrCoords; 1211 1212 if (isNaN(scr[1]) || isNaN(scr[2])) { 1213 // PenUp 1214 nextSymb = symbm; 1215 } else { 1216 // Chrome has problems with values being too far away. 1217 scr[1] = Math.max(Math.min(scr[1], maxSize), -maxSize); 1218 scr[2] = Math.max(Math.min(scr[2], maxSize), -maxSize); 1219 1220 // Attention: first coordinate may be inaccurate if far way 1221 if (nextSymb === symbm) { 1222 //pStr += [nextSymb, scr[1], ' ', scr[2]].join(''); 1223 pStr += nextSymb + scr[1] + " " + scr[2]; // Seems to be faster now (webkit and firefox) 1224 } else { 1225 k = 2 * j; 1226 pStr += [ 1227 nextSymb, 1228 lx + (scr[1] - lx) * 0.333 + f * (k * Math.random() - j), 1229 " ", 1230 ly + (scr[2] - ly) * 0.333 + f * (k * Math.random() - j), 1231 " ", 1232 lx + (scr[1] - lx) * 0.666 + f * (k * Math.random() - j), 1233 " ", 1234 ly + (scr[2] - ly) * 0.666 + f * (k * Math.random() - j), 1235 " ", 1236 scr[1], 1237 " ", 1238 scr[2] 1239 ].join(""); 1240 } 1241 1242 nextSymb = symbl; 1243 lx = scr[1]; 1244 ly = scr[2]; 1245 } 1246 } 1247 } 1248 return pStr; 1249 }, 1250 1251 // Already documented in JXG.AbstractRenderer 1252 updatePolygonPrim: function (node, el) { 1253 var i, 1254 pStr = "", 1255 scrCoords, 1256 len = el.vertices.length; 1257 1258 node.setAttributeNS(null, "stroke", "none"); 1259 node.setAttributeNS(null, "fill-rule", "evenodd"); 1260 if (el.elType === "polygonalchain") { 1261 len++; 1262 } 1263 1264 for (i = 0; i < len - 1; i++) { 1265 if (el.vertices[i].isReal) { 1266 scrCoords = el.vertices[i].coords.scrCoords; 1267 pStr = pStr + scrCoords[1] + "," + scrCoords[2]; 1268 } else { 1269 node.setAttributeNS(null, "points", ""); 1270 return; 1271 } 1272 1273 if (i < len - 2) { 1274 pStr += " "; 1275 } 1276 } 1277 if (pStr.indexOf("NaN") === -1) { 1278 node.setAttributeNS(null, "points", pStr); 1279 } 1280 }, 1281 1282 // Already documented in JXG.AbstractRenderer 1283 updateRectPrim: function (node, x, y, w, h) { 1284 node.setAttributeNS(null, "x", x); 1285 node.setAttributeNS(null, "y", y); 1286 node.setAttributeNS(null, "width", w); 1287 node.setAttributeNS(null, "height", h); 1288 }, 1289 1290 /* ************************** 1291 * Set Attributes 1292 * **************************/ 1293 1294 // documented in JXG.AbstractRenderer 1295 setPropertyPrim: function (node, key, val) { 1296 if (key === "stroked") { 1297 return; 1298 } 1299 node.setAttributeNS(null, key, val); 1300 }, 1301 1302 display: function (el, val) { 1303 var node; 1304 1305 if (el && el.rendNode) { 1306 el.visPropOld.visible = val; 1307 node = el.rendNode; 1308 if (val) { 1309 node.setAttributeNS(null, "display", "inline"); 1310 node.style.visibility = "inherit"; 1311 } else { 1312 node.setAttributeNS(null, "display", "none"); 1313 node.style.visibility = "hidden"; 1314 } 1315 } 1316 }, 1317 1318 // documented in JXG.AbstractRenderer 1319 show: function (el) { 1320 JXG.deprecated("Board.renderer.show()", "Board.renderer.display()"); 1321 this.display(el, true); 1322 // var node; 1323 // 1324 // if (el && el.rendNode) { 1325 // node = el.rendNode; 1326 // node.setAttributeNS(null, 'display', 'inline'); 1327 // node.style.visibility = "inherit"; 1328 // } 1329 }, 1330 1331 // documented in JXG.AbstractRenderer 1332 hide: function (el) { 1333 JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()"); 1334 this.display(el, false); 1335 // var node; 1336 // 1337 // if (el && el.rendNode) { 1338 // node = el.rendNode; 1339 // node.setAttributeNS(null, 'display', 'none'); 1340 // node.style.visibility = "hidden"; 1341 // } 1342 }, 1343 1344 // documented in JXG.AbstractRenderer 1345 setBuffering: function (el, type) { 1346 el.rendNode.setAttribute("buffered-rendering", type); 1347 }, 1348 1349 // documented in JXG.AbstractRenderer 1350 setDashStyle: function (el) { 1351 var dashStyle = Type.evaluate(el.visProp.dash), 1352 ds = Type.evaluate(el.visProp.dashscale), 1353 sw = ds ? 0.5 * Type.evaluate(el.visProp.strokewidth) : 1, 1354 node = el.rendNode; 1355 1356 if (dashStyle > 0) { 1357 node.setAttributeNS(null, "stroke-dasharray", 1358 // sw could distinguish highlighting or not. 1359 // But it seems to preferable to ignore this. 1360 this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',') 1361 ); 1362 } else { 1363 if (node.hasAttributeNS(null, "stroke-dasharray")) { 1364 node.removeAttributeNS(null, "stroke-dasharray"); 1365 } 1366 } 1367 }, 1368 1369 // documented in JXG.AbstractRenderer 1370 setGradient: function (el) { 1371 var fillNode = el.rendNode, 1372 node, node2, node3, 1373 ev_g = Type.evaluate(el.visProp.gradient); 1374 1375 if (ev_g === "linear" || ev_g === "radial") { 1376 node = this.createPrim(ev_g + "Gradient", el.id + "_gradient"); 1377 node2 = this.createPrim("stop", el.id + "_gradient1"); 1378 node3 = this.createPrim("stop", el.id + "_gradient2"); 1379 node.appendChild(node2); 1380 node.appendChild(node3); 1381 this.defs.appendChild(node); 1382 fillNode.setAttributeNS( 1383 null, 1384 'style', 1385 // "fill:url(#" + this.container.id + "_" + el.id + "_gradient)" 1386 'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient') 1387 ); 1388 el.gradNode1 = node2; 1389 el.gradNode2 = node3; 1390 el.gradNode = node; 1391 } else { 1392 fillNode.removeAttributeNS(null, "style"); 1393 } 1394 }, 1395 1396 /** 1397 * Set the gradient angle for linear color gradients. 1398 * 1399 * @private 1400 * @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element. 1401 * @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom. 1402 */ 1403 updateGradientAngle: function (node, radians) { 1404 // Angles: 1405 // 0: -> 1406 // 90: down 1407 // 180: <- 1408 // 90: up 1409 var f = 1.0, 1410 co = Math.cos(radians), 1411 si = Math.sin(radians); 1412 1413 if (Math.abs(co) > Math.abs(si)) { 1414 f /= Math.abs(co); 1415 } else { 1416 f /= Math.abs(si); 1417 } 1418 1419 if (co >= 0) { 1420 node.setAttributeNS(null, "x1", 0); 1421 node.setAttributeNS(null, "x2", co * f); 1422 } else { 1423 node.setAttributeNS(null, "x1", -co * f); 1424 node.setAttributeNS(null, "x2", 0); 1425 } 1426 if (si >= 0) { 1427 node.setAttributeNS(null, "y1", 0); 1428 node.setAttributeNS(null, "y2", si * f); 1429 } else { 1430 node.setAttributeNS(null, "y1", -si * f); 1431 node.setAttributeNS(null, "y2", 0); 1432 } 1433 }, 1434 1435 /** 1436 * Set circles for radial color gradients. 1437 * 1438 * @private 1439 * @param {SVGnode} node SVG gradient node 1440 * @param {Number} cx SVG value cx (value between 0 and 1) 1441 * @param {Number} cy SVG value cy (value between 0 and 1) 1442 * @param {Number} r SVG value r (value between 0 and 1) 1443 * @param {Number} fx SVG value fx (value between 0 and 1) 1444 * @param {Number} fy SVG value fy (value between 0 and 1) 1445 * @param {Number} fr SVG value fr (value between 0 and 1) 1446 */ 1447 updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) { 1448 node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color 1449 node.setAttributeNS(null, "cy", cy * 100 + "%"); 1450 node.setAttributeNS(null, "r", r * 100 + "%"); 1451 node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point 1452 node.setAttributeNS(null, "fy", fy * 100 + "%"); 1453 node.setAttributeNS(null, "fr", fr * 100 + "%"); 1454 }, 1455 1456 // documented in JXG.AbstractRenderer 1457 updateGradient: function (el) { 1458 var col, 1459 op, 1460 node2 = el.gradNode1, 1461 node3 = el.gradNode2, 1462 ev_g = Type.evaluate(el.visProp.gradient); 1463 1464 if (!Type.exists(node2) || !Type.exists(node3)) { 1465 return; 1466 } 1467 1468 op = Type.evaluate(el.visProp.fillopacity); 1469 op = op > 0 ? op : 0; 1470 col = Type.evaluate(el.visProp.fillcolor); 1471 1472 node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op); 1473 node3.setAttributeNS( 1474 null, 1475 "style", 1476 "stop-color:" + 1477 Type.evaluate(el.visProp.gradientsecondcolor) + 1478 ";stop-opacity:" + 1479 Type.evaluate(el.visProp.gradientsecondopacity) 1480 ); 1481 node2.setAttributeNS( 1482 null, 1483 "offset", 1484 Type.evaluate(el.visProp.gradientstartoffset) * 100 + "%" 1485 ); 1486 node3.setAttributeNS( 1487 null, 1488 "offset", 1489 Type.evaluate(el.visProp.gradientendoffset) * 100 + "%" 1490 ); 1491 if (ev_g === "linear") { 1492 this.updateGradientAngle(el.gradNode, Type.evaluate(el.visProp.gradientangle)); 1493 } else if (ev_g === "radial") { 1494 this.updateGradientCircle( 1495 el.gradNode, 1496 Type.evaluate(el.visProp.gradientcx), 1497 Type.evaluate(el.visProp.gradientcy), 1498 Type.evaluate(el.visProp.gradientr), 1499 Type.evaluate(el.visProp.gradientfx), 1500 Type.evaluate(el.visProp.gradientfy), 1501 Type.evaluate(el.visProp.gradientfr) 1502 ); 1503 } 1504 }, 1505 1506 // documented in JXG.AbstractRenderer 1507 setObjectTransition: function (el, duration) { 1508 var node, props, 1509 transitionArr = [], 1510 transitionStr, 1511 i, 1512 len = 0, 1513 nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"]; 1514 1515 if (duration === undefined) { 1516 duration = Type.evaluate(el.visProp.transitionduration); 1517 } 1518 1519 props = Type.evaluate(el.visProp.transitionproperties); 1520 if (duration === el.visPropOld.transitionduration && 1521 props === el.visPropOld.transitionproperties) { 1522 return; 1523 } 1524 1525 // if ( 1526 // el.elementClass === Const.OBJECT_CLASS_TEXT && 1527 // Type.evaluate(el.visProp.display) === "html" 1528 // ) { 1529 // // transitionStr = " color " + duration + "ms," + 1530 // // " opacity " + duration + "ms"; 1531 // transitionStr = " all " + duration + "ms ease"; 1532 // } else { 1533 // transitionStr = 1534 // " fill " + duration + "ms," + 1535 // " fill-opacity " + duration + "ms," + 1536 // " stroke " + duration + "ms," + 1537 // " stroke-opacity " + duration + "ms," + 1538 // " stroke-width " + duration + "ms," + 1539 // " width " + duration + "ms," + 1540 // " height " + duration + "ms," + 1541 // " rx " + duration + "ms," + 1542 // " ry " + duration + "ms"; 1543 // } 1544 1545 if (Type.exists(props)) { 1546 len = props.length; 1547 } 1548 for (i = 0; i < len; i++) { 1549 transitionArr.push(props[i] + ' ' + duration + 'ms'); 1550 } 1551 transitionStr = transitionArr.join(', '); 1552 1553 len = nodes.length; 1554 for (i = 0; i < len; ++i) { 1555 if (el[nodes[i]]) { 1556 node = el[nodes[i]]; 1557 node.style.transition = transitionStr; 1558 } 1559 } 1560 1561 el.visPropOld.transitionduration = duration; 1562 el.visPropOld.transitionproperties = props; 1563 }, 1564 1565 // documented in JXG.AbstractRenderer 1566 setObjectViewport: function(el, isHtml) { 1567 var val = Type.evaluate(el.visProp.viewport), 1568 vp, i, 1569 len = 0, 1570 bb, bbc, l, t, r, b, 1571 nodes = ['rendNode']; //, "rendNodeTriangleStart", "rendNodeTriangleEnd"]; 1572 1573 // Check viewport attribute of the board 1574 if (val === 'inherit') { 1575 val = Type.evaluate(el.board.attr.viewport); 1576 } 1577 1578 // Required order: top, right, bottom, left 1579 if (isHtml) { 1580 bb = el.rendNode.getBoundingClientRect(); 1581 bbc = this.container.getBoundingClientRect(); 1582 t = parseFloat(val[1]); 1583 r = parseFloat(val[2]); 1584 b = parseFloat(val[3]); 1585 l = parseFloat(val[0]); 1586 1587 if (Type.isString(val[1]) && val[1].indexOf('%') > 0) { 1588 t = (bbc.height) * t / 100; 1589 } 1590 if (Type.isString(val[2]) && val[2].indexOf('%') > 0) { 1591 r = (bbc.width) * r / 100; 1592 } 1593 if (Type.isString(val[3]) && val[3].indexOf('%') > 0) { 1594 b = (bbc.height) * b / 100; 1595 } 1596 if (Type.isString(val[0]) && val[0].indexOf('%') > 0) { 1597 l = (bbc.width) * l / 100; 1598 } 1599 1600 t = parseFloat(bbc.top) - parseFloat(bb.top) + t; 1601 r = parseFloat(bb.right) - parseFloat(bbc.right) + r; 1602 b = parseFloat(bb.bottom) - parseFloat(bbc.bottom) + b; 1603 l = parseFloat(bbc.left) - parseFloat(bb.left) + l; 1604 val = [l, t, r, b]; 1605 } 1606 1607 vp = [ 1608 (typeof val[1] === 'number') ? val[1] + 'px' : val[1], 1609 (typeof val[2] === 'number') ? val[2] + 'px' : val[2], 1610 (typeof val[3] === 'number') ? val[3] + 'px' : val[3], 1611 (typeof val[0] === 'number') ? val[0] + 'px' : val[0] 1612 ].join(' '); 1613 1614 len = nodes.length; 1615 for (i = 0; i < len; ++i) { 1616 if (el[nodes[i]]) { 1617 if (isHtml) { 1618 el[nodes[i]].style.clipPath = 'inset(' + vp + ')'; 1619 } else { 1620 el[nodes[i]].setAttributeNS(null, "clip-path", 'view-box inset(' + vp + ')'); 1621 } 1622 } 1623 } 1624 }, 1625 1626 /** 1627 * Call user-defined function to set visual attributes. 1628 * If "testAttribute" is the empty string, the function 1629 * is called immediately, otherwise it is called in a timeOut. 1630 * 1631 * This is necessary to realize smooth transitions but avoid transitions 1632 * when first creating the objects. 1633 * 1634 * Usually, the string in testAttribute is the visPropOld attribute 1635 * of the values which are set. 1636 * 1637 * @param {Function} setFunc Some function which usually sets some attributes 1638 * @param {String} testAttribute If this string is the empty string the function is called immediately, 1639 * otherwise it is called in a setImeout. 1640 * @see JXG.SVGRenderer#setObjectFillColor 1641 * @see JXG.SVGRenderer#setObjectStrokeColor 1642 * @see JXG.SVGRenderer#_setArrowColor 1643 * @private 1644 */ 1645 _setAttribute: function (setFunc, testAttribute) { 1646 if (testAttribute === "") { 1647 setFunc(); 1648 } else { 1649 window.setTimeout(setFunc, 1); 1650 } 1651 }, 1652 1653 // documented in JXG.AbstractRenderer 1654 setObjectFillColor: function (el, color, opacity, rendNode) { 1655 var node, c, rgbo, oo, 1656 rgba = Type.evaluate(color), 1657 o = Type.evaluate(opacity), 1658 grad = Type.evaluate(el.visProp.gradient); 1659 1660 o = o > 0 ? o : 0; 1661 1662 // TODO save gradient and gradientangle 1663 if ( 1664 el.visPropOld.fillcolor === rgba && 1665 el.visPropOld.fillopacity === o && 1666 grad === null 1667 ) { 1668 return; 1669 } 1670 1671 if (Type.exists(rgba) && rgba !== false) { 1672 if (rgba.length !== 9) { 1673 // RGB, not RGBA 1674 c = rgba; 1675 oo = o; 1676 } else { 1677 // True RGBA, not RGB 1678 rgbo = Color.rgba2rgbo(rgba); 1679 c = rgbo[0]; 1680 oo = o * rgbo[1]; 1681 } 1682 1683 if (rendNode === undefined) { 1684 node = el.rendNode; 1685 } else { 1686 node = rendNode; 1687 } 1688 1689 if (c !== "none") { 1690 this._setAttribute(function () { 1691 node.setAttributeNS(null, "fill", c); 1692 }, el.visPropOld.fillcolor); 1693 } 1694 1695 if (el.type === JXG.OBJECT_TYPE_IMAGE) { 1696 this._setAttribute(function () { 1697 node.setAttributeNS(null, "opacity", oo); 1698 }, el.visPropOld.fillopacity); 1699 //node.style['opacity'] = oo; // This would overwrite values set by CSS class. 1700 } else { 1701 if (c === "none") { 1702 // This is done only for non-images 1703 // because images have no fill color. 1704 oo = 0; 1705 // This is necessary if there is a foreignObject below. 1706 node.setAttributeNS(null, "pointer-events", "visibleStroke"); 1707 } else { 1708 // This is the default 1709 node.setAttributeNS(null, "pointer-events", "visiblePainted"); 1710 } 1711 this._setAttribute(function () { 1712 node.setAttributeNS(null, "fill-opacity", oo); 1713 }, el.visPropOld.fillopacity); 1714 } 1715 1716 if (grad === "linear" || grad === "radial") { 1717 this.updateGradient(el); 1718 } 1719 } 1720 el.visPropOld.fillcolor = rgba; 1721 el.visPropOld.fillopacity = o; 1722 }, 1723 1724 // documented in JXG.AbstractRenderer 1725 setObjectStrokeColor: function (el, color, opacity) { 1726 var rgba = Type.evaluate(color), 1727 c, rgbo, 1728 o = Type.evaluate(opacity), 1729 oo, node; 1730 1731 o = o > 0 ? o : 0; 1732 1733 if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) { 1734 return; 1735 } 1736 1737 if (Type.exists(rgba) && rgba !== false) { 1738 if (rgba.length !== 9) { 1739 // RGB, not RGBA 1740 c = rgba; 1741 oo = o; 1742 } else { 1743 // True RGBA, not RGB 1744 rgbo = Color.rgba2rgbo(rgba); 1745 c = rgbo[0]; 1746 oo = o * rgbo[1]; 1747 } 1748 1749 node = el.rendNode; 1750 1751 if (el.elementClass === Const.OBJECT_CLASS_TEXT) { 1752 if (Type.evaluate(el.visProp.display) === "html") { 1753 this._setAttribute(function () { 1754 node.style.color = c; 1755 node.style.opacity = oo; 1756 }, el.visPropOld.strokecolor); 1757 } else { 1758 this._setAttribute(function () { 1759 node.setAttributeNS(null, "style", "fill:" + c); 1760 node.setAttributeNS(null, "style", "fill-opacity:" + oo); 1761 }, el.visPropOld.strokecolor); 1762 } 1763 } else { 1764 this._setAttribute(function () { 1765 node.setAttributeNS(null, "stroke", c); 1766 node.setAttributeNS(null, "stroke-opacity", oo); 1767 }, el.visPropOld.strokecolor); 1768 } 1769 1770 if ( 1771 el.elementClass === Const.OBJECT_CLASS_CURVE || 1772 el.elementClass === Const.OBJECT_CLASS_LINE 1773 ) { 1774 if (Type.evaluate(el.visProp.firstarrow)) { 1775 this._setArrowColor( 1776 el.rendNodeTriangleStart, 1777 c, oo, el, 1778 el.visPropCalc.typeFirst 1779 ); 1780 } 1781 1782 if (Type.evaluate(el.visProp.lastarrow)) { 1783 this._setArrowColor( 1784 el.rendNodeTriangleEnd, 1785 c, oo, el, 1786 el.visPropCalc.typeLast 1787 ); 1788 } 1789 } 1790 } 1791 1792 el.visPropOld.strokecolor = rgba; 1793 el.visPropOld.strokeopacity = o; 1794 }, 1795 1796 // documented in JXG.AbstractRenderer 1797 setObjectStrokeWidth: function (el, width) { 1798 var node, 1799 w = Type.evaluate(width); 1800 1801 if (isNaN(w) || el.visPropOld.strokewidth === w) { 1802 return; 1803 } 1804 1805 node = el.rendNode; 1806 this.setPropertyPrim(node, "stroked", "true"); 1807 if (Type.exists(w)) { 1808 this.setPropertyPrim(node, "stroke-width", w + "px"); 1809 1810 // if (el.elementClass === Const.OBJECT_CLASS_CURVE || 1811 // el.elementClass === Const.OBJECT_CLASS_LINE) { 1812 // if (Type.evaluate(el.visProp.firstarrow)) { 1813 // this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode); 1814 // } 1815 // 1816 // if (Type.evaluate(el.visProp.lastarrow)) { 1817 // this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode); 1818 // } 1819 // } 1820 } 1821 el.visPropOld.strokewidth = w; 1822 }, 1823 1824 // documented in JXG.AbstractRenderer 1825 setLineCap: function (el) { 1826 var capStyle = Type.evaluate(el.visProp.linecap); 1827 1828 if ( 1829 capStyle === undefined || 1830 capStyle === "" || 1831 el.visPropOld.linecap === capStyle || 1832 !Type.exists(el.rendNode) 1833 ) { 1834 return; 1835 } 1836 1837 this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle); 1838 el.visPropOld.linecap = capStyle; 1839 }, 1840 1841 // documented in JXG.AbstractRenderer 1842 setShadow: function (el) { 1843 var ev_s = Type.evaluate(el.visProp.shadow), 1844 ev_s_json, c, b, bl, o, op, id, node, 1845 use_board_filter = true, 1846 show = false; 1847 1848 ev_s_json = JSON.stringify(ev_s); 1849 if (ev_s_json === el.visPropOld.shadow) { 1850 return; 1851 } 1852 1853 if (typeof ev_s === 'boolean') { 1854 use_board_filter = true; 1855 show = ev_s; 1856 c = 'none'; 1857 b = 3; 1858 bl = 0.1; 1859 o = [5, 5]; 1860 op = 1; 1861 } else { 1862 if (Type.evaluate(ev_s.enabled)) { 1863 use_board_filter = false; 1864 show = true; 1865 c = JXG.rgbParser(Type.evaluate(ev_s.color)); 1866 b = Type.evaluate(ev_s.blur); 1867 bl = Type.evaluate(ev_s.blend); 1868 o = Type.evaluate(ev_s.offset); 1869 op = Type.evaluate(ev_s.opacity); 1870 } else { 1871 show = false; 1872 } 1873 } 1874 1875 if (Type.exists(el.rendNode)) { 1876 if (show) { 1877 if (use_board_filter) { 1878 el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1')); 1879 // 'url(#' + this.container.id + '_' + 'f1)'); 1880 } else { 1881 node = this.container.ownerDocument.getElementById(id); 1882 if (node) { 1883 this.defs.removeChild(node); 1884 } 1885 id = el.rendNode.id + '_' + 'f1'; 1886 this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o)); 1887 el.rendNode.setAttributeNS(null, 'filter', this.toURL(id)); 1888 // 'url(#' + id + ')'); 1889 } 1890 } else { 1891 el.rendNode.removeAttributeNS(null, 'filter'); 1892 } 1893 } 1894 1895 el.visPropOld.shadow = ev_s_json; 1896 }, 1897 1898 /* ************************** 1899 * renderer control 1900 * **************************/ 1901 1902 // documented in JXG.AbstractRenderer 1903 suspendRedraw: function () { 1904 // It seems to be important for the Linux version of firefox 1905 this.suspendHandle = this.svgRoot.suspendRedraw(10000); 1906 }, 1907 1908 // documented in JXG.AbstractRenderer 1909 unsuspendRedraw: function () { 1910 this.svgRoot.unsuspendRedraw(this.suspendHandle); 1911 // this.svgRoot.unsuspendRedrawAll(); 1912 //this.svgRoot.forceRedraw(); 1913 }, 1914 1915 // documented in AbstractRenderer 1916 resize: function (w, h) { 1917 this.svgRoot.setAttribute("width", parseFloat(w)); 1918 this.svgRoot.setAttribute("height", parseFloat(h)); 1919 }, 1920 1921 // documented in JXG.AbstractRenderer 1922 createTouchpoints: function (n) { 1923 var i, na1, na2, node; 1924 this.touchpoints = []; 1925 for (i = 0; i < n; i++) { 1926 na1 = "touchpoint1_" + i; 1927 node = this.createPrim("path", na1); 1928 this.appendChildPrim(node, 19); 1929 node.setAttributeNS(null, "d", "M 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, "stroke-opacity", 1.0); 1936 node.setAttributeNS(null, "display", "none"); 1937 1938 na2 = "touchpoint2_" + i; 1939 node = this.createPrim("ellipse", na2); 1940 this.appendChildPrim(node, 19); 1941 this.updateEllipsePrim(node, 0, 0, 0, 0); 1942 this.touchpoints.push(node); 1943 1944 this.setPropertyPrim(node, "stroked", "true"); 1945 this.setPropertyPrim(node, "stroke-width", "1px"); 1946 node.setAttributeNS(null, "stroke", "#000000"); 1947 node.setAttributeNS(null, "stroke-opacity", 1.0); 1948 node.setAttributeNS(null, "fill", "#ffffff"); 1949 node.setAttributeNS(null, "fill-opacity", 0.0); 1950 1951 node.setAttributeNS(null, "display", "none"); 1952 } 1953 }, 1954 1955 // documented in JXG.AbstractRenderer 1956 showTouchpoint: function (i) { 1957 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1958 this.touchpoints[2 * i].setAttributeNS(null, "display", "inline"); 1959 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "inline"); 1960 } 1961 }, 1962 1963 // documented in JXG.AbstractRenderer 1964 hideTouchpoint: function (i) { 1965 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1966 this.touchpoints[2 * i].setAttributeNS(null, "display", "none"); 1967 this.touchpoints[2 * i + 1].setAttributeNS(null, "display", "none"); 1968 } 1969 }, 1970 1971 // documented in JXG.AbstractRenderer 1972 updateTouchpoint: function (i, pos) { 1973 var x, 1974 y, 1975 d = 37; 1976 1977 if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) { 1978 x = pos[0]; 1979 y = pos[1]; 1980 1981 this.touchpoints[2 * i].setAttributeNS( 1982 null, 1983 "d", 1984 "M " + 1985 (x - d) + 1986 " " + 1987 y + 1988 " " + 1989 "L " + 1990 (x + d) + 1991 " " + 1992 y + 1993 " " + 1994 "M " + 1995 x + 1996 " " + 1997 (y - d) + 1998 " " + 1999 "L " + 2000 x + 2001 " " + 2002 (y + d) 2003 ); 2004 this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25); 2005 } 2006 }, 2007 2008 /** 2009 * Walk recursively through the DOM subtree of a node and collect all 2010 * value attributes together with the id of that node. 2011 * <b>Attention:</b> Only values of nodes having a valid id are taken. 2012 * @param {Node} node root node of DOM subtree that will be searched recursively. 2013 * @return {Array} Array with entries of the form [id, value] 2014 * @private 2015 */ 2016 _getValuesOfDOMElements: function (node) { 2017 var values = []; 2018 if (node.nodeType === 1) { 2019 node = node.firstChild; 2020 while (node) { 2021 if (node.id !== undefined && node.value !== undefined) { 2022 values.push([node.id, node.value]); 2023 } 2024 values = values.concat(this._getValuesOfDOMElements(node)); 2025 node = node.nextSibling; 2026 } 2027 } 2028 return values; 2029 }, 2030 2031 _getDataUri: function (url, callback) { 2032 var image = new Image(); 2033 2034 image.onload = function () { 2035 var canvas = document.createElement("canvas"); 2036 canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size 2037 canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size 2038 2039 canvas.getContext("2d").drawImage(this, 0, 0); 2040 2041 callback(canvas.toDataURL("image/png")); 2042 canvas.remove(); 2043 }; 2044 2045 image.src = url; 2046 }, 2047 2048 _getImgDataURL: function (svgRoot) { 2049 var images, len, canvas, ctx, ur, i; 2050 2051 images = svgRoot.getElementsByTagName("image"); 2052 len = images.length; 2053 if (len > 0) { 2054 canvas = document.createElement("canvas"); 2055 //img = new Image(); 2056 for (i = 0; i < len; i++) { 2057 images[i].setAttribute("crossorigin", "anonymous"); 2058 //img.src = images[i].href; 2059 //img.onload = function() { 2060 // img.crossOrigin = "anonymous"; 2061 ctx = canvas.getContext("2d"); 2062 canvas.width = images[i].getAttribute("width"); 2063 canvas.height = images[i].getAttribute("height"); 2064 try { 2065 ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height); 2066 2067 // If the image is not png, the format must be specified here 2068 ur = canvas.toDataURL(); 2069 images[i].setAttribute("xlink:href", ur); 2070 } catch (err) { 2071 console.log("CORS problem! Image can not be used", err); 2072 } 2073 } 2074 //canvas.remove(); 2075 } 2076 return true; 2077 }, 2078 2079 /** 2080 * Return a data URI of the SVG code representing the construction. 2081 * The SVG code of the construction is base64 encoded. The return string starts 2082 * with "data:image/svg+xml;base64,...". 2083 * 2084 * @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none. 2085 * This is necessary for older versions of Safari. Default: false 2086 * @returns {String} data URI string 2087 * 2088 * @example 2089 * var A = board.create('point', [2, 2]); 2090 * 2091 * var txt = board.renderer.dumpToDataURI(false); 2092 * // txt consists of a string of the form 2093 * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+ 2094 * // Behind the comma, there is the base64 encoded SVG code 2095 * // which is decoded with atob(). 2096 * // The call of decodeURIComponent(escape(...)) is necessary 2097 * // to handle unicode strings correctly. 2098 * var ar = txt.split(','); 2099 * document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1]))); 2100 * 2101 * </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div> 2102 * <textarea id="output2023" rows="5" cols="50"></textarea> 2103 * <script type="text/javascript"> 2104 * (function() { 2105 * var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d', 2106 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2107 * var A = board.create('point', [2, 2]); 2108 * 2109 * var txt = board.renderer.dumpToDataURI(false); 2110 * // txt consists of a string of the form 2111 * // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+ 2112 * // Behind the comma, there is the base64 encoded SVG code 2113 * // which is decoded with atob(). 2114 * // The call of decodeURIComponent(escape(...)) is necessary 2115 * // to handle unicode strings correctly. 2116 * var ar = txt.split(','); 2117 * document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1]))); 2118 * 2119 * })(); 2120 * 2121 * </script><pre> 2122 * 2123 */ 2124 dumpToDataURI: function (ignoreTexts) { 2125 var svgRoot = this.svgRoot, 2126 btoa = window.btoa || Base64.encode, 2127 svg, i, len, 2128 values = []; 2129 2130 // Move all HTML tags (beside the SVG root) of the container 2131 // to the foreignObject element inside of the svgRoot node 2132 // Problem: 2133 // input values are not copied. This can be verified by looking at an innerHTML output 2134 // of an input element. Therefore, we do it "by hand". 2135 if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) { 2136 if (!ignoreTexts) { 2137 this.foreignObjLayer.setAttribute("display", "inline"); 2138 while (svgRoot.nextSibling) { 2139 // Copy all value attributes 2140 values = values.concat(this._getValuesOfDOMElements(svgRoot.nextSibling)); 2141 this.foreignObjLayer.appendChild(svgRoot.nextSibling); 2142 } 2143 } 2144 } 2145 2146 this._getImgDataURL(svgRoot); 2147 2148 // Convert the SVG graphic into a string containing SVG code 2149 svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 2150 svg = new XMLSerializer().serializeToString(svgRoot); 2151 2152 if (ignoreTexts !== true) { 2153 // Handle SVG texts 2154 // Insert all value attributes back into the svg string 2155 len = values.length; 2156 for (i = 0; i < len; i++) { 2157 svg = svg.replace( 2158 'id="' + values[i][0] + '"', 2159 'id="' + values[i][0] + '" value="' + values[i][1] + '"' 2160 ); 2161 } 2162 } 2163 2164 // if (false) { 2165 // // Debug: use example svg image 2166 // 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>'; 2167 // } 2168 2169 // In IE we have to remove the namespace again. 2170 if ((svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1) { 2171 svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, ""); 2172 } 2173 2174 // Safari fails if the svg string contains a " " 2175 // Obsolete with Safari 12+ 2176 svg = svg.replace(/ /g, " "); 2177 svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); 2178 2179 // Move all HTML tags back from 2180 // the foreignObject element to the container 2181 if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) { 2182 // Restore all HTML elements 2183 while (this.foreignObjLayer.firstChild) { 2184 this.container.appendChild(this.foreignObjLayer.firstChild); 2185 } 2186 this.foreignObjLayer.setAttribute("display", "none"); 2187 } 2188 2189 return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); 2190 }, 2191 2192 /** 2193 * Convert the SVG construction into an HTML canvas image. 2194 * This works for all SVG supporting browsers. Implemented as Promise. 2195 * <p> 2196 * Might fail if any text element or foreign object element contains SVG. This 2197 * is the case e.g. for the default fullscreen symbol. 2198 * <p> 2199 * For IE, it is realized as function. 2200 * It works from version 9, with the exception that HTML texts 2201 * are ignored on IE. The drawing is done with a delay of 2202 * 200 ms. Otherwise there would be problems with IE. 2203 * 2204 * @param {String} canvasId Id of an HTML canvas element 2205 * @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag. 2206 * @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag. 2207 * @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root. 2208 * This is necessary for older versions of Safari. Default: false 2209 * @returns {Promise} Promise object 2210 * 2211 * @example 2212 * board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); }); 2213 * 2214 * @example 2215 * // IE 11 example: 2216 * board.renderer.dumpToCanvas('canvas'); 2217 * setTimeout(function() { console.log('done'); }, 400); 2218 */ 2219 dumpToCanvas: function (canvasId, w, h, ignoreTexts) { 2220 var svg, tmpImg, 2221 cv, ctx, 2222 doc = this.container.ownerDocument; 2223 2224 // Prepare the canvas element 2225 cv = doc.getElementById(canvasId); 2226 2227 // Clear the canvas 2228 /* eslint-disable no-self-assign */ 2229 cv.width = cv.width; 2230 /* eslint-enable no-self-assign */ 2231 2232 ctx = cv.getContext("2d"); 2233 if (w !== undefined && h !== undefined) { 2234 cv.style.width = parseFloat(w) + "px"; 2235 cv.style.height = parseFloat(h) + "px"; 2236 // Scale twice the CSS size to make the image crisp 2237 // cv.setAttribute('width', 2 * parseFloat(wOrg)); 2238 // cv.setAttribute('height', 2 * parseFloat(hOrg)); 2239 // ctx.scale(2 * wOrg / w, 2 * hOrg / h); 2240 cv.setAttribute("width", parseFloat(w)); 2241 cv.setAttribute("height", parseFloat(h)); 2242 } 2243 2244 // Display the SVG string as data-uri in an HTML img. 2245 /** 2246 * @type {Image} 2247 * @ignore 2248 * {ignore} 2249 */ 2250 tmpImg = new Image(); 2251 svg = this.dumpToDataURI(ignoreTexts); 2252 tmpImg.src = svg; 2253 2254 // Finally, draw the HTML img in the canvas. 2255 if (!("Promise" in window)) { 2256 /** 2257 * @function 2258 * @ignore 2259 */ 2260 tmpImg.onload = function () { 2261 // IE needs a pause... 2262 // Seems to be broken 2263 window.setTimeout(function () { 2264 try { 2265 ctx.drawImage(tmpImg, 0, 0, w, h); 2266 } catch (err) { 2267 console.log("screenshots not longer supported on IE"); 2268 } 2269 }, 200); 2270 }; 2271 return this; 2272 } 2273 2274 return new Promise(function (resolve, reject) { 2275 try { 2276 tmpImg.onload = function () { 2277 ctx.drawImage(tmpImg, 0, 0, w, h); 2278 resolve(); 2279 }; 2280 } catch (e) { 2281 reject(e); 2282 } 2283 }); 2284 }, 2285 2286 /** 2287 * Display SVG image in html img-tag which enables 2288 * easy download for the user. 2289 * 2290 * Support: 2291 * <ul> 2292 * <li> IE: No 2293 * <li> Edge: full 2294 * <li> Firefox: full 2295 * <li> Chrome: full 2296 * <li> Safari: full (No text support in versions prior to 12). 2297 * </ul> 2298 * 2299 * @param {JXG.Board} board Link to the board. 2300 * @param {String} imgId Optional id of an img object. If given and different from the empty string, 2301 * the screenshot is copied to this img object. The width and height will be set to the values of the 2302 * JSXGraph container. 2303 * @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the 2304 * SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false 2305 * @return {Object} the svg renderer object 2306 */ 2307 screenshot: function (board, imgId, ignoreTexts) { 2308 var node, 2309 doc = this.container.ownerDocument, 2310 parent = this.container.parentNode, 2311 // cPos, 2312 // cssTxt, 2313 canvas, id, img, 2314 button, buttonText, 2315 w, h, 2316 bas = board.attr.screenshot, 2317 navbar, navbarDisplay, insert, 2318 newImg = false, 2319 _copyCanvasToImg, 2320 isDebug = false; 2321 2322 if (this.type === "no") { 2323 return this; 2324 } 2325 2326 w = bas.scale * this.container.getBoundingClientRect().width; 2327 h = bas.scale * this.container.getBoundingClientRect().height; 2328 2329 if (imgId === undefined || imgId === "") { 2330 newImg = true; 2331 img = new Image(); //doc.createElement('img'); 2332 img.style.width = w + "px"; 2333 img.style.height = h + "px"; 2334 } else { 2335 newImg = false; 2336 img = doc.getElementById(imgId); 2337 } 2338 // img.crossOrigin = 'anonymous'; 2339 2340 // Create div which contains canvas element and close button 2341 if (newImg) { 2342 node = doc.createElement("div"); 2343 node.style.cssText = bas.css; 2344 node.style.width = w + "px"; 2345 node.style.height = h + "px"; 2346 node.style.zIndex = this.container.style.zIndex + 120; 2347 2348 // Try to position the div exactly over the JSXGraph board 2349 node.style.position = "absolute"; 2350 node.style.top = this.container.offsetTop + "px"; 2351 node.style.left = this.container.offsetLeft + "px"; 2352 } 2353 2354 if (!isDebug) { 2355 // Create canvas element and add it to the DOM 2356 // It will be removed after the image has been stored. 2357 canvas = doc.createElement("canvas"); 2358 id = Math.random().toString(36).slice(2, 7); 2359 canvas.setAttribute("id", id); 2360 canvas.setAttribute("width", w); 2361 canvas.setAttribute("height", h); 2362 canvas.style.width = w + "px"; 2363 canvas.style.height = w + "px"; 2364 canvas.style.display = "none"; 2365 parent.appendChild(canvas); 2366 } else { 2367 // Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html 2368 id = "jxgbox_canvas"; 2369 // canvas = document.getElementById(id); 2370 canvas = doc.getElementById(id); 2371 } 2372 2373 if (newImg) { 2374 // Create close button 2375 button = doc.createElement("span"); 2376 buttonText = doc.createTextNode("\u2716"); 2377 button.style.cssText = bas.cssButton; 2378 button.appendChild(buttonText); 2379 button.onclick = function () { 2380 node.parentNode.removeChild(node); 2381 }; 2382 2383 // Add all nodes 2384 node.appendChild(img); 2385 node.appendChild(button); 2386 parent.insertBefore(node, this.container.nextSibling); 2387 } 2388 2389 // Hide navigation bar in board 2390 navbar = doc.getElementById(this.uniqName('navigationbar')); 2391 if (Type.exists(navbar)) { 2392 navbarDisplay = navbar.style.display; 2393 navbar.style.display = "none"; 2394 insert = this.removeToInsertLater(navbar); 2395 } 2396 2397 _copyCanvasToImg = function () { 2398 // Show image in img tag 2399 img.src = canvas.toDataURL("image/png"); 2400 2401 // Remove canvas node 2402 if (!isDebug) { 2403 parent.removeChild(canvas); 2404 } 2405 }; 2406 2407 // Create screenshot in image element 2408 if ("Promise" in window) { 2409 this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg); 2410 } else { 2411 // IE 2412 this.dumpToCanvas(id, w, h, ignoreTexts); 2413 window.setTimeout(_copyCanvasToImg, 200); 2414 } 2415 2416 // Reinsert navigation bar in board 2417 if (Type.exists(navbar)) { 2418 navbar.style.display = navbarDisplay; 2419 insert(); 2420 } 2421 2422 return this; 2423 } 2424 } 2425 ); 2426 2427 export default JXG.SVGRenderer; 2428