@frangoteam/fuxa
Version:
Web-based Process Visualization (SCADA/HMI/Dashboard) software
1,798 lines (1,488 loc) • 148 kB
JavaScript
/*!
* svg.js - A lightweight library for manipulating and animating SVG.
* @version 2.6.4
* https://svgdotjs.github.io/
*
* @copyright Wout Fierens <wout@mick-wout.com>
* @license MIT
*
* BUILT: Sat Apr 21 2018 16:30:04 GMT-0400 (EDT)
*/;
(function(root, factory) {
/* istanbul ignore next */
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.draw)
SVG.prepare()
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 window.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() {
// Select document body and create invisible svg element
var body = document.getElementsByTagName('body')[0]
, draw = (body ? new SVG.Doc(body) : SVG.adopt(document.documentElement).nested()).size(2, 0)
// Create parser object
SVG.parser = {
body: body || document.documentElement
, draw: draw.style('opacity:0;position:absolute;left:-100%;top:-100%;overflow:hidden').node
, poly: draw.polyline().node
, path: draw.path().node
, native: SVG.create('svg')
}
}
SVG.parser = {
native: SVG.create('svg')
}
document.addEventListener('DOMContentLoaded', function() {
if(!SVG.parser.draw)
SVG.prepare()
}, false)
// 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
// splits a transformation chain
, transforms: /\)\s*,?\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
// split at whitespace and comma
, delimiter: /[\s,]+/
// The following regex are used to parse the d attribute of a path
// Matches all hyphens which are not after an exponent
, hyphen: /([^e])\-/gi
// Replaces and tests for all path letters
, pathLetters: /[MLHVCSQTAZ]/gi
// yes we need this one, too
, isPathLetter: /[MLHVCSQTAZ]/i
// matches 0.154.23.45
, numbersWithDots: /((\d?\.\d+(?:e[+-]?\d+)?)((?:\.\d+(?:e[+-]?\d+)?)+))+/gi
// matches .
, dots: /\./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
}
// Filter function
, filter: function(array, block) {
var i
, il = array.length
, result = []
for (i = 0; i < il; i++)
if (block(array[i]))
result.push(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(nodes) {
return this.filter( nodes, function(el) { return el instanceof window.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
if(!color) return
// parse color
if (typeof color === 'string') {
if (SVG.regex.isRgb.test(color)) {
// get rgb values
match = SVG.regex.rgb.exec(color.replace(SVG.regex.whitespace,''))
// 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(SVG.regex.delimiter).map(parseFloat)
}
// Reverse array
, reverse: function() {
this.value.reverse()
return this
}
, clone: function() {
var clone = new this.constructor()
clone.value = array_clone(this.value)
return clone
}
})
// Poly points array
SVG.PointArray = function(array, fallback) {
SVG.Array.call(this, array, fallback || [[0,0]])
}
// Inherit from SVG.Array
SVG.PointArray.prototype = new SVG.Array
SVG.PointArray.prototype.constructor = SVG.PointArray
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 and flat array
, parse: function(array) {
var points = []
array = array.valueOf()
// if it is an array
if (Array.isArray(array)) {
// and it is not flat, there is no need to parse it
if(Array.isArray(array[0])) {
return array
}
} else { // Else, it is considered as a string
// parse points
array = array.trim().split(SVG.regex.delimiter).map(parseFloat)
}
// validate points - https://svgwg.org/svg2-draft/shapes.html#DataTypePoints
// Odd number of coordinates is an error. In such cases, drop the last odd coordinate.
if (array.length % 2 !== 0) array.pop()
// wrap points in two-tuples and parse points as floats
for(var i = 0, len = array.length; i < len; i = i + 2)
points.push([ array[i], array[i+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--) {
if(box.width) this.value[i][0] = ((this.value[i][0] - box.x) * width) / box.width + box.x
if(box.height) 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()
}
})
var pathHandlers = {
M: function(c, p, p0) {
p.x = p0.x = c[0]
p.y = p0.y = c[1]
return ['M', p.x, p.y]
},
L: function(c, p) {
p.x = c[0]
p.y = c[1]
return ['L', c[0], c[1]]
},
H: function(c, p) {
p.x = c[0]
return ['H', c[0]]
},
V: function(c, p) {
p.y = c[0]
return ['V', c[0]]
},
C: function(c, p) {
p.x = c[4]
p.y = c[5]
return ['C', c[0], c[1], c[2], c[3], c[4], c[5]]
},
S: function(c, p) {
p.x = c[2]
p.y = c[3]
return ['S', c[0], c[1], c[2], c[3]]
},
Q: function(c, p) {
p.x = c[2]
p.y = c[3]
return ['Q', c[0], c[1], c[2], c[3]]
},
T: function(c, p) {
p.x = c[0]
p.y = c[1]
return ['T', c[0], c[1]]
},
Z: function(c, p, p0) {
p.x = p0.x
p.y = p0.y
return ['Z']
},
A: function(c, p) {
p.x = c[5]
p.y = c[6]
return ['A', c[0], c[1], c[2], c[3], c[4], c[5], c[6]]
}
}
var mlhvqtcsa = 'mlhvqtcsaz'.split('')
for(var i = 0, il = mlhvqtcsa.length; i < il; ++i){
pathHandlers[mlhvqtcsa[i]] = (function(i){
return function(c, p, p0) {
if(i == 'H') c[0] = c[0] + p.x
else if(i == 'V') c[0] = c[0] + p.y
else if(i == 'A'){
c[5] = c[5] + p.x,
c[6] = c[6] + p.y
}
else
for(var j = 0, jl = c.length; j < jl; ++j) {
c[j] = c[j] + (j%2 ? p.y : p.x)
}
return pathHandlers[i](c, p, p0)
}
})(mlhvqtcsa[i].toUpperCase())
}
// Path points array
SVG.PathArray = function(array, fallback) {
SVG.Array.call(this, array, fallback || [['M', 0, 0]])
}
// Inherit from SVG.Array
SVG.PathArray.prototype = new SVG.Array
SVG.PathArray.prototype.constructor = SVG.PathArray
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
}
// Test if the passed path array use the same path data commands as this path array
, equalCommands: function(pathArray) {
var i, il, equalCommands
pathArray = new SVG.PathArray(pathArray)
equalCommands = this.value.length === pathArray.value.length
for(i = 0, il = this.value.length; equalCommands && i < il; i++) {
equalCommands = this.value[i][0] === pathArray.value[i][0]
}
return equalCommands
}
// Make path array morphable
, morph: function(pathArray) {
pathArray = new SVG.PathArray(pathArray)
if(this.equalCommands(pathArray)) {
this.destination = pathArray
} else {
this.destination = null
}
return this
}
// Get morphed path array at given position
, at: function(pos) {
// make sure a destination is defined
if (!this.destination) return this
var sourceArray = this.value
, destinationArray = this.destination.value
, array = [], pathArray = new SVG.PathArray()
, i, il, j, jl
// Animate has specified in the SVG spec
// See: https://www.w3.org/TR/SVG11/paths.html#PathElement
for (i = 0, il = sourceArray.length; i < il; i++) {
array[i] = [sourceArray[i][0]]
for(j = 1, jl = sourceArray[i].length; j < jl; j++) {
array[i][j] = sourceArray[i][j] + (destinationArray[i][j] - sourceArray[i][j]) * pos
}
// For the two flags of the elliptical arc command, the SVG spec say:
// Flags and booleans are interpolated as fractions between zero and one, with any non-zero value considered to be a value of one/true
// Elliptical arc command as an array followed by corresponding indexes:
// ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
// 0 1 2 3 4 5 6 7
if(array[i][0] === 'A') {
array[i][4] = +(array[i][4] != 0)
array[i][5] = +(array[i][5] != 0)
}
}
// Directly modify the value of a path array, this is done this way for performance
pathArray.value = array
return pathArray
}
// 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, 'Z':0 }
if(typeof array == 'string'){
array = array
.replace(SVG.regex.numbersWithDots, pathRegReplace) // convert 45.123.123 to 45.123 .123
.replace(SVG.regex.pathLetters, ' $& ') // put some room between letters and numbers
.replace(SVG.regex.hyphen, '$1 -') // add space before hyphen
.trim() // trim
.split(SVG.regex.delimiter) // split into array
}else{
array = array.reduce(function(prev, curr){
return [].concat.call(prev, curr)
}, [])
}
// array now is an array containing all parts of a path e.g. ['M', '0', '0', 'L', '30', '30' ...]
var arr = []
, p = new SVG.Point()
, p0 = new SVG.Point()
, index = 0
, len = array.length
do{
// Test if we have a path letter
if(SVG.regex.isPathLetter.test(array[index])){
s = array[index]
++index
// 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'
}
arr.push(pathHandlers[s].call(null,
array.slice(index, (index = index + paramCnt[s.toUpperCase()])).map(parseFloat),
p, p0
)
)
}while(len > index)
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) {
number = new SVG.Number(number)
return new SVG.Number(this + number, this.unit || number.unit)
}
// Subtract number
, minus: function(number) {
number = new SVG.Number(number)
return new SVG.Number(this - number, this.unit || number.unit)
}
// Multiply number
, times: function(number) {
number = new SVG.Number(number)
return new SVG.Number(this * number, this.unit || number.unit)
}
// Divide number
, divide: function(number) {
number = new SVG.Number(number)
return new SVG.Number(this / number, this.unit || number.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)
if(number.relative) {
this.destination.value += this.value
}
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.Element = SVG.invent({
// Initialize node
create: function(node) {
// make stroke value accessible dynamically
this._stroke = SVG.defaults.attrs.stroke
this._event = null
// 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, width, height)
return this
.width(new SVG.Number(p.width))
.height(new SVG.Number(p.height))
}
// Clone element
, clone: function(parent, withData) {
// write dom data to the dom so the clone can pickup the data
this.writeDataToDom()
// clone element and assign new id
var clone = assignNewId(this.node.cloneNode(true))
// insert the clone in the given parent or after myself
if(parent) parent.add(clone)
else 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(SVG.regex.delimiter)
}
// 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 window.SVGElement){
if(typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent
if(parent.node.parentNode.nodeName == '#document') return null // #720
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>' + svg.replace(/\n/, '').replace(/<([\w:-]+)([^<]+?)\/>/g, '<$1$2></$1>') + '</svg>'
// 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(/^<svg>/, '').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}
}
SVG.morph = function(pos){
return function(from, to) {
return new SVG.MorphObj(from, to).at(pos)
}
}
SVG.Situation = SVG.invent({
create: function(o){
this.init = false
this.reversed = false
this.reversing = false
this.duration = new SVG.Number(o.duration).valueOf()
this.delay = new SVG.Number(o.delay).valueOf()
this.start = +new Date() + this.delay
this.finish = this.start + this.duration
this.ease = o.ease
// this.loop is incremented from 0 to this.loops
// it is also incremented when in an infinite loop (when this.loops is true)
this.loop = 0
this.loops = false
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 as transformation objects
// e.g. [SVG.Rotate, SVG.Translate, SVG.Matrix]
]
this.once = {
// functions to fire at a specific position
// e.g. "0.5": function foo(){}
}
}
})
SVG.FX = SVG.invent({
create: function(element) {
this._target = element
this.situations = []
this.active = false
this.situation = null
this.paused = false
this.lastPos = 0
this.pos = 0
// The absolute position of an animation is its position in the context of its complete duration (including delay and loops)
// When performing a delay, absPos is below 0 and when performing a loop, its value is above 1
this.absPos = 0
this._speed = 1
}
, 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 ease 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, ease, delay){
if(typeof o == 'object'){
ease = o.ease
delay = o.delay
o = o.duration
}
var situation = new SVG.Situation({
duration: o || 1000,
delay: delay || 0,
ease: SVG.easing[ease || '-'] || ease
})
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()
*/
, delay: function(delay){
// The delay is performed by an empty situation with its duration
// attribute set to the duration of the delay
var situation = new SVG.Situation({
duration: delay,
delay: 0,
ease: SVG.easing['-']
})
return this.queue(situation)
}
/**
* sets or returns the target of this animation
* @param null || target SVG.Element 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 absolute position at a given time
, timeToAbsPos: function(timestamp){
return (timestamp - this.situation.start) / (this.situation.duration/this._speed)
}
// returns the timestamp from a given absolute positon
, absPosToTime: function(absPos){
return this.situation.duration/this._speed * absPos + this.situation.start
}
// starts the animationloop
, startAnimFrame: function(){
this.stopAnimFrame()
this.animationFrame = window.requestAnimationFrame(function(){ this.step() }.bind(this))
}
// cancels the animationframe
, stopAnimFrame: function(){
window.cancelAnimationFrame(this.animationFrame)
}
// kicks off the animation - only does something when the queue is currently not active and at least one situation is set
, start: function(){
// dont start if already started
if(!this.active && this.situation){
this.active = true
this.startCurrent()
}
return this
}
// start the current situation
, startCurrent: function(){
this.situation.start = +new Date + this.situation.delay/this._speed
this.situation.finish = this.situation.start + this.situation.duration/this._speed
return this.initAnimations().step()
}
/**
* 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)
this.situations.push(fn)
if(!this.situation) this.situation = this.situations.shift()
return this
}
/**
* pulls next element from the queue and execute it
* @return this
*/
, dequeue: function(){
// stop current animation
this.stop()
// get next animation from queue
this.situation = this.situations.shift()
if(this.situation){
if(this.situation instanceof SVG.Situation) {
this.start()
} else {
// If it is not a SVG.Situation, then it is a function, we execute it
this.situation.call(this)
}
}
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, j, source
var s = this.situation
if(s.init) return this
for(i in s.animations){
source = this.target()[i]()
if(!Array.isArray(source)) {
source = [source]
}
if(!Array.isArray(s.animations[i])) {
s.animations[i] = [s.animations[i]]
}
//if(s.animations[i].length > source.length) {
// source.concat = source.concat(s.animations[i].slice(source.length, s.animations[i].length))
//}
for(j = source.length; j--;) {
// The condition is because some methods return a normal number instead
// of a SVG.Number
if(s.animations[i][j] instanceof SVG.Number)
source[j] = new SVG.Number(source[j])
s.animations[i][j] = source[j].morph(s.animations[i][j])
}
}
for(i in s.attrs){
s.attrs[i] = new SVG.MorphObj(this.target().attr(i), s.attrs[i])
}
for(i in s.styles){
s.styles[i] = new SVG.MorphObj(this.target().style(i), s.styles[i])
}
s.initialTransformation = this.target().matrixify()
s.init = true
return this
}
, clearQueue: function(){
this.situations = []
return this
}
, clearCurrent: function(){
this.situation = 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){
var active = this.active
this.active = false
if(clearQueue){
this.clearQueue()
}
if(jumpToEnd && this.situation){
// initialize the situation if it was not
!active && this.startCurrent()
this.atEnd()
}
this.stopAnimFrame()
return this.clearCurrent()
}
/** resets the element to the state where the current element has started
* @return this
*/
, reset: function(){
if(this.situation){
var temp = this.situation
this.stop()
this.situation = temp
this.atStart()
}
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().situation && this.stop(true, false));
this.clearQueue().clearCurrent()
return this
}
// set the internal animation pointer at the start position, before any loops, and updates the visualisation
, atStart: function() {
return this.at(0, true)
}
// set the internal animation pointer at the end position, after all the loops, and updates the visualisation
, atEnd: function() {
if (this.situation.loops === true) {
// If in a infinite loop, we end the current iteration
this.situation.loops = this.situation.loop + 1
}
if(typeof this.situation.loops == 'number') {
// If performing a finite number of loops, we go after all the loops
return this.at(this.situation.loops, true)
} else {
// If no loops, we just go at the end
return this.at(1, true)
}
}
// set the internal animation pointer to the specified position and updates the visualisation
// if isAbsPos is true, pos is treated as an absolute position
, at: function(pos, isAbsPos){
var durDivSpd = this.situation.duration/this._speed
this.absPos = pos
// If pos is not an absolute position, we convert it into one
if (!isAbsPos) {
if (this.situation.reversed) this.absPos = 1 - this.absPos
this.absPos += this.situation.loop
}
this.situation.start = +new Date - this.absPos * durDivSpd
this.situation.finish = this.situation.start + durDivSpd
return this.step(true)
}
/**
* sets or returns the speed of the animations
* @param speed null || Number The new speed of the animations
* @return Number || this
*/
, speed: function(speed){
if (speed === 0) return this.pause()
if (speed) {
this._speed = speed
// We use an absolute position here so that speed can affect the delay before the animation
return this.at(this.absPos, true)
} else return this._speed
}
// Make loopable
, loop: function(times, reverse) {
var c = this.last()
// store total loops
c.loops = (times != null) ? times : true
c.loop = 0
if(reverse) c.reversing = true
return this
}
// pauses the animation
, pause: function(){
this.paused = true
this.stopAnimFrame()
return this
}
// unpause the animation
, play: function(){
if(!this.paused) return this
this.paused = false
// We use an absolute position here so that the delay before the animation can be paused
return this.at(this.absPos, true)
}
/**
* 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.situation.ease(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._callStart()
}
// 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, SVG.morph(e.detail.pos), e.detail.eased, c)
}
}
// see above
this.target().off('during.fx', wrapper).on('during.fx', wrapper)
this.after(function(){
this.off('during.fx', wrapper)
})
return this._callStart()
}
// 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._callStart()
}
// calls on every animation step for all animations
, duringAll: function(fn){
var wrapper = function(e){
fn.call(this, e.detail.pos, SVG.morph(e.detail.pos), e.detail.eased, e.detail.situation)
}
this.target().off('during.fx', wrapper).on('during.fx', wrapper)
this.afterAll(function(){
this.off('during.fx', wrapper)
})
return this._callStart()
}
, last: function(){
return this.situations.length ? this.situations[this.situations.length-1] : this.situation
}
// adds one property to the animations
, add: function(method, args, type){
this.last()[type || 'animations'][method] = args
return this._callStart()
}
/** 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 an absolute position
if(!ignoreTime) this.absPos = this.timeToAbsPos(+new Date)
// This part convert a