aboutsummaryrefslogtreecommitdiffstats
path: root/src/fx.js
diff options
context:
space:
mode:
authorUlrich-Matthias Schäfer <ulima.ums@googlemail.com>2016-03-24 23:44:59 +0100
committerUlrich-Matthias Schäfer <ulima.ums@googlemail.com>2016-03-24 23:44:59 +0100
commit000b071d04d67091c148cbe66e515e63b4b46986 (patch)
tree433bc79e68b00e06d2c1657b9a0b48e24248b944 /src/fx.js
parent595777968f56ebafc8ab0b25d8605ce258f87c98 (diff)
parentc33e7f6c5b6cd2e4ef595fce65559eb74b8c5e63 (diff)
downloadsvg.js-000b071d04d67091c148cbe66e515e63b4b46986.tar.gz
svg.js-000b071d04d67091c148cbe66e515e63b4b46986.zip
Merge branch '3.0.0' introducing several changes:
- textpath now is a parent element, the lines method of text will return the tspans inside the textpath (#450) - fx module rewritten to support animation chaining and several other stuff (see docs when commited) - fixed absolute transformation animations (not perfect but better) - fixed event listeners which didnt work correctly when identic funtions used - added `element.is()` which helps to check for the object instance faster (instanceof check) - added more fx specs
Diffstat (limited to 'src/fx.js')
-rw-r--r--src/fx.js1095
1 files changed, 724 insertions, 371 deletions
diff --git a/src/fx.js b/src/fx.js
index 758ce50..7514a40 100644
--- a/src/fx.js
+++ b/src/fx.js
@@ -1,444 +1,641 @@
+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}
+}
+
+var someVar = 0
+
+SVG.Situation = SVG.invent({
+
+ create: function(o){
+ this.init = false
+ this.reversed = false
+
+ this.duration = o.duration
+ this.delay = o.delay
+
+ this.start = +new Date() + this.delay
+ this.end = this.start + this.duration
+ this.easing = o.easing
+
+ 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 = delay
+}
+
SVG.FX = SVG.invent({
- // Initialize FX object
+
create: function(element) {
- // store target element
- this.target = element
+ this._target = element
+ this.situations = []
+ this.active = false
+ this.current = null
+ this.paused = false
+ this.lastPos = 0
+ this.pos = 0
}
- // 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
+
+ /**
+ * 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 easing 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, easing, delay){
+
+ if(typeof o == 'object'){
+ easing = o.ease
+ delay = o.delay
+ o = o.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)
- }
- }
- }
+ var situation = new SVG.Situation({
+ duration: o || 1000,
+ delay: delay || 0,
+ easing: SVG.easing[easing || '-'] || easing
+ })
- // collect style keys
- if (skeys == null) {
- skeys = []
- for (key in fx.styles)
- skeys.push(key)
- }
+ this.queue(situation)
- // 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))
- }
+ return this
+ }
- // 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)
- }
-
- }
+ /**
+ * 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)
+ }
- // start animation
- fx.render()
-
- }, new SVG.Number(delay).valueOf())
+ /**
+ * 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
+
+ return this._target
}
- // 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] = (new SVG.Matrix(this.target)).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.numberAndUnit.test(v) ?
- // prepare number for morphing
- new SVG.Number(from).morph(v) :
- // prepare for plain morphing
- { from: from, to: v }
- }
+ // returns the position at a given time
+ , timeToPos: function(timestamp){
+ return (timestamp - this.current.start) / (this.current.duration)
+ }
+
+ // returns the timestamp from a given positon
+ , posToTime: function(pos){
+ return this.current.duration * pos + this.current.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.current){
+ this.current.start = +new Date + this.current.delay
+ this.current.end = this.current.start + this.current.duration
+
+ this.initAnimations()
+ this.active = true
+ this.startAnimFrame()
}
-
+
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 }
-
+
+ /**
+ * 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.current) this.current = this.situations.shift()
+
return this
}
- // Animatable x-axis
- , x: function(x) {
- this.destination.x = new SVG.Number(this.target.x()).morph(x)
-
+
+ /**
+ * pulls next element from the queue and execute it
+ * @return this
+ */
+ , dequeue: function(){
+ // stop current animation
+ this.current && this.current.stop && this.current.stop()
+
+ // get next animation from queue
+ this.current = this.situations.shift()
+
+ if(this.current){
+
+ var fn = function(){
+ if(this.current instanceof SVG.Situation)
+ this.initAnimations().seek(0)
+ else if(this.current instanceof SVG.Delay)
+ this.dequeue()
+ else
+ this.current.call(this)
+ }.bind(this)
+
+ // start next animation
+ if(this.current.delay){
+ setTimeout(function(){fn()}, this.current.delay)
+ }else{
+ fn()
+ }
+
+ }
+
return this
}
- // Animatable y-axis
- , y: function(y) {
- this.destination.y = new SVG.Number(this.target.y()).morph(y)
-
+
+ // 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.current
+
+ 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
}
- // Animatable center x-axis
- , cx: function(x) {
- this.destination.cx = new SVG.Number(this.target.cx()).morph(x)
-
+ , clearQueue: function(){
+ this.situations = []
return this
}
- // Animatable center y-axis
- , cy: function(y) {
- this.destination.cy = new SVG.Number(this.target.cy()).morph(y)
-
+ , clearCurrent: function(){
+ this.current = null
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)
- }
+ /** 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()
}
-
- return this
+
+ this.active = false
+
+ if(jumpToEnd){
+ this.seek(1)
+ }
+
+ this.stopAnimFrame()
+ clearTimeout(this.timeout)
+
+ return this.clearCurrent()
}
- // Add animatable plot
- , plot: function(p) {
- this.destination.plot = p
+ /** resets the element to the state where the current element has started
+ * @return this
+ */
+ , reset: function(){
+ if(this.current){
+ var temp = this.current
+ this.stop()
+ this.current = temp
+ this.seek(0)
+ }
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
+ // 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().current && this.stop(true, false));
+
+ return this.clearQueue().clearCurrent()
}
- // 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)
- }
- }
-
+
+ // set the internal animation pointer to the specified position and updates the visualisation
+ , seek: function(pos){
+ this.pos = pos
+ this.current.start = +new Date - pos * this.current.duration
+ this.current.end = this.current.start + this.current.duration
+ return this.step(true)
+ }
+
+ // speeds up the animation by the given factor
+ // this changes the duration of the animation
+ , speed: function(speed){
+ this.current.duration = this.current.duration * this.pos + (1-this.pos) * this.current.duration / speed
+ this.current.end = this.current.start + this.current.duration
+ return this.seek(this.pos)
+ }
+ // Make loopable
+ , loop: function(times, reverse) {
+ // store current loop and total loops
+ this.current.loop = times || true
+
+ if(reverse) return this.reverse()
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))
- }
+ // pauses the animation
+ , pause: function(){
+ this.paused = true
+ this.stopAnimFrame()
+ clearTimeout(this.timeout)
return this
}
- // Add callback for each keyframe
- , during: function(during) {
- this.situation.during = during
-
+
+ // unpause the animation
+ , play: function(){
+ if(!this.paused) return this
+ this.paused = false
+ return this.seek(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
}
- // Callback after animation
- , after: function(after) {
- this.situation.after = after
-
+
+
+ /**
+ * 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.current.easing(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
}
- // 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
+ // 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, 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
}
- // Stop running animation
- , stop: function(fulfill) {
- // fulfill animation
- if (fulfill === true) {
- this.animate(0)
+ // calls on every animation step for all animations
+ , duringAll: function(fn){
+ var wrapper = function(e){
+ fn.call(this, e.detail.pos, e.detail.eased, e.detail.fx, e.detail.situation)
+ }
- if (this.situation.after)
- this.situation.after.apply(this.target, [this])
+ this.target().off('during.fx', wrapper).on('during.fx', wrapper)
- } else {
- // stop current animation
- clearTimeout(this.timeout)
- cancelAnimationFrame(this.animationFrame);
+ return this.afterAll(function(){
+ this.off('during.fx', wrapper)
+ })
+ }
- // reset storage for properties
- this.attrs = {}
- this.styles = {}
- this.situation = {}
- this.destination = {}
+ /**
+ * returns a float from 0-1 indicating the progress of the whole animation queue
+ * we recalculate the end time because it may be changed from methods like seek()
+ * @return number
+ */
+ // FIXME: current start always changes so the progress get a reset whenever one situation finishes. We need a global start which is only modified on pause and stop
+ , totalProgress: function(){
+ var start = this.current.start
+ , end = this.current
+
+ for(var i = 0, len = this.situations.length; i < len; ++i){
+ end += (situations[i].duration || 0) + (situations[i].delay || 0)
}
-
+
+ return (this.pos * this.current.duration + this.start - start) / (end - start)
+ }
+
+ , last: function(){
+ return this.situations.length ? this.situations[this.situations.length-1] : this.current
+ }
+
+ // adds one property to the animations
+ , add: function(method, args, type){
+ //if(this.situations.length){
+ // this.situations[this.situations.length-1][type || 'animations'][method] = args
+ //}else{
+ // this.current[type || 'animations'][method] = args
+ //}
+ this.last()[type || 'animations'][method] = args
+ setTimeout(function(){this.start()}.bind(this), 0)
return this
}
- // Pause running animation
- , pause: function() {
- if (this.situation.play === true) {
- this.situation.play = false
- this.situation.pause = new Date().getTime()
+
+ /** 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.current.loop === true || (typeof this.current.loop == 'number' && --this.current.loop))){
+ return this.seek(this.pos-1)
+ }
+
+ if(this.current.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.current.easing(this.pos)
+
+ // call once-callbacks
+ for(var i in this.current.once){
+ if(i > this.lastPos && i <= eased){
+ this.current.once[i].call(this.target(), this.pos, eased)
+ delete this.current.once[i]
+ }
}
+ // fire during callback with position, eased position and current situation as parameter
+ this.target().fire('during', {pos: this.pos, eased: eased, fx: this, situation: this.current})
+
+ // apply the actual animation to every property
+ this.eachAt()
+
+ // do final code when situation is finished
+ if((this.pos == 1 && !this.current.reversed) || (this.current.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.current})
+
+ if(!this.situations.length && !this.current && this.active){
+ this.target().fire('allfinished')
+ this.target().off('.fx')
+ 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
+
}
- // 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
+
+ // calculates the step for every property and calls block with it
+ // todo: include block directly cause it is used only for this purpose
+ , eachAt: function(){
+ var i, at, self = this, target = this.target(), c = this.current
+
+ // 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.easing(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.easing(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.easing(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.easing(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.easing(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.current.easing(pos)
+
+ this.current.once[pos] = fn
+
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)
+ animate: function(o, easing, delay) {
+ return (this.fx || (this.fx = new SVG.FX(this))).animate(o, easing, delay)
+ }
+ , delay: function(delay){
+ return (this.fx || (this.fx = new SVG.FX(this))).delay(delay)
}
- // Stop current animation; this is an alias to the fx instance
- , stop: function(fulfill) {
+ , stop: function(jumpToEnd, clearQueue) {
if (this.fx)
- this.fx.stop(fulfill)
-
+ this.fx.stop(jumpToEnd, clearQueue)
+
return this
}
// Pause current animation
@@ -455,6 +652,162 @@ SVG.FX = SVG.invent({
return this
}
-
}
+
+})
+
+// MorphObj is used whenever no morphable object is given
+SVG.MorphObj = SVG.invent({
+
+ create: function(to){
+ // prepare color for morphing
+ if(SVG.Color.isColor(to)) return new SVG.Color().morph(to)
+ // prepare number for morphing
+ if(SVG.regex.numberAndUnit.test(to)) return new SVG.Number().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(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(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
+ }
+}) \ No newline at end of file