webgl-forensics 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.github/workflows/forensics.yml +44 -0
  2. package/CLAUDE_CODE.md +49 -0
  3. package/MASTER_PROMPT.md +202 -0
  4. package/README.md +579 -0
  5. package/SKILL.md +1256 -0
  6. package/package.json +53 -0
  7. package/project summary.md +33 -0
  8. package/puppeteer-runner.js +486 -0
  9. package/scripts/00-tech-stack-detect.js +185 -0
  10. package/scripts/01-source-map-extractor.js +73 -0
  11. package/scripts/02-interaction-model.js +129 -0
  12. package/scripts/03-responsive-analysis.js +115 -0
  13. package/scripts/04-page-transitions.js +128 -0
  14. package/scripts/05-loading-sequence.js +124 -0
  15. package/scripts/06-audio-extraction.js +115 -0
  16. package/scripts/07-accessibility-reduced-motion.js +133 -0
  17. package/scripts/08-complexity-scorer.js +138 -0
  18. package/scripts/09-visual-diff-validator.js +113 -0
  19. package/scripts/10-webgpu-extractor.js +248 -0
  20. package/scripts/11-scroll-screenshot-grid.js +76 -0
  21. package/scripts/12-network-waterfall.js +151 -0
  22. package/scripts/13-react-fiber-walker.js +285 -0
  23. package/scripts/14-shader-hotpatch.js +349 -0
  24. package/scripts/15-multipage-crawler.js +221 -0
  25. package/scripts/16-gsap-timeline-recorder.js +335 -0
  26. package/scripts/17-r3f-fiber-serializer.js +316 -0
  27. package/scripts/18-font-extractor.js +290 -0
  28. package/scripts/19-design-token-export.js +85 -0
  29. package/scripts/20-timeline-visualizer.js +61 -0
  30. package/scripts/21-lighthouse-audit.js +35 -0
  31. package/scripts/22-sitemap-crawler.js +128 -0
  32. package/scripts/23-scaffold-generator.js +451 -0
  33. package/scripts/24-ai-reconstruction.js +226 -0
  34. package/scripts/25-shader-annotator.js +226 -0
  35. package/tui.js +196 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * SCRIPT 00 — Universal Tech Stack Detection
3
+ * webgl-forensics skill | Phase 0
4
+ *
5
+ * Run via: evaluate_script (paste entire IIFE)
6
+ * Returns: full tech stack report object
7
+ */
8
+ (() => {
9
+ const report = {
10
+ url: window.location.href,
11
+ timestamp: new Date().toISOString(),
12
+ framework: null,
13
+ rendering: { webgl: false, webgpu: false, canvas2d: false, svg: false, css3d: false },
14
+ libraries: {},
15
+ animations: {},
16
+ scroll: {},
17
+ pageTransitions: {},
18
+ audio: {},
19
+ accessibility: {},
20
+ fonts: [],
21
+ meta: {},
22
+ scripts: [],
23
+ };
24
+
25
+ // ═══ FRAMEWORK DETECTION ═══
26
+ if (window.__NEXT_DATA__) {
27
+ report.framework = { name: 'Next.js', router: window.__NEXT_DATA__?.page ? 'pages' : 'app', buildId: window.__NEXT_DATA__?.buildId };
28
+ } else if (window.__NUXT__) {
29
+ report.framework = { name: 'Nuxt', version: window.__NUXT__?.config?.public?.version || 'detected' };
30
+ } else if (window.Remix || document.querySelector('[data-remix-route]')) {
31
+ report.framework = { name: 'Remix', version: 'detected' };
32
+ } else if (document.querySelector('[data-reactroot]') || document.querySelector('#__next') || document.querySelector('#root[data-reactroot]')) {
33
+ report.framework = { name: 'React', version: window.React?.version || 'detected' };
34
+ } else if (document.querySelector('[data-v-]') || window.Vue) {
35
+ report.framework = { name: 'Vue', version: window.Vue?.version || 'detected' };
36
+ } else if (document.querySelector('[data-svelte-h]') || window.__svelte) {
37
+ report.framework = { name: 'Svelte/SvelteKit', version: 'detected' };
38
+ } else if (window.ng || document.querySelector('[ng-version]')) {
39
+ report.framework = { name: 'Angular', version: document.querySelector('[ng-version]')?.getAttribute('ng-version') || 'detected' };
40
+ } else if (document.querySelector('[data-astro-cid]') || document.querySelector('astro-island')) {
41
+ report.framework = { name: 'Astro', version: 'detected' };
42
+ } else if (document.querySelector('meta[name="generator"][content*="Gatsby"]')) {
43
+ report.framework = { name: 'Gatsby', version: 'detected' };
44
+ } else if (document.querySelector('meta[name="generator"][content*="WordPress"]')) {
45
+ report.framework = { name: 'WordPress', version: document.querySelector('meta[name="generator"]')?.content?.match(/\d[\d.]*/)?.[0] || 'detected' };
46
+ } else if (document.querySelector('meta[name="generator"][content*="Webflow"]')) {
47
+ report.framework = { name: 'Webflow', version: 'detected' };
48
+ } else if (document.querySelector('meta[name="generator"][content*="Framer"]') || window.__framer) {
49
+ report.framework = { name: 'Framer', version: 'detected' };
50
+ } else if (document.querySelector('meta[name="generator"][content*="Squarespace"]')) {
51
+ report.framework = { name: 'Squarespace', version: 'detected' };
52
+ } else if (document.querySelector('meta[name="generator"][content*="Wix"]') || window.wixBiSession) {
53
+ report.framework = { name: 'Wix', version: 'detected' };
54
+ } else if (window.Shopify) {
55
+ report.framework = { name: 'Shopify', version: 'detected' };
56
+ } else {
57
+ report.framework = { name: 'Unknown/Vanilla', version: null };
58
+ }
59
+
60
+ // ═══ 3D / RENDERING DETECTION ═══
61
+ const canvases = [...document.querySelectorAll('canvas')];
62
+ canvases.forEach(c => {
63
+ const gl2 = c.getContext('webgl2');
64
+ const gl = gl2 || c.getContext('webgl');
65
+ if (gl) report.rendering.webgl = { version: gl2 ? 2 : 1, renderer: gl.getParameter(gl.RENDERER), vendor: gl.getParameter(gl.VENDOR) };
66
+ if (c.getContext('2d')) report.rendering.canvas2d = true;
67
+ if (c.__r$) report.libraries.r3f = true;
68
+ });
69
+ if (navigator.gpu) report.rendering.webgpu = { available: true };
70
+ if (document.querySelectorAll('svg').length > 2) report.rendering.svg = { count: document.querySelectorAll('svg').length };
71
+ const bodyCs = getComputedStyle(document.body);
72
+ if (bodyCs.perspective !== 'none' || document.querySelector('[style*="transform-style: preserve-3d"]') || document.querySelector('[style*="perspective"]'))
73
+ report.rendering.css3d = true;
74
+
75
+ // 3D Libraries
76
+ if (window.THREE) report.libraries.three = { version: window.THREE.REVISION, source: 'global' };
77
+ else {
78
+ const s = [...document.querySelectorAll('script[src]')].find(s => /three/i.test(s.src));
79
+ if (s) report.libraries.three = { version: 'bundled', bundleUrl: s.src };
80
+ }
81
+ if (window.BABYLON) report.libraries.babylon = { version: window.BABYLON.Engine?.Version || 'detected' };
82
+ if (window.pc) report.libraries.playcanvas = { version: window.pc.version || 'detected' };
83
+ if (window.AFRAME) report.libraries.aframe = { version: window.AFRAME.version || 'detected' };
84
+ if (window.unityInstance || document.querySelector('[data-unity-loader]') || document.querySelector('#unity-container')) report.libraries.unity = true;
85
+ if (window.PIXI) report.libraries.pixi = { version: window.PIXI.VERSION || 'detected' };
86
+ if (window.p5) report.libraries.p5 = 'detected';
87
+ if (document.querySelector('spline-viewer') || window.__spline) report.libraries.spline = true;
88
+ if (window.ogl) report.libraries.ogl = 'detected';
89
+
90
+ // ═══ ANIMATION LIBRARY DETECTION ═══
91
+ if (window.gsap) report.animations.gsap = { version: window.gsap.version };
92
+ if (window.ScrollTrigger) report.animations.scrollTrigger = true;
93
+ if (window.ScrollSmoother) report.animations.scrollSmoother = true;
94
+ if (document.querySelector('[data-framer-appear-id]') || document.querySelector('[style*="--framer"]')) report.animations.framerMotion = true;
95
+ if (window.anime) report.animations.anime = { version: window.anime.version || 'detected' };
96
+ if (window.lottie || document.querySelector('lottie-player') || document.querySelector('[data-lottie]')) report.animations.lottie = true;
97
+ if (window.rive || document.querySelector('canvas[data-rive]') || document.querySelector('rive-canvas')) report.animations.rive = true;
98
+ if (window.Motion || window.motion) report.animations.motionOne = 'detected';
99
+ if (window.popmotion) report.animations.popmotion = 'detected';
100
+ if (window.velocity) report.animations.velocity = 'detected';
101
+
102
+ const waapi = document.getAnimations?.();
103
+ if (waapi?.length > 0) report.animations.waapi = { count: waapi.length };
104
+
105
+ let cssAnimCount = 0, cssTransitionCount = 0;
106
+ const sample = [...document.querySelectorAll('*')].slice(0, 300);
107
+ sample.forEach(el => {
108
+ const s = getComputedStyle(el);
109
+ if (s.animationName !== 'none') cssAnimCount++;
110
+ if (s.transitionProperty !== 'all' && s.transitionProperty !== 'none' && s.transitionDuration !== '0s') cssTransitionCount++;
111
+ });
112
+ if (cssAnimCount > 0) report.animations.cssAnimations = cssAnimCount;
113
+ if (cssTransitionCount > 0) report.animations.cssTransitions = cssTransitionCount;
114
+
115
+ // ═══ SCROLL LIBRARY DETECTION ═══
116
+ if (window.__lenis || document.querySelector('[data-lenis-prevent]') || document.querySelector('.lenis')) report.scroll.lenis = true;
117
+ if (window.LocomotiveScroll || document.querySelector('[data-scroll-container]')) report.scroll.locomotive = true;
118
+ if (window.ScrollMagic) report.scroll.scrollMagic = true;
119
+ if (window.fullPage || window.fullpage_api) report.scroll.fullPage = true;
120
+ if (window.pagePiling) report.scroll.pagePiling = true;
121
+ if (CSS?.supports?.('animation-timeline', 'scroll()')) report.scroll.nativeScrollDriven = true;
122
+ if (getComputedStyle(document.documentElement).scrollBehavior === 'smooth') report.scroll.smoothScroll = 'css';
123
+
124
+ // ═══ PAGE TRANSITION DETECTION ═══
125
+ if (window.barba) report.pageTransitions.barba = { version: window.barba.version || 'detected' };
126
+ if (window.swup) report.pageTransitions.swup = { version: window.swup.version || 'detected' };
127
+ if (document.startViewTransition || CSS?.supports?.('view-transition-name', 'root')) report.pageTransitions.viewTransitionsAPI = true;
128
+ if (document.querySelector('[data-router-view]')) report.pageTransitions.customRouter = true;
129
+
130
+ // ═══ AUDIO / WEB AUDIO API DETECTION ═══
131
+ try {
132
+ if (window.AudioContext || window.webkitAudioContext) {
133
+ // Check if any AudioContext is instantiated
134
+ report.audio.webAudioAPI = true;
135
+ if (window.Howl || window.Howler) report.audio.howler = { version: window.Howler?.version || 'detected' };
136
+ if (window.Tone) report.audio.tone = { version: window.Tone.version || 'detected' };
137
+ // Check media elements
138
+ const audioEls = document.querySelectorAll('audio, video');
139
+ if (audioEls.length > 0) report.audio.mediaElements = audioEls.length;
140
+ }
141
+ } catch(e) {}
142
+
143
+ // ═══ ACCESSIBILITY / REDUCED MOTION ═══
144
+ report.accessibility.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
145
+ report.accessibility.prefersColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
146
+ report.accessibility.ariaLive = document.querySelectorAll('[aria-live]').length;
147
+ report.accessibility.skipLinks = document.querySelectorAll('[href="#main"], [href="#content"], .skip-link').length;
148
+
149
+ // ═══ CSS FRAMEWORK DETECTION ═══
150
+ const hasClass = (patterns) => patterns.some(p => document.querySelector(p));
151
+ if (hasClass(['[class*="tw-"]', '[class*="bg-gray"]', '[class*="text-gray"]']) ||
152
+ (() => { try { return [...document.styleSheets][0]?.cssRules?.[0]?.cssText?.includes('--tw-') } catch(e) { return false; }})())
153
+ report.libraries.tailwind = true;
154
+ if (hasClass(['[class*="chakra-"]', '[data-chakra-component]'])) report.libraries.chakra = true;
155
+ if (hasClass(['[class*="MuiButton"]', '[class*="MuiBox"]'])) report.libraries.mui = true;
156
+ if (hasClass(['[class*="mantine-"]'])) report.libraries.mantine = true;
157
+ if (hasClass(['[class*="ant-"]'])) report.libraries.antd = true;
158
+ if (hasClass(['[class*="shadcn"]', '[data-radix-popper-content-wrapper]'])) report.libraries.shadcnRadix = true;
159
+
160
+ // ═══ META / HEAD ═══
161
+ report.meta = {
162
+ title: document.title,
163
+ description: document.querySelector('meta[name="description"]')?.content,
164
+ viewport: document.querySelector('meta[name="viewport"]')?.content,
165
+ ogImage: document.querySelector('meta[property="og:image"]')?.content,
166
+ charset: document.characterSet,
167
+ lang: document.documentElement.lang,
168
+ themeColor: document.querySelector('meta[name="theme-color"]')?.content,
169
+ colorScheme: document.querySelector('meta[name="color-scheme"]')?.content,
170
+ };
171
+
172
+ // ═══ FONT DETECTION ═══
173
+ try {
174
+ const fonts = new Set();
175
+ document.fonts.forEach(f => fonts.add(`${f.family} (weight:${f.weight} style:${f.style} status:${f.status})`));
176
+ report.fonts = [...fonts];
177
+ } catch(e) { report.fonts = ['detection failed']; }
178
+
179
+ // ═══ SCRIPT INVENTORY ═══
180
+ report.scripts = [...document.querySelectorAll('script[src]')].map(s => s.src)
181
+ .filter(s => !/gtag|analytics|pixel|facebook|hotjar|clarity|cdn\.cookie|recaptcha/i.test(s))
182
+ .slice(0, 40);
183
+
184
+ return report;
185
+ })()
@@ -0,0 +1,73 @@
1
+ /**
2
+ * SCRIPT 01 — Source Map & Bundle Analysis
3
+ * webgl-forensics skill | Phase 1 (NEW — Gap #1)
4
+ *
5
+ * Run via: evaluate_script (paste entire IIFE)
6
+ * Purpose: Find source maps and extract original un-minified shader + animation code
7
+ */
8
+ (() => {
9
+ const result = {
10
+ bundler: null,
11
+ sourceMaps: [],
12
+ bundleUrls: [],
13
+ glslStrings: [],
14
+ gsapStrings: [],
15
+ threeStrings: [],
16
+ hasSourceMaps: false,
17
+ };
18
+
19
+ // ═══ COLLECT ALL JS BUNDLE URLS ═══
20
+ const scripts = [...document.querySelectorAll('script[src]')].map(s => s.src);
21
+ const moduleScripts = [...document.querySelectorAll('script[type="module"][src]')].map(s => s.src);
22
+ const allScripts = [...new Set([...scripts, ...moduleScripts])];
23
+
24
+ // Filter to likely app bundles (exclude CDNs and trackers)
25
+ result.bundleUrls = allScripts.filter(url =>
26
+ !/(gtag|analytics|pixel|facebook|hotjar|recaptcha|stripe|intercom|zendesk)/i.test(url)
27
+ );
28
+
29
+ // ═══ DETECT BUNDLER FROM CHUNK NAMING ═══
30
+ const urls = result.bundleUrls.join(' ');
31
+ if (/\/_next\/static\/chunks/.test(urls)) result.bundler = 'Next.js (webpack/turbopack)';
32
+ else if (/\/assets\/index-[a-z0-9]+\.js/.test(urls)) result.bundler = 'Vite';
33
+ else if (/\/static\/js\/(main|chunk)\.[a-z0-9]+\.js/.test(urls)) result.bundler = 'Create React App (webpack)';
34
+ else if (/\/dist\/[a-z0-9]+\.js/.test(urls)) result.bundler = 'webpack/rollup (generic)';
35
+ else if (/\.chunk\.js/.test(urls)) result.bundler = 'webpack (code splitting)';
36
+ else result.bundler = 'Unknown — check script URLs manually';
37
+
38
+ // ═══ CHECK FOR SOURCE MAP COMMENTS IN PERFORMANCE ENTRIES ═══
39
+ // Source maps are referenced via sourceMappingURL at end of each bundle
40
+ // We can't read bundle content from evaluate_script due to CORS/same-origin
41
+ // Instead, construct the map URLs and report them for manual fetch
42
+ result.sourceMaps = result.bundleUrls.map(url => ({
43
+ bundleUrl: url,
44
+ expectedMapUrl: url + '.map',
45
+ instruction: `Fetch ${url + '.map'} — if 200 OK, source map found. Parse with 'source-map' npm package.`,
46
+ }));
47
+
48
+ // ═══ SCAN INLINE SCRIPTS FOR SHADER CODE ═══
49
+ const inlineScripts = [...document.querySelectorAll('script:not([src])')];
50
+ const glslKeywords = /gl_Position|gl_FragColor|varying\s+|uniform\s+|attribute\s+|precision\s+(mediump|highp|lowp)|void\s+main\s*\(/g;
51
+ const gsapKeywords = /gsap\.(to|from|fromTo|timeline|set)|ScrollTrigger\.(create|getAll|batch)/g;
52
+ const threeKeywords = /new THREE\.|ShaderMaterial|RawShaderMaterial|vertexShader|fragmentShader|useFrame|useThree/g;
53
+
54
+ inlineScripts.forEach((script, i) => {
55
+ const content = script.textContent || '';
56
+ if (glslKeywords.test(content)) result.glslStrings.push({ scriptIndex: i, length: content.length, preview: content.slice(0, 200) });
57
+ if (gsapKeywords.test(content)) result.gsapStrings.push({ scriptIndex: i, length: content.length, preview: content.slice(0, 200) });
58
+ if (threeKeywords.test(content)) result.threeStrings.push({ scriptIndex: i, length: content.length, preview: content.slice(0, 200) });
59
+ });
60
+
61
+ result.hasSourceMaps = result.bundleUrls.length > 0;
62
+
63
+ // ═══ INSTRUCTIONS FOR NEXT STEPS ═══
64
+ result.nextSteps = [
65
+ '1. Use get_network_request to download each bundleUrl',
66
+ '2. Search downloaded content for: vertexShader, fragmentShader, ShaderMaterial, "gl_Position", "gl_FragColor"',
67
+ '3. For each bundleUrl, also fetch bundleUrl + ".map" to check for source maps',
68
+ '4. If source maps found, you have original component code — search for shader props',
69
+ `5. Bundler detected: ${result.bundler} — use this to understand chunk naming`,
70
+ ];
71
+
72
+ return result;
73
+ })()
@@ -0,0 +1,129 @@
1
+ /**
2
+ * SCRIPT 02 — Interaction Model Extraction
3
+ * webgl-forensics skill | Phase 7 (Gap #2)
4
+ *
5
+ * Run via: evaluate_script (paste entire IIFE)
6
+ * Purpose: Extract mouse parallax, magnetic cursor physics, touch/gesture handlers,
7
+ * gyroscope bindings, and how interactions feed into 3D/animation systems.
8
+ */
9
+ (() => {
10
+ const result = {
11
+ mouse: { parallax: [], magnetic: [], trails: [], blend: [] },
12
+ touch: { gestures: [], swipe: false, pinch: false },
13
+ gyroscope: { detected: false },
14
+ keyboard: { shortcuts: [] },
15
+ customCursor: null,
16
+ hoverEffects: [],
17
+ eventListenerSummary: {},
18
+ };
19
+
20
+ // ═══ CUSTOM CURSOR DETECTION ═══
21
+ const cursorEls = document.querySelectorAll('[class*="cursor"], [id*="cursor"], [data-cursor]');
22
+ if (cursorEls.length > 0) {
23
+ result.customCursor = {
24
+ elements: [...cursorEls].map(el => ({
25
+ tag: el.tagName,
26
+ className: el.className?.toString?.().slice(0, 80),
27
+ computedPosition: getComputedStyle(el).position,
28
+ computedMixBlend: getComputedStyle(el).mixBlendMode,
29
+ width: el.offsetWidth,
30
+ height: el.offsetHeight,
31
+ })),
32
+ };
33
+ }
34
+
35
+ // Check for mix-blend-mode: difference (inversion cursor pattern)
36
+ const inverseCursors = [...document.querySelectorAll('*')].filter(el => {
37
+ try { return getComputedStyle(el).mixBlendMode === 'difference'; } catch(e) { return false; }
38
+ }).slice(0, 5);
39
+ if (inverseCursors.length > 0) result.mouse.blend = inverseCursors.map(el => ({
40
+ tag: el.tagName,
41
+ className: el.className?.toString?.().slice(0, 60),
42
+ mixBlend: 'difference',
43
+ }));
44
+
45
+ // ═══ MAGNETIC ELEMENT DETECTION ═══
46
+ // Magnetic buttons typically have data attrs or class patterns
47
+ const magneticEls = document.querySelectorAll(
48
+ '[data-magnetic], [data-cursor-magnetic], [class*="magnetic"], [class*="attract"]'
49
+ );
50
+ result.mouse.magnetic = [...magneticEls].map(el => ({
51
+ tag: el.tagName,
52
+ text: el.textContent?.trim().slice(0, 40),
53
+ className: el.className?.toString?.().slice(0, 60),
54
+ magneticStrength: el.dataset.magnetic || el.dataset.cursorMagnetic || 'unknown',
55
+ }));
56
+
57
+ // ═══ PARALLAX ELEMENT DETECTION ═══
58
+ const parallaxEls = document.querySelectorAll(
59
+ '[data-parallax], [data-speed], [data-depth], [class*="parallax"], [data-mouse-parallax]'
60
+ );
61
+ result.mouse.parallax = [...parallaxEls].map(el => ({
62
+ tag: el.tagName,
63
+ speed: el.dataset.parallaxSpeed || el.dataset.speed || 'unknown',
64
+ depth: el.dataset.depth || el.dataset.parallaxDepth || 'unknown',
65
+ direction: el.dataset.parallaxDirection || 'both',
66
+ }));
67
+
68
+ // ═══ MOUSE TRAIL DETECTION ═══
69
+ const canvases = document.querySelectorAll('canvas');
70
+ canvases.forEach(c => {
71
+ // Small canvases overlaying full viewport = likely cursor trail
72
+ if (getComputedStyle(c).position === 'fixed' &&
73
+ (c.style.pointerEvents === 'none' || getComputedStyle(c).pointerEvents === 'none')) {
74
+ result.mouse.trails.push({
75
+ width: c.width, height: c.height,
76
+ note: 'Fixed, non-interactive canvas — likely cursor trail or background FX',
77
+ });
78
+ }
79
+ });
80
+
81
+ // ═══ TOUCH / GESTURE DETECTION ═══
82
+ // Look for touch-specific data attributes
83
+ const touchEls = document.querySelectorAll('[data-swipe], [data-touch], [class*="swiper"], [class*="slider"], [class*="carousel"]');
84
+ result.touch.swipe = touchEls.length > 0;
85
+
86
+ // Check for Swiper.js
87
+ if (window.Swiper) result.touch.swiper = { version: window.Swiper.version || 'detected' };
88
+ // Check for Embla
89
+ if (window.EmblaCarousel) result.touch.embla = true;
90
+ // Check for Hammer.js (gesture library)
91
+ if (window.Hammer) result.touch.hammer = 'detected';
92
+
93
+ // ═══ GYROSCOPE / DEVICE ORIENTATION ═══
94
+ // Can't confirm active listeners from JS, but check for device motion elements
95
+ result.gyroscope.detected = !!(window.DeviceOrientationEvent || window.DeviceMotionEvent);
96
+ result.gyroscope.hasPermission = typeof DeviceOrientationEvent?.requestPermission === 'function';
97
+ result.gyroscope.note = result.gyroscope.hasPermission
98
+ ? 'iOS 13+ — requires explicit user permission. Check for "requestPermission" calls on interaction.'
99
+ : 'Standard DeviceOrientation API available';
100
+
101
+ // ═══ KEYBOARD SHORTCUT DETECTION ═══
102
+ // Check if site adds custom keyboard handlers (common in portfolio sites)
103
+ result.keyboard.note = 'Keyboard listeners cannot be enumerated from JS. Check via event listener breakpoints in DevTools.';
104
+
105
+ // ═══ HOVER EFFECT CLASSIFICATION ═══
106
+ // Sample buttons and links to understand hover transform patterns
107
+ const interactiveEls = [...document.querySelectorAll('a, button, [role="button"]')].slice(0, 20);
108
+ result.hoverEffects = interactiveEls.map(el => {
109
+ const base = getComputedStyle(el);
110
+ return {
111
+ tag: el.tagName,
112
+ text: el.textContent?.trim().slice(0, 30),
113
+ cursor: base.cursor,
114
+ transition: base.transition?.slice(0, 100),
115
+ transform: base.transform,
116
+ overflow: base.overflow,
117
+ dataCursor: el.dataset.cursor || undefined,
118
+ };
119
+ });
120
+
121
+ // ═══ UNIFORMS THAT MIGHT BE DRIVEN BY MOUSE ═══
122
+ // Look for uniform names that suggest mouse input
123
+ result.mouseUniforms = {
124
+ commonNames: ['uMouse', 'uMousePos', 'mouse', 'mousePos', 'cursor', 'uCursor', 'pointer', 'uPointer'],
125
+ instruction: 'Search extracted shader uniforms for these names — they indicate mouse → GLSL binding',
126
+ };
127
+
128
+ return result;
129
+ })()
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SCRIPT 03 — Responsive & Breakpoint Analysis
3
+ * webgl-forensics skill | Phase 8 (Gap #3)
4
+ *
5
+ * Run via: evaluate_script at multiple viewport widths
6
+ * Purpose: Detect how animations/3D changes at different breakpoints,
7
+ * find mobile-kill switches for heavy effects, map responsive design tokens.
8
+ *
9
+ * USAGE: Run this at each breakpoint by resizing via resize_page tool:
10
+ * - 375px (iPhone SE)
11
+ * - 768px (iPad)
12
+ * - 1024px (Laptop)
13
+ * - 1440px (Desktop)
14
+ * - 1920px (Wide)
15
+ */
16
+ (() => {
17
+ const vw = window.innerWidth;
18
+ const vh = window.innerHeight;
19
+
20
+ const result = {
21
+ viewport: { width: vw, height: vh, devicePixelRatio: window.devicePixelRatio },
22
+ breakpointCategory: vw < 768 ? 'mobile' : vw < 1024 ? 'tablet' : vw < 1440 ? 'laptop' : 'desktop',
23
+ visibleCanvases: 0,
24
+ hiddenCanvases: 0,
25
+ activeAnimations: 0,
26
+ pausedAnimations: 0,
27
+ reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
28
+ cssBreakpoints: {},
29
+ mediaQueryMatches: {},
30
+ gridColumns: {},
31
+ };
32
+
33
+ // ═══ CANVAS VISIBILITY AT THIS VIEWPORT ═══
34
+ document.querySelectorAll('canvas').forEach(c => {
35
+ const rect = c.getBoundingClientRect();
36
+ const cs = getComputedStyle(c);
37
+ if (cs.display === 'none' || cs.visibility === 'hidden' || rect.width === 0) {
38
+ result.hiddenCanvases++;
39
+ } else {
40
+ result.visibleCanvases++;
41
+ }
42
+ });
43
+
44
+ // ═══ ANIMATION STATE AT THIS VIEWPORT ═══
45
+ if (document.getAnimations) {
46
+ const anims = document.getAnimations();
47
+ result.activeAnimations = anims.filter(a => a.playState === 'running').length;
48
+ result.pausedAnimations = anims.filter(a => a.playState === 'paused').length;
49
+ }
50
+
51
+ // GSAP kill/pause state at this viewport
52
+ if (window.gsap) {
53
+ const sts = window.ScrollTrigger?.getAll?.() || [];
54
+ result.scrollTriggersActive = sts.filter(st => !st.vars?.matchMedia).length;
55
+ result.scrollTriggersMediaQuery = sts.filter(st => !!st.vars?.matchMedia).length;
56
+
57
+ // Check for mm() matchMedia GSAP usage
58
+ result.gsapMatchMedia = !!window.ScrollTrigger?.matchMedia ||
59
+ !!window.gsap?.matchMedia;
60
+ }
61
+
62
+ // ═══ DETECT CSS BREAKPOINTS FROM STYLESHEETS ═══
63
+ try {
64
+ const mediaRules = [];
65
+ [...document.styleSheets].forEach(sheet => {
66
+ try {
67
+ [...sheet.cssRules].forEach(rule => {
68
+ if (rule instanceof CSSMediaRule) {
69
+ const condition = rule.conditionText || rule.media?.mediaText;
70
+ const widthMatch = condition.match(/(\d+)px/g);
71
+ if (widthMatch) mediaRules.push(condition);
72
+ }
73
+ });
74
+ } catch(e) {}
75
+ });
76
+ // Extract unique breakpoint values
77
+ const breakpointValues = new Set();
78
+ mediaRules.forEach(r => { r.match(/\d+/g)?.forEach(n => { if (n > 200 && n < 3000) breakpointValues.add(+n); }); });
79
+ result.cssBreakpoints.values = [...breakpointValues].sort((a, b) => a - b);
80
+ result.cssBreakpoints.mediaRules = [...new Set(mediaRules)].slice(0, 20);
81
+ } catch(e) {}
82
+
83
+ // ═══ COMMON BREAKPOINT TEST ═══
84
+ const breakpoints = { sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536 };
85
+ Object.entries(breakpoints).forEach(([name, width]) => {
86
+ result.mediaQueryMatches[name] = window.matchMedia(`(min-width: ${width}px)`).matches;
87
+ });
88
+
89
+ // ═══ LAYOUT GRID AT THIS VIEWPORT ═══
90
+ const gridContainers = [...document.querySelectorAll('[class*="grid"], [class*="container"], main, section')].slice(0, 10);
91
+ result.gridColumns = gridContainers.map(el => ({
92
+ tag: el.tagName,
93
+ className: el.className?.toString?.().slice(0, 60),
94
+ display: getComputedStyle(el).display,
95
+ gridTemplateColumns: getComputedStyle(el).gridTemplateColumns?.slice(0, 100),
96
+ width: el.offsetWidth,
97
+ maxWidth: getComputedStyle(el).maxWidth,
98
+ }));
99
+
100
+ // ═══ MOBILE PERFORMANCE KILL-SWITCHES ═══
101
+ // Look for elements hidden/simplified on mobile
102
+ const reducedEls = [...document.querySelectorAll('[class*="hidden"], [class*="mobile-hidden"], [data-mobile-disabled]')]
103
+ .filter(el => {
104
+ const cs = getComputedStyle(el);
105
+ return cs.display === 'none' || cs.visibility === 'hidden';
106
+ }).slice(0, 10);
107
+
108
+ result.mobileKillSwitches = {
109
+ hiddenElementCount: reducedEls.length,
110
+ elements: reducedEls.map(el => ({ tag: el.tagName, className: el.className?.toString?.().slice(0, 60) })),
111
+ note: 'Elements hidden at this viewport — compare across breakpoints to understand degradation strategy',
112
+ };
113
+
114
+ return result;
115
+ })()
@@ -0,0 +1,128 @@
1
+ /**
2
+ * SCRIPT 04 — Page Transition Extraction
3
+ * webgl-forensics skill | Phase 9 (Gap #4)
4
+ *
5
+ * Run via: evaluate_script (paste entire IIFE)
6
+ * Purpose: Detect SPA route transition systems — Barba.js, Swup, View Transitions API,
7
+ * GSAP page wipes, custom transition overlays.
8
+ */
9
+ (() => {
10
+ const result = {
11
+ detected: [],
12
+ transitionElements: [],
13
+ overlayElements: [],
14
+ viewTransitions: false,
15
+ nextjsTransitions: false,
16
+ navigationTiming: {},
17
+ };
18
+
19
+ // ═══ BARBA.JS ═══
20
+ if (window.barba) {
21
+ result.detected.push('Barba.js');
22
+ result.barba = {
23
+ version: window.barba.version || 'detected',
24
+ currentPageName: window.barba.wrapper?.dataset?.barbaNamespace || 'unknown',
25
+ containers: [...document.querySelectorAll('[data-barba="container"]')].map(el => ({
26
+ namespace: el.dataset.barbaNamaspace,
27
+ tag: el.tagName,
28
+ })),
29
+ };
30
+ }
31
+
32
+ // ═══ SWUP ═══
33
+ if (window.swup) {
34
+ result.detected.push('Swup');
35
+ result.swup = {
36
+ version: window.swup.version || 'detected',
37
+ containers: [...document.querySelectorAll('[id="swup"], [class*="swup"]')].map(el => el.id || el.className?.toString()?.slice(0, 40)),
38
+ };
39
+ }
40
+
41
+ // ═══ VIEW TRANSITIONS API ═══
42
+ if (document.startViewTransition) {
43
+ result.viewTransitions = true;
44
+ result.detected.push('View Transitions API (native)');
45
+ // Check CSS for view-transition-name declarations
46
+ try {
47
+ const vtElements = [];
48
+ [...document.querySelectorAll('*')].slice(0, 500).forEach(el => {
49
+ const vtn = getComputedStyle(el).getPropertyValue('view-transition-name');
50
+ if (vtn && vtn !== 'none') vtElements.push({ tag: el.tagName, name: vtn.trim() });
51
+ });
52
+ result.viewTransitionNames = vtElements;
53
+ } catch(e) {}
54
+ }
55
+
56
+ // ═══ NEXT.JS APP ROUTER TRANSITIONS ═══
57
+ if (window.__NEXT_DATA__ || document.querySelector('#__next')) {
58
+ result.nextjsTransitions = true;
59
+ result.detected.push('Next.js (built-in routing)');
60
+ // Check for next-view-transitions package
61
+ const vtScript = [...document.querySelectorAll('script[src]')].find(s => /view-transitions/i.test(s.src));
62
+ if (vtScript) result.nextjsViewTransitions = vtScript.src;
63
+ }
64
+
65
+ // ═══ TRANSITION OVERLAY ELEMENTS ═══
66
+ // Look for full-screen overlay elements used for page wipe transitions
67
+ const potentialOverlays = [...document.querySelectorAll('[class*="overlay"], [class*="transition"], [class*="curtain"], [class*="wipe"], [class*="cover"], [class*="loader"]')]
68
+ .filter(el => {
69
+ const cs = getComputedStyle(el);
70
+ return (cs.position === 'fixed' || cs.position === 'absolute') &&
71
+ parseFloat(cs.zIndex) > 10;
72
+ }).slice(0, 10);
73
+
74
+ result.overlayElements = potentialOverlays.map(el => ({
75
+ tag: el.tagName,
76
+ className: el.className?.toString?.().slice(0, 80),
77
+ position: getComputedStyle(el).position,
78
+ zIndex: getComputedStyle(el).zIndex,
79
+ backgroundColor: getComputedStyle(el).backgroundColor,
80
+ width: el.offsetWidth,
81
+ height: el.offsetHeight,
82
+ transform: getComputedStyle(el).transform,
83
+ opacity: getComputedStyle(el).opacity,
84
+ visibility: getComputedStyle(el).visibility,
85
+ display: getComputedStyle(el).display,
86
+ transition: getComputedStyle(el).transition?.slice(0, 100),
87
+ animation: getComputedStyle(el).animationName,
88
+ }));
89
+
90
+ // ═══ GSAP PAGE WIPE DETECTION ═══
91
+ // Look for GSAP timelines that target overlay-like elements
92
+ if (window.gsap) {
93
+ const sts = window.ScrollTrigger?.getAll?.() || [];
94
+ // Transitions that PIN the entire body or main element = page-level wipe
95
+ const pageLevelPins = sts.filter(st =>
96
+ st.vars?.pin && ['body', 'main', 'html', '#__next', '#app'].includes(st.trigger?.tagName?.toLowerCase() || '')
97
+ );
98
+ result.gsapPageWipes = pageLevelPins.map(st => ({
99
+ trigger: st.trigger?.tagName,
100
+ pin: st.vars.pin,
101
+ scrub: st.vars.scrub,
102
+ note: 'Page-level pin detected — likely a scroll-driven transition',
103
+ }));
104
+ }
105
+
106
+ // ═══ ANCHOR CLICK INTERCEPTION ═══
107
+ // Sites with custom transitions intercept anchor clicks
108
+ result.anchorInterception = {
109
+ note: 'Cannot enumerate click listeners from JS. Check DevTools Event Listeners panel on <a> tags.',
110
+ instruction: 'In DevTools: select any <a> element → Event Listeners panel → look for "click" handlers on parent elements',
111
+ };
112
+
113
+ // ═══ NAVIGATION PERFORMANCE TIMING ═══
114
+ try {
115
+ const nav = performance.getEntriesByType('navigation')[0];
116
+ result.navigationTiming = {
117
+ type: nav?.type, // 'navigate', 'reload', 'back_forward'
118
+ domContentLoaded: nav?.domContentLoadedEventEnd?.toFixed(0) + 'ms',
119
+ loadComplete: nav?.loadEventEnd?.toFixed(0) + 'ms',
120
+ };
121
+ } catch(e) {}
122
+
123
+ result.summary = result.detected.length > 0
124
+ ? `Detected: ${result.detected.join(', ')}`
125
+ : 'No standard page transition library detected. May use custom GSAP timelines or CSS transitions.';
126
+
127
+ return result;
128
+ })()