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