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
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 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
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 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 AnimatableQuadCurve
s.
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
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.