921 lines
26 KiB
JavaScript
921 lines
26 KiB
JavaScript
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.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, 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().style(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()
|
|
, 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._callStart()
|
|
}
|
|
|
|
// 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)
|
|
|
|
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, len, 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 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.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){
|
|
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)
|
|
// check if we have a list of values
|
|
if(SVG.regex.delimiter.test(from)) {
|
|
// prepare path for morphing
|
|
if(SVG.regex.pathLetters.test(from)) return new SVG.PathArray(from).morph(to)
|
|
// prepare value list for morphing
|
|
else 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
|
|
, style: function(s, v) {
|
|
if (typeof s == 'object')
|
|
for (var key in s)
|
|
this.style(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.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
|
|
}
|
|
})
|