/*! JointJS v0.9.7 - JavaScript diagramming library 2016-04-20 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['g'], function(g) { return factory(g); }); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. var g = require('./geometry'); module.exports = factory(g); } else { // Browser globals. var g = root.g; root.Vectorizer = root.V = factory(g); } }(this, function(g) { // Vectorizer. // ----------- // A tiny library for making your life easier when dealing with SVG. // The only Vectorizer dependency is the Geometry library. // Copyright © 2012 - 2015 client IO (http://client.io) var V; var Vectorizer; V = Vectorizer = (function() { 'use strict'; var hasSvg = typeof window === 'object' && !!( window.SVGAngle || document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') ); // SVG support is required. if (!hasSvg) { // Return a function that throws an error when it is used. return function() { throw new Error('SVG is required to use Vectorizer.'); }; } // XML namespaces. var ns = { xmlns: 'http://www.w3.org/2000/svg', xlink: 'http://www.w3.org/1999/xlink' }; var SVGversion = '1.1'; var V = function(el, attrs, children) { // This allows using V() without the new keyword. if (!(this instanceof V)) { return V.apply(Object.create(V.prototype), arguments); } if (!el) return; if (V.isV(el)) { el = el.node; } attrs = attrs || {}; if (V.isString(el)) { if (el.toLowerCase() === 'svg') { // Create a new SVG canvas. el = V.createSvgDocument(); } else if (el[0] === '<') { // Create element from an SVG string. // Allows constructs of type: `document.appendChild(V('').node)`. var svgDoc = V.createSvgDocument(el); // Note that `V()` might also return an array should the SVG string passed as // the first argument contain more than one root element. if (svgDoc.childNodes.length > 1) { // Map child nodes to `V`s. var arrayOfVels = []; var i, len; for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { var childNode = svgDoc.childNodes[i]; arrayOfVels.push(new V(document.importNode(childNode, true))); } return arrayOfVels; } el = document.importNode(svgDoc.firstChild, true); } else { el = document.createElementNS(ns.xmlns, el); } } this.node = el; if (!this.node.id) { this.node.id = V.uniqueId(); } this.setAttributes(attrs); if (children) { this.append(children); } return this; }; /** * @param {SVGGElement} toElem * @returns {SVGMatrix} */ V.prototype.getTransformToElement = function(toElem) { return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); }; /** * @param {SVGMatrix} matrix * @returns {V, SVGMatrix} Setter / Getter */ V.prototype.transform = function(matrix) { if (V.isUndefined(matrix)) { return (this.node.parentNode) ? this.getTransformToElement(this.node.parentNode) : this.node.getScreenCTM(); } var svgTransform = V.createSVGTransform(matrix); this.node.transform.baseVal.appendItem(svgTransform); return this; }; V.prototype.translate = function(tx, ty, opt) { opt = opt || {}; ty = ty || 0; var transformAttr = this.attr('transform') || ''; var transform = V.parseTransformString(transformAttr); // Is it a getter? if (V.isUndefined(tx)) { return transform.translate; } transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); var newTx = opt.absolute ? tx : transform.translate.tx + tx; var newTy = opt.absolute ? ty : transform.translate.ty + ty; var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; // Note that `translate()` is always the first transformation. This is // usually the desired case. this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); return this; }; V.prototype.rotate = function(angle, cx, cy, opt) { opt = opt || {}; var transformAttr = this.attr('transform') || ''; var transform = V.parseTransformString(transformAttr); // Is it a getter? if (V.isUndefined(angle)) { return transform.rotate; } transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); angle %= 360; var newAngle = opt.absolute ? angle : transform.rotate.angle + angle; var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : ''; var newRotate = 'rotate(' + newAngle + newOrigin + ')'; this.attr('transform', (transformAttr + ' ' + newRotate).trim()); return this; }; // Note that `scale` as the only transformation does not combine with previous values. V.prototype.scale = function(sx, sy) { sy = V.isUndefined(sy) ? sx : sy; var transformAttr = this.attr('transform') || ''; var transform = V.parseTransformString(transformAttr); // Is it a getter? if (V.isUndefined(sx)) { return transform.scale; } transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim(); var newScale = 'scale(' + sx + ',' + sy + ')'; this.attr('transform', (transformAttr + ' ' + newScale).trim()); return this; }; // Get SVGRect that contains coordinates and dimension of the real bounding box, // i.e. after transformations are applied. // If `target` is specified, bounding box will be computed relatively to `target` element. V.prototype.bbox = function(withoutTransformations, target) { // If the element is not in the live DOM, it does not have a bounding box defined and // so fall back to 'zero' dimension element. if (!this.node.ownerSVGElement) return { x: 0, y: 0, width: 0, height: 0 }; var box; try { box = this.node.getBBox(); // We are creating a new object as the standard says that you can't // modify the attributes of a bbox. box = { x: box.x, y: box.y, width: box.width, height: box.height }; } catch (e) { // Fallback for IE. box = { x: this.node.clientLeft, y: this.node.clientTop, width: this.node.clientWidth, height: this.node.clientHeight }; } if (withoutTransformations) { return box; } var matrix = this.getTransformToElement(target || this.node.ownerSVGElement); return V.transformRect(box, matrix); }; V.prototype.text = function(content, opt) { // Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm). // IE would otherwise collapse all spaces into one. content = V.sanitizeText(content); opt = opt || {}; var lines = content.split('\n'); var i = 0; var tspan; // `alignment-baseline` does not work in Firefox. // Setting `dominant-baseline` on the `` element doesn't work in IE9. // In order to have the 0,0 coordinate of the `` element (or the first ``) // in the top left corner we translate the `` element by `0.8em`. // See `http://www.w3.org/Graphics/SVG/WG/wiki/How_to_determine_dominant_baseline`. // See also `http://apike.ca/prog_svg_text_style.html`. var y = this.attr('y'); if (!y) { this.attr('y', '0.8em'); } // An empty text gets rendered into the DOM in webkit-based browsers. // In order to unify this behaviour across all browsers // we rather hide the text element when it's empty. this.attr('display', content ? null : 'none'); // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. this.node.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); // Easy way to erase all `` children; this.node.textContent = ''; var textNode = this.node; if (opt.textPath) { // Wrap the text in the SVG element that points // to a path defined by `opt.textPath` inside the internal `` element. var defs = this.find('defs'); if (defs.length === 0) { defs = V('defs'); this.append(defs); } // If `opt.textPath` is a plain string, consider it to be directly the // SVG path data for the text to go along (this is a shortcut). // Otherwise if it is an object and contains the `d` property, then this is our path. var d = Object(opt.textPath) === opt.textPath ? opt.textPath.d : opt.textPath; if (d) { var path = V('path', { d: d }); defs.append(path); } var textPath = V('textPath'); // Set attributes on the ``. The most important one // is the `xlink:href` that points to our newly created `` element in ``. // Note that we also allow the following construct: // `t.text('my text', { textPath: { 'xlink:href': '#my-other-path' } })`. // In other words, one can completely skip the auto-creation of the path // and use any other arbitrary path that is in the document. if (!opt.textPath['xlink:href'] && path) { textPath.attr('xlink:href', '#' + path.node.id); } if (Object(opt.textPath) === opt.textPath) { textPath.attr(opt.textPath); } this.append(textPath); // Now all the ``s will be inside the ``. textNode = textPath.node; } var offset = 0; for (var i = 0; i < lines.length; i++) { var line = lines[i]; // Shift all the but first by one line (`1em`) var lineHeight = opt.lineHeight || '1em'; if (opt.lineHeight === 'auto') { lineHeight = '1.5em'; } var vLine = V('tspan', { dy: (i == 0 ? '0em' : lineHeight), x: this.attr('x') || 0 }); vLine.addClass('v-line'); if (line) { if (opt.annotations) { // Get the line height based on the biggest font size in the annotations for this line. var maxFontSize = 0; // Find the *compacted* annotations for this line. var lineAnnotations = V.annotateString(lines[i], V.isArray(opt.annotations) ? opt.annotations : [opt.annotations], { offset: -offset, includeAnnotationIndices: opt.includeAnnotationIndices }); for (var j = 0; j < lineAnnotations.length; j++) { var annotation = lineAnnotations[j]; if (V.isObject(annotation)) { var fontSize = parseInt(annotation.attrs['font-size'], 10); if (fontSize && fontSize > maxFontSize) { maxFontSize = fontSize; } tspan = V('tspan', annotation.attrs); if (opt.includeAnnotationIndices) { // If `opt.includeAnnotationIndices` is `true`, // set the list of indices of all the applied annotations // in the `annotations` attribute. This list is a comma // separated list of indices. tspan.attr('annotations', annotation.annotations); } if (annotation.attrs['class']) { tspan.addClass(annotation.attrs['class']); } tspan.node.textContent = annotation.t; } else { tspan = document.createTextNode(annotation || ' '); } vLine.append(tspan); } if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) { vLine.attr('dy', (maxFontSize * 1.2) + 'px'); } } else { vLine.node.textContent = line; } } else { // Make sure the textContent is never empty. If it is, add a dummy // character and make it invisible, making the following lines correctly // relatively positioned. `dy=1em` won't work with empty lines otherwise. vLine.addClass('v-empty-line'); vLine.node.style.opacity = 0; vLine.node.textContent = '-'; } V(textNode).append(vLine); offset += line.length + 1; // + 1 = newline character. } return this; }; V.prototype.attr = function(name, value) { if (V.isUndefined(name)) { // Return all attributes. var attributes = this.node.attributes; var attrs = {}; for (var i = 0; i < attributes.length; i++) { attrs[attributes[i].nodeName] = attributes[i].nodeValue; } return attrs; } if (V.isString(name) && V.isUndefined(value)) { return this.node.getAttribute(name); } if (typeof name === 'object') { for (var attrName in name) { if (name.hasOwnProperty(attrName)) { V.setAttribute(this.node, attrName, name[attrName]); } } } else { V.setAttribute(this.node, name, value); } return this; }; V.prototype.remove = function() { if (this.node.parentNode) { this.node.parentNode.removeChild(this.node); } return this; }; V.prototype.empty = function() { while (this.node.firstChild) { this.node.removeChild(this.node.firstChild); } return this; }; V.prototype.setAttributes = function(attrs) { var key; for (key in attrs) { V.setAttribute(this.node, key, attrs[key]); } return this; }; V.prototype.append = function(els) { if (!V.isArray(els)) { els = [els]; } var i, len, el; for (i = 0, len = els.length; i < len; i++) { el = els[i]; this.node.appendChild(V.isV(el) ? el.node : (el.nodeName && el || el[0])); } return this; }; V.prototype.prepend = function(el) { this.node.insertBefore(V.isV(el) ? el.node : el, this.node.firstChild); return this; }; V.prototype.svg = function() { return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); }; V.prototype.defs = function() { var defs = this.svg().node.getElementsByTagName('defs'); return (defs && defs.length) ? V(defs[0]) : undefined; }; V.prototype.clone = function() { var clone = V(this.node.cloneNode(true/* deep */)); // Note that clone inherits also ID. Therefore, we need to change it here. clone.node.id = V.uniqueId(); return clone; }; V.prototype.findOne = function(selector) { var found = this.node.querySelector(selector); return found ? V(found) : undefined; }; V.prototype.find = function(selector) { var vels = []; var nodes = this.node.querySelectorAll(selector); if (nodes) { // Map DOM elements to `V`s. for (var i = 0; i < nodes.length; i++) { vels.push(V(nodes[i])); } } return vels; }; // Find an index of an element inside its container. V.prototype.index = function() { var index = 0; var node = this.node.previousSibling; while (node) { // nodeType 1 for ELEMENT_NODE if (node.nodeType === 1) index++; node = node.previousSibling; } return index; }; V.prototype.findParentByClass = function(className, terminator) { var ownerSVGElement = this.node.ownerSVGElement; var node = this.node.parentNode; while (node && node !== terminator && node !== ownerSVGElement) { var vel = V(node); if (vel.hasClass(className)) { return vel; } node = node.parentNode; } return null; }; // Convert global point into the coordinate space of this element. V.prototype.toLocalPoint = function(x, y) { var svg = this.svg().node; var p = svg.createSVGPoint(); p.x = x; p.y = y; try { var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse()); var globalToLocalMatrix = this.getTransformToElement(svg).inverse(); } catch (e) { // IE9 throws an exception in odd cases. (`Unexpected call to method or property access`) // We have to make do with the original coordianates. return p; } return globalPoint.matrixTransform(globalToLocalMatrix); }; V.prototype.translateCenterToPoint = function(p) { var bbox = this.bbox(); var center = g.rect(bbox).center(); this.translate(p.x - center.x, p.y - center.y); }; // Efficiently auto-orient an element. This basically implements the orient=auto attribute // of markers. The easiest way of understanding on what this does is to imagine the element is an // arrowhead. Calling this method on the arrowhead makes it point to the `position` point while // being auto-oriented (properly rotated) towards the `reference` point. // `target` is the element relative to which the transformations are applied. Usually a viewport. V.prototype.translateAndAutoOrient = function(position, reference, target) { // Clean-up previously set transformations except the scale. If we didn't clean up the // previous transformations then they'd add up with the old ones. Scale is an exception as // it doesn't add up, consider: `this.scale(2).scale(2).scale(2)`. The result is that the // element is scaled by the factor 2, not 8. var s = this.scale(); this.attr('transform', ''); this.scale(s.sx, s.sy); var svg = this.svg().node; var bbox = this.bbox(false, target); // 1. Translate to origin. var translateToOrigin = svg.createSVGTransform(); translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2); // 2. Rotate around origin. var rotateAroundOrigin = svg.createSVGTransform(); var angle = g.point(position).changeInAngle(position.x - reference.x, position.y - reference.y, reference); rotateAroundOrigin.setRotate(angle, 0, 0); // 3. Translate to the `position` + the offset (half my width) towards the `reference` point. var translateFinal = svg.createSVGTransform(); var finalPosition = g.point(position).move(reference, bbox.width / 2); translateFinal.setTranslate(position.x + (position.x - finalPosition.x), position.y + (position.y - finalPosition.y)); // 4. Apply transformations. var ctm = this.getTransformToElement(target); var transform = svg.createSVGTransform(); transform.setMatrix( translateFinal.matrix.multiply( rotateAroundOrigin.matrix.multiply( translateToOrigin.matrix.multiply( ctm))) ); // Instead of directly setting the `matrix()` transform on the element, first, decompose // the matrix into separate transforms. This allows us to use normal Vectorizer methods // as they don't work on matrices. An example of this is to retrieve a scale of an element. // this.node.transform.baseVal.initialize(transform); var decomposition = V.decomposeMatrix(transform.matrix); this.translate(decomposition.translateX, decomposition.translateY); this.rotate(decomposition.rotation); // Note that scale has been already applied, hence the following line stays commented. (it's here just for reference). //this.scale(decomposition.scaleX, decomposition.scaleY); return this; }; V.prototype.animateAlongPath = function(attrs, path) { var animateMotion = V('animateMotion', attrs); var mpath = V('mpath', { 'xlink:href': '#' + V(path).node.id }); animateMotion.append(mpath); this.append(animateMotion); try { animateMotion.node.beginElement(); } catch (e) { // Fallback for IE 9. // Run the animation programatically if FakeSmile (`http://leunen.me/fakesmile/`) present if (document.documentElement.getAttribute('smiling') === 'fake') { // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) var animation = animateMotion.node; animation.animators = []; var animationID = animation.getAttribute('id'); if (animationID) id2anim[animationID] = animation; var targets = getTargets(animation); for (var i = 0, len = targets.length; i < len; i++) { var target = targets[i]; var animator = new Animator(animation, target, i); animators.push(animator); animation.animators[i] = animator; animator.register(); } } } }; V.prototype.hasClass = function(className) { return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); }; V.prototype.addClass = function(className) { if (!this.hasClass(className)) { var prevClasses = this.node.getAttribute('class') || ''; this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); } return this; }; V.prototype.removeClass = function(className) { if (this.hasClass(className)) { var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2'); this.node.setAttribute('class', newClasses); } return this; }; V.prototype.toggleClass = function(className, toAdd) { var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; if (toRemove) { this.removeClass(className); } else { this.addClass(className); } return this; }; // Interpolate path by discrete points. The precision of the sampling // is controlled by `interval`. In other words, `sample()` will generate // a point on the path starting at the beginning of the path going to the end // every `interval` pixels. // The sampler can be very useful for e.g. finding intersection between two // paths (finding the two closest points from two samples). V.prototype.sample = function(interval) { interval = interval || 1; var node = this.node; var length = node.getTotalLength(); var samples = []; var distance = 0; var sample; while (distance < length) { sample = node.getPointAtLength(distance); samples.push({ x: sample.x, y: sample.y, distance: distance }); distance += interval; } return samples; }; V.prototype.convertToPath = function() { var path = V('path'); path.attr(this.attr()); var d = this.convertToPathData(); if (d) { path.attr('d', d); } return path; }; V.prototype.convertToPathData = function() { var tagName = this.node.tagName.toUpperCase(); switch (tagName) { case 'PATH': return this.attr('d'); case 'LINE': return V.convertLineToPathData(this.node); case 'POLYGON': return V.convertPolygonToPathData(this.node); case 'POLYLINE': return V.convertPolylineToPathData(this.node); case 'ELLIPSE': return V.convertEllipseToPathData(this.node); case 'CIRCLE': return V.convertCircleToPathData(this.node); case 'RECT': return V.convertRectToPathData(this.node); } throw new Error(tagName + ' cannot be converted to PATH.'); }; // Find the intersection of a line starting in the center // of the SVG `node` ending in the point `ref`. // `target` is an SVG element to which `node`s transformations are relative to. // In JointJS, `target` is the `paper.viewport` SVG group element. // Note that `ref` point must be in the coordinate system of the `target` for this function to work properly. // Returns a point in the `target` coordinte system (the same system as `ref` is in) if // an intersection is found. Returns `undefined` otherwise. V.prototype.findIntersection = function(ref, target) { var svg = this.svg().node; target = target || svg; var bbox = g.rect(this.bbox(false, target)); var center = bbox.center(); if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; var spot; var tagName = this.node.localName.toUpperCase(); // Little speed up optimalization for `` element. We do not do conversion // to path element and sampling but directly calculate the intersection through // a transformed geometrical rectangle. if (tagName === 'RECT') { var gRect = g.rect( parseFloat(this.attr('x') || 0), parseFloat(this.attr('y') || 0), parseFloat(this.attr('width')), parseFloat(this.attr('height')) ); // Get the rect transformation matrix with regards to the SVG document. var rectMatrix = this.getTransformToElement(target); // Decompose the matrix to find the rotation angle. var rectMatrixComponents = V.decomposeMatrix(rectMatrix); // Now we want to rotate the rectangle back so that we // can use `intersectionWithLineFromCenterToPoint()` passing the angle as the second argument. var resetRotation = svg.createSVGTransform(); resetRotation.setRotate(-rectMatrixComponents.rotation, center.x, center.y); var rect = V.transformRect(gRect, resetRotation.matrix.multiply(rectMatrix)); spot = g.rect(rect).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation); } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { var pathNode = (tagName === 'PATH') ? this : this.convertToPath(); var samples = pathNode.sample(); var minDistance = Infinity; var closestSamples = []; var i, sample, gp, centerDistance, refDistance, distance; for (i = 0; i < samples.length; i++) { sample = samples[i]; // Convert the sample point in the local coordinate system to the global coordinate system. gp = V.createSVGPoint(sample.x, sample.y); gp = gp.matrixTransform(this.getTransformToElement(target)); sample = g.point(gp); centerDistance = sample.distance(center); // Penalize a higher distance to the reference point by 10%. // This gives better results. This is due to // inaccuracies introduced by rounding errors and getPointAtLength() returns. refDistance = sample.distance(ref) * 1.1; distance = centerDistance + refDistance; if (distance < minDistance) { minDistance = distance; closestSamples = [{ sample: sample, refDistance: refDistance }]; } else if (distance < minDistance + 1) { closestSamples.push({ sample: sample, refDistance: refDistance }); } } closestSamples.sort(function(a, b) { return a.refDistance - b.refDistance; }); if (closestSamples[0]) { spot = closestSamples[0].sample; } } return spot; }; // Create an SVG document element. // If `content` is passed, it will be used as the SVG content of the `` root element. V.createSvgDocument = function(content) { var svg = '' + (content || '') + ''; var xml = V.parseXML(svg, { async: false }); return xml.documentElement; }; V.idCounter = 0; // A function returning a unique identifier for this client session with every call. V.uniqueId = function() { var id = ++V.idCounter + ''; return 'v-' + id; }; // Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm). // IE would otherwise collapse all spaces into one. This is used in the text() method but it is // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests // when you want to compare the actual DOM text content without having to add the unicode character in // the place of all spaces. V.sanitizeText = function(text) { return (text || '').replace(/ /g, '\u00A0'); }; V.isUndefined = function(value) { return typeof value === 'undefined'; }; V.isString = function(value) { return typeof value === 'string'; }; V.isObject = function(value) { return value && (typeof value === 'object'); }; V.isArray = Array.isArray; V.parseXML = function(data, opt) { opt = opt || {}; var xml; try { var parser = new DOMParser(); if (!V.isUndefined(opt.async)) { parser.async = opt.async; } xml = parser.parseFromString(data, 'text/xml'); } catch (error) { xml = undefined; } if (!xml || xml.getElementsByTagName('parsererror').length) { throw new Error('Invalid XML: ' + data); } return xml; }; V.setAttribute = function(el, name, value) { if (name.indexOf(':') > -1) { // Attribute names can be namespaced. E.g. `image` elements // have a `xlink:href` attribute to set the source of the image. var combinedKey = name.split(':'); el.setAttributeNS(ns[combinedKey[0]], combinedKey[1], value); } else if (name === 'id') { el.id = value; } else { el.setAttribute(name, value); } }; V.parseTransformString = function(transform) { var translate, rotate, scale; if (transform) { var separator = /[ ,]+/; var translateMatch = transform.match(/translate\((.*)\)/); if (translateMatch) { translate = translateMatch[1].split(separator); } var rotateMatch = transform.match(/rotate\((.*)\)/); if (rotateMatch) { rotate = rotateMatch[1].split(separator); } var scaleMatch = transform.match(/scale\((.*)\)/); if (scaleMatch) { scale = scaleMatch[1].split(separator); } } var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; return { translate: { tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 }, rotate: { angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined }, scale: { sx: sx, sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx } }; }; V.deltaTransformPoint = function(matrix, point) { var dx = point.x * matrix.a + point.y * matrix.c + 0; var dy = point.x * matrix.b + point.y * matrix.d + 0; return { x: dx, y: dy }; }; V.decomposeMatrix = function(matrix) { // @see https://gist.github.com/2052247 // calculate delta transform point var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); // calculate skew var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); return { translateX: matrix.e, translateY: matrix.f, scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), skewX: skewX, skewY: skewY, rotation: skewX // rotation is the same as skew x }; }; V.isV = function(object) { return object instanceof V; }; // For backwards compatibility: V.isVElement = V.isV; var svgDocument = V('svg').node; V.createSVGMatrix = function(matrix) { var svgMatrix = svgDocument.createSVGMatrix(); for (var component in matrix) { svgMatrix[component] = matrix[component]; } return svgMatrix; }; V.createSVGTransform = function(matrix) { if (!V.isUndefined(matrix)) { if (!(matrix instanceof SVGMatrix)) { matrix = V.createSVGMatrix(matrix); } return svgDocument.createSVGTransformFromMatrix(matrix); } return svgDocument.createSVGTransform(); }; V.createSVGPoint = function(x, y) { var p = svgDocument.createSVGPoint(); p.x = x; p.y = y; return p; }; V.transformRect = function(r, matrix) { var p = svgDocument.createSVGPoint(); p.x = r.x; p.y = r.y; var corner1 = p.matrixTransform(matrix); p.x = r.x + r.width; p.y = r.y; var corner2 = p.matrixTransform(matrix); p.x = r.x + r.width; p.y = r.y + r.height; var corner3 = p.matrixTransform(matrix); p.x = r.x; p.y = r.y + r.height; var corner4 = p.matrixTransform(matrix); var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x); var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x); var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y); var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }; V.transformPoint = function(p, matrix) { return V.createSVGPoint(p.x, p.y).matrixTransform(matrix); }; // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to // an object (`{ fill: 'blue', stroke: 'red' }`). V.styleToObject = function(styleString) { var ret = {}; var styles = styleString.split(';'); for (var i = 0; i < styles.length; i++) { var style = styles[i]; var pair = style.split('='); ret[pair[0].trim()] = pair[1].trim(); } return ret; }; // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { var svgArcMax = 2 * Math.PI - 1e-6; var r0 = innerRadius; var r1 = outerRadius; var a0 = startAngle; var a1 = endAngle; var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); var df = da < Math.PI ? '0' : '1'; var c0 = Math.cos(a0); var s0 = Math.sin(a0); var c1 = Math.cos(a1); var s1 = Math.sin(a1); return (da >= svgArcMax) ? (r0 ? 'M0,' + r1 + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + 'M0,' + r0 + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 + 'Z' : 'M0,' + r1 + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + 'Z') : (r0 ? 'M' + r1 * c0 + ',' + r1 * s0 + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + 'L' + r0 * c1 + ',' + r0 * s1 + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 + 'Z' : 'M' + r1 * c0 + ',' + r1 * s0 + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + 'L0,0' + 'Z'); }; // Merge attributes from object `b` with attributes in object `a`. // Note that this modifies the object `a`. // Also important to note that attributes are merged but CSS classes are concatenated. V.mergeAttrs = function(a, b) { for (var attr in b) { if (attr === 'class') { // Concatenate classes. a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; } else if (attr === 'style') { // `style` attribute can be an object. if (V.isObject(a[attr]) && V.isObject(b[attr])) { // `style` stored in `a` is an object. a[attr] = V.mergeAttrs(a[attr], b[attr]); } else if (V.isObject(a[attr])) { // `style` in `a` is an object but it's a string in `b`. // Convert the style represented as a string to an object in `b`. a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); } else if (V.isObject(b[attr])) { // `style` in `a` is a string, in `b` it's an object. a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); } else { // Both styles are strings. a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); } } else { a[attr] = b[attr]; } } return a; }; V.annotateString = function(t, annotations, opt) { annotations = annotations || []; opt = opt || {}; var offset = opt.offset || 0; var compacted = []; var batch; var ret = []; var item; var prev; for (var i = 0; i < t.length; i++) { item = ret[i] = t[i]; for (var j = 0; j < annotations.length; j++) { var annotation = annotations[j]; var start = annotation.start + offset; var end = annotation.end + offset; if (i >= start && i < end) { // Annotation applies. if (V.isObject(item)) { // There is more than one annotation to be applied => Merge attributes. item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); } else { item = ret[i] = { t: t[i], attrs: annotation.attrs }; } if (opt.includeAnnotationIndices) { (item.annotations || (item.annotations = [])).push(j); } } } prev = ret[i - 1]; if (!prev) { batch = item; } else if (V.isObject(item) && V.isObject(prev)) { // Both previous item and the current one are annotations. If the attributes // didn't change, merge the text. if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { batch.t += item.t; } else { compacted.push(batch); batch = item; } } else if (V.isObject(item)) { // Previous item was a string, current item is an annotation. compacted.push(batch); batch = item; } else if (V.isObject(prev)) { // Previous item was an annotation, current item is a string. compacted.push(batch); batch = item; } else { // Both previous and current item are strings. batch = (batch || '') + item; } } if (batch) { compacted.push(batch); } return compacted; }; V.findAnnotationsAtIndex = function(annotations, index) { var found = []; if (annotations) { annotations.forEach(function(annotation) { if (annotation.start < index && index <= annotation.end) { found.push(annotation); } }); } return found; }; V.findAnnotationsBetweenIndexes = function(annotations, start, end) { var found = []; if (annotations) { annotations.forEach(function(annotation) { if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { found.push(annotation); } }); } return found; }; // Shift all the text annotations after character `index` by `offset` positions. V.shiftAnnotations = function(annotations, index, offset) { if (annotations) { annotations.forEach(function(annotation) { if (annotation.start < index && annotation.end >= index) { annotation.end += offset; } else if (annotation.start >= index) { annotation.start += offset; annotation.end += offset; } }); } return annotations; }; V.convertLineToPathData = function(line) { line = V(line); var d = [ 'M', line.attr('x1'), line.attr('y1'), 'L', line.attr('x2'), line.attr('y2') ].join(' '); return d; }; V.convertPolygonToPathData = function(polygon) { polygon = V(polygon); var points = V.getPointsFromSvgNode(polygon.node); if (!(points.length > 0)) return null; return V.svgPointsToPath(points); }; V.convertPolylineToPathData = function(polyline) { var points = V.getPointsFromSvgNode(polyline.node); if (!(points.length > 0)) return null; return V.svgPointsToPath(points); }; V.svgPointsToPath = function(points) { var i; for (i = 0; i < points.length; i++) { points[i] = points[i].x + ' ' + points[i].y; } return 'M ' + points.join(' L') + ' Z'; }; V.getPointsFromSvgNode = function(node) { var points = []; var i; for (i = 0; i < node.points.numberOfItems; i++) { points.push(node.points.getItem(i)); } return points; }; V.KAPPA = 0.5522847498307935; V.convertCircleToPathData = function(circle) { circle = V(circle); var cx = parseFloat(circle.attr('cx')) || 0; var cy = parseFloat(circle.attr('cy')) || 0; var r = parseFloat(circle.attr('r')); var cd = r * V.KAPPA; // Control distance. var d = [ 'M', cx, cy - r, // Move to the first point. 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. 'Z' ].join(' '); return d; }; V.convertEllipseToPathData = function(ellipse) { ellipse = V(ellipse); var cx = parseFloat(ellipse.attr('cx')) || 0; var cy = parseFloat(ellipse.attr('cy')) || 0; var rx = parseFloat(ellipse.attr('rx')); var ry = parseFloat(ellipse.attr('ry')) || rx; var cdx = rx * V.KAPPA; // Control distance x. var cdy = ry * V.KAPPA; // Control distance y. var d = [ 'M', cx, cy - ry, // Move to the first point. 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. 'Z' ].join(' '); return d; }; V.convertRectToPathData = function(rect) { rect = V(rect); var x = parseFloat(rect.attr('x')) || 0; var y = parseFloat(rect.attr('y')) || 0; var width = parseFloat(rect.attr('width')) || 0; var height = parseFloat(rect.attr('height')) || 0; var rx = parseFloat(rect.attr('rx')) || 0; var ry = parseFloat(rect.attr('ry')) || 0; var bbox = g.rect(x, y, width, height); var d; if (!rx && !ry) { d = [ 'M', bbox.origin().x, bbox.origin().y, 'H', bbox.corner().x, 'V', bbox.corner().y, 'H', bbox.origin().x, 'V', bbox.origin().y, 'Z' ].join(' '); } else { var r = x + width; var b = y + height; d = [ 'M', x + rx, y, 'L', r - rx, y, 'Q', r, y, r, y + ry, 'L', r, y + height - ry, 'Q', r, b, r - rx, b, 'L', x + rx, b, 'Q', x, b, x, b - rx, 'L', x, y + ry, 'Q', x, y, x + rx, y, 'Z' ].join(' '); } return d; }; // Convert a rectangle to SVG path commands. `r` is an object of the form: // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle // that has only `rx` and `ry` attributes). V.rectToPath = function(r) { var topRx = r.rx || r['top-rx'] || 0; var bottomRx = r.rx || r['bottom-rx'] || 0; var topRy = r.ry || r['top-ry'] || 0; var bottomRy = r.ry || r['bottom-ry'] || 0; return [ 'M', r.x, r.y + topRy, 'v', r.height - topRy - bottomRy, 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, 'h', r.width - 2 * bottomRx, 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, 'v', -(r.height - bottomRy - topRy), 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, 'h', -(r.width - 2 * topRx), 'a', topRx, topRy, 0, 0, 0, -topRx, topRy ].join(' '); }; return V; })(); return V; }));