How to animate a quadratic curve change in SwiftUI

Continuing on the exploration of animating Paths in SwiftUI. This article shows how to animate the movement of a quadratic curved line from one position to another. The smooth transition from a straight line to a curved line is also shown by adjusting the control point.



Animated Quadratic Curve Change

In How to animate a line move in SwiftUI we defined an AnimatableSegment that conforms to VectorArithmetic protocol, so it can be used in the animatableData instance property of an animatable line segment Shape. We now do the same for the quadratic curve path. This code defines AnimatableQuadCurve that conforms to VectorArithmetic. It takes three CGPoints for the start point, end point and the quadratic curve control point. With the three points that define a quadratic curve in a struct that conforms to VectorArithmetic, it can be used as the animatableData for a shape so SwiftUI will be able to animate transition from one quadratic curve to another.

 1struct AnimatableQuadCurve : VectorArithmetic {
 2    var start: CGPoint
 3    var end: CGPoint
 4    var control: CGPoint
 5
 6    var length: Double {
 7        return Double(((end.x - start.x) * (end.x - start.x)) +
 8                        ((end.y - start.y) * (end.y - start.y))).squareRoot()
 9    }
10
11    var magnitudeSquared: Double {
12        return length * length
13    }
14
15    mutating func scale(by rhs: Double) {
16        self.start.x.scale(by: rhs)
17        self.start.y.scale(by: rhs)
18        self.end.x.scale(by: rhs)
19        self.end.y.scale(by: rhs)
20        self.control.x.scale(by: rhs)
21        self.control.y.scale(by: rhs)
22    }
23
24    static var zero: AnimatableQuadCurve {
25        return AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 0.0),
26                                   end: CGPoint(x: 0.0, y: 0.0),
27                                   control: CGPoint(x: 0.0, y: 0.0))
28    }
29
30    static func - (lhs: AnimatableQuadCurve, rhs: AnimatableQuadCurve) -> AnimatableQuadCurve {
31        return AnimatableQuadCurve(
32            start: CGPoint(
33                x: lhs.start.x - rhs.start.x,
34                y: lhs.start.y - rhs.start.y),
35            end: CGPoint(
36                x: lhs.end.x - rhs.end.x,
37                y: lhs.end.y - rhs.end.y),
38            control: CGPoint(
39                x: lhs.control.x - rhs.control.x,
40                y: lhs.control.y - rhs.control.y))
41    }
42
43    static func -= (lhs: inout AnimatableQuadCurve, rhs: AnimatableQuadCurve) {
44        lhs = lhs - rhs
45    }
46
47    static func + (lhs: AnimatableQuadCurve, rhs: AnimatableQuadCurve) -> AnimatableQuadCurve {
48        return AnimatableQuadCurve(
49            start: CGPoint(
50                x: lhs.start.x + rhs.start.x,
51                y: lhs.start.y + rhs.start.y),
52            end: CGPoint(
53                x: lhs.end.x + rhs.end.x,
54                y: lhs.end.y + rhs.end.y),
55            control: CGPoint(
56                x: lhs.control.x + rhs.control.x,
57                y: lhs.control.y + rhs.control.y))
58    }
59
60    static func += (lhs: inout AnimatableQuadCurve, rhs: AnimatableQuadCurve) {
61        lhs = lhs + rhs
62    }
63}

A QuadCurveShape is defined that uses the AnimatableQuadCurve as the animatableData and implements the Path function for the quadratic curve from the start point to the end point with the specified control point. The points are defined with coordinates between 0.0 and 1.0 and the path adjusted to fit inside the allocated frame for the shape.

 1struct QuadCurveShape: Shape {
 2    var startPoint: CGPoint
 3    var endPoint: CGPoint
 4    var controlPoint: CGPoint
 5
 6    private var animatableSegment: AnimatableQuadCurve
 7
 8    var animatableData: AnimatableQuadCurve {
 9        get { AnimatableQuadCurve(
10            start: startPoint, end: endPoint, control: controlPoint) }
11        set {
12            startPoint = newValue.start
13            endPoint = newValue.end
14            controlPoint = newValue.control
15        }
16    }
17
18    init(startPoint: CGPoint, endPoint: CGPoint, controlPoint: CGPoint) {
19        self.startPoint = startPoint
20        self.endPoint = endPoint
21        self.controlPoint = controlPoint
22        self.animatableSegment = AnimatableQuadCurve(
23            start: startPoint,
24            end: endPoint,
25            control: controlPoint)
26    }
27
28    func path(in rect: CGRect) -> Path {
29        let start = CGPoint(x: startPoint.x * rect.width,
30                            y: startPoint.y * rect.height)
31        let end = CGPoint(x: endPoint.x * rect.width,
32                          y: endPoint.y * rect.height)
33        let control = CGPoint(x: controlPoint.x * rect.width,
34                          y: controlPoint.y * rect.height)
35        var path = Path()
36        path.move(to: start)
37        path.addQuadCurve(to: end, control: control)
38        return path
39    }
40}

This code shows the use of the QuadCurveShape in a view smoothly animating when the button is clicked.

 1struct QuadCurveView1: View {
 2    @State private var changeCurve = true
 3
 4    var body: some View {
 5        VStack(spacing:50) {
 6            Text("Animatable Quadratic Curve")
 7
 8            HStack {
 9                QuadCurveShape(startPoint: CGPoint(
10                                x: changeCurve ? 0.2 : 0.8,
11                                y: changeCurve ? 0.3 : 0.9),
12                               endPoint: CGPoint(
13                                x: changeCurve ? 0.5 : 0.9,
14                                y: changeCurve ? 0.9 : 0.4),
15                               controlPoint: CGPoint(x: 0.5, y: 0.2))
16                    .stroke(changeCurve ? Color(.red) : .purple, lineWidth: 4.0)
17                    .frame(width: 200, height: 250)
18                    .animation(.linear(duration: 1))
19
20                QuadCurveShape(startPoint: CGPoint(
21                                x: 0.5,
22                                y: changeCurve ? 0.1 : 0.9),
23                               endPoint: CGPoint(
24                                x: 0.5,
25                                y: changeCurve ? 0.9 : 0.1),
26                               controlPoint: CGPoint(
27                                x: changeCurve ? 0.1 : 0.9,
28                                y: 0.5))
29                    .stroke(changeCurve ? Color(.red) : .purple, lineWidth: 4.0)
30                    .frame(width: 200, height: 250)
31                    .animation(.linear(duration: 1))
32
33                QuadCurveShape(startPoint: CGPoint(
34                                x: 0.5,
35                                y: 0.9),
36                               endPoint: CGPoint(
37                                x: 0.5,
38                                y: 0.1),
39                               controlPoint: CGPoint(
40                                x: changeCurve ? 0.1 : 0.9,
41                                y: 0.5))
42                    .stroke(changeCurve ? Color(.red) : .purple, lineWidth: 4.0)
43                    .frame(width: 200, height: 250)
44                    .animation(.linear(duration: 1))
45            }
46
47            Button("Change Curve") {
48                changeCurve.toggle()
49            }
50            .buttonStyle(BlueButtonStyle())
51
52            Spacer()
53        }
54    }
55}

Animate movement of quadratic curves

Animate movement of quadratic curves



Add initializer to take AnimatableQuadCurve

In a view with QuadCurveShape all three points have to be specified. It would be nice to be able to define a number of curves and set the animation to change from one curve to another. This is done by adding another initializer to the QuadCurveShape that takes an AnimatableQuadCurve as the only parameter.

 1struct QuadCurveShape: Shape {
 2
 3    ...
 4
 5    init(startPoint: CGPoint, endPoint: CGPoint, controlPoint: CGPoint) {
 6        self.startPoint = startPoint
 7        self.endPoint = endPoint
 8        self.controlPoint = controlPoint
 9        self.animatableSegment = AnimatableQuadCurve(
10            start: startPoint,
11            end: endPoint,
12            control: controlPoint)
13    }
14
15    init(curve: AnimatableQuadCurve) {
16        self.init(startPoint: curve.start, endPoint: curve.end, controlPoint: curve.control)
17    }
18
19    ...

With this change to QuadCurveShape, it is possible to define curve shapes and switch from one curve to another. The change in curve is smoothly animated.

 1struct QuadCurveView2: View {
 2    @State private var changeCurve = true
 3
 4    let quadCurve1 = AnimatableQuadCurve(
 5        start: CGPoint(x: 0.2, y: 0.8),
 6        end: CGPoint(x: 0.8, y: 0.9),
 7        control: CGPoint(x: 0.5, y: 0.2))
 8
 9    let quadCurve2 = AnimatableQuadCurve(
10        start: CGPoint(x: 0.1, y: 0.2),
11        end: CGPoint(x: 0.9, y: 0.2),
12        control: CGPoint(x: 0.6, y: 1.2))
13
14    var body: some View {
15        VStack(spacing:50)  {
16            Text("Animatable Quadratic Curve")
17
18            QuadCurveShape(curve: changeCurve ? quadCurve1 : quadCurve2)
19                .stroke(changeCurve ? Color(.red) : .purple, lineWidth: 4.0)
20                .frame(width: 450, height: 250)
21                .animation(.linear(duration: 1))
22
23            Button("Change Curve") {
24                changeCurve.toggle()
25            }
26            .buttonStyle(BlueButtonStyle())
27
28            Spacer()
29        }
30    }
31}

Animate movement of a quadratic curve

Animate movement of a quadratic curve



Animate Quadratic Curved line to Straight line

Is is possible to use the AnimatableQuadCurve structure to specify a straight line if the control point is on the straight line between the two end points. This code defines three AnimatableQuadCurves that change the control point through the line between the two endpoints. The change from a curved line to a straight line is animated. The color is cycled through three colors to demonstrate how the line position and color are changed simultaneously.

 1struct CurveToLineView: View {
 2    @State private var index = 0
 3
 4    let colors:[Color] = [.red, .green, .blue]
 5    let quadCurves = [
 6        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 1.0), end: CGPoint(x: 1.0, y: 0.0), control: CGPoint(x: 0.0, y: 0.0)),
 7        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 1.0), end: CGPoint(x: 1.0, y: 0.0), control: CGPoint(x: 0.5, y: 0.5)),
 8        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 1.0), end: CGPoint(x: 1.0, y: 0.0), control: CGPoint(x: 1.0, y: 1.0))
 9    ]
10
11    var body: some View {
12        VStack(spacing:50) {
13            Text("Animatable Curve to Line")
14
15            QuadCurveShape(curve: quadCurves[index])
16                .stroke(colors[index], lineWidth: 4.0)
17                .frame(width: 450, height: 250)
18                .animation(.linear(duration: 1))
19
20            Button("Change Curve") {
21                self.index = (self.index + 1) % 3
22            }
23            .buttonStyle(BlueButtonStyle())
24
25            Spacer()
26        }
27    }
28}

Animate movement of a quadratic curve to a straight line

Animate movement of a quadratic curve to a straight line



Diamond Shape

The diamond shape can be created by combining four quadratic curves. A view is created by combining four quadratic curves in a ZStack. The diamond can be animated by animating each curve change.

 1struct DiamondView: View {
 2    @State private var index = 0
 3
 4    let colors:[Color] = [.red, .orange, .yellow, .green]
 5    let diamond = [
 6        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 0.0), end: CGPoint(x: 1.0, y: 0.5), control: CGPoint(x: 0.6, y: 0.4)),
 7        AnimatableQuadCurve(start: CGPoint(x: 1.0, y: 0.5), end: CGPoint(x: 0.5, y: 1.0), control: CGPoint(x: 0.6, y: 0.6)),
 8        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 1.0), end: CGPoint(x: 0.0, y: 0.5), control: CGPoint(x: 0.4, y: 0.6)),
 9        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 0.5), end: CGPoint(x: 0.5, y: 0.0), control: CGPoint(x: 0.4, y: 0.4))
10    ]
11
12    var body: some View {
13        VStack(spacing:50) {
14            Text("Animatable Diamond Shape")
15
16            ZStack {
17                ForEach(0..<diamond.count) { i in
18                    QuadCurveShape(curve: diamond[(i+index)%diamond.count])
19                        .stroke(colors[index], lineWidth: 8.0)
20                }
21            }
22            .frame(width: 300, height: 300)
23            .animation(.linear(duration: 1))
24
25            Button("Change") {
26                self.index = (self.index + 1) % diamond.count
27            }
28            .buttonStyle(BlueButtonStyle())
29
30            Spacer()
31        }
32    }
33}

Animate rotation of diamond shape by moving quadratic curves

Animate rotation of diamond shape by moving quadratic curves



Animate shape change

The animation can be modified by changing the quadratic curves that are moved. This code defines three shapes and animates change between them changing the AnimatableQuadCurves.

 1struct ShapeChangeView: View {
 2    @State private var shapeIndex = 0
 3    @State private var colorIndex = 0
 4    @State private var index1 = 0
 5    @State private var index2 = 0
 6
 7    let colors:[Color] = [.red, .orange, .yellow, .green, .blue, .purple]
 8    let shapes = [[
 9        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 0.0), end: CGPoint(x: 1.0, y: 0.5), control: CGPoint(x: 0.6, y: 0.4)),
10        AnimatableQuadCurve(start: CGPoint(x: 1.0, y: 0.5), end: CGPoint(x: 0.5, y: 1.0), control: CGPoint(x: 0.6, y: 0.6)),
11        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 1.0), end: CGPoint(x: 0.0, y: 0.5), control: CGPoint(x: 0.4, y: 0.6)),
12        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 0.5), end: CGPoint(x: 0.5, y: 0.0), control: CGPoint(x: 0.4, y: 0.4))
13    ],
14    [
15        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 0.0), end: CGPoint(x: 1.0, y: 0.5), control: CGPoint(x: 0.75, y: 0.25)),
16        AnimatableQuadCurve(start: CGPoint(x: 1.0, y: 0.5), end: CGPoint(x: 0.5, y: 1.0), control: CGPoint(x: 0.75, y: 0.75)),
17        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 1.0), end: CGPoint(x: 0.0, y: 0.5), control: CGPoint(x: 0.25, y: 0.75)),
18        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 0.5), end: CGPoint(x: 0.5, y: 0.0), control: CGPoint(x: 0.25, y: 0.25))
19    ],
20    [
21        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 0.0), end: CGPoint(x: 1.0, y: 0.5), control: CGPoint(x: 1.0, y: 0.0)),
22        AnimatableQuadCurve(start: CGPoint(x: 1.0, y: 0.5), end: CGPoint(x: 0.5, y: 1.0), control: CGPoint(x: 1.0, y: 1.0)),
23        AnimatableQuadCurve(start: CGPoint(x: 0.5, y: 1.0), end: CGPoint(x: 0.0, y: 0.5), control: CGPoint(x: 0.0, y: 1.0)),
24        AnimatableQuadCurve(start: CGPoint(x: 0.0, y: 0.5), end: CGPoint(x: 0.5, y: 0.0), control: CGPoint(x: 0.0, y: 0.0))
25    ]]
26
27    var body: some View {
28        VStack(spacing:120) {
29            Text("Animatable Quadratic Curve")
30
31            HStack(spacing:50) {                
32                ZStack {
33                    ForEach(0..<4) { i in
34                        QuadCurveShape(curve: shapes[shapeIndex][i])
35                            .stroke(colors[colorIndex], lineWidth: 6.0)
36                    }
37                }
38                .frame(width: 250, height: 250)
39                .animation(.linear(duration: 1))
40
41                ZStack {
42                    ForEach(0..<4) { i in
43                        QuadCurveShape(curve: shapes[shapeIndex][(i+index1)%4])
44                            .stroke(colors[colorIndex], lineWidth: 6.0)
45                    }
46                }
47                .frame(width: 250, height: 250)
48                .animation(.linear(duration: 1))
49
50                ZStack {
51                    ForEach(0..<4) { i in
52                        QuadCurveShape(curve: shapes[shapeIndex][(i+index2)%4])
53                            .stroke(colors[colorIndex], lineWidth: 6.0)
54                    }
55                }
56                .frame(width: 250, height: 250)
57                .animation(.linear(duration: 1))
58            }
59
60            Button("Change") {
61                self.shapeIndex = (self.shapeIndex + 1) % self.shapes.count
62                self.colorIndex = (self.colorIndex + 1) % self.colors.count
63                self.index1 = (self.index1 + 1) % 4
64                self.index2 = (self.index2 + 2) % 4
65            }
66            .buttonStyle(BlueButtonStyle())
67
68            Spacer()
69        }
70    }
71}

Animate rotation and shape changing by moving quadratic curves

Animate rotation and shape changing by moving quadratic curves




Conclusion

Animation works well when there is a predefined transition from one state to another such as a color change. Animation of a quadratic curve requires the animation of six values - the coordinates of the start, end and control points of the quadratic curve path. This is achieved by defining a structure for AnimatableQuadCurve that conforms to the VectorArithmetic protocol and using this new type as the AnimatableData in a shape structure. The QuadCurveShape structure can be used in views and combined with other view layouts to create interesting animations.