webgl-forensics 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/forensics.yml +44 -0
- package/CLAUDE_CODE.md +49 -0
- package/MASTER_PROMPT.md +202 -0
- package/README.md +579 -0
- package/SKILL.md +1256 -0
- package/package.json +53 -0
- package/project summary.md +33 -0
- package/puppeteer-runner.js +486 -0
- package/scripts/00-tech-stack-detect.js +185 -0
- package/scripts/01-source-map-extractor.js +73 -0
- package/scripts/02-interaction-model.js +129 -0
- package/scripts/03-responsive-analysis.js +115 -0
- package/scripts/04-page-transitions.js +128 -0
- package/scripts/05-loading-sequence.js +124 -0
- package/scripts/06-audio-extraction.js +115 -0
- package/scripts/07-accessibility-reduced-motion.js +133 -0
- package/scripts/08-complexity-scorer.js +138 -0
- package/scripts/09-visual-diff-validator.js +113 -0
- package/scripts/10-webgpu-extractor.js +248 -0
- package/scripts/11-scroll-screenshot-grid.js +76 -0
- package/scripts/12-network-waterfall.js +151 -0
- package/scripts/13-react-fiber-walker.js +285 -0
- package/scripts/14-shader-hotpatch.js +349 -0
- package/scripts/15-multipage-crawler.js +221 -0
- package/scripts/16-gsap-timeline-recorder.js +335 -0
- package/scripts/17-r3f-fiber-serializer.js +316 -0
- package/scripts/18-font-extractor.js +290 -0
- package/scripts/19-design-token-export.js +85 -0
- package/scripts/20-timeline-visualizer.js +61 -0
- package/scripts/21-lighthouse-audit.js +35 -0
- package/scripts/22-sitemap-crawler.js +128 -0
- package/scripts/23-scaffold-generator.js +451 -0
- package/scripts/24-ai-reconstruction.js +226 -0
- package/scripts/25-shader-annotator.js +226 -0
- package/tui.js +196 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 05 — Loading Sequence & Preloader Analysis
|
|
3
|
+
* webgl-forensics skill | Phase 10 (Gap #5)
|
|
4
|
+
*
|
|
5
|
+
* Run via: evaluate_script immediately after page load (before scrolling)
|
|
6
|
+
* Purpose: Map the site's first-few-seconds experience:
|
|
7
|
+
* - Preloader structure and animation
|
|
8
|
+
* - Asset loading strategy (lazy, eager, priority)
|
|
9
|
+
* - Suspense boundaries / loading states
|
|
10
|
+
* - Progressive enhancement for 3D
|
|
11
|
+
* - Performance budgets and timing
|
|
12
|
+
*/
|
|
13
|
+
(() => {
|
|
14
|
+
const result = {
|
|
15
|
+
preloader: null,
|
|
16
|
+
assetStrategy: {},
|
|
17
|
+
lazyLoadedElements: [],
|
|
18
|
+
suspenseBoundaries: [],
|
|
19
|
+
performanceTiming: {},
|
|
20
|
+
resourceHints: {},
|
|
21
|
+
criticalPath: {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ═══ PRELOADER DETECTION ═══
|
|
25
|
+
const preloaderSelectors = [
|
|
26
|
+
'[id*="preloader"]', '[id*="loader"]', '[id*="loading"]',
|
|
27
|
+
'[class*="preloader"]', '[class*="loader"]', '[class*="loading"]',
|
|
28
|
+
'[class*="intro"]', '[class*="splash"]', '[data-preloader]',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const preloaderEl = document.querySelector(preloaderSelectors.join(', '));
|
|
32
|
+
if (preloaderEl) {
|
|
33
|
+
const cs = getComputedStyle(preloaderEl);
|
|
34
|
+
result.preloader = {
|
|
35
|
+
found: true,
|
|
36
|
+
tag: preloaderEl.tagName,
|
|
37
|
+
className: preloaderEl.className?.toString?.().slice(0, 100),
|
|
38
|
+
position: cs.position,
|
|
39
|
+
zIndex: cs.zIndex,
|
|
40
|
+
display: cs.display,
|
|
41
|
+
opacity: cs.opacity,
|
|
42
|
+
visibility: cs.visibility,
|
|
43
|
+
backgroundColor: cs.backgroundColor,
|
|
44
|
+
animationName: cs.animationName,
|
|
45
|
+
isCurrentlyVisible: cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0',
|
|
46
|
+
children: [...preloaderEl.children].map(c => ({
|
|
47
|
+
tag: c.tagName,
|
|
48
|
+
className: c.className?.toString?.().slice(0, 60),
|
|
49
|
+
text: c.textContent?.trim().slice(0, 40),
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
result.preloader = { found: false, note: 'No preloader detected — may have already completed or use inline JS logic' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ═══ RESOURCE HINTS ═══
|
|
57
|
+
result.resourceHints = {
|
|
58
|
+
preload: [...document.querySelectorAll('link[rel="preload"]')].map(l => ({ href: l.href, as: l.as, type: l.type })),
|
|
59
|
+
prefetch: [...document.querySelectorAll('link[rel="prefetch"]')].map(l => ({ href: l.href })),
|
|
60
|
+
preconnect: [...document.querySelectorAll('link[rel="preconnect"]')].map(l => l.href),
|
|
61
|
+
dnsPrefetch: [...document.querySelectorAll('link[rel="dns-prefetch"]')].map(l => l.href),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ═══ LAZY LOADING ═══
|
|
65
|
+
result.lazyLoadedElements = [
|
|
66
|
+
...[...document.querySelectorAll('img[loading="lazy"]')].map(el => ({ type: 'img', src: el.src?.slice(0, 80), loading: 'lazy' })),
|
|
67
|
+
...[...document.querySelectorAll('[data-src], [data-lazy]')].map(el => ({ type: el.tagName, dataSrc: el.dataset.src?.slice(0, 80), lazy: true })),
|
|
68
|
+
...[...document.querySelectorAll('iframe[loading="lazy"]')].map(el => ({ type: 'iframe', src: el.src?.slice(0, 80) })),
|
|
69
|
+
].slice(0, 30);
|
|
70
|
+
|
|
71
|
+
// ═══ SUSPENSE / LOADING STATE BOUNDARIES ═══
|
|
72
|
+
// Next.js loading.tsx / Suspense boundaries
|
|
73
|
+
const loadingStates = [
|
|
74
|
+
...document.querySelectorAll('[data-loading], [aria-busy="true"], [class*="skeleton"], [class*="shimmer"], [class*="placeholder"]'),
|
|
75
|
+
];
|
|
76
|
+
result.suspenseBoundaries = loadingStates.map(el => ({
|
|
77
|
+
tag: el.tagName,
|
|
78
|
+
className: el.className?.toString?.().slice(0, 60),
|
|
79
|
+
ariaBusy: el.getAttribute('aria-busy'),
|
|
80
|
+
})).slice(0, 10);
|
|
81
|
+
|
|
82
|
+
// ═══ 3D PROGRESSIVE ENHANCEMENT ═══
|
|
83
|
+
// Does the site show a fallback when WebGL isn't available?
|
|
84
|
+
const fallbacks = document.querySelectorAll('[class*="fallback"], [class*="no-webgl"], [data-no-3d]');
|
|
85
|
+
result.progressiveEnhancement = {
|
|
86
|
+
fallbackElements: [...fallbacks].map(el => ({
|
|
87
|
+
tag: el.tagName,
|
|
88
|
+
className: el.className?.toString?.().slice(0, 60),
|
|
89
|
+
visible: getComputedStyle(el).display !== 'none',
|
|
90
|
+
})),
|
|
91
|
+
note: 'Compare fallback visibility with/without WebGL to understand degradation strategy',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ═══ PERFORMANCE TIMING ═══
|
|
95
|
+
try {
|
|
96
|
+
const paint = performance.getEntriesByType('paint');
|
|
97
|
+
const fcp = paint.find(p => p.name === 'first-contentful-paint');
|
|
98
|
+
const fp = paint.find(p => p.name === 'first-paint');
|
|
99
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
100
|
+
|
|
101
|
+
result.performanceTiming = {
|
|
102
|
+
firstPaint: fp?.startTime?.toFixed(0) + 'ms',
|
|
103
|
+
firstContentfulPaint: fcp?.startTime?.toFixed(0) + 'ms',
|
|
104
|
+
domInteractive: nav?.domInteractive?.toFixed(0) + 'ms',
|
|
105
|
+
domComplete: nav?.domComplete?.toFixed(0) + 'ms',
|
|
106
|
+
totalPageLoad: nav?.loadEventEnd?.toFixed(0) + 'ms',
|
|
107
|
+
transferSize: nav?.transferSize ? (nav.transferSize / 1024).toFixed(1) + 'KB' : 'unknown',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Largest resources loaded
|
|
111
|
+
const resources = performance.getEntriesByType('resource');
|
|
112
|
+
result.heaviestResources = [...resources]
|
|
113
|
+
.sort((a, b) => b.transferSize - a.transferSize)
|
|
114
|
+
.slice(0, 10)
|
|
115
|
+
.map(r => ({
|
|
116
|
+
name: r.name.split('/').pop().slice(0, 60),
|
|
117
|
+
type: r.initiatorType,
|
|
118
|
+
size: (r.transferSize / 1024).toFixed(1) + 'KB',
|
|
119
|
+
duration: r.duration.toFixed(0) + 'ms',
|
|
120
|
+
}));
|
|
121
|
+
} catch(e) { result.performanceTiming = { error: e.message }; }
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
})()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 06 — Audio & Web Audio API Extraction
|
|
3
|
+
* webgl-forensics skill | Phase 11 (Gap #6)
|
|
4
|
+
*
|
|
5
|
+
* Run via: evaluate_script (paste entire IIFE)
|
|
6
|
+
* Purpose: Map all audio systems — spatial audio in 3D scenes, scroll-triggered sound,
|
|
7
|
+
* ambient soundscapes, Howler.js, Tone.js, raw Web Audio API graphs.
|
|
8
|
+
*/
|
|
9
|
+
(() => {
|
|
10
|
+
const result = {
|
|
11
|
+
webAudioAPI: { available: !!window.AudioContext || !!window.webkitAudioContext },
|
|
12
|
+
activeContexts: [],
|
|
13
|
+
libraries: {},
|
|
14
|
+
mediaElements: [],
|
|
15
|
+
scrollTriggeredAudio: [],
|
|
16
|
+
spatialAudio: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ═══ WEB AUDIO CONTEXT DETECTION ═══
|
|
20
|
+
// Try to find any AudioContext instances attached to window or global scope
|
|
21
|
+
if (result.webAudioAPI.available) {
|
|
22
|
+
|
|
23
|
+
// Scan window for AudioContext instances
|
|
24
|
+
const ACInstanceKeys = Object.keys(window).filter(key => {
|
|
25
|
+
try {
|
|
26
|
+
const val = window[key];
|
|
27
|
+
return val instanceof AudioContext || val instanceof (window.webkitAudioContext || AudioContext);
|
|
28
|
+
} catch(e) { return false; }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
result.activeContexts = ACInstanceKeys.map(key => {
|
|
32
|
+
const ctx = window[key];
|
|
33
|
+
return {
|
|
34
|
+
key,
|
|
35
|
+
state: ctx.state, // 'running', 'suspended', 'closed'
|
|
36
|
+
sampleRate: ctx.sampleRate,
|
|
37
|
+
baseLatency: ctx.baseLatency?.toFixed(3),
|
|
38
|
+
currentTime: ctx.currentTime?.toFixed(2),
|
|
39
|
+
destination: {
|
|
40
|
+
channelCount: ctx.destination.channelCount,
|
|
41
|
+
maxChannelCount: ctx.destination.maxChannelCount,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ═══ LIBRARY DETECTION ═══
|
|
48
|
+
if (window.Howl || window.Howler) {
|
|
49
|
+
result.libraries.howler = {
|
|
50
|
+
version: window.Howler?.codecs ? 'v2+' : 'detected',
|
|
51
|
+
globalVolume: window.Howler?.volume?.(),
|
|
52
|
+
ctx: window.Howler?.ctx?.state || 'unknown',
|
|
53
|
+
codecs: window.Howler?.codecs ? Object.keys(window.Howler.codecs) : [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (window.Tone) {
|
|
58
|
+
result.libraries.tone = {
|
|
59
|
+
version: window.Tone.version || 'detected',
|
|
60
|
+
context: window.Tone.getContext?.()?.state || 'unknown',
|
|
61
|
+
bpm: window.Tone.Transport?.bpm?.value,
|
|
62
|
+
transportState: window.Tone.Transport?.state,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (window.buzz) result.libraries.buzz = 'detected';
|
|
67
|
+
if (window.SoundJS || window.createjs?.Sound) result.libraries.soundJS = 'detected';
|
|
68
|
+
|
|
69
|
+
// ═══ MEDIA ELEMENTS (audio/video) ═══
|
|
70
|
+
const mediaEls = [...document.querySelectorAll('audio, video')];
|
|
71
|
+
result.mediaElements = mediaEls.map(el => ({
|
|
72
|
+
tag: el.tagName,
|
|
73
|
+
src: el.src?.slice(0, 80) || el.currentSrc?.slice(0, 80),
|
|
74
|
+
autoplay: el.autoplay,
|
|
75
|
+
loop: el.loop,
|
|
76
|
+
muted: el.muted,
|
|
77
|
+
paused: el.paused,
|
|
78
|
+
readyState: el.readyState,
|
|
79
|
+
volume: el.volume,
|
|
80
|
+
className: el.className?.toString?.().slice(0, 60),
|
|
81
|
+
id: el.id || undefined,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// ═══ AUDIO DATA ATTRIBUTES (scroll-triggered patterns) ═══
|
|
85
|
+
const audioTriggerEls = document.querySelectorAll('[data-audio], [data-sound], [data-sfx], [data-play], [class*="has-sound"]');
|
|
86
|
+
result.scrollTriggeredAudio = [...audioTriggerEls].map(el => ({
|
|
87
|
+
tag: el.tagName,
|
|
88
|
+
className: el.className?.toString?.().slice(0, 60),
|
|
89
|
+
dataAudio: el.dataset.audio,
|
|
90
|
+
dataSound: el.dataset.sound,
|
|
91
|
+
dataSfx: el.dataset.sfx,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// ═══ SPATIAL AUDIO INDICATORS ═══
|
|
95
|
+
// PannerNode / AudioListener = spatial/positional audio (common in 3D scenes)
|
|
96
|
+
result.spatialAudio = Object.keys(window).some(key => {
|
|
97
|
+
try { return window[key] instanceof PannerNode || window[key] instanceof AudioListener; } catch(e) { return false; }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ═══ AUDIO FEATURE SUMMARY ═══
|
|
101
|
+
result.summary = {
|
|
102
|
+
hasAudio: result.activeContexts.length > 0 || result.mediaElements.length > 0 || Object.keys(result.libraries).length > 0,
|
|
103
|
+
audioLibraries: Object.keys(result.libraries),
|
|
104
|
+
mediaCount: result.mediaElements.length,
|
|
105
|
+
contextCount: result.activeContexts.length,
|
|
106
|
+
spatialAudio: result.spatialAudio,
|
|
107
|
+
recommendation: result.activeContexts.length > 0
|
|
108
|
+
? 'Active Web Audio contexts found — check for oscillators, filters, and gain nodes creating ambient sound.'
|
|
109
|
+
: result.mediaElements.length > 0
|
|
110
|
+
? 'Standard media elements found — check autoplay/muted patterns for ambient video or audio tracks.'
|
|
111
|
+
: 'No obvious audio systems. Site may initialize audio on first user interaction (autoplay policy requirement).',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
})()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 07 — Accessibility & Reduced Motion Mapping
|
|
3
|
+
* webgl-forensics skill | Phase 12 (Gap #7)
|
|
4
|
+
*
|
|
5
|
+
* Run via: evaluate_script twice — once normally, once with reduced-motion emulated
|
|
6
|
+
* Purpose: Map how the site degrades for users with motion sensitivity,
|
|
7
|
+
* find CSS/JS reduced-motion kill-switches, ARIA patterns.
|
|
8
|
+
*
|
|
9
|
+
* To test with reduced motion active:
|
|
10
|
+
* 1. DevTools → Rendering tab → Emulate CSS media → prefers-reduced-motion: reduce
|
|
11
|
+
* 2. Re-run this script and compare output
|
|
12
|
+
*/
|
|
13
|
+
(() => {
|
|
14
|
+
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
15
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
16
|
+
const prefersHighContrast = window.matchMedia('(prefers-contrast: more)').matches;
|
|
17
|
+
|
|
18
|
+
const result = {
|
|
19
|
+
userPreferences: {
|
|
20
|
+
prefersReducedMotion: prefersReduced,
|
|
21
|
+
prefersDarkMode: prefersDark,
|
|
22
|
+
prefersHighContrast: prefersHighContrast,
|
|
23
|
+
},
|
|
24
|
+
reducedMotion: {
|
|
25
|
+
cssRules: [],
|
|
26
|
+
jsKillSwitches: [],
|
|
27
|
+
activeAnimationsKilled: 0,
|
|
28
|
+
},
|
|
29
|
+
accessibility: {
|
|
30
|
+
ariaRoles: {},
|
|
31
|
+
ariaLive: 0,
|
|
32
|
+
skipLinks: 0,
|
|
33
|
+
focusVisible: false,
|
|
34
|
+
colorContrastNote: 'Manual check required — use axe DevTools or Lighthouse',
|
|
35
|
+
},
|
|
36
|
+
focusManagement: {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ═══ CSS REDUCED MOTION RULES ═══
|
|
40
|
+
try {
|
|
41
|
+
[...document.styleSheets].forEach(sheet => {
|
|
42
|
+
try {
|
|
43
|
+
[...sheet.cssRules].forEach(rule => {
|
|
44
|
+
if (rule instanceof CSSMediaRule) {
|
|
45
|
+
const media = rule.conditionText || rule.media?.mediaText || '';
|
|
46
|
+
if (/prefers-reduced-motion/.test(media)) {
|
|
47
|
+
result.reducedMotion.cssRules.push({
|
|
48
|
+
mediaQuery: media,
|
|
49
|
+
ruleCount: rule.cssRules?.length,
|
|
50
|
+
preview: [...rule.cssRules].slice(0, 3).map(r => r.cssText?.slice(0, 100)),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} catch(e) {}
|
|
56
|
+
});
|
|
57
|
+
} catch(e) {}
|
|
58
|
+
|
|
59
|
+
// ═══ ANIMATION STATE WITH REDUCED MOTION ═══
|
|
60
|
+
if (prefersReduced && document.getAnimations) {
|
|
61
|
+
const allAnims = document.getAnimations();
|
|
62
|
+
result.reducedMotion.activeAnimationsKilled = allAnims.filter(a => a.playState !== 'running').length;
|
|
63
|
+
result.reducedMotion.runningDespiteReducedMotion = allAnims.filter(a => a.playState === 'running').length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ═══ JS KILL-SWITCH PATTERNS ═══
|
|
67
|
+
// Can't read event listeners, but check for data attributes that signal reduced-motion handling
|
|
68
|
+
const reducedEls = document.querySelectorAll('[data-reduced-motion], [class*="reduced-motion"], [aria-reducedmotion]');
|
|
69
|
+
result.reducedMotion.jsKillSwitches = [...reducedEls].map(el => ({
|
|
70
|
+
tag: el.tagName,
|
|
71
|
+
className: el.className?.toString?.().slice(0, 60),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// ═══ GSAP REDUCED MOTION ═══
|
|
75
|
+
if (window.gsap) {
|
|
76
|
+
// gsap.matchMedia is the canonical way to handle reduced motion in GSAP 3.11+
|
|
77
|
+
result.reducedMotion.gsapMatchMedia = !!window.gsap.matchMedia;
|
|
78
|
+
result.reducedMotion.gsapNote = window.gsap.matchMedia
|
|
79
|
+
? 'Site uses gsap.matchMedia() — animations may be conditionally killed for reduced-motion preference'
|
|
80
|
+
: 'gsap.matchMedia() not detected — check if site manually reads matchMedia in JS';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ═══ ARIA ROLES AUDIT ═══
|
|
84
|
+
const ariaRoles = {};
|
|
85
|
+
document.querySelectorAll('[role]').forEach(el => {
|
|
86
|
+
const role = el.getAttribute('role');
|
|
87
|
+
ariaRoles[role] = (ariaRoles[role] || 0) + 1;
|
|
88
|
+
});
|
|
89
|
+
result.accessibility.ariaRoles = ariaRoles;
|
|
90
|
+
result.accessibility.ariaLive = document.querySelectorAll('[aria-live]').length;
|
|
91
|
+
result.accessibility.skipLinks = document.querySelectorAll('[href="#main"], [href="#content"], .sr-only[href], .skip-link').length;
|
|
92
|
+
|
|
93
|
+
// ═══ FOCUS MANAGEMENT ═══
|
|
94
|
+
result.focusManagement = {
|
|
95
|
+
focusVisibleCSS: (() => {
|
|
96
|
+
try {
|
|
97
|
+
let found = false;
|
|
98
|
+
[...document.styleSheets].forEach(sheet => {
|
|
99
|
+
try {
|
|
100
|
+
[...sheet.cssRules].forEach(rule => {
|
|
101
|
+
if (rule.selectorText?.includes(':focus-visible')) found = true;
|
|
102
|
+
});
|
|
103
|
+
} catch(e) {}
|
|
104
|
+
});
|
|
105
|
+
return found;
|
|
106
|
+
} catch(e) { return false; }
|
|
107
|
+
})(),
|
|
108
|
+
outlineCSSNote: 'Check if body/global CSS has `outline: none` — this is an accessibility red flag',
|
|
109
|
+
dialogElements: document.querySelectorAll('[role="dialog"], dialog').length,
|
|
110
|
+
trapFocusPatterns: document.querySelectorAll('[data-focus-trap], [aria-modal="true"]').length,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ═══ COLOR SCHEME SUPPORT ═══
|
|
114
|
+
result.colorScheme = {
|
|
115
|
+
metaColorScheme: document.querySelector('meta[name="color-scheme"]')?.content,
|
|
116
|
+
cssColorSchemeProperty: (() => {
|
|
117
|
+
try { return getComputedStyle(document.documentElement).colorScheme; } catch(e) { return 'unknown'; }
|
|
118
|
+
})(),
|
|
119
|
+
darkModeDataAttr: document.documentElement.dataset.theme || document.documentElement.classList.contains('dark') ? 'dark class detected' : null,
|
|
120
|
+
darkModeNote: 'Check if 3D scenes swap geometries, materials, or shaders between light/dark mode',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ═══ RECOMMENDATION ═══
|
|
124
|
+
result.summary = {
|
|
125
|
+
reducedMotionCSSRules: result.reducedMotion.cssRules.length,
|
|
126
|
+
hasReducedMotionSupport: result.reducedMotion.cssRules.length > 0 || result.reducedMotion.jsKillSwitches.length > 0,
|
|
127
|
+
recommendation: result.reducedMotion.cssRules.length > 0
|
|
128
|
+
? `Good: ${result.reducedMotion.cssRules.length} CSS @media (prefers-reduced-motion) rule(s) found.`
|
|
129
|
+
: 'Warning: No CSS reduced-motion rules detected. Site may not respect user motion preferences.',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
})()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 08 — Complexity Scorer
|
|
3
|
+
* webgl-forensics skill | Structural Upgrade C
|
|
4
|
+
*
|
|
5
|
+
* Run via: evaluate_script AFTER all other phases have completed
|
|
6
|
+
* Input: Pass the collected data as JSON — or run standalone for rough scoring
|
|
7
|
+
* Output: Complexity score (1-100), rating, breakdown, and estimated rebuild time
|
|
8
|
+
*
|
|
9
|
+
* USAGE: Call this at the END of the forensics session.
|
|
10
|
+
* Pass collected phase data as input, or run standalone for auto-estimation.
|
|
11
|
+
*/
|
|
12
|
+
(() => {
|
|
13
|
+
const scores = {};
|
|
14
|
+
let total = 0;
|
|
15
|
+
|
|
16
|
+
// ═══ 1. 3D COMPLEXITY (0–25 pts) ═══
|
|
17
|
+
let s3d = 0;
|
|
18
|
+
const canvases = document.querySelectorAll('canvas');
|
|
19
|
+
if (canvases.length > 0) s3d += 5;
|
|
20
|
+
if (canvases.length > 1) s3d += 5; // multiple WebGL contexts = complex
|
|
21
|
+
// Check for custom shaders (ShaderMaterial markers)
|
|
22
|
+
if (window.THREE) {
|
|
23
|
+
s3d += 5;
|
|
24
|
+
// R3F = usually more complex setup
|
|
25
|
+
const hasR3F = [...canvases].some(c => c.__r$);
|
|
26
|
+
if (hasR3F) s3d += 5;
|
|
27
|
+
}
|
|
28
|
+
if (window.BABYLON) s3d += 8;
|
|
29
|
+
if (window.unityInstance) s3d += 15; // Unity = very hard to clone
|
|
30
|
+
// Post-processing strongly implies custom shaders
|
|
31
|
+
if (document.querySelectorAll('canvas').length > 0 && window.THREE) {
|
|
32
|
+
// Heuristic: large canvas on hero = likely custom shader + post processing
|
|
33
|
+
const heroCanvas = [...document.querySelectorAll('canvas')].find(c => c.offsetWidth > 400);
|
|
34
|
+
if (heroCanvas) s3d += 5;
|
|
35
|
+
}
|
|
36
|
+
scores['3D Engine'] = Math.min(s3d, 25);
|
|
37
|
+
|
|
38
|
+
// ═══ 2. ANIMATION COMPLEXITY (0–20 pts) ═══
|
|
39
|
+
let sAnim = 0;
|
|
40
|
+
if (window.gsap) sAnim += 5;
|
|
41
|
+
if (window.ScrollTrigger?.getAll?.()?.length > 3) sAnim += 5;
|
|
42
|
+
if (window.ScrollTrigger?.getAll?.()?.length > 10) sAnim += 5;
|
|
43
|
+
if (window.ScrollSmoother) sAnim += 3;
|
|
44
|
+
if (window.anime) sAnim += 3;
|
|
45
|
+
if (document.querySelector('[data-framer-appear-id]')) sAnim += 4;
|
|
46
|
+
if (window.lottie) sAnim += 3;
|
|
47
|
+
if (window.rive) sAnim += 5;
|
|
48
|
+
if (document.getAnimations?.()?.length > 20) sAnim += 3;
|
|
49
|
+
scores['Animation System'] = Math.min(sAnim, 20);
|
|
50
|
+
|
|
51
|
+
// ═══ 3. SCROLL COMPLEXITY (0–15 pts) ═══
|
|
52
|
+
let sScroll = 0;
|
|
53
|
+
if (window.__lenis) sScroll += 5;
|
|
54
|
+
if (window.LocomotiveScroll) sScroll += 5;
|
|
55
|
+
if (window.fullPage) sScroll += 8;
|
|
56
|
+
if (CSS?.supports?.('animation-timeline', 'scroll()')) sScroll += 4;
|
|
57
|
+
const scrollTriggerCount = window.ScrollTrigger?.getAll?.()?.length || 0;
|
|
58
|
+
if (scrollTriggerCount > 5) sScroll += 3;
|
|
59
|
+
if (scrollTriggerCount > 15) sScroll += 5;
|
|
60
|
+
scores['Scroll System'] = Math.min(sScroll, 15);
|
|
61
|
+
|
|
62
|
+
// ═══ 4. INTERACTION COMPLEXITY (0–15 pts) ═══
|
|
63
|
+
let sInteract = 0;
|
|
64
|
+
const cursor = document.querySelectorAll('[class*="cursor"]');
|
|
65
|
+
if (cursor.length > 0) sInteract += 5;
|
|
66
|
+
const magnetic = document.querySelectorAll('[data-magnetic], [class*="magnetic"]');
|
|
67
|
+
if (magnetic.length > 0) sInteract += 5;
|
|
68
|
+
const parallax = document.querySelectorAll('[data-parallax], [data-speed]');
|
|
69
|
+
if (parallax.length > 3) sInteract += 5;
|
|
70
|
+
const mixBlend = [...document.querySelectorAll('*')].slice(0, 200).filter(el => {
|
|
71
|
+
try { return getComputedStyle(el).mixBlendMode !== 'normal'; } catch(e) { return false; }
|
|
72
|
+
});
|
|
73
|
+
if (mixBlend.length > 3) sInteract += 3;
|
|
74
|
+
scores['Interaction Model'] = Math.min(sInteract, 15);
|
|
75
|
+
|
|
76
|
+
// ═══ 5. PAGE TRANSITION COMPLEXITY (0–10 pts) ═══
|
|
77
|
+
let sTrans = 0;
|
|
78
|
+
if (window.barba) sTrans += 7;
|
|
79
|
+
if (window.swup) sTrans += 5;
|
|
80
|
+
if (document.startViewTransition) sTrans += 4;
|
|
81
|
+
const overlays = [...document.querySelectorAll('[class*="overlay"], [class*="curtain"], [class*="wipe"]')]
|
|
82
|
+
.filter(el => {
|
|
83
|
+
const cs = getComputedStyle(el);
|
|
84
|
+
return cs.position === 'fixed' && parseInt(cs.zIndex) > 10;
|
|
85
|
+
});
|
|
86
|
+
if (overlays.length > 0) sTrans += 3;
|
|
87
|
+
scores['Page Transitions'] = Math.min(sTrans, 10);
|
|
88
|
+
|
|
89
|
+
// ═══ 6. AUDIO COMPLEXITY (0–5 pts) ═══
|
|
90
|
+
let sAudio = 0;
|
|
91
|
+
if (window.Howl || window.Howler) sAudio += 3;
|
|
92
|
+
if (window.Tone) sAudio += 5;
|
|
93
|
+
if (window.AudioContext || window.webkitAudioContext) sAudio += 2;
|
|
94
|
+
scores['Audio System'] = Math.min(sAudio, 5);
|
|
95
|
+
|
|
96
|
+
// ═══ 7. FRAMEWORK COMPLEXITY (bonus/penalty) ═══
|
|
97
|
+
let sFramework = 0;
|
|
98
|
+
if (window.__NEXT_DATA__) sFramework = 8; // Next.js App Router = moderate setup
|
|
99
|
+
else if (window.__NUXT__) sFramework = 8;
|
|
100
|
+
else if (window.Shopify) sFramework = 5; // Shopify has constraints
|
|
101
|
+
else if (document.querySelector('meta[name="generator"][content*="Webflow"]')) sFramework = 3; // no-code = harder to clone accurately
|
|
102
|
+
else if (document.querySelector('meta[name="generator"][content*="Framer"]')) sFramework = 4;
|
|
103
|
+
else sFramework = 5; // Unknown vanilla = medium complexity
|
|
104
|
+
scores['Framework'] = sFramework;
|
|
105
|
+
|
|
106
|
+
// ═══ TOTAL ═══
|
|
107
|
+
total = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
108
|
+
|
|
109
|
+
// ═══ RATING ═══
|
|
110
|
+
let rating, emoji, estimatedDays;
|
|
111
|
+
if (total <= 20) { rating = 'Simple'; emoji = '🟢'; estimatedDays = '1–2 days'; }
|
|
112
|
+
else if (total <= 40) { rating = 'Moderate'; emoji = '🟡'; estimatedDays = '3–5 days'; }
|
|
113
|
+
else if (total <= 60) { rating = 'Complex'; emoji = '🟠'; estimatedDays = '1–2 weeks'; }
|
|
114
|
+
else if (total <= 80) { rating = 'Very Complex'; emoji = '🔴'; estimatedDays = '2–4 weeks'; }
|
|
115
|
+
else { rating = 'Extreme'; emoji = '⚫'; estimatedDays = '1–3 months'; }
|
|
116
|
+
|
|
117
|
+
// ═══ BREAKDOWN BAR ═══
|
|
118
|
+
const breakdown = Object.entries(scores).map(([k, v]) => {
|
|
119
|
+
const bar = '█'.repeat(Math.round(v / 2)) + '░'.repeat(Math.round((25 - v) / 5));
|
|
120
|
+
return `${k.padEnd(20)}: ${String(v).padStart(2)}pts ${bar}`;
|
|
121
|
+
}).join('\n');
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
score: total,
|
|
125
|
+
maxScore: 100,
|
|
126
|
+
rating,
|
|
127
|
+
emoji,
|
|
128
|
+
estimatedRebuildTime: estimatedDays,
|
|
129
|
+
breakdown: scores,
|
|
130
|
+
visualBreakdown: breakdown,
|
|
131
|
+
recommendation: total > 60
|
|
132
|
+
? 'High complexity. Focus on Phase 1 (3D shaders) and Phase 2 (GSAP timelines) first — those are the hardest to recreate.'
|
|
133
|
+
: total > 30
|
|
134
|
+
? 'Moderate complexity. Animation system is the core challenge. Extract GSAP configs precisely.'
|
|
135
|
+
: 'Lower complexity. Focus on design tokens, CSS animations, and layout structure.',
|
|
136
|
+
verdict: `${emoji} ${rating} (${total}/100) — Estimated rebuild: ${estimatedDays}`,
|
|
137
|
+
};
|
|
138
|
+
})()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 09 — Visual Diff & Clone Validation
|
|
3
|
+
* webgl-forensics skill | Phase 13 (Gap #8)
|
|
4
|
+
*
|
|
5
|
+
* Purpose: After building the clone, compare it against the source.
|
|
6
|
+
* Run on BOTH the source and clone — compare the JSON output.
|
|
7
|
+
*
|
|
8
|
+
* Automated comparison metrics:
|
|
9
|
+
* - Color palette match
|
|
10
|
+
* - Typography match
|
|
11
|
+
* - Layout section count match
|
|
12
|
+
* - Animation presence match
|
|
13
|
+
* - Navigation structure match
|
|
14
|
+
*/
|
|
15
|
+
(() => {
|
|
16
|
+
const result = {
|
|
17
|
+
url: window.location.href,
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
snapshot: {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ═══ COLOR PALETTE FINGERPRINT ═══
|
|
23
|
+
const colors = new Set();
|
|
24
|
+
const sampleEls = [...document.querySelectorAll('*')].slice(0, 300);
|
|
25
|
+
sampleEls.forEach(el => {
|
|
26
|
+
try {
|
|
27
|
+
const cs = getComputedStyle(el);
|
|
28
|
+
if (cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)') colors.add(cs.backgroundColor);
|
|
29
|
+
if (cs.color) colors.add(cs.color);
|
|
30
|
+
} catch(e) {}
|
|
31
|
+
});
|
|
32
|
+
result.snapshot.colorPalette = [...colors].slice(0, 20);
|
|
33
|
+
|
|
34
|
+
// ═══ TYPOGRAPHY FINGERPRINT ═══
|
|
35
|
+
const fonts = new Set();
|
|
36
|
+
sampleEls.forEach(el => {
|
|
37
|
+
try { fonts.add(getComputedStyle(el).fontFamily?.split(',')[0]?.trim()); } catch(e) {}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const bodyStyle = getComputedStyle(document.body);
|
|
41
|
+
result.snapshot.typography = {
|
|
42
|
+
bodyFont: bodyStyle.fontFamily?.split(',')[0]?.trim(),
|
|
43
|
+
bodySize: bodyStyle.fontSize,
|
|
44
|
+
bodyColor: bodyStyle.color,
|
|
45
|
+
bodyBg: bodyStyle.backgroundColor,
|
|
46
|
+
uniqueFonts: [...fonts].filter(Boolean).slice(0, 10),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ═══ LAYOUT FINGERPRINT ═══
|
|
50
|
+
const sections = document.querySelectorAll('section, [class*="section"], main > div, [data-section]');
|
|
51
|
+
result.snapshot.layout = {
|
|
52
|
+
sectionCount: sections.length,
|
|
53
|
+
totalPageHeight: document.documentElement.scrollHeight,
|
|
54
|
+
viewportHeight: window.innerHeight,
|
|
55
|
+
navExists: !!document.querySelector('nav, header, [role="navigation"]'),
|
|
56
|
+
footerExists: !!document.querySelector('footer, [class*="footer"]'),
|
|
57
|
+
hasH1: !!document.querySelector('h1'),
|
|
58
|
+
h1Text: document.querySelector('h1')?.textContent?.trim().slice(0, 60),
|
|
59
|
+
linkCount: document.querySelectorAll('a').length,
|
|
60
|
+
imageCount: document.querySelectorAll('img').length,
|
|
61
|
+
canvasCount: document.querySelectorAll('canvas').length,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ═══ ANIMATION PRESENCE FINGERPRINT ═══
|
|
65
|
+
result.snapshot.animations = {
|
|
66
|
+
hasGSAP: !!window.gsap,
|
|
67
|
+
scrollTriggerCount: window.ScrollTrigger?.getAll?.()?.length || 0,
|
|
68
|
+
hasLenis: !!(window.__lenis || document.querySelector('[data-lenis-prevent]')),
|
|
69
|
+
hasLocomotive: !!(window.LocomotiveScroll || document.querySelector('[data-scroll-container]')),
|
|
70
|
+
cssAnimationCount: (() => {
|
|
71
|
+
let count = 0;
|
|
72
|
+
sampleEls.forEach(el => {
|
|
73
|
+
try { if (getComputedStyle(el).animationName !== 'none') count++; } catch(e) {}
|
|
74
|
+
});
|
|
75
|
+
return count;
|
|
76
|
+
})(),
|
|
77
|
+
waapiCount: document.getAnimations?.()?.length || 0,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ═══ NAVIGATION FINGERPRINT ═══
|
|
81
|
+
const nav = document.querySelector('nav, header, [role="navigation"]');
|
|
82
|
+
result.snapshot.navigation = nav ? {
|
|
83
|
+
linkCount: nav.querySelectorAll('a').length,
|
|
84
|
+
links: [...nav.querySelectorAll('a')].map(a => a.textContent?.trim().slice(0, 30)).filter(Boolean),
|
|
85
|
+
hasLogo: !!(nav.querySelector('svg, img, [class*="logo"]')),
|
|
86
|
+
} : null;
|
|
87
|
+
|
|
88
|
+
// ═══ COMPARISON INSTRUCTIONS ═══
|
|
89
|
+
result.instructions = [
|
|
90
|
+
'1. Run this script on SOURCE site → save output as source-snapshot.json',
|
|
91
|
+
'2. Run this script on CLONE site → save output as clone-snapshot.json',
|
|
92
|
+
'3. Compare key fields:',
|
|
93
|
+
' - colorPalette: Colors should match within tolerance',
|
|
94
|
+
' - typography.bodyFont: Must match exactly',
|
|
95
|
+
' - layout.sectionCount: Should match ±1',
|
|
96
|
+
' - layout.canvasCount: Must match',
|
|
97
|
+
' - animations.scrollTriggerCount: Should be equal or close',
|
|
98
|
+
'4. Take full-page screenshots of both and visually compare',
|
|
99
|
+
'5. Use Chrome DevTools > Rendering > Show paint flashing to verify scroll performance',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// ═══ PASS/FAIL CRITERIA (fill in manually after comparison) ═══
|
|
103
|
+
result.passFailTemplate = {
|
|
104
|
+
bodyFont: '[ ] SOURCE: _____ | CLONE: _____ | MATCH: Y/N',
|
|
105
|
+
backgroundColor: '[ ] SOURCE: _____ | CLONE: _____ | MATCH: Y/N',
|
|
106
|
+
sectionCount: '[ ] SOURCE: _____ | CLONE: _____ | MATCH: Y/N',
|
|
107
|
+
canvasExists: '[ ] SOURCE: _____ | CLONE: _____ | MATCH: Y/N',
|
|
108
|
+
scrollSmooth: '[ ] SOURCE: _____ | CLONE: _____ | MATCH: Y/N',
|
|
109
|
+
h1Match: '[ ] SOURCE: _____ | CLONE: _____ | MATCH: Y/N',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
})()
|