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 = requestAnimationFrame(function(){ this.step() }.bind(this)) } // cancels the animationframe , stopAnimFrame: function(){ 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.situation && this.situation.stop && this.situation.stop() // get next animation from queue this.situation = this.situations.shift() if(this.situation){ if(this.situation instanceof SVG.Situation) { this.startCurrent() } 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 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.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){ if(!this.active) this.start() if(clearQueue){ this.clearQueue() } this.active = false if(jumpToEnd && this.situation){ 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 return this.at(this.situation.loop+1, true) } else 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 } // 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) 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.pos), e.detail.eased, 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 an absolute position if(!ignoreTime) this.absPos = this.timeToAbsPos(+new Date) // This part convert an absolute position to a position if(this.situation.loops !== false) { var absPos, absPosInt, lastLoop // If the absolute position is below 0, we just treat it as if it was 0 absPos = Math.max(this.absPos, 0) absPosInt = Math.floor(absPos) if(this.situation.loops === true || absPosInt < this.situation.loops) { this.pos = absPos - absPosInt lastLoop = this.situation.loop this.situation.loop = absPosInt } else { this.absPos = this.situation.loops this.pos = 1 // The -1 here is because we don't want to toggle reversed when all the loops have been completed lastLoop = this.situation.loop - 1 this.situation.loop = this.situation.loops } if(this.situation.reversing) { // Toggle reversed if an odd number of loops as occured since the last call of step this.situation.reversed = this.situation.reversed != Boolean((this.situation.loop - lastLoop) % 2) } } else { // If there are no loop, the absolute position must not be above 1 this.absPos = Math.min(this.absPos, 1) this.pos = this.absPos } // while the absolute position can be below 0, the position must not be below 0 if(this.pos < 0) this.pos = 0 if(this.situation.reversed) this.pos = 1 - this.pos // 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 this.stopAnimFrame() // fire finished callback with current situation as parameter this.target().fire('finished', {fx:this, situation: this.situation}) if(!this.situations.length){ 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(), s = this.situation // apply animations which can be called trough a method for(i in s.animations){ at = [].concat(s.animations[i]).map(function(el){ return typeof el !== 'string' && el.at ? el.at(s.ease(self.pos), self.pos) : el }) target[i].apply(target, at) } // apply animation which has to be applied with attr() for(i in s.attrs){ at = [i].concat(s.attrs[i]).map(function(el){ return typeof el !== 'string' && el.at ? el.at(s.ease(self.pos), self.pos) : el }) target.attr.apply(target, at) } // apply animation which has to be applied with style() for(i in s.styles){ at = [i].concat(s.styles[i]).map(function(el){ return typeof el !== 'string' && el.at ? el.at(s.ease(self.pos), self.pos) : el }) target.style.apply(target, at) } // animate initialTransformation which has to be chained if(s.transforms.length){ // get inital initialTransformation at = s.initialTransformation for(i in s.transforms){ // get next transformation in chain var a = s.transforms[i] // multiply matrix directly if(a instanceof SVG.Matrix){ if(a.relative){ at = at.multiply(a.at(s.ease(this.pos))) }else{ at = at.morph(a).at(s.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(s.ease(this.pos))) } // 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 } , finish: function() { if (this.fx) this.fx.finish() 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 } // Set/Get the speed of the animations , speed: function(speed) { if (this.fx) if (speed == null) return this.fx.speed() else this.fx.speed(speed) 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 } })