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
package/SKILL.md ADDED
@@ -0,0 +1,1256 @@
1
+ ---
2
+ name: webgl-forensics
3
+ description: "Universal website forensics and cloning skill. Reverse-engineer ANY site regardless of tech stack — extracts 3D scenes (Three.js, Babylon, R3F, custom WebGL/WebGPU), animations (GSAP, Framer Motion, anime.js, Lottie, CSS, WAAPI), scroll systems (Lenis, Locomotive, ScrollTrigger, native), responsive/breakpoint behavior, page transitions (Barba.js, Swup, View Transitions), loading sequences, audio systems, accessibility/reduced-motion patterns, and all assets. Auto-scores rebuild complexity (0-100) and produces a diff-validated reconstruction blueprint. Can scaffold the full clone in Next.js, Vite, or Astro."
4
+ category: reverse-engineering
5
+ risk: medium
6
+ source: custom
7
+ tags: [cloning, reverse-engineering, webgl, three.js, gsap, animations, scroll, design-system, forensics, chrome-devtools, responsive, page-transitions, audio, accessibility, complexity-scoring]
8
+ date_added: "2026-04-02"
9
+ version: "3.0"
10
+ ---
11
+
12
+ # WebGL Forensics — Universal Site Cloner
13
+
14
+ Reverse-engineer **any** website into a complete reconstruction blueprint, regardless of the underlying technology.
15
+ Extracts everything: 3D scenes, shaders, animations, scroll interactions, layout systems, design tokens, fonts, assets, and interaction models.
16
+
17
+ ---
18
+
19
+ ## When to Use
20
+
21
+ - User provides a URL and wants to understand or recreate its effects
22
+ - "Reverse engineer this site"
23
+ - "Clone the animations from [url]"
24
+ - "How does this site do [effect]?"
25
+ - "Extract the 3D / scroll / animations from [url]"
26
+ - User wants a reconstruction blueprint for any website
27
+ - User wants to analyze a competitor's implementation
28
+
29
+ ## Do Not Use When
30
+
31
+ - Target requires authentication (login walls) — use manual browser instead
32
+ - User wants a pixel-perfect screenshot-to-HTML conversion only (use `web-clone` skill)
33
+ - User explicitly says "don't analyze, just build something similar"
34
+
35
+ ## Prerequisites
36
+
37
+ **Required**: Chrome DevTools MCP access.
38
+ - Claude Code: `claude --chrome`
39
+ - Antigravity: Chrome DevTools MCP server must be connected
40
+
41
+ **Required MCP Tools**:
42
+ | Tool | Purpose |
43
+ |------|---------|
44
+ | `navigate_page` | Load target, scroll to positions |
45
+ | `evaluate_script` | Runtime extraction (the core of everything) |
46
+ | `take_screenshot` | Visual state capture at scroll breakpoints |
47
+ | `take_snapshot` | DOM/a11y tree analysis |
48
+ | `list_network_requests` | Intercept asset loads |
49
+ | `get_network_request` | Download specific assets |
50
+
51
+ **Optional Chrome Extensions** (enhance depth):
52
+ - Spector.js — WebGL frame capture
53
+ - Three.js DevTools — live scene graph
54
+ - GSAP DevTools — timeline visualization
55
+ - React DevTools — component tree for R3F/React sites
56
+ - Vue DevTools — component tree for Vue/Nuxt sites
57
+
58
+ ## Inputs
59
+
60
+ | Param | Required | Default | Description |
61
+ |-------|----------|---------|-------------|
62
+ | `target_url` | ✅ | — | URL to reverse-engineer |
63
+ | `output_path` | ❌ | `docs/research/{domain}-forensics.md` | Report destination |
64
+ | `focus` | ❌ | `all` | Narrow scope: `3d`, `animations`, `scroll`, `layout`, `assets`, `all` |
65
+ | `clone` | ❌ | `false` | If `true`, scaffold a working project after analysis |
66
+ | `clone_framework` | ❌ | `next` | Target framework for clone: `next`, `vite`, `astro`, `vanilla` |
67
+
68
+ ---
69
+
70
+ ## PHASE 0: UNIVERSAL TECH STACK DETECTION
71
+
72
+ > Run this FIRST on every site. The results determine which subsequent phases to run.
73
+
74
+ Execute via `evaluate_script`:
75
+
76
+ ```javascript
77
+ (() => {
78
+ const report = {
79
+ url: window.location.href,
80
+ timestamp: new Date().toISOString(),
81
+ framework: null,
82
+ rendering: { webgl: false, webgpu: false, canvas2d: false, svg: false, css3d: false },
83
+ libraries: {},
84
+ animations: {},
85
+ scroll: {},
86
+ fonts: [],
87
+ meta: {},
88
+ };
89
+
90
+ // ═══ FRAMEWORK DETECTION ═══
91
+ if (window.__NEXT_DATA__) report.framework = { name: 'Next.js', version: window.__NEXT_DATA__?.buildId ? 'detected' : null, router: 'app' };
92
+ else if (window.__NUXT__) report.framework = { name: 'Nuxt', version: window.__NUXT__?.config?.public?.version || 'detected' };
93
+ else if (document.querySelector('[data-reactroot]') || document.querySelector('#__next')) report.framework = { name: 'React', version: 'detected' };
94
+ else if (document.querySelector('[data-v-]')) report.framework = { name: 'Vue', version: 'detected' };
95
+ else if (document.querySelector('[data-svelte-h]') || window.__svelte) report.framework = { name: 'Svelte/SvelteKit', version: 'detected' };
96
+ else if (window.ng || document.querySelector('[ng-version]')) report.framework = { name: 'Angular', version: document.querySelector('[ng-version]')?.getAttribute('ng-version') || 'detected' };
97
+ else if (document.querySelector('[data-astro-cid]') || document.querySelector('astro-island')) report.framework = { name: 'Astro', version: 'detected' };
98
+ else if (document.querySelector('meta[name="generator"][content*="Gatsby"]')) report.framework = { name: 'Gatsby', version: 'detected' };
99
+ else if (document.querySelector('meta[name="generator"][content*="WordPress"]')) report.framework = { name: 'WordPress', version: 'detected' };
100
+ else if (document.querySelector('meta[name="generator"][content*="Webflow"]')) report.framework = { name: 'Webflow', version: 'detected' };
101
+ else if (document.querySelector('meta[name="generator"][content*="Framer"]') || window.__framer) report.framework = { name: 'Framer', version: 'detected' };
102
+ else if (document.querySelector('meta[name="generator"][content*="Squarespace"]')) report.framework = { name: 'Squarespace', version: 'detected' };
103
+ else if (document.querySelector('meta[name="generator"][content*="Wix"]') || window.wixBiSession) report.framework = { name: 'Wix', version: 'detected' };
104
+ else report.framework = { name: 'Unknown/Vanilla', version: null };
105
+
106
+ // ═══ 3D / RENDERING DETECTION ═══
107
+ const canvases = [...document.querySelectorAll('canvas')];
108
+ canvases.forEach(c => {
109
+ const gl2 = c.getContext('webgl2', { failIfMajorPerformanceCaveat: true });
110
+ const gl = gl2 || c.getContext('webgl');
111
+ const gpu = navigator.gpu ? true : false;
112
+ if (gl) report.rendering.webgl = { version: gl2 ? 2 : 1, renderer: gl.getParameter(gl.RENDERER) };
113
+ if (c.getContext('2d')) report.rendering.canvas2d = true;
114
+ if (gpu) report.rendering.webgpu = true;
115
+ });
116
+ if (document.querySelectorAll('svg').length > 3) report.rendering.svg = true;
117
+ if (getComputedStyle(document.body).perspective !== 'none' || document.querySelector('[style*="transform-style: preserve-3d"]') || document.querySelector('[style*="perspective"]'))
118
+ report.rendering.css3d = true;
119
+
120
+ // Three.js
121
+ if (window.THREE) report.libraries.three = { version: window.THREE.REVISION, source: 'global' };
122
+ else {
123
+ const threeScripts = [...document.querySelectorAll('script[src]')].filter(s => /three/i.test(s.src));
124
+ if (threeScripts.length) report.libraries.three = { version: 'bundled', source: threeScripts[0].src };
125
+ // Check for R3F markers
126
+ canvases.forEach(c => { if (c.__r$) report.libraries.r3f = true; });
127
+ }
128
+ // Babylon.js
129
+ if (window.BABYLON) report.libraries.babylon = { version: window.BABYLON.Engine?.Version || 'detected' };
130
+ // PlayCanvas
131
+ if (window.pc) report.libraries.playcanvas = { version: window.pc.version || 'detected' };
132
+ // A-Frame
133
+ if (window.AFRAME) report.libraries.aframe = { version: window.AFRAME.version || 'detected' };
134
+ // Unity WebGL
135
+ if (window.unityInstance || document.querySelector('[data-unity-loader]') || document.querySelector('#unity-container'))
136
+ report.libraries.unity = { version: 'detected' };
137
+ // PixiJS
138
+ if (window.PIXI) report.libraries.pixi = { version: window.PIXI.VERSION || 'detected' };
139
+ // p5.js
140
+ if (window.p5) report.libraries.p5 = { version: 'detected' };
141
+ // Spline
142
+ if (document.querySelector('spline-viewer') || window.__spline) report.libraries.spline = true;
143
+
144
+ // ═══ ANIMATION LIBRARY DETECTION ═══
145
+ if (window.gsap) report.animations.gsap = { version: window.gsap.version };
146
+ if (window.ScrollTrigger) report.animations.scrollTrigger = true;
147
+ if (window.ScrollSmoother) report.animations.scrollSmoother = true;
148
+ // Framer Motion (React)
149
+ if (document.querySelector('[data-framer-appear-id]') || document.querySelector('[style*="--framer"]'))
150
+ report.animations.framerMotion = true;
151
+ // anime.js
152
+ if (window.anime) report.animations.anime = { version: window.anime.version || 'detected' };
153
+ // Lottie
154
+ if (window.lottie || document.querySelector('lottie-player') || document.querySelector('[data-lottie]'))
155
+ report.animations.lottie = true;
156
+ // Rive
157
+ if (window.rive || document.querySelector('canvas[data-rive]') || document.querySelector('rive-canvas'))
158
+ report.animations.rive = true;
159
+ // Web Animations API usage
160
+ const waapi = document.getAnimations?.();
161
+ if (waapi?.length > 0) report.animations.waapi = { count: waapi.length };
162
+ // CSS animations count
163
+ const allEls = document.querySelectorAll('*');
164
+ let cssAnimCount = 0;
165
+ let cssTransitionCount = 0;
166
+ const sample = Math.min(allEls.length, 200); // sample first 200
167
+ for (let i = 0; i < sample; i++) {
168
+ const s = getComputedStyle(allEls[i]);
169
+ if (s.animationName !== 'none') cssAnimCount++;
170
+ if (s.transitionProperty !== 'all' && s.transitionProperty !== 'none' && s.transitionDuration !== '0s') cssTransitionCount++;
171
+ }
172
+ if (cssAnimCount > 0) report.animations.cssAnimations = cssAnimCount;
173
+ if (cssTransitionCount > 0) report.animations.cssTransitions = cssTransitionCount;
174
+ // Motion One
175
+ if (window.Motion || window.motion) report.animations.motionOne = true;
176
+ // Popmotion
177
+ if (window.popmotion) report.animations.popmotion = true;
178
+
179
+ // ═══ SCROLL LIBRARY DETECTION ═══
180
+ if (window.__lenis || document.querySelector('[data-lenis-prevent]') || document.querySelector('.lenis'))
181
+ report.scroll.lenis = true;
182
+ if (window.LocomotiveScroll || document.querySelector('[data-scroll-container]'))
183
+ report.scroll.locomotive = true;
184
+ if (window.ScrollMagic) report.scroll.scrollMagic = true;
185
+ // Native scroll-driven animations (CSS)
186
+ const hasScrollTimeline = CSS?.supports?.('animation-timeline', 'scroll()');
187
+ if (hasScrollTimeline) report.scroll.nativeScrollDriven = true;
188
+ // Smooth scroll detection
189
+ const htmlStyle = getComputedStyle(document.documentElement);
190
+ if (htmlStyle.scrollBehavior === 'smooth') report.scroll.smoothScroll = 'css';
191
+
192
+ // ═══ FONT DETECTION ═══
193
+ try {
194
+ const fonts = new Set();
195
+ document.fonts.forEach(f => fonts.add(`${f.family} (${f.weight}, ${f.style})`));
196
+ report.fonts = [...fonts];
197
+ } catch(e) { report.fonts = ['detection failed']; }
198
+
199
+ // ═══ CSS FRAMEWORK DETECTION ═══
200
+ const sheets = [...document.styleSheets];
201
+ const firstSheetText = (() => { try { return sheets[0]?.cssRules?.[0]?.cssText || ''; } catch(e) { return ''; }})();
202
+ if (document.querySelector('[class*="tw-"]') || document.querySelector('[class*="bg-"]') || firstSheetText.includes('--tw-'))
203
+ report.libraries.tailwind = true;
204
+ if (document.querySelector('.chakra-ui') || document.querySelector('[data-chakra-component]'))
205
+ report.libraries.chakra = true;
206
+ if (document.querySelector('[class*="MuiButton"]') || document.querySelector('[class*="css-"][class*="Mui"]'))
207
+ report.libraries.mui = true;
208
+ if (document.querySelector('[class*="mantine-"]')) report.libraries.mantine = true;
209
+
210
+ // ═══ META / HEAD ═══
211
+ report.meta.title = document.title;
212
+ report.meta.description = document.querySelector('meta[name="description"]')?.content;
213
+ report.meta.viewport = document.querySelector('meta[name="viewport"]')?.content;
214
+ report.meta.ogImage = document.querySelector('meta[property="og:image"]')?.content;
215
+ report.meta.charset = document.characterSet;
216
+ report.meta.lang = document.documentElement.lang;
217
+
218
+ // ═══ SCRIPT INVENTORY ═══
219
+ report.scripts = [...document.querySelectorAll('script[src]')].map(s => s.src)
220
+ .filter(s => !/gtag|analytics|pixel|facebook|hotjar|clarity|cdn\.cookie/i.test(s))
221
+ .slice(0, 30);
222
+
223
+ return report;
224
+ })()
225
+ ```
226
+
227
+ **Decision Tree** — Based on detection results, run the appropriate phases:
228
+
229
+ | Detection | Phase to Run |
230
+ |-----------|-------------|
231
+ | `rendering.webgl` or any 3D library | → Phase 1 (3D Forensics) |
232
+ | `animations.gsap` or `animations.scrollTrigger` | → Phase 2A (GSAP Extraction) |
233
+ | `animations.framerMotion` | → Phase 2B (Framer Motion Extraction) |
234
+ | `animations.anime` | → Phase 2C (anime.js Extraction) |
235
+ | `animations.lottie` | → Phase 2D (Lottie Extraction) |
236
+ | `animations.rive` | → Phase 2E (Rive Extraction) |
237
+ | `animations.cssAnimations > 0` or `waapi` | → Phase 2F (CSS/WAAPI Extraction) |
238
+ | `scroll.lenis` or `scroll.locomotive` or `scroll.scrollMagic` | → Phase 3 (Scroll System Extraction) |
239
+ | Always | → Phase 4 (Layout & Design Token Extraction) |
240
+ | Always | → Phase 5 (Asset Inventory) |
241
+ | If `clone: true` | → Phase 6 (Project Scaffolding) |
242
+ | **Always — Bonus Phases (run in parallel with above)** | |
243
+ | Always | → Phase 7 (Responsive & Breakpoint Analysis) · script: `03-responsive-analysis.js` |
244
+ | Always | → Phase 8 (Page Transition Extraction) · script: `04-page-transitions.js` |
245
+ | Always, run first | → Phase 9 (Loading Sequence & Preloader) · script: `05-loading-sequence.js` |
246
+ | Always | → Phase 10 (Audio System Extraction) · script: `06-audio-extraction.js` |
247
+ | Always | → Phase 11 (Accessibility & Reduced Motion) · script: `07-accessibility-reduced-motion.js` |
248
+ | Always | → Phase 12 (Scroll Screenshots) · script: `11-scroll-screenshot-grid.js` |
249
+ | Always | → Phase 13 (Network Waterfall) · script: `12-network-waterfall.js` |
250
+ | If R3F detected | → Phase 14 (React Fiber Walker) · script: `13-react-fiber-walker.js` |
251
+ | If Custom WebGL detected | → Phase 15 (Shader Hot-Patch) · script: `14-shader-hotpatch.js` |
252
+ | Always | → Phase 16 (Multi-Page Crawler) · script: `15-multipage-crawler.js` |
253
+ | If GSAP detected | → Phase 17 (GSAP Timeline) · script: `16-gsap-timeline-recorder.js` |
254
+ | If R3F detected | → Phase 18 (R3F Serializer) · script: `17-r3f-fiber-serializer.js` |
255
+ | Always | → Phase 19 (Font Extractor) · script: `18-font-extractor.js` |
256
+ | Always | → Phase 20 (Token Exporter) · script: `19-design-token-export.js` |
257
+ | Always | → Phase 21 (Visual Timelines) · script: `20-timeline-visualizer.js` |
258
+ | **Run LAST** | → Phase 22 (Complexity Score) · script: `08-complexity-scorer.js` |
259
+ | Always | → Phase 23 (Lighthouse Auditing) · script: `21-lighthouse-audit.js` |
260
+ | After clone is built | → Phase 24 (Visual Diff Validation) · script: `09-visual-diff-validator.js` |
261
+
262
+ ---
263
+
264
+ ## PHASE 1: 3D SCENE FORENSICS
265
+
266
+ > Run when ANY WebGL/WebGPU/3D library is detected.
267
+
268
+ ### 1A. Renderer Extraction
269
+
270
+ ```javascript
271
+ (() => {
272
+ const canvases = [...document.querySelectorAll('canvas')];
273
+ return canvases.map((c, i) => {
274
+ const gl = c.getContext('webgl2') || c.getContext('webgl');
275
+ if (!gl) return { index: i, webgl: false };
276
+ return {
277
+ index: i,
278
+ width: c.width, height: c.height,
279
+ pixelRatio: window.devicePixelRatio,
280
+ version: gl.getParameter(gl.VERSION),
281
+ renderer: gl.getParameter(gl.RENDERER),
282
+ vendor: gl.getParameter(gl.VENDOR),
283
+ maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
284
+ extensionCount: gl.getSupportedExtensions()?.length,
285
+ antialias: gl.getContextAttributes()?.antialias,
286
+ alpha: gl.getContextAttributes()?.alpha,
287
+ stencil: gl.getContextAttributes()?.stencil,
288
+ };
289
+ });
290
+ })()
291
+ ```
292
+
293
+ ### 1B. Shader Program Extraction (THE GOLD)
294
+
295
+ > Works with ANY WebGL app — Three.js, Babylon, PlayCanvas, custom.
296
+
297
+ **Strategy**: Hook into the WebGL context to intercept shader programs.
298
+
299
+ ```javascript
300
+ (() => {
301
+ const canvas = document.querySelector('canvas');
302
+ if (!canvas) return { error: 'No canvas found' };
303
+ const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
304
+ if (!gl) return { error: 'No WebGL context' };
305
+
306
+ // Method 1: Enumerate via WEBGL_debug_shaders extension
307
+ const debugExt = gl.getExtension('WEBGL_debug_shaders');
308
+
309
+ // Method 2: Try Three.js renderer cache
310
+ let threePrograms = null;
311
+ const keys = Object.getOwnPropertyNames(canvas);
312
+ for (const key of keys) {
313
+ try {
314
+ if (canvas[key]?.info?.programs) {
315
+ threePrograms = canvas[key].info;
316
+ break;
317
+ }
318
+ } catch(e) {}
319
+ }
320
+
321
+ // Method 3: Babylon.js engine
322
+ let babylonEngine = null;
323
+ if (window.BABYLON) {
324
+ const engines = window.BABYLON.Engine?.Instances;
325
+ if (engines?.length) babylonEngine = engines[0];
326
+ }
327
+
328
+ // Method 4: Walk the prototype chain for any renderer
329
+ const results = {
330
+ debugExtension: !!debugExt,
331
+ threeInfo: threePrograms ? {
332
+ programCount: threePrograms.programs?.length,
333
+ render: threePrograms.render,
334
+ memory: threePrograms.memory,
335
+ } : null,
336
+ babylonEngine: babylonEngine ? {
337
+ version: babylonEngine.name,
338
+ webglVersion: babylonEngine.webGLVersion,
339
+ } : null,
340
+ };
341
+
342
+ return results;
343
+ })()
344
+ ```
345
+
346
+ ### 1C. Three.js Scene Graph (if Three.js detected)
347
+
348
+ ```javascript
349
+ (() => {
350
+ // Multiple strategies to find the scene
351
+ let scene = null;
352
+ let scenePath = '';
353
+
354
+ // Strategy 1: Direct window scan
355
+ for (const key of Object.keys(window)) {
356
+ try {
357
+ if (window[key]?.isScene) { scene = window[key]; scenePath = `window.${key}`; break; }
358
+ if (window[key]?.scene?.isScene) { scene = window[key].scene; scenePath = `window.${key}.scene`; break; }
359
+ } catch(e) {}
360
+ }
361
+
362
+ // Strategy 2: R3F store (React Three Fiber)
363
+ if (!scene) {
364
+ const canvas = document.querySelector('canvas');
365
+ if (canvas) {
366
+ const fiberKey = Object.keys(canvas).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
367
+ if (fiberKey) {
368
+ let fiber = canvas[fiberKey];
369
+ for (let i = 0; i < 20 && fiber; i++) {
370
+ const state = fiber.memoizedState;
371
+ if (state?.memoizedState?.scene?.isScene) {
372
+ scene = state.memoizedState.scene;
373
+ scenePath = 'R3F fiber store';
374
+ break;
375
+ }
376
+ fiber = fiber.return;
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ // Strategy 3: THREE.Cache or global instances
383
+ if (!scene && window.THREE) {
384
+ window.THREE.Cache?.files && Object.values(window.THREE.Cache.files).forEach(f => {
385
+ if (f?.isScene) { scene = f; scenePath = 'THREE.Cache'; }
386
+ });
387
+ }
388
+
389
+ if (!scene) return { error: 'Scene not found. Try Spector.js for WebGL-level extraction.' };
390
+
391
+ const extract = (obj, depth = 0) => {
392
+ if (depth > 8) return '...max depth';
393
+ const data = {
394
+ type: obj.type,
395
+ name: obj.name || undefined,
396
+ uuid: obj.uuid?.slice(0, 8),
397
+ visible: obj.visible,
398
+ position: obj.position ? { x: +obj.position.x.toFixed(3), y: +obj.position.y.toFixed(3), z: +obj.position.z.toFixed(3) } : undefined,
399
+ rotation: obj.rotation ? { x: +obj.rotation.x.toFixed(3), y: +obj.rotation.y.toFixed(3), z: +obj.rotation.z.toFixed(3) } : undefined,
400
+ scale: obj.scale ? { x: +obj.scale.x.toFixed(3), y: +obj.scale.y.toFixed(3), z: +obj.scale.z.toFixed(3) } : undefined,
401
+ };
402
+
403
+ if (obj.isMesh || obj.isPoints || obj.isLine || obj.isSprite) {
404
+ data.geometry = { type: obj.geometry?.type, params: obj.geometry?.parameters };
405
+ if (obj.geometry?.attributes) {
406
+ data.geometry.attributes = Object.keys(obj.geometry.attributes);
407
+ data.geometry.vertexCount = obj.geometry.attributes.position?.count;
408
+ }
409
+ data.material = {
410
+ type: obj.material?.type,
411
+ color: obj.material?.color?.getHexString?.(),
412
+ opacity: obj.material?.opacity,
413
+ transparent: obj.material?.transparent,
414
+ wireframe: obj.material?.wireframe,
415
+ side: obj.material?.side,
416
+ blending: obj.material?.blending,
417
+ };
418
+ // Extract shaders if ShaderMaterial / RawShaderMaterial
419
+ if (obj.material?.isShaderMaterial || obj.material?.isRawShaderMaterial) {
420
+ data.material.vertexShader = obj.material.vertexShader;
421
+ data.material.fragmentShader = obj.material.fragmentShader;
422
+ data.material.uniforms = {};
423
+ if (obj.material.uniforms) {
424
+ for (const [k, u] of Object.entries(obj.material.uniforms)) {
425
+ data.material.uniforms[k] = {
426
+ type: u.value?.constructor?.name || typeof u.value,
427
+ value: typeof u.value === 'number' ? u.value :
428
+ typeof u.value === 'boolean' ? u.value :
429
+ u.value?.isVector2 ? { x: u.value.x, y: u.value.y } :
430
+ u.value?.isVector3 ? { x: u.value.x, y: u.value.y, z: u.value.z } :
431
+ u.value?.isColor ? u.value.getHexString() :
432
+ String(u.value).slice(0, 80)
433
+ };
434
+ }
435
+ }
436
+ }
437
+ // Standard material textures
438
+ ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'envMap', 'aoMap'].forEach(prop => {
439
+ if (obj.material?.[prop]) {
440
+ data.material[prop] = {
441
+ image: obj.material[prop].image?.src?.slice(0, 100) || obj.material[prop].image?.width + 'x' + obj.material[prop].image?.height,
442
+ wrapS: obj.material[prop].wrapS,
443
+ wrapT: obj.material[prop].wrapT,
444
+ };
445
+ }
446
+ });
447
+ }
448
+
449
+ if (obj.isCamera) {
450
+ data.fov = obj.fov; data.near = obj.near; data.far = obj.far; data.aspect = obj.aspect;
451
+ data.zoom = obj.zoom;
452
+ }
453
+ if (obj.isLight) {
454
+ data.color = obj.color?.getHexString(); data.intensity = obj.intensity;
455
+ if (obj.isDirectionalLight || obj.isSpotLight) data.target = obj.target?.position;
456
+ if (obj.isSpotLight) { data.angle = obj.angle; data.penumbra = obj.penumbra; }
457
+ }
458
+
459
+ if (obj.children?.length > 0) {
460
+ data.children = obj.children.map(c => extract(c, depth + 1));
461
+ }
462
+ return data;
463
+ };
464
+
465
+ return { path: scenePath, graph: extract(scene) };
466
+ })()
467
+ ```
468
+
469
+ ### 1D. Babylon.js Scene Graph (if Babylon detected)
470
+
471
+ ```javascript
472
+ (() => {
473
+ if (!window.BABYLON) return { skip: 'No Babylon.js' };
474
+ const engines = window.BABYLON.Engine.Instances;
475
+ if (!engines?.length) return { error: 'No engine instances' };
476
+ const engine = engines[0];
477
+ const scenes = engine.scenes;
478
+ return scenes.map((scene, i) => ({
479
+ index: i,
480
+ meshCount: scene.meshes.length,
481
+ lightCount: scene.lights.length,
482
+ cameraCount: scene.cameras.length,
483
+ materialCount: scene.materials.length,
484
+ meshes: scene.meshes.map(m => ({
485
+ name: m.name, type: m.getClassName(),
486
+ position: { x: m.position.x, y: m.position.y, z: m.position.z },
487
+ material: m.material?.name,
488
+ })),
489
+ activeCam: { name: scene.activeCamera?.name, type: scene.activeCamera?.getClassName() },
490
+ }));
491
+ })()
492
+ ```
493
+
494
+ ### 1E. Post-Processing Detection
495
+
496
+ ```javascript
497
+ (() => {
498
+ // Three.js EffectComposer
499
+ const composers = [];
500
+ for (const key of Object.keys(window)) {
501
+ try {
502
+ const obj = window[key];
503
+ if (obj?.passes) composers.push({ path: key, passes: obj.passes.map(p => p.constructor.name) });
504
+ if (obj?.composer?.passes) composers.push({ path: key + '.composer', passes: obj.composer.passes.map(p => p.constructor.name) });
505
+ } catch(e) {}
506
+ }
507
+
508
+ // Common post-processing effects to search for in bundles
509
+ const knownEffects = ['BloomEffect', 'VignetteEffect', 'NoiseEffect', 'ChromaticAberration',
510
+ 'UnrealBloomPass', 'RenderPass', 'ShaderPass', 'FilmPass', 'GlitchPass', 'SMAAPass',
511
+ 'FXAAShader', 'BokehPass', 'SSAOPass', 'OutlinePass'];
512
+
513
+ return { composers, knownEffects: 'search bundles for these string literals' };
514
+ })()
515
+ ```
516
+
517
+ ---
518
+
519
+ ## PHASE 2: ANIMATION EXTRACTION
520
+
521
+ > Run the sub-phase matching the detected animation library.
522
+
523
+ ### 2A. GSAP Extraction
524
+
525
+ ```javascript
526
+ (() => {
527
+ if (!window.gsap) return { error: 'GSAP not on window' };
528
+
529
+ const result = { version: window.gsap.version, tweens: [], scrollTriggers: [], timelines: [] };
530
+
531
+ // All tweens
532
+ const children = window.gsap.globalTimeline?.getChildren(true, true, true) || [];
533
+ result.tweens = children.slice(0, 100).map((t, i) => ({
534
+ index: i,
535
+ type: t.constructor.name,
536
+ duration: t.duration?.(),
537
+ target: t.targets?.()?.map(el => el.tagName ? el.tagName + (el.className ? '.' + String(el.className).split(' ')[0] : '') : typeof el).join(', ') || 'unknown',
538
+ vars: Object.keys(t.vars || {}).filter(k => !['onComplete', 'onUpdate', 'callbackScope'].includes(k)),
539
+ }));
540
+
541
+ // ScrollTriggers
542
+ try {
543
+ const sts = window.ScrollTrigger?.getAll?.() || [];
544
+ result.scrollTriggers = sts.map((st, i) => ({
545
+ index: i,
546
+ trigger: st.trigger?.tagName + (st.trigger?.id ? '#' + st.trigger.id : '') + (st.trigger?.className ? '.' + String(st.trigger.className).split(' ')[0] : ''),
547
+ start: st.vars?.start,
548
+ end: st.vars?.end,
549
+ scrub: st.vars?.scrub,
550
+ pin: !!st.vars?.pin,
551
+ pinSpacing: st.vars?.pinSpacing,
552
+ anticipatePin: st.vars?.anticipatePin,
553
+ toggleActions: st.vars?.toggleActions,
554
+ snap: st.vars?.snap,
555
+ progress: +st.progress.toFixed(3),
556
+ isActive: st.isActive,
557
+ }));
558
+ } catch(e) { result.scrollTriggerError = e.message; }
559
+
560
+ return result;
561
+ })()
562
+ ```
563
+
564
+ ### 2B. Framer Motion Extraction
565
+
566
+ ```javascript
567
+ (() => {
568
+ // Framer Motion stores animation data in data attributes
569
+ const elements = document.querySelectorAll('[data-framer-appear-id], [style*="--framer"], [data-framer-component-type]');
570
+ return [...elements].map(el => ({
571
+ tag: el.tagName,
572
+ appearId: el.dataset.framerAppearId,
573
+ componentType: el.dataset.framerComponentType,
574
+ style: {
575
+ transform: el.style.transform,
576
+ opacity: el.style.opacity,
577
+ transition: el.style.transition,
578
+ },
579
+ framerVars: [...el.style].filter(p => p.startsWith('--framer')).reduce((acc, p) => {
580
+ acc[p] = el.style.getPropertyValue(p); return acc;
581
+ }, {}),
582
+ }));
583
+ })()
584
+ ```
585
+
586
+ ### 2C. anime.js Extraction
587
+
588
+ ```javascript
589
+ (() => {
590
+ if (!window.anime) return { error: 'anime.js not found' };
591
+ const running = window.anime.running || [];
592
+ return {
593
+ version: window.anime.version || 'detected',
594
+ runningCount: running.length,
595
+ animations: running.map(a => ({
596
+ type: a.type,
597
+ duration: a.duration,
598
+ delay: a.delay,
599
+ easing: a.easing,
600
+ direction: a.direction,
601
+ loop: a.loop,
602
+ targets: a.animatables?.map(t => t.target?.tagName || typeof t.target),
603
+ })),
604
+ };
605
+ })()
606
+ ```
607
+
608
+ ### 2D. Lottie Extraction
609
+
610
+ ```javascript
611
+ (() => {
612
+ // Find Lottie players
613
+ const players = document.querySelectorAll('lottie-player, dotlottie-player, [data-lottie]');
614
+ const iframes = document.querySelectorAll('iframe[src*="lottie"]');
615
+ // Check for bodymovin
616
+ const bodymovin = window.bodymovin || window.lottie;
617
+
618
+ return {
619
+ playerElements: [...players].map(p => ({
620
+ tag: p.tagName,
621
+ src: p.getAttribute('src') || p.dataset.lottie,
622
+ autoplay: p.hasAttribute('autoplay'),
623
+ loop: p.hasAttribute('loop'),
624
+ mode: p.getAttribute('mode'),
625
+ })),
626
+ bodymovin: bodymovin ? {
627
+ version: bodymovin.version || 'detected',
628
+ instances: bodymovin.getRegisteredAnimations?.()?.length || 'unknown',
629
+ } : null,
630
+ iframes: [...iframes].map(f => f.src),
631
+ };
632
+ })()
633
+ ```
634
+
635
+ ### 2E. Rive Extraction
636
+
637
+ ```javascript
638
+ (() => {
639
+ const riveCanvases = document.querySelectorAll('canvas[data-rive], rive-canvas');
640
+ return {
641
+ riveElements: riveCanvases.length,
642
+ globalRive: !!window.rive,
643
+ };
644
+ })()
645
+ ```
646
+
647
+ ### 2F. CSS Animations & Web Animations API Extraction
648
+
649
+ ```javascript
650
+ (() => {
651
+ const animations = document.getAnimations?.() || [];
652
+ return {
653
+ waapi: animations.map(a => ({
654
+ id: a.id || undefined,
655
+ playState: a.playState,
656
+ duration: a.effect?.getTiming?.()?.duration,
657
+ delay: a.effect?.getTiming?.()?.delay,
658
+ easing: a.effect?.getTiming?.()?.easing,
659
+ iterations: a.effect?.getTiming?.()?.iterations,
660
+ target: a.effect?.target?.tagName + (a.effect?.target?.className ? '.' + String(a.effect.target.className).split(' ')[0] : ''),
661
+ keyframes: a.effect?.getKeyframes?.()?.slice(0, 5), // first 5
662
+ })).slice(0, 50),
663
+ cssKeyframes: (() => {
664
+ const rules = [];
665
+ try {
666
+ [...document.styleSheets].forEach(sheet => {
667
+ try {
668
+ [...sheet.cssRules].forEach(rule => {
669
+ if (rule instanceof CSSKeyframesRule) {
670
+ rules.push({ name: rule.name, keyframeCount: rule.cssRules.length });
671
+ }
672
+ });
673
+ } catch(e) {} // CORS
674
+ });
675
+ } catch(e) {}
676
+ return rules;
677
+ })(),
678
+ };
679
+ })()
680
+ ```
681
+
682
+ ---
683
+
684
+ ## PHASE 3: SCROLL SYSTEM EXTRACTION
685
+
686
+ ### 3A. Lenis / Locomotive Config
687
+
688
+ ```javascript
689
+ (() => {
690
+ const result = {};
691
+
692
+ // Lenis
693
+ if (window.__lenis) {
694
+ const l = window.__lenis;
695
+ result.lenis = {
696
+ duration: l.options?.duration,
697
+ easing: l.options?.easing?.toString?.(),
698
+ orientation: l.options?.orientation,
699
+ gestureOrientation: l.options?.gestureOrientation,
700
+ smoothWheel: l.options?.smoothWheel,
701
+ wheelMultiplier: l.options?.wheelMultiplier,
702
+ touchMultiplier: l.options?.touchMultiplier,
703
+ };
704
+ }
705
+
706
+ // Locomotive Scroll
707
+ if (window.LocomotiveScroll) {
708
+ const container = document.querySelector('[data-scroll-container]');
709
+ result.locomotive = {
710
+ container: container ? container.tagName + (container.id ? '#' + container.id : '') : null,
711
+ sections: document.querySelectorAll('[data-scroll-section]').length,
712
+ scrollElements: [...document.querySelectorAll('[data-scroll]')].map(el => ({
713
+ speed: el.dataset.scrollSpeed,
714
+ direction: el.dataset.scrollDirection,
715
+ position: el.dataset.scrollPosition,
716
+ class: el.dataset.scrollClass,
717
+ repeat: el.dataset.scrollRepeat,
718
+ })).slice(0, 30),
719
+ };
720
+ }
721
+
722
+ // Scroll-position metadata
723
+ result.pageHeight = document.documentElement.scrollHeight;
724
+ result.viewportHeight = window.innerHeight;
725
+ result.scrollSections = result.pageHeight / window.innerHeight;
726
+
727
+ return result;
728
+ })()
729
+ ```
730
+
731
+ ### 3B. Scroll-to-State Mapping
732
+
733
+ Programmatically scroll in 10vh increments. At each position:
734
+ 1. Screenshot
735
+ 2. Re-extract active ScrollTrigger states
736
+ 3. Re-extract 3D object positions/uniforms
737
+ 4. Record which CSS classes toggled
738
+
739
+ ```javascript
740
+ // Execute for each scroll position: 0, 10vh, 20vh, ... 100vh, 200vh, etc.
741
+ // Set SCROLL_Y before running:
742
+ (() => {
743
+ const SCROLL_Y = 0; // REPLACE per iteration
744
+ window.scrollTo({ top: SCROLL_Y, behavior: 'instant' });
745
+
746
+ // Wait a frame for renders
747
+ return new Promise(resolve => requestAnimationFrame(() => {
748
+ const state = { scrollY: window.scrollY };
749
+
750
+ // Active ScrollTriggers
751
+ if (window.ScrollTrigger) {
752
+ state.activeTriggers = window.ScrollTrigger.getAll()
753
+ .filter(st => st.isActive)
754
+ .map(st => ({
755
+ trigger: st.trigger?.className?.split(' ')[0],
756
+ progress: +st.progress.toFixed(3),
757
+ }));
758
+ }
759
+
760
+ resolve(state);
761
+ }));
762
+ })()
763
+ ```
764
+
765
+ ---
766
+
767
+ ## PHASE 4: LAYOUT & DESIGN TOKEN EXTRACTION
768
+
769
+ > Always run this phase.
770
+
771
+ ### 4A. Design Tokens
772
+
773
+ ```javascript
774
+ (() => {
775
+ const root = getComputedStyle(document.documentElement);
776
+ const tokens = {};
777
+
778
+ // Extract all CSS custom properties
779
+ const allProps = [];
780
+ try {
781
+ [...document.styleSheets].forEach(sheet => {
782
+ try {
783
+ [...sheet.cssRules].forEach(rule => {
784
+ if (rule.selectorText === ':root' || rule.selectorText === ':host') {
785
+ const style = rule.style;
786
+ for (let i = 0; i < style.length; i++) {
787
+ if (style[i].startsWith('--')) allProps.push({ name: style[i], value: style.getPropertyValue(style[i]).trim() });
788
+ }
789
+ }
790
+ });
791
+ } catch(e) {} // CORS
792
+ });
793
+ } catch(e) {}
794
+
795
+ tokens.cssVariables = allProps;
796
+ tokens.colors = allProps.filter(p => /color|bg|background|accent|primary|secondary|border|text|surface|muted|destructive|foreground/i.test(p.name));
797
+ tokens.spacing = allProps.filter(p => /space|gap|padding|margin|radius|size/i.test(p.name));
798
+ tokens.typography = allProps.filter(p => /font|text|letter|line-height|tracking|leading/i.test(p.name));
799
+
800
+ // Body computed styles
801
+ const body = getComputedStyle(document.body);
802
+ tokens.bodyStyles = {
803
+ backgroundColor: body.backgroundColor,
804
+ color: body.color,
805
+ fontFamily: body.fontFamily,
806
+ fontSize: body.fontSize,
807
+ lineHeight: body.lineHeight,
808
+ };
809
+
810
+ return tokens;
811
+ })()
812
+ ```
813
+
814
+ ### 4B. Layout Structure
815
+
816
+ ```javascript
817
+ (() => {
818
+ // Analyze the main content sections
819
+ const sections = document.querySelectorAll('section, [class*="section"], main > div, [data-section]');
820
+ return {
821
+ sectionCount: sections.length,
822
+ sections: [...sections].map((s, i) => ({
823
+ index: i,
824
+ tag: s.tagName,
825
+ id: s.id || undefined,
826
+ className: s.className?.toString?.().slice(0, 100),
827
+ rect: s.getBoundingClientRect(),
828
+ display: getComputedStyle(s).display,
829
+ position: getComputedStyle(s).position,
830
+ height: s.offsetHeight,
831
+ childCount: s.children.length,
832
+ })),
833
+ nav: (() => {
834
+ const nav = document.querySelector('nav, header, [class*="nav"], [role="navigation"]');
835
+ return nav ? {
836
+ tag: nav.tagName,
837
+ position: getComputedStyle(nav).position,
838
+ links: [...nav.querySelectorAll('a')].map(a => ({ text: a.textContent.trim(), href: a.getAttribute('href') })),
839
+ } : null;
840
+ })(),
841
+ footer: (() => {
842
+ const footer = document.querySelector('footer, [class*="footer"]');
843
+ return footer ? { tag: footer.tagName, height: footer.offsetHeight } : null;
844
+ })(),
845
+ };
846
+ })()
847
+ ```
848
+
849
+ ---
850
+
851
+ ## PHASE 5: ASSET INVENTORY
852
+
853
+ ### 5A. Network Request Audit
854
+
855
+ Use `list_network_requests` to find:
856
+ - JS bundles (`.js`, `.mjs`, `.chunk.js`)
857
+ - 3D models (`.glb`, `.gltf`, `.obj`, `.fbx`, `.draco`)
858
+ - Shaders (`.glsl`, `.vert`, `.frag`, `.wgsl`)
859
+ - Textures (`.hdr`, `.exr`, `.ktx2`, `.basis`)
860
+ - Fonts (`.woff2`, `.woff`, `.ttf`, `.otf`)
861
+ - Images (`.png`, `.jpg`, `.webp`, `.avif`, `.svg`)
862
+ - Videos (`.mp4`, `.webm`)
863
+ - Lottie/Rive (`.json`, `.riv`, `.lottie`)
864
+ - Audio (`.mp3`, `.ogg`, `.wav`)
865
+
866
+ ### 5B. Font Extraction
867
+
868
+ ```javascript
869
+ (() => {
870
+ const fontFaces = [];
871
+ try {
872
+ [...document.styleSheets].forEach(sheet => {
873
+ try {
874
+ [...sheet.cssRules].forEach(rule => {
875
+ if (rule instanceof CSSFontFaceRule) {
876
+ fontFaces.push({
877
+ family: rule.style.fontFamily,
878
+ src: rule.style.src?.slice(0, 200),
879
+ weight: rule.style.fontWeight,
880
+ style: rule.style.fontStyle,
881
+ display: rule.style.fontDisplay,
882
+ });
883
+ }
884
+ });
885
+ } catch(e) {}
886
+ });
887
+ } catch(e) {}
888
+
889
+ // Google Fonts links
890
+ const googleFonts = [...document.querySelectorAll('link[href*="fonts.googleapis.com"]')]
891
+ .map(l => l.href);
892
+
893
+ return { fontFaces, googleFonts, loadedFonts: [...document.fonts].map(f => f.family) };
894
+ })()
895
+ ```
896
+
897
+ ### 5C. Image/Media Extraction
898
+
899
+ ```javascript
900
+ (() => {
901
+ const images = [...document.querySelectorAll('img, picture source, [style*="background-image"]')];
902
+ return images.map(el => ({
903
+ tag: el.tagName,
904
+ src: el.src || el.srcset || (() => {
905
+ const bg = getComputedStyle(el).backgroundImage;
906
+ return bg !== 'none' ? bg.match(/url\("?(.+?)"?\)/)?.[1] : null;
907
+ })(),
908
+ alt: el.alt || undefined,
909
+ width: el.naturalWidth || el.width,
910
+ height: el.naturalHeight || el.height,
911
+ loading: el.loading,
912
+ sizes: el.sizes || undefined,
913
+ })).filter(i => i.src).slice(0, 100);
914
+ })()
915
+ ```
916
+
917
+ ---
918
+
919
+ ## PHASE 6: CLONE SCAFFOLDING (Optional)
920
+
921
+ > Only run if `clone: true`.
922
+
923
+ ### Project Setup
924
+
925
+ Based on `clone_framework`:
926
+
927
+ **Next.js**:
928
+ ```bash
929
+ npx -y create-next-app@latest ./ --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-pnpm
930
+ pnpm add gsap @gsap/react three @types/three @react-three/fiber @react-three/drei
931
+ ```
932
+
933
+ **Vite**:
934
+ ```bash
935
+ npx -y create-vite@latest ./ --template react-ts
936
+ pnpm add gsap three @react-three/fiber @react-three/drei
937
+ ```
938
+
939
+ **Astro**:
940
+ ```bash
941
+ npx -y create-astro@latest ./ --template minimal --typescript strict --install
942
+ npx -y astro add react
943
+ pnpm add gsap three
944
+ ```
945
+
946
+ ### Scaffolding Checklist
947
+
948
+ After project creation, generate:
949
+ 1. Design token CSS file from Phase 4A extraction
950
+ 2. Layout component matching Phase 4B structure
951
+ 3. Animation utility hooks from Phase 2 data
952
+ 4. 3D scene component from Phase 1C scene graph
953
+ 5. Scroll configuration from Phase 3
954
+ 6. Asset directories with downloaded files from Phase 5
955
+
956
+ ---
957
+
958
+ ## PHASE 7: RESPONSIVE & BREAKPOINT ANALYSIS
959
+
960
+ > Run script `scripts/03-responsive-analysis.js` via `evaluate_script` at each breakpoint.
961
+ > Use `resize_page` tool to switch viewports: 375, 768, 1024, 1440, 1920.
962
+
963
+ ```javascript
964
+ // Paste full contents of scripts/03-responsive-analysis.js here
965
+ // or execute directly via evaluate_script with the file contents
966
+ ```
967
+
968
+ **Workflow:**
969
+ 1. `resize_page` → width: 375, height: 812 → run script → save as `breakpoint-mobile.json`
970
+ 2. `resize_page` → width: 768, height: 1024 → run script → save as `breakpoint-tablet.json`
971
+ 3. `resize_page` → width: 1440, height: 900 → run script → save as `breakpoint-desktop.json`
972
+ 4. Compare `visibleCanvases`, `activeAnimations`, `cssBreakpoints` across all three
973
+ 5. Document any mobile kill-switches found in `mobileKillSwitches.elements`
974
+
975
+ **Key outputs to record:**
976
+ - CSS breakpoint values detected
977
+ - Whether 3D/canvas is hidden on mobile (`hiddenCanvases > 0`)
978
+ - Animation count change across viewports
979
+ - Grid column changes
980
+
981
+ ---
982
+
983
+ ## PHASE 8: PAGE TRANSITION EXTRACTION
984
+
985
+ > Run script `scripts/04-page-transitions.js` via `evaluate_script` on the initial page load.
986
+
987
+ **Additional manual steps:**
988
+ 1. Navigate to a second page while monitoring Network panel
989
+ 2. Look for fetch requests to JSON/HTML that indicate SPA routing
990
+ 3. Observe overlay elements in DOM during transition (use take_snapshot before/after navigation)
991
+ 4. Check if `#app` or `#__next` innerHTML changes (React/Vue SPA) or full page reloads
992
+
993
+ **Key outputs to record:**
994
+ - Libraries detected (Barba.js, Swup, View Transitions)
995
+ - Overlay elements with `position: fixed; z-index > 10`
996
+ - GSAP page-level pins
997
+ - View Transition names (for CSS-native transitions)
998
+
999
+ ---
1000
+
1001
+ ## PHASE 9: LOADING SEQUENCE & PRELOADER
1002
+
1003
+ > Run script `scripts/05-loading-sequence.js` via `evaluate_script` immediately after `navigate_page`.
1004
+ > Do NOT scroll before running — you want the initial load state.
1005
+
1006
+ **Manual steps:**
1007
+ 1. Open Network panel → set throttling to "Fast 3G" to observe preloader behavior
1008
+ 2. Take screenshot immediately after load (`take_screenshot`)
1009
+ 3. Run the loading sequence script
1010
+ 4. Take another screenshot after 2–3 seconds to capture preloader exit
1011
+
1012
+ **Key outputs to record:**
1013
+ - Preloader element structure and CSS
1014
+ - Resource hints (`<link rel="preload">` assets)
1015
+ - Heaviest resources (those impacting Time to Interactive)
1016
+ - `firstContentfulPaint` timing
1017
+ - Whether 3D canvas is lazily initialized or eager
1018
+
1019
+ ---
1020
+
1021
+ ## PHASE 10: AUDIO SYSTEM EXTRACTION
1022
+
1023
+ > Run script `scripts/06-audio-extraction.js` via `evaluate_script`.
1024
+ > Note: Browser autoplay policies may block audio until first user gesture.
1025
+
1026
+ **Manual steps:**
1027
+ 1. Run script immediately on load
1028
+ 2. Then click anywhere on the page (user gesture)
1029
+ 3. Run script again — compare `activeContexts` before/after
1030
+ 4. Hover over interactive elements and check the audio tab in DevTools for new activity
1031
+
1032
+ **Key outputs to record:**
1033
+ - Audio libraries present (Howler, Tone.js, Web Audio API)
1034
+ - Active AudioContext count and state
1035
+ - Media elements (ambient video/audio tracks)
1036
+ - Scroll-triggered audio data attributes
1037
+
1038
+ ---
1039
+
1040
+ ## PHASE 11: ACCESSIBILITY & REDUCED MOTION
1041
+
1042
+ > Run script `scripts/07-accessibility-reduced-motion.js` twice:
1043
+ > 1. Normal (DevTools > Rendering > prefers-reduced-motion: no preference)
1044
+ > 2. With reduced motion emulated (DevTools > Rendering > prefers-reduced-motion: reduce)
1045
+
1046
+ **Manual steps:**
1047
+ 1. Desktop DevTools → ⋮ menu → More tools → Rendering → Emulate CSS media features
1048
+ 2. Set `prefers-reduced-motion: reduce`
1049
+ 3. Reload page and observe — do animations stop? Does the 3D scene disappear?
1050
+ 4. Run the script and compare `activeAnimations` count
1051
+ 5. Run Lighthouse accessibility audit for a score
1052
+
1053
+ **Key outputs to record:**
1054
+ - CSS `@media (prefers-reduced-motion)` rule count
1055
+ - Whether GSAP uses `gsap.matchMedia()` for motion handling
1056
+ - ARIA role coverage
1057
+ - Focus management quality
1058
+ - Whether `:focus-visible` CSS is present
1059
+
1060
+ ---
1061
+
1062
+ ## PHASE 12: COMPLEXITY SCORING
1063
+
1064
+ > Run LAST, after all other phases complete.
1065
+ > Uses script `scripts/08-complexity-scorer.js` via `evaluate_script`.
1066
+
1067
+ The scorer returns:
1068
+ - **Score** (0–100)
1069
+ - **Rating**: Simple / Moderate / Complex / Very Complex / Extreme
1070
+ - **Estimated rebuild time**
1071
+ - **Breakdown** by category (3D, Animations, Scroll, Interactions, Transitions, Audio, Framework)
1072
+
1073
+ ```javascript
1074
+ // Paste full contents of scripts/08-complexity-scorer.js
1075
+ // Result example:
1076
+ // { score: 67, rating: 'Very Complex', estimatedRebuildTime: '2-4 weeks', ... }
1077
+ ```
1078
+
1079
+ Record the verdict in the Executive Summary of the reconstruction blueprint.
1080
+
1081
+ ---
1082
+
1083
+ ## PHASE 13: VISUAL DIFF VALIDATION (Clone QA)
1084
+
1085
+ > Run AFTER the clone is built and running locally.
1086
+ > Uses script `scripts/09-visual-diff-validator.js` on BOTH source and clone.
1087
+
1088
+ **Workflow:**
1089
+ 1. Open source site → `evaluate_script` with validator → save JSON as `source-snapshot.json`
1090
+ 2. Open localhost clone → `evaluate_script` with validator → save JSON as `clone-snapshot.json`
1091
+ 3. Compare key values:
1092
+ - `typography.bodyFont` — must match exactly
1093
+ - `layout.sectionCount` — should match ±1
1094
+ - `layout.canvasCount` — must match
1095
+ - `layout.totalPageHeight` — should be within 20%
1096
+ - `animations.scrollTriggerCount` — should be ≥ source
1097
+ - `colorPalette` — compare primary/background colors
1098
+ 4. Take full-page screenshots of both with `take_screenshot` and visually compare
1099
+ 5. Fill in the `passFailTemplate` in the report
1100
+
1101
+ **Pass criteria for "production-ready" clone:**
1102
+ - [ ] Body font matches
1103
+ - [ ] Background color matches
1104
+ - [ ] Section count matches (±1)
1105
+ - [ ] Canvas exists (if source had WebGL)
1106
+ - [ ] Scroll feels smooth (Lenis/GSAP present)
1107
+ - [ ] No console errors
1108
+ - [ ] Mobile layout is responsive
1109
+ - [ ] Lighthouse performance > 80
1110
+
1111
+ ---
1112
+
1113
+ ## OUTPUT: RECONSTRUCTION BLUEPRINT
1114
+
1115
+ Save a comprehensive report to `{output_path}`:
1116
+
1117
+ ```markdown
1118
+ # {domain} — Complete Forensics Report
1119
+ > Generated: {timestamp}
1120
+ > Target: {url}
1121
+
1122
+ ## Executive Summary
1123
+ - Framework: {framework}
1124
+ - 3D Engine: {3d_library}
1125
+ - Animation: {animation_libraries}
1126
+ - Scroll: {scroll_system}
1127
+ - Estimated rebuild complexity: {low|medium|high|extreme}
1128
+
1129
+ ## 1. Tech Stack
1130
+ | Category | Library | Version |
1131
+ |----------|---------|---------|
1132
+
1133
+ ## 2. 3D Scene Architecture
1134
+ {scene graph tree}
1135
+ {camera setup}
1136
+ {lighting rig}
1137
+ {post-processing pipeline}
1138
+
1139
+ ## 3. Extracted Shaders
1140
+ {for each shader: vertex GLSL, fragment GLSL, uniforms table}
1141
+
1142
+ ## 4. Animation System
1143
+ {GSAP timelines / Framer Motion configs / CSS keyframes}
1144
+
1145
+ ## 5. Scroll-to-State Map
1146
+ | Scroll Y | Active Triggers | 3D State | Screenshot |
1147
+ |----------|----------------|----------|------------|
1148
+
1149
+ ## 6. Layout & Design Tokens
1150
+ {CSS variables, typography, spacing, color palette}
1151
+
1152
+ ## 7. Asset Manifest
1153
+ | Asset | Type | URL | Dimensions | Used By |
1154
+ |-------|------|-----|-----------|---------|
1155
+
1156
+ ## 8. Responsive Behavior
1157
+ | Breakpoint | Canvas Visible | Animations Active | Grid Columns | Mobile Kill-Switches |
1158
+ |------------|---------------|-------------------|-------------|----------------------|
1159
+
1160
+ ## 9. Page Transitions
1161
+ - System: {barba/swup/view-transitions/custom/none}
1162
+ - Overlay elements: {count and CSS}
1163
+ - GSAP wipe timelines: {description}
1164
+
1165
+ ## 10. Loading Sequence
1166
+ - Preloader: {yes/no, CSS structure}
1167
+ - FCP: {ms}
1168
+ - Heaviest resources: {list}
1169
+ - 3D init strategy: {eager/lazy/intersection-observer}
1170
+
1171
+ ## 11. Audio
1172
+ - System: {none/howler/tone/web-audio-raw}
1173
+ - Triggered by: {scroll/hover/auto/user-gesture}
1174
+
1175
+ ## 12. Accessibility
1176
+ - Reduced motion CSS rules: {count}
1177
+ - GSAP matchMedia: {yes/no}
1178
+ - Focus visible: {yes/no}
1179
+
1180
+ ## 13. Complexity Score
1181
+ - **Score: {X}/100 — {Rating}**
1182
+ - Estimated rebuild: {time}
1183
+ - Breakdown: {per-category scores}
1184
+
1185
+ ## 14. Reproduction Steps
1186
+ {step-by-step rebuild instructions for the chosen framework}
1187
+
1188
+ ## 15. Clone Validation Checklist
1189
+ - [ ] Body font matches
1190
+ - [ ] Background color matches
1191
+ - [ ] Section count matches
1192
+ - [ ] Canvas present (if source used WebGL)
1193
+ - [ ] Scroll system implemented
1194
+ - [ ] No console errors
1195
+ - [ ] Mobile responsive
1196
+ - [ ] Lighthouse performance > 80
1197
+ ```
1198
+
1199
+ ---
1200
+
1201
+ ## ERROR HANDLING
1202
+
1203
+ | Issue | Resolution |
1204
+ |-------|------------|
1205
+ | No canvas found | Site may lazy-load. Scroll to trigger, wait 3s, retry |
1206
+ | No WebGL context | Canvas is 2D-only. Skip to CSS animation extraction |
1207
+ | Scene not on window | Try R3F Fiber walk, prototype scan, `performance.getEntries()` |
1208
+ | Minified shaders | Extract raw GLSL anyway — valid code even with mangled names |
1209
+ | GSAP not on window | Search via bundle analysis |
1210
+ | CORS on stylesheets | Extract inline styles and computed styles instead |
1211
+ | Evaluation timeout | Break scripts into smaller chunks; extract one thing at a time |
1212
+ | Dynamic/lazy content | Scroll entire page first, then re-run detection scripts |
1213
+ | Audio context suspended | Click page once (user gesture) then re-run `06-audio-extraction.js` |
1214
+ | Breakpoint changes not visible | Reload after `resize_page` — some sites SSR different markup per breakpoint |
1215
+ | Complexity score seems off | Manual override: add +15 to score for any custom GLSL shaders found |
1216
+ | Clone validation mismatch | Acceptable: ±1 section, ±20% page height, different scroll trigger count from deferred loading |
1217
+
1218
+ ## EXECUTION RULES
1219
+
1220
+ 1. **Run Phase 0 first, always.** Results determine which phases run.
1221
+ 2. **Run Phase 9 (Loading) immediately after navigate_page** — before any scrolling.
1222
+ 3. **Be exhaustive.** Extract everything. Don't skip steps.
1223
+ 4. **Prioritize shaders** — they're the DNA of 3D effects and can't be faked.
1224
+ 5. **Screenshot every major scroll state** for visual reference.
1225
+ 6. **If a script fails**, try alternate property paths, prototype chain walking, `globalThis` scanning.
1226
+ 7. **Beautify minified code** and annotate behavior.
1227
+ 8. **Run Phase 12 (Complexity Scorer) last** to cap the session with a clear rebuild estimate.
1228
+ 9. **Run Phase 13 (Validation) on the clone** — never ship without a diff check.
1229
+ 10. **Save the report** before concluding. Don't lose findings.
1230
+
1231
+ ## SCRIPTS DIRECTORY
1232
+
1233
+ All extraction scripts are in `~/.gemini/antigravity/skills/webgl-forensics/scripts/`:
1234
+
1235
+ | Script | Phase | Purpose |
1236
+ |--------|-------|--------|
1237
+ | `00-tech-stack-detect.js` | Phase 0 | Full tech stack auto-detection |
1238
+ | `01-source-map-extractor.js` | Phase 1 | Source map & bundle analysis |
1239
+ | `02-interaction-model.js` | Phase 2 | Magnetic cursors, parallax, touch |
1240
+ | `03-responsive-analysis.js` | Phase 7 | Breakpoint behavior at each viewport |
1241
+ | `04-page-transitions.js` | Phase 8 | Barba.js, Swup, View Transitions |
1242
+ | `05-loading-sequence.js` | Phase 9 | Preloader, resource hints, perf budget |
1243
+ | `06-audio-extraction.js` | Phase 10 | Web Audio API, Howler, Tone.js |
1244
+ | `07-accessibility-reduced-motion.js` | Phase 11 | ARIA, reduced motion CSS/JS |
1245
+ | `08-complexity-scorer.js` | Phase 12 | 0–100 rebuild difficulty score |
1246
+ | `09-visual-diff-validator.js` | Phase 13 | Clone-vs-source fingerprint comparison |
1247
+
1248
+ ## LEGAL NOTICE
1249
+
1250
+ This skill is intended for:
1251
+ - Learning how production effects are implemented
1252
+ - Migrating your own sites to new stacks
1253
+ - Academic research and technical education
1254
+ - Creating inspired-by (not copied) experiences
1255
+
1256
+ Do NOT use extracted shaders, models, or proprietary assets in commercial projects without proper licensing.