Chapter 17
Animating the Menu Rings
Time to bring the radial menu to life with embedded animation.
Next, we want to add the icons to our menu and animate them in. The design concept for the icons is straightforward: they start as a dot, move out to their final position, and animate to their full shape.
The tricky part of this step isn’t the animation and movement – those are easy – but the positioning of each shape so that it looks like they are dots that turn into full shapes. There are a few ways of thinking about how to do this:
strokeEnd
to the right position and its lineCap
to .Round
We’re going to use the strokeEnd
to manage the animating of the icons, so the third option is the best approach. However, the problem with this approach is that each of the “starting” points for the shapes will be different – we will need to offset them either on the dot or full icon end of the animation.
Here’s the icon for Capricorn, with its start point highlighted:
The position of the start point is {30.0,12.2}
, which with respect to its frame is {0.750,0.387}
.
To create the effect of a “dot” we simply set the following on each of the icons:
1
2
shape.strokeEnd = 0.001
shape.lineCap = .Round
If we use the center of the icon to position, the menu will look like this for its closed and open states.
If we use the first point as the anchorPoint
of the shape we get the following:
Our ideal is using the anchorPoint
for the closed state, and the center
for the second state. But, this will get complicated if we are constantly switching positions. So, we’ll have to use the anchorPoint
to calculate the actual center
of the icon at one of the states and then use just the center to move it back and forth between those states afterwards.
Let’s get to it.
The first step is to grab the icons out of the sign provider, and update their anchor points.
Open MenuIcons.swift
and add the following methods to the class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
func taurus() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.taurus().shape
shape.anchorPoint = Point()
return shape
}
func aries() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.aries().shape
shape.anchorPoint = Point(0.0777,0.536)
return shape
}
func gemini() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.gemini().shape
shape.anchorPoint = Point(0.996,0.0)
return shape
}
func cancer() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.cancer().shape
shape.anchorPoint = Point(0.0,0.275)
return shape
}
func leo() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.leo().shape
shape.anchorPoint = Point(0.379,0.636)
return shape
}
func virgo() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.virgo().shape
shape.anchorPoint = Point(0.750,0.387)
return shape
}
func libra() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.libra().shape
shape.anchorPoint = Point(1.00,0.559)
return shape
}
func pisces() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.pisces().shape
shape.anchorPoint = Point(0.099,0.004)
return shape
}
func aquarius() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.aquarius().shape
shape.anchorPoint = Point(0.0,0.263)
return shape
}
func sagittarius() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.sagittarius().shape
shape.anchorPoint = Point(1.0,0.349)
return shape
}
func capricorn() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.capricorn().shape
shape.anchorPoint = Point(0.288,0.663)
return shape
}
func scorpio() -> Shape {
let shape = AstrologicalSignProvider.sharedInstance.scorpio().shape
shape.anchorPoint = Point(0.255,0.775)
return shape
}
Each of those methods grabs the raw icon from the sign provider and sets the anchor point to the start point of icon. We don’t need any of the other details from the sign (e.g. big / small points, lines, etc.) so we return only the shape with a modified anchor point.
Next, we also want to have a stored copy of the signs we’re working with because we’ll manipulate them a bit. So, we create a variable shape dictionary to store the signs and a method that will create and style them for us:
1
var signIcons : [String:Shape]!
Then, add the following method to your class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func createSignIcons() {
signIcons = [String:Shape]()
signIcons["aries"] = aries()
signIcons["taurus"] = taurus()
signIcons["gemini"] = gemini()
signIcons["cancer"] = cancer()
signIcons["leo"] = leo()
signIcons["virgo"] = virgo()
signIcons["libra"] = libra()
signIcons["scorpio"] = scorpio()
signIcons["sagittarius"] = sagittarius()
signIcons["capricorn"] = capricorn()
signIcons["aquarius"] = aquarius()
signIcons["pisces"] = pisces()
for shape in [Shape](self.signIcons.values) {
shape.strokeEnd = 0.001 //in combination with the next two settings
shape.lineCap = .Round //strokeEnd 0.001 makes a round dot at
shape.lineJoin = .Round //the beginning of the shape's path
shape.transform = Transform.makeScale(0.64, 0.64, 1.0)
shape.lineWidth = 2
shape.strokeColor = white
shape.fillColor = clear
}
}
The shape we’re getting out of the sign provider is raw, so we need to scale it to fit our design. If we don’t apply the following:
shape.transform = Transform.makeScale(0.64, 0.64, 1.0) Then the shapes would be too big and look like this:
Next, we need to calculate the target positions for each of the shapes and store them in two arrays. Add the following variables to your class:
1
2
var innerTargets : [Point]!
var outerTargets : [Point]!
Then, add the following method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func positionSignIcons() {
innerTargets = [Point]()
let provider = AstrologicalSignProvider.sharedInstance
let r = 10.5
let dx = canvas.center.x
let dy = canvas.center.y
for i in 0..<provider.order.count {
let ϴ = M_PI/6 * Double(i)
let name = provider.order[i]
if let sign = signIcons[name] {
sign.center = Point(r * cos(ϴ) + dx, r * sin(ϴ) + dy)
canvas.add(sign)
sign.anchorPoint = Point(0.5,0.5)
innerTargets.append(sign.center)
}
}
outerTargets = [Point]()
for i in 0..<provider.order.count {
let r = 129.0
let ϴ = M_PI/6 * Double(i) + M_PI/12.0
outerTargets.append(Point(r * cos(ϴ) + dx, r * sin(ϴ) + dy))
}
}
The process above takes advantage of the following concepts:
anchorPoint
to each one’s start pointshape.center
actually returns the position of the shape’s anchorPoint
with respect to the shape’s superview
anchorPoint
back to the shapes center
(e.g. {0.5,0.5}
) won’t change the shape’s positionanchorPoint
we are able to grab the actual center position of the shape and use this as a targetFinally, at the end of createSignIcons
, after styling all the signs, add the following:
1
positionSignIcons()
Now, let’s have a look at how these line up.
Change the backgroundColor
of the menu to C4Purple
and create the sign icons. Your setup()
should look like this:
1
2
3
4
public override func setup() {
canvas.backgroundColor = COSMOSbkgd
createSignIcons()
}
And, go to your project’s WorkSpace
and update the setup()
there to look like:
1
2
3
override func setup() {
canvas.add(MenuIcons().canvas)
}
Run it, and you should see this:
There are 4 different animations we need to create: position out / in, shape reveal / hide. So, create 4 animation variables like so:
1
2
3
4
var signIconsOut : ViewAnimation!
var signIconsIn : ViewAnimation!
var revealSignIcons : ViewAnimation!
var hideSignIcons : ViewAnimation!
Then add this method:
1
2
func createSignIconAnimations() {
}
The reason we need 4 animations is based on our design, specifically the order in which we animate the lines out and in is different in either direction:
OUT: move, reveal IN: hide, move
Since the pattern is backwards when animating in we’re required to have separate animations for moving and for revealing that we can then order in a sequence. Add the following to the blank method:
1
2
3
4
5
6
7
8
9
10
11
12
13
revealSignIcons = ViewAnimation(duration: 0.5) {
for sign in [Shape](self.signIcons.values) {
sign.strokeEnd = 1.0
}
}
revealSignIcons?.curve = .EaseOut
hideSignIcons = ViewAnimation(duration: 0.5) {
for sign in [Shape](self.signIcons.values) {
sign.strokeEnd = 0.001
}
}
hideSignIcons?.curve = .EaseOut
A couple of things to note here:
0.001
to preserve the dotNow, add the following to the createSignIconAnimations
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
signIconsOut = ViewAnimation(duration: 0.33) {
for i in 0..<AstrologicalSignProvider.sharedInstance.order.count {
let name = AstrologicalSignProvider.sharedInstance.order[i]
if let sign = self.signIcons[name] {
sign.center = self.outerTargets[i]
}
}
}
signIconsOut?.curve = .EaseOut
//moves icons to closed position
signIconsIn = ViewAnimation(duration: 0.33) {
for i in 0..<AstrologicalSignProvider.sharedInstance.order.count {
let name = AstrologicalSignProvider.sharedInstance.order[i]
if let sign = self.signIcons[name] {
sign.center = self.innerTargets[i]
}
}
}
signIconsIn?.curve = .EaseOut
Here all we’re doing is grabbing the icons and either setting their center to the outer or inner positions.
Test the animations by calling the reveal and move animations like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func animOut() {
delay(1.0) {
self.signIconsOut?.animate()
}
delay(1.5) {
self.revealSignIcons?.animate()
}
delay(2.5) {
self.animIn()
}
}
func animIn() {
delay(0.25) {
self.hideSignIcons?.animate()
}
delay(1.0) {
self.signIconsIn?.animate()
}
delay(2.5) {
self.animOut()
}
}
Then call animOut()
at the end of setup()
, which should look like this:
1
2
3
4
5
6
public override func setup() {
canvas.backgroundColor = COSMOSbkgd
createSignIcons()
createSignIconAnimations()
animOut()
}
You should see this:
Now, delete animIn
and animOut
methods and make your setup()
look like this:
1
2
3
4
5
6
public override func setup() {
canvas.frame = Rect(0,0,80,80)
canvas.backgroundColor = clear
createSignIcons()
createSignIconAnimations()
}
Grab a copy of MenuIcons.swift.
Baddabing!