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(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().css(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() 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() function wrapper (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 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') // Recheck the length since the user may call animate in the afterAll callback if (!this.situations.length) { 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 var self = this var target = this.target() var 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 css() 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.css.apply(target, at) } // animate initialTransformation which has to be chained if (s.transforms.length) { // TODO: ANIMATE THE TRANSFORMS // // get initial initialTransformation // at = s.initialTransformation // for(i = 0, len = s.transforms.length; i < len; i++){ // // // get next transformation in chain // var a = s.transforms[i] // // // multiply matrix directly // if(a instanceof SVG.Matrix){ // // if(a.relative){ // at = at.multiply(new SVG.Matrix().morph(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.decompose()) // // // 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) { var c = this.last() if (!isEased) pos = c.ease(pos) c.once[pos] = fn return this }, _callStart: function () { setTimeout(function () { this.start() }.bind(this), 0) 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 value list for morphing if (SVG.regex.delimiter.test(from)) return new SVG.Array(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 = from 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 { this.add(a, v, 'attrs') } return this }, // Add animatable styles css: function (s, v) { if (typeof s === 'object') { for (var key in s) { this.css(key, s[key]) } } else { this.add(s, 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(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(y) num.relative = relative return this.add('y', num) }, // Animatable center x-axis cx: function (x) { return this.add('cx', new SVG.Number(x)) }, // Animatable center y-axis cy: function (y) { return this.add('cy', new SVG.Number(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(width)) .add('height', new SVG.Number(height)) } return this }, // Add animatable width width: function (width) { return this.add('width', new SVG.Number(width)) }, // Add animatable height height: function (height) { return this.add('height', new SVG.Number(height)) }, // Add animatable plot plot: function (a, b, c, d) { // Lines can be plotted with 4 arguments if (arguments.length === 4) { return this.plot([a, b, c, d]) } return this.add('plot', new (this.target().MorphArray)(a)) }, // Add leading method leading: function (value) { return this.target().leading ? this.add('leading', new SVG.Number(value)) : this }, // Add animatable viewbox viewbox: function (x, y, width, height) { if (this.target() instanceof SVG.Container) { this.add('viewbox', new SVG.Box(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 } })