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.
Files changed (61) hide show
  1. package/COMPATIBILITY.md +244 -0
  2. package/LICENSE +21 -0
  3. package/MODELS.md +92 -0
  4. package/README.md +250 -0
  5. package/SKILL.md +1003 -0
  6. package/audit-mode.md +497 -0
  7. package/bin/install.mjs +91 -0
  8. package/compile-choreography.mjs +296 -0
  9. package/decision-log.md +241 -0
  10. package/examples/GETTING_STARTED.md +279 -0
  11. package/examples/KNOWN_ISSUES.md +50 -0
  12. package/examples/PROMPTS.md +166 -0
  13. package/examples/luxe/README.md +88 -0
  14. package/examples/luxe/index.html +662 -0
  15. package/examples/noir/README.md +72 -0
  16. package/examples/noir/index.html +634 -0
  17. package/examples/pop/README.md +81 -0
  18. package/examples/pop/index.html +711 -0
  19. package/examples/renaissance/README.md +39 -0
  20. package/examples/renaissance/index.html +648 -0
  21. package/examples/studio/README.md +77 -0
  22. package/examples/studio/chapters.js +105 -0
  23. package/examples/studio/index.html +520 -0
  24. package/manifest.json +92 -0
  25. package/manifest.md +136 -0
  26. package/package.json +56 -0
  27. package/references/film-archetypes.md +211 -0
  28. package/references/performance-budget.md +499 -0
  29. package/references/scroll-patterns.md +693 -0
  30. package/scroll-choreography-compilation.md +543 -0
  31. package/scroll-choreography.json +1512 -0
  32. package/taste-guardrails.md +164 -0
  33. package/templates/nextjs/.env.example +41 -0
  34. package/templates/nextjs/app/api/fal/proxy/route.ts +33 -0
  35. package/templates/nextjs/app/api/fal/webhook/route.ts +132 -0
  36. package/templates/nextjs/app/api/generate-edition-asset/route.ts +66 -0
  37. package/templates/nextjs/app/globals.css +80 -0
  38. package/templates/nextjs/app/layout.tsx +21 -0
  39. package/templates/nextjs/app/page.tsx +10 -0
  40. package/templates/nextjs/components/ChapterDemoVisual.tsx +212 -0
  41. package/templates/nextjs/components/ChapterScene.tsx +373 -0
  42. package/templates/nextjs/components/EditionsPage.tsx +116 -0
  43. package/templates/nextjs/components/SmoothScrollProvider.tsx +8 -0
  44. package/templates/nextjs/lib/api-guard.ts +110 -0
  45. package/templates/nextjs/lib/editions-manifest.ts +224 -0
  46. package/templates/nextjs/lib/fal-client.ts +12 -0
  47. package/templates/nextjs/lib/fal-generate.ts +86 -0
  48. package/templates/nextjs/lib/fal-models.ts +213 -0
  49. package/templates/nextjs/lib/prompt-contract.ts +97 -0
  50. package/templates/nextjs/lib/use-device.ts +42 -0
  51. package/templates/nextjs/lib/use-lenis.ts +35 -0
  52. package/templates/nextjs/next.config.ts +29 -0
  53. package/templates/nextjs/package-lock.json +6455 -0
  54. package/templates/nextjs/package.json +41 -0
  55. package/templates/nextjs/package.patch.json +28 -0
  56. package/templates/nextjs/postcss.config.js +6 -0
  57. package/templates/nextjs/scripts/generate-chapter-assets.mjs +243 -0
  58. package/templates/nextjs/scripts/setup.mjs +170 -0
  59. package/templates/nextjs/tailwind.config.ts +37 -0
  60. package/templates/nextjs/tsconfig.json +23 -0
  61. 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.