$npx -y skills add chongdashu/threejs-capacitor-ios-game --skill threejs-capacitor-iosBuild and ship Three.js apps on Capacitor iOS with Vite and Swift Package Manager: GLTF loading, assets_index animation UI, OrbitControls mouse/touch mappings, and iOS sync/run troubleshooting.
| 1 | # Three.js Capacitor iOS |
| 2 | |
| 3 | Build interactive Three.js apps that run in browser and ship in an iOS native shell via Capacitor. |
| 4 | This skill focuses on the integration boundary where most breakage happens: web build output, animation contracts, controls, and native sync/run workflow. |
| 5 | |
| 6 | ## Philosophy: Two Runtimes, One Contract |
| 7 | |
| 8 | Treat the project as two systems that must agree: |
| 9 | - A web renderer runtime (Three.js + Vite) |
| 10 | - A native runtime wrapper (Capacitor iOS) |
| 11 | |
| 12 | Most failures happen when their contract is implicit. |
| 13 | Make file paths, animation names, build output, and iOS package manager choices explicit and testable. |
| 14 | |
| 15 | **Before implementing, ask:** |
| 16 | - What is the exact web output directory (`dist` or `www`) and does Capacitor `webDir` match it? |
| 17 | - Are animation names loaded from data (`assets_index.json`) instead of hardcoded strings? |
| 18 | - Is iOS using SPM or CocoaPods, and are plugin dependencies compatible with that choice? |
| 19 | - Are desktop and touch controls intentionally mapped, or left to defaults that may not match product UX? |
| 20 | |
| 21 | **Core principles:** |
| 22 | 1. Contract-first data flow: UI and animation playback should derive from JSON metadata, not ad-hoc clip names in code. |
| 23 | 2. SPM-first iOS setup: on modern Capacitor, default to Swift Package Manager unless a specific plugin forces CocoaPods. |
| 24 | 3. Symmetric controls: define mouse and touch mappings together so desktop and mobile behavior stay aligned. |
| 25 | 4. Build-sync discipline: every native run depends on fresh web assets and sync. |
| 26 | 5. Fast diagnosis: prefer small runtime checks for paths, clip names, and action resolution before deep debugging. |
| 27 | |
| 28 | ## Quick Start Workflow |
| 29 | |
| 30 | 1. Build the Three.js app with Vite (`npm run build`). |
| 31 | 2. Keep static assets under `public/` and load via absolute URLs (`/assets/...`). |
| 32 | 3. Configure Capacitor with `webDir: "dist"`. |
| 33 | 4. Add iOS platform with SPM (`npx cap add ios --packagemanager SPM`). |
| 34 | 5. Repeat day-to-day loop: |
| 35 | - `npm run build` |
| 36 | - `npx cap sync ios` |
| 37 | - `npx cap run ios` or `npx cap open ios` |
| 38 | |
| 39 | For command-level details, see `references/capacitor-ios-spm-workflow.md`. |
| 40 | |
| 41 | ## Implementation Guidelines |
| 42 | |
| 43 | ### 1) Project Shape |
| 44 | |
| 45 | Prefer this shape for minimal ambiguity: |
| 46 | - `index.html` and `src/*` for app code |
| 47 | - `public/assets/...` for GLBs and JSON contracts |
| 48 | - `capacitor.config.ts` with `webDir: "dist"` |
| 49 | |
| 50 | If using Vite, keep all runtime fetches compatible with both browser and WKWebView: |
| 51 | - Good: `fetch('/assets/assets_index.json')` |
| 52 | - Avoid: filesystem paths or environment-specific base URLs unless intentionally configured. |
| 53 | |
| 54 | ### 2) Animation Contract via `assets_index.json` |
| 55 | |
| 56 | Use one source of truth: |
| 57 | - Character skeleton URL |
| 58 | - Animation source URL |
| 59 | - `animations[]` entries with: |
| 60 | - stable app id (`idle`, `walk`, `run`) |
| 61 | - `sourceClipName` (exact `AnimationClip.name`) |
| 62 | - loop mode and defaults |
| 63 | |
| 64 | Runtime pattern: |
| 65 | 1. Load index JSON |
| 66 | 2. Load skeleton GLB and animation GLB |
| 67 | 3. Resolve each UI button to a clip by `sourceClipName` |
| 68 | 4. Build `AnimationAction` map keyed by app id |
| 69 | 5. Play default action from index |
| 70 | |
| 71 | See `references/threejs-animation-index-pattern.md`. |
| 72 | |
| 73 | ### 3) Controls: Desktop and Touch |
| 74 | |
| 75 | Use `OrbitControls` and set mappings explicitly: |
| 76 | - Mouse: |
| 77 | - left = rotate |
| 78 | - wheel = dolly/zoom |
| 79 | - right = pan |
| 80 | - Touch: |
| 81 | - one-finger = rotate |
| 82 | - two-finger = dolly + pan |
| 83 | |
| 84 | If product requires vertical-only pan, constrain target/camera translation after `controls.update()` each frame. |
| 85 | Do not silently change rotate/zoom semantics when adding this constraint. |
| 86 | |
| 87 | ### 4) Performance and Stability Guardrails |
| 88 | |
| 89 | - Cap pixel ratio: `Math.min(devicePixelRatio, 2)`. |
| 90 | - Reuse mixer/actions; do not recreate per click. |
| 91 | - On resize, always update camera aspect, projection, and renderer size. |
| 92 | - Keep animation switching with fade transitions from metadata defaults. |
| 93 | |
| 94 | ### 5) Capacitor iOS Integration |
| 95 | |
| 96 | Use SPM by default with Capacitor 8+. |
| 97 | For existing CocoaPods projects, migrate intentionally (assistant or recreate iOS platform). |
| 98 | |
| 99 | After native-side changes or plugin changes, run `npx cap sync ios` again. |
| 100 | |
| 101 | ## Anti-Patterns to Avoid |
| 102 | |
| 103 | ❌ **Hardcoding clip names in UI handlers** |
| 104 | Why bad: a renamed clip in GLB silently breaks buttons. |
| 105 | Better: map buttons from `assets_index.json` and resolve clip names once at startup. |
| 106 | |
| 107 | ❌ **Mixing SPM and CocoaPods assumptions** |
| 108 | Why bad: dependency drift and broken Xcode project expectations. |
| 109 | Better: choose one package manager per project; for modern setups prefer SPM. |
| 110 | |
| 111 | ❌ **Running iOS without rebuilding web assets** |
| 112 | Why bad: simulator shows stale JS/CSS and debugging becomes misleading. |
| 113 | Better: use scripts that always build before `cap sync`/`cap run`. |
| 114 | |
| 115 | ❌ **Leaving control mappings implicit** |
| 116 | Why bad: desktop and mobile interaction diverge from |