/*! * svg.js - A lightweight library for manipulating and animating SVG. * @version 2.2.5 * http://www.svgjs.com * * @copyright Wout Fierens * @license MIT * * BUILT: Fri Mar 25 2016 00:01:14 GMT+0100 (Mitteleuropäische Zeit) */; (function(root, factory) { if (typeof define === 'function' && define.amd) { define(function(){ return factory(root, root.document) }) } else if (typeof exports === 'object') { module.exports = root.document ? factory(root, root.document) : function(w){ return factory(w, w.document) } } else { root.SVG = factory(root, root.document) } }(typeof window !== "undefined" ? window : this, function(window, document) { // The main wrapping element var SVG = this.SVG = function(element) { if (SVG.supported) { element = new SVG.Doc(element) if (!SVG.parser) SVG.prepare(element) return element } } // Default namespaces SVG.ns = 'http://www.w3.org/2000/svg' SVG.xmlns = 'http://www.w3.org/2000/xmlns/' SVG.xlink = 'http://www.w3.org/1999/xlink' SVG.svgjs = 'http://svgjs.com/svgjs' // Svg support test SVG.supported = (function() { return !! document.createElementNS && !! document.createElementNS(SVG.ns,'svg').createSVGRect })() // Don't bother to continue if SVG is not supported if (!SVG.supported) return false // Element id sequence SVG.did = 1000 // Get next named element id SVG.eid = function(name) { return 'Svgjs' + capitalize(name) + (SVG.did++) } // Method for element creation SVG.create = function(name) { // create element var element = document.createElementNS(this.ns, name) // apply unique id element.setAttribute('id', this.eid(name)) return element } // Method for extending objects SVG.extend = function() { var modules, methods, key, i // Get list of modules modules = [].slice.call(arguments) // Get object with extensions methods = modules.pop() for (i = modules.length - 1; i >= 0; i--) if (modules[i]) for (key in methods) modules[i].prototype[key] = methods[key] // Make sure SVG.Set inherits any newly added methods if (SVG.Set && SVG.Set.inherit) SVG.Set.inherit() } // Invent new element SVG.invent = function(config) { // Create element initializer var initializer = typeof config.create == 'function' ? config.create : function() { this.constructor.call(this, SVG.create(config.create)) } // Inherit prototype if (config.inherit) initializer.prototype = new config.inherit // Extend with methods if (config.extend) SVG.extend(initializer, config.extend) // Attach construct method to parent if (config.construct) SVG.extend(config.parent || SVG.Container, config.construct) return initializer } // Adopt existing svg elements SVG.adopt = function(node) { // check for presence of node if (!node) return null // make sure a node isn't already adopted if (node.instance) return node.instance // initialize variables var element // adopt with element-specific settings if (node.nodeName == 'svg') element = node.parentNode instanceof SVGElement ? new SVG.Nested : new SVG.Doc else if (node.nodeName == 'linearGradient') element = new SVG.Gradient('linear') else if (node.nodeName == 'radialGradient') element = new SVG.Gradient('radial') else if (SVG[capitalize(node.nodeName)]) element = new SVG[capitalize(node.nodeName)] else element = new SVG.Element(node) // ensure references element.type = node.nodeName element.node = node node.instance = element // SVG.Class specific preparations if (element instanceof SVG.Doc) element.namespace().defs() // pull svgjs data from the dom (getAttributeNS doesn't work in html5) element.setData(JSON.parse(node.getAttribute('svgjs:data')) || {}) return element } // Initialize parsing element SVG.prepare = function(element) { // Select document body and create invisible svg element var body = document.getElementsByTagName('body')[0] , draw = (body ? new SVG.Doc(body) : element.nested()).size(2, 0) , path = SVG.create('path') // Insert parsers draw.node.appendChild(path) // Create parser object SVG.parser = { body: body || element.parent() , draw: draw.style('opacity:0;position:fixed;left:100%;top:100%;overflow:hidden') , poly: draw.polyline().node , path: path } } // Storage for regular expressions SVG.regex = { // Parse unit value numberAndUnit: /^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i // Parse hex value , hex: /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i // Parse rgb value , rgb: /rgb\((\d+),(\d+),(\d+)\)/ // Parse reference id , reference: /#([a-z0-9\-_]+)/i // Parse matrix wrapper , matrix: /matrix\(|\)/g // Elements of a matrix , matrixElements: /,*\s+|,/ // Whitespace , whitespace: /\s/g // Test hex value , isHex: /^#[a-f0-9]{3,6}$/i // Test rgb value , isRgb: /^rgb\(/ // Test css declaration , isCss: /[^:]+:[^;]+;?/ // Test for blank string , isBlank: /^(\s+)?$/ // Test for numeric string , isNumber: /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i // Test for percent value , isPercent: /^-?[\d\.]+%$/ // Test for image url , isImage: /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i // The following regex are used to parse the d attribute of a path // Replaces all negative exponents , negExp: /e\-/gi // Replaces all comma , comma: /,/g // Replaces all hyphens , hyphen: /\-/g // Replaces and tests for all path letters , pathLetters: /[MLHVCSQTAZ]/gi // yes we need this one, too , isPathLetter: /[MLHVCSQTAZ]/i // split at whitespaces , whitespaces: /\s+/ // matches X , X: /X/g } SVG.utils = { // Map function map: function(array, block) { var i , il = array.length , result = [] for (i = 0; i < il; i++) result.push(block(array[i])) return result } // Degrees to radians , radians: function(d) { return d % 360 * Math.PI / 180 } // Radians to degrees , degrees: function(r) { return r * 180 / Math.PI % 360 } , filterSVGElements: function(p) { return [].filter.call(p, function(el){ return el instanceof SVGElement }) } } SVG.defaults = { // Default attribute values attrs: { // fill and stroke 'fill-opacity': 1 , 'stroke-opacity': 1 , 'stroke-width': 0 , 'stroke-linejoin': 'miter' , 'stroke-linecap': 'butt' , fill: '#000000' , stroke: '#000000' , opacity: 1 // position , x: 0 , y: 0 , cx: 0 , cy: 0 // size , width: 0 , height: 0 // radius , r: 0 , rx: 0 , ry: 0 // gradient , offset: 0 , 'stop-opacity': 1 , 'stop-color': '#000000' // text , 'font-size': 16 , 'font-family': 'Helvetica, Arial, sans-serif' , 'text-anchor': 'start' } } // Module for color convertions SVG.Color = function(color) { var match // initialize defaults this.r = 0 this.g = 0 this.b = 0 // parse color if (typeof color === 'string') { if (SVG.regex.isRgb.test(color)) { // get rgb values match = SVG.regex.rgb.exec(color.replace(/\s/g,'')) // parse numeric values this.r = parseInt(match[1]) this.g = parseInt(match[2]) this.b = parseInt(match[3]) } else if (SVG.regex.isHex.test(color)) { // get hex values match = SVG.regex.hex.exec(fullHex(color)) // parse numeric values this.r = parseInt(match[1], 16) this.g = parseInt(match[2], 16) this.b = parseInt(match[3], 16) } } else if (typeof color === 'object') { this.r = color.r this.g = color.g this.b = color.b } } SVG.extend(SVG.Color, { // Default to hex conversion toString: function() { return this.toHex() } // Build hex value , toHex: function() { return '#' + compToHex(this.r) + compToHex(this.g) + compToHex(this.b) } // Build rgb value , toRgb: function() { return 'rgb(' + [this.r, this.g, this.b].join() + ')' } // Calculate true brightness , brightness: function() { return (this.r / 255 * 0.30) + (this.g / 255 * 0.59) + (this.b / 255 * 0.11) } // Make color morphable , morph: function(color) { this.destination = new SVG.Color(color) return this } // Get morphed color at given position , at: function(pos) { // make sure a destination is defined if (!this.destination) return this // normalise pos pos = pos < 0 ? 0 : pos > 1 ? 1 : pos // generate morphed color return new SVG.Color({ r: ~~(this.r + (this.destination.r - this.r) * pos) , g: ~~(this.g + (this.destination.g - this.g) * pos) , b: ~~(this.b + (this.destination.b - this.b) * pos) }) } }) // Testers // Test if given value is a color string SVG.Color.test = function(color) { color += '' return SVG.regex.isHex.test(color) || SVG.regex.isRgb.test(color) } // Test if given value is a rgb object SVG.Color.isRgb = function(color) { return color && typeof color.r == 'number' && typeof color.g == 'number' && typeof color.b == 'number' } // Test if given value is a color SVG.Color.isColor = function(color) { return SVG.Color.isRgb(color) || SVG.Color.test(color) } // Module for array conversion SVG.Array = function(array, fallback) { array = (array || []).valueOf() // if array is empty and fallback is provided, use fallback if (array.length == 0 && fallback) array = fallback.valueOf() // parse array this.value = this.parse(array) } SVG.extend(SVG.Array, { // Make array morphable morph: function(array) { this.destination = this.parse(array) // normalize length of arrays if (this.value.length != this.destination.length) { var lastValue = this.value[this.value.length - 1] , lastDestination = this.destination[this.destination.length - 1] while(this.value.length > this.destination.length) this.destination.push(lastDestination) while(this.value.length < this.destination.length) this.value.push(lastValue) } return this } // Clean up any duplicate points , settle: function() { // find all unique values for (var i = 0, il = this.value.length, seen = []; i < il; i++) if (seen.indexOf(this.value[i]) == -1) seen.push(this.value[i]) // set new value return this.value = seen } // Get morphed array at given position , at: function(pos) { // make sure a destination is defined if (!this.destination) return this // generate morphed array for (var i = 0, il = this.value.length, array = []; i < il; i++) array.push(this.value[i] + (this.destination[i] - this.value[i]) * pos) return new SVG.Array(array) } // Convert array to string , toString: function() { return this.value.join(' ') } // Real value , valueOf: function() { return this.value } // Parse whitespace separated string , parse: function(array) { array = array.valueOf() // if already is an array, no need to parse it if (Array.isArray(array)) return array return this.split(array) } // Strip unnecessary whitespace , split: function(string) { return string.trim().split(/\s+/) } // Reverse array , reverse: function() { this.value.reverse() return this } }) // Poly points array SVG.PointArray = function(array, fallback) { this.constructor.call(this, array, fallback || [[0,0]]) } // Inherit from SVG.Array SVG.PointArray.prototype = new SVG.Array SVG.extend(SVG.PointArray, { // Convert array to string toString: function() { // convert to a poly point string for (var i = 0, il = this.value.length, array = []; i < il; i++) array.push(this.value[i].join(',')) return array.join(' ') } // Convert array to line object , toLine: function() { return { x1: this.value[0][0] , y1: this.value[0][1] , x2: this.value[1][0] , y2: this.value[1][1] } } // Get morphed array at given position , at: function(pos) { // make sure a destination is defined if (!this.destination) return this // generate morphed point string for (var i = 0, il = this.value.length, array = []; i < il; i++) array.push([ this.value[i][0] + (this.destination[i][0] - this.value[i][0]) * pos , this.value[i][1] + (this.destination[i][1] - this.value[i][1]) * pos ]) return new SVG.PointArray(array) } // Parse point string , parse: function(array) { array = array.valueOf() // if already is an array, no need to parse it if (Array.isArray(array)) return array // split points array = this.split(array) // parse points for (var i = 0, il = array.length, p, points = []; i < il; i++) { p = array[i].split(',') points.push([parseFloat(p[0]), parseFloat(p[1])]) } return points } // Move point string , move: function(x, y) { var box = this.bbox() // get relative offset x -= box.x y -= box.y // move every point if (!isNaN(x) && !isNaN(y)) for (var i = this.value.length - 1; i >= 0; i--) this.value[i] = [this.value[i][0] + x, this.value[i][1] + y] return this } // Resize poly string , size: function(width, height) { var i, box = this.bbox() // recalculate position of all points according to new size for (i = this.value.length - 1; i >= 0; i--) { this.value[i][0] = ((this.value[i][0] - box.x) * width) / box.width + box.x this.value[i][1] = ((this.value[i][1] - box.y) * height) / box.height + box.y } return this } // Get bounding box of points , bbox: function() { SVG.parser.poly.setAttribute('points', this.toString()) return SVG.parser.poly.getBBox() } }) // Path points array SVG.PathArray = function(array, fallback) { this.constructor.call(this, array, fallback || [['M', 0, 0]]) } // Inherit from SVG.Array SVG.PathArray.prototype = new SVG.Array SVG.extend(SVG.PathArray, { // Convert array to string toString: function() { return arrayToString(this.value) } // Move path string , move: function(x, y) { // get bounding box of current situation var box = this.bbox() // get relative offset x -= box.x y -= box.y if (!isNaN(x) && !isNaN(y)) { // move every point for (var l, i = this.value.length - 1; i >= 0; i--) { l = this.value[i][0] if (l == 'M' || l == 'L' || l == 'T') { this.value[i][1] += x this.value[i][2] += y } else if (l == 'H') { this.value[i][1] += x } else if (l == 'V') { this.value[i][1] += y } else if (l == 'C' || l == 'S' || l == 'Q') { this.value[i][1] += x this.value[i][2] += y this.value[i][3] += x this.value[i][4] += y if (l == 'C') { this.value[i][5] += x this.value[i][6] += y } } else if (l == 'A') { this.value[i][6] += x this.value[i][7] += y } } } return this } // Resize path string , size: function(width, height) { // get bounding box of current situation var i, l, box = this.bbox() // recalculate position of all points according to new size for (i = this.value.length - 1; i >= 0; i--) { l = this.value[i][0] if (l == 'M' || l == 'L' || l == 'T') { this.value[i][1] = ((this.value[i][1] - box.x) * width) / box.width + box.x this.value[i][2] = ((this.value[i][2] - box.y) * height) / box.height + box.y } else if (l == 'H') { this.value[i][1] = ((this.value[i][1] - box.x) * width) / box.width + box.x } else if (l == 'V') { this.value[i][1] = ((this.value[i][1] - box.y) * height) / box.height + box.y } else if (l == 'C' || l == 'S' || l == 'Q') { this.value[i][1] = ((this.value[i][1] - box.x) * width) / box.width + box.x this.value[i][2] = ((this.value[i][2] - box.y) * height) / box.height + box.y this.value[i][3] = ((this.value[i][3] - box.x) * width) / box.width + box.x this.value[i][4] = ((this.value[i][4] - box.y) * height) / box.height + box.y if (l == 'C') { this.value[i][5] = ((this.value[i][5] - box.x) * width) / box.width + box.x this.value[i][6] = ((this.value[i][6] - box.y) * height) / box.height + box.y } } else if (l == 'A') { // resize radii this.value[i][1] = (this.value[i][1] * width) / box.width this.value[i][2] = (this.value[i][2] * height) / box.height // move position values this.value[i][6] = ((this.value[i][6] - box.x) * width) / box.width + box.x this.value[i][7] = ((this.value[i][7] - box.y) * height) / box.height + box.y } } return this } // Absolutize and parse path to array , parse: function(array) { // if it's already a patharray, no need to parse it if (array instanceof SVG.PathArray) return array.valueOf() // prepare for parsing var i, x0, y0, s, seg, arr , x = 0 , y = 0 , paramCnt = { 'M':2, 'L':2, 'H':1, 'V':1, 'C':6, 'S':4, 'Q':4, 'T':2, 'A':7 } if(typeof array == 'string'){ array = array .replace(SVG.regex.negExp, 'X') // replace all negative exponents with certain char .replace(SVG.regex.pathLetters, ' $& ') // put some room between letters and numbers .replace(SVG.regex.hyphen, ' -') // add space before hyphen .replace(SVG.regex.comma, ' ') // unify all spaces .replace(SVG.regex.X, 'e-') // add back the expoent .trim() // trim .split(SVG.regex.whitespaces) // split into array // at this place there could be parts like ['3.124.854.32'] because we could not determine the point as seperator till now // we fix this elements in the next loop for(i = array.length; --i;){ if(array[i].indexOf('.') != array[i].lastIndexOf('.')){ var split = array[i].split('.') // split at the point var first = [split.shift(), split.shift()].join('.') // join the first number together array.splice.apply(array, [i, 1].concat(first, split.map(function(el){ return '.'+el }))) // add first and all other entries back to array } } }else{ array = array.reduce(function(prev, curr){ return [].concat.apply(prev, curr) }, []) } // array now is an array containing all parts of a path e.g. ['M', '0', '0', 'L', '30', '30' ...] var arr = [] do{ // Test if we have a path letter if(SVG.regex.isPathLetter.test(array[0])){ s = array[0] array.shift() // If last letter was a move command and we got no new, it defaults to [L]ine }else if(s == 'M'){ s = 'L' }else if(s == 'm'){ s = 'l' } // add path letter as first element seg = [s.toUpperCase()] // push all necessary parameters to segment for(i = 0; i < paramCnt[seg[0]]; ++i){ seg.push(parseFloat(array.shift())) } // upper case if(s == seg[0]){ if(s == 'M' || s == 'L' || s == 'C' || s == 'Q'){ x = seg[paramCnt[seg[0]]-1] y = seg[paramCnt[seg[0]]] }else if(s == 'V'){ y = seg[1] }else if(s == 'H'){ x = seg[1] }else if(s == 'A'){ x = seg[6] y = seg[7] } // lower case }else{ // convert relative to absolute values if(s == 'm' || s == 'l' || s == 'c' || s == 's' || s == 'q' || s == 't'){ seg[1] += x seg[2] += y if(seg[3] != null){ seg[3] += x seg[4] += y } if(seg[5] != null){ seg[5] += x seg[6] += y } // move pointer x = seg[paramCnt[seg[0]]-1] y = seg[paramCnt[seg[0]]] }else if(s == 'v'){ seg[1] += y y = seg[1] }else if(s == 'h'){ seg[1] += x x = seg[1] }else if(s == 'a'){ seg[6] += x seg[7] += y x = seg[6] y = seg[7] } } if(seg[0] == 'M'){ x0 = x y0 = y } if(seg[0] == 'Z'){ x = x0 y = y0 } arr.push(seg) }while(array.length) return arr } // Get bounding box of path , bbox: function() { SVG.parser.path.setAttribute('d', this.toString()) return SVG.parser.path.getBBox() } }) // Module for unit convertions SVG.Number = SVG.invent({ // Initialize create: function(value, unit) { // initialize defaults this.value = 0 this.unit = unit || '' // parse value if (typeof value === 'number') { // ensure a valid numeric value this.value = isNaN(value) ? 0 : !isFinite(value) ? (value < 0 ? -3.4e+38 : +3.4e+38) : value } else if (typeof value === 'string') { unit = value.match(SVG.regex.numberAndUnit) if (unit) { // make value numeric this.value = parseFloat(unit[1]) // normalize if (unit[5] == '%') this.value /= 100 else if (unit[5] == 's') this.value *= 1000 // store unit this.unit = unit[5] } } else { if (value instanceof SVG.Number) { this.value = value.valueOf() this.unit = value.unit } } } // Add methods , extend: { // Stringalize toString: function() { return ( this.unit == '%' ? ~~(this.value * 1e8) / 1e6: this.unit == 's' ? this.value / 1e3 : this.value ) + this.unit } , toJSON: function() { return this.toString() } , // Convert to primitive valueOf: function() { return this.value } // Add number , plus: function(number) { return new SVG.Number(this + new SVG.Number(number), this.unit) } // Subtract number , minus: function(number) { return this.plus(-new SVG.Number(number)) } // Multiply number , times: function(number) { return new SVG.Number(this * new SVG.Number(number), this.unit) } // Divide number , divide: function(number) { return new SVG.Number(this / new SVG.Number(number), this.unit) } // Convert to different unit , to: function(unit) { var number = new SVG.Number(this) if (typeof unit === 'string') number.unit = unit return number } // Make number morphable , morph: function(number) { this.destination = new SVG.Number(number) return this } // Get morphed number at given position , at: function(pos) { // Make sure a destination is defined if (!this.destination) return this // Generate new morphed number return new SVG.Number(this.destination) .minus(this) .times(pos) .plus(this) } } }) SVG.ViewBox = SVG.invent({ create: function(source) { var i, base = [1, 0, 0, 1] var x, y, width, height, box, view, we, he , wm = 1 // width multiplier , hm = 1 // height multiplier , reg = /-?[\d\.]+/g if(source instanceof SVG.Element){ we = source he = source view = (source.attr('viewBox') || '').match(reg) box = source.bbox // get dimensions of current node width = new SVG.Number(source.width()) height = new SVG.Number(source.height()) // find nearest non-percentual dimensions while (width.unit == '%') { wm *= width.value width = new SVG.Number(we instanceof SVG.Doc ? we.parent().offsetWidth : we.parent().width()) we = we.parent() } while (height.unit == '%') { hm *= height.value height = new SVG.Number(he instanceof SVG.Doc ? he.parent().offsetHeight : he.parent().height()) he = he.parent() } // ensure defaults this.x = 0 this.y = 0 this.width = width * wm this.height = height * hm this.zoom = 1 if (view) { // get width and height from viewbox x = parseFloat(view[0]) y = parseFloat(view[1]) width = parseFloat(view[2]) height = parseFloat(view[3]) // calculate zoom accoring to viewbox this.zoom = ((this.width / this.height) > (width / height)) ? this.height / height : this.width / width // calculate real pixel dimensions on parent SVG.Doc element this.x = x this.y = y this.width = width this.height = height } }else{ // ensure source as object source = typeof source === 'string' ? source.match(reg).map(function(el){ return parseFloat(el) }) : Array.isArray(source) ? source : typeof source == 'object' ? [source.x, source.y, source.width, source.height] : arguments.length == 4 ? [].slice.call(arguments) : base this.x = source[0] this.y = source[1] this.width = source[2] this.height = source[3] } } , extend: { toString: function() { return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height } , morph: function(v){ var v = arguments.length == 1 ? [v.x, v.y, v.width, v.height] : [].slice.call(arguments) this.destination = new SVG.ViewBox(v) return this } , at: function(pos) { if(!this.destination) return this return new SVG.ViewBox([ this.x + (this.destination.x - this.x) * pos , this.y + (this.destination.y - this.y) * pos , this.width + (this.destination.width - this.width) * pos , this.height + (this.destination.height - this.height) * pos ]) } } }) SVG.Element = SVG.invent({ // Initialize node create: function(node) { // make stroke value accessible dynamically this._stroke = SVG.defaults.attrs.stroke // initialize data object this.dom = {} // create circular reference if (this.node = node) { this.type = node.nodeName this.node.instance = this // store current attribute value this._stroke = node.getAttribute('stroke') || this._stroke } } // Add class methods , extend: { // Move over x-axis x: function(x) { return this.attr('x', x) } // Move over y-axis , y: function(y) { return this.attr('y', y) } // Move by center over x-axis , cx: function(x) { return x == null ? this.x() + this.width() / 2 : this.x(x - this.width() / 2) } // Move by center over y-axis , cy: function(y) { return y == null ? this.y() + this.height() / 2 : this.y(y - this.height() / 2) } // Move element to given x and y values , move: function(x, y) { return this.x(x).y(y) } // Move element by its center , center: function(x, y) { return this.cx(x).cy(y) } // Set width of element , width: function(width) { return this.attr('width', width) } // Set height of element , height: function(height) { return this.attr('height', height) } // Set element size to given width and height , size: function(width, height) { var p = proportionalSize(this.bbox(), width, height) return this .width(new SVG.Number(p.width)) .height(new SVG.Number(p.height)) } // Clone element , clone: function() { // clone element and assign new id var clone = assignNewId(this.node.cloneNode(true)) // insert the clone after myself this.after(clone) return clone } // Remove element , remove: function() { if (this.parent()) this.parent().removeElement(this) return this } // Replace element , replace: function(element) { this.after(element).remove() return element } // Add element to given container and return self , addTo: function(parent) { return parent.put(this) } // Add element to given container and return container , putIn: function(parent) { return parent.add(this) } // Get / set id , id: function(id) { return this.attr('id', id) } // Checks whether the given point inside the bounding box of the element , inside: function(x, y) { var box = this.bbox() return x > box.x && y > box.y && x < box.x + box.width && y < box.y + box.height } // Show element , show: function() { return this.style('display', '') } // Hide element , hide: function() { return this.style('display', 'none') } // Is element visible? , visible: function() { return this.style('display') != 'none' } // Return id on string conversion , toString: function() { return this.attr('id') } // Return array of classes on the node , classes: function() { var attr = this.attr('class') return attr == null ? [] : attr.trim().split(/\s+/) } // Return true if class exists on the node, false otherwise , hasClass: function(name) { return this.classes().indexOf(name) != -1 } // Add class to the node , addClass: function(name) { if (!this.hasClass(name)) { var array = this.classes() array.push(name) this.attr('class', array.join(' ')) } return this } // Remove class from the node , removeClass: function(name) { if (this.hasClass(name)) { this.attr('class', this.classes().filter(function(c) { return c != name }).join(' ')) } return this } // Toggle the presence of a class on the node , toggleClass: function(name) { return this.hasClass(name) ? this.removeClass(name) : this.addClass(name) } // Get referenced element form attribute value , reference: function(attr) { return SVG.get(this.attr(attr)) } // Returns the parent element instance , parent: function(type) { var parent = this // check for parent if(!parent.node.parentNode) return null // get parent element parent = SVG.adopt(parent.node.parentNode) if(!type) return parent // loop trough ancestors if type is given while(parent && parent.node instanceof SVGElement){ if(typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent parent = SVG.adopt(parent.node.parentNode) } } // Get parent document , doc: function() { return this instanceof SVG.Doc ? this : this.parent(SVG.Doc) } // return array of all ancestors of given type up to the root svg , parents: function(type) { var parents = [], parent = this do{ parent = parent.parent(type) if(!parent || !parent.node) break parents.push(parent) } while(parent.parent) return parents } // matches the element vs a css selector , matches: function(selector){ return matches(this.node, selector) } // Returns the svg node to call native svg methods on it , native: function() { return this.node } // Import raw svg , svg: function(svg) { // create temporary holder var well = document.createElement('svg') // act as a setter if svg is given if (svg && this instanceof SVG.Parent) { // dump raw svg well.innerHTML = '' + svg.replace(/\n/, '').replace(/<(\w+)([^<]+?)\/>/g, '<$1$2>') + '' // transplant nodes for (var i = 0, il = well.firstChild.childNodes.length; i < il; i++) this.node.appendChild(well.firstChild.firstChild) // otherwise act as a getter } else { // create a wrapping svg element in case of partial content well.appendChild(svg = document.createElement('svg')) // write svgjs data to the dom this.writeDataToDom() // insert a copy of this node svg.appendChild(this.node.cloneNode(true)) // return target element return well.innerHTML.replace(/^/, '').replace(/<\/svg>$/, '') } return this } // write svgjs data to the dom , writeDataToDom: function() { // dump variables recursively if(this.each || this.lines){ var fn = this.each ? this : this.lines(); fn.each(function(){ this.writeDataToDom() }) } // remove previously set data this.node.removeAttribute('svgjs:data') if(Object.keys(this.dom).length) this.node.setAttribute('svgjs:data', JSON.stringify(this.dom)) // see #428 return this } // set given data to the elements data property , setData: function(o){ this.dom = o return this } , is: function(obj){ return is(this, obj) } } }) SVG.easing = { '-': function(pos){return pos} , '<>':function(pos){return -Math.cos(pos * Math.PI) / 2 + 0.5} , '>': function(pos){return Math.sin(pos * Math.PI / 2)} , '<': function(pos){return -Math.cos(pos * Math.PI / 2) + 1} } var someVar = 0 SVG.Situation = SVG.invent({ create: function(o){ this.init = false this.reversed = false this.reversing = false this.duration = o.duration this.delay = o.delay this.start = +new Date() + this.delay this.end = this.start + this.duration this.easing = o.easing this.animations = { // functionToCall: [list of morphable objects] // e.g. move: [SVG.Number, SVG.Number] } this.attrs = { // holds all attributes which are not represented from a function svg.js provides // e.g. someAttr: SVG.Number } this.styles = { // holds all styles which should be animated // e.g. fill-color: SVG.Color } this.transforms = [ // holds all transformations of the form: // [A, B, C] or, [A, [25, 0, 0]] where ABC are matrixes and the array represents a rotation ] this.once = { // functions to fire at a specific position // e.g. "0.5": function foo(){} } } }) SVG.Delay = function(delay){ this.delay = delay } SVG.FX = SVG.invent({ create: function(element) { this._target = element this.situations = [] this.active = false this.current = null this.paused = false this.lastPos = 0 this.pos = 0 } , extend: { /** * sets or returns the target of this animation * @param o object || number In case of Object it holds all parameters. In case of number its the duration of the animation * @param easing function || string Function which should be used for easing or easing keyword * @param delay Number indicating the delay before the animation starts * @return target || this */ animate: function(o, easing, delay){ if(typeof o == 'object'){ easing = o.ease delay = o.delay o = o.duration } var situation = new SVG.Situation({ duration: o || 1000, delay: delay || 0, easing: SVG.easing[easing || '-'] || easing }) this.queue(situation) return this } /** * sets a delay before the next element of the queue is called * @param delay Duration of delay in milliseconds * @return this.target() */ // FIXME: the function needs to get a delay property to make sure, that the totalProgress can be calculated , delay: function(delay){ var delay = new SVG.Delay(delay) return this.queue(delay) } /** * sets or returns the target of this animation * @param null || target SVG.Elemenet which should be set as new target * @return target || this */ , target: function(target){ if(target && target instanceof SVG.Element){ this._target = target return this } return this._target } // returns the position at a given time , timeToPos: function(timestamp){ return (timestamp - this.current.start) / (this.current.duration) } // returns the timestamp from a given positon , posToTime: function(pos){ return this.current.duration * pos + this.current.start } // starts the animationloop // TODO: It may be enough to call just this.step() , startAnimFrame: function(){ this.stopAnimFrame() this.animationFrame = requestAnimationFrame(function(){ this.step() }.bind(this)) } // cancels the animationframe // TODO: remove this in favour of the oneliner , stopAnimFrame: function(){ cancelAnimationFrame(this.animationFrame) } // kicks off the animation - only does something when the queue is curretly not active and at least one situation is set , start: function(){ // dont start if already started if(!this.active && this.current){ this.current.start = +new Date + this.current.delay this.current.end = this.current.start + this.current.duration this.initAnimations() this.active = true this.startAnimFrame() } return this } /** * adds a function / Situation to the animation queue * @param fn function / situation to add * @return this */ , queue: function(fn){ if(typeof fn == 'function' || fn instanceof SVG.Situation || fn instanceof SVG.Delay) this.situations.push(fn) if(!this.current) this.current = this.situations.shift() return this } /** * pulls next element from the queue and execute it * @return this */ , dequeue: function(){ // stop current animation this.current && this.current.stop && this.current.stop() // get next animation from queue this.current = this.situations.shift() if(this.current){ var fn = function(){ if(this.current instanceof SVG.Situation) this.initAnimations().seek(0) else if(this.current instanceof SVG.Delay) this.dequeue() else this.current.call(this) }.bind(this) // start next animation if(this.current.delay){ setTimeout(function(){fn()}, this.current.delay) }else{ fn() } } return this } // updates all animations to the current state of the element // this is important when one property could be changed from another property , initAnimations: function() { var i var s = this.current if(s.init) return this for(i in s.animations){ if(i == 'viewbox'){ s.animations[i] = this.target().viewbox().morph(s.animations[i]) }else{ // TODO: this is not a clean clone of the array. We may have some unchecked references s.animations[i].value = (i == 'plot' ? this.target().array().value : this.target()[i]()) // sometimes we get back an object and not the real value, fix this if(s.animations[i].value.value){ s.animations[i].value = s.animations[i].value.value } if(s.animations[i].relative) s.animations[i].destination.value = s.animations[i].destination.value + s.animations[i].value } } for(i in s.attrs){ if(s.attrs[i] instanceof SVG.Color){ var color = new SVG.Color(this.target().attr(i)) s.attrs[i].r = color.r s.attrs[i].g = color.g s.attrs[i].b = color.b }else{ s.attrs[i].value = this.target().attr(i)// + s.attrs[i].value } } for(i in s.styles){ s.styles[i].value = this.target().style(i) } s.transformations = this.target().matrixify() s.init = true return this } , clearQueue: function(){ this.situations = [] return this } , clearCurrent: function(){ this.current = null return this } /** stops the animation immediately * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately. * @param clearQueue A Boolean indicating whether to remove queued animation as well. * @return this */ , stop: function(jumpToEnd, clearQueue){ if(!this.active) this.start() if(clearQueue){ this.clearQueue() } this.active = false if(jumpToEnd){ this.seek(1) } this.stopAnimFrame() clearTimeout(this.timeout) return this.clearCurrent() } /** resets the element to the state where the current element has started * @return this */ , reset: function(){ if(this.current){ var temp = this.current this.stop() this.current = temp this.seek(0) } return this } // Stop the currently-running animation, remove all queued animations, and complete all animations for the element. , finish: function(){ this.stop(true, false) while(this.dequeue().current && this.stop(true, false)); return this.clearQueue().clearCurrent() } // set the internal animation pointer to the specified position and updates the visualisation , seek: function(pos){ this.pos = pos this.current.start = +new Date - pos * this.current.duration this.current.end = this.current.start + this.current.duration return this.step(true) } // speeds up the animation by the given factor // this changes the duration of the animation , speed: function(speed){ this.current.duration = this.current.duration * this.pos + (1-this.pos) * this.current.duration / speed this.current.end = this.current.start + this.current.duration return this.seek(this.pos) } // Make loopable , loop: function(times, reverse) { // store current loop and total loops this.current.loop = times || true if(reverse) this.last().reversing = true return this } // pauses the animation , pause: function(){ this.paused = true this.stopAnimFrame() clearTimeout(this.timeout) return this } // unpause the animation , play: function(){ if(!this.paused) return this this.paused = false return this.seek(this.pos) } /** toggle or set the direction of the animation * true sets direction to backwards while false sets it to forwards * @param reversed Boolean indicating whether to reverse the animation or not (default: toggle the reverse status) * @return this */ , reverse: function(reversed){ var c = this.last() if(typeof reversed == 'undefined') c.reversed = !c.reversed else c.reversed = reversed return this } /** * returns a float from 0-1 indicating the progress of the current animation * @param eased Boolean indicating whether the returned position should be eased or not * @return number */ , progress: function(easeIt){ return easeIt ? this.current.easing(this.pos) : this.pos } /** * adds a callback function which is called when the current animation is finished * @param fn Function which should be executed as callback * @return number */ , after: function(fn){ var c = this.last() , wrapper = function wrapper(e){ if(e.detail.situation == c){ fn.call(this, c) this.off('finished.fx', wrapper) // prevent memory leak } } this.target().on('finished.fx', wrapper) return this } // adds a callback which is called whenever one animation step is performed , during: function(fn){ var c = this.last() , wrapper = function(e){ if(e.detail.situation == c){ fn.call(this, e.detail.pos, e.detail.eased, c) } } // see above this.target().off('during.fx', wrapper).on('during.fx', wrapper) return this.after(function(){ this.off('during.fx', wrapper) }) } // calls after ALL animations in the queue are finished , afterAll: function(fn){ var wrapper = function wrapper(e){ fn.call(this) this.off('allfinished.fx', wrapper) } // see above this.target().off('allfinished.fx', wrapper).on('allfinished.fx', wrapper) return this } // calls on every animation step for all animations , duringAll: function(fn){ var wrapper = function(e){ fn.call(this, e.detail.pos, e.detail.eased, e.detail.fx, e.detail.situation) } this.target().off('during.fx', wrapper).on('during.fx', wrapper) return this.afterAll(function(){ this.off('during.fx', wrapper) }) } /** * returns a float from 0-1 indicating the progress of the whole animation queue * we recalculate the end time because it may be changed from methods like seek() * @return number */ // FIXME: current start always changes so the progress get a reset whenever one situation finishes. We need a global start which is only modified on pause and stop , totalProgress: function(){ var start = this.current.start , end = this.current for(var i = 0, len = this.situations.length; i < len; ++i){ end += (situations[i].duration || 0) + (situations[i].delay || 0) } return (this.pos * this.current.duration + this.start - start) / (end - start) } , last: function(){ return this.situations.length ? this.situations[this.situations.length-1] : this.current } // adds one property to the animations , add: function(method, args, type){ this.last()[type || 'animations'][method] = args setTimeout(function(){this.start()}.bind(this), 0) return this } /** perform one step of the animation * @param ignoreTime Boolean indicating whether to ignore time and use position directly or recalculate position based on time * @return this */ , step: function(ignoreTime){ // convert current time to position if(!ignoreTime) this.pos = this.timeToPos(+new Date) if(this.pos >= 1 && (this.current.loop === true || (typeof this.current.loop == 'number' && --this.current.loop))){ if(this.current.reversing){ this.current.reversed = !this.current.reversed console.log('asd') } return this.seek(this.pos-1) } if(this.current.reversed) this.pos = 1 - this.pos // correct position if(this.pos > 1)this.pos = 1 if(this.pos < 0)this.pos = 0 // apply easing var eased = this.current.easing(this.pos) // call once-callbacks for(var i in this.current.once){ if(i > this.lastPos && i <= eased){ this.current.once[i].call(this.target(), this.pos, eased) delete this.current.once[i] } } // fire during callback with position, eased position and current situation as parameter this.target().fire('during', {pos: this.pos, eased: eased, fx: this, situation: this.current}) // apply the actual animation to every property this.eachAt() // do final code when situation is finished if((this.pos == 1 && !this.current.reversed) || (this.current.reversed && this.pos == 0)){ // stop animation callback cancelAnimationFrame(this.animationFrame) // fire finished callback with current situation as parameter this.target().fire('finished', {fx:this, situation: this.current}) if(!this.situations.length && !this.current && this.active){ this.target().fire('allfinished') this.target().off('.fx') this.active = false } // start next animation if(this.active) this.dequeue() else this.clearCurrent() }else if(!this.paused && this.active){ // we continue animating when we are not at the end this.startAnimFrame() } // save last eased position for once callback triggering this.lastPos = eased return this } // calculates the step for every property and calls block with it // todo: include block directly cause it is used only for this purpose , eachAt: function(){ var i, at, self = this, target = this.target(), c = this.current // apply animations which can be called trough a method for(i in c.animations){ at = [].concat(c.animations[i]).map(function(el){ return el.at ? el.at(c.easing(self.pos), self.pos) : el }) target[i].apply(target, at) } // apply animation which has to be applied with attr() for(i in c.attrs){ at = [i].concat(c.attrs[i]).map(function(el){ return el.at ? el.at(c.easing(self.pos), self.pos) : el }) target.attr.apply(target, at) } // apply animation which has to be applied with style() for(i in c.styles){ at = [i].concat(c.styles[i]).map(function(el){ return el.at ? el.at(c.easing(self.pos), self.pos) : el }) target.style.apply(target, at) } // animate transformations which has to be chained if(c.transforms.length){ // get inital transformations at = c.transformations for(i in c.transforms){ // get next transformation in chain var a = c.transforms[i] // multiply matrix directly if(a instanceof SVG.Matrix){ if(a.relative){ at = at.multiply(a.at(this.pos)) }else{ at = at.morph(a).at(c.easing(this.pos)) } continue } // when transformation is absolute we have to reset the needed transformation first if(!a.relative) a.undo(at.extract()) // and reapply it after at = at.multiply(a.at(c.easing(this.pos))) continue; } // set new matrix on element target.matrix(at) } return this } // adds an once-callback which is called at a specific position and never again , once: function(pos, fn, isEased){ if(!isEased)pos = this.current.easing(pos) this.current.once[pos] = fn return this } } , parent: SVG.Element // Add method to parent elements , construct: { // Get fx module or create a new one, then animate with given duration and ease animate: function(o, easing, delay) { return (this.fx || (this.fx = new SVG.FX(this))).animate(o, easing, delay) } , delay: function(delay){ return (this.fx || (this.fx = new SVG.FX(this))).delay(delay) } , stop: function(jumpToEnd, clearQueue) { if (this.fx) this.fx.stop(jumpToEnd, clearQueue) return this } // Pause current animation , pause: function() { if (this.fx) this.fx.pause() return this } // Play paused current animation , play: function() { if (this.fx) this.fx.play() return this } } }) // MorphObj is used whenever no morphable object is given SVG.MorphObj = SVG.invent({ create: function(to){ // prepare color for morphing if(SVG.Color.isColor(to)) return new SVG.Color().morph(to) // prepare number for morphing if(SVG.regex.numberAndUnit.test(to)) return new SVG.Number().morph(to) // prepare for plain morphing this.value = 0 this.destination = to } , extend: { at: function(pos, real){ return real < 1 ? this.value : this.destination }, valueOf: function(){ return this.value } } }) SVG.extend(SVG.FX, { // Add animatable attributes attr: function(a, v, relative) { // apply attributes individually if (typeof a == 'object') { for (var key in a) this.attr(key, a[key]) } else { // the MorphObj takes care about the right function used this.add(a, new SVG.MorphObj(v), 'attrs') } return this } // Add animatable styles , style: function(s, v) { if (typeof s == 'object') for (var key in s) this.style(key, s[key]) else this.add(s, new SVG.MorphObj(v), 'styles') return this } // Animatable x-axis , x: function(x, relative) { if(this.target() instanceof SVG.G){ this.transform({x:x}, relative) return this } var num = new SVG.Number().morph(x) num.relative = relative return this.add('x', num) } // Animatable y-axis , y: function(y, relative) { if(this.target() instanceof SVG.G){ this.transform({y:y}, relative) return this } var num = new SVG.Number().morph(y) num.relative = relative return this.add('y', num) } // Animatable center x-axis , cx: function(x) { return this.add('cx', new SVG.Number().morph(x)) } // Animatable center y-axis , cy: function(y) { return this.add('cy', new SVG.Number().morph(y)) } // Add animatable move , move: function(x, y) { return this.x(x).y(y) } // Add animatable center , center: function(x, y) { return this.cx(x).cy(y) } // Add animatable size , size: function(width, height) { if (this.target() instanceof SVG.Text) { // animate font size for Text elements this.attr('font-size', width) } else { // animate bbox based size for all other elements var box if(!width || !height){ box = this.target().bbox() } if(!width){ width = box.width / box.height * height } if(!height){ height = box.height / box.width * width } this.add('width' , new SVG.Number().morph(width)) .add('height', new SVG.Number().morph(height)) } return this } // Add animatable plot , plot: function(p) { return this.add('plot', this.target().array().morph(p)) } // Add leading method , leading: function(value) { return this.target().leading ? this.add('leading', new SVG.Number().morph(value)) : this } // Add animatable viewbox , viewbox: function(x, y, width, height) { if (this.target() instanceof SVG.Container) { this.add('viewbox', new SVG.ViewBox(x, y, width, height)) } return this } , update: function(o) { if (this.target() instanceof SVG.Stop) { if (typeof o == 'number' || o instanceof SVG.Number) { return this.update({ offset: arguments[0] , color: arguments[1] , opacity: arguments[2] }) } if (o.opacity != null) this.attr('stop-opacity', o.opacity) if (o.color != null) this.attr('stop-color', o.color) if (o.offset != null) this.attr('offset', o.offset) } return this } }) SVG.BBox = SVG.invent({ // Initialize create: function(element) { // get values if element is given if (element) { var box // yes this is ugly, but Firefox can be a bitch when it comes to elements that are not yet rendered try { // find native bbox box = element.node.getBBox() } catch(e) { if(element instanceof SVG.Shape){ var clone = element.clone().addTo(SVG.parser.draw) box = clone.bbox() clone.remove() }else{ box = { x: element.node.clientLeft , y: element.node.clientTop , width: element.node.clientWidth , height: element.node.clientHeight } } } // plain x and y this.x = box.x this.y = box.y // plain width and height this.width = box.width this.height = box.height } // add center, right and bottom fullBox(this) } // Define Parent , parent: SVG.Element // Constructor , construct: { // Get bounding box bbox: function() { return new SVG.BBox(this) } } }) SVG.TBox = SVG.invent({ // Initialize create: function(element) { // get values if element is given if (element) { var t = element.ctm().extract() , box = element.bbox() // width and height including transformations this.width = box.width * t.scaleX this.height = box.height * t.scaleY // x and y including transformations this.x = box.x + t.x this.y = box.y + t.y } // add center, right and bottom fullBox(this) } // Define Parent , parent: SVG.Element // Constructor , construct: { // Get transformed bounding box tbox: function() { return new SVG.TBox(this) } } }) SVG.RBox = SVG.invent({ // Initialize create: function(element) { if (element) { var e = element.doc().parent() , box = element.node.getBoundingClientRect() , zoom = 1 // get screen offset this.x = box.left this.y = box.top // subtract parent offset this.x -= e.offsetLeft this.y -= e.offsetTop while (e = e.offsetParent) { this.x -= e.offsetLeft this.y -= e.offsetTop } // calculate cumulative zoom from svg documents e = element while (e.parent && (e = e.parent())) { if (e.viewbox) { zoom *= e.viewbox().zoom this.x -= e.x() || 0 this.y -= e.y() || 0 } } // recalculate viewbox distortion this.width = box.width /= zoom this.height = box.height /= zoom } // add center, right and bottom fullBox(this) // offset by window scroll position, because getBoundingClientRect changes when window is scrolled this.x += window.pageXOffset this.y += window.pageYOffset } // define Parent , parent: SVG.Element // Constructor , construct: { // Get rect box rbox: function() { return new SVG.RBox(this) } } }) // Add universal merge method ;[SVG.BBox, SVG.TBox, SVG.RBox].forEach(function(c) { SVG.extend(c, { // Merge rect box with another, return a new instance merge: function(box) { var b = new c() // merge boxes b.x = Math.min(this.x, box.x) b.y = Math.min(this.y, box.y) b.width = Math.max(this.x + this.width, box.x + box.width) - b.x b.height = Math.max(this.y + this.height, box.y + box.height) - b.y return fullBox(b) } }) }) SVG.Matrix = SVG.invent({ // Initialize create: function(source) { var i, base = arrayToMatrix([1, 0, 0, 1, 0, 0]) // ensure source as object source = source instanceof SVG.Element ? source.matrixify() : typeof source === 'string' ? stringToMatrix(source) : arguments.length == 6 ? arrayToMatrix([].slice.call(arguments)) : typeof source === 'object' ? source : base // merge source for (i = abcdef.length - 1; i >= 0; --i) this[abcdef[i]] = source && typeof source[abcdef[i]] === 'number' ? source[abcdef[i]] : base[abcdef[i]] } // Add methods , extend: { // Extract individual transformations extract: function() { // find delta transform points var px = deltaTransformPoint(this, 0, 1) , py = deltaTransformPoint(this, 1, 0) , skewX = 180 / Math.PI * Math.atan2(px.y, px.x) - 90 return { // translation x: this.e , y: this.f , transformedX:(this.e * Math.cos(skewX * Math.PI / 180) + this.f * Math.sin(skewX * Math.PI / 180)) / Math.sqrt(this.a * this.a + this.b * this.b) , transformedY:(this.f * Math.cos(skewX * Math.PI / 180) + this.e * Math.sin(-skewX * Math.PI / 180)) / Math.sqrt(this.c * this.c + this.d * this.d) // skew , skewX: -skewX , skewY: 180 / Math.PI * Math.atan2(py.y, py.x) // scale , scaleX: Math.sqrt(this.a * this.a + this.b * this.b) , scaleY: Math.sqrt(this.c * this.c + this.d * this.d) // rotation , rotation: skewX , a: this.a , b: this.b , c: this.c , d: this.d , e: this.e , f: this.f , matrix: new SVG.Matrix(this) } } // Clone matrix , clone: function() { return new SVG.Matrix(this) } // Morph one matrix into another , morph: function(matrix) { // store new destination this.destination = new SVG.Matrix(matrix) return this } // Get morphed matrix at a given position , at: function(pos) { // make sure a destination is defined if (!this.destination) return this // calculate morphed matrix at a given position var matrix = new SVG.Matrix({ a: this.a + (this.destination.a - this.a) * pos , b: this.b + (this.destination.b - this.b) * pos , c: this.c + (this.destination.c - this.c) * pos , d: this.d + (this.destination.d - this.d) * pos , e: this.e + (this.destination.e - this.e) * pos , f: this.f + (this.destination.f - this.f) * pos }) // process parametric rotation if present if (this.param && this.param.to) { // calculate current parametric position var param = { rotation: this.param.from.rotation + (this.param.to.rotation - this.param.from.rotation) * pos , cx: this.param.from.cx , cy: this.param.from.cy } // rotate matrix matrix = matrix.rotate( (this.param.to.rotation - this.param.from.rotation * 2) * pos , param.cx , param.cy ) // store current parametric values matrix.param = param } return matrix } // Multiplies by given matrix , multiply: function(matrix) { return new SVG.Matrix(this.native().multiply(parseMatrix(matrix).native())) } // Inverses matrix , inverse: function() { return new SVG.Matrix(this.native().inverse()) } // Translate matrix , translate: function(x, y) { return new SVG.Matrix(this.native().translate(x || 0, y || 0)) } // Scale matrix , scale: function(x, y, cx, cy) { // support universal scale if (arguments.length == 1 || arguments.length == 3) y = x if (arguments.length == 3) { cy = cx cx = y } return this.around(cx, cy, new SVG.Matrix(x, 0, 0, y, 0, 0)) } // Rotate matrix , rotate: function(r, cx, cy) { // convert degrees to radians r = SVG.utils.radians(r) return this.around(cx, cy, new SVG.Matrix(Math.cos(r), Math.sin(r), -Math.sin(r), Math.cos(r), 0, 0)) } // Flip matrix on x or y, at a given offset , flip: function(a, o) { return a == 'x' ? this.scale(-1, 1, o, 0) : this.scale(1, -1, 0, o) } // Skew , skew: function(x, y, cx, cy) { return this.around(cx, cy, this.native().skewX(x || 0).skewY(y || 0)) } // SkewX , skewX: function(x, cx, cy) { return this.around(cx, cy, this.native().skewX(x || 0)) } // SkewY , skewY: function(y, cx, cy) { return this.around(cx, cy, this.native().skewY(y || 0)) } // Transform around a center point , around: function(cx, cy, matrix) { return this .multiply(new SVG.Matrix(1, 0, 0, 1, cx || 0, cy || 0)) .multiply(matrix) .multiply(new SVG.Matrix(1, 0, 0, 1, -cx || 0, -cy || 0)) } // Convert to native SVGMatrix , native: function() { // create new matrix var matrix = SVG.parser.draw.node.createSVGMatrix() // update with current values for (var i = abcdef.length - 1; i >= 0; i--) matrix[abcdef[i]] = this[abcdef[i]] return matrix } // Convert matrix to string , toString: function() { return 'matrix(' + this.a + ',' + this.b + ',' + this.c + ',' + this.d + ',' + this.e + ',' + this.f + ')' } } // Define parent , parent: SVG.Element // Add parent method , construct: { // Get current matrix ctm: function() { return new SVG.Matrix(this.node.getCTM()) }, // Get current screen matrix screenCTM: function() { return new SVG.Matrix(this.node.getScreenCTM()) } } }) SVG.Point = SVG.invent({ // Initialize create: function(x,y) { var i, source, base = {x:0, y:0} // ensure source as object source = Array.isArray(x) ? {x:x[0], y:x[1]} : typeof x === 'object' ? {x:x.x, y:x.y} : y != null ? {x:x, y:y} : base // merge source this.x = source.x this.y = source.y } // Add methods , extend: { // Clone point clone: function() { return new SVG.Point(this) } // Morph one point into another , morph: function(point) { // store new destination this.destination = new SVG.Point(point) return this } // Get morphed point at a given position , at: function(pos) { // make sure a destination is defined if (!this.destination) return this // calculate morphed matrix at a given position var point = new SVG.Point({ x: this.x + (this.destination.x - this.x) * pos , y: this.y + (this.destination.y - this.y) * pos }) return point } // Convert to native SVGPoint , native: function() { // create new point var point = SVG.parser.draw.node.createSVGPoint() // update with current values point.x = this.x point.y = this.y return point } // transform point with matrix , transform: function(matrix) { return new SVG.Point(this.native().matrixTransform(matrix.native())) } } }) SVG.extend(SVG.Element, { // Get point point: function(x, y) { return new SVG.Point(x,y).transform(this.screenCTM().inverse()); } }) SVG.extend(SVG.Element, { // Set svg element attribute attr: function(a, v, n) { // act as full getter if (a == null) { // get an object of attributes a = {} v = this.node.attributes for (n = v.length - 1; n >= 0; n--) a[v[n].nodeName] = SVG.regex.isNumber.test(v[n].nodeValue) ? parseFloat(v[n].nodeValue) : v[n].nodeValue return a } else if (typeof a == 'object') { // apply every attribute individually if an object is passed for (v in a) this.attr(v, a[v]) } else if (v === null) { // remove value this.node.removeAttribute(a) } else if (v == null) { // act as a getter if the first and only argument is not an object v = this.node.getAttribute(a) return v == null ? SVG.defaults.attrs[a] : SVG.regex.isNumber.test(v) ? parseFloat(v) : v } else { // BUG FIX: some browsers will render a stroke if a color is given even though stroke width is 0 if (a == 'stroke-width') this.attr('stroke', parseFloat(v) > 0 ? this._stroke : null) else if (a == 'stroke') this._stroke = v // convert image fill and stroke to patterns if (a == 'fill' || a == 'stroke') { if (SVG.regex.isImage.test(v)) v = this.doc().defs().image(v, 0, 0) if (v instanceof SVG.Image) v = this.doc().defs().pattern(0, 0, function() { this.add(v) }) } // ensure correct numeric values (also accepts NaN and Infinity) if (typeof v === 'number') v = new SVG.Number(v) // ensure full hex color else if (SVG.Color.isColor(v)) v = new SVG.Color(v) // parse array values else if (Array.isArray(v)) v = new SVG.Array(v) // store parametric transformation values locally else if (v instanceof SVG.Matrix && v.param) this.param = v.param // if the passed attribute is leading... if (a == 'leading') { // ... call the leading method instead if (this.leading) this.leading(v) } else { // set given attribute on node typeof n === 'string' ? this.node.setAttributeNS(n, a, v.toString()) : this.node.setAttribute(a, v.toString()) } // rebuild if required if (this.rebuild && (a == 'font-size' || a == 'x')) this.rebuild(a, v) } return this } }) SVG.extend(SVG.Element, { // Add transformations transform: function(o, relative) { // get target in case of the fx module, otherwise reference this var target = this , matrix // act as a getter if (typeof o !== 'object') { // get current matrix matrix = new SVG.Matrix(target).extract() return typeof o === 'string' ? matrix[o] : matrix } // get current matrix matrix = new SVG.Matrix(target) // ensure relative flag relative = !!relative || !!o.relative // act on matrix if (o.a != null) { matrix = relative ? // relative matrix.multiply(new SVG.Matrix(o)) : // absolute new SVG.Matrix(o) // act on rotation } else if (o.rotation != null) { // ensure centre point ensureCentre(o, target) // apply transformation matrix = relative ? // relative matrix.rotate(o.rotation, o.cx, o.cy) : // absolute matrix.rotate(o.rotation - matrix.extract().rotation, o.cx, o.cy) // act on scale } else if (o.scale != null || o.scaleX != null || o.scaleY != null) { // ensure centre point ensureCentre(o, target) // ensure scale values on both axes o.scaleX = o.scale != null ? o.scale : o.scaleX != null ? o.scaleX : 1 o.scaleY = o.scale != null ? o.scale : o.scaleY != null ? o.scaleY : 1 if (!relative) { // absolute; multiply inversed values var e = matrix.extract() o.scaleX = o.scaleX * 1 / e.scaleX o.scaleY = o.scaleY * 1 / e.scaleY } matrix = matrix.scale(o.scaleX, o.scaleY, o.cx, o.cy) // act on skew } else if (o.skewX != null || o.skewY != null) { // ensure centre point ensureCentre(o, target) // ensure skew values on both axes o.skewX = o.skewX != null ? o.skewX : 0 o.skewY = o.skewY != null ? o.skewY : 0 if (!relative) { // absolute; reset skew values var e = matrix.extract() matrix = matrix.multiply(new SVG.Matrix().skew(e.skewX, e.skewY, o.cx, o.cy).inverse()) } matrix = matrix.skew(o.skewX, o.skewY, o.cx, o.cy) // act on flip } else if (o.flip) { matrix = matrix.flip( o.flip , o.offset == null ? target.bbox()['c' + o.flip] : o.offset ) // act on translate } else if (o.x != null || o.y != null) { if (relative) { // relative matrix = matrix.translate(o.x, o.y) } else { // absolute if (o.x != null) matrix.e = o.x if (o.y != null) matrix.f = o.y } } return this.attr('transform', matrix) } }) SVG.extend(SVG.FX, { transform: function(o, relative) { // get target in case of the fx module, otherwise reference this var target = this.target() , matrix // act as a getter if (typeof o !== 'object') { // get current matrix matrix = new SVG.Matrix(target).extract() return typeof o === 'string' ? matrix[o] : matrix } // ensure relative flag relative = !!relative || !!o.relative // act on matrix if (o.a != null) { matrix = new SVG.Matrix(o) // act on rotation } else if (o.rotation != null) { // ensure centre point ensureCentre(o, target) // apply transformation matrix = new SVG.Rotate(o.rotation, o.cx, o.cy) // act on scale } else if (o.scale != null || o.scaleX != null || o.scaleY != null) { // ensure centre point ensureCentre(o, target) // ensure scale values on both axes o.scaleX = o.scale != null ? o.scale : o.scaleX != null ? o.scaleX : 1 o.scaleY = o.scale != null ? o.scale : o.scaleY != null ? o.scaleY : 1 matrix = new SVG.Scale(o.scaleX, o.scaleY, o.cx, o.cy) // act on skew } else if (o.skewX != null || o.skewY != null) { // ensure centre point ensureCentre(o, target) // ensure skew values on both axes o.skewX = o.skewX != null ? o.skewX : 0 o.skewY = o.skewY != null ? o.skewY : 0 matrix = new SVG.Skew(o.skewX, o.skewY, o.cx, o.cy) // act on flip } else if (o.flip) { matrix = new SVG.Matrix().morph(new SVG.Matrix().flip( o.flip , o.offset == null ? target.bbox()['c' + o.flip] : o.offset )) // act on translate } else if (o.x != null || o.y != null) { matrix = new SVG.Translate(o.x, o.y) } if(!matrix) return this matrix.relative = relative var situation = this.situations.length ? this.situations[this.situations.length-1] : this.current situation.transforms.push(matrix) setTimeout(function(){this.start()}.bind(this), 0) return this } }) SVG.extend(SVG.Element, { // Reset all transformations untransform: function() { return this.attr('transform', null) }, // merge the whole transformation chain into one matrix and returns it matrixify: function() { var matrix = (this.attr('transform') || '') // split transformations .split(/\)\s*/).slice(0,-1).map(function(str){ // generate key => value pairs var kv = str.trim().split('(') return [kv[0], kv[1].split(SVG.regex.matrixElements).map(function(str){ return parseFloat(str) })] }) // calculate every transformation into one matrix .reduce(function(matrix, transform){ if(transform[0] == 'matrix') return matrix.multiply(arrayToMatrix(transform[1])) return matrix[transform[0]].apply(matrix, transform[1]) }, new SVG.Matrix()) return matrix }, // add an element to another parent without changing the visual representation on the screen toParent: function(parent) { if(this == parent) return this var ctm = this.screenCTM() var temp = parent.rect(1,1) var pCtm = temp.screenCTM().inverse() temp.remove() this.addTo(parent).untransform().transform(pCtm.multiply(ctm)) return this }, // same as above with parent equals root-svg toDoc: function() { return this.toParent(this.doc()) } }) SVG.Transformation = SVG.invent({ create: function(source, inversed){ if(arguments.length > 1 && typeof inversed != 'boolean'){ return this.create([].slice.call(arguments)) } if(typeof source == 'object'){ for(var i = 0, len = this.arguments.length; i < len; ++i){ this[this.arguments[i]] = source[this.arguments[i]] } } if(Array.isArray(source)){ for(var i = 0, len = this.arguments.length; i < len; ++i){ this[this.arguments[i]] = source[i] } } this.inversed = false if(inversed === true){ this.inversed = true } } , extend: { at: function(pos){ var params = [] for(var i = 0, len = this.arguments.length; i < len; ++i){ params.push(this[this.arguments[i]]) } var m = this._undo || new SVG.Matrix() m = new SVG.Matrix().morph(SVG.Matrix.prototype[this.method].apply(m, params)).at(pos) return this.inversed ? m.inverse() : m } , undo: function(o){ this._undo = new SVG[capitalize(this.method)](o, true).at(1) return this } } }) SVG.Translate = SVG.invent({ parent: SVG.Matrix , inherit: SVG.Transformation , create: function(source, inversed){ if(typeof source == 'object') this.constructor.call(this, source, inversed) else this.constructor.call(this, [].slice.call(arguments)) } , extend: { arguments: ['transformedX', 'transformedY'] , method: 'translate' } }) SVG.Rotate = SVG.invent({ parent: SVG.Matrix , inherit: SVG.Transformation , create: function(source, inversed){ if(typeof source == 'object') this.constructor.call(this, source, inversed) else this.constructor.call(this, [].slice.call(arguments)) } , extend: { arguments: ['rotation', 'cx', 'cy'] , method: 'rotate' , at: function(pos){ var m = new SVG.Matrix().rotate(new SVG.Number().morph(this.rotation - (this._undo ? this._undo.rotation : 0)).at(pos), this.cx, this.cy) return this.inversed ? m.inverse() : m } , undo: function(o){ this._undo = o } } }) SVG.Scale = SVG.invent({ parent: SVG.Matrix , inherit: SVG.Transformation , create: function(source, inversed){ if(typeof source == 'object') this.constructor.call(this, source, inversed) else this.constructor.call(this, [].slice.call(arguments)) } , extend: { arguments: ['scaleX', 'scaleY', 'cx', 'cy'] , method: 'scale' } }) SVG.Skew = SVG.invent({ parent: SVG.Matrix , inherit: SVG.Transformation , create: function(source, inversed){ if(typeof source == 'object') this.constructor.call(this, source, inversed) else this.constructor.call(this, [].slice.call(arguments)) } , extend: { arguments: ['skewX', 'skewY', 'cx', 'cy'] , method: 'skew' } }) SVG.extend(SVG.Element, { // Dynamic style generator style: function(s, v) { if (arguments.length == 0) { // get full style return this.node.style.cssText || '' } else if (arguments.length < 2) { // apply every style individually if an object is passed if (typeof s == 'object') { for (v in s) this.style(v, s[v]) } else if (SVG.regex.isCss.test(s)) { // parse css string s = s.split(';') // apply every definition individually for (var i = 0; i < s.length; i++) { v = s[i].split(':') this.style(v[0].replace(/\s+/g, ''), v[1]) } } else { // act as a getter if the first and only argument is not an object return this.node.style[camelCase(s)] } } else { this.node.style[camelCase(s)] = v === null || SVG.regex.isBlank.test(v) ? '' : v } return this } }) SVG.Parent = SVG.invent({ // Initialize node create: function(element) { this.constructor.call(this, element) } // Inherit from , inherit: SVG.Element // Add class methods , extend: { // Returns all child elements children: function() { return SVG.utils.map(SVG.utils.filterSVGElements(this.node.childNodes), function(node) { return SVG.adopt(node) }) } // Add given element at a position , add: function(element, i) { if (!this.has(element)) { // define insertion index if none given i = i == null ? this.children().length : i // add element references this.node.insertBefore(element.node, this.node.childNodes[i] || null) } return this } // Basically does the same as `add()` but returns the added element instead , put: function(element, i) { this.add(element, i) return element } // Checks if the given element is a child , has: function(element) { return this.index(element) >= 0 } // Gets index of given element , index: function(element) { return this.children().indexOf(element) } // Get a element at the given index , get: function(i) { return this.children()[i] } // Get first child, skipping the defs node , first: function() { return this.children()[0] } // Get the last child , last: function() { return this.children()[this.children().length - 1] } // Iterates over all children and invokes a given block , each: function(block, deep) { var i, il , children = this.children() for (i = 0, il = children.length; i < il; i++) { if (children[i] instanceof SVG.Element) block.apply(children[i], [i, children]) if (deep && (children[i] instanceof SVG.Container)) children[i].each(block, deep) } return this } // Remove a given child , removeElement: function(element) { this.node.removeChild(element.node) return this } // Remove all elements in this container , clear: function() { // remove children while(this.node.hasChildNodes()) this.node.removeChild(this.node.lastChild) // remove defs reference delete this._defs return this } , // Get defs defs: function() { return this.doc().defs() } } }) SVG.extend(SVG.Parent, { ungroup: function(parent, depth) { if(depth === 0 || this instanceof SVG.Defs) return this parent = parent || (this instanceof SVG.Doc ? this : this.parent(SVG.Parent)) depth = depth || Infinity this.each(function(){ if(this instanceof SVG.Defs) return this if(this instanceof SVG.Parent) return this.ungroup(parent, depth-1) return this.toParent(parent) }) this.node.firstChild || this.remove() return this }, flatten: function(parent, depth) { return this.ungroup(parent, depth) } }) SVG.Container = SVG.invent({ // Initialize node create: function(element) { this.constructor.call(this, element) } // Inherit from , inherit: SVG.Parent // Add class methods , extend: { // Get the viewBox and calculate the zoom value viewbox: function(v) { if (arguments.length == 0) // act as a getter if there are no arguments return new SVG.ViewBox(this) // otherwise act as a setter v = arguments.length == 1 ? [v.x, v.y, v.width, v.height] : [].slice.call(arguments) return this.attr('viewBox', v) } } }) // Add events to elements ;[ 'click' , 'dblclick' , 'mousedown' , 'mouseup' , 'mouseover' , 'mouseout' , 'mousemove' // , 'mouseenter' -> not supported by IE // , 'mouseleave' -> not supported by IE , 'touchstart' , 'touchmove' , 'touchleave' , 'touchend' , 'touchcancel' ].forEach(function(event) { // add event to SVG.Element SVG.Element.prototype[event] = function(f) { var self = this // bind event to element rather than element node this.node['on' + event] = typeof f == 'function' ? function() { return f.apply(self, arguments) } : null return this } }) // Initialize listeners stack SVG.listeners = [] SVG.handlerMap = [] SVG.listenerId = 0 // Add event binder in the SVG namespace SVG.on = function(node, event, listener, binding) { // create listener, get object-index var l = listener.bind(binding || node.instance || node) , index = (SVG.handlerMap.indexOf(node) + 1 || SVG.handlerMap.push(node)) - 1 , ev = event.split('.')[0] , ns = event.split('.')[1] || '*' // ensure valid object SVG.listeners[index] = SVG.listeners[index] || {} SVG.listeners[index][ev] = SVG.listeners[index][ev] || {} SVG.listeners[index][ev][ns] = SVG.listeners[index][ev][ns] || {} if(!listener._svgjsListenerId) listener._svgjsListenerId = ++SVG.listenerId // reference listener SVG.listeners[index][ev][ns][listener._svgjsListenerId] = l // add listener node.addEventListener(ev, l, false) } // Add event unbinder in the SVG namespace SVG.off = function(node, event, listener) { var index = SVG.handlerMap.indexOf(node) , ev = event && event.split('.')[0] , ns = event && event.split('.')[1] if(index == -1) return if (listener) { if(typeof listener == 'function') listener = listener._svgjsListenerId if(!listener) return // remove listener reference if (SVG.listeners[index][ev] && SVG.listeners[index][ev][ns || '*']) { // remove listener node.removeEventListener(ev, SVG.listeners[index][ev][ns || '*'][listener], false) delete SVG.listeners[index][ev][ns || '*'][listener] } } else if (ns && ev) { // remove all listeners for a namespaced event if (SVG.listeners[index][ev] && SVG.listeners[index][ev][ns]) { for (listener in SVG.listeners[index][ev][ns]) SVG.off(node, [ev, ns].join('.'), listener) delete SVG.listeners[index][ev][ns] } } else if (ns){ // remove all listeners for a specific namespace for(event in SVG.listeners[index]){ for(namespace in SVG.listeners[index][event]){ if(ns === namespace){ SVG.off(node, [event, ns].join('.')) } } } } else if (ev) { // remove all listeners for the event if (SVG.listeners[index][ev]) { for (namespace in SVG.listeners[index][ev]) SVG.off(node, [ev, namespace].join('.')) delete SVG.listeners[index][ev] } } else { // remove all listeners on a given node for (event in SVG.listeners[index]) SVG.off(node, event) delete SVG.listeners[index] } } // SVG.extend(SVG.Element, { // Bind given event to listener on: function(event, listener, binding) { SVG.on(this.node, event, listener, binding) return this } // Unbind event from listener , off: function(event, listener) { SVG.off(this.node, event, listener) return this } // Fire given event , fire: function(event, data) { // Dispatch event if(event instanceof Event){ this.node.dispatchEvent(event) }else{ this.node.dispatchEvent(new CustomEvent(event, {detail:data})) } return this } }) SVG.Defs = SVG.invent({ // Initialize node create: 'defs' // Inherit from , inherit: SVG.Container }) SVG.G = SVG.invent({ // Initialize node create: 'g' // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Move over x-axis x: function(x) { return x == null ? this.transform('x') : this.transform({ x: x - this.x() }, true) } // Move over y-axis , y: function(y) { return y == null ? this.transform('y') : this.transform({ y: y - this.y() }, true) } // Move by center over x-axis , cx: function(x) { return x == null ? this.gbox().cx : this.x(x - this.gbox().width / 2) } // Move by center over y-axis , cy: function(y) { return y == null ? this.gbox().cy : this.y(y - this.gbox().height / 2) } , gbox: function() { var bbox = this.bbox() , trans = this.transform() bbox.x += trans.x bbox.x2 += trans.x bbox.cx += trans.x bbox.y += trans.y bbox.y2 += trans.y bbox.cy += trans.y return bbox } } // Add parent method , construct: { // Create a group element group: function() { return this.put(new SVG.G) } } }) // ### This module adds backward / forward functionality to elements. // SVG.extend(SVG.Element, { // Get all siblings, including myself siblings: function() { return this.parent().children() } // Get the curent position siblings , position: function() { return this.parent().index(this) } // Get the next element (will return null if there is none) , next: function() { return this.siblings()[this.position() + 1] } // Get the next element (will return null if there is none) , previous: function() { return this.siblings()[this.position() - 1] } // Send given element one step forward , forward: function() { var i = this.position() + 1 , p = this.parent() // move node one step forward p.removeElement(this).add(this, i) // make sure defs node is always at the top if (p instanceof SVG.Doc) p.node.appendChild(p.defs().node) return this } // Send given element one step backward , backward: function() { var i = this.position() if (i > 0) this.parent().removeElement(this).add(this, i - 1) return this } // Send given element all the way to the front , front: function() { var p = this.parent() // Move node forward p.node.appendChild(this.node) // Make sure defs node is always at the top if (p instanceof SVG.Doc) p.node.appendChild(p.defs().node) return this } // Send given element all the way to the back , back: function() { if (this.position() > 0) this.parent().removeElement(this).add(this, 0) return this } // Inserts a given element before the targeted element , before: function(element) { element.remove() var i = this.position() this.parent().add(element, i) return this } // Insters a given element after the targeted element , after: function(element) { element.remove() var i = this.position() this.parent().add(element, i + 1) return this } }) SVG.Mask = SVG.invent({ // Initialize node create: function() { this.constructor.call(this, SVG.create('mask')) // keep references to masked elements this.targets = [] } // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Unmask all masked elements and remove itself remove: function() { // unmask all targets for (var i = this.targets.length - 1; i >= 0; i--) if (this.targets[i]) this.targets[i].unmask() this.targets = [] // remove mask from parent this.parent().removeElement(this) return this } } // Add parent method , construct: { // Create masking element mask: function() { return this.defs().put(new SVG.Mask) } } }) SVG.extend(SVG.Element, { // Distribute mask to svg element maskWith: function(element) { // use given mask or create a new one this.masker = element instanceof SVG.Mask ? element : this.parent().mask().add(element) // store reverence on self in mask this.masker.targets.push(this) // apply mask return this.attr('mask', 'url("#' + this.masker.attr('id') + '")') } // Unmask element , unmask: function() { delete this.masker return this.attr('mask', null) } }) SVG.ClipPath = SVG.invent({ // Initialize node create: function() { this.constructor.call(this, SVG.create('clipPath')) // keep references to clipped elements this.targets = [] } // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Unclip all clipped elements and remove itself remove: function() { // unclip all targets for (var i = this.targets.length - 1; i >= 0; i--) if (this.targets[i]) this.targets[i].unclip() this.targets = [] // remove clipPath from parent this.parent().removeElement(this) return this } } // Add parent method , construct: { // Create clipping element clip: function() { return this.defs().put(new SVG.ClipPath) } } }) // SVG.extend(SVG.Element, { // Distribute clipPath to svg element clipWith: function(element) { // use given clip or create a new one this.clipper = element instanceof SVG.ClipPath ? element : this.parent().clip().add(element) // store reverence on self in mask this.clipper.targets.push(this) // apply mask return this.attr('clip-path', 'url("#' + this.clipper.attr('id') + '")') } // Unclip element , unclip: function() { delete this.clipper return this.attr('clip-path', null) } }) SVG.Gradient = SVG.invent({ // Initialize node create: function(type) { this.constructor.call(this, SVG.create(type + 'Gradient')) // store type this.type = type } // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Add a color stop at: function(offset, color, opacity) { return this.put(new SVG.Stop).update(offset, color, opacity) } // Update gradient , update: function(block) { // remove all stops this.clear() // invoke passed block if (typeof block == 'function') block.call(this, this) return this } // Return the fill id , fill: function() { return 'url(#' + this.id() + ')' } // Alias string convertion to fill , toString: function() { return this.fill() } // custom attr to handle transform , attr: function(a, b, c) { if(a == 'transform') a = 'gradientTransform' return SVG.Container.prototype.attr.call(this, a, b, c) } } // Add parent method , construct: { // Create gradient element in defs gradient: function(type, block) { return this.defs().gradient(type, block) } } }) // Add animatable methods to both gradient and fx module SVG.extend(SVG.Gradient, SVG.FX, { // From position from: function(x, y) { return (this.target || this).type == 'radial' ? this.attr({ fx: new SVG.Number(x), fy: new SVG.Number(y) }) : this.attr({ x1: new SVG.Number(x), y1: new SVG.Number(y) }) } // To position , to: function(x, y) { return (this.target || this).type == 'radial' ? this.attr({ cx: new SVG.Number(x), cy: new SVG.Number(y) }) : this.attr({ x2: new SVG.Number(x), y2: new SVG.Number(y) }) } }) // Base gradient generation SVG.extend(SVG.Defs, { // define gradient gradient: function(type, block) { return this.put(new SVG.Gradient(type)).update(block) } }) SVG.Stop = SVG.invent({ // Initialize node create: 'stop' // Inherit from , inherit: SVG.Element // Add class methods , extend: { // add color stops update: function(o) { if (typeof o == 'number' || o instanceof SVG.Number) { o = { offset: arguments[0] , color: arguments[1] , opacity: arguments[2] } } // set attributes if (o.opacity != null) this.attr('stop-opacity', o.opacity) if (o.color != null) this.attr('stop-color', o.color) if (o.offset != null) this.attr('offset', new SVG.Number(o.offset)) return this } } }) SVG.Pattern = SVG.invent({ // Initialize node create: 'pattern' // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Return the fill id fill: function() { return 'url(#' + this.id() + ')' } // Update pattern by rebuilding , update: function(block) { // remove content this.clear() // invoke passed block if (typeof block == 'function') block.call(this, this) return this } // Alias string convertion to fill , toString: function() { return this.fill() } // custom attr to handle transform , attr: function(a, b, c) { if(a == 'transform') a = 'patternTransform' return SVG.Container.prototype.attr.call(this, a, b, c) } } // Add parent method , construct: { // Create pattern element in defs pattern: function(width, height, block) { return this.defs().pattern(width, height, block) } } }) SVG.extend(SVG.Defs, { // Define gradient pattern: function(width, height, block) { return this.put(new SVG.Pattern).update(block).attr({ x: 0 , y: 0 , width: width , height: height , patternUnits: 'userSpaceOnUse' }) } }) SVG.Doc = SVG.invent({ // Initialize node create: function(element) { if (element) { // ensure the presence of a dom element element = typeof element == 'string' ? document.getElementById(element) : element // If the target is an svg element, use that element as the main wrapper. // This allows svg.js to work with svg documents as well. if (element.nodeName == 'svg') { this.constructor.call(this, element) } else { this.constructor.call(this, SVG.create('svg')) element.appendChild(this.node) } // set svg element attributes and ensure defs node this.namespace().size('100%', '100%').defs() } } // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Add namespaces namespace: function() { return this .attr({ xmlns: SVG.ns, version: '1.1' }) .attr('xmlns:xlink', SVG.xlink, SVG.xmlns) .attr('xmlns:svgjs', SVG.svgjs, SVG.xmlns) } // Creates and returns defs element , defs: function() { if (!this._defs) { var defs // Find or create a defs element in this instance if (defs = this.node.getElementsByTagName('defs')[0]) this._defs = SVG.adopt(defs) else this._defs = new SVG.Defs // Make sure the defs node is at the end of the stack this.node.appendChild(this._defs.node) } return this._defs } // custom parent method , parent: function() { return this.node.parentNode.nodeName == '#document' ? null : this.node.parentNode } // Fix for possible sub-pixel offset. See: // https://bugzilla.mozilla.org/show_bug.cgi?id=608812 , spof: function(spof) { var pos = this.node.getScreenCTM() if (pos) this .style('left', (-pos.e % 1) + 'px') .style('top', (-pos.f % 1) + 'px') return this } // Removes the doc from the DOM , remove: function() { if(this.parent()) { this.parent().removeChild(this.node); } return this; } } }) SVG.Shape = SVG.invent({ // Initialize node create: function(element) { this.constructor.call(this, element) } // Inherit from , inherit: SVG.Element }) SVG.Bare = SVG.invent({ // Initialize create: function(element, inherit) { // construct element this.constructor.call(this, SVG.create(element)) // inherit custom methods if (inherit) for (var method in inherit.prototype) if (typeof inherit.prototype[method] === 'function') this[method] = inherit.prototype[method] } // Inherit from , inherit: SVG.Element // Add methods , extend: { // Insert some plain text words: function(text) { // remove contents while (this.node.hasChildNodes()) this.node.removeChild(this.node.lastChild) // create text node this.node.appendChild(document.createTextNode(text)) return this } } }) SVG.extend(SVG.Parent, { // Create an element that is not described by SVG.js element: function(element, inherit) { return this.put(new SVG.Bare(element, inherit)) } // Add symbol element , symbol: function() { return this.defs().element('symbol', SVG.Container) } }) SVG.Use = SVG.invent({ // Initialize node create: 'use' // Inherit from , inherit: SVG.Shape // Add class methods , extend: { // Use element as a reference element: function(element, file) { // Set lined element return this.attr('href', (file || '') + '#' + element, SVG.xlink) } } // Add parent method , construct: { // Create a use element use: function(element, file) { return this.put(new SVG.Use).element(element, file) } } }) SVG.Rect = SVG.invent({ // Initialize node create: 'rect' // Inherit from , inherit: SVG.Shape // Add parent method , construct: { // Create a rect element rect: function(width, height) { return this.put(new SVG.Rect()).size(width, height) } } }) SVG.Circle = SVG.invent({ // Initialize node create: 'circle' // Inherit from , inherit: SVG.Shape // Add parent method , construct: { // Create circle element, based on ellipse circle: function(size) { return this.put(new SVG.Circle).rx(new SVG.Number(size).divide(2)).move(0, 0) } } }) SVG.extend(SVG.Circle, SVG.FX, { // Radius x value rx: function(rx) { return this.attr('r', rx) } // Alias radius x value , ry: function(ry) { return this.rx(ry) } }) SVG.Ellipse = SVG.invent({ // Initialize node create: 'ellipse' // Inherit from , inherit: SVG.Shape // Add parent method , construct: { // Create an ellipse ellipse: function(width, height) { return this.put(new SVG.Ellipse).size(width, height).move(0, 0) } } }) SVG.extend(SVG.Ellipse, SVG.Rect, SVG.FX, { // Radius x value rx: function(rx) { return this.attr('rx', rx) } // Radius y value , ry: function(ry) { return this.attr('ry', ry) } }) // Add common method SVG.extend(SVG.Circle, SVG.Ellipse, { // Move over x-axis x: function(x) { return x == null ? this.cx() - this.rx() : this.cx(x + this.rx()) } // Move over y-axis , y: function(y) { return y == null ? this.cy() - this.ry() : this.cy(y + this.ry()) } // Move by center over x-axis , cx: function(x) { return x == null ? this.attr('cx') : this.attr('cx', x) } // Move by center over y-axis , cy: function(y) { return y == null ? this.attr('cy') : this.attr('cy', y) } // Set width of element , width: function(width) { return width == null ? this.rx() * 2 : this.rx(new SVG.Number(width).divide(2)) } // Set height of element , height: function(height) { return height == null ? this.ry() * 2 : this.ry(new SVG.Number(height).divide(2)) } // Custom size function , size: function(width, height) { var p = proportionalSize(this.bbox(), width, height) return this .rx(new SVG.Number(p.width).divide(2)) .ry(new SVG.Number(p.height).divide(2)) } }) SVG.Line = SVG.invent({ // Initialize node create: 'line' // Inherit from , inherit: SVG.Shape // Add class methods , extend: { // Get array array: function() { return new SVG.PointArray([ [ this.attr('x1'), this.attr('y1') ] , [ this.attr('x2'), this.attr('y2') ] ]) } // Overwrite native plot() method , plot: function(x1, y1, x2, y2) { if (arguments.length == 4) x1 = { x1: x1, y1: y1, x2: x2, y2: y2 } else x1 = new SVG.PointArray(x1).toLine() return this.attr(x1) } // Move by left top corner , move: function(x, y) { return this.attr(this.array().move(x, y).toLine()) } // Set element size to given width and height , size: function(width, height) { var p = proportionalSize(this.bbox(), width, height) return this.attr(this.array().size(p.width, p.height).toLine()) } } // Add parent method , construct: { // Create a line element line: function(x1, y1, x2, y2) { return this.put(new SVG.Line).plot(x1, y1, x2, y2) } } }) SVG.Polyline = SVG.invent({ // Initialize node create: 'polyline' // Inherit from , inherit: SVG.Shape // Add parent method , construct: { // Create a wrapped polyline element polyline: function(p) { return this.put(new SVG.Polyline).plot(p) } } }) SVG.Polygon = SVG.invent({ // Initialize node create: 'polygon' // Inherit from , inherit: SVG.Shape // Add parent method , construct: { // Create a wrapped polygon element polygon: function(p) { return this.put(new SVG.Polygon).plot(p) } } }) // Add polygon-specific functions SVG.extend(SVG.Polyline, SVG.Polygon, { // Get array array: function() { return this._array || (this._array = new SVG.PointArray(this.attr('points'))) } // Plot new path , plot: function(p) { return this.attr('points', (this._array = new SVG.PointArray(p))) } // Move by left top corner , move: function(x, y) { return this.attr('points', this.array().move(x, y)) } // Set element size to given width and height , size: function(width, height) { var p = proportionalSize(this.bbox(), width, height) return this.attr('points', this.array().size(p.width, p.height)) } }) // unify all point to point elements SVG.extend(SVG.Line, SVG.Polyline, SVG.Polygon, { // Define morphable array morphArray: SVG.PointArray // Move by left top corner over x-axis , x: function(x) { return x == null ? this.bbox().x : this.move(x, this.bbox().y) } // Move by left top corner over y-axis , y: function(y) { return y == null ? this.bbox().y : this.move(this.bbox().x, y) } // Set width of element , width: function(width) { var b = this.bbox() return width == null ? b.width : this.size(width, b.height) } // Set height of element , height: function(height) { var b = this.bbox() return height == null ? b.height : this.size(b.width, height) } }) SVG.Path = SVG.invent({ // Initialize node create: 'path' // Inherit from , inherit: SVG.Shape // Add class methods , extend: { // Define morphable array morphArray: SVG.PathArray // Get array , array: function() { return this._array || (this._array = new SVG.PathArray(this.attr('d'))) } // Plot new poly points , plot: function(p) { return this.attr('d', (this._array = new SVG.PathArray(p))) } // Move by left top corner , move: function(x, y) { return this.attr('d', this.array().move(x, y)) } // Move by left top corner over x-axis , x: function(x) { return x == null ? this.bbox().x : this.move(x, this.bbox().y) } // Move by left top corner over y-axis , y: function(y) { return y == null ? this.bbox().y : this.move(this.bbox().x, y) } // Set element size to given width and height , size: function(width, height) { var p = proportionalSize(this.bbox(), width, height) return this.attr('d', this.array().size(p.width, p.height)) } // Set width of element , width: function(width) { return width == null ? this.bbox().width : this.size(width, this.bbox().height) } // Set height of element , height: function(height) { return height == null ? this.bbox().height : this.size(this.bbox().width, height) } } // Add parent method , construct: { // Create a wrapped path element path: function(d) { return this.put(new SVG.Path).plot(d) } } }) SVG.Image = SVG.invent({ // Initialize node create: 'image' // Inherit from , inherit: SVG.Shape // Add class methods , extend: { // (re)load image load: function(url) { if (!url) return this var self = this , img = document.createElement('img') // preload image img.onload = function() { var p = self.parent(SVG.Pattern) if(p === null) return // ensure image size if (self.width() == 0 && self.height() == 0) self.size(img.width, img.height) // ensure pattern size if not set if (p && p.width() == 0 && p.height() == 0) p.size(self.width(), self.height()) // callback if (typeof self._loaded === 'function') self._loaded.call(self, { width: img.width , height: img.height , ratio: img.width / img.height , url: url }) } return this.attr('href', (img.src = this.src = url), SVG.xlink) } // Add loaded callback , loaded: function(loaded) { this._loaded = loaded return this } } // Add parent method , construct: { // create image element, load image and set its size image: function(source, width, height) { return this.put(new SVG.Image).load(source).size(width || 0, height || width || 0) } } }) SVG.Text = SVG.invent({ // Initialize node create: function() { this.constructor.call(this, SVG.create('text')) this.dom.leading = new SVG.Number(1.3) // store leading value for rebuilding this._rebuild = true // enable automatic updating of dy values this._build = false // disable build mode for adding multiple lines // set default font this.attr('font-family', SVG.defaults.attrs['font-family']) } // Inherit from , inherit: SVG.Shape // Add class methods , extend: { clone: function(){ // clone element and assign new id var clone = assignNewId(this.node.cloneNode(true)) // insert the clone after myself this.after(clone) return clone } // Move over x-axis , x: function(x) { // act as getter if (x == null) return this.attr('x') // move lines as well if no textPath is present if (!this.textPath) this.lines().each(function() { if (this.dom.newLined) this.x(x) }) return this.attr('x', x) } // Move over y-axis , y: function(y) { var oy = this.attr('y') , o = typeof oy === 'number' ? oy - this.bbox().y : 0 // act as getter if (y == null) return typeof oy === 'number' ? oy - o : oy return this.attr('y', typeof y === 'number' ? y + o : y) } // Move center over x-axis , cx: function(x) { return x == null ? this.bbox().cx : this.x(x - this.bbox().width / 2) } // Move center over y-axis , cy: function(y) { return y == null ? this.bbox().cy : this.y(y - this.bbox().height / 2) } // Set the text content , text: function(text) { // act as getter if (typeof text === 'undefined'){ var text = '' var children = this.node.childNodes for(var i = 0, len = children.length; i < len; ++i){ // add newline if its not the first child and newLined is set to true if(i != 0 && children[i].nodeType != 3 && SVG.adopt(children[i]).dom.newLined == true){ text += '\n' } // add content of this node text += children[i].textContent } return text } // remove existing content this.clear().build(true) if (typeof text === 'function') { // call block text.call(this, this) } else { // store text and make sure text is not blank text = text.split('\n') // build new lines for (var i = 0, il = text.length; i < il; i++) this.tspan(text[i]).newLine() } // disable build mode and rebuild lines return this.build(false).rebuild() } // Set font size , size: function(size) { return this.attr('font-size', size).rebuild() } // Set / get leading , leading: function(value) { // act as getter if (value == null) return this.dom.leading // act as setter this.dom.leading = new SVG.Number(value) return this.rebuild() } // Get all the first level lines , lines: function() { var node = (this.textPath && this.textPath() || this).node // filter tspans and map them to SVG.js instances var lines = SVG.utils.map(SVG.utils.filterSVGElements(node.childNodes), function(el){ return SVG.adopt(el) }) // return an instance of SVG.set return new SVG.Set(lines) } // Rebuild appearance type , rebuild: function(rebuild) { // store new rebuild flag if given if (typeof rebuild == 'boolean') this._rebuild = rebuild // define position of all lines if (this._rebuild) { var self = this , blankLineOffset = 0 , dy = this.dom.leading * new SVG.Number(this.attr('font-size')) this.lines().each(function() { if (this.dom.newLined) { if (!this.textPath) this.attr('x', self.attr('x')) if(this.text() == '\n') { blankLineOffset += dy }else{ this.attr('dy', dy + blankLineOffset) blankLineOffset = 0 } } }) this.fire('rebuild') } return this } // Enable / disable build mode , build: function(build) { this._build = !!build return this } // overwrite method from parent to set data properly , setData: function(o){ this.dom = o this.dom.leading = new SVG.Number(o.leading || 1.3) return this } } // Add parent method , construct: { // Create text element text: function(text) { return this.put(new SVG.Text).text(text) } // Create plain text element , plain: function(text) { return this.put(new SVG.Text).plain(text) } } }) SVG.Tspan = SVG.invent({ // Initialize node create: 'tspan' // Inherit from , inherit: SVG.Shape // Add class methods , extend: { // Set text content text: function(text) { if(text == null) return this.node.textContent + (this.dom.newLined ? '\n' : '') typeof text === 'function' ? text.call(this, this) : this.plain(text) return this } // Shortcut dx , dx: function(dx) { return this.attr('dx', dx) } // Shortcut dy , dy: function(dy) { return this.attr('dy', dy) } // Create new line , newLine: function() { // fetch text parent var t = this.parent(SVG.Text) // mark new line this.dom.newLined = true // apply new hy¡n return this.dy(t.dom.leading * t.attr('font-size')).attr('x', t.x()) } } }) SVG.extend(SVG.Text, SVG.Tspan, { // Create plain text node plain: function(text) { // clear if build mode is disabled if (this._build === false) this.clear() // create text node this.node.appendChild(document.createTextNode(text)) return this } // Create a tspan , tspan: function(text) { var node = (this.textPath && this.textPath() || this).node , tspan = new SVG.Tspan // clear if build mode is disabled if (this._build === false) this.clear() // add new tspan node.appendChild(tspan.node) return tspan.text(text) } // Clear all lines , clear: function() { var node = (this.textPath && this.textPath() || this).node // remove existing child nodes while (node.hasChildNodes()) node.removeChild(node.lastChild) return this } // Get length of text element , length: function() { return this.node.getComputedTextLength() } }) SVG.TextPath = SVG.invent({ // Initialize node create: 'textPath' // Inherit from , inherit: SVG.Parent // Define parent class , parent: SVG.Text // Add parent method , construct: { // Create path for text to run on path: function(d) { // create textPath element var path = new SVG.TextPath , track = this.doc().defs().path(d) // move lines to textpath while (this.node.hasChildNodes()) path.node.appendChild(this.node.firstChild) // add textPath element as child node this.node.appendChild(path.node) // link textPath to path and add content path.attr('href', '#' + track, SVG.xlink) return this } // Plot path if any , plot: function(d) { var track = this.track() if (track) track.plot(d) return this } // Get the path track element , track: function() { var path = this.textPath() if (path) return path.reference('href') } // Get the textPath child , textPath: function() { if (this.node.firstChild && this.node.firstChild.nodeName == 'textPath') return SVG.adopt(this.node.firstChild) } } }) SVG.Nested = SVG.invent({ // Initialize node create: function() { this.constructor.call(this, SVG.create('svg')) this.style('overflow', 'visible') } // Inherit from , inherit: SVG.Container // Add parent method , construct: { // Create nested svg document nested: function() { return this.put(new SVG.Nested) } } }) SVG.A = SVG.invent({ // Initialize node create: 'a' // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Link url to: function(url) { return this.attr('href', url, SVG.xlink) } // Link show attribute , show: function(target) { return this.attr('show', target, SVG.xlink) } // Link target attribute , target: function(target) { return this.attr('target', target) } } // Add parent method , construct: { // Create a hyperlink element link: function(url) { return this.put(new SVG.A).to(url) } } }) SVG.extend(SVG.Element, { // Create a hyperlink element linkTo: function(url) { var link = new SVG.A if (typeof url == 'function') url.call(link, link) else link.to(url) return this.parent().put(link).put(this) } }) SVG.Marker = SVG.invent({ // Initialize node create: 'marker' // Inherit from , inherit: SVG.Container // Add class methods , extend: { // Set width of element width: function(width) { return this.attr('markerWidth', width) } // Set height of element , height: function(height) { return this.attr('markerHeight', height) } // Set marker refX and refY , ref: function(x, y) { return this.attr('refX', x).attr('refY', y) } // Update marker , update: function(block) { // remove all content this.clear() // invoke passed block if (typeof block == 'function') block.call(this, this) return this } // Return the fill id , toString: function() { return 'url(#' + this.id() + ')' } } // Add parent method , construct: { marker: function(width, height, block) { // Create marker element in defs return this.defs().marker(width, height, block) } } }) SVG.extend(SVG.Defs, { // Create marker marker: function(width, height, block) { // Set default viewbox to match the width and height, set ref to cx and cy and set orient to auto return this.put(new SVG.Marker) .size(width, height) .ref(width / 2, height / 2) .viewbox(0, 0, width, height) .attr('orient', 'auto') .update(block) } }) SVG.extend(SVG.Line, SVG.Polyline, SVG.Polygon, SVG.Path, { // Create and attach markers marker: function(marker, width, height, block) { var attr = ['marker'] // Build attribute name if (marker != 'all') attr.push(marker) attr = attr.join('-') // Set marker attribute marker = arguments[1] instanceof SVG.Marker ? arguments[1] : this.doc().marker(width, height, block) return this.attr(attr, marker) } }) // Define list of available attributes for stroke and fill var sugar = { stroke: ['color', 'width', 'opacity', 'linecap', 'linejoin', 'miterlimit', 'dasharray', 'dashoffset'] , fill: ['color', 'opacity', 'rule'] , prefix: function(t, a) { return a == 'color' ? t : t + '-' + a } } // Add sugar for fill and stroke ;['fill', 'stroke'].forEach(function(m) { var i, extension = {} extension[m] = function(o) { if (typeof o == 'string' || SVG.Color.isRgb(o) || (o && typeof o.fill === 'function')) this.attr(m, o) else // set all attributes from sugar.fill and sugar.stroke list for (i = sugar[m].length - 1; i >= 0; i--) if (o[sugar[m][i]] != null) this.attr(sugar.prefix(m, sugar[m][i]), o[sugar[m][i]]) return this } SVG.extend(SVG.Element, SVG.FX, extension) }) SVG.extend(SVG.Element, SVG.FX, { // Map rotation to transform rotate: function(d, cx, cy) { return this.transform({ rotation: d, cx: cx, cy: cy }) } // Map skew to transform , skew: function(x, y, cx, cy) { return this.transform({ skewX: x, skewY: y, cx: cx, cy: cy }) } // Map scale to transform , scale: function(x, y, cx, cy) { return arguments.length == 1 || arguments.length == 3 ? this.transform({ scale: x, cx: y, cy: cx }) : this.transform({ scaleX: x, scaleY: y, cx: cx, cy: cy }) } // Map translate to transform , translate: function(x, y) { return this.transform({ x: x, y: y }) } // Map flip to transform , flip: function(a, o) { return this.transform({ flip: a, offset: o }) } // Map matrix to transform , matrix: function(m) { return this.attr('transform', new SVG.Matrix(m)) } // Opacity , opacity: function(value) { return this.attr('opacity', value) } // Relative move over x axis , dx: function(x) { return this.x((this instanceof SVG.FX ? 0 : this.x()) + x, true) } // Relative move over y axis , dy: function(y) { return this.y((this instanceof SVG.FX ? 0 : this.y()) + y, true) } // Relative move over x and y axes , dmove: function(x, y) { return this.dx(x).dy(y) } }) SVG.extend(SVG.Rect, SVG.Ellipse, SVG.Circle, SVG.Gradient, SVG.FX, { // Add x and y radius radius: function(x, y) { var type = (this.target || this).type; return type == 'radial' || type == 'circle' ? this.attr({ 'r': new SVG.Number(x) }) : this.rx(x).ry(y == null ? x : y) } }) SVG.extend(SVG.Path, { // Get path length length: function() { return this.node.getTotalLength() } // Get point at length , pointAt: function(length) { return this.node.getPointAtLength(length) } }) SVG.extend(SVG.Parent, SVG.Text, SVG.FX, { // Set font font: function(o) { for (var k in o) k == 'leading' ? this.leading(o[k]) : k == 'anchor' ? this.attr('text-anchor', o[k]) : k == 'size' || k == 'family' || k == 'weight' || k == 'stretch' || k == 'variant' || k == 'style' ? this.attr('font-'+ k, o[k]) : this.attr(k, o[k]) return this } }) SVG.Set = SVG.invent({ // Initialize create: function(members) { // Set initial state Array.isArray(members) ? this.members = members : this.clear() } // Add class methods , extend: { // Add element to set add: function() { var i, il, elements = [].slice.call(arguments) for (i = 0, il = elements.length; i < il; i++) this.members.push(elements[i]) return this } // Remove element from set , remove: function(element) { var i = this.index(element) // remove given child if (i > -1) this.members.splice(i, 1) return this } // Iterate over all members , each: function(block) { for (var i = 0, il = this.members.length; i < il; i++) block.apply(this.members[i], [i, this.members]) return this } // Restore to defaults , clear: function() { // initialize store this.members = [] return this } // Get the length of a set , length: function() { return this.members.length } // Checks if a given element is present in set , has: function(element) { return this.index(element) >= 0 } // retuns index of given element in set , index: function(element) { return this.members.indexOf(element) } // Get member at given index , get: function(i) { return this.members[i] } // Get first member , first: function() { return this.get(0) } // Get last member , last: function() { return this.get(this.members.length - 1) } // Default value , valueOf: function() { return this.members } // Get the bounding box of all members included or empty box if set has no items , bbox: function(){ var box = new SVG.BBox() // return an empty box of there are no members if (this.members.length == 0) return box // get the first rbox and update the target bbox var rbox = this.members[0].rbox() box.x = rbox.x box.y = rbox.y box.width = rbox.width box.height = rbox.height this.each(function() { // user rbox for correct position and visual representation box = box.merge(this.rbox()) }) return box } } // Add parent method , construct: { // Create a new set set: function(members) { return new SVG.Set(members) } } }) SVG.FX.Set = SVG.invent({ // Initialize node create: function(set) { // store reference to set this.set = set } }) // Alias methods SVG.Set.inherit = function() { var m , methods = [] // gather shape methods for(var m in SVG.Shape.prototype) if (typeof SVG.Shape.prototype[m] == 'function' && typeof SVG.Set.prototype[m] != 'function') methods.push(m) // apply shape aliasses methods.forEach(function(method) { SVG.Set.prototype[method] = function() { for (var i = 0, il = this.members.length; i < il; i++) if (this.members[i] && typeof this.members[i][method] == 'function') this.members[i][method].apply(this.members[i], arguments) return method == 'animate' ? (this.fx || (this.fx = new SVG.FX.Set(this))) : this } }) // clear methods for the next round methods = [] // gather fx methods for(var m in SVG.FX.prototype) if (typeof SVG.FX.prototype[m] == 'function' && typeof SVG.FX.Set.prototype[m] != 'function') methods.push(m) // apply fx aliasses methods.forEach(function(method) { SVG.FX.Set.prototype[method] = function() { for (var i = 0, il = this.set.members.length; i < il; i++) this.set.members[i].fx[method].apply(this.set.members[i].fx, arguments) return this } }) } SVG.extend(SVG.Element, { // Store data values on svg nodes data: function(a, v, r) { if (typeof a == 'object') { for (v in a) this.data(v, a[v]) } else if (arguments.length < 2) { try { return JSON.parse(this.attr('data-' + a)) } catch(e) { return this.attr('data-' + a) } } else { this.attr( 'data-' + a , v === null ? null : r === true || typeof v === 'string' || typeof v === 'number' ? v : JSON.stringify(v) ) } return this } }) SVG.extend(SVG.Element, { // Remember arbitrary data remember: function(k, v) { // remember every item in an object individually if (typeof arguments[0] == 'object') for (var v in k) this.remember(v, k[v]) // retrieve memory else if (arguments.length == 1) return this.memory()[k] // store memory else this.memory()[k] = v return this } // Erase a given memory , forget: function() { if (arguments.length == 0) this._memory = {} else for (var i = arguments.length - 1; i >= 0; i--) delete this.memory()[arguments[i]] return this } // Initialize or return local memory object , memory: function() { return this._memory || (this._memory = {}) } }) // Method for getting an element by id SVG.get = function(id) { var node = document.getElementById(idFromReference(id) || id) return SVG.adopt(node) } // Select elements by query string SVG.select = function(query, parent) { return new SVG.Set( SVG.utils.map((parent || document).querySelectorAll(query), function(node) { return SVG.adopt(node) }) ) } SVG.extend(SVG.Parent, { // Scoped select method select: function(query) { return SVG.select(query, this.node) } }) function is(el, obj){ return el instanceof obj } // tests if a given selector matches an element function matches(el, selector) { return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); } // Convert dash-separated-string to camelCase function camelCase(s) { return s.toLowerCase().replace(/-(.)/g, function(m, g) { return g.toUpperCase() }) } // Capitalize first letter of a string function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1) } // Ensure to six-based hex function fullHex(hex) { return hex.length == 4 ? [ '#', hex.substring(1, 2), hex.substring(1, 2) , hex.substring(2, 3), hex.substring(2, 3) , hex.substring(3, 4), hex.substring(3, 4) ].join('') : hex } // Component to hex value function compToHex(comp) { var hex = comp.toString(16) return hex.length == 1 ? '0' + hex : hex } // Calculate proportional width and height values when necessary function proportionalSize(box, width, height) { if (height == null) height = box.height / box.width * width else if (width == null) width = box.width / box.height * height return { width: width , height: height } } // Delta transform point function deltaTransformPoint(matrix, x, y) { return { x: x * matrix.a + y * matrix.c + 0 , y: x * matrix.b + y * matrix.d + 0 } } // Map matrix array to object function arrayToMatrix(a) { return { a: a[0], b: a[1], c: a[2], d: a[3], e: a[4], f: a[5] } } // Parse matrix if required function parseMatrix(matrix) { if (!(matrix instanceof SVG.Matrix)) matrix = new SVG.Matrix(matrix) return matrix } // Add centre point to transform object function ensureCentre(o, target) { o.cx = o.cx == null ? target.bbox().cx : o.cx o.cy = o.cy == null ? target.bbox().cy : o.cy } // Convert string to matrix function stringToMatrix(source) { // remove matrix wrapper and split to individual numbers source = source .replace(SVG.regex.whitespace, '') .replace(SVG.regex.matrix, '') .split(SVG.regex.matrixElements) // convert string values to floats and convert to a matrix-formatted object return arrayToMatrix( SVG.utils.map(source, function(n) { return parseFloat(n) }) ) } // Calculate position according to from and to function at(o, pos) { // number recalculation (don't bother converting to SVG.Number for performance reasons) return typeof o.from == 'number' ? o.from + (o.to - o.from) * pos : // instance recalculation o instanceof SVG.Color || o instanceof SVG.Number || o instanceof SVG.Matrix ? o.at(pos) : // for all other values wait until pos has reached 1 to return the final value pos < 1 ? o.from : o.to } // PathArray Helpers function arrayToString(a) { for (var i = 0, il = a.length, s = ''; i < il; i++) { s += a[i][0] if (a[i][1] != null) { s += a[i][1] if (a[i][2] != null) { s += ' ' s += a[i][2] if (a[i][3] != null) { s += ' ' s += a[i][3] s += ' ' s += a[i][4] if (a[i][5] != null) { s += ' ' s += a[i][5] s += ' ' s += a[i][6] if (a[i][7] != null) { s += ' ' s += a[i][7] } } } } } } return s + ' ' } // Deep new id assignment function assignNewId(node) { // do the same for SVG child nodes as well for (var i = node.childNodes.length - 1; i >= 0; i--) if (node.childNodes[i] instanceof SVGElement) assignNewId(node.childNodes[i]) return SVG.adopt(node).id(SVG.eid(node.nodeName)) } // Add more bounding box properties function fullBox(b) { if (b.x == null) { b.x = 0 b.y = 0 b.width = 0 b.height = 0 } b.w = b.width b.h = b.height b.x2 = b.x + b.width b.y2 = b.y + b.height b.cx = b.x + b.width / 2 b.cy = b.y + b.height / 2 return b } // Get id from reference string function idFromReference(url) { var m = url.toString().match(SVG.regex.reference) if (m) return m[1] } // Create matrix array for looping var abcdef = 'abcdef'.split('') // Add CustomEvent to IE9 and IE10 if (typeof CustomEvent !== 'function') { // Code from: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent var CustomEvent = function(event, options) { options = options || { bubbles: false, cancelable: false, detail: undefined } var e = document.createEvent('CustomEvent') e.initCustomEvent(event, options.bubbles, options.cancelable, options.detail) return e } CustomEvent.prototype = window.Event.prototype window.CustomEvent = CustomEvent } // requestAnimationFrame / cancelAnimationFrame Polyfill with fallback based on Paul Irish (function(w) { var lastTime = 0 var vendors = ['moz', 'webkit'] for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { w.requestAnimationFrame = w[vendors[x] + 'RequestAnimationFrame'] w.cancelAnimationFrame = w[vendors[x] + 'CancelAnimationFrame'] || w[vendors[x] + 'CancelRequestAnimationFrame'] } w.requestAnimationFrame = w.requestAnimationFrame || function(callback) { var currTime = new Date().getTime() var timeToCall = Math.max(0, 16 - (currTime - lastTime)) var id = w.setTimeout(function() { callback(currTime + timeToCall) }, timeToCall) lastTime = currTime + timeToCall return id } w.cancelAnimationFrame = w.cancelAnimationFrame || w.clearTimeout; }(window)) return SVG }));