       kanim.duration = duration
       kanim.timingFunctions = [timingFunction]
       anim = kanim
-    } else if #available(iOS 9.0, *), key != "cornerRadius", let springOptions = context[view, "spring"] {
+    } else if #available(iOS 9.0, *), key != "cornerRadius", let springOptions = parameters["spring"] {
       let sanim = CASpringAnimation(keyPath: key)
       sanim.stiffness = (springOptions.getCGFloat(0) ?? 250)
       sanim.damping = (springOptions.getCGFloat(1) ?? 30)

  var snapshot:UIView
  var state = [String:(Any, Any)]()
  var appearing:Bool

  init(view:UIView, snapshot:UIView, modifiers:HeroModifiers, appearing:Bool){
    self.view = view
    self.snapshot = snapshot
    self.appearing = appearing

    let disappeared = viewState(for: view, with: modifiers)
    for (key, disappearedState) in disappeared{
      let appearingState = snapshot.layer.value(forKeyPath: key)
      let toValue = appearing ? appearingState : disappearedState
      let fromValue = !appearing ? appearingState : disappearedState
      state[key] = (fromValue, toValue)
    }
  }
}

public class HeroDefaultAnimator:HeroAnimator{
  // TODO: calculate duration based on distance travelled
  // https://material.io/guidelines/motion/duration-easing.html
  var duration:TimeInterval = HeroDefaultAnimator.defaultAnimationDuration
  static let defaultAnimationDuration:TimeInterval = 0.375

  var context:HeroContext!
  var animationGroups:[CALayer:(TimeInterval, CAAnimationGroup)] = [:]
  let animatableOptions:Set<String> = ["fade", "opacity", "position", "size", "cornerRadius", "transform", "scale", "translate", "rotate"]
  var removedAnimations:[(CALayer, CAPropertyAnimation)] = []
  var animationStates:[UIView: AnimationState] = [:]
  
  var paused = true

  public func seekTo(progress:Double) {
    paused = true
    for (layer, (delay, anim)) in animationGroups{
      anim.speed = 0
      let timeOffset = progress * duration - delay
      anim.timeOffset = max(0, min(anim.duration - 0.01, timeOffset))
      layer.removeAnimation(forKey: "hero")
      layer.add(anim, forKey: "hero")
    }
  }
  
  public func resume(from progress:Double, reverse:Bool) -> TimeInterval{
    paused = false
    let timePassed = (reverse ? 1 - progress : progress) * duration
    
    var neededTime:TimeInterval = self.duration - timePassed
    for (layer, anim) in removedAnimations{
      anim.fillMode = kCAFillModeBoth
      anim.isRemovedOnCompletion = false
      if #available(iOS 9.0, *), let anim = anim as? CASpringAnimation{
        anim.speed = 1
        if reverse {
          anim.toValue = anim.fromValue
        }
        anim.fromValue = layer.value(forKeyPath: anim.keyPath!)
        neededTime = max(neededTime, anim.settlingDuration)
      } else if let anim = anim as? CABasicAnimation{
        anim.speed = reverse ? -1 : 1
        if reverse{
          anim.toValue = layer.value(forKeyPath: anim.keyPath!)
        } else {
          anim.fromValue = layer.value(forKeyPath: anim.keyPath!)
        }
      }
    }
    for (layer, (delay, group)) in animationGroups{
      group.speed = reverse ? -1 : 1
      group.timeOffset = 0
      if reverse{
        let timePassed = (1 - progress) * (delay + group.duration)
        group.beginTime = layer.convertTime(CACurrentMediaTime(), from: nil) - timePassed
      } else {
        group.beginTime = layer.convertTime(CACurrentMediaTime(), from: nil) + delay - timePassed
      }
    }
    for (layer, (_, group)) in animationGroups{
      layer.removeAnimation(forKey: "hero")
      layer.add(group, forKey: "hero")
    }
    for (layer, anim) in removedAnimations{
      anim.duration = neededTime
      layer.add(anim, forKey: "hero_\(anim.keyPath!)")
    }
    return neededTime
  }
  
  public func temporarilySet(view:UIView, to modifiers:HeroModifiers){
    guard let snapshot = animationStates[view]?.snapshot else {
      print("HERO: unable to temporarily set to \(view). The view must be running at least one animation before it can be interactively changed")
      return
    }
    let (_, group) = animationGroups[snapshot.layer]!
    let state = viewState(for: view, with: modifiers)
    for (key, targetValue) in state{
      if let index = group.animations!.index(where:{ return ($0 as! CAPropertyAnimation).keyPath == key }) {
        let anim = group.animations!.remove(at: index)
        if let anim = anim as? CABasicAnimation{
          removedAnimations.append((snapshot.layer, anim))
        } else if let anim = anim as? CAKeyframeAnimation {
          let anim = animation(for:view, key: key, fromValue: anim.values![0], toValue: anim.values![1], ignoreArc: true)
          removedAnimations.append((snapshot.layer, anim))
        }
      } else if removedAnimations.index(where: { return $0.0 == snapshot.layer && $0.1.keyPath == key }) == nil{
        let originValue = snapshot.layer.value(forKeyPath: key)
        let anim = animation(for:view, key: key, fromValue: originValue, toValue: originValue, ignoreArc: true)
        removedAnimations.append((snapshot.layer, anim))
      }
      snapshot.layer.setValue(targetValue, forKeyPath: key)
      if key == "bounds.size"{
        let contentLayer = snapshot.subviews[0].layer
        if removedAnimations.index(where: { return $0.0 == contentLayer }) == nil{
          if let (_, group) = animationGroups[contentLayer]{
            for anim in group.animations!{
              removedAnimations.append((contentLayer, anim as! CAPropertyAnimation))
            }
          } else {
            let originSize = contentLayer.value(forKeyPath: "bounds.size")
            removedAnimations.append((contentLayer, animation(for:view, key: "bounds.size", fromValue: originSize, toValue: originSize)))
            let originPos = contentLayer.value(forKeyPath: "position")
            removedAnimations.append((contentLayer, animation(for:view, key: "position", fromValue: originPos, toValue: originPos)))
          }
        }
        contentLayer.bounds = (targetValue as! NSValue).cgRectValue
        contentLayer.position = contentLayer.bounds.center
      }
    }
    snapshot.layer.removeAnimation(forKey: "hero")
    snapshot.layer.add(group, forKey: "hero")
  }

  public func canAnimate(context:HeroContext, view:UIView, appearing:Bool) -> Bool{
    if let modifierNames = context.modifierNames(for: view){
      return self.animatableOptions.intersection(Set(modifierNames)).count > 0
    }
    return false
  }
  
  public func takeSnapshot(for v:UIView) -> UIView{
    v.isHidden = false
    
    // capture a snapshot without cornerRadius
    let oldCornerRadius = v.layer.cornerRadius
    v.layer.cornerRadius = 0
    let snapshot = v.snapshotView(afterScreenUpdates: true)!
    v.layer.cornerRadius = oldCornerRadius
    
    // the Snapshot's contentView must have hold the cornerRadius value,
    // since the snapshot might not have maskToBounds set
    let contentView = snapshot.subviews[0]
    contentView.layer.cornerRadius = v.layer.cornerRadius
    contentView.layer.masksToBounds = true
    
    snapshot.layer.cornerRadius = v.layer.cornerRadius
    if let zPos = context[v, "zPosition"]?.getCGFloat(0){
      snapshot.layer.zPosition = zPos
    } else {
      snapshot.layer.zPosition = v.layer.zPosition
    }
    snapshot.layer.opacity = v.layer.opacity
    snapshot.layer.isOpaque = v.layer.isOpaque
    snapshot.layer.anchorPoint = v.layer.anchorPoint
    snapshot.layer.masksToBounds = v.layer.masksToBounds
    snapshot.layer.borderColor = v.layer.borderColor
    snapshot.layer.borderWidth = v.layer.borderWidth
    snapshot.layer.transform = v.layer.transform
    snapshot.layer.shadowRadius = v.layer.shadowRadius
    snapshot.layer.shadowOpacity = v.layer.shadowOpacity
    snapshot.layer.shadowColor = v.layer.shadowColor
    snapshot.layer.shadowOffset = v.layer.shadowOffset
    
    snapshot.frame = context.container.convert(v.bounds, from: v)
    snapshot.heroID = v.heroID

    v.isHidden = true
    
    // insert below views from other plugins
    context.container.addSubview(snapshot)
    
    return snapshot
  }
  public func animate(context:HeroContext, fromViews:[UIView], toViews:[UIView]) -> TimeInterval{
    self.paused = false
    self.context = context
    
    // animate
    for v in fromViews{
      animate(view: v, appearing: false)
    }
    for v in toViews{
      animate(view: v, appearing: true)
    }

    return duration
  }

  public func clean(){
    paused = true
    context = nil
    animationStates.removeAll()
    removedAnimations.removeAll()
    animationGroups.removeAll()
  }
}


private extension HeroDefaultAnimator {
  
  func getTimingFunction(for view:UIView) -> CAMediaTimingFunction{
    // get the timing function
    if let curveOptions = context[view, "curve"]{
      if let c1 = curveOptions.getFloat(0),
        let c2 = curveOptions.getFloat(1),
        let c3 = curveOptions.getFloat(2),


  func animation(for view:UIView, key:String, fromValue:Any?, toValue:Any?, ignoreArc:Bool = false) -> CAPropertyAnimation {
    let anim:CAPropertyAnimation
    let duration:Double = parameters["duration"]?.getDouble(0) ?? HeroDefaultAnimator.defaultAnimationDuration
    
    let timingFunction = getTimingFunction(for: view)
    
    if !ignoreArc, key == "position", let arcOptions = parameters["arc"],
      let fromPos = (fromValue as? NSValue)?.cgPointValue,
      let toPos = (toValue as? NSValue)?.cgPointValue,
      abs(fromPos.x - toPos.x) >= 1, abs(fromPos.y - toPos.y) >= 1 {
