Determine springWithDamping and initialSpringVelocity based off from friction and tension
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).
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)
}
}