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(from, to) { return new 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 = false 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 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 = new SVG.Number(delay).valueOf() } 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 } , 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() */ // 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.situation.start) / (this.situation.duration) } // returns the timestamp from a given positon , posToTime: function(pos){ return this.situation.duration * pos + this.situation.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.situation){ this.situation.start = +new Date + this.situation.delay this.situation.finish = this.situation.start + this.situation.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.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.situation && this.situation.stop && this.situation.stop() // get next animation from queue this.situation = this.situations.shift() if(this.situation){ var fn = function(){ if(this.situation instanceof SVG.Situation) this.initAnimations().at(0) else if(this.situation instanceof SVG.Delay) this.dequeue() else this.situation.call(this) }.bind(this) // start next animation if(this.situation.delay){ setTimeout(function(){fn()}, this.situation.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.situation 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.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){ if(!this.active) this.start() if(clearQueue){ this.clearQueue() } this.active = false if(jumpToEnd){ this.situation.loop = false // TODO: test this if(this.situation.loops % 2 == 0 && this.situation.reversing){ this.situation.reversed = true } this.at(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.situation){ var temp = this.situation this.stop() this.situation = temp this.at(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().situation && this.stop(true, false)); this.clearQueue().clearCurrent() return this } // set the internal animation pointer to the specified position and updates the visualisation , at: function(pos){ this.pos = pos this.situation.start = +new Date - pos * this.situation.duration this.situation.finish = this.situation.start + this.situation.duration return this.step(true) } // speeds up the animation by the given factor // this changes the duration of the animation , speed: function(speed){ this.situation.duration = this.situation.duration * this.pos + (1-this.pos) * this.situation.duration / speed this.situation.finish = this.situation.start + this.situation.duration return this.at(this.pos) } // Make loopable , loop: function(times, reverse) { // store current loop and total loops this.situation.loop = this.situation.loops = 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.at(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.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 } // 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.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, SVG.morph, 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) }) } , 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 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.situation.loop === true || (typeof this.situation.loop == 'number' && --this.situation.loop))){ if(this.situation.reversing){ this.situation.reversed = !this.situation.reversed } return this.at(this.pos-1) } if(this.situation.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.situation.ease(this.pos) // call once-callbacks for(var i in this.situation.once){ if(i > this.lastPos && i <= eased){ this.situation.once[i].call(this.target(), this.pos, eased) delete this.situation.once[i] } } // fire during callback with position, eased position and current situation as parameter if(this.active) this.target().fire('during', {pos: this.pos, eased: eased, fx: this, situation: this.situation}) // the user may call stop or finish in the during callback // so make sure that we still have a valid situation if(!this.situation){ return this } // apply the actual animation to every property this.eachAt() // do final code when situation is finished if((this.pos == 1 && !this.situation.reversed) || (this.situation.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.situation}) if(!this.situations.length/* && this.active*/){ this.target().fire('allfinished') this.target().off('.fx') // there shouldnt be any binding left, but to make sure... 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 , eachAt: function(){ var i, at, self = this, target = this.target(), c = this.situation // 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.ease(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.ease(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.ease(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.ease(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.ease(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.situation.ease(pos) this.situation.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, ease, delay) { return (this.fx || (this.fx = new SVG.FX(this))).animate(o, ease, 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(from, to){ // prepare color for morphing if(SVG.Color.isColor(to)) return new SVG.Color(from).morph(to) // prepare number for morphing if(SVG.regex.numberAndUnit.test(to)) return new SVG.Number(from).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(null, 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(null, 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 } })