Determine springWithDamping and initialSpringVelocity based off from friction and tension
Asked Answered
S

4

9

My design team gives us animation parameters using friction and tension. For instance:

Has a spring affect (280 tension and 20.5 friction) Over 0.3 seconds

Unfortunately, i've always guessed what these values convert to, and eyeball it, if it looks close I send it over and they approve it. But the time that it takes to constantly build the project with different values is time consuming. There has to be an easier way.

I found Framer on Github and it's led me to believe that the damping can be calculated like so:

let damping: CGFloat = 20.5 / (2 * (1 * 280)).squareRoot()

However, I cannot seem to figure out how to calculate the velocity based off from friction and tension. Is there an easier way to save this developer some valuable time?

Example of animation:

UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: damping,
                       initialSpringVelocity: ???, options: .curveEaseIn, animations: { // Do Stuff })
Song answered 25/4, 2018 at 19:5 Comment(0)
D
12

You're right that the code you linked to can be used to calculate the dampingRatio (I'm flattered, because I'm the one that wrote it ;). You seem to have an error in your derived Swift code, though. I think it should be (notice the difference in parentheses):

let damping: CGFloat = 20.5 / (2 * (1 * 280).squareRoot())

The velocity value is only needed if you want to give the object some initial velocity when starting the animation. The use case for this is if the object is already moving when starting the animation (for example when starting the animation after a drag interaction).

So if the object starts animating from a non-moving state, you can just use 0 as the initial velocity.

I'm confused by your design team giving you a tension, friction and duration though. Because springs are simulated physics, the tension and friction will simulate a spring that will stop animating after a specific duration. A spring with tension 280 and friction 20.5 results in a duration close to 0.65, not 0.3. (see the computeDuration function in Framer how to calculate the duration from the tension and friction). Here's the coffeescript version:

# Tries to compute the duration of a spring,
# but can't for certain velocities and if dampingRatio >= 1
# In those cases it will return null
exports.computeDuration = (tension, friction, velocity = 0, mass = 1) ->
    dampingRatio = computeDampingRatio(tension, friction)
    undampedFrequency = Math.sqrt(tension / mass)
    # This is basically duration extracted out of the envelope functions
    if dampingRatio < 1
        a = Math.sqrt(1 - Math.pow(dampingRatio, 2))
        b = velocity / (a * undampedFrequency)
        c = dampingRatio / a
        d = - ((b - c) / epsilon)
        if d <= 0
            return null
        duration = Math.log(d) / (dampingRatio * undampedFrequency)
    else
        return null
    return duration

The reason you can specify a duration for the springs used by iOS, is that it calculates the tension an friction of a spring, based on a dampingRatio and duration. Under the hood it will still use the tension and friction for the spring simulation. To get some insight on how that code works in iOS look at the computeDerivedCurveOptions in Framer, which is a direct port of the code used by iOS (created by disassembling and analyzing the iOS binaries).

Doggerel answered 28/4, 2018 at 21:1 Comment(1)
Thank you so much! Could you provide the computeDuration function in your answer and I will mark this as answered. :)Song
H
3

I converted this into a handy UIView extension so you can just call UIView.animate with tension and friction directly.

extension UIView {
    class func animate(withTension tension: CGFloat, friction: CGFloat, mass: CGFloat = 1.0, delay: TimeInterval = 0, initialSpringVelocity velocity: CGFloat = 0, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
        let damping = friction / sqrt(2 * (1 * tension))
        let undampedFrequency = sqrt(tension / mass)

        let epsilon: CGFloat = 0.001
        var duration: TimeInterval = 0

        if damping < 1 {
            let a = sqrt(1 - pow(damping, 2))
            let b = velocity / (a * undampedFrequency)
            let c = damping / a
            let d = -((b - c) / epsilon)
            if d > 0 {
                duration = TimeInterval(log(d) / (damping * undampedFrequency))
            }
        }

        UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: velocity, options: options, animations: animations, completion: completion)
    }
}
Hysell answered 3/5, 2019 at 18:17 Comment(0)
M
2

There is an easier way to achieve this kind of animation using friction and tension directly without any calculations.

There is UISpringTimingParameters which we use with UIViewPropertyAnimator.

  1. Create UISpringTimingParameters
let springParameters = UISpringTimingParameters(
  mass: 1.0, 
  stiffness: 260, // tension
  damping: 20,    // friction
  initialVelocity: .init(dx: 0, dy: 1.0)
)

Notice that stiffness is tension and damping is friction.

  1. Create an animator and add animations
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: springParameters)
animator.addAnimations {
    animations()
}
animator.startAnimation(afterDelay: delay)

So, a convenience extension can be made:

extension UIView {
    class func animate(withDuration duration: TimeInterval, tension: CGFloat, friction: CGFloat, mass: CGFloat = 1.0, delay: TimeInterval = 0, initialSpringVelocity velocity:  CGVector = .zero, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
       
        let springParameters = UISpringTimingParameters(
            mass: mass, stiffness: tension,
            damping: friction,
            initialVelocity: .init(dx: 0, dy: 1.0)
        )
        let animator = UIViewPropertyAnimator(duration: duration, timingParameters: springParameters)
        animator.addAnimations {
            animations()
        }
        animator.startAnimation(afterDelay: delay)
    }
}
Marsha answered 23/8, 2021 at 13:42 Comment(0)
C
1

I translated @Niels code to Swift.

import UIKit

func computeDuration(tension: Double, friction: Double, velocity: Double = 0.0, mass: Double = 1.0) -> Double {
    let dampingRatio = computeDampingRatio(tension: tension, friction: friction)
    let undampedFrequency = sqrt(tension / mass)

    let epsilon = 0.001
    var duration = 0.0

    //This is basically duration extracted out of the envelope functions
    if dampingRatio < 1 {
        let a = sqrt(1 - pow(dampingRatio, 2))
        let b = velocity / (a * undampedFrequency)
        let c = dampingRatio / a
        let d = -((b - c) / epsilon)
        if d <= 0 {
            return duration
        }

        duration = log(d) / (dampingRatio * undampedFrequency)
    }

    return duration
}

func computeDampingRatio(tension: Double, friction: Double) -> Double {
    let damping = friction / sqrt(2 * (1 * tension))
    return damping
}
Cocainism answered 14/5, 2018 at 23:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.