ui-ux-consultant-cli 1.0.0-beta.1

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/assets/ui-ux-consultant/SKILL.md +844 -0
  2. package/assets/ui-ux-consultant/references/accessibility.md +175 -0
  3. package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
  4. package/assets/ui-ux-consultant/references/animations.md +448 -0
  5. package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
  6. package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
  7. package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
  8. package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
  9. package/assets/ui-ux-consultant/references/components.md +1116 -0
  10. package/assets/ui-ux-consultant/references/patterns.md +600 -0
  11. package/assets/ui-ux-consultant/references/performance.md +198 -0
  12. package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
  13. package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
  14. package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
  15. package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
  16. package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
  17. package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
  18. package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
  19. package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
  20. package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
  21. package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
  22. package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
  23. package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
  24. package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
  25. package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
  26. package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
  27. package/assets/ui-ux-consultant/references/theming.md +701 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +130 -0
  30. package/package.json +51 -0
@@ -0,0 +1,336 @@
1
+ # SwiftUI Reference
2
+
3
+ ## When to Read
4
+ Read this file when building SwiftUI apps (iOS, macOS, watchOS, tvOS) — views, state, navigation, lists, forms, async data, animation, or accessibility.
5
+
6
+ ---
7
+
8
+ ## Recommended Libraries
9
+
10
+ | Library | Purpose |
11
+ |---|---|
12
+ | Alamofire | HTTP networking |
13
+ | Kingfisher | Async image loading/caching |
14
+ | SwiftData (iOS 17+) | Local persistence (replaces CoreData) |
15
+ | The Composable Architecture (TCA) | Strict unidirectional state |
16
+ | Nuke | High-performance image pipeline |
17
+ | swift-collections | Ordered/deque collections |
18
+
19
+ ---
20
+
21
+ ## Style Recommendations
22
+
23
+ - Follow Apple HIG spacing: 8, 16, 20, 24pt increments
24
+ - Use SF Symbols everywhere — native, scales with Dynamic Type, supports multicolor
25
+ - Prefer system colors (`Color.primary`, `.secondary`, `.accentColor`) for automatic dark mode
26
+ - `.font(.headline)` / `.font(.body)` — not hardcoded point sizes
27
+ - `cornerRadius(12)` on cards; `cornerRadius(10)` on buttons — matches native iOS feel
28
+ - Avoid custom navigation bars when Apple's is sufficient — saves maintenance
29
+
30
+ ---
31
+
32
+ ## State Property Wrappers
33
+
34
+ | Wrapper | Use |
35
+ |---|---|
36
+ | `@State` | Local mutable state (owned by this view) |
37
+ | `@Binding` | Passed-down mutable reference |
38
+ | `@StateObject` | Owned ObservableObject (created once, survives body recompute) |
39
+ | `@ObservedObject` | Injected ObservableObject |
40
+ | `@EnvironmentObject` | App-wide injected object |
41
+ | `@Environment` | System values (colorScheme, locale, sizeClass) |
42
+ | `@Observable` (iOS 17+) | Modern replacement for ObservableObject |
43
+
44
+ ---
45
+
46
+ ## Top UX Patterns (with Code)
47
+
48
+ ### Modern ViewModel with @Observable (iOS 17+)
49
+ ```swift
50
+ @Observable class UserStore {
51
+ var users: [User] = []
52
+ var isLoading = false
53
+ var errorMessage: String?
54
+
55
+ func load() async {
56
+ isLoading = true
57
+ defer { isLoading = false }
58
+ do {
59
+ users = try await api.getUsers()
60
+ } catch {
61
+ errorMessage = error.localizedDescription
62
+ }
63
+ }
64
+ }
65
+
66
+ struct UserListView: View {
67
+ @State private var store = UserStore()
68
+
69
+ var body: some View {
70
+ Group {
71
+ if store.isLoading {
72
+ ProgressView("Loading users…")
73
+ } else if let error = store.errorMessage {
74
+ ContentUnavailableView(error, systemImage: "exclamationmark.triangle")
75
+ } else {
76
+ List(store.users) { user in
77
+ UserRow(user: user)
78
+ }
79
+ }
80
+ }
81
+ .task { await store.load() }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### NavigationStack with typed destinations (iOS 16+)
87
+ ```swift
88
+ @State private var path = NavigationPath()
89
+
90
+ NavigationStack(path: $path) {
91
+ List(items) { item in
92
+ NavigationLink(value: item) {
93
+ ItemRow(item: item)
94
+ }
95
+ }
96
+ .navigationTitle("Items")
97
+ .navigationDestination(for: Item.self) { item in
98
+ ItemDetailView(item: item)
99
+ }
100
+ .navigationDestination(for: UserProfile.self) { profile in
101
+ ProfileView(profile: profile)
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Async data loading with .task
107
+ ```swift
108
+ struct ContentView: View {
109
+ @State private var posts: [Post] = []
110
+
111
+ var body: some View {
112
+ List(posts) { post in PostRow(post: post) }
113
+ .task {
114
+ // auto-cancels when view disappears
115
+ posts = (try? await api.fetchPosts()) ?? []
116
+ }
117
+ }
118
+ }
119
+ // .task is always preferred over .onAppear + Task { } — cancellation is automatic
120
+ ```
121
+
122
+ ### Swipe actions on List rows
123
+ ```swift
124
+ List {
125
+ ForEach(items) { item in
126
+ ItemRow(item: item)
127
+ .swipeActions(edge: .trailing, allowsFullSwipe: true) {
128
+ Button(role: .destructive) {
129
+ delete(item)
130
+ } label: {
131
+ Label("Delete", systemImage: "trash")
132
+ }
133
+ }
134
+ .swipeActions(edge: .leading) {
135
+ Button {
136
+ archive(item)
137
+ } label: {
138
+ Label("Archive", systemImage: "archivebox")
139
+ }
140
+ .tint(.blue)
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Form with validation
147
+ ```swift
148
+ struct EditProfileForm: View {
149
+ @State private var name = ""
150
+ @State private var email = ""
151
+ @State private var password = ""
152
+
153
+ private var isValid: Bool {
154
+ !name.isEmpty && email.contains("@") && password.count >= 8
155
+ }
156
+
157
+ var body: some View {
158
+ Form {
159
+ Section("Account") {
160
+ TextField("Name", text: $name)
161
+ TextField("Email", text: $email)
162
+ .textContentType(.emailAddress)
163
+ .keyboardType(.emailAddress)
164
+ .autocorrectionDisabled()
165
+ SecureField("Password", text: $password)
166
+ .textContentType(.newPassword)
167
+ }
168
+
169
+ Section {
170
+ Button("Save") { save() }
171
+ .disabled(!isValid)
172
+ }
173
+ }
174
+ .navigationTitle("Edit Profile")
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### Animation respecting reduced motion
180
+ ```swift
181
+ @Environment(\.accessibilityReduceMotion) var reduceMotion
182
+
183
+ Button("Toggle") {
184
+ withAnimation(reduceMotion ? .none : .spring(duration: 0.3)) {
185
+ isExpanded.toggle()
186
+ }
187
+ }
188
+
189
+ // Conditional animation modifier
190
+ .animation(reduceMotion ? .none : .easeInOut, value: isExpanded)
191
+ ```
192
+
193
+ ### Accessibility labels and hints
194
+ ```swift
195
+ Button(action: delete) {
196
+ Image(systemName: "trash")
197
+ }
198
+ .accessibilityLabel("Delete item")
199
+ .accessibilityHint("Double tap to permanently delete this item")
200
+
201
+ // Group related elements
202
+ HStack {
203
+ Text(user.name)
204
+ Text(user.email).foregroundStyle(.secondary)
205
+ }
206
+ .accessibilityElement(children: .combine)
207
+ ```
208
+
209
+ ### LazyVStack inside ScrollView (for custom layouts)
210
+ ```swift
211
+ ScrollView {
212
+ LazyVStack(spacing: 12) {
213
+ ForEach(items) { item in
214
+ ItemCard(item: item)
215
+ }
216
+ }
217
+ .padding(.horizontal, 16)
218
+ }
219
+ // Use List for interactive rows; LazyVStack for custom card layouts
220
+ ```
221
+
222
+ ### Conditional view without branching in body
223
+ ```swift
224
+ // Prefer computed properties over inline if/else trees
225
+ private var contentView: some View {
226
+ if items.isEmpty {
227
+ return AnyView(EmptyStateView())
228
+ }
229
+ return AnyView(ItemGrid(items: items))
230
+ }
231
+
232
+ // Even better — use @ViewBuilder
233
+ @ViewBuilder
234
+ private var contentView: some View {
235
+ if items.isEmpty {
236
+ EmptyStateView()
237
+ } else {
238
+ ItemGrid(items: items)
239
+ }
240
+ }
241
+ ```
242
+
243
+ ### Sheet / fullScreenCover
244
+ ```swift
245
+ @State private var showingDetail = false
246
+ @State private var selectedItem: Item?
247
+
248
+ .sheet(item: $selectedItem) { item in
249
+ ItemDetailView(item: item)
250
+ .presentationDetents([.medium, .large])
251
+ .presentationDragIndicator(.visible)
252
+ }
253
+ ```
254
+
255
+ ---
256
+
257
+ ## Best Practices by Category
258
+
259
+ ### Views
260
+ - Keep `body` under ~40 lines — extract sub-views or use `@ViewBuilder` helpers
261
+ - Prefer computed properties for conditional logic over inline ternaries in body
262
+ - Use `Group { }` to apply modifiers to multiple views without adding layout container
263
+ - `ViewModifier` for reusable style bundles (card style, section header style)
264
+
265
+ ### State
266
+ - `@State` is private — never pass it across views directly; use `@Binding` for child writes
267
+ - `@StateObject` owns the object — use when the view creates the ViewModel
268
+ - `@ObservedObject` does NOT own — use for injected ViewModels (won't survive view recreation)
269
+ - `@Observable` (iOS 17+) is simpler: no `@Published` needed, just mark class `@Observable`
270
+ - Avoid storing the same state in multiple places — single source of truth
271
+
272
+ ### Navigation
273
+ - `NavigationStack` replaces `NavigationView` (iOS 16+)
274
+ - Use typed `NavigationPath` for programmatic deep linking
275
+ - `.navigationDestination` decouples routing from row UI
276
+ - For tab apps: `TabView` at root, `NavigationStack` inside each tab
277
+
278
+ ### Lists & Performance
279
+ - `List` for interactive rows — cell reuse built in
280
+ - `LazyVStack` in `ScrollView` for custom card UIs
281
+ - `ForEach` requires `Identifiable` items or explicit `id:` parameter
282
+ - Avoid heavy computation in row `body` — cache in ViewModel
283
+
284
+ ### Async
285
+ - `.task { }` for view-lifecycle async work — cancels automatically on disappear
286
+ - `.task(id:)` re-runs when the id value changes (replaces `.onChange` + `Task`)
287
+ - `async let` for parallel fetches within a single task
288
+
289
+ ### Forms
290
+ - `textContentType` on every text field — enables autofill
291
+ - `keyboardType` matches expected input (`.emailAddress`, `.numberPad`, `.URL`)
292
+ - `.submitLabel(.next)` and `FocusState` for keyboard tab order
293
+ - Disable submit button (`disabled(!isValid)`) — never rely only on server validation
294
+
295
+ ### Animation
296
+ - `withAnimation` for state-driven animations
297
+ - `.matchedGeometryEffect` for hero-style transitions between views
298
+ - Always check `accessibilityReduceMotion` before animating
299
+ - `.transition(.scale.combined(with: .opacity))` for enter/exit
300
+
301
+ ### Accessibility
302
+ - Every icon-only button needs `.accessibilityLabel`
303
+ - `.accessibilityHint` for non-obvious actions
304
+ - `.accessibilityElement(children: .combine)` for grouped content
305
+ - Test with VoiceOver on device — simulator is insufficient
306
+ - Support Dynamic Type — never override `.font` with fixed sizes
307
+
308
+ ---
309
+
310
+ ## Common Anti-Patterns
311
+
312
+ 1. `@StateObject` for injected objects — use `@ObservedObject`; `@StateObject` creates/owns the instance and will recreate it
313
+ 2. `@ObservableObject` + `@Published` for new iOS 17+ code — use `@Observable` macro (cleaner, faster)
314
+ 3. `.onAppear + Task { }` — use `.task { }` instead (auto-cancels on disappear, preventing data races)
315
+ 4. Force-unwrapping optionals in views — use `if let` or `guard let`; crashes are not recoverable in SwiftUI previews
316
+ 5. Complex logic in `body` — extract to computed properties or ViewModels; body re-evaluates frequently
317
+ 6. Missing `accessibilityLabel` on icon buttons — VoiceOver reads "button" with no context
318
+ 7. `NavigationView` in new code — deprecated; use `NavigationStack`
319
+ 8. Storing `@EnvironmentObject` locally with `@State` — breaks injection; access via `@EnvironmentObject` directly
320
+ 9. Deeply nested closures in body — extract to `@ViewBuilder` functions
321
+ 10. Ignoring `task` cancellation — long tasks should check `Task.isCancelled`
322
+
323
+ ---
324
+
325
+ ## Performance Checklist
326
+
327
+ - [ ] `.task` not `.onAppear + Task { }` for async loading
328
+ - [ ] `@Observable` (iOS 17+) for ViewModels — finer-grained updates than `@ObservableObject`
329
+ - [ ] `LazyVStack`/`LazyHStack` inside `ScrollView` for long custom content
330
+ - [ ] `List` for interactive rows (reuses cells natively), `ScrollView + LazyVStack` for custom cards
331
+ - [ ] `accessibilityReduceMotion` check before any auto-animation
332
+ - [ ] `Equatable` conformance on views with complex bodies to skip unnecessary redraws
333
+ - [ ] `.id(item.id)` on list items when reordering is possible
334
+ - [ ] Profile in Instruments (SwiftUI template) — not just in simulator
335
+ - [ ] `nonisolated` on pure functions to avoid main-actor overhead
336
+ - [ ] `@MainActor` on ViewModels that update UI — prevents threading bugs
@@ -0,0 +1,366 @@
1
+ # Three.js / React Three Fiber UI/UX Guidelines
2
+
3
+ ## When to read this
4
+ Use when building WebGL scenes, 3D product viewers, interactive data visualization, creative canvases, or scroll-driven 3D experiences with Three.js or React Three Fiber (R3F).
5
+
6
+ ---
7
+
8
+ ## Recommended Libraries
9
+
10
+ | Library | Purpose | Install |
11
+ |---|---|---|
12
+ | three | Core WebGL renderer | `npm install three` |
13
+ | @react-three/fiber | React renderer for Three.js | `npm install @react-three/fiber` |
14
+ | @react-three/drei | R3F helpers (OrbitControls, Environment, Text…) | `npm install @react-three/drei` |
15
+ | @react-three/postprocessing | Post-processing (bloom, SSR, depth of field) | `npm install @react-three/postprocessing` |
16
+ | GSAP | Animation timelines + ScrollTrigger | `npm install gsap` |
17
+ | Leva | Debug panel for scene parameters | `npm install leva` |
18
+ | @types/three | TypeScript types | `npm install -D @types/three` |
19
+
20
+ ---
21
+
22
+ ## Style Recommendations by Use Case
23
+
24
+ | Use Case | Visual Style | Camera | Lighting |
25
+ |---|---|---|---|
26
+ | Product showcase | Dark bg, clean geometry, rim lighting | Orbit / turntable | 3-point: key + fill + rim |
27
+ | Interactive art / portfolio | Aurora/Neon, full-screen canvas | Free orbit | HDR environment map |
28
+ | Scroll-driven narrative | Cinematic, fog, depth | GSAP ScrollTrigger path | Directional + ambient |
29
+ | Data visualization | Minimal, muted palette, labeled axes | Orthographic or fixed perspective | Flat ambient only |
30
+ | Game / immersive | Full-screen, minimal HUD | First-person or follow cam | Dynamic point lights |
31
+ | SaaS hero / landing | Subtle 3D accent, no full-screen | Fixed, no user control | Soft ambient + directional |
32
+
33
+ ---
34
+
35
+ ## Two Approaches
36
+
37
+ ### Vanilla Three.js (imperative)
38
+ Best for: standalone canvas, full control, no React.
39
+
40
+ ### React Three Fiber (declarative)
41
+ Best for: integrating 3D into a React/Next.js app, sharing state with UI, composable scene graphs.
42
+
43
+ ---
44
+
45
+ ## Top UX Patterns
46
+
47
+ ### 1. Core Renderer Setup (Vanilla)
48
+
49
+ ```javascript
50
+ import * as THREE from 'three';
51
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
52
+
53
+ const scene = new THREE.Scene();
54
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
55
+ camera.position.set(0, 1, 5);
56
+
57
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
58
+ renderer.setSize(window.innerWidth, window.innerHeight);
59
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // NEVER skip the cap
60
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
61
+ renderer.toneMappingExposure = 1.0;
62
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
63
+ document.body.appendChild(renderer.domElement);
64
+
65
+ // Accessibility
66
+ renderer.domElement.setAttribute('role', 'img');
67
+ renderer.domElement.setAttribute('aria-label', 'Interactive 3D scene. Drag to rotate, scroll to zoom.');
68
+
69
+ const controls = new OrbitControls(camera, renderer.domElement);
70
+ controls.enableDamping = true;
71
+
72
+ const clock = new THREE.Clock();
73
+ renderer.setAnimationLoop(() => {
74
+ const dt = clock.getDelta(); // call ONCE per frame — never call getDelta() again this frame
75
+ controls.update();
76
+ renderer.render(scene, camera);
77
+ });
78
+ ```
79
+
80
+ ### 2. Core Setup (React Three Fiber)
81
+
82
+ ```tsx
83
+ import { Canvas, useFrame, useThree } from '@react-three/fiber';
84
+ import { OrbitControls, Environment, useGLTF } from '@react-three/drei';
85
+ import { useRef } from 'react';
86
+ import * as THREE from 'three';
87
+
88
+ function RotatingBox() {
89
+ const ref = useRef<THREE.Mesh>(null);
90
+ useFrame((_, delta) => {
91
+ if (ref.current) ref.current.rotation.y += delta * 0.5; // delta = framerate-independent
92
+ });
93
+ return (
94
+ <mesh ref={ref} castShadow>
95
+ <boxGeometry args={[1, 1, 1]} />
96
+ <meshStandardMaterial color="#2563EB" roughness={0.3} metalness={0.1} />
97
+ </mesh>
98
+ );
99
+ }
100
+
101
+ export default function Scene() {
102
+ return (
103
+ <Canvas
104
+ shadows
105
+ camera={{ position: [0, 1, 5], fov: 75 }}
106
+ gl={{ antialias: true, toneMapping: THREE.ACESFilmicToneMapping }}
107
+ aria-label="Interactive 3D scene"
108
+ >
109
+ <ambientLight intensity={0.4} />
110
+ <directionalLight position={[5, 10, 5]} intensity={1} castShadow />
111
+ <RotatingBox />
112
+ <OrbitControls enableDamping />
113
+ <Environment preset="sunset" />
114
+ </Canvas>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ### 3. Delta-Time Animation (framerate-independent)
120
+
121
+ ```javascript
122
+ // BAD — speed varies with frame rate (2× faster on 120Hz vs 60Hz)
123
+ mesh.rotation.y += 0.01;
124
+
125
+ // GOOD — consistent speed regardless of frame rate
126
+ const dt = clock.getDelta(); // called once at top of animate()
127
+ mesh.rotation.y += dt * 0.8;
128
+
129
+ // Lerp for smooth follow/easing
130
+ camera.position.x += (targetX - camera.position.x) * 0.05;
131
+ ```
132
+
133
+ ### 4. Responsive Canvas
134
+
135
+ ```javascript
136
+ // ResizeObserver for container-aware sizing (not window.resize)
137
+ const ro = new ResizeObserver(entries => {
138
+ const { width, height } = entries[0].contentRect;
139
+ renderer.setSize(width, height);
140
+ camera.aspect = width / height;
141
+ camera.updateProjectionMatrix();
142
+ });
143
+ ro.observe(canvas.parentElement); // observe container, not window
144
+
145
+ // Touch support for mobile
146
+ canvas.addEventListener('touchmove', e => {
147
+ e.preventDefault();
148
+ const t = e.touches[0];
149
+ mouse.x = (t.clientX / canvas.clientWidth) * 2 - 1;
150
+ mouse.y = -(t.clientY / canvas.clientHeight) * 2 + 1;
151
+ }, { passive: false });
152
+ ```
153
+
154
+ ### 5. Pause Render Loop When Tab Hidden
155
+
156
+ ```javascript
157
+ // Use setAnimationLoop as the driver — it can be paused
158
+ renderer.setAnimationLoop(animate);
159
+
160
+ document.addEventListener('visibilitychange', () => {
161
+ if (document.hidden) renderer.setAnimationLoop(null); // pause — saves battery
162
+ else renderer.setAnimationLoop(animate); // resume
163
+ });
164
+ ```
165
+
166
+ ### 6. InstancedMesh for 50+ Repeated Objects
167
+
168
+ ```javascript
169
+ const COUNT = 500;
170
+ const mesh = new THREE.InstancedMesh(geometry, material, COUNT);
171
+ const matrix = new THREE.Matrix4();
172
+
173
+ for (let i = 0; i < COUNT; i++) {
174
+ matrix.setPosition(
175
+ (Math.random() - 0.5) * 20,
176
+ (Math.random() - 0.5) * 20,
177
+ (Math.random() - 0.5) * 20
178
+ );
179
+ mesh.setMatrixAt(i, matrix);
180
+ }
181
+ mesh.instanceMatrix.needsUpdate = true;
182
+ scene.add(mesh);
183
+ // Result: 1 draw call instead of 500
184
+ ```
185
+
186
+ ### 7. GLTF Model Loading
187
+
188
+ ```javascript
189
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
190
+ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
191
+
192
+ const dracoLoader = new DRACOLoader();
193
+ dracoLoader.setDecoderPath('/draco/');
194
+
195
+ const loader = new GLTFLoader();
196
+ loader.setDRACOLoader(dracoLoader);
197
+
198
+ loader.load('model.glb', (gltf) => {
199
+ gltf.scene.traverse(child => {
200
+ if (child.isMesh) {
201
+ child.castShadow = true;
202
+ child.receiveShadow = true;
203
+ }
204
+ });
205
+ scene.add(gltf.scene);
206
+ }, undefined, (error) => {
207
+ console.error('Failed to load model:', error);
208
+ });
209
+
210
+ // R3F version:
211
+ function Model() {
212
+ const { scene } = useGLTF('/model.glb');
213
+ return <primitive object={scene} />;
214
+ }
215
+ useGLTF.preload('/model.glb');
216
+ ```
217
+
218
+ ### 8. Scroll-Driven Camera with GSAP ScrollTrigger
219
+
220
+ ```javascript
221
+ import gsap from 'gsap';
222
+ import { ScrollTrigger } from 'gsap/ScrollTrigger';
223
+
224
+ gsap.registerPlugin(ScrollTrigger); // must call before use
225
+
226
+ gsap.to(camera.position, {
227
+ x: 3,
228
+ y: 1,
229
+ z: 2,
230
+ ease: 'none',
231
+ scrollTrigger: {
232
+ trigger: '.canvas-wrapper',
233
+ start: 'top top',
234
+ end: 'bottom bottom',
235
+ scrub: 1, // 1-second lag for cinematic smoothness
236
+ },
237
+ });
238
+ ```
239
+
240
+ ### 9. Particle System
241
+
242
+ ```javascript
243
+ const COUNT = 3000; // safe mobile baseline — profile before raising
244
+ const geometry = new THREE.BufferGeometry();
245
+ const positions = new Float32Array(COUNT * 3);
246
+
247
+ for (let i = 0; i < COUNT * 3; i++) {
248
+ positions[i] = (Math.random() - 0.5) * 20;
249
+ }
250
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
251
+
252
+ const particles = new THREE.Points(
253
+ geometry,
254
+ new THREE.PointsMaterial({ size: 0.05, color: 0xffffff, sizeAttenuation: true })
255
+ );
256
+ scene.add(particles);
257
+
258
+ // Animating particles — must set needsUpdate
259
+ function animate() {
260
+ const pos = geometry.attributes.position.array;
261
+ for (let i = 1; i < pos.length; i += 3) {
262
+ pos[i] += Math.sin(clock.getElapsedTime() + i) * 0.001;
263
+ }
264
+ geometry.attributes.position.needsUpdate = true; // GPU re-upload — REQUIRED
265
+ }
266
+ ```
267
+
268
+ ### 10. Geometry and Material Disposal (Memory Management)
269
+
270
+ ```javascript
271
+ // Always dispose when removing objects
272
+ function removeObject(mesh) {
273
+ mesh.geometry.dispose();
274
+ if (Array.isArray(mesh.material)) {
275
+ mesh.material.forEach(m => m.dispose());
276
+ } else {
277
+ mesh.material.dispose();
278
+ }
279
+ if (mesh.material.map) mesh.material.map.dispose();
280
+ scene.remove(mesh);
281
+ }
282
+
283
+ // R3F: dispose in useEffect cleanup
284
+ useEffect(() => {
285
+ return () => {
286
+ geometry.dispose();
287
+ material.dispose();
288
+ };
289
+ }, []);
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Best Practices by Category
295
+
296
+ ### Scene Setup
297
+ - Always `setPixelRatio(Math.min(devicePixelRatio, 2))` — beyond 2 is invisible but doubles GPU load
298
+ - `antialias: true` must be set in constructor — cannot be changed after
299
+ - Use `ACESFilmicToneMapping` for perceptually accurate colors
300
+ - `SRGBColorSpace` for output — prevents washed-out colors
301
+
302
+ ### Animation
303
+ - `clock.getDelta()` exactly **once** per `animate()` frame — store in `dt`, reuse it
304
+ - All motion multiplied by `dt` — framerate-independent at 30fps, 60fps, 120fps
305
+ - Use `renderer.setAnimationLoop()` not recursive `requestAnimationFrame` — enables pause
306
+ - Lerp (`value += (target - value) * alpha`) for organic easing without libraries
307
+ - GSAP timelines for multi-step sequences; `scrub` for scroll-driven camera
308
+
309
+ ### Performance
310
+ - `InstancedMesh` for 50+ identical objects — 1 draw call vs N
311
+ - `LOD` (Level of Detail) for objects at varying distances
312
+ - `FogExp2` for atmospheric depth + implicitly culls far objects
313
+ - `BufferGeometry` + `Points` for particles — never individual `Mesh` objects
314
+ - Particle ceiling: 3,000 safe baseline; 50,000+ drops frames on mid-range mobile
315
+
316
+ ### Memory
317
+ - Dispose geometry, material, and textures when removing objects
318
+ - Never create new geometries/materials inside `animate()` — allocates each frame
319
+ - Reuse geometry and material instances across objects
320
+
321
+ ### Responsive
322
+ - `ResizeObserver` on container (not `window resize`) — fires on any container resize
323
+ - Use `canvas.clientWidth/Height` not `window.innerWidth/Height` for contained canvases
324
+ - Touch events alongside mouse events for mobile interactivity
325
+
326
+ ### Accessibility
327
+ - `role="img"` + descriptive `aria-label` on canvas — screen readers get context
328
+ - Gate all auto-animation on `prefers-reduced-motion` — track changes with `addEventListener`
329
+ - Provide keyboard alternative for any pointer-only interactions
330
+
331
+ ---
332
+
333
+ ## Common Anti-Patterns
334
+
335
+ | Anti-Pattern | Why It's Wrong | Fix |
336
+ |---|---|---|
337
+ | `devicePixelRatio` without cap | 3× GPU cost on retina displays, no visual gain | `Math.min(devicePixelRatio, 2)` |
338
+ | `getDelta()` called twice per frame | Second call always returns ~0 | Call once, store in `dt` |
339
+ | No `geometry.dispose()` | Memory leak — GPU VRAM never freed | Dispose on removal |
340
+ | `window.innerWidth` for contained canvas | Wrong dimensions in flex/grid layouts | `ResizeObserver` on container |
341
+ | 500 individual `Mesh` for particles | 500 draw calls per frame | `Points` + `BufferGeometry` |
342
+ | No `prefers-reduced-motion` check | Causes vestibular disorders | Gate all auto-animation |
343
+ | GSAP without `registerPlugin` | TypeError: ScrollTrigger is not a constructor | `gsap.registerPlugin(ScrollTrigger)` |
344
+ | Inline `requestAnimationFrame` (self-referencing) | Cannot be paused | `renderer.setAnimationLoop()` |
345
+ | Fixed `+= 0.01` animation | 2× faster on 120Hz | Multiply by `clock.getDelta()` |
346
+ | Missing `needsUpdate = true` | Particle positions frozen on GPU | Set after mutating buffer array |
347
+
348
+ ---
349
+
350
+ ## Performance Checklist
351
+
352
+ - [ ] `setPixelRatio(Math.min(devicePixelRatio, 2))`
353
+ - [ ] `getDelta()` called once at top of `animate()`, reused as `dt`
354
+ - [ ] All animations multiplied by `dt` (framerate-independent)
355
+ - [ ] `renderer.setAnimationLoop()` as loop driver (pauseable)
356
+ - [ ] `visibilitychange` pauses loop on hidden tab
357
+ - [ ] `dispose()` geometry + material + textures on removal
358
+ - [ ] `InstancedMesh` for 50+ identical objects
359
+ - [ ] Particle count ≤ 3,000 (test on mobile before raising)
360
+ - [ ] `needsUpdate = true` after mutating `BufferAttribute` arrays
361
+ - [ ] `ResizeObserver` on container (not `window resize`)
362
+ - [ ] `prefers-reduced-motion` gating all auto-animation
363
+ - [ ] `role="img"` + `aria-label` on canvas element
364
+ - [ ] GSAP `registerPlugin(ScrollTrigger)` before any ScrollTrigger use
365
+ - [ ] `scrub: 1` (not `onEnter`) for scroll-driven camera paths
366
+ - [ ] Production: Vite + `npm install three` for tree-shaking