animate-my-app 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CREDITS.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +71 -2
  4. package/bin/install.js +41 -0
  5. package/package.json +32 -4
  6. package/skills/animate-my-app/SKILL.md +121 -0
  7. package/skills/animate-my-app/assets/template/__APP__/App.swift +38 -0
  8. package/skills/animate-my-app/assets/template/__APP__/Assets.xcassets/Contents.json +6 -0
  9. package/skills/animate-my-app/assets/template/__APP__/ContentView.swift +123 -0
  10. package/skills/animate-my-app/assets/template/__APP__/SceneBridge.swift +88 -0
  11. package/skills/animate-my-app/assets/template/__APP__/Web/content.js +53 -0
  12. package/skills/animate-my-app/assets/template/__APP__/Web/scene.html +28 -0
  13. package/skills/animate-my-app/assets/template/__APP__/Web/scene.js +133 -0
  14. package/skills/animate-my-app/assets/template/__APP__/Web/three.min.js +7 -0
  15. package/skills/animate-my-app/assets/template/__APP__.xcodeproj/project.pbxproj +164 -0
  16. package/skills/animate-my-app/assets/template/__APP__.xcodeproj/xcshareddata/xcschemes/__APP__.xcscheme +46 -0
  17. package/skills/animate-my-app/references/gotchas.md +446 -0
  18. package/skills/animate-my-app/references/motion-physics.md +260 -0
  19. package/skills/animate-my-app/references/photo-layers.md +84 -0
  20. package/skills/animate-my-app/references/procedural-3d.md +252 -0
  21. package/skills/animate-my-app/references/research-and-tools.md +127 -0
  22. package/skills/animate-my-app/references/stack.md +68 -0
  23. package/skills/animate-my-app/references/testing.md +162 -0
  24. package/skills/animate-my-app/scripts/frame_video.swift +190 -0
  25. package/skills/animate-my-app/scripts/frames.swift +27 -0
  26. package/skills/animate-my-app/scripts/hitch.swift +28 -0
  27. package/skills/animate-my-app/scripts/new_app.sh +24 -0
  28. package/skills/animate-my-app/scripts/sf_symbol_bitmap.swift +18 -0
  29. package/skills/animate-my-app/scripts/sf_symbol_codepoints.py +30 -0
  30. package/skills/animate-my-app/scripts/side_by_side.swift +67 -0
package/CREDITS.md ADDED
@@ -0,0 +1,17 @@
1
+ # Credits
2
+
3
+ animate-my-app is written by [@oiharshit](https://x.com/oiharshit). It stands on other people's work:
4
+
5
+ - **three.js** (r159), bundled in the app template at `skills/animate-my-app/assets/template/__APP__/Web/three.min.js`.
6
+ Copyright 2010-2023 three.js authors, MIT licence (the notice is kept in the file). https://threejs.org
7
+ - **Emil Kowalski**: the motion and polish rules this skill follows (strong ease-out curves, no entrances from
8
+ `scale(0)`, 0.97 press feedback, restraint) come from his design-engineering teaching. His course:
9
+ https://animations.dev. If you have his `emil-design-eng` / `emil-animations` skills installed, this skill loads them.
10
+ - **Apple, WWDC "Designing Fluid Interfaces" (2018)**: springs described by damping ratio and response,
11
+ interruptible motion and velocity hand-off.
12
+ - **Companion skills** that animate-my-app loads when you have them installed (optional, not included here, credit to
13
+ their authors): `motion-design`, `apple-design`, `micro-interactions`, `interaction-design`, `lottie-motion-design`,
14
+ `better-ui`, `better-typography`, and Figma's official skills (`figma-design-to-code`, `figma-implement-motion`,
15
+ `figma-swiftui`).
16
+ - **Apple SF Symbols**: `scripts/sf_symbol_bitmap.swift` renders symbols from your own Mac for use in your apps;
17
+ follow Apple's SF Symbols licence terms.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 @oiharshit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # animate-my-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Claude Code skill that turns an app design into a **native, animated iOS app**, the kind of motion people post and ask "how did you make this?".
4
+
5
+ Drop in a Figma link or screenshots and ask Claude to animate it. The skill makes Claude:
6
+
7
+ - build a real SwiftUI app: layout, text, buttons, haptics and safe areas stay native;
8
+ - render hero parts (logos, 3D objects, clay characters, paper, chrome, painted backgrounds) in a **three.js** layer, built procedurally rather than from cut-up images;
9
+ - move things with **real physics**: damped springs, gravity, squash and stretch that keeps volume, contact shadows, and staging that waits for things to land;
10
+ - load every texture and compile every shader **before** the clock starts, so nothing loads mid-animation;
11
+ - **test what it builds**: Simulator recordings checked frame by frame, a frame-time probe (target: 0 frames over 25 ms), and an optional self-test on your iPhone;
12
+ - ask you about the motion before guessing.
13
+
14
+ ## Install
15
+
16
+ **Claude Code plugin (recommended)**
17
+
18
+ ```bash
19
+ claude plugin marketplace add contactharshitdesign-design/animate-my-app
20
+ ```
21
+
22
+ ```bash
23
+ claude plugin install animate-my-app@animate-my-app
24
+ ```
25
+
26
+ Or inside Claude Code: `/plugin marketplace add contactharshitdesign-design/animate-my-app`, then `/plugin install animate-my-app@animate-my-app`.
27
+
28
+ **npx (copies the skill into `~/.claude/skills`)**
29
+
30
+ ```bash
31
+ npx animate-my-app
32
+ ```
33
+
34
+ Add `--project` to install it into the current project's `.claude/skills` instead, and `--force` to replace an older copy (it's backed up first).
35
+
36
+ ## Use
37
+
38
+ Ask Claude Code for any app motion, for example:
39
+
40
+ - "Animate this onboarding from Figma: <link>"
41
+ - "Make the splash logo drop in and squash like clay"
42
+ - "The paper should fall onto page 2 with real physics"
43
+ - "Turn this seat map into 3D and fly into the seat I tap"
44
+
45
+ The skill scaffolds an app with `scripts/new_app.sh`, builds it, runs it in the Simulator and reports measured results.
46
+
47
+ ## Requirements
48
+
49
+ - macOS with Xcode and an iOS Simulator runtime
50
+ - Claude Code
51
+ - Optional: an Apple developer team ID to run on your own iPhone; the Figma MCP server for Figma links
52
+
53
+ **Optional companion skills.** If these are installed, the skill loads them; otherwise it uses its own rules:
54
+ `motion-design`, `apple-design`, `emil-design-eng`, `micro-interactions`, and Figma's official skills.
55
+
56
+ ## What's inside
57
+
58
+ ```
59
+ skills/animate-my-app/
60
+ ├── SKILL.md the workflow and the rules
61
+ ├── references/ stack, motion physics, procedural 3D, testing, research and tools, lessons learned
62
+ ├── scripts/ new_app.sh scaffold, frame extraction, hitch probe, framed video, SF Symbol export
63
+ └── assets/template/ a working SwiftUI + three.js app the scaffold copies
64
+ ```
65
+
66
+ ## Credits
67
+
68
+ Built by [@oiharshit](https://x.com/oiharshit). It builds on three.js (MIT), Emil Kowalski's design-engineering teaching and Apple's WWDC talk *Designing Fluid Interfaces*; see [CREDITS.md](CREDITS.md).
69
+
70
+ ## License
71
+
72
+ [MIT](LICENSE)
package/bin/install.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // npx animate-my-app → installs the skill for you (~/.claude/skills/animate-my-app)
3
+ // npx animate-my-app --project → installs it into this project (./.claude/skills/animate-my-app)
4
+ // add --force to replace an existing copy (the old one is kept as a timestamped backup)
5
+ 'use strict';
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ const args = process.argv.slice(2);
11
+ if (args.includes('--help') || args.includes('-h')) {
12
+ console.log('Usage: npx animate-my-app [--project] [--force]');
13
+ process.exit(0);
14
+ }
15
+ const project = args.includes('--project');
16
+ const force = args.includes('--force');
17
+ const src = path.join(__dirname, '..', 'skills', 'animate-my-app');
18
+ const root = project ? path.join(process.cwd(), '.claude', 'skills') : path.join(os.homedir(), '.claude', 'skills');
19
+ const dest = path.join(root, 'animate-my-app');
20
+
21
+ if (!fs.existsSync(path.join(src, 'SKILL.md'))) { console.error('animate-my-app: the packaged skill is missing.'); process.exit(1); }
22
+ fs.mkdirSync(root, { recursive: true });
23
+ if (fs.existsSync(dest)) {
24
+ if (!force) {
25
+ console.log(`animate-my-app is already installed at ${dest}\nRun again with --force to replace it (your current copy is backed up).`);
26
+ process.exit(0);
27
+ }
28
+ // backups live outside the skills folder, or Claude would load them as a second copy of the skill
29
+ const backups = path.join(os.homedir(), '.claude', 'skill-backups');
30
+ fs.mkdirSync(backups, { recursive: true });
31
+ const backup = path.join(backups, `animate-my-app-${new Date().toISOString().replace(/[:.]/g, '-')}`);
32
+ fs.renameSync(dest, backup);
33
+ console.log(`Backed up the old copy to ${backup}`);
34
+ }
35
+ fs.cpSync(src, dest, { recursive: true });
36
+ for (const f of ['new_app.sh']) { // keep scripts executable
37
+ const p = path.join(dest, 'scripts', f);
38
+ if (fs.existsSync(p)) fs.chmodSync(p, 0o755);
39
+ }
40
+ console.log(`Installed animate-my-app → ${dest}`);
41
+ console.log('Restart Claude Code (or run /reload-skills), then ask it to animate your app design.');
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "animate-my-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "A Claude Code skill: turn an app design into a native, animated iOS app (SwiftUI + three.js), tested on device. Run `npx animate-my-app` to install.",
5
+ "bin": {
6
+ "animate-my-app": "bin/install.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "skills/",
11
+ "README.md",
12
+ "LICENSE",
13
+ "CREDITS.md"
14
+ ],
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "skill",
19
+ "animation",
20
+ "ios",
21
+ "swiftui",
22
+ "threejs"
23
+ ],
24
+ "author": "@oiharshit",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=16"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/contactharshitdesign-design/animate-my-app.git"
32
+ },
33
+ "homepage": "https://github.com/contactharshitdesign-design/animate-my-app#readme"
34
+ }
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: animate-my-app
3
+ description: >
4
+ Turn an app design (Figma frames, screenshots, a reference video or an existing app) into a real, native,
5
+ animated iOS app that runs in the Simulator and on iPhone: SwiftUI for layout, text and buttons, plus a
6
+ transparent three.js/WebGL layer for hero motion (clay or chrome 3D objects, logos that assemble, shader and
7
+ paint backgrounds, page curls, particles, physics, gesture-driven 3D). Use it for any request to animate an
8
+ app design or screen; build or fix an onboarding, splash, intro, screen transition, logo or hero animation;
9
+ recreate motion from a reference (Dribbble, Pinterest, a video); tune how something moves (springs, squash and
10
+ stretch, gestures, swipes, card stacks, pull-to-refresh); or add three.js, shaders or physics to a native iOS
11
+ app. Ships a scaffold script, motion and procedural-3D recipes, measurement scripts for frame times and
12
+ motion, and a list of WKWebView, three.js, SwiftUI and Simulator gotchas.
13
+ license: MIT
14
+ metadata:
15
+ author: "@oiharshit"
16
+ version: "1.0.0"
17
+ ---
18
+
19
+ # Animate My App
20
+
21
+ Take a designed app screen (usually from Figma) and ship it as a native iOS app whose hero moments move like a
22
+ Rive- or three.js-level piece: physical, staged, measured. Speed comes from **reusing this pipeline** (scaffold,
23
+ engine, test scripts, recipes), not re-deriving it for every app.
24
+
25
+ ## 0. Before anything: optional companion skills
26
+
27
+ If these skills are installed, load them (Skill tool) at the start of a build; they raise the motion and design bar:
28
+ `motion-design`, `apple-design`, `emil-design-eng`, `micro-interactions`.
29
+ Add as relevant when installed: `figma-design-to-code` (required before the Figma MCP `get_design_context` tool),
30
+ `figma-implement-motion` (Figma motion data), `figma-swiftui`, `interaction-design`, `lottie-motion-design`
31
+ (Lottie), typography/UI polish skills.
32
+ If none are installed, follow the rules in this skill and its references; they are self-contained.
33
+
34
+ ## 1. Intake (fast), then ASK
35
+
36
+ 1. Pull every frame that was linked: Figma MCP `get_screenshot` + `get_design_context` + `get_metadata`
37
+ (+ `download_assets` for raster art, fonts, exact colours and sizes). Details and fallbacks:
38
+ `references/research-and-tools.md`.
39
+ 2. Find references when the motion idea is open: Mobbin (MCP or website), Pinterest, Dribbble, a supplied video.
40
+ Generated assets or motion references can come from an image/video generation service if one is connected.
41
+ 3. **Ask** (2–4 pointed questions) whenever the motion intent is not explicit: sequence and staging, feel (bouncy
42
+ clay / crisp / calm), total length, what is procedural vs image, sound or none, the hand-off between screens.
43
+ When motion is described in words, restate it as physics and confirm ("squeeze → stretch and move on the same
44
+ frame, tallest early in the climb?"). Ask again mid-build whenever a choice changes the result. Never guess
45
+ on intent that matters.
46
+
47
+ ## 2. Build: native app + three.js layer
48
+
49
+ Scaffold in one command (working app, replay protocol, preload/warm-start engine, demo content):
50
+
51
+ ```bash
52
+ scripts/new_app.sh <AppName> <parent-dir> [bundle.id] # default bundle id: com.example.<appname>
53
+ ```
54
+
55
+ Architecture, bridge and engine API: `references/stack.md`. In short:
56
+ - SwiftUI owns layout, text, buttons, haptics, safe areas. A transparent full-screen `WKWebView` runs three.js
57
+ for hero parts, served over a custom scheme (WebGL refuses `file://` textures). Created eagerly at launch.
58
+ - **One timeline** (JS) is the source of truth; it fires cues (`headline`, `button`, …) that SwiftUI animates.
59
+ - Design canvas = the Figma frame in points (iPhone 17 Pro 402×874); ortho camera in design points.
60
+ - Pick the right tool per part: three.js for 3D/clay/shaders/procedural paint; SwiftUI springs,
61
+ `matchedGeometryEffect`, `PhaseAnimator`/`KeyframeAnimator` for UI; Rive/Lottie only when such assets are
62
+ supplied or the part is pure 2D vector character animation.
63
+
64
+ **Build hero parts procedurally, not from cut-up images.** Image cut-outs crack, fringe and flicker. Recipes for
65
+ clay letters (SDF + surface nets + clay shader), chrome and metal, self-painting brush backgrounds, paper, and
66
+ splitting illustrations into clean animatable pieces: `references/procedural-3d.md`. Photographed people with
67
+ swappable pieces: `references/photo-layers.md`.
68
+
69
+ ## 3. Motion rules
70
+
71
+ Full formulas and constants: `references/motion-physics.md`.
72
+ - Real physics: damped springs (Apple damping/response), gravity drops, squash and stretch that **preserves
73
+ volume**, contact shadows that react to height.
74
+ - Anticipation without dwell: squeeze, then stretch **and** move on the same frame; peak stretch early in the
75
+ travel; jelly follow-through; overshoot then a braking squash. Stretch must be clearly visible.
76
+ - **Staging is gated on arrival**: the next phase starts only after the previous element has truly landed
77
+ (compute the arrival time, don't guess a delay).
78
+ - Entrances never from `scale(0)`; press feedback 0.97 + light haptic; always a reduced-motion path.
79
+ - Buttons and text match Figma exactly (measure size, radius, colour, inner glows) and sit per Apple HIG
80
+ (primary button 16 pt above the bottom safe area; keep the designed spacing to the headline).
81
+
82
+ ## 4. Hard rules (defaults)
83
+
84
+ - **No clipping** unless asked or the Figma frame has "Clip content" on. Pad every bounded surface (planes,
85
+ textures, canvases, frames) so brush ends, shadows, glows and overshoot fit. Check every edge of rendered frames
86
+ for straight cuts before delivering. If source art itself is cut, repair it.
87
+ - **No mid-animation loading:** decode + GPU-upload every texture and pre-warm every shader *before* the clock
88
+ starts; warm frames before the first move. Measure; target 0 frames > 25 ms on replay.
89
+ - **Replay protocol:** every app listens for the Darwin notification `com.animatemyapp.replay` and replays its
90
+ intro in place (fade to white → reset → warm → start), plus tap-to-replay outside buttons. Never relaunch to replay.
91
+ - Real native app in the Simulator (and on iPhone), never a localhost/web demo. Status bar 9:41 for anything shown.
92
+ - **Concentric corners:** outer radius = inner radius + padding for every nested rounded shape (thumbnail in a
93
+ card, highlight in a menu, chip in a composer, outline around media). Derive radii from the innermost element
94
+ out; break it only when truly necessary (full-bleed media clipped by its card simply inherits the card radius).
95
+ - No sound unless asked.
96
+ - **Test everything** yourself before reporting (section 5). Report measured numbers, not impressions.
97
+
98
+ ## 5. Verify
99
+
100
+ `references/testing.md` has the scripts and checks. Minimum every iteration:
101
+ 1. Build (iOS Simulator build tool if available, else `xcodebuild`), install, launch on a simulator you booted
102
+ (pass its UDID to every `simctl` call).
103
+ 2. Record (`xcrun simctl io <UDID> recordVideo`), extract frames (`scripts/frames.swift`), and check the
104
+ sequence numerically: timing order, stretch amounts, overshoot, arrival before next phase, no cuts at edges.
105
+ 3. Frame-time probe in-app for stalls (snippet in testing.md); replay via the notification and confirm 0 stalls.
106
+ Simulator WebGL is software-rendered and noisy under load: judge performance on a real iPhone when one is available.
107
+ 4. Look at contact sheets yourself; compare against the Figma screenshot.
108
+
109
+ ## 6. Deliver
110
+
111
+ - **Recording:** override the status bar to 9:41, warm-launch once, then trigger the in-place replay
112
+ (`xcrun simctl spawn <UDID> notifyutil -p com.animatemyapp.replay`) while recording with
113
+ `xcrun simctl io <UDID> recordVideo`, or with your own screen recorder. Don't record unless asked.
114
+ - **Framed video** when asked: `scripts/frame_video.swift` (iPhone frame on white, ≥ 120 px margins, 60 fps).
115
+ - **Side-by-side with a reference:** `scripts/side_by_side.swift`.
116
+ - Gotchas that cost time before: `references/gotchas.md`. Read it once per build.
117
+
118
+ ## 7. Keep this skill growing
119
+
120
+ After each app, add what was learned (new recipe, new gotcha, a measured constant) to the matching reference
121
+ file, written as general guidance, so the next build is faster.
@@ -0,0 +1,38 @@
1
+ import SwiftUI
2
+ import CoreText
3
+
4
+ @main
5
+ struct __APP__App: App {
6
+ init() {
7
+ Self.registerFonts()
8
+ _ = SceneBridge.shared // start the scene's web process now, in parallel with launch
9
+ Self.listenForReplay()
10
+ }
11
+
12
+ /// Registers every font file bundled with the app (drop .ttf/.otf anywhere in the app folder).
13
+ /// Use the font's PostScript name in `.font(.custom(...))`.
14
+ static func registerFonts() {
15
+ for ext in ["ttf", "otf", "ttc"] {
16
+ for url in Bundle.main.urls(forResourcesWithExtension: ext, subdirectory: nil) ?? [] {
17
+ CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
18
+ }
19
+ }
20
+ }
21
+
22
+ /// `xcrun simctl spawn booted notifyutil -p com.animatemyapp.replay` (or any tool posting that Darwin notification) replays the intro in place.
23
+ static func listenForReplay() {
24
+ CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), nil, { _, _, _, _, _ in
25
+ DispatchQueue.main.async { NotificationCenter.default.post(name: .replayRequested, object: nil) }
26
+ }, "com.animatemyapp.replay" as CFString, nil, .deliverImmediately)
27
+ }
28
+
29
+ var body: some Scene {
30
+ WindowGroup {
31
+ ContentView()
32
+ }
33
+ }
34
+ }
35
+
36
+ extension Notification.Name {
37
+ static let replayRequested = Notification.Name("com.animatemyapp.replay")
38
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "info": {
3
+ "author": "xcode",
4
+ "version": 1
5
+ }
6
+ }
@@ -0,0 +1,123 @@
1
+ import SwiftUI
2
+
3
+ /// Laid out on the Figma canvas in points and scaled to the device. The three.js timeline is the single
4
+ /// source of truth: it sends cues ("headline", "button", …) and SwiftUI animates the native UI on them.
5
+ struct ContentView: View {
6
+
7
+ // Design canvas = the Figma frame (iPhone 17 Pro logical size).
8
+ private let CW: CGFloat = 402
9
+ private let CH: CGFloat = 874
10
+
11
+ private let bridge = SceneBridge.shared
12
+ @State private var headIn = false
13
+ @State private var btnIn = false
14
+ @State private var veil = false // content fades to white while a replay resets behind it
15
+ @State private var replaying = false
16
+
17
+ private let brand = Color(red: 8 / 255, green: 118 / 255, blue: 255 / 255)
18
+ private let ink = Color(red: 26 / 255, green: 26 / 255, blue: 26 / 255)
19
+
20
+ /// Primary action sits in the thumb zone: 16pt above the bottom safe area (Apple HIG), never in the
21
+ /// home-indicator area. Keep the designed spacing between headline and button.
22
+ private let buttonGapAboveSafeArea: CGFloat = 16
23
+
24
+ private var bottomSafeInset: CGFloat {
25
+ UIApplication.shared.connectedScenes
26
+ .compactMap { $0 as? UIWindowScene }
27
+ .flatMap { $0.windows }
28
+ .first { $0.isKeyWindow }?.safeAreaInsets.bottom ?? 34
29
+ }
30
+
31
+ var body: some View {
32
+ GeometryReader { geo in
33
+ let s = min(geo.size.width / CW, geo.size.height / CH)
34
+ ZStack {
35
+ Color.white
36
+
37
+ SceneLayer(bridge: bridge)
38
+ .frame(width: CW, height: CH)
39
+ .allowsHitTesting(false)
40
+ .opacity(veil ? 0 : 1)
41
+
42
+ // tap anywhere (except the button) to replay; the web view would otherwise swallow touches
43
+ Color.clear
44
+ .contentShape(Rectangle())
45
+ .onTapGesture { replay() }
46
+
47
+ VStack(spacing: 32) {
48
+ Text("Replace with the design's headline")
49
+ .font(.system(size: 34, weight: .bold))
50
+ .multilineTextAlignment(.center)
51
+ .foregroundColor(ink)
52
+ .frame(width: 360)
53
+ .allowsHitTesting(false)
54
+ .opacity(headIn ? 1 : 0)
55
+ .offset(y: headIn ? 0 : 26)
56
+
57
+ Button {
58
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
59
+ } label: {
60
+ Text("Get started")
61
+ .font(.system(size: 16, weight: .bold))
62
+ .foregroundColor(.white)
63
+ .frame(width: 354, height: 58)
64
+ .background(Capsule().fill(brand))
65
+ }
66
+ .buttonStyle(PressScaleStyle())
67
+ .allowsHitTesting(btnIn)
68
+ .opacity(btnIn ? 1 : 0)
69
+ .offset(y: btnIn ? 0 : 26)
70
+ .scaleEffect(btnIn ? 1 : 0.9)
71
+ }
72
+ .padding(.bottom, bottomSafeInset / s + buttonGapAboveSafeArea)
73
+ .frame(width: CW, height: CH, alignment: .bottom)
74
+ .opacity(veil ? 0 : 1)
75
+ }
76
+ .frame(width: CW, height: CH)
77
+ .scaleEffect(s)
78
+ .position(x: geo.size.width / 2, y: geo.size.height / 2)
79
+ }
80
+ .ignoresSafeArea()
81
+ .background(Color.white.ignoresSafeArea())
82
+ .onReceive(NotificationCenter.default.publisher(for: .replayRequested)) { _ in replay() }
83
+ .onAppear {
84
+ bridge.onEvent = { event in
85
+ switch event {
86
+ case "headline": withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { headIn = true }
87
+ case "button": withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { btnIn = true }
88
+ case "reset" where replaying:
89
+ // the scene is blank again; lift the veil a beat later so no old frame can show
90
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
91
+ withTransaction(Transaction(animation: nil)) { veil = false }
92
+ replaying = false
93
+ }
94
+ default: break
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ /// Restart the intro from its first frame: fade to white, reset behind the veil, start warm.
101
+ private func replay() {
102
+ guard !replaying else { return }
103
+ replaying = true
104
+ withAnimation(.easeOut(duration: 0.18)) { veil = true }
105
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
106
+ withTransaction(Transaction(animation: nil)) {
107
+ headIn = false
108
+ btnIn = false
109
+ }
110
+ bridge.replay()
111
+ }
112
+ }
113
+ }
114
+
115
+ /// Pressables acknowledge the touch immediately: a quick 0.97 scale-down, released with a soft spring.
116
+ private struct PressScaleStyle: ButtonStyle {
117
+ func makeBody(configuration: Configuration) -> some View {
118
+ configuration.label
119
+ .scaleEffect(configuration.isPressed ? 0.97 : 1)
120
+ .animation(configuration.isPressed ? .easeOut(duration: 0.1) : .spring(response: 0.3, dampingFraction: 0.6),
121
+ value: configuration.isPressed)
122
+ }
123
+ }
@@ -0,0 +1,88 @@
1
+ import SwiftUI
2
+ import WebKit
3
+
4
+ /// Serves bundled files over `appbundle://` so every file (scripts, textures) is same-origin —
5
+ /// WebGL refuses textures loaded from `file://` URLs.
6
+ final class BundleSchemeHandler: NSObject, WKURLSchemeHandler {
7
+ static let scheme = "appbundle"
8
+
9
+ func webView(_ webView: WKWebView, start task: WKURLSchemeTask) {
10
+ guard let url = task.request.url,
11
+ let file = Bundle.main.url(forResource: url.lastPathComponent, withExtension: nil),
12
+ let data = try? Data(contentsOf: file) else {
13
+ task.didFailWithError(URLError(.fileDoesNotExist))
14
+ return
15
+ }
16
+ let mime: String
17
+ switch file.pathExtension.lowercased() {
18
+ case "html": mime = "text/html; charset=utf-8"
19
+ case "js": mime = "text/javascript; charset=utf-8"
20
+ case "json": mime = "application/json"
21
+ case "png": mime = "image/png"
22
+ case "jpg", "jpeg": mime = "image/jpeg"
23
+ case "webp": mime = "image/webp"
24
+ case "glb": mime = "model/gltf-binary"
25
+ default: mime = "application/octet-stream"
26
+ }
27
+ let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "HTTP/1.1",
28
+ headerFields: ["Content-Type": mime, "Content-Length": "\(data.count)",
29
+ "Cache-Control": "no-store"])! // never a stale script after a reinstall
30
+ task.didReceive(response)
31
+ task.didReceive(data)
32
+ task.didFinish()
33
+ }
34
+
35
+ func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {}
36
+ }
37
+
38
+ /// Owns the WKWebView that renders the three.js scene and bridges its timeline cues to SwiftUI.
39
+ /// Created eagerly at app launch so the web process spins up in parallel with the app.
40
+ final class SceneBridge: NSObject, WKScriptMessageHandler {
41
+ static let shared = SceneBridge()
42
+
43
+ let webView: WKWebView
44
+ var onEvent: ((String) -> Void)?
45
+
46
+ private override init() {
47
+ let config = WKWebViewConfiguration()
48
+ config.websiteDataStore = .nonPersistent() // no disk cache between launches
49
+ config.setURLSchemeHandler(BundleSchemeHandler(), forURLScheme: BundleSchemeHandler.scheme)
50
+ webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 402, height: 874), configuration: config)
51
+ super.init()
52
+ config.userContentController.add(self, name: "scene")
53
+
54
+ webView.isOpaque = false
55
+ webView.backgroundColor = .clear
56
+ webView.scrollView.backgroundColor = .clear
57
+ webView.scrollView.isScrollEnabled = false
58
+ webView.scrollView.contentInsetAdjustmentBehavior = .never
59
+ webView.isUserInteractionEnabled = false
60
+ #if DEBUG
61
+ if #available(iOS 16.4, *) { webView.isInspectable = true }
62
+ #endif
63
+
64
+ // look-dev / test flags: SIMCTL_CHILD_SCENE_QUERY="perf&debug" xcrun simctl launch booted <bundle id>
65
+ let query = ProcessInfo.processInfo.environment["SCENE_QUERY"].map { "?\($0)" } ?? ""
66
+ if let url = URL(string: "\(BundleSchemeHandler.scheme)://app/scene.html\(query)") {
67
+ webView.load(URLRequest(url: url))
68
+ }
69
+ }
70
+
71
+ func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
72
+ guard let event = message.body as? String else { return }
73
+ if event.hasPrefix("perf:") { NSLog("[scene] %@", event); return } // frame-time probe (?perf)
74
+ onEvent?(event)
75
+ }
76
+
77
+ func replay() {
78
+ webView.evaluateJavaScript("window.scenePlay && window.scenePlay()")
79
+ }
80
+ }
81
+
82
+ /// Transparent, non-interactive layer that shows the three.js scene above the native background.
83
+ struct SceneLayer: UIViewRepresentable {
84
+ let bridge: SceneBridge
85
+
86
+ func makeUIView(context: Context) -> WKWebView { bridge.webView }
87
+ func updateUIView(_ webView: WKWebView, context: Context) {}
88
+ }
@@ -0,0 +1,53 @@
1
+ // Demo part — replace with the design's hero animation. Shows the patterns every part follows:
2
+ // state is a pure function of t (so replays are identical), physics via springs, volume-preserving squash,
3
+ // a contact shadow that reacts to height, and cues that bring in the native UI.
4
+ (function () {
5
+ 'use strict';
6
+ const S = window.SCENE;
7
+ if (!S) return;
8
+ const { THREE, scene, toWorld, spring, k, REDUCED } = S;
9
+
10
+ // a soft clay pill (replace with procedural geometry built for the design)
11
+ const body = new THREE.Mesh(
12
+ new THREE.CapsuleGeometry(34 * k, 60 * k, 12, 32),
13
+ new THREE.MeshPhysicalMaterial({ color: 0xF6BE2C, roughness: 0.55, sheen: 0.35, sheenColor: 0xffffff }));
14
+ body.rotation.z = Math.PI / 2;
15
+ const pivot = new THREE.Group(); // pivot at the object's base, so squash stays grounded
16
+ body.position.y = 34 * k;
17
+ pivot.add(body);
18
+ scene.add(pivot);
19
+
20
+ // contact shadow: radial-gradient blob on the ground
21
+ const c = document.createElement('canvas'); c.width = c.height = 128;
22
+ const g = c.getContext('2d'), gr = g.createRadialGradient(64, 64, 0, 64, 64, 64);
23
+ gr.addColorStop(0, 'rgba(255,255,255,1)'); gr.addColorStop(0.45, 'rgba(255,255,255,0.4)'); gr.addColorStop(1, 'rgba(255,255,255,0)');
24
+ g.fillStyle = gr; g.fillRect(0, 0, 128, 128);
25
+ const shadow = new THREE.Mesh(new THREE.PlaneGeometry(1, 1),
26
+ new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(c), color: 0x2a1e14, transparent: true, depthWrite: false, opacity: 0 }));
27
+ shadow.renderOrder = -1;
28
+ scene.add(shadow);
29
+
30
+ const GROUND = toWorld(201, 470), T0 = 80, FALL = 0.32, DROP = 260 * k;
31
+ S.track((t) => {
32
+ const s = (t - T0) / 1000;
33
+ if (s < 0) { pivot.visible = shadow.visible = false; return; }
34
+ pivot.visible = shadow.visible = true;
35
+ let y = 0, sy = 1;
36
+ if (REDUCED) { y = 0; }
37
+ else if (s < FALL) { const u = s / FALL; y = DROP * (1 - u * u); sy = 1 + 0.18 * u; } // gravity, stretched by speed
38
+ else { const tl = s - FALL; y = spring(0, 260 * k, 0.5, 0.32, tl); sy = 1 + spring(-0.22, -8, 0.42, 0.3, tl); } // squash + hop
39
+ const sx = 1 / Math.sqrt(Math.max(sy, 0.3)); // keep the clay's volume
40
+ pivot.position.set(GROUND.x, GROUND.y + Math.max(0, y), 0);
41
+ pivot.scale.set(sx, sy, sx);
42
+ const h = Math.max(0, y) / (100 * k);
43
+ shadow.position.set(GROUND.x, GROUND.y - 2 * k, -10);
44
+ shadow.scale.set(150 * k * sx * (1 + 0.4 * h), 34 * k * (1 + 0.3 * h), 1);
45
+ shadow.material.opacity = 0.3 * Math.exp(-1.2 * h) * Math.min(1, s / 0.1);
46
+ });
47
+
48
+ // native UI comes in after the hero has landed (staging gated on arrival)
49
+ S.cue(T0 + FALL * 1000 + 450, 'headline');
50
+ S.cue(T0 + FALL * 1000 + 600, 'button');
51
+
52
+ // Textures for real parts: const tex = S.loadTexture('my-art.png'); (on the GPU before the clock starts)
53
+ })();
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
6
+ <style>
7
+ html, body { margin: 0; height: 100%; overflow: hidden; background: transparent; }
8
+ canvas { display: block; }
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <script>
13
+ // ?debug shows script errors on screen (look-dev in the simulator's Safari inspector also works)
14
+ if (location.search.indexOf('debug') !== -1) {
15
+ window.onerror = function (m, s, l, c, e) {
16
+ var p = document.createElement('pre');
17
+ p.style.cssText = 'position:fixed;top:60px;left:8px;right:8px;font:11px monospace;color:#c00;white-space:pre-wrap;z-index:9';
18
+ p.textContent = m + ' @' + l + ':' + c + '\n' + (e && e.stack || '');
19
+ document.body.appendChild(p);
20
+ };
21
+ }
22
+ </script>
23
+ <script src="three.min.js"></script>
24
+ <!-- engine first, then one script per animated part -->
25
+ <script src="scene.js"></script>
26
+ <script src="content.js"></script>
27
+ </body>
28
+ </html>