Replace the Typst/ImageMagick studio with a SwiftPM executable that composes each marketing screen as a SwiftUI view and renders it headlessly via ImageRenderer to exact 1284x2778 PNGs. - Devices are a real iPhone 17 Pro Max .usdz textured + posed in SceneKit (graphite body, studio IBL); screen curvature/bezel are the model's geometry. - Per-screen layouts (pose, globe, route arc, encryption flow) in ScreenSpec. - send-anywhere: globe + Paris->LA route arc; uses the app's share screen. - stay-private: converging beams -> glowing padlock -> binary protection stream, with localized CHIFFREMENT/PROTECTION banners and an auto-sizing header panel. - Captions auto-shrink per locale (ViewThatFits) so long translations never clip. - Screenshots are transient (generated/shots, git-ignored), captured per locale; generate.sh does capture + composite in one shot. - Commit the .usdz model (CC BY 4.0, see assets/ATTRIBUTION.md) and globe asset. - Remove the superseded Typst pipeline (screens.typ, build.sh, frame.sh, warp.sh).
4.8 KiB
App Store screenshot studio
Code-driven, fully local App Store screenshots — composed natively with SwiftUI +
ImageRenderer (no Typst, no ImageMagick). Each frame is a SwiftUI view (gradient
background + globe + device with the app screenshot + localized caption) rendered
off-screen to an exact-size PNG. Captions come from strings.json (9 locales); app
screenshots come from the real app (see capture, below).
Why SwiftUI: the screen curvature is the real iOS squircle
(RoundedRectangle(style: .continuous)), SF Pro resolves for free, and the hero tilt
is a native .rotation3DEffect (real perspective) — no mask-guessing or warp math.
Requirements
- Xcode / Swift toolchain (macOS 14+). That's it.
Render
swift run studio # all locales × screens -> generated/<Language>/
swift run studio --publish # -> ../<Language>/ (ships to App Store)
LOCALES="fr de" SCREENS="share-securely" swift run studio # subset
Default writes to generated/ (safe). --publish overwrites the real language folders.
Run from this directory (it reads strings.json and assets/ relative to cwd).
Full pipeline
./capture.sh # real localized app screens -> assets/shots/<locale>/<screen>.png
swift run studio # composite everything -> generated/<Language>/
swift run studio --publish # when happy -> ships to ../<Language>/
capture.sh drives the app into each screen via the DEBUG -VniScreenshot launch
argument (see apple/VniDrop/App/ScreenshotSupport.swift), once per locale via
-AppleLanguages, in dark mode with a 9:41 status bar.
Device rendering (3D by default)
The device is a real iPhone 17 Pro Max .usdz (assets/iphone-17-pro-max.usdz, CC BY
4.0 — see assets/ATTRIBUTION.md). SceneKitDeviceRenderer textures the app screenshot
onto the screen mesh and renders it off-screen with SceneKit. The screen curvature,
bezel and Dynamic Island are the model's real geometry; the hero tilt is a real camera
perspective (spec.device.tilt), not a warp.
Model-specific quirks handled in SceneKitDeviceRenderer.swift (re-derive if the model
changes — see the inspection scripts approach in git history):
- Warm-up render:
SCNRenderer.snapshotreturns empty on its first call; we render a throwaway 16×32 first, then the real frame. - Bounding box: use
rootNode.boundingBox(the model is ~16 units tall via an ancestor scale).flattenedClone()collapses this model to nothing — don't use it. - Camera orientation: front is −Z;
look(at:)renders nothing, so the camera is yawed 180° by hand (eulerAngles). - Crisp screen: the screen material has a wavy normal map and there's a front glass mesh with its own waviness — both ripple the image. We clear the screen normal and hide the glass mesh so the app content stays flat/crisp (App Store requirement).
Set DEVICE=2d to fall back to the flat mockup compositor (ClipDeviceRenderer +
assets/mockup-straight.png, .continuous clip) if you ever want the non-3D path.
Assets (git-ignored except the model — you supply the rest)
assets/iphone-17-pro-max.usdz— the 3D device (committed, withATTRIBUTION.md).assets/globe.png— transparent globe for the hero.assets/shots/<locale>/<screen>.png— captured app screenshots (fromcapture.sh).assets/mockup-straight.png— only needed for theDEVICE=2dfallback.
Layout & tuning
Sources/studio/ScreenSpec.swift— per-screen layout (gradient stops, caption placement, globe + device position/size, hero tilt). Numbers are in pixels at scale 1.MockupGeometry.straightin the same file — where the glass sits insidemockup-straight.png(fractions of the mockup) + corner radius. Measure once against your mockup export and set these; placeholders are approximate.Sources/studio/ScreenFrame.swift— the composition (layer order, caption styling).Sources/studio/DeviceView.swift— the device layer.ClipDeviceRendererdoes the 2D.continuousclip today; aSceneKitDeviceRenderer(texture the shot onto a real iPhone.usdz, render withSCNRenderer.snapshot()) can drop in behind the sameDeviceRendererprotocol for full 3D — nothing else changes.
Status
- ✅ SwiftUI compositor: gradient + caption + globe + device, exact 1284×2778, headless.
- ✅ Real 3D iPhone: screenshot textured onto the
.usdzscreen mesh (SceneKit), crisp. - ✅ Real hero tilt via 3D camera perspective (replaces
warp.sh+ rotated mockup). - ✅ Screenshot capture:
capture.sh+ the#if DEBUGfixture gateway, per locale. - ⬜ Tune device position/size/tilt per screen in
ScreenSpec.swiftagainst the originals (esp.send-anywhere: caption overlaps the phone; globe layer needs its asset). - ⬜ Ribbon art layer for
stay-private. - ✅ 2D fallback (
DEVICE=2d) retained for the flat mockup path.