SVG.FX = SVG.invent({ // Initialize FX object create: function(element) { // store target element this.target = element } // Add class methods , extend: { // Add animation parameters and start animation animate: function(d, ease, delay) { var akeys, skeys, key , element = this.target , fx = this // dissect object if one is passed if (typeof d == 'object') { delay = d.delay ease = d.ease d = d.duration } // ensure default duration and easing d = d == '=' ? d : d == null ? 1000 : new SVG.Number(d).valueOf() ease = ease || '<>' // process values fx.at = function(pos) { var i // normalise pos pos = pos < 0 ? 0 : pos > 1 ? 1 : pos // collect attribute keys if (akeys == null) { akeys = [] for (key in fx.attrs) akeys.push(key) // make sure morphable elements are scaled, translated and morphed all together if (element.morphArray && (fx.destination.plot || akeys.indexOf('points') > -1)) { // get destination var box , p = new element.morphArray(fx.destination.plot || fx.attrs.points || element.array) // add size if (fx.destination.size) p.size(fx.destination.size.width.to, fx.destination.size.height.to) // add movement box = p.bbox() if (fx.destination.x) p.move(fx.destination.x.to, box.y) else if (fx.destination.cx) p.move(fx.destination.cx.to - box.width / 2, box.y) box = p.bbox() if (fx.destination.y) p.move(box.x, fx.destination.y.to) else if (fx.destination.cy) p.move(box.x, fx.destination.cy.to - box.height / 2) // reset destination values fx.destination = { plot: element.array.morph(p) } } } // collect style keys if (skeys == null) { skeys = [] for (key in fx.styles) skeys.push(key) } // apply easing pos = ease == '<>' ? (-Math.cos(pos * Math.PI) / 2) + 0.5 : ease == '>' ? Math.sin(pos * Math.PI / 2) : ease == '<' ? -Math.cos(pos * Math.PI / 2) + 1 : ease == '-' ? pos : typeof ease == 'function' ? ease(pos) : pos // run plot function if (fx.destination.plot) { element.plot(fx.destination.plot.at(pos)) } else { // run all x-position properties if (fx.destination.x) element.x(fx.destination.x.at(pos)) else if (fx.destination.cx) element.cx(fx.destination.cx.at(pos)) // run all y-position properties if (fx.destination.y) element.y(fx.destination.y.at(pos)) else if (fx.destination.cy) element.cy(fx.destination.cy.at(pos)) // run all size properties if (fx.destination.size) element.size(fx.destination.size.width.at(pos), fx.destination.size.height.at(pos)) } // run all viewbox properties if (fx.destination.viewbox) element.viewbox( fx.destination.viewbox.x.at(pos) , fx.destination.viewbox.y.at(pos) , fx.destination.viewbox.width.at(pos) , fx.destination.viewbox.height.at(pos) ) // run leading property if (fx.destination.leading) element.leading(fx.destination.leading.at(pos)) // animate attributes for (i = akeys.length - 1; i >= 0; i--) element.attr(akeys[i], at(fx.attrs[akeys[i]], pos)) // animate styles for (i = skeys.length - 1; i >= 0; i--) element.style(skeys[i], at(fx.styles[skeys[i]], pos)) // callback for each keyframe if (fx.situation.during) fx.situation.during.call(element, pos, function(from, to) { return at({ from: from, to: to }, pos) }) } if (typeof d === 'number') { // delay animation this.timeout = setTimeout(function() { var start = new Date().getTime() // initialize situation object fx.situation.start = start fx.situation.play = true fx.situation.finish = start + d fx.situation.duration = d fx.situation.ease = ease // render function fx.render = function() { if (fx.situation.play === true) { // calculate pos var time = new Date().getTime() , pos = time > fx.situation.finish ? 1 : (time - fx.situation.start) / d // reverse pos if animation is reversed if (fx.situation.reversing) pos = -pos + 1 // process values fx.at(pos) // finish off animation if (time > fx.situation.finish) { if (fx.destination.plot) element.plot(new SVG.PointArray(fx.destination.plot.destination).settle()) if (fx.situation.loop === true || (typeof fx.situation.loop == 'number' && fx.situation.loop > 0)) { // register reverse if (fx.situation.reverse) fx.situation.reversing = !fx.situation.reversing if (typeof fx.situation.loop == 'number') { // reduce loop count if (!fx.situation.reverse || fx.situation.reversing) --fx.situation.loop // remove last loop if reverse is disabled if (!fx.situation.reverse && fx.situation.loop == 1) --fx.situation.loop } fx.animate(d, ease, delay) } else { fx.situation.after ? fx.situation.after.apply(element, [fx]) : fx.stop() } } else { fx.animationFrame = requestAnimationFrame(fx.render) } } else { fx.animationFrame = requestAnimationFrame(fx.render) } } // start animation fx.render() }, new SVG.Number(delay).valueOf()) } return this } // Get bounding box of target element , bbox: function() { return this.target.bbox() } // Add animatable attributes , attr: function(a, v) { // apply attributes individually if (typeof a == 'object') { for (var key in a) this.attr(key, a[key]) } else { // get the current state var from = this.target.attr(a) // detect format if (a == 'transform') { // merge given transformation with an existing one if (this.attrs[a]) v = this.attrs[a].destination.multiply(v) // prepare matrix for morphing this.attrs[a] = this.target.ctm().morph(v) // add parametric rotation values if (this.param) { // get initial rotation v = this.target.transform('rotation') // add param this.attrs[a].param = { from: this.target.param || { rotation: v, cx: this.param.cx, cy: this.param.cy } , to: this.param } } } else { this.attrs[a] = SVG.Color.isColor(v) ? // prepare color for morphing new SVG.Color(from).morph(v) : SVG.regex.unit.test(v) ? // prepare number for morphing new SVG.Number(from).morph(v) : // prepare for plain morphing { from: from, to: v } } } 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.styles[s] = { from: this.target.style(s), to: v } return this } // Animatable x-axis , x: function(x) { this.destination.x = new SVG.Number(this.target.x()).morph(x) return this } // Animatable y-axis , y: function(y) { this.destination.y = new SVG.Number(this.target.y()).morph(y) return this } // Animatable center x-axis , cx: function(x) { this.destination.cx = new SVG.Number(this.target.cx()).morph(x) return this } // Animatable center y-axis , cy: function(y) { this.destination.cy = new SVG.Number(this.target.cy()).morph(y) return this } // 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 = this.target.bbox() this.destination.size = { width: new SVG.Number(box.width).morph(width) , height: new SVG.Number(box.height).morph(height) } } return this } // Add animatable plot , plot: function(p) { this.destination.plot = p return this } // Add leading method , leading: function(value) { if (this.target.destination.leading) this.destination.leading = new SVG.Number(this.target.destination.leading).morph(value) return this } // Add animatable viewbox , viewbox: function(x, y, width, height) { if (this.target instanceof SVG.Container) { var box = this.target.viewbox() this.destination.viewbox = { x: new SVG.Number(box.x).morph(x) , y: new SVG.Number(box.y).morph(y) , width: new SVG.Number(box.width).morph(width) , height: new SVG.Number(box.height).morph(height) } } return this } // Add animateable gradient update , update: function(o) { if (this.target instanceof SVG.Stop) { 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 } // Add callback for each keyframe , during: function(during) { this.situation.during = during return this } // Callback after animation , after: function(after) { this.situation.after = after return this } // Make loopable , loop: function(times, reverse) { // store current loop and total loops this.situation.loop = this.situation.loops = times || true // make reversable this.situation.reverse = !!reverse return this } // Stop running animation , stop: function(fulfill) { // fulfill animation if (fulfill === true) { this.animate(0) if (this.situation.after) this.situation.after.apply(this.target, [this]) } else { // stop current animation clearTimeout(this.timeout) cancelAnimationFrame(this.animationFrame); // reset storage for properties this.attrs = {} this.styles = {} this.situation = {} this.destination = {} } return this } // Pause running animation , pause: function() { if (this.situation.play === true) { this.situation.play = false this.situation.pause = new Date().getTime() } return this } // Play running animation , play: function() { if (this.situation.play === false) { var pause = new Date().getTime() - this.situation.pause this.situation.finish += pause this.situation.start += pause this.situation.play = true } return this } } // Define parent class , 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(d, ease, delay) { return (this.fx || (this.fx = new SVG.FX(this))).stop().animate(d, ease, delay) } // Stop current animation; this is an alias to the fx instance , stop: function(fulfill) { if (this.fx) this.fx.stop(fulfill) 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 } } })