How to animate a Cubic Curve change in SwiftUI
Continuing on the exploration of animating Paths in SwiftUI. This article shows how to animate the movement of a cubic Bézier curve from one position to another. The smooth transition from a straight line to a curved line is also shown by adjusting the two control points.
The path structure can be used to create a cubic Bézier curve using the addCurve function that takes the end point and two control points as parameters.
Animate Cubic Curve Shape
First define a struct AnimatableCubicCurve
that conforms to VectorArithmetic
protocol. This is similar to the AnimatableQuadCurve
that was defined in How to
animate a quadratic curve change in SwiftUI.
1struct AnimatableCubicCurve : VectorArithmetic {
2 var start: CGPoint
3 var end: CGPoint
4 var control1: CGPoint
5 var control2: CGPoint
6
7 var length: Double {
8 return Double(((end.x - start.x) * (end.x - start.x)) +
9 ((end.y - start.y) * (end.y - start.y))).squareRoot()
10 }
11
12 var magnitudeSquared: Double {
13 return length * length
14 }
15
16 mutating func scale(by rhs: Double) {
17 self.start.x.scale(by: rhs)
18 self.start.y.scale(by: rhs)
19 self.end.x.scale(by: rhs)
20 self.end.y.scale(by: rhs)
21 self.control1.x.scale(by: rhs)
22 self.control1.y.scale(by: rhs)
23 self.control2.x.scale(by: rhs)
24 self.control2.y.scale(by: rhs)
25 }
26
27 static var zero: AnimatableCubicCurve {
28 return AnimatableCubicCurve(start: CGPoint(x: 0.0, y: 0.0),
29 end: CGPoint(x: 0.0, y: 0.0),
30 control1: CGPoint(x: 0.0, y: 0.0),
31 control2: CGPoint(x: 0.0, y: 0.0))
32 }
33
34 static func - (lhs: AnimatableCubicCurve, rhs: AnimatableCubicCurve) -> AnimatableCubicCurve {
35 return AnimatableCubicCurve(
36 start: CGPoint(
37 x: lhs.start.x - rhs.start.x,
38 y: lhs.start.y - rhs.start.y),
39 end: CGPoint(
40 x: lhs.end.x - rhs.end.x,
41 y: lhs.end.y - rhs.end.y),
42 control1: CGPoint(
43 x: lhs.control1.x - rhs.control1.x,
44 y: lhs.control1.y - rhs.control1.y),
45 control2: CGPoint(
46 x: lhs.control2.x - rhs.control2.x,
47 y: lhs.control2.y - rhs.control2.y))
48 }
49
50 static func -= (lhs: inout AnimatableCubicCurve, rhs: AnimatableCubicCurve) {
51 lhs = lhs - rhs
52 }
53
54 static func + (lhs: AnimatableCubicCurve, rhs: AnimatableCubicCurve) -> AnimatableCubicCurve {
55 return AnimatableCubicCurve(
56 start: CGPoint(
57 x: lhs.start.x + rhs.start.x,
58 y: lhs.start.y + rhs.start.y),
59 end: CGPoint(
60 x: lhs.end.x + rhs.end.x,
61 y: lhs.end.y + rhs.end.y),
62 control1: CGPoint(
63 x: lhs.control1.x + rhs.control1.x,
64 y: lhs.control1.y + rhs.control1.y),
65 control2: CGPoint(
66 x: lhs.control2.x + rhs.control2.x,
67 y: lhs.control2.y + rhs.control2.y))
68 }
69
70 static func += (lhs: inout AnimatableCubicCurve, rhs: AnimatableCubicCurve) {
71 lhs = lhs + rhs
72 }
73}
Next, define a CubicCurveShape
Shape that uses the AnimatableCubicCurve
for the
AnimatableData. This can be constructed either with the four points or with an
AnimatableCubicCurve.
1struct CubicCurveShape: Shape {
2 var startPoint: CGPoint
3 var endPoint: CGPoint
4 var controlPoint1: CGPoint
5 var controlPoint2: CGPoint
6
7 private var animatableSegment: AnimatableCubicCurve
8
9 var animatableData: AnimatableCubicCurve {
10 get { AnimatableCubicCurve(
11 start: startPoint, end: endPoint, control1: controlPoint1, control2: controlPoint2) }
12 set {
13 startPoint = newValue.start
14 endPoint = newValue.end
15 controlPoint1 = newValue.control1
16 controlPoint2 = newValue.control2
17 }
18 }
19
20 init(startPoint: CGPoint, endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) {
21 self.startPoint = startPoint
22 self.endPoint = endPoint
23 self.controlPoint1 = controlPoint1
24 self.controlPoint2 = controlPoint2
25 self.animatableSegment = AnimatableCubicCurve(
26 start: startPoint,
27 end: endPoint,
28 control1: controlPoint1,
29 control2: controlPoint2)
30 }
31
32 init(curve: AnimatableCubicCurve) {
33 self.init(startPoint: curve.start,
34 endPoint: curve.end,
35 controlPoint1: curve.control1,
36 controlPoint2: curve.control2)
37 }
38
39 func path(in rect: CGRect) -> Path {
40 let start = CGPoint(x: startPoint.x * rect.width,
41 y: startPoint.y * rect.height)
42 let end = CGPoint(x: rect.width * endPoint.x,
43 y: rect.height * endPoint.y)
44 let control1 = CGPoint(x: rect.width * controlPoint1.x,
45 y: rect.height * controlPoint1.y)
46 let control2 = CGPoint(x: rect.width * controlPoint2.x,
47 y: rect.height * controlPoint2.y)
48
49 var path = Path()
50 path.move(to: start)
51 path.addCurve(to: end, control1: control1, control2: control2)
52 return path
53 }
54}
Display AnimatableCubicCurve
Two cubic curves are defined and CubicCurveShape
is used to transition from one curve
to another when the button is selected. The line move is animated as well as the
color change. The start point, end point and control points all transition smoothly
from one location to another.
1struct CurveView1: View {
2 @State private var changeCurve = true
3
4 let segment1 = AnimatableCubicCurve(
5 start: CGPoint(x: 0.8, y: 0.0),
6 end: CGPoint(x: 0.2, y: 1.0),
7 control1: CGPoint(x: 0.1, y: 0.25),
8 control2: CGPoint(x: 0.9, y: 0.75)
9 )
10 let segment2 = AnimatableCubicCurve(
11 start: CGPoint(x: 0.2, y: 0.9),
12 end: CGPoint(x: 0.9, y: 0.7),
13 control1: CGPoint(x: 1.0, y: 1.0),
14 control2: CGPoint(x: 0.0, y: 0.0)
15 )
16
17 var body: some View {
18 VStack(spacing:30) {
19 Text("Animatable Cubic Curve")
20
21 CubicCurveShape(curve: changeCurve ? segment1 : segment2)
22 .stroke(changeCurve ? Color(.purple) : .orange, lineWidth: 4.0)
23 .frame(width: 250, height: 250)
24 .animation(.linear(duration: 1))
25
26 Button("Change") {
27 changeCurve.toggle()
28 }
29 .buttonStyle(BlueButtonStyle())
30
31 Spacer()
32 }
33 }
34}
Animate a cubic curve transition
Animate a cubic curve
Animate a sequence of curve lines
An array of AnimatableCubicCurve
is defined and the index of the selected curve is
changed with a Timer. The timer is set to update every second and increment the
index of the selected curve some the series of Cubic curves is cycled through and
each transition is animated.
1struct curves {
2 static let segments = createSegments()
3 static func createSegments() -> [AnimatableCubicCurve] {
4 var segs = [AnimatableCubicCurve]()
5 for i in (0...5) {
6 let seg = AnimatableCubicCurve(
7 start: CGPoint(x: 0.0 + (0.2 * Double(i)), y: 0.0 + (0.2 * Double(i))),
8 end: CGPoint(x: 1.0 - (0.2 * Double(i)), y: 1.0 - (0.2 * Double(i))),
9 control1: CGPoint(x: 0.0, y: 0.0 + (0.2 * Double(i))),
10 control2: CGPoint(x: 1.0, y: 1.0 - (0.2 * Double(i))))
11 segs.append(seg)
12 }
13 for i in (0...5) {
14 let seg = AnimatableCubicCurve(
15 start: CGPoint(x: 1.0, y: 1.0 - (0.2 * Double(i))),
16 end: CGPoint(x: 0.0, y: 0.0 + (0.2 * Double(i))),
17 control1: CGPoint(x: 1.0 - (0.2 * Double(i)), y: 1.0 - (0.2 * Double(i))),
18 control2: CGPoint(x: 1.0 - (0.2 * Double(i)), y: 0.0 + (0.2 * Double(i))))
19 segs.append(seg)
20 }
21 return segs
22 }
23}
1struct CurveView2: View {
2 @State private var index = 0
3 @State private var counter = 0
4
5 let timer = Timer.publish(every: 1,
6 on: .main,
7 in: .common)
8 .autoconnect()
9 let colors:[Color] = [.red, .orange, .yellow, .green, .blue, .purple]
10 let segments = curves.segments
11
12 var body: some View {
13 VStack(spacing:30) {
14 Text("Animatable Cubic Curve")
15
16 CubicCurveShape(curve: segments[index])
17 .stroke(colors[index % colors.count], lineWidth: 4.0)
18 .frame(width: 250, height: 300)
19 .animation(.linear(duration: 1))
20 .onReceive(timer) { time in
21 if self.counter == (segments.count * 3) {
22 self.timer.upstream.connect().cancel()
23 } else {
24 self.index = (self.index + 1) % segments.count
25 }
26
27 self.counter += 1
28 }
29
30 Spacer()
31 }
32 }
33}
Animate a series of curves
Combining curves to make a shape
This code defines the shapes of the Playing card suits as an array of
AnimatableCubicCurve
s. These are variations on the shapes used in Create a blob
shape in SwiftUI except All segments of the shapes are defined using
AnimatableCubicCurve
.
1struct Suit {
2 static let spade = [
3 AnimatableCubicCurve(start: CGPoint(x: 0.50, y: 0.00), end: CGPoint(x: 0.85, y: 0.40), control1: CGPoint(x: 0.60, y: 0.30), control2: CGPoint(x: 0.60, y: 0.30)),
4 AnimatableCubicCurve(start: CGPoint(x: 0.85, y: 0.40), end: CGPoint(x: 0.55, y: 0.65), control1: CGPoint(x: 1.15, y: 0.50), control2: CGPoint(x: 0.95, y: 1.20)),
5 AnimatableCubicCurve(start: CGPoint(x: 0.55, y: 0.65), end: CGPoint(x: 0.70, y: 0.90), control1: CGPoint(x: 0.55, y: 0.80), control2: CGPoint(x: 0.55, y: 0.80)),
6 AnimatableCubicCurve(start: CGPoint(x: 0.70, y: 0.90), end: CGPoint(x: 0.70, y: 1.00), control1: CGPoint(x: 0.80, y: 0.98), control2: CGPoint(x: 0.80, y: 0.98)),
7 AnimatableCubicCurve(start: CGPoint(x: 0.70, y: 1.00), end: CGPoint(x: 0.30, y: 1.00), control1: CGPoint(x: 0.70, y: 1.00), control2: CGPoint(x: 0.70, y: 1.00)),
8 AnimatableCubicCurve(start: CGPoint(x: 0.30, y: 1.00), end: CGPoint(x: 0.30, y: 0.90), control1: CGPoint(x: 0.20, y: 0.98), control2: CGPoint(x: 0.20, y: 0.98)),
9 AnimatableCubicCurve(start: CGPoint(x: 0.30, y: 0.90), end: CGPoint(x: 0.45, y: 0.65), control1: CGPoint(x: 0.45, y: 0.80), control2: CGPoint(x: 0.45, y: 0.80)),
10 AnimatableCubicCurve(start: CGPoint(x: 0.45, y: 0.65), end: CGPoint(x: 0.15, y: 0.40), control1: CGPoint(x: 0.05, y: 1.20), control2: CGPoint(x: -0.15, y: 0.50)),
11 AnimatableCubicCurve(start: CGPoint(x: 0.15, y: 0.40), end: CGPoint(x: 0.50, y: 0.00), control1: CGPoint(x: 0.40, y: 0.30), control2: CGPoint(x: 0.40, y: 0.30))
12 ]
13
14 static let club = [
15 AnimatableCubicCurve(start: CGPoint(x: 0.30, y: 0.30), end: CGPoint(x: 0.70, y: 0.30), control1: CGPoint(x: 0.0, y: -0.1), control2: CGPoint(x: 1.0, y: -0.1)),
16 AnimatableCubicCurve(start: CGPoint(x: 0.70, y: 0.30), end: CGPoint(x: 0.55, y: 0.45), control1: CGPoint(x: 0.70, y: 0.30), control2: CGPoint(x: 0.70, y: 0.30)),
17 AnimatableCubicCurve(start: CGPoint(x: 0.55, y: 0.45), end: CGPoint(x: 0.55, y: 0.70), control1: CGPoint(x: 1.15, y: 0.0), control2: CGPoint(x: 1.15, y: 1.20)),
18 AnimatableCubicCurve(start: CGPoint(x: 0.55, y: 0.70), end: CGPoint(x: 0.70, y: 1.0), control1: CGPoint(x: 0.55, y: 0.70), control2: CGPoint(x: 0.55, y: 0.70)),
19 AnimatableCubicCurve(start: CGPoint(x: 0.70, y: 1.0), end: CGPoint(x: 0.30, y: 1.0), control1: CGPoint(x: 0.70, y: 1.0), control2: CGPoint(x: 0.70, y: 1.0)),
20 AnimatableCubicCurve(start: CGPoint(x: 0.30, y: 1.0), end: CGPoint(x: 0.45, y: 0.70), control1: CGPoint(x: 0.30, y: 1.0), control2: CGPoint(x: 0.30, y: 1.0)),
21 AnimatableCubicCurve(start: CGPoint(x: 0.45, y: 0.70), end: CGPoint(x: 0.45, y: 0.45), control1: CGPoint(x: -0.15, y: 1.2), control2: CGPoint(x: -0.15, y: 0.0)),
22 AnimatableCubicCurve(start: CGPoint(x: 0.45, y: 0.45), end: CGPoint(x: 0.3, y: 0.3), control1: CGPoint(x: 0.45, y: 0.45), control2: CGPoint(x: 0.45, y: 0.45))
23 ]
24
25 static let diamond = [
26 AnimatableCubicCurve(start: CGPoint(x: 0.5, y: 0.0), end: CGPoint(x: 1.0, y: 0.5), control1: CGPoint(x: 0.7, y: 0.3), control2: CGPoint(x: 0.7, y: 0.3)),
27 AnimatableCubicCurve(start: CGPoint(x: 1.0, y: 0.5), end: CGPoint(x: 0.5, y: 1.0), control1: CGPoint(x: 0.7, y: 0.7), control2: CGPoint(x: 0.7, y: 0.7)),
28 AnimatableCubicCurve(start: CGPoint(x: 0.5, y: 1.0), end: CGPoint(x: 0.0, y: 0.5), control1: CGPoint(x: 0.3, y: 0.7), control2: CGPoint(x: 0.3, y: 0.7)),
29 AnimatableCubicCurve(start: CGPoint(x: 0.0, y: 0.5), end: CGPoint(x: 0.5, y: 0.0), control1: CGPoint(x: 0.3, y: 0.3), control2: CGPoint(x: 0.3, y: 0.3))
30 ]
31
32 static let heart = [
33 AnimatableCubicCurve(start: CGPoint(x: 0.5, y: 0.25), end: CGPoint(x: 1.0, y: 0.25), control1: CGPoint(x: 0.5, y: -0.1), control2: CGPoint(x: 1.0, y: 0.0)),
34 AnimatableCubicCurve(start: CGPoint(x: 1.0, y: 0.25), end: CGPoint(x: 0.5, y: 1.0), control1: CGPoint(x: 1.0, y: 0.6), control2: CGPoint(x: 0.5, y: 0.8)),
35 AnimatableCubicCurve(start: CGPoint(x: 0.5, y: 1.0), end: CGPoint(x: 0.0, y: 0.25), control1: CGPoint(x: 0.5, y: 0.8), control2: CGPoint(x: 0.0, y: 0.6)),
36 AnimatableCubicCurve(start: CGPoint(x: 0.0, y: 0.25), end: CGPoint(x: 0.5, y: 0.25), control1: CGPoint(x: 0.0, y: 0.0), control2: CGPoint(x: 0.5, y: -0.1))
37 ]
38}
The cubic line segments are arranged in a ZStack to create the shape of the card
suit. A loop is used inside the ZStack to add a stroke for the shape. The card suit
is changed from one suit to another with the use of a state variable changeCurve
.
This codes switches from Club to Spade and also from heard to diamond. These shapes
have similar number of lines and the animation effect shows the shape breaking up
into component lines and rearranging into the new shape.
1struct SuitChangeView1: View {
2 @State private var changeCurve = true
3
4 var body: some View {
5 VStack(spacing:30) {
6 Text("Shape change with Animatable Cubic Curve")
7
8 Spacer()
9 .frame(height:20)
10
11 HStack(spacing:30) {
12 ZStack {
13 ForEach(0..<max(Suit.club.count, Suit.spade.count)) { i in
14 CubicCurveShape(curve: changeCurve ? Suit.club[(i % Suit.club.count)] : Suit.spade[(i % Suit.spade.count)])
15 .stroke(changeCurve ? Color(.black) : .blue, lineWidth: 4.0)
16 }
17 }
18 .frame(width: 150, height: 150)
19 .animation(.linear(duration: 1))
20
21 ZStack {
22 ForEach(0..<max(Suit.heart.count, Suit.diamond.count)) { i in
23 CubicCurveShape(curve: changeCurve ? Suit.heart[(i % Suit.heart.count)] : Suit.diamond[(i % Suit.diamond.count)])
24 .stroke(changeCurve ? Color(.red) : .orange, lineWidth: 4.0)
25 }
26 }
27 .frame(width: 150, height: 150)
28 .animation(.linear(duration: 1))
29
30 }
31
32 Spacer()
33 .frame(height:20)
34
35 Button("Change") {
36 self.changeCurve.toggle()
37 }
38 .buttonStyle(BlueButtonStyle())
39
40 Spacer()
41 }
42 }
43}
Transition to shape with similar number of segments
Shapes with different number of segments
The transition from one shape to another using the AnimatableCubicCurve
in a ZStack
works much better when the number of segments are equal. This can be seen in the
animation from heart to diamond and back again. The code that loops through the shape
segments does allow for shapes with different number of segments by cycling around to
the start of the shape. If you look closely it can be seen that there are duplicate
lines in the simpler shapes like heart when it is transitioned from the club shape.
1struct SuitChangeView2: View {
2 @State private var changeCurve = true
3
4 var body: some View {
5 VStack(spacing:50) {
6 Text("Shape change with Animatable Cubic Curve")
7
8 HStack(spacing:40) {
9 ZStack {
10 ForEach(0..<max(Suit.club.count, Suit.heart.count)) { i in
11 CubicCurveShape(curve: changeCurve ?
12 Suit.club[(i % Suit.club.count)] :
13 Suit.heart[(i % Suit.heart.count)])
14 .stroke(changeCurve ? Color(.black) : .red, lineWidth: 4.0)
15 }
16 }
17 .frame(width: 200, height: 200)
18 .animation(.linear(duration: 1))
19
20 ZStack {
21 ForEach(0..<max(Suit.heart.count, Suit.spade.count)) { i in
22 CubicCurveShape(curve: changeCurve ?
23 Suit.heart[(i % Suit.heart.count)] :
24 Suit.spade[(i % Suit.spade.count)])
25 .stroke(changeCurve ? Color(.red) : .black, lineWidth: 4.0)
26 }
27 }
28 .frame(width: 200, height: 200)
29 .animation(.linear(duration: 1))
30
31 ZStack {
32 ForEach(0..<max(Suit.diamond.count, Suit.spade.count)) { i in
33 CubicCurveShape(curve: changeCurve ?
34 Suit.spade[(i % Suit.spade.count)] :
35 Suit.diamond[(i % Suit.diamond.count)])
36 .stroke(changeCurve ? Color(.black) : .red, lineWidth: 4.0)
37 }
38 }
39 .frame(width: 200, height: 200)
40 .animation(.linear(duration: 1))
41
42 ZStack {
43 ForEach(0..<max(Suit.club.count, Suit.diamond.count)) { i in
44 CubicCurveShape(curve: changeCurve ?
45 Suit.diamond[(i % Suit.diamond.count)] :
46 Suit.club[(i % Suit.club.count)])
47 .stroke(changeCurve ? Color(.red) : .black, lineWidth: 4.0)
48 }
49 }
50 .frame(width: 200, height: 200)
51 .animation(.linear(duration: 1))
52 }
53
54 Button("Change") {
55 self.changeCurve.toggle()
56 }
57 .buttonStyle(BlueButtonStyle())
58
59 Spacer()
60 }
61 }
62}
Transition to shape with different number of segments
Conclusion
This article built on the animation of quadratic Bézier curve by defining
AnimatableCubicCurve
to animate changes to cubic Bézier curves.The animation of a
cubic curve requires the animation of eight values - the coordinates of the start,
end and two control points of the cubic curve path. This is achieved by defining a
structure for AnimatableCubicCurve
that conforms to the VectorArithmetic protocol
and using this new type as the AnimatableData in a shape structure. The
CubicCurveShape
structure can be used in views to animate the transition from
straight lines, through quadratic curves to cubic curves as all these paths can be
defined as cubic Bézier paths.
The cubic line segments can be arranged in a ZStack to create shapes. The change from one shape to another can be animated by changing the individual segments. The transition from one shape to another is better when the number of segments are equal. Animation of shape change between shapes with different number of segments is shown and duplicate segments can be seen in the more simpler shapes during the transition.