@struq/skill 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/bin/struq.mjs +132 -0
- package/package.json +41 -4
- package/skills/struq/SKILL.md +237 -0
- package/skills/struq/cinematic/README.md +409 -0
- package/skills/struq/cinematic/checklists/01-preproduction.md +15 -0
- package/skills/struq/cinematic/checklists/02-implementation.md +17 -0
- package/skills/struq/cinematic/checklists/03-polish.md +17 -0
- package/skills/struq/cinematic/examples/code/nextjs/README.md +13 -0
- package/skills/struq/cinematic/examples/code/nextjs/app/cinematic-experience.tsx +13 -0
- package/skills/struq/cinematic/examples/code/nextjs/app/components/LenisRoot.tsx +42 -0
- package/skills/struq/cinematic/examples/code/nextjs/app/page.tsx +6 -0
- package/skills/struq/cinematic/examples/code/shared/CameraRig.tsx +65 -0
- package/skills/struq/cinematic/examples/code/shared/ExperienceCanvas.tsx +20 -0
- package/skills/struq/cinematic/examples/code/shared/ExperienceDirector.tsx +57 -0
- package/skills/struq/cinematic/examples/code/shared/ExperienceOverlay.tsx +37 -0
- package/skills/struq/cinematic/examples/code/shared/ExperiencePage.tsx +52 -0
- package/skills/struq/cinematic/examples/code/shared/GsapFrameLoop.tsx +32 -0
- package/skills/struq/cinematic/examples/code/shared/InteriorRoot.tsx +30 -0
- package/skills/struq/cinematic/examples/code/shared/README.md +20 -0
- package/skills/struq/cinematic/examples/code/shared/SubjectRig.tsx +58 -0
- package/skills/struq/cinematic/examples/code/shared/build-experience-timeline.ts +237 -0
- package/skills/struq/cinematic/examples/code/shared/direction.ts +52 -0
- package/skills/struq/cinematic/examples/code/shared/gsap-client.ts +32 -0
- package/skills/struq/cinematic/examples/code/shared/math.ts +12 -0
- package/skills/struq/cinematic/examples/code/shared/registry.ts +58 -0
- package/skills/struq/cinematic/examples/code/shared/runtime.ts +52 -0
- package/skills/struq/cinematic/examples/code/shared/styles.css +127 -0
- package/skills/struq/cinematic/examples/code/shared/useExperienceInput.ts +42 -0
- package/skills/struq/cinematic/examples/code/vite/README.md +14 -0
- package/skills/struq/cinematic/examples/code/vite/src/App.tsx +11 -0
- package/skills/struq/cinematic/examples/code/vite/src/LenisRoot.tsx +37 -0
- package/skills/struq/cinematic/examples/code/vite/src/main.tsx +9 -0
- package/skills/struq/cinematic/examples/concise/01-chapter-spec-example.md +42 -0
- package/skills/struq/cinematic/examples/concise/02-handoff-example.md +25 -0
- package/skills/struq/cinematic/examples/concise/03-audit-finding-example.md +13 -0
- package/skills/struq/cinematic/examples/verbose/01-full-experience-direction.md +687 -0
- package/skills/struq/cinematic/examples/verbose/02-full-audit-example.md +362 -0
- package/skills/struq/cinematic/references/01-core-mental-model.md +221 -0
- package/skills/struq/cinematic/references/02-vocabulary.md +384 -0
- package/skills/struq/cinematic/references/03-experience-architecture.md +302 -0
- package/skills/struq/cinematic/references/04-timeline-choreography.md +264 -0
- package/skills/struq/cinematic/references/05-handoff-contracts.md +241 -0
- package/skills/struq/cinematic/references/06-three-rig-architecture.md +286 -0
- package/skills/struq/cinematic/references/07-stack-integration.md +228 -0
- package/skills/struq/cinematic/references/08-gsap-plugin-patterns.md +218 -0
- package/skills/struq/cinematic/references/09-tuning-harness.md +202 -0
- package/skills/struq/cinematic/references/10-quality-systems.md +206 -0
- package/skills/struq/cinematic/references/11-agent-workflows.md +246 -0
- package/skills/struq/cinematic/references/12-audit-rubric.md +176 -0
- package/skills/struq/cinematic/references/13-official-docs.md +53 -0
- package/skills/struq/cinematic/templates/01-experience-brief.md +105 -0
- package/skills/struq/cinematic/templates/02-experience-contract.md +80 -0
- package/skills/struq/cinematic/templates/03-chapter-spec.md +103 -0
- package/skills/struq/cinematic/templates/04-handoff-contract.md +102 -0
- package/skills/struq/cinematic/templates/05-ownership-matrix.md +29 -0
- package/skills/struq/cinematic/templates/06-shot-presets.md +59 -0
- package/skills/struq/cinematic/templates/07-implementation-slice.md +66 -0
- package/skills/struq/cinematic/templates/08-tuning-register.md +36 -0
- package/skills/struq/cinematic/templates/09-acceptance-tests.md +57 -0
- package/skills/struq/references/anti-patterns.md +265 -0
- package/skills/struq/references/baseline-prompt.md +118 -0
- package/skills/struq/references/brand.md +143 -0
- package/skills/struq/references/brief-router.md +219 -0
- package/skills/struq/references/composition.md +191 -0
- package/skills/struq/references/depth-parallax.md +235 -0
- package/skills/struq/references/direction.md +188 -0
- package/skills/struq/references/feel-layer.md +107 -0
- package/skills/struq/references/gsap-api.md +293 -0
- package/skills/struq/references/iconography.md +255 -0
- package/skills/struq/references/imagery.md +144 -0
- package/skills/struq/references/loaders.md +287 -0
- package/skills/struq/references/micro-interactions.md +270 -0
- package/skills/struq/references/mobile-composition.md +70 -0
- package/skills/struq/references/nav-header.md +296 -0
- package/skills/struq/references/page-architecture.md +134 -0
- package/skills/struq/references/page-entrances.md +198 -0
- package/skills/struq/references/performance.md +235 -0
- package/skills/struq/references/pinning-scrub.md +341 -0
- package/skills/struq/references/pitfalls.md +290 -0
- package/skills/struq/references/route-transitions.md +281 -0
- package/skills/struq/references/scaling.md +151 -0
- package/skills/struq/references/scroll-reveals.md +280 -0
- package/skills/struq/references/seo.md +219 -0
- package/skills/struq/references/setup.md +397 -0
- package/skills/struq/references/showcase.md +87 -0
- package/skills/struq/references/stack-targets.md +78 -0
- package/skills/struq/references/struq-imagery.md +107 -0
- package/skills/struq/references/svg-motion.md +390 -0
- package/skills/struq/references/transitions.md +160 -0
- package/skills/struq/references/triage.md +71 -0
- package/skills/struq/references/typography.md +720 -0
- package/skills/struq/references/vibe-presets.md +287 -0
- package/skills/struq/references/webgl-r3f.md +274 -0
- package/skills/struq/references/webgl-scenes.md +593 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Three.js and React Three Fiber Rig Architecture
|
|
2
|
+
|
|
3
|
+
## Why rigs matter
|
|
4
|
+
|
|
5
|
+
Rigs turn transform conflicts into explicit composition. A rig is simply a parent `Object3D` or `Group` created to own one category of motion.
|
|
6
|
+
|
|
7
|
+
Without rigs:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
GSAP writes camera.position
|
|
11
|
+
pointer handler writes camera.position
|
|
12
|
+
noise loop writes camera.position
|
|
13
|
+
route transition writes camera.position
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
With rigs:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
cameraPlacementRig ← route or viewport placement
|
|
20
|
+
└── cameraNarrativeRig ← chapter and handoff timelines
|
|
21
|
+
└── cameraReactionRig ← pointer and velocity modulation
|
|
22
|
+
└── cameraShakeRig ← procedural noise
|
|
23
|
+
└── camera
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The final transform is composed by the scene graph.
|
|
27
|
+
|
|
28
|
+
## Suggested camera hierarchy
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<group ref={placementRig}>
|
|
32
|
+
<group ref={narrativeRig}>
|
|
33
|
+
<group ref={reactionRig}>
|
|
34
|
+
<group ref={shakeRig}>
|
|
35
|
+
<PerspectiveCamera ref={camera} makeDefault />
|
|
36
|
+
</group>
|
|
37
|
+
</group>
|
|
38
|
+
</group>
|
|
39
|
+
<object3D ref={targetRig} />
|
|
40
|
+
</group>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The target may have its own narrative and reactive hierarchy if needed.
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
targetNarrativeRig
|
|
47
|
+
└── targetReactionRig
|
|
48
|
+
└── targetAnchor
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
During each frame, orient the physical camera toward the resolved target anchor. If roll is used, apply it after look-at orientation through a roll rig or quaternion composition.
|
|
52
|
+
|
|
53
|
+
## Suggested subject hierarchy
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
subjectWorldAnchor
|
|
57
|
+
└── subjectPlacementRig ← chapter composition
|
|
58
|
+
└── subjectTransitionRig ← handoff ownership
|
|
59
|
+
└── subjectReactionRig
|
|
60
|
+
└── subjectIdleRig
|
|
61
|
+
└── modelRoot
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Not every project needs every rig. Add a rig only when a distinct system must own a transform channel.
|
|
65
|
+
|
|
66
|
+
## Named anchors
|
|
67
|
+
|
|
68
|
+
Create anchors inside or around the model:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
productRoot
|
|
72
|
+
├── anchor:center
|
|
73
|
+
├── anchor:lens
|
|
74
|
+
├── anchor:logo
|
|
75
|
+
├── anchor:detailA
|
|
76
|
+
├── anchor:portalPlane
|
|
77
|
+
└── anchor:cameraTarget
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Anchors support:
|
|
81
|
+
|
|
82
|
+
- shot targeting,
|
|
83
|
+
- DOM labels aligned to 3D,
|
|
84
|
+
- transition portals,
|
|
85
|
+
- particle emitters,
|
|
86
|
+
- lighting attachment,
|
|
87
|
+
- screen-space measurements,
|
|
88
|
+
- authored motion paths.
|
|
89
|
+
|
|
90
|
+
Prefer anchors exported from the 3D file or created in a model setup module instead of repeated magic coordinates.
|
|
91
|
+
|
|
92
|
+
## Camera shot application
|
|
93
|
+
|
|
94
|
+
A shot contains target values. Apply it to narrative rigs, not reactive rigs.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
function addShotTween(
|
|
98
|
+
tl: gsap.core.Timeline,
|
|
99
|
+
rig: THREE.Group,
|
|
100
|
+
target: THREE.Object3D,
|
|
101
|
+
camera: THREE.PerspectiveCamera,
|
|
102
|
+
shot: ShotPreset,
|
|
103
|
+
position: gsap.Position,
|
|
104
|
+
duration: number,
|
|
105
|
+
) {
|
|
106
|
+
tl.to(rig.position, {
|
|
107
|
+
x: shot.camera.position[0],
|
|
108
|
+
y: shot.camera.position[1],
|
|
109
|
+
z: shot.camera.position[2],
|
|
110
|
+
duration,
|
|
111
|
+
ease: shot.ease ?? 'power2.inOut',
|
|
112
|
+
}, position);
|
|
113
|
+
|
|
114
|
+
tl.to(target.position, {
|
|
115
|
+
x: shot.camera.target[0],
|
|
116
|
+
y: shot.camera.target[1],
|
|
117
|
+
z: shot.camera.target[2],
|
|
118
|
+
duration,
|
|
119
|
+
ease: shot.targetEase ?? shot.ease ?? 'power2.inOut',
|
|
120
|
+
}, position);
|
|
121
|
+
|
|
122
|
+
tl.to(camera, {
|
|
123
|
+
fov: shot.camera.fov,
|
|
124
|
+
duration,
|
|
125
|
+
ease: shot.fovEase ?? 'power1.inOut',
|
|
126
|
+
onUpdate: () => camera.updateProjectionMatrix(),
|
|
127
|
+
}, position);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For performance, avoid creating new vectors every frame. Reuse temporary objects.
|
|
132
|
+
|
|
133
|
+
## Frame composition
|
|
134
|
+
|
|
135
|
+
`useFrame` is suitable for high-frequency visual composition inside the Canvas.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
useFrame((state, delta) => {
|
|
139
|
+
const runtime = runtimeRef.current;
|
|
140
|
+
|
|
141
|
+
reactionRig.current.rotation.y = damp(
|
|
142
|
+
reactionRig.current.rotation.y,
|
|
143
|
+
runtime.pointer.x * direction.pointerYaw,
|
|
144
|
+
direction.pointerDamping,
|
|
145
|
+
delta,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
shakeRig.current.position.y =
|
|
149
|
+
Math.sin(state.clock.elapsedTime * 0.7) * direction.idleFloat;
|
|
150
|
+
|
|
151
|
+
camera.current.lookAt(targetAnchor.current.getWorldPosition(tempTarget));
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
GSAP owns narrative rig values. `useFrame` owns reaction, ambient motion, and final camera look-at resolution.
|
|
156
|
+
|
|
157
|
+
`useFrame` runs on the single clock: the canvas is mounted with `frameloop="never"` and advanced from `gsap.ticker` (see [`07-stack-integration.md`](07-stack-integration.md) and [`../examples/code/shared/GsapFrameLoop.tsx`](../examples/code/shared/GsapFrameLoop.tsx)). Because scroll, timelines and rendering share one ticker, there is no separate render loop that can drift out of step with the narrative values `useFrame` reads.
|
|
158
|
+
|
|
159
|
+
## Animating Three.js with GSAP
|
|
160
|
+
|
|
161
|
+
GSAP can tween numeric object properties directly:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
gsap.to(group.rotation, { y: Math.PI * 0.75, duration: 1 });
|
|
165
|
+
gsap.to(material, { opacity: 0.2, duration: 0.4 });
|
|
166
|
+
gsap.to(uniforms.uProgress, { value: 1, duration: 1 });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use proxy objects when:
|
|
170
|
+
|
|
171
|
+
- several final values must be composed,
|
|
172
|
+
- a uniform requires clamping or transformation,
|
|
173
|
+
- a timeline should remain decoupled from a material instance,
|
|
174
|
+
- responsive variants share the same logical channel.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const channels = {
|
|
178
|
+
narrativeDistortion: 0,
|
|
179
|
+
velocityDistortion: 0,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
gsap.to(channels, { narrativeDistortion: 0.7 });
|
|
183
|
+
|
|
184
|
+
useFrame(() => {
|
|
185
|
+
material.uniforms.uDistortion.value = Math.min(
|
|
186
|
+
1,
|
|
187
|
+
channels.narrativeDistortion + Math.abs(channels.velocityDistortion),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Reparenting and continuity
|
|
193
|
+
|
|
194
|
+
When a bridge object must move between rigs, preserve world transform.
|
|
195
|
+
|
|
196
|
+
Three.js `Object3D.attach()` can reparent while preserving world transform when the hierarchy has compatible scale assumptions. Test carefully when non-uniform scale exists.
|
|
197
|
+
|
|
198
|
+
Alternative:
|
|
199
|
+
|
|
200
|
+
1. read world position, quaternion, and scale,
|
|
201
|
+
2. reparent,
|
|
202
|
+
3. convert world transform into the new parent's local space,
|
|
203
|
+
4. apply values,
|
|
204
|
+
5. continue the timeline from that local state.
|
|
205
|
+
|
|
206
|
+
Document the coordinate-space requirement in the handoff contract.
|
|
207
|
+
|
|
208
|
+
## DOM-to-WebGL alignment
|
|
209
|
+
|
|
210
|
+
For labels, masks, or shared transitions, convert world positions to normalized device coordinates and then screen pixels.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
worldPosition.project(camera);
|
|
214
|
+
const x = (worldPosition.x * 0.5 + 0.5) * viewportWidth;
|
|
215
|
+
const y = (-worldPosition.y * 0.5 + 0.5) * viewportHeight;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Consider:
|
|
219
|
+
|
|
220
|
+
- device pixel ratio,
|
|
221
|
+
- DOM scroll transforms,
|
|
222
|
+
- canvas bounds,
|
|
223
|
+
- perspective scale,
|
|
224
|
+
- visibility behind the camera,
|
|
225
|
+
- occlusion,
|
|
226
|
+
- responsive safe areas.
|
|
227
|
+
|
|
228
|
+
Do not update React state every frame solely to position a DOM label. Write to a CSS transform through a ref or use an established HTML-in-3D helper when appropriate.
|
|
229
|
+
|
|
230
|
+
## Scene strategy
|
|
231
|
+
|
|
232
|
+
### One persistent scene
|
|
233
|
+
Best for a unified product or world with continuous lighting and camera behavior.
|
|
234
|
+
|
|
235
|
+
### Multiple scene roots in one scene
|
|
236
|
+
Toggle or blend environment groups while sharing a camera and effects pipeline.
|
|
237
|
+
|
|
238
|
+
### Multiple scenes with render targets
|
|
239
|
+
Best for portals, compositing, cross-scene displacement, or radically different render settings.
|
|
240
|
+
|
|
241
|
+
### Route-specific canvases
|
|
242
|
+
Best when routes are independently immersive and route transition continuity can be handled outside the canvas or through snapshots.
|
|
243
|
+
|
|
244
|
+
Choose the least complex strategy that supports the handoff language.
|
|
245
|
+
|
|
246
|
+
## Lighting rigs
|
|
247
|
+
|
|
248
|
+
Separate lighting responsibilities:
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
lightingRoot
|
|
252
|
+
├── narrativeLightingRig
|
|
253
|
+
│ ├── key
|
|
254
|
+
│ ├── fill
|
|
255
|
+
│ └── rim
|
|
256
|
+
├── environmentLightingRig
|
|
257
|
+
└── reactiveAccentLights
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Animate intensity, color, position, and environment contribution through named lighting states. Avoid changing every light independently without a state plan.
|
|
261
|
+
|
|
262
|
+
## Effects ownership
|
|
263
|
+
|
|
264
|
+
Post-processing should support narrative state:
|
|
265
|
+
|
|
266
|
+
- bloom increases at a reveal apex,
|
|
267
|
+
- depth of field shifts during rack focus,
|
|
268
|
+
- chromatic displacement responds briefly to velocity,
|
|
269
|
+
- vignette increases before a portal pass,
|
|
270
|
+
- grain remains ambient and stable,
|
|
271
|
+
- color grading transitions between chapter moods.
|
|
272
|
+
|
|
273
|
+
Create effect channels rather than allowing each chapter to instantiate its own independent composer.
|
|
274
|
+
|
|
275
|
+
## Performance rules
|
|
276
|
+
|
|
277
|
+
- Reuse geometries and materials where possible.
|
|
278
|
+
- Avoid mounting and unmounting large model trees during a visible handoff.
|
|
279
|
+
- Preload assets needed by the next transition.
|
|
280
|
+
- Limit transparent particle overdraw.
|
|
281
|
+
- Use instancing for repeated geometry.
|
|
282
|
+
- Avoid React state updates in `useFrame`.
|
|
283
|
+
- Reuse vectors, matrices, and quaternions.
|
|
284
|
+
- Scale DPR and post-processing by performance tier.
|
|
285
|
+
- Dispose custom resources on unmount.
|
|
286
|
+
- Profile actual target devices.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Stack Integration
|
|
2
|
+
|
|
3
|
+
## Responsibility map
|
|
4
|
+
|
|
5
|
+
The stack is strongest when each tool has a clear job.
|
|
6
|
+
|
|
7
|
+
| Tool | Primary responsibility | Avoid |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Next.js / Vite | application shell, bundling, routing, asset delivery | embedding choreography logic in route files |
|
|
10
|
+
| React | lifecycle, composition, semantic state | high-frequency visual state updates every frame |
|
|
11
|
+
| React Three Fiber | Three.js scene graph and frame composition | forcing all scene changes through React re-renders |
|
|
12
|
+
| GSAP | authored sequences and property interpolation | becoming an unstructured global mutation layer |
|
|
13
|
+
| ScrollTrigger | mapping scroll ranges to timeline progress | treating each section as an isolated animation by default |
|
|
14
|
+
| Lenis | smoothing and normalizing physical scroll | controlling narrative state independently of ScrollTrigger |
|
|
15
|
+
| Motion for React | local presence, layout, gestures, UI state | controlling the same transform as GSAP |
|
|
16
|
+
| SVG GSAP plugins | vector drawing, morphing, path movement | using incompatible paths without shape preparation |
|
|
17
|
+
|
|
18
|
+
## Next.js guidance
|
|
19
|
+
|
|
20
|
+
Place browser-dependent experience systems behind a client boundary.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
'use client';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Register GSAP plugins in a client-side module. Build timelines after refs and WebGL targets are ready. Route transitions must account for App Router mounting behavior and must kill route-specific timelines and ScrollTriggers.
|
|
27
|
+
|
|
28
|
+
For a large canvas, consider dynamically loading the experience component with server-side rendering disabled when that improves reliability. Preserve semantic content and fallback output outside the canvas.
|
|
29
|
+
|
|
30
|
+
Do not put mutable Three.js objects into server components or serialize them through props.
|
|
31
|
+
|
|
32
|
+
## Vite guidance
|
|
33
|
+
|
|
34
|
+
Vite has fewer server/client boundaries. Keep the same lifecycle discipline:
|
|
35
|
+
|
|
36
|
+
- register plugins once,
|
|
37
|
+
- create timelines inside scoped hooks,
|
|
38
|
+
- remove ticker callbacks,
|
|
39
|
+
- kill ScrollTriggers,
|
|
40
|
+
- dispose route-specific scene resources,
|
|
41
|
+
- refresh measurements after fonts and assets are ready.
|
|
42
|
+
|
|
43
|
+
## GSAP in React
|
|
44
|
+
|
|
45
|
+
Use the official React integration or scoped GSAP context so animation and ScrollTrigger instances revert on cleanup.
|
|
46
|
+
|
|
47
|
+
Conceptual pattern:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const root = useRef<HTMLDivElement>(null);
|
|
51
|
+
|
|
52
|
+
useGSAP(() => {
|
|
53
|
+
const tl = gsap.timeline();
|
|
54
|
+
// Build scoped timelines and ScrollTriggers.
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
// Additional cleanup not handled by context.
|
|
58
|
+
};
|
|
59
|
+
}, { scope: root, dependencies: [isReady] });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Do not rebuild expensive timelines on every React render. Keep direction objects stable and recreate only when relevant responsive or asset-readiness conditions change.
|
|
63
|
+
|
|
64
|
+
## Lenis and ScrollTrigger
|
|
65
|
+
|
|
66
|
+
Use one Lenis instance for the root scroll unless the design intentionally uses a custom scroll container.
|
|
67
|
+
|
|
68
|
+
Key integration responsibilities:
|
|
69
|
+
|
|
70
|
+
- Lenis emits scroll updates to ScrollTrigger.
|
|
71
|
+
- Lenis is advanced from a controlled animation ticker.
|
|
72
|
+
- ScrollTrigger refreshes after layout-affecting assets are ready.
|
|
73
|
+
- Lenis is stopped during states that require scroll lock.
|
|
74
|
+
- native nested scrolling is handled deliberately.
|
|
75
|
+
|
|
76
|
+
Lenis must be created with `autoRaf: false`. If it keeps its own animation loop it becomes a second clock next to `gsap.ticker`, and scroll, ScrollTrigger and the canvas drift apart by a frame. The canonical setup lives in [`../../references/setup.md`](../../references/setup.md); this is the conceptual shape.
|
|
77
|
+
|
|
78
|
+
Conceptual setup:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// ScrollTrigger sync: useLenis subscribes once the root instance exists.
|
|
82
|
+
// Never capture lenisRef.current?.lenis once in an effect; on first mount it
|
|
83
|
+
// is often still undefined, and the listener is silently never bound.
|
|
84
|
+
useLenis(() => ScrollTrigger.update());
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
// Registered unconditionally, instance resolved per frame.
|
|
88
|
+
const update = (time: number) => {
|
|
89
|
+
lenisRef.current?.lenis?.raf(time * 1000);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
gsap.ticker.add(update);
|
|
93
|
+
gsap.ticker.lagSmoothing(0);
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
gsap.ticker.remove(update);
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Do not add CSS `scroll-behavior: smooth` on top of Lenis.
|
|
102
|
+
|
|
103
|
+
## R3F frame-loop options
|
|
104
|
+
|
|
105
|
+
The single-clock rule applies: the canvas is rendered from `gsap.ticker`, the same callback chain that advances Lenis. The exact bridge is documented in [`../../references/webgl-r3f.md`](../../references/webgl-r3f.md) and shown in [`../examples/code/shared/GsapFrameLoop.tsx`](../examples/code/shared/GsapFrameLoop.tsx).
|
|
106
|
+
|
|
107
|
+
### Ticker-driven loop (default)
|
|
108
|
+
|
|
109
|
+
Mount the canvas with `frameloop="never"` and call `advance()` from a `gsap.ticker` callback. `useFrame` still runs, once per ticker tick, so ambient and reactive layers keep working, and GSAP tweens of Three.js objects land in the frame they were written.
|
|
110
|
+
|
|
111
|
+
Use when ambient animation is always active, which is the normal case for a directed experience.
|
|
112
|
+
|
|
113
|
+
Advantages:
|
|
114
|
+
|
|
115
|
+
- one clock for scroll, timelines and rendering,
|
|
116
|
+
- no drift between ScrollTrigger measurements and the rendered frame,
|
|
117
|
+
- `useFrame` reads narrative values from the same tick that produced them, with no separate render loop to run ahead or behind.
|
|
118
|
+
|
|
119
|
+
### Demand loop with invalidation
|
|
120
|
+
|
|
121
|
+
Use only when the scene changes solely with scroll or pointer and has no ambient animation, so it can sit idle (power efficiency matters).
|
|
122
|
+
|
|
123
|
+
Mount the canvas with `frameloop="demand"` and call `invalidate()` from the things that change it (a ScrollTrigger `onUpdate`, a pointer handler, a uniform setter). R3F then renders on demand without a continuous loop of its own. A directed experience with ambient shaders, particles or model idles is not this case, so use the ticker-driven loop instead.
|
|
124
|
+
|
|
125
|
+
### Fully coordinated manual loop
|
|
126
|
+
|
|
127
|
+
Use only when exact render ordering or multi-scene compositing requires it. This is more complex and should be justified by the transition design. It still advances from the one ticker.
|
|
128
|
+
|
|
129
|
+
Do not leave the canvas on R3F's default `frameloop="always"` next to a Lenis ticker. R3F then runs its own `requestAnimationFrame` loop, which is a second clock.
|
|
130
|
+
|
|
131
|
+
## Motion for React boundaries
|
|
132
|
+
|
|
133
|
+
Use Motion for:
|
|
134
|
+
|
|
135
|
+
- menus,
|
|
136
|
+
- route-level interface shells,
|
|
137
|
+
- local hover and press states,
|
|
138
|
+
- layout transitions,
|
|
139
|
+
- presence and exit behavior,
|
|
140
|
+
- interface controls,
|
|
141
|
+
- non-central content transitions.
|
|
142
|
+
|
|
143
|
+
Use GSAP for:
|
|
144
|
+
|
|
145
|
+
- the central chapter sequence,
|
|
146
|
+
- camera and WebGL choreography,
|
|
147
|
+
- multi-element timeline relationships,
|
|
148
|
+
- scroll-scrubbed sequences,
|
|
149
|
+
- SVG path drawing and morphing,
|
|
150
|
+
- branded typography direction.
|
|
151
|
+
|
|
152
|
+
The split that keeps the two libraries from colliding: GSAP owns scroll-linked and chapter-timeline motion, Motion owns React-lifecycle motion (mount, unmount, presence, layout, UI state). Motion never reads scroll progress for the central narrative.
|
|
153
|
+
|
|
154
|
+
If a Motion component must participate in a GSAP sequence, let GSAP own a wrapper and Motion own an inner element or vice versa. Never animate the same CSS transform layer from both systems.
|
|
155
|
+
|
|
156
|
+
## SplitText lifecycle
|
|
157
|
+
|
|
158
|
+
Typography layout can change after fonts load or the viewport changes. The text system must account for re-splitting.
|
|
159
|
+
|
|
160
|
+
Define:
|
|
161
|
+
|
|
162
|
+
- split mode,
|
|
163
|
+
- overflow masks,
|
|
164
|
+
- accessibility output,
|
|
165
|
+
- font-readiness condition,
|
|
166
|
+
- resize behavior,
|
|
167
|
+
- timeline rebuild or rebind behavior,
|
|
168
|
+
- cleanup and revert.
|
|
169
|
+
|
|
170
|
+
Do not cache line elements across a re-split without updating the timeline targets.
|
|
171
|
+
|
|
172
|
+
## ScrollTrigger architecture
|
|
173
|
+
|
|
174
|
+
Use ScrollTrigger to define input ranges, not the full conceptual architecture.
|
|
175
|
+
|
|
176
|
+
Preferred patterns:
|
|
177
|
+
|
|
178
|
+
### One long persistent-stage trigger
|
|
179
|
+
|
|
180
|
+
A master timeline maps the complete scroll narrative.
|
|
181
|
+
|
|
182
|
+
Best when the route is a highly integrated continuous experience.
|
|
183
|
+
|
|
184
|
+
### Chapter triggers within one persistent stage
|
|
185
|
+
|
|
186
|
+
Each chapter has a trigger and local timeline while the visual stage remains fixed.
|
|
187
|
+
|
|
188
|
+
Best when chapters need separate activation and easier tuning.
|
|
189
|
+
|
|
190
|
+
### Mixed pinned and flowing content
|
|
191
|
+
|
|
192
|
+
Some chapters control the persistent stage while semantic content can flow normally between immersive sequences.
|
|
193
|
+
|
|
194
|
+
Best when readability and editorial depth matter.
|
|
195
|
+
|
|
196
|
+
Avoid nested pinning unless it is carefully tested. Pin spacing and measurement changes can easily create fragile layouts.
|
|
197
|
+
|
|
198
|
+
## Refresh and resize sequence
|
|
199
|
+
|
|
200
|
+
After a meaningful responsive or asset change:
|
|
201
|
+
|
|
202
|
+
1. update viewport classification,
|
|
203
|
+
2. apply responsive direction data,
|
|
204
|
+
3. update camera projection and renderer size,
|
|
205
|
+
4. re-split typography if required,
|
|
206
|
+
5. rebuild affected timelines,
|
|
207
|
+
6. refresh ScrollTrigger measurements,
|
|
208
|
+
7. restore or remap progress,
|
|
209
|
+
8. validate the active state.
|
|
210
|
+
|
|
211
|
+
Debounce expensive rebuilds, but do not leave stale measurements.
|
|
212
|
+
|
|
213
|
+
## Route transitions
|
|
214
|
+
|
|
215
|
+
A professional route transition defines:
|
|
216
|
+
|
|
217
|
+
- click intent,
|
|
218
|
+
- interaction lock,
|
|
219
|
+
- leave timeline,
|
|
220
|
+
- destination asset prefetch,
|
|
221
|
+
- scroll state handling,
|
|
222
|
+
- history behavior,
|
|
223
|
+
- transition cover,
|
|
224
|
+
- destination ready condition,
|
|
225
|
+
- enter timeline,
|
|
226
|
+
- cleanup and unlock.
|
|
227
|
+
|
|
228
|
+
Do not begin navigation before the transition system has captured or covered the current visual state if continuity depends on it.
|