/*
Copyright 2008-2026
Matthias Ehmann,
Michael Gerhaeuser,
Carsten Miller,
Bianca Valentin,
Alfred Wassermann,
Peter Wilfahrt
This file is part of JSXGraph.
JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
You can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version
OR
* MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
JSXGraph is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License and
the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
and <https://opensource.org/licenses/MIT/>.
*/
/*global JXG: true, define: true, AMprocessNode: true, MathJax: true, document: true */
/*jslint nomen: true, plusplus: true, newcap:true*/
import JXG from "../jxg.js";
import Options from "../options.js";
import AbstractRenderer from "./abstract.js";
import Const from "../base/constants.js";
import Env from "../utils/env.js";
import Type from "../utils/type.js";
import Color from "../utils/color.js";
import Base64 from "../utils/base64.js";
import Numerics from "../math/numerics.js";
/**
* @class Uses SVG to implement the rendering methods defined in {@link JXG.AbstractRenderer}.
*
* @augments JXG.AbstractRenderer
* @param {Node} container Reference to a DOM node containing the board.
* @param {Object} dim The dimensions of the board
* @param {Number} dim.width
* @param {Number} dim.height
* @see JXG.CanvasRenderer
* @see JXG.NoRenderer
*/
JXG.SVGRenderer = function (container, dim) {
var i;
// docstring in AbstractRenderer
this.type = 'svg';
this.isIE =
typeof navigator !== 'undefined' &&
(navigator.appVersion.indexOf('MSIE') !== -1 || navigator.userAgent.match(/Trident\//));
/**
* SVG root node
* @type Node
*/
this.svgRoot = null;
/**
* The SVG Namespace used in JSXGraph.
* @see http://www.w3.org/TR/SVG2/
* @type String
* @default http://www.w3.org/2000/svg
*/
this.svgNamespace = "http://www.w3.org/2000/svg";
/**
* The xlink namespace. This is used for images.
* @see http://www.w3.org/TR/xlink/
* @type String
* @default http://www.w3.org/1999/xlink
*/
this.xlinkNamespace = "http://www.w3.org/1999/xlink";
// container is documented in AbstractRenderer.
// Type node
this.container = container;
// prepare the div container and the svg root node for use with JSXGraph
this.container.style.MozUserSelect = 'none';
this.container.style.userSelect = 'none';
this.container.style.overflow = 'hidden';
if (this.container.style.position === "") {
this.container.style.position = 'relative';
}
this.svgRoot = this.container.ownerDocument.createElementNS(this.svgNamespace, 'svg');
this.svgRoot.style.overflow = 'hidden';
this.svgRoot.style.display = 'block';
this.resize(dim.width, dim.height);
//this.svgRoot.setAttributeNS(null, 'shape-rendering', 'crispEdge'); //'optimizeQuality'); //geometricPrecision');
this.container.appendChild(this.svgRoot);
/**
* The `defs` element is a container element to reference reusable SVG elements.
* @type Node
* @see https://www.w3.org/TR/SVG2/struct.html#DefsElement
*/
this.defs = this.container.ownerDocument.createElementNS(this.svgNamespace, 'defs');
this.svgRoot.appendChild(this.defs);
/**
* Filters are used to apply shadows.
* @type Node
* @see https://www.w3.org/TR/SVG2/struct.html#DefsElement
*/
/**
* Create an SVG shadow filter. If the object's RGB color is [r,g,b], it's opacity is op, and
* the parameter color is given as [r', g', b'] with opacity op'
* the shadow will have RGB color [blend*r + r', blend*g + g', blend*b + b'] and the opacity will be equal to op * op'.
* Further, blur and offset can be adjusted.
*
* The shadow color is [r*ble
* @param {String} id Node is of the filter.
* @param {Array|String} rgb RGB value for the blend color or the string 'none' for default values. Default 'black'.
* @param {Number} opacity Value between 0 and 1, default is 1.
* @param {Number} blend Value between 0 and 1, default is 0.1.
* @param {Number} blur Default: 3
* @param {Array} offset [dx, dy]. Default is [5,5].
* @returns DOM node to be added to this.defs.
* @private
*/
this.createShadowFilter = function (id, rgb, opacity, blend, blur, offset) {
var filter = this.container.ownerDocument.createElementNS(this.svgNamespace, 'filter'),
feOffset, feColor, feGaussianBlur, feBlend,
mat;
filter.setAttributeNS(null, 'id', id);
filter.setAttributeNS(null, 'width', '300%');
filter.setAttributeNS(null, 'height', '300%');
filter.setAttributeNS(null, 'filterUnits', 'userSpaceOnUse');
feOffset = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feOffset');
feOffset.setAttributeNS(null, 'in', 'SourceGraphic'); // b/w: SourceAlpha, Color: SourceGraphic
feOffset.setAttributeNS(null, 'result', 'offOut');
feOffset.setAttributeNS(null, 'dx', offset[0]);
feOffset.setAttributeNS(null, 'dy', offset[1]);
filter.appendChild(feOffset);
feColor = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feColorMatrix');
feColor.setAttributeNS(null, 'in', 'offOut');
feColor.setAttributeNS(null, 'result', 'colorOut');
feColor.setAttributeNS(null, 'type', 'matrix');
// See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix
if (rgb === 'none' || !Type.isArray(rgb) || rgb.length < 3) {
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');
} else {
rgb[0] /= 255;
rgb[1] /= 255;
rgb[2] /= 255;
mat = blend + ' 0 0 0 ' + rgb[0] +
' 0 ' + blend + ' 0 0 ' + rgb[1] +
' 0 0 ' + blend + ' 0 ' + rgb[2] +
' 0 0 0 ' + opacity + ' 0';
feColor.setAttributeNS(null, 'values', mat);
}
filter.appendChild(feColor);
feGaussianBlur = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feGaussianBlur');
feGaussianBlur.setAttributeNS(null, 'in', 'colorOut');
feGaussianBlur.setAttributeNS(null, 'result', 'blurOut');
feGaussianBlur.setAttributeNS(null, 'stdDeviation', blur);
filter.appendChild(feGaussianBlur);
feBlend = this.container.ownerDocument.createElementNS(this.svgNamespace, 'feBlend');
feBlend.setAttributeNS(null, 'in', 'SourceGraphic');
feBlend.setAttributeNS(null, 'in2', 'blurOut');
feBlend.setAttributeNS(null, 'mode', 'normal');
filter.appendChild(feBlend);
return filter;
};
/**
* Create a "unique" string id from the arguments of the function.
* Concatenate all arguments by "_".
* "Unique" is achieved by simply prepending the container id.
* Do not escape the string.
*
* If the id is used in an "url()" call it must be eascaped.
*
* @param {String} one or strings which will be concatenated.
* @return {String}
* @private
*/
this.uniqName = function () {
return this.container.id + '_' +
Array.prototype.slice.call(arguments).join('_');
};
/**
* Combine arguments to a string, joined by empty string.
* The container id needs to be escaped, as it may contain URI-unsafe characters
*
* @param {String} str variable number of strings
* @returns String
* @see JXG.SVGRenderer#toURL
* @private
* @example
* this.toStr('aaa', '_', 'bbb', 'TriangleEnd')
* // Output:
* // xxx_bbbTriangleEnd
*/
this.toStr = function() {
// ES6 would be [...arguments].join()
var str = Array.prototype.slice.call(arguments).join('');
// Mask special symbols like '/' and '\' in id
if (Type.exists(encodeURIComponent)) {
str = encodeURIComponent(str);
}
return str;
};
/**
* Combine arguments to an URL string of the form url(#...)
* Masks the container id. Calls {@link JXG.SVGRenderer#toStr}.
*
* @param {String} str variable number of strings
* @returns URL string
* @see JXG.SVGRenderer#toStr
* @private
* @example
* this.toURL('aaa', '_', 'bbb', 'TriangleEnd')
* // Output:
* // url(#xxx_bbbTriangleEnd)
*/
this.toURL = function () {
return 'url(#' +
this.toStr.apply(this, arguments) + // Pass the arguments to toStr
')';
};
/* Default shadow filter */
this.defs.appendChild(this.createShadowFilter(this.uniqName('f1'), 'none', 1, 0.1, 3, [5, 5]));
this.createClip = function() {
var id = this.uniqName('ClipFull'),
node1 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'clipPath'),
node2 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'rect'),
style, rx, ry;
node1.setAttributeNS(null, 'id', id);
node2.setAttributeNS(null, 'x', 0);
node2.setAttributeNS(null, 'y', 0);
node2.setAttributeNS(null, 'width', dim.width);
node2.setAttributeNS(null, 'height', dim.height);
// Inherit border-radius
style = getComputedStyle(this.container);
rx = Type.exists(style['border-radius']) ? parseFloat(style['border-radius']) : 0;
ry = rx;
node2.setAttributeNS(null, 'rx', rx);
node2.setAttributeNS(null, 'ry', ry);
node1.appendChild(node2);
return node1;
};
this.defs.appendChild(this.createClip());
// Already documented in JXG.AbstractRenderer
this.setClipPath = function(el, val) {
if (val) {
el.rendNode.style.clipPath = this.toURL(this.uniqName('ClipFull'));
} else {
el.rendNode.style.removeProperty('clip-path');
}
return this;
};
/**
* Update the filter node which does the clipping of elements (beside HTML texts) outside of the SVG.
* It is called in procedure resize().
* @param {Number} w
* @param {Number} h
* @see JXG.AbstractRenderer#setClipPath
*/
this.updateClipPathRect = function (w, h) {
var id = this.uniqName('ClipFull'),
clipNode, node;
// if (Type.exists(this.container.ownerDocument.getElementById(id).firstChild)) {
clipNode = this.container.ownerDocument.getElementById(id);
if (Type.exists(clipNode) && Type.exists(clipNode.firstChild)) {
node = clipNode.firstChild;
if (Type.exists(node)) {
node.setAttributeNS(null, 'width', w);
node.setAttributeNS(null, 'height', h);
}
}
};
/**
* JSXGraph uses a layer system to sort the elements on the board. This puts certain types of elements in front
* of other types of elements. For the order used see {@link JXG.Options.layer}. The number of layers is documented
* there, too. The higher the number, the "more on top" are the elements on this layer.
* @type Array
*/
this.layer = [];
for (i = 0; i < Options.layer.numlayers; i++) {
this.layer[i] = this.container.ownerDocument.createElementNS(this.svgNamespace, 'g');
// this.layer[i].style.clipPath = this.toURL(this.uniqName('ClipFull'));
this.svgRoot.appendChild(this.layer[i]);
}
try {
this.foreignObjLayer = this.container.ownerDocument.createElementNS(
this.svgNamespace,
"foreignObject"
);
this.foreignObjLayer.setAttribute("display", 'none');
this.foreignObjLayer.setAttribute("x", 0);
this.foreignObjLayer.setAttribute("y", 0);
this.foreignObjLayer.setAttribute("width", "100%");
this.foreignObjLayer.setAttribute("height", "100%");
this.foreignObjLayer.setAttribute("id", this.uniqName('foreignObj'));
this.svgRoot.appendChild(this.foreignObjLayer);
this.supportsForeignObject = true;
} catch (e) {
this.supportsForeignObject = false;
}
};
JXG.SVGRenderer.prototype = new AbstractRenderer();
JXG.extend(
JXG.SVGRenderer.prototype,
/** @lends JXG.SVGRenderer.prototype */ {
/* ******************************** *
* This renderer does not need to
* override draw/update* methods
* since it provides draw/update*Prim
* methods except for some cases like
* internal texts or images.
* ******************************** */
/* ********* Arrow head related stuff *********** */
/**
* Creates an arrow DOM node. Arrows are displayed in SVG with a <em>marker</em> tag.
* @private
* @param {JXG.GeometryElement} el A JSXGraph element, preferably one that can have an arrow attached.
* @param {String} [idAppendix=''] A string that is added to the node's id.
* @returns {Node} Reference to the node added to the DOM.
*/
_createArrowHead: function (el, idAppendix, type) {
var node2,
node3,
id = el.id + "Triangle",
//type = null,
v,
h;
if (Type.exists(idAppendix)) {
id += idAppendix;
}
if (Type.exists(type)) {
id += type;
}
node2 = this.createPrim('marker', id);
// 'context-stroke': property is inherited from line or curve
if (JXG.isWebkitApple()) {
// 2025: Safari does not support 'context-stroke'
node2.setAttributeNS(null, 'fill', el.evalVisProp('strokecolor'));
node2.setAttributeNS(null, 'stroke', el.evalVisProp('strokecolor'));
} else {
node2.setAttributeNS(null, 'fill', 'context-stroke');
node2.setAttributeNS(null, 'stroke', 'context-stroke');
}
node2.setAttributeNS(null, 'stroke-width', 0); // this is the stroke-width of the arrow head.
// node2.setAttributeNS(null, 'fill-opacity', 'context-stroke'); // Not available
// node2.setAttributeNS(null, 'stroke-opacity', 'context-stroke');
node2.setAttributeNS(null, 'stroke-width', 0); // this is the stroke-width of the arrow head.
// Should be zero to simplify the calculations
node2.setAttributeNS(null, 'orient', 'auto');
node2.setAttributeNS(null, 'markerUnits', 'strokeWidth'); // 'strokeWidth' 'userSpaceOnUse');
/*
Types 1, 2:
The arrow head is an isosceles triangle with base length 10 and height 10.
Type 3:
A rectangle
Types 4, 5, 6:
Defined by Bezier curves from mp_arrowheads.html
In any case but type 3 the arrow head is 10 units long,
type 3 is 10 units high.
These 10 units are scaled to strokeWidth * arrowSize pixels, see
this._setArrowWidth().
See also abstractRenderer.updateLine() where the line path is shortened accordingly.
Changes here are also necessary in setArrowWidth().
So far, lines with arrow heads are shortenend to avoid overlapping of
arrow head and line. This is not the case for curves, yet.
Therefore, the offset refX has to be adapted to the path type.
*/
node3 = this.container.ownerDocument.createElementNS(this.svgNamespace, 'path');
h = 5;
if (idAppendix === 'Start') {
// First arrow
v = 0;
if (type === 2) {
node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 L 5,5 z");
} else if (type === 3) {
node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z");
} else if (type === 4) {
// insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
h = 3.31;
node3.setAttributeNS(
null,
"d",
"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"
);
} else if (type === 5) {
// insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
h = 3.28;
node3.setAttributeNS(
null,
"d",
"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"
);
} else if (type === 6) {
// insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
h = 2.84;
node3.setAttributeNS(
null,
"d",
"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"
);
} else if (type === 7) {
// insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
h = 5.2;
node3.setAttributeNS(
null,
"d",
"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"
);
} else {
// type == 1 or > 6
node3.setAttributeNS(null, "d", "M 10,0 L 0,5 L 10,10 z");
}
if (
// !Type.exists(el.rendNode.getTotalLength) &&
el.elementClass === Const.OBJECT_CLASS_LINE
) {
if (type === 2) {
v = 4.9;
} else if (type === 3) {
v = 3.3;
} else if (type === 4 || type === 5 || type === 6) {
v = 6.66;
} else if (type === 7) {
v = 0.0;
} else {
v = 10.0;
}
}
} else {
// Last arrow
v = 10.0;
if (type === 2) {
node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 L 5,5 z");
} else if (type === 3) {
v = 3.3;
node3.setAttributeNS(null, "d", "M 0,0 L 3.33,0 L 3.33,10 L 0,10 z");
} else if (type === 4) {
// insetRatio:0.8 tipAngle:45 wingCurve:15 tailCurve:0
h = 3.31;
node3.setAttributeNS(
null,
"d",
"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"
);
} else if (type === 5) {
// insetRatio:0.9 tipAngle:40 wingCurve:5 tailCurve:15
h = 3.28;
node3.setAttributeNS(
null,
"d",
"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"
);
} else if (type === 6) {
// insetRatio:0.9 tipAngle:35 wingCurve:5 tailCurve:0
h = 2.84;
node3.setAttributeNS(
null,
"d",
"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"
);
} else if (type === 7) {
// insetRatio:0.9 tipAngle:60 wingCurve:30 tailCurve:0
h = 5.2;
node3.setAttributeNS(
null,
"d",
"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"
);
} else {
// type == 1 or > 6
node3.setAttributeNS(null, "d", "M 0,0 L 10,5 L 0,10 z");
}
if (
// !Type.exists(el.rendNode.getTotalLength) &&
el.elementClass === Const.OBJECT_CLASS_LINE
) {
if (type === 2) {
v = 5.1;
} else if (type === 3) {
v = 0.02;
} else if (type === 4 || type === 5 || type === 6) {
v = 3.33;
} else if (type === 7) {
v = 10.0;
} else {
v = 0.05;
}
}
}
if (type === 7) {
node2.setAttributeNS(null, 'fill', 'none');
node2.setAttributeNS(null, 'stroke-width', 1); // this is the stroke-width of the arrow head.
}
node2.setAttributeNS(null, "refY", h);
node2.setAttributeNS(null, "refX", v);
// this.setPropertyPrim(node2, 'class', el.evalVisProp('cssclass'));
node2.appendChild(node3);
// Set color and opacity
this._setArrowColor(node2, el.evalVisProp('strokecolor'), el.evalVisProp('strokeopacity'), el, type);
return node2;
},
/**
* Updates color of an arrow DOM node.
* @param {Node} node The arrow node.
* @param {String} color Color value in a HTML compatible format, e.g. `#00ff00` or `green` for green.
* @param {Number} opacity
* @param {JXG.GeometryElement} el The element the arrows are to be attached to
*/
_setArrowColor: function (node, color, opacity, el, type) {
if (node) {
if (Type.isString(color)) {
if (type !== 7) {
this._setAttribute(function () {
node.setAttributeNS(null, 'fill-opacity', opacity);
if (JXG.isWebkitApple()) {
// 2025: Safari does not support 'context-stroke'
node.setAttributeNS(null, 'fill', color);
} else {
node.setAttributeNS(null, 'fill', 'context-stroke');
}
}, el.visPropOld.fillcolor);
} else {
this._setAttribute(function () {
node.setAttributeNS(null, 'fill', 'none');
node.setAttributeNS(null, 'stroke-opacity', opacity);
if (JXG.isWebkitApple()) {
node.setAttributeNS(null, 'stroke', color);
} else {
node.setAttributeNS(null, 'stroke', 'context-stroke');
}
}, el.visPropOld.fillcolor);
}
}
// if (this.isIE) {
// Necessary, since Safari is the new IE (11.2024)
el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
// }
}
},
// Already documented in JXG.AbstractRenderer
_setArrowWidth: function (node, width, parentNode, size) {
var s, d;
if (node) {
// if (width === 0) {
// // display:none does not work well in webkit
// node.setAttributeNS(null, 'display', 'none');
// } else {
s = width;
d = s * size;
node.setAttributeNS(null, "viewBox", 0 + " " + 0 + " " + s * 10 + " " + s * 10);
node.setAttributeNS(null, "markerHeight", d);
node.setAttributeNS(null, "markerWidth", d);
node.setAttributeNS(null, "display", 'inherit');
// }
// if (this.isIE) {
// Necessary, since Safari is the new IE (11.2024)
parentNode.parentNode.insertBefore(parentNode, parentNode);
// }
}
},
/* ********* Line related stuff *********** */
// documented in AbstractRenderer
updateTicks: function (ticks) {
var i,
j,
c,
node,
x,
y,
tickStr = "",
len = ticks.ticks.length,
len2,
str,
isReal = true;
for (i = 0; i < len; i++) {
c = ticks.ticks[i];
x = c[0];
y = c[1];
len2 = x.length;
str = " M " + x[0] + " " + y[0];
if (!Type.isNumber(x[0])) {
isReal = false;
}
for (j = 1; isReal && j < len2; ++j) {
if (Type.isNumber(x[j])) {
str += " L " + x[j] + " " + y[j];
} else {
isReal = false;
}
}
if (isReal) {
tickStr += str;
}
}
node = ticks.rendNode;
if (!Type.exists(node)) {
node = this.createPrim("path", ticks.id);
this.appendChildPrim(node, ticks.evalVisProp('layer'));
ticks.rendNode = node;
}
node.setAttributeNS(null, "stroke", ticks.evalVisProp('strokecolor'));
node.setAttributeNS(null, "fill", 'none');
// node.setAttributeNS(null, 'fill', ticks.evalVisProp('fillcolor'));
// node.setAttributeNS(null, 'fill-opacity', ticks.evalVisProp('fillopacity'));
node.setAttributeNS(null, 'stroke-opacity', ticks.evalVisProp('strokeopacity'));
node.setAttributeNS(null, "stroke-width", ticks.evalVisProp('strokewidth'));
this.setClipPath(ticks, ticks.evalVisProp('clip'));
this.updatePathPrim(node, tickStr, ticks.board);
},
/* ********* Text related stuff *********** */
// Already documented in JXG.AbstractRenderer
displayCopyright: function (str, fontsize) {
var node, t,
x = 4 + 1.8 * fontsize,
y = 6 + fontsize,
alpha = 0.2;
node = this.createPrim("text", 'licenseText');
node.setAttributeNS(null, 'x', x + 'px');
node.setAttributeNS(null, 'y', y + 'px');
node.setAttributeNS(null, 'style', 'font-family:Arial,Helvetica,sans-serif; font-size:' +
fontsize + 'px; opacity:' + alpha + ';');
// fill:#356AA0;
node.setAttributeNS(null, 'aria-hidden', 'true');
t = this.container.ownerDocument.createTextNode(str);
node.appendChild(t);
this.appendChildPrim(node, 0);
},
// Already documented in JXG.AbstractRenderer
displayLogo: function (str, fontsize) {
var node,
s = 1.5 * fontsize,
alpha = 0.2;
node = this.createPrim("image", 'licenseLogo');
node.setAttributeNS(null, 'x', '5px');
node.setAttributeNS(null, 'y', '5px');
node.setAttributeNS(null, 'width', s + 'px');
node.setAttributeNS(null, 'height', s + 'px');
node.setAttributeNS(null, "preserveAspectRatio", 'none');
node.setAttributeNS(null, 'style', 'opacity:' + alpha + ';');
node.setAttributeNS(null, 'aria-hidden', 'true');
node.setAttributeNS(this.xlinkNamespace, 'xlink:href', str); // Deprecated
node.setAttributeNS(null, 'href', str);
this.appendChildPrim(node, 0);
},
// Already documented in JXG.AbstractRenderer
drawInternalText: function (el) {
var node = this.createPrim("text", el.id);
//node.setAttributeNS(null, "style", "alignment-baseline:middle"); // Not yet supported by Firefox
// Preserve spaces
//node.setAttributeNS("http://www.w3.org/XML/1998/namespace", "space", 'preserve');
node.style.whiteSpace = 'nowrap';
el.rendNodeText = this.container.ownerDocument.createTextNode("");
node.appendChild(el.rendNodeText);
this.appendChildPrim(node, el.evalVisProp('layer'));
return node;
},
// Already documented in JXG.AbstractRenderer
updateInternalText: function (el) {
var content = el.plaintext,
v, css,
ev_ax = el.getAnchorX(),
ev_ay = el.getAnchorY();
css = el.evalVisProp('cssclass');
if (el.rendNode.getAttributeNS(null, 'class') !== css) {
el.rendNode.setAttributeNS(null, "class", css);
el.needsSizeUpdate = true;
}
if (!isNaN(el.coords.scrCoords[1] + el.coords.scrCoords[2])) {
// Horizontal
v = el.coords.scrCoords[1];
if (el.visPropOld.left !== ev_ax + v) {
el.rendNode.setAttributeNS(null, "x", v + 'px');
if (ev_ax === 'left') {
el.rendNode.setAttributeNS(null, "text-anchor", 'start');
} else if (ev_ax === 'right') {
el.rendNode.setAttributeNS(null, "text-anchor", 'end');
} else if (ev_ax === 'middle') {
el.rendNode.setAttributeNS(null, "text-anchor", 'middle');
}
el.visPropOld.left = ev_ax + v;
}
// Vertical
v = el.coords.scrCoords[2];
if (el.visPropOld.top !== ev_ay + v) {
el.rendNode.setAttributeNS(null, "y", v + this.vOffsetText * 0.5 + 'px');
// Not supported by IE, edge
// el.rendNode.setAttributeNS(null, "dy", '0');
// if (ev_ay === 'bottom') {
// el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-after-edge');
// } else if (ev_ay === 'top') {
// el.rendNode.setAttributeNS(null, 'dominant-baseline', 'text-before-edge');
// } else if (ev_ay === 'middle') {
// el.rendNode.setAttributeNS(null, 'dominant-baseline', 'middle');
// }
if (ev_ay === 'bottom') {
el.rendNode.setAttributeNS(null, "dy", '0');
el.rendNode.setAttributeNS(null, 'dominant-baseline', 'auto');
} else if (ev_ay === 'top') {
el.rendNode.setAttributeNS(null, "dy", '1.6ex');
el.rendNode.setAttributeNS(null, 'dominant-baseline', 'auto');
} else if (ev_ay === 'middle') {
el.rendNode.setAttributeNS(null, "dy", '0.6ex');
el.rendNode.setAttributeNS(null, 'dominant-baseline', 'auto');
}
el.visPropOld.top = ev_ay + v;
}
}
if (el.htmlStr !== content) {
el.rendNodeText.data = content;
el.htmlStr = content;
}
this.transformRect(el, el.transformations);
this.setClipPath(el, !!el.evalVisProp('clip'));
},
/**
* Set color and opacity of internal texts.
* @private
* @see JXG.AbstractRenderer#updateTextStyle
* @see JXG.AbstractRenderer#updateInternalTextStyle
*/
updateInternalTextStyle: function (el, strokeColor, strokeOpacity, duration) {
this.setObjectFillColor(el, strokeColor, strokeOpacity);
},
/* ********* Image related stuff *********** */
// Already documented in JXG.AbstractRenderer
drawImage: function (el) {
var node = this.createPrim("image", el.id);
node.setAttributeNS(null, "preserveAspectRatio", 'none');
this.appendChildPrim(node, el.evalVisProp('layer'));
el.rendNode = node;
this.updateImage(el);
},
// Already documented in JXG.AbstractRenderer
transformRect: function (el, t) {
var s, m, node,
str = "",
cx, cy,
len = t.length;
if (len > 0) {
node = el.rendNode;
m = this.joinTransforms(el, t);
s = [m[1][1], m[2][1], m[1][2], m[2][2], m[1][0], m[2][0]].join(",");
if (s.indexOf('NaN') === -1) {
str += " matrix(" + s + ") ";
if (el.elementClass === Const.OBJECT_CLASS_TEXT && el.visProp.display === 'html') {
node.style.transform = str;
cx = -el.coords.scrCoords[1];
cy = -el.coords.scrCoords[2];
switch (el.evalVisProp('anchorx')) {
case 'right': cx += el.size[0]; break;
case 'middle': cx += el.size[0] * 0.5; break;
}
switch (el.evalVisProp('anchory')) {
case 'bottom': cy += el.size[1]; break;
case 'middle': cy += el.size[1] * 0.5; break;
}
node.style['transform-origin'] = (cx) + 'px ' + (cy) + 'px';
} else {
// Images and texts with display:'internal'
node.setAttributeNS(null, "transform", str);
}
}
}
},
// Already documented in JXG.AbstractRenderer
updateImageURL: function (el) {
var url = el.eval(el.url);
if (el._src !== url) {
el.imgIsLoaded = false;
el.rendNode.setAttributeNS(this.xlinkNamespace, 'xlink:href', url); // Deprecated
el.rendNode.setAttributeNS(null, 'href', url);
el._src = url;
return true;
}
return false;
},
// Already documented in JXG.AbstractRenderer
updateImageStyle: function (el, doHighlight) {
var css = el.evalVisProp(
doHighlight ? 'highlightcssclass' : 'cssclass'
);
el.rendNode.setAttributeNS(null, "class", css);
},
// Already documented in JXG.AbstractRenderer
drawForeignObject: function (el) {
el.rendNode = this.appendChildPrim(
this.createPrim("foreignObject", el.id),
el.evalVisProp('layer')
);
this.appendNodesToElement(el, 'foreignObject');
this.updateForeignObject(el);
},
// Already documented in JXG.AbstractRenderer
updateForeignObject: function (el) {
if (el._useUserSize) {
el.rendNode.style.overflow = 'hidden';
} else {
el.rendNode.style.overflow = 'visible';
}
this.updateRectPrim(
el.rendNode,
el.coords.scrCoords[1],
el.coords.scrCoords[2] - el.size[1],
el.size[0],
el.size[1]
);
if (el.evalVisProp('evaluateOnlyOnce') !== true || !el.renderedOnce) {
el.rendNode.innerHTML = el.content;
el.renderedOnce = true;
}
this._updateVisual(el, { stroke: true, dash: true }, true);
},
/* ********* Render primitive objects *********** */
// Already documented in JXG.AbstractRenderer
appendChildPrim: function (node, level) {
if (!Type.exists(level)) {
// trace nodes have level not set
level = 0;
} else if (level >= Options.layer.numlayers) {
level = Options.layer.numlayers - 1;
}
this.layer[level].appendChild(node);
return node;
},
// Already documented in JXG.AbstractRenderer
createPrim: function (type, id) {
var node = this.container.ownerDocument.createElementNS(this.svgNamespace, type);
node.setAttributeNS(null, "id", this.uniqName(id));
node.style.position = 'absolute';
if (type === 'path') {
node.setAttributeNS(null, "stroke-linecap", 'round');
node.setAttributeNS(null, "stroke-linejoin", 'round');
node.setAttributeNS(null, "fill-rule", 'evenodd');
}
return node;
},
// Already documented in JXG.AbstractRenderer
remove: function (shape) {
if (Type.exists(shape) && Type.exists(shape.parentNode)) {
shape.parentNode.removeChild(shape);
}
},
// Already documented in JXG.AbstractRenderer
setLayer: function (el, level) {
var node;
if (!Type.exists(level)) {
level = 0;
} else if (level >= Options.layer.numlayers) {
level = Options.layer.numlayers - 1;
}
node = this.layer[level];
if (Type.exists(node.moveBefore)) {
node.moveBefore(el.rendNode, null);
} else {
node.appendChild(el.rendNode);
}
},
// Already documented in JXG.AbstractRenderer
makeArrows: function (el, a) {
var node2, str,
ev_fa = a.evFirst,
ev_la = a.evLast;
if (this.isIE && el.visPropCalc.visible && (ev_fa || ev_la)) {
// Necessary, since Safari is the new IE (11.2024)
el.rendNode.parentNode.insertBefore(el.rendNode, el.rendNode);
return;
}
// We can not compare against visPropOld if there is need for a new arrow head,
// since here visPropOld and ev_fa / ev_la already have the same value.
// This has been set in _updateVisual.
//
node2 = el.rendNodeTriangleStart;
if (ev_fa) {
str = this.toStr(this.container.id, '_', el.id, 'TriangleStart', a.typeFirst);
// If we try to set the same arrow head as is already set, we can bail out now
if (!Type.exists(node2) || node2.id !== str) {
node2 = this.container.ownerDocument.getElementById(str);
// Check if the marker already exists.
// If not, create a new marker
if (node2 === null) {
node2 = this._createArrowHead(el, "Start", a.typeFirst);
this.defs.appendChild(node2);
}
el.rendNodeTriangleStart = node2;
el.rendNode.setAttributeNS(null, 'marker-start', this.toURL(str));
}
} else {
if (Type.exists(node2)) {
this.remove(node2);
el.rendNodeTriangleStart = null;
}
// el.rendNode.setAttributeNS(null, "marker-start", null);
el.rendNode.removeAttributeNS(null, 'marker-start');
}
node2 = el.rendNodeTriangleEnd;
if (ev_la) {
str = this.toStr(this.container.id, '_', el.id, 'TriangleEnd', a.typeLast);
// If we try to set the same arrow head as is already set, we can bail out now
if (!Type.exists(node2) || node2.id !== str) {
node2 = this.container.ownerDocument.getElementById(str);
// Check if the marker already exists.
// If not, create a new marker
if (node2 === null) {
node2 = this._createArrowHead(el, "End", a.typeLast);
this.defs.appendChild(node2);
}
el.rendNodeTriangleEnd = node2;
el.rendNode.setAttributeNS(null, "marker-end", this.toURL(str));
}
} else {
if (Type.exists(node2)) {
this.remove(node2);
el.rendNodeTriangleEnd = null;
}
// el.rendNode.setAttributeNS(null, "marker-end", null);
el.rendNode.removeAttributeNS(null, "marker-end");
}
},
// Already documented in JXG.AbstractRenderer
updateEllipsePrim: function (node, x, y, rx, ry) {
var huge = 1000000;
huge = 200000; // IE
// webkit does not like huge values if the object is dashed
// iE doesn't like huge values above 216000
x = Math.abs(x) < huge ? x : (huge * x) / Math.abs(x);
y = Math.abs(y) < huge ? y : (huge * y) / Math.abs(y);
rx = Math.abs(rx) < huge ? rx : (huge * rx) / Math.abs(rx);
ry = Math.abs(ry) < huge ? ry : (huge * ry) / Math.abs(ry);
node.setAttributeNS(null, "cx", x);
node.setAttributeNS(null, "cy", y);
node.setAttributeNS(null, "rx", Math.abs(rx));
node.setAttributeNS(null, "ry", Math.abs(ry));
},
// Already documented in JXG.AbstractRenderer
updateLinePrim: function (node, p1x, p1y, p2x, p2y) {
var huge = 1000000;
huge = 200000; //IE
if (!isNaN(p1x + p1y + p2x + p2y)) {
// webkit does not like huge values if the object is dashed
// IE doesn't like huge values above 216000
p1x = Math.abs(p1x) < huge ? p1x : (huge * p1x) / Math.abs(p1x);
p1y = Math.abs(p1y) < huge ? p1y : (huge * p1y) / Math.abs(p1y);
p2x = Math.abs(p2x) < huge ? p2x : (huge * p2x) / Math.abs(p2x);
p2y = Math.abs(p2y) < huge ? p2y : (huge * p2y) / Math.abs(p2y);
node.setAttributeNS(null, "x1", p1x);
node.setAttributeNS(null, "y1", p1y);
node.setAttributeNS(null, "x2", p2x);
node.setAttributeNS(null, "y2", p2y);
}
},
// Already documented in JXG.AbstractRenderer
updatePathPrim: function (node, str) {
if (str === "") {
str = "M 0 0";
}
node.setAttributeNS(null, "d", str);
},
// Already documented in JXG.AbstractRenderer
updatePathStringPoint: function (el, size, type) {
var s = "",
scr = el.coords.scrCoords,
sqrt32 = size * Math.sqrt(3) * 0.5,
s05 = size * 0.5;
if (type === 'x') {
s = ' M ' + (scr[1] - size) + ' ' + (scr[2] - size) +
' L ' + (scr[1] + size) + ' ' + (scr[2] + size) +
' M ' + (scr[1] + size) + ' ' + (scr[2] - size) +
' L ' + (scr[1] - size) + ' ' + (scr[2] + size);
} else if (type === '+') {
s = ' M ' + (scr[1] - size) + ' ' + scr[2] +
' L ' + (scr[1] + size) + ' ' + scr[2] +
' M ' + scr[1] + ' ' + (scr[2] - size) +
' L ' + scr[1] + ' ' + (scr[2] + size);
} else if (type === '|') {
s = ' M ' + scr[1] + ' ' + (scr[2] - size) +
' L ' + scr[1] + ' ' + (scr[2] + size);
} else if (type === '-') {
s = ' M ' + (scr[1] - size) + ' ' + scr[2] +
' L ' + (scr[1] + size) + ' ' + scr[2];
} else if (type === '<>' || type === '<<>>') {
if (type === '<<>>') {
size *= 1.41;
}
s = ' M ' + (scr[1] - size) + ' ' + scr[2] +
' L ' + scr[1] + ' ' + (scr[2] + size) +
' L ' + (scr[1] + size) + ' ' + scr[2] +
' L ' + scr[1] + ' ' + (scr[2] - size) +' Z ';
} else if (type === '^') {
s = ' M ' + scr[1] + ' ' + (scr[2] - size) +
' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] + s05) +
' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] + s05) +' Z '; // close path
} else if (type === 'v') {
s = ' M ' + scr[1] + ' ' + (scr[2] + size) +
' L ' + (scr[1] - sqrt32) + ' ' + (scr[2] - s05) +
' L ' + (scr[1] + sqrt32) + ' ' + (scr[2] - s05) + ' Z ';
} else if (type === '>') {
s = ' M ' + (scr[1] + size) + ' ' + scr[2] +
' L ' + (scr[1] - s05) + ' ' + (scr[2] - sqrt32) +
' L ' + (scr[1] - s05) + ' ' + (scr[2] + sqrt32) + ' Z ';
} else if (type === '<') {
s = ' M ' + (scr[1] - size) + ' ' + scr[2] +
' L ' + (scr[1] + s05) + ' ' + (scr[2] - sqrt32) +
' L ' + (scr[1] + s05) + ' ' + (scr[2] + sqrt32) + ' Z ';
}
return s;
},
// Already documented in JXG.AbstractRenderer
updatePathStringPrim: function (el) {
var i,
scr, scx, scy,
len,
symbm = ' M ',
symbl = ' L ',
symbc = ' C ',
nextSymb = symbm,
M = Env.maxScreenCoord,
scr2, d,
// sc1, sc2,
// sc, scr2,
// d, z1, scr1, lbda, mu,
// xt, xb, yt, yb,
// xl, xr, yl, yr,
pStr = '';
if (el.numberPoints <= 0) {
return '';
}
len = Math.min(el.points.length, el.numberPoints);
if (el.bezierDegree === 1) {
for (i = 0; i < len; i++) {
if (el.points[i] === undefined) {
continue;
}
scr = el.points[i].scrCoords;
if (isNaN(scr[1]) || isNaN(scr[2])) {
// PenUp
nextSymb = symbm;
} else {
// Chrome has problems with values being too far away.
// In early implementations it was recommended to restrict numbers to abs value 5000,
// see https://oreillymedia.github.io/Using_SVG/extras/ch08-precision.html#:~:text=If%20you%20are%20creating%20a,no%20bigger%20than%20%C2%B15%2C000.
// Attention: there may be conflicts with RDP smoothing.
//
// March 2026: This restriction seems to be obsolete.
// Meanwhile all major browsers support 32 floats, see
// https://www.w3.org/TR/SVG/types.html, section "4.2.1. Real number precision"
//
// Change in-place:
// scr[1] = Math.max(Math.min(scr[1], M), -M);
// scr[2] = Math.max(Math.min(scr[2], M), -M);
// Change not in-place (preferred 2026):
// sc1 = Math.max(Math.min(scr[1], M), -M);
// sc2 = Math.max(Math.min(scr[2], M), -M);
scx = scr[1];
scy = scr[2];
// Some first steps to project coordinates to the virtual
// clip box [-M, M, M, -M].
if (Math.abs(scx) > M || Math.abs(scy) > M) {
// Search for point inside of virtual canvas
if (i > 0) {
scr2 = el.points[i - 1].scrCoords;
} else if (i < len) {
scr2 = el.points[i + 1].scrCoords;
} else {
continue;
}
if (i < len - 1 && (isNaN(scr2[1]) || isNaN(scr2[2]))) {
scr2 = el.points[i + 1].scrCoords;
}
if ((isNaN(scr2[1]) || isNaN(scr2[2]))) {
continue;
}
// Approximate point
if (Math.abs(scy) > M) {
d = scy - scr2[2];
scy = Math.max(Math.min(scy, M), -M);
scx = scr2[1] + (scx - scr2[1]) * (scy - scr2[2]) / d;
}
if (Math.abs(scx) > M) {
d = scx - scr2[1];
scx = Math.max(Math.min(scx, M), -M);
scy = scr2[2] + (scy - scr2[2]) * (scx - scr2[1]) / d;
}
}
// scx = (Math.abs(scx) < M) ? scx : Math.max(Math.min(scx, M), -M);
// scy = (Math.abs(scy) < M) ? scy : Math.max(Math.min(scy, M), -M);
// Intersections with the clip box.
// Todo: choose the right one.
// if (i > 0) {
// scr1 = el.points[i - 1].scrCoords;
// d = sc2 - scr1[2];
// if (d !== 0) {
// lbda = (M - scr1[2]) / d;
// xt = scr1[1] + lbda * (sc1 - scr1[1]); yt = M;
// lbda = (-M - scr1[2]) / d;
// xb = scr1[1] + lbda * (sc1 - scr1[1]); yb = -M;
// }
// d = sc1 - scr1[1];
// if (d !== 0) {
// lbda = (M - scr1[2]) / d;
// yr = scr1[2] + lbda * (sc2 - scr1[2]); xr = M;
// lbda = (-M - scr1[2]) / d;
// yl = scr1[2] + lbda * (sc2 - scr1[2]); xl = -M;
// }
// }
//
// Attention: first coordinate may be inaccurate if far way
// pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
// pStr += nextSymb + scr[1] + ' ' + scr[2]; // '+' seems to be faster than 'join' now (webkit and firefox)
pStr += nextSymb + scx + ' ' + scy; // '+' seems to be faster than 'join' now (webkit and firefox)
nextSymb = symbl;
}
}
} else if (el.bezierDegree === 3) {
i = 0;
while (i < len) {
if (el.points[i] === undefined) {
continue;
}
scr = el.points[i].scrCoords;
scx = scr[1];
scy = scr[2];
if (isNaN(scx) || isNaN(scy)) {
// PenUp
nextSymb = symbm;
} else {
pStr += nextSymb + scx + ' ' + scy;
if (nextSymb === symbc) {
i += 1;
scr = el.points[i].scrCoords;
pStr += ' ' + scr[1] + ' ' + scr[2];
i += 1;
scr = el.points[i].scrCoords;
pStr += ' ' + scr[1] + ' ' + scr[2];
}
nextSymb = symbc;
}
i += 1;
}
}
return pStr;
},
// Already documented in JXG.AbstractRenderer
updatePathStringBezierPrim: function (el) {
var i, j, k,
scr, sc1, sc2,
lx, ly,
len,
symbm = ' M ',
symbl = ' C ',
nextSymb = symbm,
// M = Env.maxScreenCoord,
pStr = '',
f = el.evalVisProp('strokewidth'),
isNoPlot = el.evalVisProp('curvetype') !== 'plot';
if (el.numberPoints <= 0) {
return '';
}
if (isNoPlot && el.board.options.curve.RDPsmoothing) {
el.points = Numerics.RamerDouglasPeucker(el.points, 0.5);
}
len = Math.min(el.points.length, el.numberPoints);
for (j = 1; j < 3; j++) {
nextSymb = symbm;
for (i = 0; i < len; i++) {
scr = el.points[i].scrCoords;
if (isNaN(scr[1]) || isNaN(scr[2])) {
// PenUp
nextSymb = symbm;
} else {
// Chrome has problems with values being too far away.
// scr[1] = Math.max(Math.min(scr[1], M), -M);
// scr[2] = Math.max(Math.min(scr[2], M), -M);
// sc1 = Math.max(Math.min(scr[1], M), -M);
// sc2 = Math.max(Math.min(scr[2], M), -M);
sc1 = scr[1];
sc2 = scr[2];
// Attention: first coordinate may be inaccurate if far way
if (nextSymb === symbm) {
//pStr += [nextSymb, scr[1], ' ', scr[2]].join('');
pStr += nextSymb + sc1 + ' ' + sc2; // Seems to be faster now (webkit and firefox)
} else {
k = 2 * j;
pStr += [
nextSymb,
lx + (sc1 - lx) * 0.333 + f * (k * Math.random() - j), ' ',
ly + (sc2 - ly) * 0.333 + f * (k * Math.random() - j), ' ',
lx + (sc1 - lx) * 0.666 + f * (k * Math.random() - j), ' ',
ly + (sc2 - ly) * 0.666 + f * (k * Math.random() - j), ' ',
sc1, ' ', sc2
].join('');
}
nextSymb = symbl;
lx = sc1;
ly = sc2;
}
}
}
return pStr;
},
// Already documented in JXG.AbstractRenderer
updatePolygonPrim: function (node, el) {
var i,
pStr = "",
scrCoords,
len = el.vertices.length;
node.setAttributeNS(null, "stroke", 'none');
node.setAttributeNS(null, "fill-rule", 'evenodd');
if (el.elType === 'polygonalchain') {
len++;
}
for (i = 0; i < len - 1; i++) {
if (el.vertices[i].isReal) {
scrCoords = el.vertices[i].coords.scrCoords;
pStr = pStr + scrCoords[1] + "," + scrCoords[2];
} else {
node.setAttributeNS(null, "points", "");
return;
}
if (i < len - 2) {
pStr += " ";
}
}
if (pStr.indexOf('NaN') === -1) {
node.setAttributeNS(null, "points", pStr);
}
},
// Already documented in JXG.AbstractRenderer
updateRectPrim: function (node, x, y, w, h) {
node.setAttributeNS(null, "x", x);
node.setAttributeNS(null, "y", y);
node.setAttributeNS(null, "width", w);
node.setAttributeNS(null, "height", h);
},
/* ********* Set attributes *********** */
/**
* Call user-defined function to set visual attributes.
* If "testAttribute" is the empty string, the function
* is called immediately, otherwise it is called in a timeOut.
*
* This is necessary to realize smooth transitions but avoid transitions
* when first creating the objects.
*
* Usually, the string in testAttribute is the visPropOld attribute
* of the values which are set.
*
* @param {Function} setFunc Some function which usually sets some attributes
* @param {String} testAttribute If this string is the empty string the function is called immediately,
* otherwise it is called in a setImeout.
* @see JXG.SVGRenderer#setObjectFillColor
* @see JXG.SVGRenderer#setObjectStrokeColor
* @see JXG.SVGRenderer#_setArrowColor
* @private
*/
_setAttribute: function (setFunc, testAttribute) {
if (testAttribute === "") {
setFunc();
} else {
window.setTimeout(setFunc, 1);
}
},
display: function (el, val) {
var node;
if (el && el.rendNode) {
el.visPropOld.visible = val;
node = el.rendNode;
if (val) {
node.setAttributeNS(null, "display", 'inline');
node.style.visibility = 'inherit';
} else {
node.setAttributeNS(null, "display", 'none');
node.style.visibility = 'hidden';
}
}
},
// documented in JXG.AbstractRenderer
hide: function (el) {
JXG.deprecated("Board.renderer.hide()", "Board.renderer.display()");
this.display(el, false);
},
// documented in JXG.AbstractRenderer
setARIA: function(el) {
// This method is only called in abstractRenderer._updateVisual() if aria.enabled == true.
var key, k, v;
// this.setPropertyPrim(el.rendNode, 'aria-label', el.evalVisProp('aria.label'));
// this.setPropertyPrim(el.rendNode, 'aria-live', el.evalVisProp('aria.live'));
for (key in el.visProp.aria) {
if (el.visProp.aria.hasOwnProperty(key) && key !== 'enabled') {
k = 'aria.' + key;
v = el.evalVisProp('aria.' + key);
if (el.visPropOld[k] !== v) {
this.setPropertyPrim(el.rendNode, 'aria-' + key, v);
el.visPropOld[k] = v;
}
}
}
},
// documented in JXG.AbstractRenderer
setBuffering: function (el, type) {
el.rendNode.setAttribute("buffered-rendering", type);
},
// documented in JXG.AbstractRenderer
setCssClass(el, cssClass) {
if (el.visPropOld.cssclass !== cssClass) {
this.setPropertyPrim(el.rendNode, 'class', cssClass);
el.visPropOld.cssclass = cssClass;
}
},
// documented in JXG.AbstractRenderer
setDashStyle: function (el) {
var dashStyle = el.evalVisProp('dash'),
ds = el.evalVisProp('dashscale'),
sw = ds ? 0.5 * el.evalVisProp('strokewidth') : 1,
node = el.rendNode;
if (dashStyle > 0) {
node.setAttributeNS(null, "stroke-dasharray",
// sw could distinguish highlighting or not.
// But it seems to preferable to ignore this.
this.dashArray[dashStyle - 1].map(function (x) { return x * sw; }).join(',')
);
} else {
if (node.hasAttributeNS(null, "stroke-dasharray")) {
node.removeAttributeNS(null, "stroke-dasharray");
}
}
},
// documented in JXG.AbstractRenderer
setGradient: function (el) {
var fillNode = el.rendNode,
node, node2, node3,
ev_g = el.evalVisProp('gradient');
if (ev_g === "linear" || ev_g === 'radial') {
node = this.createPrim(ev_g + "Gradient", el.id + "_gradient");
node2 = this.createPrim("stop", el.id + "_gradient1");
node3 = this.createPrim("stop", el.id + "_gradient2");
node.appendChild(node2);
node.appendChild(node3);
this.defs.appendChild(node);
fillNode.setAttributeNS(
null,
'style',
// "fill:url(#" + this.container.id + "_" + el.id + "_gradient)"
'fill:' + this.toURL(this.container.id + '_' + el.id + '_gradient')
);
el.gradNode1 = node2;
el.gradNode2 = node3;
el.gradNode = node;
} else {
fillNode.removeAttributeNS(null, 'style');
}
},
// documented in JXG.AbstractRenderer
setLineCap: function (el) {
var capStyle = el.evalVisProp('linecap');
if (
capStyle === undefined ||
capStyle === "" ||
el.visPropOld.linecap === capStyle ||
!Type.exists(el.rendNode)
) {
return;
}
this.setPropertyPrim(el.rendNode, "stroke-linecap", capStyle);
el.visPropOld.linecap = capStyle;
},
// documented in JXG.AbstractRenderer
setObjectFillColor: function (el, color, opacity, rendNode) {
var node, c, rgbo, oo,
rgba = color,
o = opacity,
grad = el.evalVisProp('gradient');
o = o > 0 ? o : 0;
// TODO save gradient and gradientangle
if (
el.visPropOld.fillcolor === rgba &&
el.visPropOld.fillopacity === o &&
grad === null
) {
return;
}
if (Type.exists(rgba) && rgba !== false) {
if (rgba.length !== 9) {
// RGB, not RGBA
c = rgba;
oo = o;
} else {
// True RGBA, not RGB
rgbo = Color.rgba2rgbo(rgba);
c = rgbo[0];
oo = o * rgbo[1];
}
if (rendNode === undefined) {
node = el.rendNode;
} else {
node = rendNode;
}
if (c !== "none" && c !== "" && c !== false) {
this._setAttribute(function () {
node.setAttributeNS(null, "fill", c);
}, el.visPropOld.fillcolor);
}
if (el.type === JXG.OBJECT_TYPE_IMAGE) {
this._setAttribute(function () {
node.setAttributeNS(null, "opacity", oo);
}, el.visPropOld.fillopacity);
//node.style['opacity'] = oo; // This would overwrite values set by CSS class.
} else {
if (c === 'none') {
// This is done only for non-images
// because images have no fill color.
oo = 0;
// This is necessary if there is a foreignObject below.
node.setAttributeNS(null, "pointer-events", 'visibleStroke');
} else {
// This is the default
node.setAttributeNS(null, "pointer-events", 'visiblePainted');
}
this._setAttribute(function () {
node.setAttributeNS(null, 'fill-opacity', oo);
}, el.visPropOld.fillopacity);
}
if (grad === "linear" || grad === 'radial') {
this.updateGradient(el);
}
}
el.visPropOld.fillcolor = rgba;
el.visPropOld.fillopacity = o;
},
// documented in JXG.AbstractRenderer
setObjectStrokeColor: function (el, color, opacity) {
var rgba = color,
c, rgbo,
o = opacity,
oo, node;
o = o > 0 ? o : 0;
if (el.visPropOld.strokecolor === rgba && el.visPropOld.strokeopacity === o) {
return;
}
if (Type.exists(rgba) && rgba !== false) {
if (rgba.length !== 9) {
// RGB, not RGBA
c = rgba;
oo = o;
} else {
// True RGBA, not RGB
rgbo = Color.rgba2rgbo(rgba);
c = rgbo[0];
oo = o * rgbo[1];
}
node = el.rendNode;
if (el.elementClass === Const.OBJECT_CLASS_TEXT) {
if (el.evalVisProp('display') === 'html') {
this._setAttribute(function () {
node.style.color = c;
node.style.opacity = oo;
}, el.visPropOld.strokecolor);
} else {
this._setAttribute(function () {
node.setAttributeNS(null, 'fill', c);
node.setAttributeNS(null, 'fill-opacity', oo);
}, el.visPropOld.strokecolor);
}
} else {
this._setAttribute(function () {
node.setAttributeNS(null, "stroke", c);
node.setAttributeNS(null, 'stroke-opacity', oo);
}, el.visPropOld.strokecolor);
}
if (
el.elementClass === Const.OBJECT_CLASS_CURVE ||
el.elementClass === Const.OBJECT_CLASS_LINE
) {
if (el.evalVisProp('firstarrow')) {
this._setArrowColor(
el.rendNodeTriangleStart,
c, oo, el,
el.visPropCalc.typeFirst
);
}
if (el.evalVisProp('lastarrow')) {
this._setArrowColor(
el.rendNodeTriangleEnd,
c, oo, el,
el.visPropCalc.typeLast
);
}
}
}
el.visPropOld.strokecolor = rgba;
el.visPropOld.strokeopacity = o;
},
// documented in JXG.AbstractRenderer
setObjectStrokeWidth: function (el, width) {
var node,
w = width;
if (isNaN(w) || el.visPropOld.strokewidth === w) {
return;
}
node = el.rendNode;
this.setPropertyPrim(node, "stroked", 'true');
if (Type.exists(w)) {
this.setPropertyPrim(node, "stroke-width", w + 'px');
// if (el.elementClass === Const.OBJECT_CLASS_CURVE ||
// el.elementClass === Const.OBJECT_CLASS_LINE) {
// if (el.evalVisProp('firstarrow')) {
// this._setArrowWidth(el.rendNodeTriangleStart, w, el.rendNode);
// }
//
// if (el.evalVisProp('lastarrow')) {
// this._setArrowWidth(el.rendNodeTriangleEnd, w, el.rendNode);
// }
// }
}
el.visPropOld.strokewidth = w;
},
// documented in JXG.AbstractRenderer
setObjectTransition: function (el, duration) {
var node, props,
transitionArr = [],
transitionStr,
i,
len = 0,
nodes = ["rendNode", "rendNodeTriangleStart", "rendNodeTriangleEnd"];
if (duration === undefined) {
duration = el.evalVisProp('transitionduration');
}
props = el.evalVisProp('transitionproperties');
if (duration === el.visPropOld.transitionduration &&
props === el.visPropOld.transitionproperties) {
return;
}
// if (
// el.elementClass === Const.OBJECT_CLASS_TEXT &&
// el.evalVisProp('display') === "html"
// ) {
// // transitionStr = " color " + duration + "ms," +
// // " opacity " + duration + 'ms'
// transitionStr = " all " + duration + "ms ease";
// } else {
// transitionStr =
// " fill " + duration + "ms," +
// " fill-opacity " + duration + "ms," +
// " stroke " + duration + "ms," +
// " stroke-opacity " + duration + "ms," +
// " stroke-width " + duration + "ms," +
// " width " + duration + "ms," +
// " height " + duration + "ms," +
// " rx " + duration + "ms," +
// " ry " + duration + 'ms'
// }
if (Type.exists(props)) {
len = props.length;
}
for (i = 0; i < len; i++) {
transitionArr.push(props[i] + ' ' + duration + 'ms');
}
transitionStr = transitionArr.join(', ');
len = nodes.length;
for (i = 0; i < len; ++i) {
if (el[nodes[i]]) {
node = el[nodes[i]];
node.style.transition = transitionStr;
}
}
el.visPropOld.transitionduration = duration;
el.visPropOld.transitionproperties = props;
},
// documented in JXG.AbstractRenderer
setShadow: function (el) {
var ev_s = el.evalVisProp('shadow'),
ev_s_json, c, b, bl, o, op, id, node,
use_board_filter = true,
show = false;
ev_s_json = JSON.stringify(ev_s);
if (ev_s_json === el.visPropOld.shadow) {
return;
}
if (typeof ev_s === 'boolean') {
use_board_filter = true;
show = ev_s;
c = 'none';
b = 3;
bl = 0.1;
o = [5, 5];
op = 1;
} else {
if (el.evalVisProp('shadow.enabled')) {
use_board_filter = false;
show = true;
c = JXG.rgbParser(el.evalVisProp('shadow.color'));
b = el.evalVisProp('shadow.blur');
bl = el.evalVisProp('shadow.blend');
o = el.evalVisProp('shadow.offset');
op = el.evalVisProp('shadow.opacity');
} else {
show = false;
}
}
if (Type.exists(el.rendNode)) {
if (show) {
if (use_board_filter) {
el.rendNode.setAttributeNS(null, 'filter', this.toURL(this.container.id + '_' + 'f1'));
// 'url(#' + this.container.id + '_' + 'f1)');
} else {
node = this.container.ownerDocument.getElementById(id);
if (node) {
this.defs.removeChild(node);
}
id = el.rendNode.id + '_' + 'f1';
this.defs.appendChild(this.createShadowFilter(id, c, op, bl, b, o));
el.rendNode.setAttributeNS(null, 'filter', this.toURL(id));
// 'url(#' + id + ')');
}
} else {
el.rendNode.removeAttributeNS(null, 'filter');
}
}
el.visPropOld.shadow = ev_s_json;
},
// documented in JXG.AbstractRenderer
setTabindex: function (el) {
var val;
if (el.board.attr.keyboard.enabled && Type.exists(el.rendNode)) {
val = el.evalVisProp('tabindex');
if (!el.visPropCalc.visible /* || el.evalVisProp('fixed') */) {
val = null;
}
if (val !== el.visPropOld.tabindex) {
el.rendNode.setAttribute("tabindex", val);
el.visPropOld.tabindex = val;
}
}
},
// documented in JXG.AbstractRenderer
setPropertyPrim: function (node, key, val) {
if (key === 'stroked') {
return;
}
node.setAttributeNS(null, key, val);
},
// documented in JXG.AbstractRenderer
show: function (el) {
JXG.deprecated("Board.renderer.show()", "Board.renderer.display()");
this.display(el, true);
// var node;
//
// if (el && el.rendNode) {
// node = el.rendNode;
// node.setAttributeNS(null, 'display', 'inline');
// node.style.visibility = 'inherit'
// }
},
// documented in JXG.AbstractRenderer
updateGradient: function (el) {
var col,
op,
node2 = el.gradNode1,
node3 = el.gradNode2,
ev_g = el.evalVisProp('gradient');
if (!Type.exists(node2) || !Type.exists(node3)) {
return;
}
op = el.evalVisProp('fillopacity');
op = op > 0 ? op : 0;
col = el.evalVisProp('fillcolor');
node2.setAttributeNS(null, "style", "stop-color:" + col + ";stop-opacity:" + op);
node3.setAttributeNS(
null,
"style",
"stop-color:" +
el.evalVisProp('gradientsecondcolor') +
";stop-opacity:" +
el.evalVisProp('gradientsecondopacity')
);
node2.setAttributeNS(
null,
"offset",
el.evalVisProp('gradientstartoffset') * 100 + "%"
);
node3.setAttributeNS(
null,
"offset",
el.evalVisProp('gradientendoffset') * 100 + "%"
);
if (ev_g === 'linear') {
this.updateGradientAngle(el.gradNode, el.evalVisProp('gradientangle'));
} else if (ev_g === 'radial') {
this.updateGradientCircle(
el.gradNode,
el.evalVisProp('gradientcx'),
el.evalVisProp('gradientcy'),
el.evalVisProp('gradientr'),
el.evalVisProp('gradientfx'),
el.evalVisProp('gradientfy'),
el.evalVisProp('gradientfr')
);
}
},
/**
* Set the gradient angle for linear color gradients.
*
* @private
* @param {SVGnode} node SVG gradient node of an arbitrary JSXGraph element.
* @param {Number} radians angle value in radians. 0 is horizontal from left to right, Pi/4 is vertical from top to bottom.
*/
updateGradientAngle: function (node, radians) {
// Angles:
// 0: ->
// 90: down
// 180: <-
// 90: up
var f = 1.0,
co = Math.cos(radians),
si = Math.sin(radians);
if (Math.abs(co) > Math.abs(si)) {
f /= Math.abs(co);
} else {
f /= Math.abs(si);
}
if (co >= 0) {
node.setAttributeNS(null, "x1", 0);
node.setAttributeNS(null, "x2", co * f);
} else {
node.setAttributeNS(null, "x1", -co * f);
node.setAttributeNS(null, "x2", 0);
}
if (si >= 0) {
node.setAttributeNS(null, "y1", 0);
node.setAttributeNS(null, "y2", si * f);
} else {
node.setAttributeNS(null, "y1", -si * f);
node.setAttributeNS(null, "y2", 0);
}
},
/**
* Set circles for radial color gradients.
*
* @private
* @param {SVGnode} node SVG gradient node
* @param {Number} cx SVG value cx (value between 0 and 1)
* @param {Number} cy SVG value cy (value between 0 and 1)
* @param {Number} r SVG value r (value between 0 and 1)
* @param {Number} fx SVG value fx (value between 0 and 1)
* @param {Number} fy SVG value fy (value between 0 and 1)
* @param {Number} fr SVG value fr (value between 0 and 1)
*/
updateGradientCircle: function (node, cx, cy, r, fx, fy, fr) {
node.setAttributeNS(null, "cx", cx * 100 + "%"); // Center first color
node.setAttributeNS(null, "cy", cy * 100 + "%");
node.setAttributeNS(null, "r", r * 100 + "%");
node.setAttributeNS(null, "fx", fx * 100 + "%"); // Center second color / focal point
node.setAttributeNS(null, "fy", fy * 100 + "%");
node.setAttributeNS(null, "fr", fr * 100 + "%");
},
/* ********* Renderer control *********** */
// documented in JXG.AbstractRenderer
suspendRedraw: function () {
// It seems to be important for the Linux version of firefox
this.suspendHandle = this.svgRoot.suspendRedraw(10000);
},
// documented in JXG.AbstractRenderer
unsuspendRedraw: function () {
this.svgRoot.unsuspendRedraw(this.suspendHandle);
// this.svgRoot.unsuspendRedrawAll();
//this.svgRoot.forceRedraw();
},
// documented in AbstractRenderer
resize: function (w, h) {
this.svgRoot.setAttribute("width", parseFloat(w));
this.svgRoot.setAttribute("height", parseFloat(h));
if (Type.exists(this.updateClipPathRect)) {
// Update clip-path element of the SVG box
this.updateClipPathRect(w, h);
}
},
// documented in JXG.AbstractRenderer
createTouchpoints: function (n) {
var i, na1, na2, node;
this.touchpoints = [];
for (i = 0; i < n; i++) {
na1 = "touchpoint1_" + i;
node = this.createPrim("path", na1);
this.appendChildPrim(node, 19);
node.setAttributeNS(null, "d", "M 0 0");
this.touchpoints.push(node);
this.setPropertyPrim(node, "stroked", 'true');
this.setPropertyPrim(node, "stroke-width", '1px');
node.setAttributeNS(null, "stroke", "#000000");
node.setAttributeNS(null, 'stroke-opacity', 1.0);
node.setAttributeNS(null, "display", 'none');
na2 = "touchpoint2_" + i;
node = this.createPrim("ellipse", na2);
this.appendChildPrim(node, 19);
this.updateEllipsePrim(node, 0, 0, 0, 0);
this.touchpoints.push(node);
this.setPropertyPrim(node, "stroked", 'true');
this.setPropertyPrim(node, "stroke-width", '1px');
node.setAttributeNS(null, "stroke", "#000000");
node.setAttributeNS(null, "fill", "#ffffff");
node.setAttributeNS(null, 'stroke-opacity', 1.0);
node.setAttributeNS(null, 'fill-opacity', 0.0);
node.setAttributeNS(null, "display", 'none');
}
},
// documented in JXG.AbstractRenderer
showTouchpoint: function (i) {
if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
this.touchpoints[2 * i].setAttributeNS(null, "display", 'inline');
this.touchpoints[2 * i + 1].setAttributeNS(null, "display", 'inline');
}
},
// documented in JXG.AbstractRenderer
hideTouchpoint: function (i) {
if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
this.touchpoints[2 * i].setAttributeNS(null, "display", 'none');
this.touchpoints[2 * i + 1].setAttributeNS(null, "display", 'none');
}
},
// documented in JXG.AbstractRenderer
updateTouchpoint: function (i, pos) {
var x,
y,
d = 37;
if (this.touchpoints && i >= 0 && 2 * i < this.touchpoints.length) {
x = pos[0];
y = pos[1];
this.touchpoints[2 * i].setAttributeNS(
null,
"d",
"M " +
(x - d) +
" " +
y +
" " +
"L " +
(x + d) +
" " +
y +
" " +
"M " +
x +
" " +
(y - d) +
" " +
"L " +
x +
" " +
(y + d)
);
this.updateEllipsePrim(this.touchpoints[2 * i + 1], pos[0], pos[1], 25, 25);
}
},
/* ********* Dump related stuff *********** */
/**
* Walk recursively through the DOM subtree of a node and collect all
* value attributes together with the id of that node.
* <b>Attention:</b> Only values of nodes having a valid id are taken.
* @param {Node} node root node of DOM subtree that will be searched recursively.
* @return {Array} Array with entries of the form [id, value]
* @private
*/
_getValuesOfDOMElements: function (node) {
var values = [];
if (node.nodeType === 1) {
node = node.firstChild;
while (node) {
if (node.id !== undefined && node.value !== undefined) {
values.push([node.id, node.value]);
}
Type.concat(values, this._getValuesOfDOMElements(node));
node = node.nextSibling;
}
}
return values;
},
// _getDataUri: function (url, callback) {
// var image = new Image();
// image.onload = function () {
// var canvas = document.createElement('canvas');
// canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
// canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
// canvas.getContext('2d').drawImage(this, 0, 0);
// callback(canvas.toDataURL("image/png"));
// canvas.remove();
// };
// image.src = url;
// },
_getImgDataURL: function (svgRoot) {
var images, len, canvas, ctx, ur, i,
str;
images = svgRoot.getElementsByTagName('image');
len = images.length;
if (len > 0) {
canvas = document.createElement('canvas');
for (i = 0; i < len; i++) {
if (images[i].attributes.getNamedItem('href') !== null) {
str = images[i].attributes.getNamedItem('href').value;
} else {
// Deprecated approach
str = images[i].attributes.getNamedItemNS(this.xlinkNamespace, 'xlink:href').value;
}
// If the image is already a data-URI we are done
if (str.indexOf('data:image') === 0) {
continue;
}
images[i].setAttribute("crossorigin", 'anonymous');
ctx = canvas.getContext('2d');
canvas.width = images[i].getAttribute('width');
canvas.height = images[i].getAttribute('height');
try {
ctx.drawImage(images[i], 0, 0, canvas.width, canvas.height);
// If the image is not png, the format must be specified here
ur = canvas.toDataURL();
images[i].setAttribute('xlink:href', ur); // Deprecated
images[i].setAttribute('href', ur);
} catch (err) {
console.log("CORS problem! Image can not be used", err);
}
}
//canvas.remove();
}
return true;
},
/**
* Return a data URI of the SVG code representing the construction.
* The SVG code of the construction is base64 encoded. The return string starts
* with "data:image/svg+xml;base64,...".
*
* @param {Boolean} ignoreTexts If true, the foreignObject tag is set to display=none.
* This is necessary for older versions of Safari. Default: false
* @returns {String} data URI string
*
* @example
* var A = board.create('point', [2, 2]);
*
* var txt = board.renderer.dumpToDataURI(false);
* // txt consists of a string of the form
* // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
* // Behind the comma, there is the base64 encoded SVG code
* // which is decoded with atob().
* // The call of decodeURIComponent(escape(...)) is necessary
* // to handle unicode strings correctly.
* var ar = txt.split(',');
* document.getElementById('output').value = decodeURIComponent(escape(atob(ar[1])));
*
* </pre><div id="JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d" class="jxgbox" style="width: 300px; height: 300px;"></div>
* <textarea id="output2023" rows="5" cols="50"></textarea>
* <script type="text/javascript">
* (function() {
* var board = JXG.JSXGraph.initBoard('JXG1bad4bec-6d08-4ce0-9b7f-d817e8dd762d',
* {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
* var A = board.create('point', [2, 2]);
*
* var txt = board.renderer.dumpToDataURI(false);
* // txt consists of a string of the form
* // data:image/svg+xml;base64,PHN2Zy. base64 encoded SVG..+PC9zdmc+
* // Behind the comma, there is the base64 encoded SVG code
* // which is decoded with atob().
* // The call of decodeURIComponent(escape(...)) is necessary
* // to handle unicode strings correctly.
* var ar = txt.split(',');
* document.getElementById('output2023').value = decodeURIComponent(escape(atob(ar[1])));
*
* })();
*
* </script><pre>
*
*/
dumpToDataURI: function (ignoreTexts) {
var svgRoot = this.svgRoot,
btoa = window.btoa || Base64.encode,
svg, i, len, str,
values = [];
// Move all HTML tags (beside the SVG root) of the container
// to the foreignObject element inside of the svgRoot node
// Problem:
// input values are not copied. This can be verified by looking at an innerHTML output
// of an input element. Therefore, we do it "by hand".
if (this.container.hasChildNodes() && Type.exists(this.foreignObjLayer)) {
if (!ignoreTexts) {
this.foreignObjLayer.setAttribute("display", 'inline');
}
while (svgRoot.nextSibling) {
// Copy all value attributes
Type.concat(values, this._getValuesOfDOMElements(svgRoot.nextSibling));
this.foreignObjLayer.appendChild(svgRoot.nextSibling);
}
}
// Dump all image tags
this._getImgDataURL(svgRoot);
// Convert the SVG graphic into a string containing SVG code
svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg = new XMLSerializer().serializeToString(svgRoot);
if (ignoreTexts !== true) {
// Handle SVG texts
// Insert all value attributes back into the svg string
len = values.length;
for (i = 0; i < len; i++) {
svg = svg.replace(
'id="' + values[i][0] + '"',
'id="' + values[i][0] + '" value="' + values[i][1] + '"'
);
}
}
// if (false) {
// // Debug: use example svg image
// 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>';
// }
// In IE we have to remove the namespace again.
// Since 2024 we have to check if the namespace attribute appears twice in one tag, because
// there might by a svg inside of the svg, e.g. the screenshot icon.
if (this.isIE &&
(svg.match(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g) || []).length > 1
) {
svg = svg.replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"\s+xmlns="http:\/\/www.w3.org\/2000\/svg"/g, "");
}
// Safari fails if the svg string contains a " "
// Obsolete with Safari 12+
svg = svg.replace(/ /g, " ");
// Replacing "s might be necessary for older Safari versions
// svg = svg.replace(/url\("(.*)"\)/g, "url($1)"); // Bug: does not replace matching "s
// svg = svg.replace(/"/g, "");
// Move all HTML tags back from
// the foreignObject element to the container
if (Type.exists(this.foreignObjLayer) && this.foreignObjLayer.hasChildNodes()) {
// Restore all HTML elements
while (this.foreignObjLayer.firstChild) {
this.container.appendChild(this.foreignObjLayer.firstChild);
}
this.foreignObjLayer.setAttribute("display", 'none');
}
// Parameter for btoa(): Replace utf-16 chars by their numerical entity
// In particular, this is necessary for the coyright sign
// From https://stackoverflow.com/questions/23223718/failed-to-execute-btoa-on-window-the-string-to-be-encoded-contains-characte/26603875#26603875
// str = btoa(svg.replace(/[\u00A0-\u2666]/g, function(c) { return '&#' + c.charCodeAt(0) + ';'; })); // Fails for MathJax-SVG
str = btoa(unescape(encodeURIComponent(svg))); // unescape is deprecated and can handle utf-16 chars only partially
return "data:image/svg+xml;base64," + str;
},
/**
* Convert the SVG construction into an HTML canvas image.
* This works for all SVG supporting browsers. Implemented as Promise.
*
* Might fail if any text element or foreign object element contains SVG. This
* is the case e.g. for the default fullscreen symbol.
*
* For IE, it is realized as function.
* It works from version 9, with the exception that HTML texts
* are ignored on IE. The drawing is done with a delay of
* 200 ms. Otherwise there would be problems with IE.
*
* @param {String} canvasId Id of an HTML canvas element
* @param {Number} w Width in pixel of the dumped image, i.e. of the canvas tag.
* @param {Number} h Height in pixel of the dumped image, i.e. of the canvas tag.
* @param {Boolean} ignoreTexts If true, the foreignObject tag is taken out from the SVG root.
* This is necessary for older versions of Safari. Default: false
* @returns {Promise} Promise object
*
* @example
* board.renderer.dumpToCanvas('canvas').then(function() { console.log('done'); });
*
* @example
* // IE 11 example:
* board.renderer.dumpToCanvas('canvas');
* setTimeout(function() { console.log('done'); }, 400);
*/
dumpToCanvas: function (canvasId, w, h, ignoreTexts) {
var svg, tmpImg,
cv, ctx,
doc = this.container.ownerDocument;
// Prepare the canvas element
cv = doc.getElementById(canvasId);
// Clear the canvas
/* eslint-disable no-self-assign */
cv.width = cv.width;
/* eslint-enable no-self-assign */
ctx = cv.getContext('2d');
if (w !== undefined && h !== undefined) {
cv.style.width = parseFloat(w) + 'px';
cv.style.height = parseFloat(h) + 'px';
// Scale twice the CSS size to make the image crisp
// cv.setAttribute('width', 2 * parseFloat(wOrg));
// cv.setAttribute('height', 2 * parseFloat(hOrg));
// ctx.scale(2 * wOrg / w, 2 * hOrg / h);
cv.setAttribute("width", parseFloat(w));
cv.setAttribute("height", parseFloat(h));
}
// Display the SVG string as data-uri in an HTML img.
/**
* @type {Image}
* @ignore
*/
tmpImg = new Image();
svg = this.dumpToDataURI(ignoreTexts);
tmpImg.src = svg;
// Finally, draw the HTML img in the canvas.
if (!("Promise" in window)) {
/**
* @function
* @ignore
*/
tmpImg.onload = function () {
// IE needs a pause...
// Seems to be broken
window.setTimeout(function () {
try {
ctx.drawImage(tmpImg, 0, 0, w, h);
} catch (err) {
console.log("screenshots not longer supported on IE");
}
}, 200);
};
return this;
}
return new Promise(function (resolve, reject) {
try {
tmpImg.onload = function () {
ctx.drawImage(tmpImg, 0, 0, w, h);
resolve();
};
} catch (e) {
reject(e);
}
});
},
/**
* Display SVG image in html img-tag which enables easy download for the user.
*
* Support:
*
* - IE: No
* - Edge: full
* - Firefox: full
* - Chrome: full
* - Safari: full (No text support in versions prior to 12).
*
* @param {JXG.Board} board Link to the board.
* @param {String} imgId Optional id of an img object. If given and different from the empty string,
* the screenshot is copied to this img object. The width and height will be set to the values of the
* JSXGraph container.
* @param {Boolean} ignoreTexts If set to true, the foreignObject is taken out of the
* SVGRoot and texts are not displayed. This is mandatory for Safari. Default: false
* @return {Object} the svg renderer object
*/
screenshot: function (board, imgId, ignoreTexts) {
var node,
doc = this.container.ownerDocument,
parent = this.container.parentNode,
// cPos,
// cssTxt,
canvas, id, img,
button, buttonText,
w, h,
bas = board.attr.screenshot,
navbar, navbarDisplay, insert,
newImg = false,
_copyCanvasToImg,
isDebug = false;
if (this.type === 'no') {
return this;
}
w = bas.scale * this.container.getBoundingClientRect().width;
h = bas.scale * this.container.getBoundingClientRect().height;
if (imgId === undefined || imgId === "") {
newImg = true;
img = new Image(); //doc.createElement('img');
img.style.width = w + 'px';
img.style.height = h + 'px';
} else {
newImg = false;
img = doc.getElementById(imgId);
}
// img.crossOrigin = 'anonymous';
// Create div which contains canvas element and close button
if (newImg) {
node = doc.createElement('div');
node.style.cssText = bas.css;
node.style.width = w + 'px';
node.style.height = h + 'px';
node.style.zIndex = this.container.style.zIndex + 120;
// Try to position the div exactly over the JSXGraph board
node.style.position = 'absolute';
node.style.top = this.container.offsetTop + 'px';
node.style.left = this.container.offsetLeft + 'px';
}
if (!isDebug) {
// Create canvas element and add it to the DOM
// It will be removed after the image has been stored.
canvas = doc.createElement('canvas');
id = Math.random().toString(36).slice(2, 7);
canvas.setAttribute("id", id);
canvas.setAttribute("width", w);
canvas.setAttribute("height", h);
canvas.style.width = w + 'px';
canvas.style.height = w + 'px';
canvas.style.display = 'none';
parent.appendChild(canvas);
} else {
// Debug: use canvas element 'jxgbox_canvas' from jsxdev/dump.html
id = "jxgbox_canvas";
canvas = doc.getElementById(id);
}
if (newImg) {
// Create close button
button = doc.createElement('span');
buttonText = doc.createTextNode("\u2716");
button.style.cssText = bas.cssButton;
button.appendChild(buttonText);
button.onclick = function () {
node.parentNode.removeChild(node);
};
// Add all nodes
node.appendChild(img);
node.appendChild(button);
parent.insertBefore(node, this.container.nextSibling);
}
// Hide navigation bar in board
navbar = doc.getElementById(this.uniqName('navigationbar'));
if (Type.exists(navbar)) {
navbarDisplay = navbar.style.display;
navbar.style.display = 'none';
insert = this.removeToInsertLater(navbar);
}
_copyCanvasToImg = function () {
// Show image in img tag
img.src = canvas.toDataURL("image/png");
// Remove canvas node
if (!isDebug) {
parent.removeChild(canvas);
}
};
// Create screenshot in image element
if ("Promise" in window) {
this.dumpToCanvas(id, w, h, ignoreTexts).then(_copyCanvasToImg);
} else {
// IE
this.dumpToCanvas(id, w, h, ignoreTexts);
window.setTimeout(_copyCanvasToImg, 200);
}
// Reinsert navigation bar in board
if (Type.exists(navbar)) {
navbar.style.display = navbarDisplay;
insert();
}
return this;
}
}
);
export default JXG.SVGRenderer;