First, let’s break down arc’s onboarding intro. There’s a few distinct elements we can observe:
- The desktop fades in/out to a semi-transparent black
- There’s an orb that starts small, grows bigger and reveals a window
- A staggered text animation is played
Let’s figure each out!
How does the desktop fade in/out?
That’s easy - we’ll use AppKit to achieve this. Let’s create a floating window and animate its alpha value. We can apply a borderless style mask so it doesn’t look or act like a typical macOS window (no rounded corners, borders or minimise/maximise/close buttons)
// calculate background window position + sizelet screenRect = NSScreen.main?.frame ?? .zerolet visibleRect = NSScreen.main?.visibleFrame ?? .zerolet topMenuBarHeight = screenRect.maxY - visibleRect.maxY
let backgroundWindowRect = NSRect( x: screenRect.origin.x, y: screenRect.origin.y, width: screenRect.width, height: screenRect.height - topMenuBarHeight)
// create borderless windowlet backgroundWindow = NSWindow( contentRect: backgroundWindowRect, styleMask: [.borderless], // no typical macOS window elements backing: buffered, defer: false)
// set initial appearance of window - fully transparentbackgroundWindow.alphaValue = 0backgroundWindow.isOpaque = falsebackgroundWindow.ignoresMouseEvents = truebackgroundWindow.backgroundColor = .blackbackgroundWindow.level = .floating
// show the window and animate its alpha over 1 secondbackgroundWindow.makeKeyAndOrderFront (nil)NSAnimationContext.runAnimationGroup({ context in context.duration = 1 backgroundWindow.animator().alphaValue = 0.9})
How do we create an orb that reveals a window?
It’s magic 🪄💫
… or more like an illusion.
We can create videos with transparent backgrounds, and play them inside a swiftui view. I used Spline to make 2 animations - an orb that grows bigger and a moving gradient with simplex fractal noise
I’ll transition between the orb animation and the moving gradient (as the gradient video can be looped)
Once I was happy with the results, I exported the orb animation as a PNG image sequence and converted it to a .mov
file using ffmpeg - so that transparency is preserved.
ffmpeg -framerate 60 -i %03d.png -c:v prores_ks -profile:v 5 -pix_fmt yuva444p10le output.mov
The moving gradient doesn’t need any transparency, so we can just export this straight to mp4 and use directly within an AVPlayer.
Now that the videos are ready, we’ll create the illusion of a window emerging from the orb:
- Make a normal window with a transparent background
- Hide the titlebar, shadow + other properties of a typical macOS window
- Render a swiftui view which plays the orb animation using an AVPlayer
- When the video animation finishes, restore the window’s normal properties and transition to the moving gradient video
// create normal windowlet mainWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false)
// set initial appearance of window to// display contents but hide window elementsmainWindow.titlebarAppearsTransparent = truemainWindow.titleVisibility = .hiddenmainWindow.hasShadow = falsemainWindow.isOpaque = falsemainWindow.backgroundColor = .clearmainWindow.level = .floating
// video player is in a swiftui content viewmainWindow.contentView = NSHostingView( rootView: ContentView() .ignoresSafeArea(.all))
mainWindow.makeKeyAnd0rderFront(nil)mainWindow.center()
// restore normal window properties after video finishesDispatchQueue.main.asyncAfter(deadline: .now() + 6) { self.animateWindowBackgroundAlpha( for: mainWindow, baseColor: .windowBackgroundColor, fromAlpha: 0, toAlpha: 1 )
mainWindow.isOpaque = true mainWindow.hasShadow = true
// show close/min/max buttons mainWindow.styleMask.insert(.resizable) mainWindow.styleMask.insert(.miniaturizable) mainWindow.styleMask.insert(.closable)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { mainWindow.level = .normal }}
How is the text animation created?
I used the AnimateText swift package and built a custom effect that looks like this:
Each character rotates, blurs and fades in using a spring animation - with a slight stagger for a nice cascading feel.
public struct StaggerTextEffect: ATTextAnimateEffect { public var data: ATElementData public var userInfo: Any?
public init(_ data: ATElementData, _ userInfo: Any?) { self.data = data self.userInfo = userInfo }
public func body(content: Content) -> some View { content .opacity(data.value) .blur(radius: 10 - 10 * data.value) .rotation3DEffect(Angle(degrees: 20 * data.invValue), axis: (x: 0, y: 0, z: 1), anchor: .bottomLeading, anchorZ: 0, perspective: 0.7) .offset(x: 0, y: data.invValue * (data.size.height * 1)) .animation(.spring(duration: 0.6, bounce: 0.26).delay(Double(data.index) * 0.02), value: data.value) }}
You can then use it in a view like this:
AnimateText<StaggerTextEffect>($text, type: .letters)
Now to put it all together 🕺
Once the app has launched:
- Fade the desktop out with a background window
- Create the main window and immediately play the orb animation
- After x amount of seconds, enable the normal window properties
- Play the moving gradient video in the window and fade out the orb animation video
- Play the staggered text animation
- Fade the background window out so the user can see the rest of their desktop
And that’s it! I hope this gives you some ideas, and I’d love to see what you create.
I plan to do more breakdowns like this for other effects, so let me know if you have any suggestions - whether they’re for Swift or web based projects.