根据摩擦和张力确定 springWithDamping 和 initialSpringVelocity

Determine springWithDamping and initialSpringVelocity based off from friction and tension

我的设计团队使用摩擦力和张力给我们动画参数。例如:

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

不幸的是,我一直在猜测这些值会转换成什么,然后观察它,如果它看起来很接近,我会把它发过去,他们会批准。但是不断构建具有不同值的项目所花费的时间非常耗时。必须有更简单的方法。

我在 Github 上找到了 Framer,这让我相信阻尼可以这样计算:

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

但是,我似乎无法弄清楚如何根据摩擦力和张力来计算速度。有没有更简单的方法可以为这个开发人员节省一些宝贵的时间?

动画示例:

UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: damping,
                       initialSpringVelocity: ???, options: .curveEaseIn, animations: { // Do Stuff })

您说得对,您链接到的代码可用于计算阻尼比(我受宠若惊,因为我是编写它的人;)。不过,您的派生 Swift 代码似乎有错误。我认为应该是(注意括号中的区别):

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

只有当你想在开始动画时给对象一些初始速度时才需要速度值。这样做的用例是,如果对象在开始动画时已经在移动(例如,在拖动交互后开始动画时)。

所以如果对象从非移动状态开始动画,您可以只使用 0 作为初始速度。

我对你的设计团队给你的紧张、摩擦 持续时间感到困惑。因为 springs 是模拟物理,张力和摩擦力将模拟一个 spring,它将在特定持续时间后停止动画。具有张力 280 和摩擦 20.5 的 spring 导致持续时间接近 0.65,而不是 0.3。 (参见 Framer 中的 computeDuration 函数如何根据张力和摩擦计算持续时间)。这是 coffeescript 版本:

# 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

您可以为 iOS 使用的 spring 指定持续时间的原因是它根据阻尼比和持续时间计算 spring 的张力和摩擦力.在引擎盖下,它仍将使用张力和摩擦力进行 spring 模拟。要了解该代码在 iOS 中的工作原理,请查看 Framer 中的 computeDerivedCurveOptions,它是 iOS 使用的代码的直接端口(通过反汇编和分析 iOS 二进制文件)。

我将@Niels 代码翻译成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
}

我将其转换为方便的 UIView 扩展,这样您就可以直接调用 UIView.animate 并产生摩擦。

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

有一种更简单的方法可以直接使用摩擦力和张力来实现这种动画,无需任何计算。

UISpringTimingParameters,我们用UIViewPropertyAnimator

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

注意刚度是张力,阻尼是摩擦。

  1. 创建动画师并添加动画
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: springParameters)
animator.addAnimations {
    animations()
}
animator.startAnimation(afterDelay: delay)

所以,可以做一个方便的扩展:

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