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