cinematic-scroll-skill 2.1.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.
- package/COMPATIBILITY.md +244 -0
- package/LICENSE +21 -0
- package/MODELS.md +92 -0
- package/README.md +250 -0
- package/SKILL.md +1003 -0
- package/audit-mode.md +497 -0
- package/bin/install.mjs +91 -0
- package/compile-choreography.mjs +296 -0
- package/decision-log.md +241 -0
- package/examples/GETTING_STARTED.md +279 -0
- package/examples/KNOWN_ISSUES.md +50 -0
- package/examples/PROMPTS.md +166 -0
- package/examples/luxe/README.md +88 -0
- package/examples/luxe/index.html +662 -0
- package/examples/noir/README.md +72 -0
- package/examples/noir/index.html +634 -0
- package/examples/pop/README.md +81 -0
- package/examples/pop/index.html +711 -0
- package/examples/renaissance/README.md +39 -0
- package/examples/renaissance/index.html +648 -0
- package/examples/studio/README.md +77 -0
- package/examples/studio/chapters.js +105 -0
- package/examples/studio/index.html +520 -0
- package/manifest.json +92 -0
- package/manifest.md +136 -0
- package/package.json +56 -0
- package/references/film-archetypes.md +211 -0
- package/references/performance-budget.md +499 -0
- package/references/scroll-patterns.md +693 -0
- package/scroll-choreography-compilation.md +543 -0
- package/scroll-choreography.json +1512 -0
- package/taste-guardrails.md +164 -0
- package/templates/nextjs/.env.example +41 -0
- package/templates/nextjs/app/api/fal/proxy/route.ts +33 -0
- package/templates/nextjs/app/api/fal/webhook/route.ts +132 -0
- package/templates/nextjs/app/api/generate-edition-asset/route.ts +66 -0
- package/templates/nextjs/app/globals.css +80 -0
- package/templates/nextjs/app/layout.tsx +21 -0
- package/templates/nextjs/app/page.tsx +10 -0
- package/templates/nextjs/components/ChapterDemoVisual.tsx +212 -0
- package/templates/nextjs/components/ChapterScene.tsx +373 -0
- package/templates/nextjs/components/EditionsPage.tsx +116 -0
- package/templates/nextjs/components/SmoothScrollProvider.tsx +8 -0
- package/templates/nextjs/lib/api-guard.ts +110 -0
- package/templates/nextjs/lib/editions-manifest.ts +224 -0
- package/templates/nextjs/lib/fal-client.ts +12 -0
- package/templates/nextjs/lib/fal-generate.ts +86 -0
- package/templates/nextjs/lib/fal-models.ts +213 -0
- package/templates/nextjs/lib/prompt-contract.ts +97 -0
- package/templates/nextjs/lib/use-device.ts +42 -0
- package/templates/nextjs/lib/use-lenis.ts +35 -0
- package/templates/nextjs/next.config.ts +29 -0
- package/templates/nextjs/package-lock.json +6455 -0
- package/templates/nextjs/package.json +41 -0
- package/templates/nextjs/package.patch.json +28 -0
- package/templates/nextjs/postcss.config.js +6 -0
- package/templates/nextjs/scripts/generate-chapter-assets.mjs +243 -0
- package/templates/nextjs/scripts/setup.mjs +170 -0
- package/templates/nextjs/tailwind.config.ts +37 -0
- package/templates/nextjs/tsconfig.json +23 -0
- package/troubleshooting.md +1284 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# Scroll-Choreography.json Compilation Pipeline
|
|
2
|
+
|
|
3
|
+
> How the declarative schema becomes running GSAP ScrollTrigger code.
|
|
4
|
+
|
|
5
|
+
## ▶ It's real: `compile-choreography.mjs`
|
|
6
|
+
|
|
7
|
+
This pipeline ships as a working, dependency-free Node compiler at the repo root.
|
|
8
|
+
It reads a choreography document and emits runnable GSAP ScrollTrigger + Lenis code.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# compile the bundled example (the schema's examples[0]) and print to stdout
|
|
12
|
+
node compile-choreography.mjs --example
|
|
13
|
+
|
|
14
|
+
# compile your own choreography to a file
|
|
15
|
+
node compile-choreography.mjs my-scene.json --out scene.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The compiler's most important job: it maps the schema's CSS-style property names
|
|
19
|
+
(`translateX`, `translateY`, `rotateZ`…) to **GSAP's shorthand** (`x`, `y`,
|
|
20
|
+
`rotation`…). GSAP silently ignores the CSS names, so this mapping — centralized
|
|
21
|
+
in one table in the compiler — is the difference between motion and a no-op. The
|
|
22
|
+
emitted code uses `gsap.timeline` + `ScrollTrigger` per chapter (pin, scrub,
|
|
23
|
+
layer parallax, title reveal, colour morph, velocity nodes), Lenis forwarded to
|
|
24
|
+
`ScrollTrigger.update`, and a `prefers-reduced-motion` guard that skips all motion.
|
|
25
|
+
|
|
26
|
+
The sections below document the conceptual pipeline the compiler implements.
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
`scroll-choreography.json` is a **declarative, cinematic grammar** for scroll-driven experiences. It does not execute directly. Instead, it passes through the compilation pipeline — now implemented in `compile-choreography.mjs` — producing production-ready GSAP code.
|
|
31
|
+
|
|
32
|
+
## Input
|
|
33
|
+
|
|
34
|
+
- `scroll-choreography.json` -- a valid JSON document conforming to the schema
|
|
35
|
+
- `taste-guardrails.md` -- banned pattern definitions and cinematic vocabulary
|
|
36
|
+
- `performance-budget.md` -- 60fps contract, layer budgets, mobile degradation tiers
|
|
37
|
+
|
|
38
|
+
## Output
|
|
39
|
+
|
|
40
|
+
| File | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `gsap-scroll-config.ts` | TypeScript module exporting GSAP timelines, ScrollTriggers, and Lenis config |
|
|
43
|
+
| `scroll-choreography.report.md` | Validation report: warnings, errors, performance projections |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 1: Validate
|
|
48
|
+
|
|
49
|
+
### 1.1 Schema Validation
|
|
50
|
+
Check JSON conforms to `scroll-choreography.json` schema. All required fields present, types correct, enums valid.
|
|
51
|
+
|
|
52
|
+
### 1.2 Taste Guardrail Validation
|
|
53
|
+
Against `taste-guardrails.md`:
|
|
54
|
+
|
|
55
|
+
| Check | Rule | Severity |
|
|
56
|
+
|-------|------|----------|
|
|
57
|
+
| Depth range | All `depth` values in 0.15-1.40 | Error |
|
|
58
|
+
| Layer count | No chapter has >7 layers | Error |
|
|
59
|
+
| Pin duration | All enabled pins in 150-400vh | Error |
|
|
60
|
+
| Transition variety | No adjacent chapters share transition type | Error |
|
|
61
|
+
| Title variety | No adjacent chapters share title reveal type | Error |
|
|
62
|
+
| No blur animation | No `filter: blur()` references in any property | Error |
|
|
63
|
+
| No layout animation | No `width/height/top/left/margin/padding` in properties | Error |
|
|
64
|
+
| Breathing room | >=80vh free-scroll between consecutive pinned chapters | Warning |
|
|
65
|
+
| Title timing | Title reveal `end` <= 0.70 of pin duration | Warning |
|
|
66
|
+
| Stagger limits | Stagger offset in 5-8% range, maxElements <=5 | Warning |
|
|
67
|
+
| Depth variety | Depth ratios differ between adjacent chapters | Warning |
|
|
68
|
+
|
|
69
|
+
### 1.3 Performance Budget Projection
|
|
70
|
+
|
|
71
|
+
Calculate projected compositor layers per chapter:
|
|
72
|
+
```
|
|
73
|
+
layer_count = sum(1 for layer in chapter.layers if (
|
|
74
|
+
layer.willChange or
|
|
75
|
+
layer.depth != 1.0 or
|
|
76
|
+
layer.content.type == "video"
|
|
77
|
+
)) + 1 # root layer always counts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Compare against `performance-budget.md` Layer Count Budget:
|
|
81
|
+
- Desktop (>10 layers): Warning
|
|
82
|
+
- Tablet (>6 layers): Error
|
|
83
|
+
- Mobile (>4 layers): Error
|
|
84
|
+
- Budget tier (>2 layers): Error
|
|
85
|
+
|
|
86
|
+
### 1.4 Velocity Node Validation
|
|
87
|
+
- All `threshold` values > 0.1 px/ms
|
|
88
|
+
- All `lerpFactor` values in 0.01-0.5 range
|
|
89
|
+
- No more than 3 velocityNodes per chapter (performance ceiling)
|
|
90
|
+
|
|
91
|
+
### Validation Failure Modes
|
|
92
|
+
|
|
93
|
+
| Failure | Behavior |
|
|
94
|
+
|---------|----------|
|
|
95
|
+
| Schema validation error | Compilation halts. Report lists all errors with JSON paths. |
|
|
96
|
+
| Taste guardrail error | Compilation halts. Specific rule violated, offending value shown. |
|
|
97
|
+
| Performance budget warning | Compilation continues with warning. User must acknowledge. |
|
|
98
|
+
| Breathing room warning | Compilation continues. Suggests inserting release viewport. |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Step 2: Layer Sort
|
|
103
|
+
|
|
104
|
+
### 2.1 Sort by Depth (Back to Front)
|
|
105
|
+
```typescript
|
|
106
|
+
const sortedLayers = chapter.layers.sort((a, b) => a.depth - b.depth);
|
|
107
|
+
// ascending: 0.15 (far background) -> 1.40 (foreground overlay)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2.2 will-change Strategy
|
|
111
|
+
Apply `will-change: transform` strategically:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Select up to 3 elements per viewport for will-change promotion
|
|
115
|
+
const willChangeCandidates = sortedLayers
|
|
116
|
+
.filter(l => l.willChange || l.depth >= 0.60) // prioritize visible layers
|
|
117
|
+
.slice(0, 3); // hard cap: 3 elements per viewport
|
|
118
|
+
|
|
119
|
+
// Apply 200ms before animation starts
|
|
120
|
+
// Remove 200ms after animation ends
|
|
121
|
+
// Never apply globally, never to text-only elements
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2.3 Motion Density Check
|
|
125
|
+
Ensure no more than 3 simultaneous motion types in any 50vh window (taste-guardrails.md §3.8):
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
function countMotionTypes(chapter: Chapter, windowStart: number, windowEnd: number): number {
|
|
129
|
+
const activeLayers = chapter.layers.filter(l =>
|
|
130
|
+
l.animation.properties.length > 0 &&
|
|
131
|
+
l.animation.trigger.start >= windowStart &&
|
|
132
|
+
l.animation.trigger.end <= windowEnd
|
|
133
|
+
);
|
|
134
|
+
const motionTypes = new Set<string>();
|
|
135
|
+
activeLayers.forEach(l => {
|
|
136
|
+
l.animation.properties.forEach(p => motionTypes.add(p.property));
|
|
137
|
+
});
|
|
138
|
+
return motionTypes.size; // must be <= 3
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Step 3: ScrollTrigger Generation
|
|
145
|
+
|
|
146
|
+
### 3.1 Chapter Timelines
|
|
147
|
+
Each chapter produces one GSAP timeline:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
function generateChapterTimeline(chapter: Chapter): gsap.core.Timeline {
|
|
151
|
+
const tl = gsap.timeline({
|
|
152
|
+
scrollTrigger: {
|
|
153
|
+
trigger: `[data-chapter="${chapter.id}"]`,
|
|
154
|
+
start: chapter.scrollRange.start + "vh top",
|
|
155
|
+
end: chapter.scrollRange.end + "vh top",
|
|
156
|
+
scrub: chapter.layers[0]?.animation?.trigger?.scrub ?? globals.scrollSmoothing,
|
|
157
|
+
pin: chapter.pin?.enabled ?? false,
|
|
158
|
+
pinSpacing: chapter.pin?.pinSpacing ?? true,
|
|
159
|
+
anticipatePin: chapter.pin?.anticipatorySettle ?? 0.05,
|
|
160
|
+
fastScrollEnd: true,
|
|
161
|
+
invalidateOnRefresh: true,
|
|
162
|
+
markers: false, // NEVER in production
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Layer animations as parallel tweens
|
|
167
|
+
chapter.layers.forEach(layer => {
|
|
168
|
+
const anim = layer.animation;
|
|
169
|
+
const props = anim.properties.reduce((acc, prop) => {
|
|
170
|
+
const unit = prop.unit || "";
|
|
171
|
+
acc[prop.property] = prop.to + unit;
|
|
172
|
+
// Store 'from' values as timeline position 0
|
|
173
|
+
return acc;
|
|
174
|
+
}, {} as Record<string, any>);
|
|
175
|
+
|
|
176
|
+
// Set from values at timeline position 0
|
|
177
|
+
const fromProps = anim.properties.reduce((acc, prop) => {
|
|
178
|
+
const unit = prop.unit || "";
|
|
179
|
+
acc[prop.property] = prop.from + unit;
|
|
180
|
+
return acc;
|
|
181
|
+
}, {} as Record<string, any>);
|
|
182
|
+
|
|
183
|
+
tl.fromTo(`[data-layer="${layer.id}"]`, fromProps, {
|
|
184
|
+
...props,
|
|
185
|
+
ease: anim.properties[0]?.easing || globals.defaultEasing,
|
|
186
|
+
duration: 1, // normalized: 0-1 along scroll range
|
|
187
|
+
}, 0); // all layers animate in parallel from scroll position 0
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return tl;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 3.2 Title Reveals as Nested Timelines
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
function generateTitleReveal(chapter: Chapter): gsap.core.Timeline | null {
|
|
198
|
+
if (!chapter.titleReveal) return null;
|
|
199
|
+
|
|
200
|
+
const tr = chapter.titleReveal;
|
|
201
|
+
const pinDuration = chapter.pin?.pinDuration ?? 200;
|
|
202
|
+
|
|
203
|
+
// Calculate absolute vh positions from pin percentage
|
|
204
|
+
const startVh = pinDuration * tr.scrollRange.start;
|
|
205
|
+
const endVh = pinDuration * tr.scrollRange.end;
|
|
206
|
+
|
|
207
|
+
const titleTl = gsap.timeline({
|
|
208
|
+
scrollTrigger: {
|
|
209
|
+
trigger: `[data-chapter="${chapter.id}"] .title`,
|
|
210
|
+
start: `top+=${startVh}vh top`,
|
|
211
|
+
end: `top+=${endVh}vh top`,
|
|
212
|
+
scrub: 0.3,
|
|
213
|
+
invalidateOnRefresh: true,
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
switch (tr.type) {
|
|
218
|
+
case "maskReveal":
|
|
219
|
+
titleTl.fromTo(".title", {
|
|
220
|
+
clipPath: "inset(0 100% 0 0)"
|
|
221
|
+
}, {
|
|
222
|
+
clipPath: "inset(0 0% 0 0)",
|
|
223
|
+
ease: tr.easing || globals.defaultEasing,
|
|
224
|
+
duration: 1,
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case "wordStagger":
|
|
229
|
+
// Split text into words, stagger each
|
|
230
|
+
titleTl.fromTo(".title .word", {
|
|
231
|
+
opacity: 0, y: 30
|
|
232
|
+
}, {
|
|
233
|
+
opacity: 1, y: 0,
|
|
234
|
+
stagger: tr.stagger?.offset ?? 0.06,
|
|
235
|
+
ease: tr.easing || globals.defaultEasing,
|
|
236
|
+
duration: 0.4,
|
|
237
|
+
}, 0);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case "letterSpacingScrub":
|
|
241
|
+
titleTl.fromTo(".title", {
|
|
242
|
+
letterSpacing: "-0.05em", opacity: 0.3
|
|
243
|
+
}, {
|
|
244
|
+
letterSpacing: "0.05em", opacity: 1,
|
|
245
|
+
ease: "none", // scrub-driven: linear mapping
|
|
246
|
+
duration: 1,
|
|
247
|
+
});
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
// ... additional title reveal types handled similarly
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return titleTl;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 3.3 Atmosphere / Background Morph
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
function generateAtmosphere(chapter: Chapter): void {
|
|
261
|
+
if (!chapter.atmosphere?.colorMorph) return;
|
|
262
|
+
|
|
263
|
+
const morph = chapter.atmosphere.colorMorph;
|
|
264
|
+
const pinDuration = chapter.pin?.pinDuration ?? 200;
|
|
265
|
+
|
|
266
|
+
gsap.to(`[data-chapter="${chapter.id}"]`, {
|
|
267
|
+
"--bg-color": morph.to, // CSS custom property
|
|
268
|
+
scrollTrigger: {
|
|
269
|
+
trigger: `[data-chapter="${chapter.id}"]`,
|
|
270
|
+
start: `${morph.scrollStart * pinDuration}vh top`,
|
|
271
|
+
end: `${morph.scrollEnd * pinDuration}vh top`,
|
|
272
|
+
scrub: true,
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Step 4: Transition Generation
|
|
281
|
+
|
|
282
|
+
### 4.1 Cinematic Vocabulary Mapping
|
|
283
|
+
|
|
284
|
+
> **GSAP property names — critical.** GSAP does NOT use CSS transform names.
|
|
285
|
+
> Use its shorthand or the tween silently no-ops:
|
|
286
|
+
> `x` (not `translateX`), `y` (not `translateY`), `rotation` (not `rotateZ`),
|
|
287
|
+
> `rotationX` (not `rotateX`), `rotationY` (not `rotateY`), `scale`, `autoAlpha`
|
|
288
|
+
> (opacity + visibility). The table below uses GSAP names.
|
|
289
|
+
|
|
290
|
+
| Transition Type | GSAP Implementation | Properties Applied |
|
|
291
|
+
|----------------|--------------------|--------------------|
|
|
292
|
+
| `craneShot` | `y` + `rotationX` | Vertical dolly with subtle tilt. `rotationX`: ±4deg. `transformPerspective`/`perspective-origin: 50% 100%` |
|
|
293
|
+
| `whipPan` | `x` + `power4.inOut` | Fast horizontal snap. 0.4s feel via scrub compression |
|
|
294
|
+
| `matchCut` | `autoAlpha` crossfade on identical layout | Same positions, content swaps. Layout holds perfectly still |
|
|
295
|
+
| `dissolve` | `autoAlpha` 1→0 + `scale` 1→0.97 | Gentle fade with subtle compression |
|
|
296
|
+
| `pushIn` | `scale` 1→1.08 + `y` centering | Slow zoom toward subject. Minimal other motion |
|
|
297
|
+
| `hardCut` | No animation | Instant transition. No overlap. |
|
|
298
|
+
|
|
299
|
+
### 4.2 Overlapping ScrollTrigger
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
function generateTransition(transition: Transition): gsap.core.Timeline {
|
|
303
|
+
const tl = gsap.timeline({
|
|
304
|
+
scrollTrigger: {
|
|
305
|
+
trigger: "body", // global transition
|
|
306
|
+
start: `${transition.fromChapterEnd - transition.overlap}vh top`,
|
|
307
|
+
end: `${transition.toChapterStart + transition.duration}vh top`,
|
|
308
|
+
scrub: 0.5,
|
|
309
|
+
invalidateOnRefresh: true,
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Outgoing chapter exit
|
|
314
|
+
tl.to(`[data-chapter="${transition.from}"]`, {
|
|
315
|
+
...mapTransitionType(transition.type, "exit"),
|
|
316
|
+
ease: transition.easing || "power4.inOut",
|
|
317
|
+
duration: 0.5,
|
|
318
|
+
}, 0);
|
|
319
|
+
|
|
320
|
+
// Incoming chapter entrance
|
|
321
|
+
tl.from(`[data-chapter="${transition.to}"]`, {
|
|
322
|
+
...mapTransitionType(transition.type, "enter"),
|
|
323
|
+
ease: transition.easing || "power4.inOut",
|
|
324
|
+
duration: 0.5,
|
|
325
|
+
}, 0.3); // 30% offset for overlap
|
|
326
|
+
|
|
327
|
+
return tl;
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Step 5: Velocity Wiring
|
|
334
|
+
|
|
335
|
+
### 5.1 Velocity Detection
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// From Lenis or raw RAF loop
|
|
339
|
+
let velocity = 0;
|
|
340
|
+
let lastScrollY = 0;
|
|
341
|
+
let lastTime = performance.now();
|
|
342
|
+
|
|
343
|
+
function trackVelocity() {
|
|
344
|
+
const now = performance.now();
|
|
345
|
+
const dt = now - lastTime;
|
|
346
|
+
const dy = lenis?.scroll || window.scrollY - lastScrollY;
|
|
347
|
+
velocity += (dy / dt - velocity) * 0.15; // lerp smoothing
|
|
348
|
+
lastScrollY = window.scrollY;
|
|
349
|
+
lastTime = now;
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 5.2 Velocity Node Application
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
function applyVelocityNodes(chapter: Chapter, currentVelocity: number): void {
|
|
357
|
+
chapter.velocityNodes?.forEach(node => {
|
|
358
|
+
const isAbove = currentVelocity > node.threshold;
|
|
359
|
+
const config = isAbove ? node.above : node.below;
|
|
360
|
+
if (!config) return;
|
|
361
|
+
|
|
362
|
+
const lerp = node.lerpFactor ?? 0.1;
|
|
363
|
+
|
|
364
|
+
// Apply via gsap.quickTo for 60fps performance
|
|
365
|
+
chapter.layers.forEach(layer => {
|
|
366
|
+
const el = document.querySelector(`[data-layer="${layer.id}"]`);
|
|
367
|
+
if (!el) return;
|
|
368
|
+
|
|
369
|
+
if (config.opacity !== undefined) {
|
|
370
|
+
const currentOpacity = parseFloat(gsap.getProperty(el, "opacity") as string);
|
|
371
|
+
const targetOpacity = config.opacity;
|
|
372
|
+
gsap.set(el, { opacity: currentOpacity + (targetOpacity - currentOpacity) * lerp });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (config.scale !== undefined) {
|
|
376
|
+
const currentScale = parseFloat(gsap.getProperty(el, "scale") as string) || 1;
|
|
377
|
+
gsap.set(el, { scale: currentScale + (config.scale - currentScale) * lerp });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (config.skewX !== undefined) {
|
|
381
|
+
const currentSkew = parseFloat(gsap.getProperty(el, "skewX") as string) || 0;
|
|
382
|
+
gsap.set(el, { skewX: currentSkew + (config.skewX - currentSkew) * lerp });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (config.letterSpacing !== undefined) {
|
|
386
|
+
gsap.set(el, { letterSpacing: config.letterSpacing });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 5.3 RAF Integration
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
function velocityLoop() {
|
|
397
|
+
trackVelocity();
|
|
398
|
+
|
|
399
|
+
chapters.forEach(chapter => {
|
|
400
|
+
if (chapter.velocityNodes && chapter.velocityNodes.length > 0) {
|
|
401
|
+
// Only process if chapter is in or near viewport
|
|
402
|
+
const trigger = ScrollTrigger.getById(chapter.id);
|
|
403
|
+
if (trigger && trigger.isActive) {
|
|
404
|
+
applyVelocityNodes(chapter, Math.abs(velocity));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
requestAnimationFrame(velocityLoop);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Start after all ScrollTriggers are created
|
|
413
|
+
ScrollTrigger.addEventListener("refreshInit", () => {
|
|
414
|
+
requestAnimationFrame(velocityLoop);
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Output Files
|
|
421
|
+
|
|
422
|
+
### gsap-scroll-config.ts
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// Auto-generated from scroll-choreography.json
|
|
426
|
+
// Do not edit manually -- recompile instead
|
|
427
|
+
|
|
428
|
+
import gsap from "gsap";
|
|
429
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
430
|
+
import Lenis from "@studio-freight/lenis";
|
|
431
|
+
|
|
432
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
433
|
+
|
|
434
|
+
// ---- Lenis Smooth Scroll ----
|
|
435
|
+
export const lenis = new Lenis({
|
|
436
|
+
lerp: 0.6, // from globals.scrollSmoothing
|
|
437
|
+
smoothWheel: true,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ---- Metadata ----
|
|
441
|
+
export const metadata = {
|
|
442
|
+
title: "Maison Voss - Quiet Luxury Brand Launch",
|
|
443
|
+
targetDevice: "desktop",
|
|
444
|
+
totalScrollRange: 2200,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// ---- Chapter Timelines ----
|
|
448
|
+
export const chapterTimelines: gsap.core.Timeline[] = [];
|
|
449
|
+
|
|
450
|
+
export function initTimelines() {
|
|
451
|
+
// Chapter: hero-manifesto (pinnedHero)
|
|
452
|
+
const heroManifestoTl = gsap.timeline({ /* ... */ });
|
|
453
|
+
chapterTimelines.push(heroManifestoTl);
|
|
454
|
+
|
|
455
|
+
// Chapter: editorial-philosophy (editorialLongread)
|
|
456
|
+
const editorialPhilosophyTl = gsap.timeline({ /* ... */ });
|
|
457
|
+
chapterTimelines.push(editorialPhilosophyTl);
|
|
458
|
+
|
|
459
|
+
// Chapter: finale-collection (chapteredRelease)
|
|
460
|
+
const finaleCollectionTl = gsap.timeline({ /* ... */ });
|
|
461
|
+
chapterTimelines.push(finaleCollectionTl);
|
|
462
|
+
|
|
463
|
+
// ---- Transitions ----
|
|
464
|
+
// hero-manifesto -> editorial-philosophy: craneShot
|
|
465
|
+
// editorial-philosophy -> finale-collection: dissolve
|
|
466
|
+
|
|
467
|
+
// ---- Velocity Wiring ----
|
|
468
|
+
// velocityLoop starts after refresh
|
|
469
|
+
|
|
470
|
+
ScrollTrigger.refresh();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---- Cleanup ----
|
|
474
|
+
export function destroyTimelines() {
|
|
475
|
+
chapterTimelines.forEach(tl => tl.kill());
|
|
476
|
+
ScrollTrigger.getAll().forEach(st => st.kill());
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### scroll-choreography.report.md
|
|
481
|
+
|
|
482
|
+
```markdown
|
|
483
|
+
# Scroll Choreography Compilation Report
|
|
484
|
+
|
|
485
|
+
## Input: Maison Voss - Quiet Luxury Brand Launch
|
|
486
|
+
|
|
487
|
+
### Validation Results
|
|
488
|
+
| Check | Status | Details |
|
|
489
|
+
|-------|--------|---------|
|
|
490
|
+
| Schema validation | PASS | All required fields present |
|
|
491
|
+
| Depth range | PASS | 6 unique depths across chapters |
|
|
492
|
+
| Layer count | PASS | Max 6 layers (chapter 3) |
|
|
493
|
+
| Pin duration | PASS | 250vh, 150vh(disabled), 300vh |
|
|
494
|
+
| Transition variety | PASS | craneShot, dissolve (different families) |
|
|
495
|
+
| Title variety | PASS | maskReveal, wordStagger, letterSpacingScrub |
|
|
496
|
+
| No blur animation | PASS | No filter animations detected |
|
|
497
|
+
| No layout animation | PASS | Only transform + opacity used |
|
|
498
|
+
| Breathing room | PASS | 500-1300 = 800vh between pinned chapters |
|
|
499
|
+
| Title timing | PASS | All title reveals end <= 0.70 |
|
|
500
|
+
| Stagger limits | PASS | Max 5 elements, offsets in 5-8% range |
|
|
501
|
+
|
|
502
|
+
### Performance Projection
|
|
503
|
+
| Chapter | Layers | will-change | Est. GPU Mem | Status |
|
|
504
|
+
|---------|--------|-------------|--------------|--------|
|
|
505
|
+
| hero-manifesto | 5 | 3 | ~12MB | OK |
|
|
506
|
+
| editorial-philosophy | 4 | 3 | ~12MB | OK |
|
|
507
|
+
| finale-collection | 6 | 3 | ~12MB | OK |
|
|
508
|
+
|
|
509
|
+
### Warnings (0)
|
|
510
|
+
_No warnings generated._
|
|
511
|
+
|
|
512
|
+
### Generated Files
|
|
513
|
+
- `gsap-scroll-config.ts` (1,847 lines)
|
|
514
|
+
- Compilation time: 340ms
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Edge Cases & Failure Modes
|
|
520
|
+
|
|
521
|
+
### Edge Case: Pin Duration at Boundary (150vh or 400vh)
|
|
522
|
+
Behavior: Valid. Compilation proceeds normally. Warning generated if exactly at boundary advising review.
|
|
523
|
+
|
|
524
|
+
### Edge Case: Overlapping Chapter Scroll Ranges
|
|
525
|
+
Behavior: Error if overlap > transition.overlap value. Transitions must explicitly declare overlap.
|
|
526
|
+
|
|
527
|
+
### Edge Case: VelocityNode Threshold Collision
|
|
528
|
+
Behavior: If two velocityNodes in same chapter have overlapping thresholds, compilation merges them into a single node with combined properties, using the lower lerpFactor for smoothness.
|
|
529
|
+
|
|
530
|
+
### Edge Case: Missing transition between adjacent chapters
|
|
531
|
+
Behavior: Hard cut is assumed. Warning generated suggesting explicit transition definition.
|
|
532
|
+
|
|
533
|
+
### Edge Case: Empty animation.properties array
|
|
534
|
+
Behavior: Layer is rendered statically. No ScrollTrigger created for that layer. Layer still counts toward compositor budget.
|
|
535
|
+
|
|
536
|
+
### Edge Case: prefers-reduced-motion detected at runtime
|
|
537
|
+
Behavior: All ScrollTrigger instances killed immediately. Pinned sections convert to static flow layout. All content shown in final state. No motion.
|
|
538
|
+
|
|
539
|
+
### Edge Case: Mobile tier detection at runtime
|
|
540
|
+
Behavior: Layer count reduced per Mobile Degradation Matrix. 3D transforms disabled on Tier 3+. Velocity effects disabled on touch devices. Parallax reduced to opacity-only on budget tier.
|
|
541
|
+
|
|
542
|
+
### Edge Case: Emergency degradation (frame rate drops below target)
|
|
543
|
+
Behavior: All parallax disabled immediately. Reduce to opacity-only transitions. Unpin all sections. Log event to analytics. No re-enable without page reload.
|