Programmatic motion graphics in Swift. SwiftUI scenes + real Metal shaders → MP4.
The native-Apple answer to Remotion — built for the era where AI writes the motion graphics.
https://github.com/skyblanket/swift-render/raw/main/docs/assets/launch-film.mp4
The 55-second launch film above is one Swift file (
LaunchFilm.swift) — Timeline sequencing, springs, four live Metal shaders, 3D, an audio-reactive segment, and a synthesized soundtrack. Written by an AI, 3,450 frames rendered in 29 seconds on a MacBook. Click ▶. Sound on.
git clone https://github.com/skyblanket/swift-render && cd swift-render
swift run swift-render render JustRenderIt --audio audio/jri.m4a --out out/ad.mp4 # ~7s later: a finished ad, sound includedRemotion is great — and it's React rendered by headless Chromium, frame by frame, screenshot by screenshot. swift-render is a different bet: render natively on the GPU-accelerated Apple stack, and make every frame a pure function of time.
| swift-render | Remotion | |
|---|---|---|
| Render engine | Native SwiftUI ImageRenderer |
Headless Chromium screenshots |
| 1080p60 render speed | ~100–140 fps (M-series) | typically ~15–30 fps |
| Animation model | t: Double → View. That's the whole API |
useCurrentFrame() + hooks, refs, effect deps |
| Determinism | Proven — byte-identical re-renders, tested in CI | best-effort (browser, font, thread timing) |
| GPU shaders | Real Metal (.colorEffect, 12 shaders included) |
WebGL/canvas workarounds |
| Typography | Native SF / CoreText, SF Symbols, full blend modes | Web fonts in a browser |
| Audio-reactive | Built-in offline FFT → audio.band(.bass, at: t) |
useAudioData + visualization utils |
| Data-driven renders | --props file.json (Codable) |
inputProps ✓ |
| Sequencing | Timeline { Clip } result builder |
<Sequence> / <Series> ✓ |
| License | MIT — free for everyone | source-available; free ≤3-person companies, then $25/dev/mo ($100/mo min) |
| Install weight | Swift package, zero dependencies | node_modules + a Chromium download |
| Toolchain | swift run, done |
npm, bundler, browser binaries |
| Runs on | macOS 14+ | anywhere Node runs ✓ |
| Web preview/player | ❌ render PNG/MP4 fast instead | ✓ Studio + <Player> — genuinely good |
| Render farm | your Mac (it's fast) | Lambda ✓ |
Use Remotion if you need browser embeds, a web player, or Lambda-scale farms. Use swift-render if you want native quality, 5–10× faster local renders, real shaders, and an API a language model writes correctly on the first try.
A scene is a pure function: time in, view out. No state, no timers, no animation races — and nothing for an LLM to hallucinate.
import SwiftUI
public struct Hello: RenderScene {
public static let defaultDuration: Double = 3.0
@MainActor public static func body(at t: Double, duration: Double) -> some View {
let p = Ease.spring(t, from: 0, to: 1, response: 0.5, dampingFraction: 0.6)
Text("hello.")
.font(.system(size: 120, weight: .black))
.foregroundStyle(.white)
.scaleEffect(0.8 + 0.2 * p)
.opacity(Ease.clip(t, 0, 0.4))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black)
}
}Register it in Sources/SwiftRenderCLI/main.swift, then:
swift run swift-render render Hello --out out/hello.mp4
swift run swift-render frame Hello --at 1.2 --out out/check.png # preview one frame in ~1sNo segment math, no frame counting. Clips get local time; transitions overlap automatically; pinned clips float over everything:
Timeline(t) {
Clip(2.2) { local in TitleCard(t: local) }
Clip(2.4) { local in SpringShowcase(t: local) }.transition(.slide(0.45))
Clip(2.4) { local in MetalMoment(t: local) }.transition(.flash())
Clip(4.0) { local in Outro(t: local) }.transition(.fade(0.5))
Clip(at: 0, for: 9.9) { local in ProgressHUD(t: local) } // pinned overlay
}https://github.com/skyblanket/swift-render/raw/main/docs/assets/timeline-demo.mp4
Spring is solved analytically (under/critical/overdamped) — position at any t is computed directly, never integrated. Scrub to frame 4081 and get the exact same pixels, every run. Plus easeOutBack, elastic, bounce, expo, cubicBezier(…).
The file is FFT-analyzed once before rendering (RMS + bass/mid/high envelopes); scenes read it as a pure lookup, so determinism survives:
public struct AudioBars: PropsAudioScene {
public static func body(at t: Double, duration: Double,
props: Props, audio: AudioTrack) -> some View {
let bass = audio.band(.bass, at: t) // 0…1
// scale, glow, slam on the beat …
}
}https://github.com/skyblanket/swift-render/raw/main/docs/assets/audiobars.mp4
The soundtrack itself is generated by
tools/make_jri_audio.py— kicks, whooshes and an 808 placed at the scene's exact timeline anchors. Audio and video can't drift, by construction.
swift run swift-render props AudioBars > p.json # JSON template from the scene's defaults
swift run swift-render render AudioBars --props p.json --audio beat.wavPipe in JSON per record and render a thousand personalized variants — the AI/data-pipeline workflow Remotion's inputProps made popular, native.
Scenes declare their own soundtrack — same constants drive the cuts and the hits, so audio/video sync is structural, not manual. The synth (kicks, claps, hats, crashes, sub bass, risers, 808 booms, drones, whooshes — with two-bus sidechain pumping) is pure Swift, deterministic, and renders a minute of audio in ~0.1s:
public static func soundtrack(duration: Double) -> Score? {
Score(duration: duration) {
fourOnFloor(from: 2.4, to: 33.6) // kicks + backbeat claps
hatSixteenths(from: 2.4, to: 33.6)
bassline([.a1, .a1, .c2, .g1], from: 2.4, to: 33.6)
crashes(at: chapters) // the SAME array as the Timeline
riser(at: 33.6, duration: 2.4)
boom(at: 36.0)
}
}swift run swift-render render KineticType # score synthesized + muxed, zero flags
swift run swift-render audio KineticType --out out/score.wav # export the track alone--audio file always wins over a scene's score. Audio-reactive scenes react
to their own synthesized score — declare the beat, and the visuals hear it.
Drop a .metal file in Sources/SwiftRender/Shaders/ and swift build — shaders compile automatically (SwiftPM build plugin). Call them on any view:
Rectangle().fill(.black).colorEffect(
ShaderLibrary.bundle(.module).galaxy(.float2(1920, 1080), .float(Float(t)))
)Eighteen ship in three packs — rimGlow, foilHolographic, plasmaField, chromaticAberration, audioBars, caustics, liquidMetal, kaleidoscope, truchet, galaxy, neonGrid, smokeFlow, warpTunnel — plus the studio pack from the launch film: metaballs (raymarched chrome), inkFlow, interference, voronoiInk, monoTunnel:
https://github.com/skyblanket/swift-render/raw/main/docs/assets/shader-gallery.mp4
(If a .metal file is newer than the compiled metallib, the CLI warns you loudly — no silently-stale shaders.)
Swiss, neo-brutalist, Bauhaus, synthwave, glassmorphism, terminal, art deco, vaporwave, blueprint, stop-motion zine, fluid aurora, kinetic type — each one ~40 lines of Swift, chained by a live card-zoom transition, closing on all twelve running at once:
https://github.com/skyblanket/swift-render/raw/main/docs/assets/style-reel.mp4
swift run swift-render render StyleReel --audio out/reel.wavLaunchFilm2 — the launch film: every feature, one file |
swift run swift-render render LaunchFilm2 --audio out/launch.wav |
StyleReel — 12 aesthetics with card-zoom transitions |
swift run swift-render render StyleReel --audio out/reel.wav |
Kinetic — 12s kinetic-typography reel: word slams, marquee, galaxy iris, odometer ring |
swift run swift-render render Kinetic |
JustRenderIt — the hero ad, beat-synced soundtrack included |
swift run swift-render render JustRenderIt --audio audio/jri.m4a |
AudioBars — audio-reactive + props reference scene |
swift run swift-render render AudioBars --audio audio/jri.m4a |
TimelineDemo — Timeline/transition/springs reference |
swift run swift-render render TimelineDemo |
ShaderGallery, ShaderShowcase, TextReveal, CardStack, ParticleField, … |
swift run swift-render list |
https://github.com/skyblanket/swift-render/raw/main/docs/assets/kinetic.mp4
swift-render render <Scene> [--duration s] [--fps n] [--aspect 16:9|9:16|1:1]
[--audio file] [--props file.json] [--range a:b]
[--no-postfx] [--out path]
swift-render frame <Scene> --at <t> [--out path.png] fast single-frame preview
swift-render props <Scene> print default props JSON
swift-render list all registered scenes
- Your scene is
(t, duration) → some View— pure, deterministic,@MainActor. Recorderwalks frames0..<duration*fps, renders each viaImageRenderer, pipes BGRA pixel buffers intoAVAssetWriter(H.264), muxes audio withAVMutableComposition.- A global
PostFXpass (film grain + vignette, seeded and deterministic) makes raw SwiftUI feel cinema-grade. Opt out with--no-postfxor own it per-scene withownsPostFX. - There is no step 4. No browser, no server, no project file.
Determinism isn't a vibe — swift test includes a render-twice-byte-identical check, and CI runs it on every push.
.package(url: "https://github.com/skyblanket/swift-render", from: "0.5.0")import SwiftRender
let recorder = Recorder(config: .init(fps: 60, size: .init(width: 1920, height: 1080)))
try await recorder.render(to: url, duration: 5) { t in MyView(t: t) }docs/ai-quickstart.md is a compact, LLM-ready guide to the whole API. The design goal: an agent that has never seen this repo writes a working, good-looking scene on the first attempt. (This README's hero ad is the proof.)
- macOS 14+ (Apple silicon recommended; that's where the speed numbers come from)
- Xcode 15+ toolchain (
xcrun metalneeded only when editing shaders) - ffmpeg optional — handy for GIF/thumbnail post-processing
- SwiftPM build plugin for automatic metallib compilation
contact <Scene>grid-sheet export · transparent ProRes 4444 · 10-bit masters- Audio-reactive FFT improvements (configurable bands, onset detection)
- Linux? No — this is proudly the native-Apple lane.
MIT — see LICENSE.
See CONTRIBUTING.md. Scenes must stay pure functions of t — that rule is the product.