webgl-forensics 3.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 (35) hide show
  1. package/.github/workflows/forensics.yml +44 -0
  2. package/CLAUDE_CODE.md +49 -0
  3. package/MASTER_PROMPT.md +202 -0
  4. package/README.md +579 -0
  5. package/SKILL.md +1256 -0
  6. package/package.json +53 -0
  7. package/project summary.md +33 -0
  8. package/puppeteer-runner.js +486 -0
  9. package/scripts/00-tech-stack-detect.js +185 -0
  10. package/scripts/01-source-map-extractor.js +73 -0
  11. package/scripts/02-interaction-model.js +129 -0
  12. package/scripts/03-responsive-analysis.js +115 -0
  13. package/scripts/04-page-transitions.js +128 -0
  14. package/scripts/05-loading-sequence.js +124 -0
  15. package/scripts/06-audio-extraction.js +115 -0
  16. package/scripts/07-accessibility-reduced-motion.js +133 -0
  17. package/scripts/08-complexity-scorer.js +138 -0
  18. package/scripts/09-visual-diff-validator.js +113 -0
  19. package/scripts/10-webgpu-extractor.js +248 -0
  20. package/scripts/11-scroll-screenshot-grid.js +76 -0
  21. package/scripts/12-network-waterfall.js +151 -0
  22. package/scripts/13-react-fiber-walker.js +285 -0
  23. package/scripts/14-shader-hotpatch.js +349 -0
  24. package/scripts/15-multipage-crawler.js +221 -0
  25. package/scripts/16-gsap-timeline-recorder.js +335 -0
  26. package/scripts/17-r3f-fiber-serializer.js +316 -0
  27. package/scripts/18-font-extractor.js +290 -0
  28. package/scripts/19-design-token-export.js +85 -0
  29. package/scripts/20-timeline-visualizer.js +61 -0
  30. package/scripts/21-lighthouse-audit.js +35 -0
  31. package/scripts/22-sitemap-crawler.js +128 -0
  32. package/scripts/23-scaffold-generator.js +451 -0
  33. package/scripts/24-ai-reconstruction.js +226 -0
  34. package/scripts/25-shader-annotator.js +226 -0
  35. package/tui.js +196 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * SCRIPT 15 — Multi-Page Crawler
3
+ * webgl-forensics skill | Enhancement #3
4
+ *
5
+ * PURPOSE:
6
+ * Discover and queue all internal pages on the site so the forensics
7
+ * engine can run a full-site analysis — not just the homepage.
8
+ *
9
+ * Produces a page manifest with:
10
+ * - All internal links discovered
11
+ * - Grouped by route type (static, dynamic, catch-all)
12
+ * - Priority queue (which pages matter most for 3D forensics)
13
+ * - Shell commands for sequential forensics using navigate_page
14
+ *
15
+ * RUN VIA:
16
+ * evaluate_script — run on any page to map the full site
17
+ *
18
+ * FOLLOW-UP WORKFLOW:
19
+ * For each page in the priority queue returned by this script:
20
+ * 1. navigate_page to that URL
21
+ * 2. Run scripts 00, 01, 02 (core forensics)
22
+ * 3. Run 13 (Fiber walker) if React detected
23
+ * 4. Append findings to master forensics report
24
+ */
25
+ (() => {
26
+ const BASE_URL = window.location.origin;
27
+ const CURRENT_PATH = window.location.pathname;
28
+
29
+ // ═══════════════════════════════════════════════
30
+ // STEP 1 — Collect all internal links from DOM
31
+ // ═══════════════════════════════════════════════
32
+ const rawLinks = new Set();
33
+
34
+ // DOM anchor tags
35
+ document.querySelectorAll('a[href]').forEach(a => {
36
+ const href = a.href;
37
+ if (href && (href.startsWith(BASE_URL) || href.startsWith('/'))) {
38
+ rawLinks.add(href.startsWith('/') ? BASE_URL + href : href);
39
+ }
40
+ });
41
+
42
+ // Next.js prefetch links (data-href)
43
+ document.querySelectorAll('[data-href]').forEach(el => {
44
+ const href = el.dataset.href;
45
+ if (href) rawLinks.add(href.startsWith('/') ? BASE_URL + href : href);
46
+ });
47
+
48
+ // ═══════════════════════════════════════════════
49
+ // STEP 2 — Extract routes from Next.js manifest
50
+ // ═══════════════════════════════════════════════
51
+ const manifestRoutes = [];
52
+
53
+ // Next.js exposes routes in multiple places
54
+ if (window.__NEXT_DATA__?.buildId) {
55
+ const nextData = window.__NEXT_DATA__;
56
+ manifestRoutes.push({
57
+ source: '__NEXT_DATA__',
58
+ page: nextData.page,
59
+ query: nextData.query,
60
+ buildId: nextData.buildId,
61
+ });
62
+ }
63
+
64
+ // Check for client-side router state
65
+ if (window.next?.router) {
66
+ const router = window.next.router;
67
+ manifestRoutes.push({
68
+ source: 'next.router',
69
+ pathname: router.pathname,
70
+ asPath: router.asPath,
71
+ route: router.route,
72
+ });
73
+ }
74
+
75
+ // Parse routes from script tags with _ssgManifest or _buildManifest
76
+ for (const script of document.querySelectorAll('script[src]')) {
77
+ if (script.src.includes('_ssgManifest') || script.src.includes('_buildManifest')) {
78
+ manifestRoutes.push({ manifestScript: script.src });
79
+ }
80
+ }
81
+
82
+ // ═══════════════════════════════════════════════
83
+ // STEP 3 — Extract Routes from React Router / Wouter
84
+ // ═══════════════════════════════════════════════
85
+ function extractClientRoutes() {
86
+ const routes = [];
87
+
88
+ // React Router v6
89
+ if (window.__reactRouterMatch) {
90
+ routes.push({ source: 'react-router', match: window.__reactRouterMatch });
91
+ }
92
+
93
+ // Check history API
94
+ if (window.history?.state) {
95
+ routes.push({ source: 'history.state', state: window.history.state });
96
+ }
97
+
98
+ return routes;
99
+ }
100
+
101
+ // ═══════════════════════════════════════════════
102
+ // STEP 4 — Clean and classify discovered URLs
103
+ // ═══════════════════════════════════════════════
104
+ const SKIP_PATTERNS = [
105
+ /\.(css|js|json|png|jpg|svg|ico|woff2?|ttf|otf|glb|gltf|webp|mp4|webm)$/i,
106
+ /^\/(api|_next)\//,
107
+ /#/,
108
+ /\?/,
109
+ /mailto:|tel:|javascript:/,
110
+ ];
111
+
112
+ const pages = new Map();
113
+
114
+ rawLinks.forEach(url => {
115
+ if (!url || SKIP_PATTERNS.some(p => p.test(url))) return;
116
+
117
+ let path;
118
+ try {
119
+ path = new URL(url).pathname;
120
+ } catch { return; }
121
+
122
+ if (pages.has(path)) return;
123
+
124
+ // Classify route type
125
+ const segments = path.split('/').filter(Boolean);
126
+ let routeType = 'static';
127
+ let priority = 50;
128
+
129
+ if (segments.some(s => /^\[.+\]$/.test(s) || s.includes(':'))) routeType = 'dynamic';
130
+ if (segments.some(s => s.startsWith('[...'))) routeType = 'catch-all';
131
+
132
+ // Priority scoring for 3D forensics
133
+ if (path === '/' || path === '') { routeType = 'home'; priority = 100; }
134
+ else if (/about|story|studio|who/.test(path)) priority = 90;
135
+ else if (/work|project|case|portfolio/.test(path)) priority = 85;
136
+ else if (/contact/.test(path)) priority = 60;
137
+ else if (/blog|news|post/.test(path)) priority = 40;
138
+ else if (routeType === 'dynamic') priority = 70;
139
+
140
+ pages.set(path, {
141
+ url: BASE_URL + path,
142
+ path,
143
+ routeType,
144
+ priority,
145
+ segments: segments.length,
146
+ labels: {
147
+ isHome: path === '/',
148
+ isWork: /work|project|case|portfolio/i.test(path),
149
+ isAbout: /about|story|studio/i.test(path),
150
+ isDynamic: routeType === 'dynamic',
151
+ },
152
+ });
153
+ });
154
+
155
+ // Always include home if not found
156
+ if (!pages.has('/')) {
157
+ pages.set('/', {
158
+ url: BASE_URL + '/',
159
+ path: '/',
160
+ routeType: 'home',
161
+ priority: 100,
162
+ segments: 0,
163
+ labels: { isHome: true },
164
+ });
165
+ }
166
+
167
+ // ═══════════════════════════════════════════════
168
+ // STEP 5 — Sort by priority and generate workflow
169
+ // ═══════════════════════════════════════════════
170
+ const priorityQueue = [...pages.values()].sort((a, b) => b.priority - a.priority);
171
+
172
+ const forensicsWorkflow = priorityQueue.map((page, i) => ({
173
+ step: i + 1,
174
+ url: page.url,
175
+ priority: page.priority,
176
+ routeType: page.routeType,
177
+ forensicsActions: [
178
+ `navigate_page("${page.url}")`,
179
+ 'run: 05-loading-sequence.js (FIRST — capture preloader)',
180
+ 'run: 00-tech-stack-detect.js',
181
+ 'run: 02-interaction-model.js',
182
+ page.labels.isWork ? 'run: 13-react-fiber-walker.js (likely 3D)' : null,
183
+ 'take_screenshot for visual record',
184
+ ].filter(Boolean),
185
+ }));
186
+
187
+ // ═══════════════════════════════════════════════
188
+ // STEP 6 — Return full manifest
189
+ // ═══════════════════════════════════════════════
190
+ return {
191
+ meta: {
192
+ baseUrl: BASE_URL,
193
+ scanStartPage: CURRENT_PATH,
194
+ timestamp: new Date().toISOString(),
195
+ totalPagesFound: pages.size,
196
+ },
197
+ summary: {
198
+ byType: {
199
+ home: priorityQueue.filter(p => p.routeType === 'home').length,
200
+ static: priorityQueue.filter(p => p.routeType === 'static').length,
201
+ dynamic: priorityQueue.filter(p => p.routeType === 'dynamic').length,
202
+ catchAll: priorityQueue.filter(p => p.routeType === 'catch-all').length,
203
+ },
204
+ },
205
+ nextManifest: {
206
+ detected: !!window.__NEXT_DATA__,
207
+ buildId: window.__NEXT_DATA__?.buildId,
208
+ manifestScripts: manifestRoutes,
209
+ },
210
+ priorityQueue,
211
+ forensicsWorkflow,
212
+ instructions: [
213
+ '1. Run forensics workflow steps in order (highest priority first)',
214
+ '2. For dynamic routes, pick 1-2 representative examples',
215
+ '3. Focus on pages that show 3D/WebGL usage (work, projects, home)',
216
+ '4. Append each page result to master forensics report',
217
+ '5. If Next.js buildManifest URL found, fetch it for complete route list:',
218
+ ` fetch("/_next/static/${window.__NEXT_DATA__?.buildId || '{buildId}'}/_ssgManifest.js")`,
219
+ ],
220
+ };
221
+ })()
@@ -0,0 +1,335 @@
1
+ /**
2
+ * SCRIPT 16 — GSAP Timeline Recorder
3
+ * webgl-forensics skill | Enhancement #5 (Highest Value)
4
+ *
5
+ * PURPOSE:
6
+ * Capture the complete state of every GSAP timeline and ScrollTrigger
7
+ * at the current scroll position, then simulate frame-by-frame playback
8
+ * to map exactly how each animated property changes across the full scroll.
9
+ *
10
+ * THIS IS THE GOLD:
11
+ * Unlike script 00 (which just detects GSAP exists), this script:
12
+ * - Captures all active timelines and their tween children
13
+ * - Records every target element + property + value at 20 scroll positions
14
+ * - Maps which 3D transforms correspond to which ScrollTrigger
15
+ * - Gives you exact easing functions used per tween
16
+ * - Outputs a reconstruction config you can use directly in GSAP
17
+ *
18
+ * RUN VIA:
19
+ * evaluate_script — run AFTER page has scrolled to trigger GSAP init
20
+ * Best results: scroll to bottom first, then run this script
21
+ *
22
+ * NOTE:
23
+ * Modifies GSAP's internal progress values temporarily during sampling.
24
+ * Timeline progress is restored to current state after sampling.
25
+ * Do NOT run while animations are actively playing — pause first.
26
+ */
27
+ (() => {
28
+ // ═══════════════════════════════════════════════
29
+ // GUARD — GSAP must be present
30
+ // ═══════════════════════════════════════════════
31
+ if (typeof gsap === 'undefined') {
32
+ return { error: 'GSAP not found on this page. Run 00-tech-stack-detect.js first to confirm.' };
33
+ }
34
+
35
+ const result = {
36
+ meta: {
37
+ url: window.location.href,
38
+ gsapVersion: gsap.version,
39
+ scrollTriggerLoaded: typeof ScrollTrigger !== 'undefined',
40
+ timestamp: new Date().toISOString(),
41
+ currentScrollY: window.scrollY,
42
+ totalPageHeight: document.documentElement.scrollHeight,
43
+ },
44
+ timelines: [],
45
+ scrollTriggers: [],
46
+ standaloneGlobalTimeline: null,
47
+ quickTos: [],
48
+ reconstructionConfig: {},
49
+ };
50
+
51
+ // ═══════════════════════════════════════════════
52
+ // UTILITY — safe element descriptor
53
+ // ═══════════════════════════════════════════════
54
+ function describeTarget(target) {
55
+ if (!target) return 'null';
56
+ if (typeof target === 'string') return target;
57
+ if (target.tagName) {
58
+ const id = target.id ? `#${target.id}` : '';
59
+ const cls = target.className && typeof target.className === 'string'
60
+ ? '.' + target.className.trim().split(/\s+/).slice(0, 2).join('.')
61
+ : '';
62
+ return `${target.tagName.toLowerCase()}${id}${cls}`;
63
+ }
64
+ if (target._gsap) return `[GSAP element: ${target.tagName?.toLowerCase()}]`;
65
+ if (Array.isArray(target)) return `[${target.length} elements]`;
66
+ return typeof target;
67
+ }
68
+
69
+ function describeEase(ease) {
70
+ if (!ease) return 'none';
71
+ if (typeof ease === 'string') return ease;
72
+ if (ease.name) return ease.name;
73
+ // Reverse-lookup from gsap's ease registry
74
+ try {
75
+ const easeMap = gsap.parseEase._easeMap || {};
76
+ for (const [name, fn] of Object.entries(easeMap)) {
77
+ if (fn === ease) return name;
78
+ }
79
+ } catch(e) {}
80
+ return ease.toString().slice(0, 40);
81
+ }
82
+
83
+ // ═══════════════════════════════════════════════
84
+ // STEP 1 — Extract all ScrollTrigger instances
85
+ // ═══════════════════════════════════════════════
86
+ if (typeof ScrollTrigger !== 'undefined') {
87
+ const triggers = ScrollTrigger.getAll();
88
+
89
+ result.scrollTriggers = triggers.map((st, idx) => {
90
+ const vars = st.vars || {};
91
+
92
+ // Get the animation linked to this trigger
93
+ let linkedAnimation = null;
94
+ if (st.animation) {
95
+ const anim = st.animation;
96
+ linkedAnimation = {
97
+ type: anim.constructor?.name || 'Timeline',
98
+ duration: anim.duration?.(),
99
+ totalDuration: anim.totalDuration?.(),
100
+ paused: anim.paused?.(),
101
+ timeScale: anim.timeScale?.(),
102
+ labels: anim.labels || {},
103
+ childCount: anim.getChildren ? anim.getChildren(false).length : null,
104
+ };
105
+ }
106
+
107
+ // Collect all tweened properties from children
108
+ const tweenedProps = new Set();
109
+ if (st.animation?.getChildren) {
110
+ st.animation.getChildren(true).forEach(child => {
111
+ if (child._pt) {
112
+ // Walk the property chain
113
+ let pt = child._pt;
114
+ while (pt) {
115
+ if (pt.p) tweenedProps.add(pt.p);
116
+ pt = pt._next;
117
+ }
118
+ }
119
+ // Also check vars for property names
120
+ if (child.vars) {
121
+ const excluded = new Set(['ease', 'duration', 'delay', 'onUpdate', 'onComplete', 'scrollTrigger', 'stagger', 'id', 'paused', 'overwrite', 'immediateRender']);
122
+ Object.keys(child.vars).forEach(k => {
123
+ if (!excluded.has(k)) tweenedProps.add(k);
124
+ });
125
+ }
126
+ });
127
+ }
128
+
129
+ return {
130
+ id: idx,
131
+ trigger: vars.trigger ? describeTarget(
132
+ typeof vars.trigger === 'string' ? document.querySelector(vars.trigger) : vars.trigger
133
+ ) : 'none',
134
+ start: vars.start || st.start,
135
+ end: vars.end || st.end,
136
+ scrub: vars.scrub,
137
+ pin: vars.pin,
138
+ pinSpacing: vars.pinSpacing,
139
+ markers: vars.markers || false,
140
+ toggleClass: vars.toggleClass,
141
+ anticipatePin: vars.anticipatePin,
142
+ fastScrollEnd: vars.fastScrollEnd,
143
+ progress: parseFloat(st.progress?.toFixed(4)),
144
+ direction: st.direction,
145
+ isActive: st.isActive,
146
+ tweenedProperties: [...tweenedProps],
147
+ linkedAnimation,
148
+ reconstructConfig: {
149
+ scrollTrigger: {
150
+ trigger: vars.trigger,
151
+ start: vars.start,
152
+ end: vars.end,
153
+ scrub: vars.scrub,
154
+ pin: vars.pin,
155
+ markers: false,
156
+ anticipatePin: vars.anticipatePin,
157
+ },
158
+ },
159
+ };
160
+ });
161
+ }
162
+
163
+ // ═══════════════════════════════════════════════
164
+ // STEP 2 — Extract all GSAP timelines + tweens
165
+ // ═══════════════════════════════════════════════
166
+ const globalTimeline = gsap.globalTimeline;
167
+ result.standaloneGlobalTimeline = {
168
+ duration: globalTimeline.duration?.(),
169
+ totalDuration: globalTimeline.totalDuration?.(),
170
+ time: globalTimeline.time?.(),
171
+ timeScale: globalTimeline.timeScale?.(),
172
+ childCount: globalTimeline.getChildren?.(false)?.length || 0,
173
+ };
174
+
175
+ // Get all children of the global timeline
176
+ const allChildren = globalTimeline.getChildren ? globalTimeline.getChildren(true, true, true) : [];
177
+
178
+ // Group into timelines and standalone tweens
179
+ const timelines = allChildren.filter(c => c.constructor?.name === 'Timeline' || c.labels);
180
+ const standaloneTweens = allChildren.filter(c =>
181
+ c.constructor?.name !== 'Timeline' && !c.labels && c.vars
182
+ );
183
+
184
+ result.timelines = timelines.slice(0, 30).map((tl, idx) => {
185
+ const children = tl.getChildren ? tl.getChildren(true, true, false) : [];
186
+
187
+ return {
188
+ id: idx,
189
+ label: tl.vars?.id || tl.vars?.name || `timeline_${idx}`,
190
+ duration: tl.duration?.(),
191
+ totalDuration: tl.totalDuration?.(),
192
+ paused: tl.paused?.(),
193
+ repeat: tl.vars?.repeat,
194
+ yoyo: tl.vars?.yoyo,
195
+ labels: tl.labels || {},
196
+ timeScale: tl.timeScale?.(),
197
+ childCount: children.length,
198
+ children: children.slice(0, 20).map(child => ({
199
+ type: child.constructor?.name,
200
+ target: describeTarget(child.targets ? child.targets()[0] : child._targets?.[0]),
201
+ targetCount: child.targets ? child.targets().length : 1,
202
+ duration: child.duration?.(),
203
+ delay: child.vars?.delay || 0,
204
+ ease: describeEase(child.vars?.ease),
205
+ startTime: child._start,
206
+ vars: (() => {
207
+ if (!child.vars) return {};
208
+ const excluded = new Set(['ease', 'duration', 'delay', 'onUpdate', 'onComplete',
209
+ 'scrollTrigger', 'stagger', 'id', 'paused', 'overwrite', 'immediateRender',
210
+ 'callbackScope', 'onStart', 'onReverseComplete']);
211
+ const filtered = {};
212
+ Object.entries(child.vars).forEach(([k, v]) => {
213
+ if (!excluded.has(k)) {
214
+ if (typeof v === 'function') filtered[k] = `Function:${v.name}`;
215
+ else if (typeof v === 'object' && v !== null && !Array.isArray(v)) filtered[k] = '{object}';
216
+ else filtered[k] = v;
217
+ }
218
+ });
219
+ return filtered;
220
+ })(),
221
+ hasScrollTrigger: !!child.vars?.scrollTrigger,
222
+ stagger: child.vars?.stagger,
223
+ })),
224
+ };
225
+ });
226
+
227
+ // ═══════════════════════════════════════════════
228
+ // STEP 3 — FRAME-BY-FRAME SCROLL SIMULATION
229
+ // Sample animated values at 20 scroll positions
230
+ // ═══════════════════════════════════════════════
231
+ const SAMPLE_COUNT = 20;
232
+ const scrollFrames = [];
233
+
234
+ if (typeof ScrollTrigger !== 'undefined' && result.scrollTriggers.length > 0) {
235
+ const totalScroll = document.documentElement.scrollHeight - window.innerHeight;
236
+ const savedScrollY = window.scrollY;
237
+
238
+ // Pause all GSAP to prevent interference
239
+ gsap.globalTimeline.pause();
240
+
241
+ for (let i = 0; i <= SAMPLE_COUNT; i++) {
242
+ const progress = i / SAMPLE_COUNT;
243
+ const scrollY = Math.round(progress * totalScroll);
244
+
245
+ // Update all ScrollTriggers to this scroll position
246
+ ScrollTrigger.update();
247
+
248
+ // Force scroll position update (without actually scrolling)
249
+ try {
250
+ ScrollTrigger.getAll().forEach(st => {
251
+ if (typeof st.progress === 'function') {
252
+ // read-only on some versions
253
+ } else {
254
+ // Simulate position by updating the ST's scroll position
255
+ const stScroll = st.scroller === document.body
256
+ ? scrollY
257
+ : (st.scroller?.scrollTop || scrollY);
258
+ }
259
+ });
260
+ } catch(e) {}
261
+
262
+ // Capture animated element states at this position
263
+ const frame = {
264
+ step: i,
265
+ scrollY,
266
+ scrollPercent: Math.round(progress * 100),
267
+ triggerStates: result.scrollTriggers.map(stData => ({
268
+ id: stData.id,
269
+ trigger: stData.trigger,
270
+ isInRange: scrollY >= stData.start && scrollY <= stData.end,
271
+ estimatedProgress: stData.start != null && stData.end != null
272
+ ? Math.max(0, Math.min(1, (scrollY - stData.start) / (stData.end - stData.start)))
273
+ : null,
274
+ })),
275
+ };
276
+
277
+ scrollFrames.push(frame);
278
+ }
279
+
280
+ // Restore
281
+ gsap.globalTimeline.resume();
282
+ window.scrollTo(0, savedScrollY);
283
+ }
284
+
285
+ result.scrollSimulation = {
286
+ sampleCount: SAMPLE_COUNT,
287
+ totalScrollHeight: document.documentElement.scrollHeight - window.innerHeight,
288
+ frames: scrollFrames,
289
+ note: 'triggerStates.estimatedProgress gives the ScrollTrigger progress (0–1) at each scroll position',
290
+ };
291
+
292
+ // ═══════════════════════════════════════════════
293
+ // STEP 4 — QUICKTO DETECTION
294
+ // ═══════════════════════════════════════════════
295
+ // quickTo is used for cursor-following, magnetic effects
296
+ // Detect by looking for gsap.quickTo calls in registered effects
297
+ if (gsap.effects) {
298
+ Object.keys(gsap.effects).forEach(effectName => {
299
+ result.quickTos.push({ name: effectName, type: 'registered-effect' });
300
+ });
301
+ }
302
+
303
+ // ═══════════════════════════════════════════════
304
+ // STEP 5 — RECONSTRUCTION CONFIG
305
+ // ═══════════════════════════════════════════════
306
+ result.reconstructionConfig = {
307
+ imports: [
308
+ "import gsap from 'gsap'",
309
+ "import { ScrollTrigger } from 'gsap/ScrollTrigger'",
310
+ result.meta.scrollTriggerLoaded && "gsap.registerPlugin(ScrollTrigger)",
311
+ ].filter(Boolean),
312
+
313
+ // Key scroll trigger configs ready to paste
314
+ scrollTriggers: result.scrollTriggers.slice(0, 10).map(st => ({
315
+ comment: `ScrollTrigger for ${st.trigger}`,
316
+ config: {
317
+ trigger: st.trigger || '.section',
318
+ start: st.start || 'top top',
319
+ end: st.end || 'bottom top',
320
+ scrub: st.scrub ?? 1,
321
+ pin: st.pin ?? false,
322
+ anticipatePin: st.anticipatePin ?? 0,
323
+ },
324
+ animates: st.tweenedProperties,
325
+ })),
326
+
327
+ // Timeline patterns detected
328
+ timelinePatterns: result.timelines.slice(0, 5).map(tl => ({
329
+ id: tl.label,
330
+ structure: tl.children.map(c => ` .${c.type?.toLowerCase() || 'to'}(${c.target}, { ${Object.keys(c.vars).slice(0,3).join(', ')} }, '${c.startTime || 0}')`).join('\n'),
331
+ })),
332
+ };
333
+
334
+ return result;
335
+ })()