Last active
March 5, 2023 15:54
-
-
Save eschmar/e6854e5d6ce50404ee5f182e8eeb4950 to your computer and use it in GitHub Desktop.
Blog: Capture Xcode Playground SwiftUI animations as mp4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* **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