Use CAKeyFrameAnimation to simulate deceleration animation

Some time ago, I watched Lottie and wanted to make an animation to exercise my familiarity with animation. Hence this blog post

The demo is named billiards, which means billiards, because the billiard ball will change direction and slow down after being hit by the wall. This demo mimics the slow motion of billiard ball hitting the wall everywhere

Slow motion

  • Deceleration curve

The first is slowing down. The EasyOut series of animations in iOS is a slow motion. Although both are decelerating motions, the final deceleration curve should be different depending on the initial velocity and deceleration acceleration. Since the deceleration curve is not the same, let alone with the system specified animation curve is the same. Therefore, although they are all deceleration motions, the EasyOut series animations of the system should not be able to completely fit the actual deceleration animations. (Deceleration curve is a function of distance and time.)

Given the physical knowledge and constant external force, a function of distance and time for an object with initial velocity is s = v0 * t + 1/2 * a * t^2 where (a<0)

Where v0 is the initial velocity, a is the acceleration, and a is a negative value if the constant force is friction

  • Deceleration curve in iOS frame animation implementation

I chose to animate CAKeyFrameAnimation. See this blog post for more information on the properties of CAKeyFrameAnimation

You can see that there are no time and distance functions in the CAKeyFrameAnimation to implement the deceleration curve

It is easy to get confused here, because there is no concept of object speed in the animation frame, the object in each frame moves the object evenly from the beginning to the end according to the start and end and time. So how do you achieve that change in velocity? The answer is “time warp.”

By adjusting the progress of time to adjust the position of the object, to simulate the change of speed. Progress is a quadratic function (s = v0 * p + 1/2 * a * p^2). However, in iOS frame animation, the relationship between progress and position S within each frame can only be a straight line, so the speed change can be achieved by adjusting the relationship between T and progress, which I understand as “time distortion”. Sometimes time passes fast, the position changes fast, looks fast speed; Sometimes time passes slowly, position changes slowly, looks slow speed.

According to the knowledge of the composite function, s = p, then the relationship between progress and time is p = v0 * t + 1/2 * A * t^2, so you can adjust the progress to achieve the speed change within each frame of animation

TimeFuncs in CAKeyFrameAnimation is where the time and progress relationship is stored

  • Bessel curve

We found a way to realize the speed, but if we look closely at the progress function we rely on to realize the change of speed within each frame, it turns out to be a Bezier curve, not a calculated quadratic function.

Now we need to learn a little bit about Bessel functions, how to convert quadratic functions to Bessel functions. Bessel knowledge

Using Bezier fitting quadratic function, it is easy to find the beginning and end point. The starting point is when the initial velocity is maximum and the displacement is 0, and the end point is when the velocity is zero and the displacement is maximum. The hard part is finding the control points.

According to Bessel’s definition, the connection between the control point and the starting point is the control point of the starting point. What we need is a Bezier curve with one control point, because bezier with one control point is a quadratic function. Bessel with only one control point, which is the intersection of the tangent line of the beginning and end of a quadratic function. In this way, the tangent equation of the starting and ending point can be obtained through the derivative formula of the quadratic function, and then the intersection point, which is the control point of the Bessel in this section, can be calculated.

So we have a Bessel curve that fits the quadratic function of this deceleration, and it’s important to note that we have to scale the Bessel that we get. Because we need Bessel of time and progress, in CAMediaTimingFunction, the range of time is [0, 1], and the range of progress is [0, 1], so we also need scale transformation of control points, so that the start and end points are respectively [0, 0], [1, 1].

Attach the Bessel generation code

    public class func bezierPointsFromMotionParabola(v0: CGFloat.a: CGFloat) - >CGPoint? {
        V0 *t - 1/2 * a *t ^2 = s
        // start point s = 0, t = 0
        / / the end
        let tMax = v0 / a // The speed drops to 0
        let sMax = (v0 * v0) / (2 * a) // Maximum distance
        // The slope of the tangent line at any point in time s' = v = v0-at
        // The tangent equation of the starting point
        let tangentSlopeBegin: CGFloat = v0
        let tangentIntersectionBegin: CGFloat = 0 //beginP.x * v0 + b = beginP.y
        let beginTangentLine = Line(slope: tangentSlopeBegin, intersectionWithY: tangentIntersectionBegin)// Line is a Line class, as shown in the demo code
        // The end of the tangent equation
        // Slope vEnd = v0 - a * tMax = 0
        let tangentAEnd: CGFloat = 0
        let tangentBEnd: CGFloat = 1
        let tangentCEnd: CGFloat = -sMax
        let endTangentLine = Line(a: tangentAEnd, b: tangentBEnd, c: tangentCEnd)
        // The intersection of tangents, according to the Bessel definition, is the Bessel control point
        guard let controlPInST = beginTangentLine.intersection(line: endTangentLine) else {
            return nil
        }
        let controlPInPT = CGPoint(x: controlPInST.x / tMax, y: controlPInST.y / sMax)
        return controlPInPT
    }
Copy the code
  • Piecewise deceleration curve

Unfortunately, we cannot derive the Bessel function from the quadratic function of distance and time determined by initial velocity and acceleration, and use it directly in the timingFunctions of CAKeyframeAnimation. The final path of the ball has many keyframes, and each keyframe is an inflection point, where the ball hits the wall. A time progress function is required between each key frame to determine how the ball moves within the key frame. Bezier fitting remains the same. With an initial velocity, acceleration and a final velocity, bezier functions between frames can still be generated according to the above principle

Attach the segmented Bessel generated code

    public class func bezierPointsFromSegmentMotion(v0: CGFloat.a: CGFloat.vEnd: CGFloat) - >CGPoint? {
        let durtime = (v0 - vEnd) / a // Speed down to vEnd time
        let distance = v0 * durtime - 1/2 * a * pow(durtime, 2) // Total distance
        // The slope of the tangent line at any point in time s' = v = v0-at
        // The tangent equation of the starting point
        let tangentSlopeBegin: CGFloat = v0
        let tangentIntersectionBegin: CGFloat = 0 //beginP.x * v0 + b = beginP.y
        let beginTangentLine = Line(slope: tangentSlopeBegin, intersectionWithY: tangentIntersectionBegin)
        // The end of the tangent equation
        let tangentAEnd: CGFloat = vEnd
        let tangentBEnd: CGFloat = -1
        let tangentCEnd: CGFloat = distance - vEnd * durtime
        let endTangentLine = Line(a: tangentAEnd, b: tangentBEnd, c: tangentCEnd)
        // The intersection of tangents, according to the Bessel definition, is the Bessel control point
        guard let controlPInST = beginTangentLine.intersection(line: endTangentLine) else {
            return nil
        }
        let controlPInPT = CGPoint(x: controlPInST.x / durtime, y: controlPInST.y / distance)
        return controlPInST
    }    
Copy the code

We just need one more final velocity. We can calculate the trajectory of the ball according to the following collision direction, and know the starting position and ending position of the ball of each key frame, so that the duration of the key frame can be calculated according to the [quadratic function of time and distance] and [moving distance] of the key frame, and then the final speed can be calculated. The duration of the keyframe can be collected, transformed, and then used by The keyTimes of CAKeyframeAnimation. The final speed can be passed to the Above Bezier fit func to calculate the time function within the keyframe.

However, to calculate the duration of the key frame according to the [quadratic function of time and distance] and [moving distance] of the key frame can be abstracted into the following mathematical problem:

S = v0 * t – 1/2 * a * t^2 where a>0, s is the dependent variable, t is the independent variable, given v0, a, for a given s, calculate t

It’s not easy. The main difficulty is to extract t as a function of the dependent variable and S as a function of the independent variable.

Finally, THE solution I adopted was the root formula of the quadratic function

The solution of v0 * t – 1/2 * a * t^ 2-s = 0 is [v0 – SQRT (v0^ 2-2 * a * s)] / a

So that’s how long it takes for a given initial velocity, a given acceleration, to travel a given distance

Collision to

The mathematical model of collision steering problem is the reflection of vector in plane. First we need to determine the reflection plane and then calculate the reflection vector

  • Determine the plane of reflection

The solution I adopted was as follows: according to the positivity and negativity of the two directions of the vector, the two suspicious sides could be determined. For example, a vector with positive x and y directions and its starting point was in a rectangle, the derivative line of this vector direction must only intersect the rectangle at bottom or right.

Then the line between the starting point and the lower right corner forms a line, judging the line formed by the starting point and the velocity vector, which side of the line formed by the starting point and the lower right corner, you can determine whether to cross with bottom or right. I’m going to use the slope to determine whether I’m leaning toward x or toward y, and then I’m going to use these two edges, and I’m going to know which side I’m crossing

  • The reflection vector

The following two pictures illustrate the collision boundary of the ball and the principle of finding the reflection vector

As can be seen from the figure, we only need to take the point on the reflection ray as the starting point of the vector, and then calculate the end point of the vector, and then calculate the symmetric point of the end point of the vector according to the reflection ray, and the line between the symmetric point and the starting point of the vector is the reflection vector

To calculate the symmetry point, I first calculate the straight line perpendicular to the reflection ray and passing through the end point of the vector, and then calculate the intersection point of the vertical line and the reflection ray. The intermediate point of the symmetry point and the end point of the vector is the intersection point just calculated, and then the symmetry point can be calculated.

public extension CGVector {
    public func reflexVector(line: Line) -> CGVector {
        if line.a == 0 {
            assert(line.b ! =0)
            return CGVector(dx: dx, dy: -dy)
        }
        let beginP = CGPoint(x: -line.c / line.a, y: 0) // Use the intersection of line and x as the starting point of the vector
        let vectorEndP = CGPoint(x: dx + beginP.x, y: dy + beginP.y)
        let c = -line.b * vectorEndP.x + line.a * vectorEndP.y
        let verticalLine = Line(a: line.b, b: -line.a, c: c)
        // The vector is perpendicular to line
        guard let intersectionP = line.intersection(line: verticalLine) else {
            assertionFailure(a)return CGVector(dx: 0, dy: 0)}let reflexP = CGPoint(x: 2 * intersectionP.x - vectorEndP.x, y: 2 * intersectionP.y - vectorEndP.y)
        let reflexVector = CGVector(dx: reflexP.x - beginP.x, dy: reflexP.y - beginP.y)
        return reflexVector
    }
}
Copy the code

Use CAKeyframeAnimation to combine information to animate

Through the above calculation, we can get the path of the ball, the time of each section of movement, and the movement curve of each section of movement. Use this information to generate a CAKeyframeAnimation

        let durtimes: [CGFloat] // The length of each frame
        let timeFuncs: [CAMediaTimingFunction] // The motion curve of each frame
        let path: UIBezierPath  // Ball path

        // The keyTimes of CAKeyframeAnimation is the progress of each frame in the whole [0, 0
        // Calculate the total time
        let sumTime = durtimes.reduce(0) { (result, item) -> CGFloat in
            return result + item
        }
        // Calculate the end time of each frame
        let accumTimes = durtimes.reduce([CGFloat]()) { (result, item) -> [CGFloat] in
            var resultV = result
            if result.count > 0 {
                let last = result[result.count - 1]
                resultV.append(last + item)
            } else {
                resultV.append(item)
            }
            return resultV
        }
        // Convert to progress
        var keyTimes = accumTimes.map { (item) -> NSNumber in
            return NSNumber(floatLiteral: Double(item/sumTime))
        }
        //keyTimes is the start time of each frame, plus the last 1, where 0 is inserted before the end time of each frame
        keyTimes.insert(NSNumber(value: 0), at: 0)
        
        let animate = CAKeyframeAnimation(keyPath: "position")
        animate.path = path.cgPath
        animate.keyTimes = keyTimes
        animate.timingFunctions = timeFuncs
        animate.duration = CFTimeInterval(sumTime)
Copy the code