• Install
  • Tutorials
  • COSMOS
  • Examples
  • Forums
  • Docs
  • Install
  • Tutorials
  • COSMOS
  • Examples
  • Forums
  • Docs
Tutorials
>
COSMOS
>
Menu Rings

Chapter 16
Menu Rings

Tags
cosmos

Estimated Time


Tutorials
>
COSMOS
>
Menu Rings

Our main goal is to have two states for the menu, so as we go along we’re going to build out a few structures so that it’s easier for us to transition between them. For example, instead of just creating a thick line then another and animating, we’re going to create an array of target frames that we will use to update the size of the rings during the animation. We’ll build these little helper structures as we go along.

Rings

The lines are the backbone of the menu, so we’re going to start by handling this component in a few parts: build all the rings, set up the start state, set up the end state, then animate out.

We’re going to create thick lines, thin lines, dashed lines and the dividing lines.

Thick Rings

Let’s layout the thick rings. There are two: a small one, 28pt diameter, and a large one, 450pt.

Open MenuRings.swift and create a variable array to store the targets for the thick rings sizes:

1
var thickRingFrames : [Rect]!

Then, create a variable for the ring:

1
var thickRing : Circle!

Next, build a method to create the thick rings and set up the targets by building the inner and outer shapes, then adding their frames to the array:

1
2
3
4
5
6
7
func createThickRing() {
    //create 2 shapes
    let inner = Circle(center: canvas.center, radius: 14)
    let outer = Circle(center: canvas.center, radius: 225)
    //store the frames of each position
    thickRingFrames = [inner.frame,outer.frame]
}

Later, we’re going to use this frame array to make our animations happen. For now, we’re going to style the starting ring. Add the following to the end of the createThickRing function:

1
2
3
4
5
6
7
inner.fillColor = clear
inner.lineWidth = 3
inner.strokeColor = COSMOSblue
inner.interactionEnabled = false
thickRing = inner

canvas.add(thickRing)

Check It

To see what this looks like you can add the following your project’s WorkSpace setup():

1
canvas.add(MenuRings().canvas)

Running your app should look like this:

Now, to see what the ring looks like in its outer state, add the following line in MenuRings.swift, change:

1
thickRing = inner

to:

1
2
thickRing = inner
thickRing.frame = outer.frame

And, this is what you should see:

Undo that last change before moving on.

Thin Rings

The process for building the thin rings is identical, save that it has more rings. Start by creating two arrays:

1
2
var thinRings : [Circle]!
var thinRingFrames : [Rect]!

We’re using an array for the thin rings because, unlike the thick one, there are 5 that we need to track.

The design file specifies that the inner ring for the starting position is 8pt radius, and the outer rings are: 56, 78, 98, 102 and 156pt diameters.

Start setting them all up by building a create method and constructing all of the rings including the inner and outer ones like so:

1
2
3
4
5
6
7
8
9
func createThinRings() {
    thinRings = [Circle]()
    thinRings.append(Circle(center: canvas.center, radius: 8))
    thinRings.append(Circle(center: canvas.center, radius: 56))
    thinRings.append(Circle(center: canvas.center, radius: 78))
    thinRings.append(Circle(center: canvas.center, radius: 98))
    thinRings.append(Circle(center: canvas.center, radius: 102))
    thinRings.append(Circle(center: canvas.center, radius: 156))
}

This creates a bunch of shapes and adds them to an array so they can be referenced later.

Next, create a loop that will run through all the shapes and style them. Add the following to the createThinRings method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
thinRingFrames = [Rect]()
for i in 0..<self.thinRings.count {
    let ring = self.thinRings[i]
    ring.fillColor = clear
    ring.lineWidth = 1
    ring.strokeColor = COSMOSblue
    ring.interactionEnabled = false
    if i > 0 {
        ring.opacity = 0.0
    }
    self.thinRingFrames.append(ring.frame)
}

for ring in thinRings {
    canvas.add(ring)
}

The loop is fairly straightforward: it iterates through all the thing rings, executing a bunch of styling code as it goes along. The rings are added in order from smallest to biggest, so the first one ends up being the size of the inner ring (i.e. when the menu is in its default state). To start we only want to see the inner ring, so for every other one where i > 0 we set their opacity to 0. As we go along we add the frame size of each ring to thinRingFrames, and finish by adding all the rings to the canvas.

Update the setup() to look like this:

1
2
3
4
public override func setup() {
    createThickRing()
    createThinRings()
}

Check It.

Running your app should look like this:

To see the rings in their outer state, in createThinRings(), you need to change from:

1
if i > 0 

… to:

1
if i == 0

Now, your app should look like:

Undo that last change before moving on.

Dashed Lines

Just like we’ve already done twice, we’re going to create an array to store the dashed rings. However, the dashed rings are eventually going to “fill” in, so we don’t need to store targets for them.

Add the following variable to your class:

1
var dashedRings : [Circle]!

Now, create three methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func createShortDashedRing(){
}
func createLongDashedRing(){
}
func createDashedRings() {
    dashedRings = [Circle]()
    createShortDashedRing()
    createLongDashedRing()
    
    for ring in self.dashedRings {
        ring.strokeColor = COSMOSblue
        ring.fillColor = clear
        ring.interactionEnabled = false
        ring.lineCap = .Butt
        self.canvas.add(ring)
    }
}

We’re going to create the short / long dashed with two separate lines whose thicknesses are different, and whose dash patterns are lined up to give the illusion of a short/long dashed line.

Short Dashed Ring

This is what the setup for the short dashed rings looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
func createShortDashedRing() {
    let shortDashedRing = Circle(center: canvas.center, radius: 82+2)
    let pattern = [1.465,1.465,1.465,1.465,1.465,1.465,1.465,1.465*3.0] as [NSNumber]
    shortDashedRing.lineDashPattern = pattern
    shortDashedRing.strokeEnd = 0.995
    
    let angle = degToRad(-1.5)
    let rotation = Transform.makeRotation(angle)
    shortDashedRing.transform = rotation
    
    shortDashedRing.lineWidth = 0.0
    dashedRings.append(shortDashedRing)
}

There’s a lot going on here, some of which is contingent on other design factors, so I’ll break it down as much as possible.

  1. The radius of the circle is 82+2, I could have written 84 but that +2 actually refers to half of the lineWidth.1
  2. The pattern is 1.465,.... This number took some tuning to find for a couple reasons: a) the circle is supposed to be divided into 36 sections 10 degrees each, b) The 1.465*3.0 represents the gap (e.g. 2 spaces plus an additional space where the long dash should be), c) the as [NSNumber] is a required cast because thats what the underlying property requires (e.g. not [Double])
  3. Since patterns always start at the beginning of the line we need to rotate the entire shape a little bit to make it look like it starts with a space, the amount to rotate is -1.5 degrees, so we convert that to radians, create a transform and apply that to the shape.
  4. The rest of the method is straightforward.

Long Dashed Ring

The longDashedRing method should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func createLongDashedRing() {
    let longDashedRing = Circle(center: canvas.center, radius: 82+2)
    longDashedRing.lineWidth = 0.0

    let pattern = [1.465,1.465*9.0] as [NSNumber]
    longDashedRing.lineDashPattern = pattern
    longDashedRing.strokeEnd = 0.995

    let angle = degToRad(0.5)
    let rotation = Transform.makeRotation(angle)
    longDashedRing.transform = rotation

    let mask = Circle(center: longDashedRing.bounds.center, radius: 82+4)
    mask.fillColor = clear
    mask.lineWidth = 8
    longDashedRing.layer?.mask = mask.layer

    dashedRings.append(longDashedRing)
}

It’s pretty much the same process as the previous method, with a couple of tweaks:

  1. The pattern is [1.465,1.465*9.0] meaning there will be a single dash followed by a gap that is 9x wider than the dash.
  2. We rotate by 0.5 degrees to properly center the starting point of the long dashed in the gaps of the short dashes
  3. There is the tiniest discrepancy in the end of the line where the last “dash” is slightly visible, so we shift the strokeEnd from 1.0 to 0.995 to hide it.
  4. Then, we create a mask…

Update setup() to look like:

1
2
3
4
5
public override func setup() {
    createThickRing()
    createThinRings()
    createDashedRings()
}

Check It.

To see what the rings will look like, do the following:

Change:

1
shortDashedRing.lineWidth = 0.0

to:

1
shortDashedRing.lineWidth = 4.0

And, change:

1
longDashedRing.lineWidth = 0.0 

to:

1
longDashedRing.lineWidth = 12.0

Run it and you’ll see this:

Undo those two changes before moving on.

The Mask

The mask component is a nice little trick that we use to make our lives a bit easier when it comes to shaping the lines. By default, a line is drawn from the center outward, so if you have a horizontal line with 12pt font there will be 6pt above and below.

The design shows both circles starting at the same inner diameter. We want it to look like the short and long dashes grow from the baseline outward… So, we cut off the extended part by masking the shape.

This is also why the line is 12pt thick, but only looks 6pt on screen… We’re effectively clipping 6pts

This is how masks work: wherever a mask has color the object that is being masked will show through. So, we create a mask with an 8pt solid line whose diameter is offset enough so that the edges of the mask’s line touch the baseline of the circles and extend to the top of the long dashes, like this:

Oh yeah, and when you apply a mask to an object it gets positioned inside the coordinate space of the object, which is why we do longDashedRing.bounds.center for the center point of the mask.

Dividing Lines

Next step is to create the dividing lines that will eventually separate the spaces for the icons.

First, create this variable:

1
var menuDividingLines : [Line]!

Then, add this method to your class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func createMenuDividingLines() {
    menuDividingLines = [Line]()
    for i in 0...11 {
        let line = Line((Point(),Point(54,0)))
        line.anchorPoint = Point(-1.88888,0)
        line.center = canvas.center
        line.transform = Transform.makeRotation(M_PI / 6.0 * Double(i) , axis: Vector(x: 0, y: 0, z: -1))
        line.lineCap = .Butt
        line.strokeColor = COSMOSblue
        line.lineWidth = 1.0
        line.strokeEnd = 0.0
        canvas.add(line)
        menuDividingLines.append(line)
    }
}

Fairly straightforward to set up the lines. We know the gap between the two lines for the inner and outer edges of the icon section is 54pt so that’s how long we make our line. We style it and thennnnnn we change the anchorPoint.

The Anchor Point

Every visible object has an anchor point whose default position is in the middle of the object’s view. It is around this point that any visible transforms happen. For example, if I simply apply a rotation to an object (like we did for the short / long dashed lines) then the entire object will rotate around its center. 2

The effect we want to create, lines angled evenly to create spaces between two rings, relies implicitly on our being able to play with the anchorPoint property. We could calculate the rotated positions of a and b for each line, but that’s the less elegant way of creating the effect.

What we do is offset the position of the anchorPoint so that we can rotate around a position outside the space of the line. Woah, pictures speak better than words, so look at this:

Another thing about anchor points is that they are measured relative to the space of the object’s view. Specifically, the center point of a view is {0.5,0.5}. So, now we just need to figure out a which point we need to set the anchor so that our 54pt line sits at the right place.

We know the inner radius of the circle is 102 (e.g. the second-last thin ring), and we know the width of the line is 54, so all we need to do is translate that to relative coordinates:

102/54 = 1.888

And, since we want the point to be outside the view to the left of 0 we need that value to be negative, which is what this line means:

1
line.anchorPoint = CGPointMake(-1.88888,0)

The rest of the method is straightforward. We center the anchor point to the center of the canvas, then rotate the line into place. We do this for 12 lines then add them to both the canvas and an array (so we can toy with them later).

Wham. Those lines are done.

Oh, and for reference, this is what our layout would look like if we didn’t adjust the anchor point of the lines:

Your setup() should look like this:

1
2
3
4
5
6
override func setup() {
   self.createThickRing()
   self.createThinRings()
   self.createDashedRings()
   self.createMenuDividingLines()
}

Check It.

To see the dividing lines, in createMenuDividingLines change:

1
line.strokeEnd = 0.0

to:

1
line.strokeEnd = 1.0

Or comment it out.

And this is what you should see:

And, if you want to go back and change all the previous variables to see the entire state of outer rings and lines you should get this:

Undo any changes so that the lines set up in their inner state.

Wham.

The lines look good.

Let’s keep going.

  1. As I’m writing this I thought “why did I write it this way” but then after reading the code again I realized I had left it like this as a signal for myself to remember that the center needs to be just a little bit more than the specified 82 pt diameter from Jake’s design file. ↩

  2. Note that i’m implying a rotation around the z-axis, but rotations can also happen through x and y as well. ↩

Well done! Continue COSMOS?

  • Chapter 15
    Radial Menu

    Building the heart of the app - a radial menu with embedded animation that reacts to different gestures.

  • Chapter 17
    Animating the Menu Rings

    Time to bring the radial menu to life with embedded animation.

  • About
  • Features
  • Learn + Explore
  • Install C4
  • Examples
  • Tutorials
  • Docs
  • Connect
  • Forums
  • Medium
  • Twitter
  • Slack
  • Vimeo
  • GitHub
  • © 2016 C4
  • Terms of Use
  • Contact
  • Learn + Explore
  • Install C4
  • Examples
  • Tutorials
  • Docs
  • Connect
  • Forums
  • Medium
  • Twitter
  • Slack
  • Vimeo
  • GitHub
  • © 2016 C4
  • Terms of Use
  • Contact