Comets: Animating Particles in Swift

Playing with CAEmitterLayer and CAEmitterCell

Bennet van der Linden
7 min readJun 28, 2016

For a tvOS project I wanted to create the comets from the following animation in code using particle animations:

Particles and CAEmitterLayer

Particles can be used to create real-time animations for effects like fire, rain, sparks and the like. The CoreAnimation framework available in iOS has a very powerful particle engine: The CAEmitterLayer.
I had recently used particles to accentuate certain views with a subtle bubble animation, and this inspired me to experiment with particles for the comet animation.

Bubble animation created for a previous project

So what is the magic behind particles in iOS? Particle animations are made up of two components: A CAEmitterLayer, and CAEmitterCell(s).

CAEmitterLayer

Like the name suggests, the CAEmitterLayer is responsible for emitting the particles. The CAEmitterLayer is the base of the particle animation and is highly customizable by changing its properties. An emitter can have various shapes and sizes, depending on your specific needs. For example it can be a point, a line or even a two dimensional shape like a circle:

FLTR: Point, line and circle shaped CAEmitterLayer

The CAEmitterLayer only specifies the start-point of the particles. To change the appearance of the particles, we have to take a look at the CAEmitterCell.

CAEmitterCell

The CAEmitterCell is a template for the particles emitted by the CAEmitterLayer, and determines how it looks and moves. The CAEmitterLayer has an array of one or more CAEmitterCells, each with possibly a different configuration and style. To change the look of a particle you can use the following attributes of the CAEmitterCell:

  • Content
    A cell’s content needs a PNG image file as a base for the particle. The image will be used to determine the shape of the particle. It’s best to use a white colored image as the visual attributes will take care of coloring it.
  • Visual attributes
    Once the content is set, the visual attributes can change the color and the scale of the particle. It’s also possible to set the speed and range of the RGBa values of the color. The color speed defines how the color changes over the lifetime of the cell, and the range specifies the amount by which a color component can vary between each newly spawned particle.
  • Motion attributes
    The motion attributes can make the cell spin, and determine the orientation of the emission angle of the particle. Just like with the color range, the emission range can vary the emission angle (in radians) between each newly spawned particle. For example: the GIFs above, which demonstrate the shape of the CAEmitterLayer, show an emission range of 2π, which makes the particles spawn in any direction.
  • Temporal attributes
    These attributes involve anything time related, including the lifetime, birth rate (particles per second), velocity and acceleration of the particles.

Let me show an example of what four different cells can look like:

Four different CAEmitterCells. Can you spot the differences in configuration?

Pretty cool, huh?

Creating lines and comets

Now that we have a basic understanding of how particles work in iOS, let’s take a look at the lines and comets in Voicy.

Draw a line

Drawing a line is pretty simple. All you need are two coordinates, a color and a width. And boom! You have a line, just like that.
I created a LineModel struct that has attributes for the coordinates and the line color. A simple drawLine() function returns a CAShapeLayer which can be added to a UIView to draw the line between the two coordinates:

struct LineModel {
var startPoint: CGPoint
var endPoint: CGPoint
var lineColor: UIColor
func drawLine() -> CAShapeLayer {
//create the path
let linePath = UIBezierPath()
linePath.moveToPoint(startPoint)
linePath.addLineToPoint(endPoint)
//create the line as a CAShapeLayer
let lineLayer = CAShapeLayer()
lineLayer.path = linePath.CGPath
lineLayer.lineWidth = 3
lineLayer.strokeColor = lineColor.CGColor
return lineLayer
}
}

Animate a comet

For the comet animation I added another function to the LineModel which returns a CAEmitterLayer:

func animateComet() -> CAEmitterLayer {
// create emitter
let emitter = CAEmitterLayer()
emitter.emitterShape = kCAEmitterLayerPoint
emitter.emitterPosition = startPoint

// create comet cell
let cell = CAEmitterCell()
cell.contents = UIImage(named: "comet")!.CGImage
cell.birthRate = 0.2 * Float(Int.random(500...2000)) / 1000
cell.lifetime = 10.0
cell.velocity = 800
cell.velocityRange = 700
//add cell to the emitter
emitter.emitterCells = [cell]
return emitter
}

So what exactly happens here? Well, first I create a point shaped CAEmitterLayer, and set its position to be the startpoint of the LineModel. Then I create a CAEmitterCell, which is going to be the comet.

  • The content of the cell is a white png image (named “comet”) that has the shape of a comet with a fading ‘tail’ as shown below:
comet.png (white comet on a blue background for visibility)
  • The image already looks exactly how we want the comet to look like, so no need to add any visual attributes.
  • We don’t want every line to spawn a comet at the same time, so a random generated value for the birthRate attribute will vary the spawn rate between 0.1 and 0.4. This means a comet will spawn every 2.5 to 10 seconds.
  • The comet will travel out of the screen, so it needs a lifeTime value (in seconds) that is long enough so it doesn’t disappear in the middle of the animation.
  • The velocity of each spawning comet varies between 100 and 1500 to make the comet look and feel more dynamic.
First comet animation

There is a comet flying over the screen! Nice! The only problem is, it doesn’t exactly follow the line yet…

Angle calculations

By default, a cell emitted from a point shaped emitter will travel in a horizontal direction from left to right, as shown in the GIF above. Fortunately, the CAEmitterCell has an attribute to change the emission angle (in radians). Now we need to calculate the exact angle so the comet will follow the line.

It’s time to dust off my geometry math and start calculating angles. To calculate an angle, we need to know at least two sides of a right-angled triangle:

Sides of a right-angled triangle based on the line coordinates

We can define a right-angled triangle with an adjacent side based on the Δx and an opposite side based on the Δy values of the coordinates of the line. The arctangent function can then be used to calculate the corresponding angle. I wrote the following function to calculate the correct emission angle for the comet animation:

func calculateAngle() -> CGFloat {
let deltaX = endPoint.x — startPoint.x
let deltaY = endPoint.y — startPoint.y
return atan2(deltaY, deltaX)
}

This function takes the Δx and Δy of the line, and uses the atan2 math function in Swift to calculate the matching angle. We can now add a single line to our cell configuration so it will follow the line:

cell.emissionLongitude = calculateAngle()

Let’s see if this works…

Comet animation with calculated emission angle

Ahh, so close! It’s now following the line at least, but the particle needs to be rotated as well. Unfortunately, there’s no specific CAEmitterCell attribute that rotates the content of the cell. There’s only an attribute that lets it spin, but that’s not what we want. Instead, I use a custom UIImage extension to rotate the image with the same amount as the emission range:

let cometImage = UIImage(named:“comet”)!.imageRotatedByRadians(calculateAngle())cell.contents = cometImage.CGImage

And the final result looks like this:

Final comet animation

Sweet! We now have a line with a matching comet animation! Because everything is contained within the LineModel struct, it’s very simple to create multiple lines and add them to the background. We use the lines in both the Apple TV and iPhone app so I defined two arrays with lines: One for each app. To show them, all that’s needed is to loop through the correct array and add the line and animation layers to the background view:

for line in lines {
view.layer.addSublayer(line.drawLine())
view.layer.addSublayer(line.animateComet())
}

And this is how it looks like in our Apple TV app!

Lines and comets in the Apple TV app

--

--