Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine springWithDamping and initialSpringVelocity based off from friction and tension

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 })
like image 736
Joshua Hart Avatar asked Apr 25 '18 19:04

Joshua Hart


3 Answers

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).

like image 159
Niels Avatar answered Oct 31 '22 04:10

Niels


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)
    }
}
like image 33
Kevin Bui Avatar answered Oct 31 '22 03:10

Kevin Bui


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
}
like image 40
Gandalf458 Avatar answered Oct 31 '22 02:10

Gandalf458