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
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.
|