SVG.FX = SVG.invent({
  // Initialize FX object
  create: function(element) {
    /* store target element */
    this.target = element
  }

  // Add class methods
, extend: {
    // Add animation parameters and start animation
    animate: function(d, ease, delay) {
      var akeys, tkeys, 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
      }

      /* ensure default duration and easing */
      d = d == '=' ? d : d == null ? 1000 : new SVG.Number(d).valueOf()
      ease = ease || '<>'

      /* process values */
      fx.to = 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._plot || akeys.indexOf('points') > -1)) {
            /* get destination */
            var box
              , p = new element.morphArray(fx._plot || fx.attrs.points || element.array)

            /* add size */
            if (fx._size) p.size(fx._size.width.to, fx._size.height.to)

            /* add movement */
            box = p.bbox()
            if (fx._x) p.move(fx._x.to, box.y)
            else if (fx._cx) p.move(fx._cx.to - box.width / 2, box.y)

            box = p.bbox()
            if (fx._y) p.move(box.x, fx._y.to)
            else if (fx._cy) p.move(box.x, fx._cy.to - box.height / 2)

            /* delete element oriented changes */
            delete fx._x
            delete fx._y
            delete fx._cx
            delete fx._cy
            delete fx._size

            fx._plot = element.array.morph(p)
          }
        }

        /* collect transformation keys */
        if (tkeys == null) {
          tkeys = []
          for (key in fx.trans)
            tkeys.push(key)
        }

        /* collect style keys */
        if (skeys == null) {
          skeys = []
          for (key in fx.styles)
            skeys.push(key)
        }

        /* 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._plot) {
          element.plot(fx._plot.at(pos))

        } else {
          /* run all x-position properties */
          if (fx._x)
            element.x(fx._x.at(pos))
          else if (fx._cx)
            element.cx(fx._cx.at(pos))

          /* run all y-position properties */
          if (fx._y)
            element.y(fx._y.at(pos))
          else if (fx._cy)
            element.cy(fx._cy.at(pos))

          /* run all size properties */
          if (fx._size)
            element.size(fx._size.width.at(pos), fx._size.height.at(pos))
        }

        /* run all viewbox properties */
        if (fx._viewbox)
          element.viewbox(
            fx._viewbox.x.at(pos)
          , fx._viewbox.y.at(pos)
          , fx._viewbox.width.at(pos)
          , fx._viewbox.height.at(pos)
          )

        /* run leading property */
        if (fx._leading)
          element.leading(fx._leading.at(pos))

        /* animate attributes */
        for (i = akeys.length - 1; i >= 0; i--)
          element.attr(akeys[i], at(fx.attrs[akeys[i]], pos))

        /* animate transformations */
        for (i = tkeys.length - 1; i >= 0; i--)
          element.transform(tkeys[i], at(fx.trans[tkeys[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._during)
          fx._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 = {
            interval: 1000 / 60
          , start:    start
          , play:     true
          , finish:   start + d
          , duration: d
          }

          /* render function */
          fx.render = function() {
            
            if (fx.situation.play === true) {
              // This code was borrowed from the emile.js micro framework by Thomas Fuchs, aka MadRobby.
              var time = new Date().getTime()
                , pos = time > fx.situation.finish ? 1 : (time - fx.situation.start) / d
              
              /* process values */
              fx.to(pos)
              
              /* finish off animation */
              if (time > fx.situation.finish) {
                if (fx._plot)
                  element.plot(new SVG.PointArray(fx._plot.destination).settle())

                if (fx._loop === true || (typeof fx._loop == 'number' && fx._loop > 1)) {
                  if (typeof fx._loop == 'number')
                    --fx._loop
                  fx.animate(d, ease, delay)
                } else {
                  fx._after ? fx._after.apply(element, [fx]) : fx.stop()
                }

              } else {
                requestAnimFrame(fx.render)
              }
            } else {
              requestAnimFrame(fx.render)
            }
            
          }

          /* start animation */
          fx.render()
          
        }, new SVG.Number(delay).valueOf())
      }
      
      return this
    }
    // Get bounding box of target element
  , bbox: function() {
      return this.target.bbox()
    }
    // Add animatable attributes
  , attr: function(a, v) {
      if (typeof a == 'object') {
        for (var key in a)
          this.attr(key, a[key])
      
      } else {
        var from = this.target.attr(a)

        this.attrs[a] = SVG.Color.isColor(from) ?
          new SVG.Color(from).morph(v) :
        SVG.regex.unit.test(from) ?
          new SVG.Number(from).morph(v) :
          { from: from, to: v }
      }
      
      return this
    }
    // Add animatable transformations
  , transform: function(o, v) {
      if (arguments.length == 1) {
        /* parse matrix string */
        o = parseMatrix(o)
        
        /* dlete matrixstring from object */
        delete o.matrix
        
        /* store matrix values */
        for (v in o)
          this.trans[v] = { from: this.target.trans[v], to: o[v] }
        
      } else {
        /* apply transformations as object if key value arguments are given*/
        var transform = {}
        transform[o] = v
        
        this.transform(transform)
      }
      
      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 }
      
      return this
    }
    // Animatable x-axis
  , x: function(x) {
      this._x = new SVG.Number(this.target.x()).morph(x)
      
      return this
    }
    // Animatable y-axis
  , y: function(y) {
      this._y = new SVG.Number(this.target.y()).morph(y)
      
      return this
    }
    // Animatable center x-axis
  , cx: function(x) {
      this._cx = new SVG.Number(this.target.cx()).morph(x)
      
      return this
    }
    // Animatable center y-axis
  , cy: function(y) {
      this._cy = new SVG.Number(this.target.cy()).morph(y)
      
      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._size = {
          width:  new SVG.Number(box.width).morph(width)
        , height: new SVG.Number(box.height).morph(height)
        }
      }
      
      return this
    }
    // Add animatable plot
  , plot: function(p) {
      this._plot = p

      return this
    }
    // Add leading method
  , leading: function(value) {
      if (this.target._leading)
        this._leading = new SVG.Number(this.target._leading).morph(value)

      return this
    }
    // Add animatable viewbox
  , viewbox: function(x, y, width, height) {
      if (this.target instanceof SVG.Container) {
        var box = this.target.viewbox()
        
        this._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)
        }
      }
      
      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))
      }

      return this
    }
    // Add callback for each keyframe
  , during: function(during) {
      this._during = during
      
      return this
    }
    // Callback after animation
  , after: function(after) {
      this._after = after
      
      return this
    }
    // Make loopable
  , loop: function(times) {
      this._loop = times || true

      return this
    }
    // Stop running animation
  , stop: function(fulfill) {
      /* fulfill animation */
      if (fulfill === true) {
        this.animate(0)

      } else {
        /* stop current animation */
        clearTimeout(this.timeout)

        /* reset storage for properties that need animation */
        this.attrs     = {}
        this.trans     = {}
        this.styles    = {}
        this.situation = {}

        /* delete destinations */
        delete this._x
        delete this._y
        delete this._cx
        delete this._cy
        delete this._size
        delete this._plot
        delete this._loop
        delete this._after
        delete this._during
        delete this._leading
        delete this._viewbox
      }
      
      return this
    }
    // Pause running animation
  , pause: function() {
      if (this.situation.play === true) {
        this.situation.play  = false
        this.situation.pause = new Date().getTime()
      }

      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
      }

      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)
    }
    // Stop current animation; this is an alias to the fx instance
  , stop: function(fulfill) {
      if (this.fx)
        this.fx.stop(fulfill)
      
      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
    }
    
  }
})