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.
- package/.github/workflows/forensics.yml +44 -0
- package/CLAUDE_CODE.md +49 -0
- package/MASTER_PROMPT.md +202 -0
- package/README.md +579 -0
- package/SKILL.md +1256 -0
- package/package.json +53 -0
- package/project summary.md +33 -0
- package/puppeteer-runner.js +486 -0
- package/scripts/00-tech-stack-detect.js +185 -0
- package/scripts/01-source-map-extractor.js +73 -0
- package/scripts/02-interaction-model.js +129 -0
- package/scripts/03-responsive-analysis.js +115 -0
- package/scripts/04-page-transitions.js +128 -0
- package/scripts/05-loading-sequence.js +124 -0
- package/scripts/06-audio-extraction.js +115 -0
- package/scripts/07-accessibility-reduced-motion.js +133 -0
- package/scripts/08-complexity-scorer.js +138 -0
- package/scripts/09-visual-diff-validator.js +113 -0
- package/scripts/10-webgpu-extractor.js +248 -0
- package/scripts/11-scroll-screenshot-grid.js +76 -0
- package/scripts/12-network-waterfall.js +151 -0
- package/scripts/13-react-fiber-walker.js +285 -0
- package/scripts/14-shader-hotpatch.js +349 -0
- package/scripts/15-multipage-crawler.js +221 -0
- package/scripts/16-gsap-timeline-recorder.js +335 -0
- package/scripts/17-r3f-fiber-serializer.js +316 -0
- package/scripts/18-font-extractor.js +290 -0
- package/scripts/19-design-token-export.js +85 -0
- package/scripts/20-timeline-visualizer.js +61 -0
- package/scripts/21-lighthouse-audit.js +35 -0
- package/scripts/22-sitemap-crawler.js +128 -0
- package/scripts/23-scaffold-generator.js +451 -0
- package/scripts/24-ai-reconstruction.js +226 -0
- package/scripts/25-shader-annotator.js +226 -0
- 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
|
+
})()
|