Skip to content

Instantly share code, notes, and snippets.

@eschmar
Last active March 5, 2023 15:54
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save eschmar/e6854e5d6ce50404ee5f182e8eeb4950 to your computer and use it in GitHub Desktop.
Blog: Capture Xcode Playground SwiftUI animations as mp4
/**
* **Step 3**: Render all the frames
*
* This is the accompayning code of a blog post. Read more at https://eschmann.dev
*/
import SwiftUI
import PlaygroundSupport
struct MyExperimentalView: View {
@State var startAngle: Double = 135.0
@State var progress: Double = 10.0
var body: some View {
prototypeView(progress)
.onAppear {
withAnimation(.easeInOut(duration: 0.7).repeatForever(autoreverses: true)) {
progress = 270.0
}
render()
}
}
@ViewBuilder func prototypeView(_ progress: Double) -> some View {
ZStack {
/// Background color
Color.purple
/// Shape with gradient along path
PathAlongCircle(
startAngle: startAngle,
progress: progress
)
.foregroundColor(.white)
.mask {
AngularGradient(
gradient: Gradient(colors: [.clear, .white]),
center: .center,
startAngle: .degrees(startAngle - 15.0),
endAngle: .degrees(startAngle + progress + 15.0)
/// ^-- Increase gradient range slightly to include the rounded tips
)
}
/// Outline
PathAlongCircle(startAngle: startAngle, progress: progress)
.stroke(.white, lineWidth: 4)
.opacity(1.0)
}.frame(width: 320, height: 320)
}
@MainActor func render() {
/// Render configuration for animation
let fps = 144.0
let duration = 0.72
let from = 10.0
let to = 270.0
guard let url = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask
).first else {
print("Unable to find the user documents folder.")
return
}
print("Disk location: \(url)")
for i in 0...Int(ceil(duration * fps)) {
/// Calculate current animation step values
let fraction: Double = (Double(i) / (duration * fps))
let progress = from + (to - from) * fraction
let renderer = ImageRenderer(content: prototypeView(progress))
//renderer.scale = displayScale
renderer.scale = 3.0
let filename = String(format: "%05d", i)
let filepath = url.appendingPathComponent("\(filename).png")
do {
try renderer.uiImage?.pngData()?.write(to: filepath)
print("Frame: \(filename)")
} catch {
print("> Error: \(error.localizedDescription)")
return
}
}
print("Done.")
}
}
struct PathAlongCircle : Shape {
var startAngle: Double
var progress: Double
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(
center: CGPoint(x: 160, y:160),
radius: 80,
startAngle: .degrees(startAngle),
endAngle: .degrees(startAngle + progress),
clockwise: false
)
return p.strokedPath(.init(lineWidth: 60, lineCap: .round))
}
var animatableData: Double {
get { progress }
set { progress = newValue }
}
}
let view = MyExperimentalView()
PlaygroundPage.current.setLiveView(view)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment