Recreating a WWDC 2006 animation in Core Animation
Core Animation was first introduced by Apple at WWDC 2006, and one of the first demos was a slick 3D animation involving album art. I thought it would be interesting to recreate the animation, and this post documents my results. You can find the completed source code on my GitHub.
The animation starts as a grid of album art. In the first part of the animation, every couple of seconds one of the albums is flipped to reveal another album. Then in the second part of the animation, the grid of albums disintegrates and album art flows down the screen in an infinite stream. In the demo, the presenter also āflies aroundā the scene in 3D space, creating a cool camera effect.
Hereās the end result of what we'll create:
In the actual keynote presentation, the demo animation appears at 50:43. You can judge for yourself how close this recreation is. š
The first task is to layout the grid of album art. The demo animation uses a 5x8 grid, so weāll use that too. Note that album art has a 1:1 aspect ratio (i.e. is a square), so weāll size the album art to fill the viewport vertically, and distribute the remaining free space horizontally.
let viewportWidth = self.layer!.bounds.width
let viewportHeight = self.layer!.bounds.height
var albumSize = Float(Int(viewportHeight / 5.0))
let rows = Int(Float(viewportHeight) / albumSize)
let cols = Int(Float(viewportWidth) / newAlbumSize)
let horizontalSpace = Float(viewportWidth) - (Float(cols) * albumSize)
for row in 0..<self.numRows {
for col in 0..<self.numCols {
let albumLayer = CALayer()
albumLayer.frame = CGRect(
origin: NSMakePoint(
(CGFloat(self.albumSize) * CGFloat(col)) + CGFloat(horizontalSpace / 2.0),
(CGFloat(row) * CGFloat(self.albumSize))),
size: NSMakeSize(CGFloat(self.albumSize), CGFloat(self.albumSize)))
}
}
We now need to select a random album and flip it to reveal a new album. The details of how to properly perform a flip in Core Animation are nicely documented in this blog post from 2010, so I won't repeat it here. In the completed source code, the flip happens in performFlip()
.
The next step is to transition from this grid of albums into an infinite vertical scroll of albums. In the transition, there are 2 animations happening simultaneously: each album is scaled along the z-axis, and each album is translated (downward) along the y-axis. If you watch the demo animation closely, youāll also notice that the transition is staggered across the grid: the bottom row of the grid first detaches from the grid, followed by the above row, and so on. The z-axis scale animation is also staggered, with the bottom rows animating out first. To stagger the animations, weāll rely on the beginTime
property of a CAAnimation
. Setting beginTime
will offset the start of an animation by the specified amount, which is exactly what we need.
let zScaleAnimation = CABasicAnimation()
zScaleAnimation.keyPath = "transform"
zScaleAnimation.fromValue = CATransform3DIdentity
zScaleAnimation.toValue = CATransform3DTranslate(CATransform3DIdentity, 0, 0, randomFloat(min: -700, max: 700))
zScaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
var beginTimeOffset = 0.0
if row == 0 {
beginTimeOffset = 0.4
} else if row == 1 {
beginTimeOffset = 0.5
} else if row == 2 {
beginTimeOffset = 0.8
} else if row == 3 {
beginTimeOffset = 1.0
} else {
beginTimeOffset = 1.2
}
zScaleAnimation.beginTime = CACurrentMediaTime() + beginTimeOffset
let scrollAnimation = CABasicAnimation()
scrollAnimation.keyPath = "position"
let destY = -300.0 // somewhere below the viewport
let rowDistance = Float(layer.frame.origin.y) - destY
scrollAnimation.duration = rowDistance / targetVelocity
Once the transition is complete, the album art flows down the screen, with new album art flowing down from the top to keep the stream infinite. To make the animation infinite, when the current bottom row of album art is fully out of the viewport, weāll create a new row of album art that is initially located right above the viewport and animate it to below the viewport. The advantage of this approach is that we always have a bounded number of layers. (This is essentially the approach that UITableView and UICollectionView use, but because weāre using Core Animation directly we have to manually re-implement it ourselves.) You can find this in the completion handler for each vertical animation.
The final thing is to be able to fly around the scene. Although Core Animation doesnāt have an explicit camera, we can fake one by rotating the entire layer around the y-axis.
override func mouseDragged(with event: NSEvent) {
var direction: Float = 1.0
if event.deltaX.sign == .minus {
direction = -1.0
}
let cameraAnimation = CABasicAnimation()
animation.keyPath = "sublayerTransform.rotation.y"
animation.byValue = deg2rad(360.0 * direction)
self.layer!.add(animation, forKey: nil)
}
And thatās it! Fortunately, Core Animation makes a fairly complex animation like this feasible in just a few hundred lines of code, which is exactly the point of the demo.
Hereās the completed source code again. Enjoy!