1 /*! JointJS v0.9.3 - JavaScript diagramming library 2015-05-22
4 This Source Code Form is subject to the terms of the Mozilla Public
5 License, v. 2.0. If a copy of the MPL was not distributed with this
6 file, You can obtain one at http://mozilla.org/MPL/2.0/.
11 // A tiny library for making your live easier when dealing with SVG.
12 // The only Vectorizer dependency is the Geometry library.
14 // Copyright © 2012 - 2015 client IO (http://client.io)
16 (function(root, factory) {
18 if (typeof define === 'function' && define.amd) {
20 // AMD. Register as an anonymous module.
21 define(['app/gbp/js/geometry'], function(g) {
26 } else if (typeof exports === 'object') {
28 // Node. Does not work with strict CommonJS, but
29 // only CommonJS-like environments that support module.exports,
31 module.exports = factory();
36 root.Vectorizer = root.V = factory();
42 var SVGsupported = typeof window === 'object' && !!(window.SVGAngle || document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1'));
44 // SVG support is required.
45 if (!SVGsupported) return function() {};
49 xmlns: 'http://www.w3.org/2000/svg',
50 xlink: 'http://www.w3.org/1999/xlink'
53 var SVGversion = '1.1';
55 // A function returning a unique identifier for this client session with every call.
58 var id = ++idCounter + '';
62 // Create an SVG document element.
63 // If `content` is passed, it will be used as the SVG content of the `<svg>` root element.
64 function createSvgDocument(content) {
66 var svg = '<svg xmlns="' + ns.xmlns + '" xmlns:xlink="' + ns.xlink + '" version="' + SVGversion + '">' + (content || '') + '</svg>';
67 var parser = new DOMParser();
69 return parser.parseFromString(svg, 'text/xml').documentElement;
72 // Create SVG element.
73 // -------------------
75 function createElement(el, attrs, children) {
79 if (!el) return undefined;
81 // If `el` is an object, it is probably a native SVG element. Wrap it to VElement.
82 if (typeof el === 'object') {
83 return new VElement(el);
87 // If `el` is a `'svg'` or `'SVG'` string, create a new SVG canvas.
88 if (el.toLowerCase() === 'svg') {
90 return new VElement(createSvgDocument());
92 } else if (el[0] === '<') {
93 // Create element from an SVG string.
94 // Allows constructs of type: `document.appendChild(Vectorizer('<rect></rect>').node)`.
96 var svgDoc = createSvgDocument(el);
98 // Note that `createElement()` might also return an array should the SVG string passed as
99 // the first argument contain more then one root element.
100 if (svgDoc.childNodes.length > 1) {
102 // Map child nodes to `VElement`s.
104 for (i = 0, len = svgDoc.childNodes.length; i < len; i++) {
106 var childNode = svgDoc.childNodes[i];
107 ret.push(new VElement(document.importNode(childNode, true)));
112 return new VElement(document.importNode(svgDoc.firstChild, true));
115 el = document.createElementNS(ns.xmlns, el);
118 for (var key in attrs) {
120 setAttribute(el, key, attrs[key]);
123 // Normalize `children` array.
124 if (Object.prototype.toString.call(children) != '[object Array]') children = [children];
126 // Append children if they are specified.
127 for (i = 0, len = (children[0] && children.length) || 0; i < len; i++) {
128 var child = children[i];
129 el.appendChild(child instanceof VElement ? child.node : child);
132 return new VElement(el);
135 function setAttribute(el, name, value) {
137 if (name.indexOf(':') > -1) {
138 // Attribute names can be namespaced. E.g. `image` elements
139 // have a `xlink:href` attribute to set the source of the image.
140 var combinedKey = name.split(':');
141 el.setAttributeNS(ns[combinedKey[0]], combinedKey[1], value);
143 } else if (name === 'id') {
146 el.setAttribute(name, value);
150 function parseTransformString(transform) {
157 var separator = /[ ,]+/;
159 var translateMatch = transform.match(/translate\((.*)\)/);
160 if (translateMatch) {
161 translate = translateMatch[1].split(separator);
163 var rotateMatch = transform.match(/rotate\((.*)\)/);
165 rotate = rotateMatch[1].split(separator);
167 var scaleMatch = transform.match(/scale\((.*)\)/);
169 scale = scaleMatch[1].split(separator);
173 var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1;
177 tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0,
178 ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0
181 angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0,
182 cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined,
183 cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined
187 sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx
193 // Matrix decomposition.
194 // ---------------------
196 function deltaTransformPoint(matrix, point) {
198 var dx = point.x * matrix.a + point.y * matrix.c + 0;
199 var dy = point.x * matrix.b + point.y * matrix.d + 0;
200 return { x: dx, y: dy };
203 function decomposeMatrix(matrix) {
205 // @see https://gist.github.com/2052247
207 // calculate delta transform point
208 var px = deltaTransformPoint(matrix, { x: 0, y: 1 });
209 var py = deltaTransformPoint(matrix, { x: 1, y: 0 });
212 var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90);
213 var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x));
217 translateX: matrix.e,
218 translateY: matrix.f,
219 scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b),
220 scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d),
223 rotation: skewX // rotation is the same as skew x
230 function VElement(el) {
233 this.node.id = uniqueId();
237 // VElement public API.
238 // --------------------
240 VElement.prototype = {
242 translate: function(tx, ty, opt) {
247 var transformAttr = this.attr('transform') || '';
248 var transform = parseTransformString(transformAttr);
251 if (typeof tx === 'undefined') {
252 return transform.translate;
255 transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim();
257 var newTx = opt.absolute ? tx : transform.translate.tx + tx;
258 var newTy = opt.absolute ? ty : transform.translate.ty + ty;
259 var newTranslate = 'translate(' + newTx + ',' + newTy + ')';
261 // Note that `translate()` is always the first transformation. This is
262 // usually the desired case.
263 this.attr('transform', (newTranslate + ' ' + transformAttr).trim());
267 rotate: function(angle, cx, cy, opt) {
271 var transformAttr = this.attr('transform') || '';
272 var transform = parseTransformString(transformAttr);
275 if (typeof angle === 'undefined') {
276 return transform.rotate;
279 transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim();
283 var newAngle = opt.absolute ? angle : transform.rotate.angle + angle;
284 var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : '';
285 var newRotate = 'rotate(' + newAngle + newOrigin + ')';
287 this.attr('transform', (transformAttr + ' ' + newRotate).trim());
291 // Note that `scale` as the only transformation does not combine with previous values.
292 scale: function(sx, sy) {
293 sy = (typeof sy === 'undefined') ? sx : sy;
295 var transformAttr = this.attr('transform') || '';
296 var transform = parseTransformString(transformAttr);
299 if (typeof sx === 'undefined') {
300 return transform.scale;
303 transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim();
305 var newScale = 'scale(' + sx + ',' + sy + ')';
307 this.attr('transform', (transformAttr + ' ' + newScale).trim());
311 // Get SVGRect that contains coordinates and dimension of the real bounding box,
312 // i.e. after transformations are applied.
313 // If `target` is specified, bounding box will be computed relatively to `target` element.
314 bbox: function(withoutTransformations, target) {
316 // If the element is not in the live DOM, it does not have a bounding box defined and
317 // so fall back to 'zero' dimension element.
318 if (!this.node.ownerSVGElement) return { x: 0, y: 0, width: 0, height: 0 };
323 box = this.node.getBBox();
325 // Opera returns infinite values in some cases.
326 // Note that Infinity | 0 produces 0 as opposed to Infinity || 0.
327 // We also have to create new object as the standard says that you can't
328 // modify the attributes of a bbox.
329 box = { x: box.x | 0, y: box.y | 0, width: box.width | 0, height: box.height | 0 };
335 x: this.node.clientLeft,
336 y: this.node.clientTop,
337 width: this.node.clientWidth,
338 height: this.node.clientHeight
342 if (withoutTransformations) {
347 var matrix = this.node.getTransformToElement(target || this.node.ownerSVGElement);
349 return V.transformRect(box, matrix);
352 text: function(content, opt) {
355 var lines = content.split('\n');
359 // `alignment-baseline` does not work in Firefox.
360 // Setting `dominant-baseline` on the `<text>` element doesn't work in IE9.
361 // In order to have the 0,0 coordinate of the `<text>` element (or the first `<tspan>`)
362 // in the top left corner we translate the `<text>` element by `0.8em`.
363 // See `http://www.w3.org/Graphics/SVG/WG/wiki/How_to_determine_dominant_baseline`.
364 // See also `http://apike.ca/prog_svg_text_style.html`.
365 this.attr('y', '0.8em');
367 // An empty text gets rendered into the DOM in webkit-based browsers.
368 // In order to unify this behaviour across all browsers
369 // we rather hide the text element when it's empty.
370 this.attr('display', content ? null : 'none');
372 // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one.
373 this.node.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
375 // Easy way to erase all `<tspan>` children;
376 this.node.textContent = '';
378 var textNode = this.node;
382 // Wrap the text in the SVG <textPath> element that points
383 // to a path defined by `opt.textPath` inside the internal `<defs>` element.
384 var defs = this.find('defs');
385 if (defs.length === 0) {
386 defs = createElement('defs');
390 // If `opt.textPath` is a plain string, consider it to be directly the
391 // SVG path data for the text to go along (this is a shortcut).
392 // Otherwise if it is an object and contains the `d` property, then this is our path.
393 var d = Object(opt.textPath) === opt.textPath ? opt.textPath.d : opt.textPath;
395 var path = createElement('path', { d: d });
399 var textPath = createElement('textPath');
400 // Set attributes on the `<textPath>`. The most important one
401 // is the `xlink:href` that points to our newly created `<path/>` element in `<defs/>`.
402 // Note that we also allow the following construct:
403 // `t.text('my text', { textPath: { 'xlink:href': '#my-other-path' } })`.
404 // In other words, one can completely skip the auto-creation of the path
405 // and use any other arbitrary path that is in the document.
406 if (!opt.textPath['xlink:href'] && path) {
407 textPath.attr('xlink:href', '#' + path.node.id);
410 if (Object(opt.textPath) === opt.textPath) {
411 textPath.attr(opt.textPath);
413 this.append(textPath);
414 // Now all the `<tspan>`s will be inside the `<textPath>`.
415 textNode = textPath.node;
418 if (lines.length === 1) {
419 textNode.textContent = content;
423 for (; i < lines.length; i++) {
425 // Shift all the <tspan> but first by one line (`1em`)
426 tspan = V('tspan', { dy: (i == 0 ? '0em' : opt.lineHeight || '1em'), x: this.attr('x') || 0 });
427 tspan.addClass('v-line');
429 tspan.addClass('v-empty-line');
431 // Make sure the textContent is never empty. If it is, add an additional
432 // space (an invisible character) so that following lines are correctly
433 // relatively positioned. `dy=1em` won't work with empty lines otherwise.
434 tspan.node.textContent = lines[i] || ' ';
436 V(textNode).append(tspan);
441 attr: function(name, value) {
443 if (typeof name === 'undefined') {
444 // Return all attributes.
445 var attributes = this.node.attributes;
447 for (var i = 0; i < attributes.length; i++) {
448 attrs[attributes[i].nodeName] = attributes[i].nodeValue;
453 if (typeof name === 'string' && typeof value === 'undefined') {
454 return this.node.getAttribute(name);
457 if (typeof name === 'object') {
459 for (var attrName in name) {
460 if (name.hasOwnProperty(attrName)) {
461 setAttribute(this.node, attrName, name[attrName]);
467 setAttribute(this.node, name, value);
474 if (this.node.parentNode) {
475 this.node.parentNode.removeChild(this.node);
479 append: function(el) {
483 if (Object.prototype.toString.call(el) !== '[object Array]') {
488 for (var i = 0, len = els.length; i < len; i++) {
490 this.node.appendChild(el instanceof VElement ? el.node : el);
496 prepend: function(el) {
497 this.node.insertBefore(el instanceof VElement ? el.node : el, this.node.firstChild);
502 return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement);
507 var defs = this.svg().node.getElementsByTagName('defs');
509 return (defs && defs.length) ? V(defs[0]) : undefined;
513 var clone = V(this.node.cloneNode(true));
514 // Note that clone inherits also ID. Therefore, we need to change it here.
515 clone.node.id = uniqueId();
519 findOne: function(selector) {
521 var found = this.node.querySelector(selector);
522 return found ? V(found) : undefined;
525 find: function(selector) {
527 var nodes = this.node.querySelectorAll(selector);
529 // Map DOM elements to `VElement`s.
530 for (var i = 0, len = nodes.length; i < len; i++) {
531 nodes[i] = V(nodes[i]);
536 // Find an index of an element inside its container.
540 var node = this.node.previousSibling;
543 // nodeType 1 for ELEMENT_NODE
544 if (node.nodeType === 1) index++;
545 node = node.previousSibling;
551 // Convert global point into the coordinate space of this element.
552 toLocalPoint: function(x, y) {
554 var svg = this.svg().node;
556 var p = svg.createSVGPoint();
562 var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse());
563 var globalToLocalMatrix = this.node.getTransformToElement(svg).inverse();
566 // IE9 throws an exception in odd cases. (`Unexpected call to method or property access`)
567 // We have to make do with the original coordianates.
571 return globalPoint.matrixTransform(globalToLocalMatrix);
574 translateCenterToPoint: function(p) {
576 var bbox = this.bbox();
577 var center = g.rect(bbox).center();
579 this.translate(p.x - center.x, p.y - center.y);
582 // Efficiently auto-orient an element. This basically implements the orient=auto attribute
583 // of markers. The easiest way of understanding on what this does is to imagine the element is an
584 // arrowhead. Calling this method on the arrowhead makes it point to the `position` point while
585 // being auto-oriented (properly rotated) towards the `reference` point.
586 // `target` is the element relative to which the transformations are applied. Usually a viewport.
587 translateAndAutoOrient: function(position, reference, target) {
589 // Clean-up previously set transformations except the scale. If we didn't clean up the
590 // previous transformations then they'd add up with the old ones. Scale is an exception as
591 // it doesn't add up, consider: `this.scale(2).scale(2).scale(2)`. The result is that the
592 // element is scaled by the factor 2, not 8.
594 var s = this.scale();
595 this.attr('transform', '');
596 this.scale(s.sx, s.sy);
598 var svg = this.svg().node;
599 var bbox = this.bbox(false, target);
601 // 1. Translate to origin.
602 var translateToOrigin = svg.createSVGTransform();
603 translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2);
605 // 2. Rotate around origin.
606 var rotateAroundOrigin = svg.createSVGTransform();
607 var angle = g.point(position).changeInAngle(position.x - reference.x, position.y - reference.y, reference);
608 rotateAroundOrigin.setRotate(angle, 0, 0);
610 // 3. Translate to the `position` + the offset (half my width) towards the `reference` point.
611 var translateFinal = svg.createSVGTransform();
612 var finalPosition = g.point(position).move(reference, bbox.width / 2);
613 translateFinal.setTranslate(position.x + (position.x - finalPosition.x), position.y + (position.y - finalPosition.y));
615 // 4. Apply transformations.
616 var ctm = this.node.getTransformToElement(target);
617 var transform = svg.createSVGTransform();
619 translateFinal.matrix.multiply(
620 rotateAroundOrigin.matrix.multiply(
621 translateToOrigin.matrix.multiply(
625 // Instead of directly setting the `matrix()` transform on the element, first, decompose
626 // the matrix into separate transforms. This allows us to use normal Vectorizer methods
627 // as they don't work on matrices. An example of this is to retrieve a scale of an element.
628 // this.node.transform.baseVal.initialize(transform);
630 var decomposition = decomposeMatrix(transform.matrix);
632 this.translate(decomposition.translateX, decomposition.translateY);
633 this.rotate(decomposition.rotation);
634 // Note that scale has been already applied, hence the following line stays commented. (it's here just for reference).
635 //this.scale(decomposition.scaleX, decomposition.scaleY);
640 animateAlongPath: function(attrs, path) {
642 var animateMotion = V('animateMotion', attrs);
643 var mpath = V('mpath', { 'xlink:href': '#' + V(path).node.id });
645 animateMotion.append(mpath);
647 this.append(animateMotion);
649 animateMotion.node.beginElement();
651 // Fallback for IE 9.
652 // Run the animation programatically if FakeSmile (`http://leunen.me/fakesmile/`) present
653 if (document.documentElement.getAttribute('smiling') === 'fake') {
655 // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`)
656 var animation = animateMotion.node;
657 animation.animators = [];
659 var animationID = animation.getAttribute('id');
660 if (animationID) id2anim[animationID] = animation;
662 var targets = getTargets(animation);
663 for (var i = 0, len = targets.length; i < len; i++) {
664 var target = targets[i];
665 var animator = new Animator(animation, target, i);
666 animators.push(animator);
667 animation.animators[i] = animator;
674 hasClass: function(className) {
676 return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class'));
679 addClass: function(className) {
681 if (!this.hasClass(className)) {
682 var prevClasses = this.node.getAttribute('class') || '';
683 this.node.setAttribute('class', (prevClasses + ' ' + className).trim());
689 removeClass: function(className) {
691 if (this.hasClass(className)) {
692 var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2');
693 this.node.setAttribute('class', newClasses);
699 toggleClass: function(className, toAdd) {
701 var toRemove = typeof toAdd === 'undefined' ? this.hasClass(className) : !toAdd;
704 this.removeClass(className);
706 this.addClass(className);
712 // Interpolate path by discrete points. The precision of the sampling
713 // is controlled by `interval`. In other words, `sample()` will generate
714 // a point on the path starting at the beginning of the path going to the end
715 // every `interval` pixels.
716 // The sampler can be very useful for e.g. finding intersection between two
717 // paths (finding the two closest points from two samples).
718 sample: function(interval) {
720 interval = interval || 1;
721 var node = this.node;
722 var length = node.getTotalLength();
726 while (distance < length) {
727 sample = node.getPointAtLength(distance);
728 samples.push({ x: sample.x, y: sample.y, distance: distance });
729 distance += interval;
734 convertToPath: function() {
736 var path = createElement('path');
737 path.attr(this.attr());
738 var d = this.convertToPathData();
745 convertToPathData: function() {
747 var tagName = this.node.tagName.toUpperCase();
751 return this.attr('d');
753 return convertLineToPathData(this.node);
755 return convertPolygonToPathData(this.node);
757 return convertPolylineToPathData(this.node);
759 return convertEllipseToPathData(this.node);
761 return convertCircleToPathData(this.node);
763 return convertRectToPathData(this.node);
766 throw new Error(tagName + ' cannot be converted to PATH.');
769 // Find the intersection of a line starting in the center
770 // of the SVG `node` ending in the point `ref`.
771 // `target` is an SVG element to which `node`s transformations are relative to.
772 // In JointJS, `target` is the `paper.viewport` SVG group element.
773 // Note that `ref` point must be in the coordinate system of the `target` for this function to work properly.
774 // Returns a point in the `target` coordinte system (the same system as `ref` is in) if
775 // an intersection is found. Returns `undefined` otherwise.
776 findIntersection: function(ref, target) {
778 var svg = this.svg().node;
779 target = target || svg;
780 var bbox = g.rect(this.bbox(false, target));
781 var center = bbox.center();
782 var spot = bbox.intersectionWithLineFromCenterToPoint(ref);
784 if (!spot) return undefined;
786 var tagName = this.node.localName.toUpperCase();
788 // Little speed up optimalization for `<rect>` element. We do not do conversion
789 // to path element and sampling but directly calculate the intersection through
790 // a transformed geometrical rectangle.
791 if (tagName === 'RECT') {
794 parseFloat(this.attr('x') || 0),
795 parseFloat(this.attr('y') || 0),
796 parseFloat(this.attr('width')),
797 parseFloat(this.attr('height'))
799 // Get the rect transformation matrix with regards to the SVG document.
800 var rectMatrix = this.node.getTransformToElement(target);
801 // Decompose the matrix to find the rotation angle.
802 var rectMatrixComponents = V.decomposeMatrix(rectMatrix);
803 // Now we want to rotate the rectangle back so that we
804 // can use `intersectionWithLineFromCenterToPoint()` passing the angle as the second argument.
805 var resetRotation = svg.createSVGTransform();
806 resetRotation.setRotate(-rectMatrixComponents.rotation, center.x, center.y);
807 var rect = V.transformRect(gRect, resetRotation.matrix.multiply(rectMatrix));
808 spot = g.rect(rect).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation);
810 } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') {
812 var pathNode = (tagName === 'PATH') ? this : this.convertToPath();
813 var samples = pathNode.sample();
814 var minDistance = Infinity;
815 var closestSamples = [];
817 for (var i = 0, len = samples.length; i < len; i++) {
819 var sample = samples[i];
820 // Convert the sample point in the local coordinate system to the global coordinate system.
821 var gp = V.createSVGPoint(sample.x, sample.y);
822 gp = gp.matrixTransform(this.node.getTransformToElement(target));
823 sample = g.point(gp);
824 var centerDistance = sample.distance(center);
825 // Penalize a higher distance to the reference point by 10%.
826 // This gives better results. This is due to
827 // inaccuracies introduced by rounding errors and getPointAtLength() returns.
828 var refDistance = sample.distance(ref) * 1.1;
829 var distance = centerDistance + refDistance;
830 if (distance < minDistance) {
831 minDistance = distance;
832 closestSamples = [{ sample: sample, refDistance: refDistance }];
833 } else if (distance < minDistance + 1) {
834 closestSamples.push({ sample: sample, refDistance: refDistance });
837 closestSamples.sort(function(a, b) { return a.refDistance - b.refDistance; });
838 spot = closestSamples[0].sample;
845 function convertLineToPathData(line) {
847 line = createElement(line);
849 'M', line.attr('x1'), line.attr('y1'),
850 'L', line.attr('x2'), line.attr('y2')
855 function convertPolygonToPathData(polygon) {
857 polygon = createElement(polygon);
858 var points = polygon.node.points;
862 for (var i = 0; i < points.length; i++) {
864 d.push(i === 0 ? 'M' : 'L', p.x, p.y);
870 function convertPolylineToPathData(polyline) {
872 polyline = createElement(polyline);
873 var points = polyline.node.points;
877 for (var i = 0; i < points.length; i++) {
879 d.push(i === 0 ? 'M' : 'L', p.x, p.y);
884 var KAPPA = 0.5522847498307935;
886 function convertCircleToPathData(circle) {
888 circle = createElement(circle);
889 var cx = parseFloat(circle.attr('cx')) || 0;
890 var cy = parseFloat(circle.attr('cy')) || 0;
891 var r = parseFloat(circle.attr('r'));
892 var cd = r * KAPPA; // Control distance.
895 'M', cx, cy - r, // Move to the first point.
896 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant.
897 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant.
898 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant.
899 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant.
905 function convertEllipseToPathData(ellipse) {
907 ellipse = createElement(ellipse);
908 var cx = parseFloat(ellipse.attr('cx')) || 0;
909 var cy = parseFloat(ellipse.attr('cy')) || 0;
910 var rx = parseFloat(ellipse.attr('rx'));
911 var ry = parseFloat(ellipse.attr('ry')) || rx;
912 var cdx = rx * KAPPA; // Control distance x.
913 var cdy = ry * KAPPA; // Control distance y.
916 'M', cx, cy - ry, // Move to the first point.
917 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant.
918 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant.
919 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant.
920 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant.
926 function convertRectToPathData(rect) {
928 rect = createElement(rect);
929 var x = parseFloat(rect.attr('x')) || 0;
930 var y = parseFloat(rect.attr('y')) || 0;
931 var width = parseFloat(rect.attr('width')) || 0;
932 var height = parseFloat(rect.attr('height')) || 0;
933 var rx = parseFloat(rect.attr('rx')) || 0;
934 var ry = parseFloat(rect.attr('ry')) || 0;
935 var bbox = g.rect(x, y, width, height);
942 'M', bbox.origin().x, bbox.origin().y,
943 'H', bbox.corner().x,
944 'V', bbox.corner().y,
945 'H', bbox.origin().x,
946 'V', bbox.origin().y,
957 'Q', r, y, r, y + ry,
958 'L', r, y + height - ry,
959 'Q', r, b, r - rx, b,
961 'Q', x, b, x, b - rx,
963 'Q', x, y, x + rx, y,
970 // Convert a rectangle to SVG path commands. `r` is an object of the form:
971 // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`,
972 // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for
973 // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle
974 // that has only `rx` and `ry` attributes).
975 function rectToPath(r) {
977 var topRx = r.rx || r['top-rx'] || 0;
978 var bottomRx = r.rx || r['bottom-rx'] || 0;
979 var topRy = r.ry || r['top-ry'] || 0;
980 var bottomRy = r.ry || r['bottom-ry'] || 0;
983 'M', r.x, r.y + topRy,
984 'v', r.height - topRy - bottomRy,
985 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy,
986 'h', r.width - 2 * bottomRx,
987 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy,
988 'v', -(r.height - bottomRy - topRy),
989 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy,
990 'h', -(r.width - 2 * topRx),
991 'a', topRx, topRy, 0, 0, 0, -topRx, topRy
995 var V = createElement;
997 V.decomposeMatrix = decomposeMatrix;
998 V.rectToPath = rectToPath;
1000 var svgDocument = V('svg').node;
1002 V.createSVGMatrix = function(m) {
1004 var svgMatrix = svgDocument.createSVGMatrix();
1005 for (var component in m) {
1006 svgMatrix[component] = m[component];
1012 V.createSVGTransform = function() {
1014 return svgDocument.createSVGTransform();
1017 V.createSVGPoint = function(x, y) {
1019 var p = svgDocument.createSVGPoint();
1025 V.transformRect = function(r, matrix) {
1027 var p = svgDocument.createSVGPoint();
1031 var corner1 = p.matrixTransform(matrix);
1033 p.x = r.x + r.width;
1035 var corner2 = p.matrixTransform(matrix);
1037 p.x = r.x + r.width;
1038 p.y = r.y + r.height;
1039 var corner3 = p.matrixTransform(matrix);
1042 p.y = r.y + r.height;
1043 var corner4 = p.matrixTransform(matrix);
1045 var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x);
1046 var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x);
1047 var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y);
1048 var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y);
1050 return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };