Initial commit

This commit is contained in:
Samraaj Bath
2026-06-29 15:11:48 -07:00
commit 66dfdcc58d
404 changed files with 45970 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
// Initializes the shared capture bucket that every other hook writes into.
// Runs first because hooks load in lexicographic order.
(() => {
if (window.__CLONE_CAPTURE__) return;
window.__CLONE_CAPTURE__ = {
shaders: [],
gsap: [],
framer: [],
lottie: [],
threejs: [],
cssVars: {},
fonts: [],
};
// Sentinel the capture script waits on after DOMContentLoaded + a tick.
// Hooks that dump state on demand (gsap, framer) set __CLONE_READY__ when they've run.
window.__CLONE_READY__ = false;
window.addEventListener('load', () => {
setTimeout(() => {
try {
if (typeof window.__CLONE_FINALIZE__ === 'function') window.__CLONE_FINALIZE__();
} catch {}
window.__CLONE_READY__ = true;
}, 800);
});
})();
@@ -0,0 +1,193 @@
// Exposes window.__CLONE_DUMP_COMPUTED__() which walks the DOM and returns a minimal tree
// of resolved computed styles (only non-default values). The capture script calls this on demand
// after each scroll step — cheap to call repeatedly, much richer than the inline per-step snapshot
// the Playwright script does as a fallback.
(() => {
const PROPS = [
'color',
'backgroundColor',
'backgroundImage',
'backgroundSize',
'backgroundPosition',
'backgroundRepeat',
'backgroundAttachment',
'backgroundClip',
'fontFamily',
'fontSize',
'fontWeight',
'lineHeight',
'letterSpacing',
'textTransform',
'textAlign',
'textDecoration',
'textShadow',
'whiteSpace',
'display',
'flexDirection',
'flexWrap',
'justifyContent',
'alignItems',
'alignContent',
'alignSelf',
'gap',
'rowGap',
'columnGap',
'gridTemplateColumns',
'gridTemplateRows',
'gridTemplateAreas',
'gridColumn',
'gridRow',
'gridAutoFlow',
'padding',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'margin',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'width',
'height',
'maxWidth',
'maxHeight',
'minWidth',
'minHeight',
'aspectRatio',
'position',
'top',
'right',
'bottom',
'left',
'zIndex',
'inset',
'border',
'borderTop',
'borderRight',
'borderBottom',
'borderLeft',
'borderRadius',
'borderColor',
'borderWidth',
'borderStyle',
'boxShadow',
'opacity',
'transform',
'transformOrigin',
'filter',
'backdropFilter',
'clipPath',
'mask',
'maskImage',
'mixBlendMode',
'isolation',
'overflow',
'overflowX',
'overflowY',
'scrollBehavior',
'scrollSnapType',
'cursor',
'pointerEvents',
'userSelect',
'willChange',
'contain',
];
// Compute defaults once per tag by creating a hidden iframe so we don't treat
// inherited defaults as "used styles". The iframe is created lazily — at
// init-script time, document.documentElement may still be null (Playwright
// fires add_init_script before the HTML parser materialises <html>).
let iframe = null;
function ensureIframe() {
if (iframe) return iframe;
if (!document.documentElement) return null;
try {
iframe = document.createElement('iframe');
iframe.style.cssText = 'position:absolute;width:0;height:0;border:0;opacity:0;pointer-events:none';
iframe.srcdoc = '<!doctype html><html><body></body></html>';
document.documentElement.appendChild(iframe);
} catch {
iframe = null;
}
return iframe;
}
const defaultsByTag = new Map();
function defaultsFor(tag) {
if (defaultsByTag.has(tag)) return defaultsByTag.get(tag);
const ifr = ensureIframe();
const doc = ifr ? ifr.contentDocument : null;
if (!doc || !doc.body) return {};
const el = doc.createElement(tag);
doc.body.appendChild(el);
const cs = doc.defaultView.getComputedStyle(el);
const d = {};
for (const p of PROPS) d[p] = cs[p];
doc.body.removeChild(el);
defaultsByTag.set(tag, d);
return d;
}
function nonDefault(el) {
const cs = getComputedStyle(el);
const defaults = defaultsFor(el.tagName.toLowerCase());
const out = {};
for (const p of PROPS) {
const v = cs[p];
if (v == null || v === '') continue;
if (v === defaults[p]) continue;
out[p] = v;
}
return out;
}
function pseudoStyles(el) {
const out = {};
for (const pseudo of ['::before', '::after']) {
try {
const cs = getComputedStyle(el, pseudo);
const content = cs.content;
if (content && content !== 'normal' && content !== 'none') {
const block = {};
for (const p of PROPS) {
const v = cs[p];
if (v && v !== 'none' && v !== 'normal' && v !== 'auto') block[p] = v;
}
block.content = content;
out[pseudo] = block;
}
} catch {}
}
return Object.keys(out).length ? out : undefined;
}
function serialize(el) {
if (el.nodeType === Node.TEXT_NODE) {
const t = el.textContent;
return t && t.trim() ? { text: t } : null;
}
if (el.nodeType !== Node.ELEMENT_NODE) return null;
if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.tagName === 'NOSCRIPT') return null;
const attrs = {};
for (const a of el.attributes) attrs[a.name] = a.value;
const computed = nonDefault(el);
const pseudo = pseudoStyles(el);
if (pseudo) computed.pseudo = pseudo;
const r = el.getBoundingClientRect();
const children = [];
for (const c of el.childNodes) {
const s = serialize(c);
if (s) children.push(s);
}
return {
tag: el.tagName.toLowerCase(),
attrs,
computed,
bbox: { x: r.x, y: r.y + window.scrollY, width: r.width, height: r.height },
children,
};
}
window.__CLONE_DUMP_COMPUTED__ = () => serialize(document.body);
})();
+121
View File
@@ -0,0 +1,121 @@
// Dumps every same-origin CSS rule as { selector, cssText } into __CLONE_CAPTURE__.cssRules,
// and extracts every url(...) asset reference (resolved to absolute URLs) into
// __CLONE_CAPTURE__.cssAssets. Catches background-images set in stylesheets that the
// response listener missed (e.g. @media-gated, ::before, mask-image, list-style-image, cursor).
(() => {
function isSameOrigin(href) {
if (!href) return true;
try {
const u = new URL(href, location.href);
return u.origin === location.origin;
} catch {
return false;
}
}
function resolveUrl(raw, baseHref) {
if (!raw) return null;
let s = raw.trim();
if (s.startsWith('"') || s.startsWith("'")) s = s.slice(1, -1);
if (s.startsWith('data:') || s.startsWith('#')) return null;
try {
return new URL(s, baseHref || location.href).href;
} catch {
return null;
}
}
function collectFromRules(rules, baseHref, out) {
if (!rules) return;
for (const rule of rules) {
const type = rule.constructor?.name || '';
if (type === 'CSSStyleRule') {
const text = rule.cssText || '';
out.rules.push({ selector: rule.selectorText, cssText: text, baseHref });
const urls = text.match(/url\(\s*([^)]+?)\s*\)/g) || [];
for (const m of urls) {
const inner = m
.slice(4, -1)
.trim()
.replace(/^['"]|['"]$/g, '');
const abs = resolveUrl(inner, baseHref);
if (abs && !out.assetSet.has(abs)) {
out.assetSet.add(abs);
out.assets.push({ source_url: abs, selector: rule.selectorText });
}
}
} else if (type === 'CSSMediaRule' || type === 'CSSSupportsRule' || type === 'CSSContainerRule') {
const condition = rule.conditionText || rule.media?.mediaText;
out.media.push({ condition, type });
collectFromRules(rule.cssRules, baseHref, out);
} else if (type === 'CSSImportRule') {
// Recurse into imported stylesheet if same-origin and accessible
try {
const importedHref = rule.styleSheet?.href || baseHref;
if (isSameOrigin(importedHref)) {
collectFromRules(rule.styleSheet?.cssRules, importedHref, out);
}
} catch {}
} else if (type === 'CSSFontFaceRule') {
const text = rule.cssText || '';
out.fontFaces.push({ cssText: text, baseHref });
const urls = text.match(/url\(\s*([^)]+?)\s*\)/g) || [];
for (const m of urls) {
const inner = m
.slice(4, -1)
.trim()
.replace(/^['"]|['"]$/g, '');
const abs = resolveUrl(inner, baseHref);
if (abs && !out.assetSet.has(abs)) {
out.assetSet.add(abs);
out.assets.push({ source_url: abs, selector: '@font-face' });
}
}
} else if (type === 'CSSKeyframesRule') {
const frames = [];
for (const k of rule.cssRules || []) frames.push({ key: k.keyText, cssText: k.style?.cssText || '' });
out.keyframes.push({ name: rule.name, frames, baseHref });
} else if (rule.cssRules) {
collectFromRules(rule.cssRules, baseHref, out);
}
}
}
function collect() {
const out = {
rules: [],
media: [],
fontFaces: [],
keyframes: [],
assets: [],
assetSet: new Set(),
crossOrigin: 0,
};
for (const sheet of document.styleSheets) {
const baseHref = sheet.href || location.href;
let rules;
try {
rules = sheet.cssRules;
} catch {
out.crossOrigin += 1;
continue;
}
collectFromRules(rules, baseHref, out);
}
delete out.assetSet;
window.__CLONE_CAPTURE__.cssRules = out;
}
const prevFinalize = window.__CLONE_FINALIZE__;
window.__CLONE_FINALIZE__ = function () {
if (prevFinalize)
try {
prevFinalize();
} catch {}
try {
collect();
} catch (e) {
window.__CLONE_CAPTURE__.cssRules = { error: String(e) };
}
};
})();
+51
View File
@@ -0,0 +1,51 @@
// Dumps every :root --custom-property and every @property registration into
// window.__CLONE_CAPTURE__.cssVars, keyed under { custom, registered }.
(() => {
function collect() {
const custom = {};
try {
const cs = getComputedStyle(document.documentElement);
for (let i = 0; i < cs.length; i++) {
const name = cs[i];
if (!name.startsWith('--')) continue;
try {
custom[name] = cs.getPropertyValue(name).trim();
} catch {}
}
} catch {}
const registered = [];
try {
for (const sheet of document.styleSheets) {
let rules;
try {
rules = sheet.cssRules;
} catch {
continue;
}
if (!rules) continue;
for (const rule of rules) {
if (rule.constructor?.name === 'CSSPropertyRule' || rule.type === 18 /* @property */) {
registered.push({
name: rule.name,
syntax: rule.syntax,
initialValue: rule.initialValue,
inherits: rule.inherits,
});
}
}
}
} catch {}
window.__CLONE_CAPTURE__.cssVars = { custom, registered };
}
const prevFinalize = window.__CLONE_FINALIZE__;
window.__CLONE_FINALIZE__ = function () {
if (prevFinalize)
try {
prevFinalize();
} catch {}
collect();
};
})();
+107
View File
@@ -0,0 +1,107 @@
// Walks the React fiber tree (via __REACT_DEVTOOLS_GLOBAL_HOOK__) to find framer-motion
// components and dump their animate/variants/transition/whileInView/whileHover props.
// Called from __CLONE_FINALIZE__.
(() => {
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${el.id}`;
const parts = [];
while (el && el.nodeType === 1 && parts.length < 10) {
let s = el.tagName.toLowerCase();
const p = el.parentNode;
if (p) {
const sibs = Array.from(p.children).filter((c) => c.tagName === el.tagName);
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
}
parts.unshift(s);
el = el.parentNode;
}
return parts.join(' > ');
}
function isMotionComponent(fiber) {
const type = fiber?.type;
if (!type) return false;
if (type.$$typeof && type.render && type.render.displayName?.startsWith('motion.')) return true;
const dn = type.displayName || type.name;
if (typeof dn === 'string' && (dn.startsWith('motion.') || dn === 'MotionComponent')) return true;
return false;
}
function serializeProp(v) {
try {
if (v == null) return v;
if (typeof v === 'function') return null;
if (typeof v === 'object') {
const out = {};
for (const k of Object.keys(v)) {
const val = v[k];
if (typeof val === 'function') continue;
if (val && typeof val === 'object' && val.nodeType) continue;
out[k] = val;
}
return out;
}
return v;
} catch {
return null;
}
}
function findFibers(fiber, out) {
if (!fiber) return;
if (isMotionComponent(fiber)) {
const props = fiber.memoizedProps || {};
let node = fiber.stateNode;
if (!(node instanceof Element)) {
// Walk down to the first DOM host for selector extraction.
let cursor = fiber.child;
while (cursor) {
if (cursor.stateNode instanceof Element) {
node = cursor.stateNode;
break;
}
cursor = cursor.child;
}
}
out.push({
selector: node instanceof Element ? cssPath(node) : null,
initial: serializeProp(props.initial),
animate: serializeProp(props.animate),
exit: serializeProp(props.exit),
variants: serializeProp(props.variants),
transition: serializeProp(props.transition),
whileHover: serializeProp(props.whileHover),
whileTap: serializeProp(props.whileTap),
whileInView: serializeProp(props.whileInView),
viewport: serializeProp(props.viewport),
});
}
if (fiber.child) findFibers(fiber.child, out);
if (fiber.sibling) findFibers(fiber.sibling, out);
}
const prevFinalize = window.__CLONE_FINALIZE__;
window.__CLONE_FINALIZE__ = function () {
if (prevFinalize)
try {
prevFinalize();
} catch {}
try {
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (!hook || !hook.renderers) return;
for (const [, renderer] of hook.renderers) {
if (!renderer) continue;
const roots = renderer.findFiberByHostInstance ? null : renderer.roots || null;
const scanFrom = [];
if (hook.getFiberRoots) {
const rootSet = hook.getFiberRoots(1) || new Set();
for (const r of rootSet) scanFrom.push(r.current);
}
for (const fiber of scanFrom) findFibers(fiber, window.__CLONE_CAPTURE__.framer);
}
} catch (e) {
window.__CLONE_CAPTURE__.framer.push({ error: String(e) });
}
};
})();
+103
View File
@@ -0,0 +1,103 @@
// Dumps GSAP timelines + tweens into window.__CLONE_CAPTURE__.gsap after load.
// Runs on __CLONE_FINALIZE__ so that page-load animations have had time to register.
(() => {
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${el.id}`;
const parts = [];
while (el && el.nodeType === 1 && parts.length < 10) {
let s = el.tagName.toLowerCase();
const p = el.parentNode;
if (p) {
const sibs = Array.from(p.children).filter((c) => c.tagName === el.tagName);
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
}
parts.unshift(s);
el = el.parentNode;
}
return parts.join(' > ');
}
function targetsToSelectors(targets) {
if (!targets) return [];
if (targets instanceof Element) return [cssPath(targets)];
if (Array.isArray(targets) || targets.length != null) {
const out = [];
for (let i = 0; i < targets.length; i++) {
const t = targets[i];
if (t instanceof Element) out.push(cssPath(t));
else if (typeof t === 'string') out.push(t);
}
return out;
}
if (typeof targets === 'string') return [targets];
return [];
}
function serializeEase(ease) {
if (!ease) return null;
if (typeof ease === 'string') return ease;
if (typeof ease === 'function') return ease.name || ease.toString().slice(0, 40);
return String(ease);
}
function serializeTween(t) {
const vars = {};
if (t.vars) {
for (const k of Object.keys(t.vars)) {
if (['onComplete', 'onStart', 'onUpdate', 'onRepeat', 'scrollTrigger'].includes(k)) continue;
try {
const v = t.vars[k];
if (typeof v === 'function') continue;
if (v && typeof v === 'object' && v.nodeType) continue;
vars[k] = v;
} catch {}
}
}
return {
type: 'tween',
targets: targetsToSelectors(t._targets || t.targets?.()),
duration: t.duration?.() ?? t._duration,
ease: serializeEase(t.vars?.ease),
vars,
scrollTrigger: t.vars?.scrollTrigger
? {
trigger:
t.vars.scrollTrigger.trigger instanceof Element
? cssPath(t.vars.scrollTrigger.trigger)
: t.vars.scrollTrigger.trigger,
start: t.vars.scrollTrigger.start,
end: t.vars.scrollTrigger.end,
scrub: t.vars.scrollTrigger.scrub,
pin: t.vars.scrollTrigger.pin,
}
: null,
};
}
function walk(tl) {
const out = { type: 'timeline', duration: tl.duration?.(), children: [] };
const kids = tl.getChildren ? tl.getChildren(true, true, true) : [];
for (const c of kids) {
if (c.getChildren) out.children.push(walk(c));
else out.children.push(serializeTween(c));
}
return out;
}
const prevFinalize = window.__CLONE_FINALIZE__;
window.__CLONE_FINALIZE__ = function () {
if (prevFinalize)
try {
prevFinalize();
} catch {}
try {
if (!window.gsap) return;
const root = window.gsap.globalTimeline;
if (!root) return;
window.__CLONE_CAPTURE__.gsap.push(walk(root));
} catch (e) {
window.__CLONE_CAPTURE__.gsap.push({ error: String(e) });
}
};
})();
@@ -0,0 +1,47 @@
// Pulls registered Lottie animations + their animationData into window.__CLONE_CAPTURE__.lottie.
// Called from __CLONE_FINALIZE__. Supports lottie-web directly; lottie-react uses lottie-web internally.
(() => {
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${el.id}`;
const parts = [];
while (el && el.nodeType === 1 && parts.length < 10) {
let s = el.tagName.toLowerCase();
const p = el.parentNode;
if (p) {
const sibs = Array.from(p.children).filter((c) => c.tagName === el.tagName);
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
}
parts.unshift(s);
el = el.parentNode;
}
return parts.join(' > ');
}
const prevFinalize = window.__CLONE_FINALIZE__;
window.__CLONE_FINALIZE__ = function () {
if (prevFinalize)
try {
prevFinalize();
} catch {}
try {
const lottie = window.lottie || window.bodymovin;
if (!lottie || typeof lottie.getRegisteredAnimations !== 'function') return;
const anims = lottie.getRegisteredAnimations();
for (const a of anims) {
try {
window.__CLONE_CAPTURE__.lottie.push({
selector: a.wrapper instanceof Element ? cssPath(a.wrapper) : null,
renderer: a.renderer?.name || 'unknown',
animationData: a.animationData ? JSON.parse(JSON.stringify(a.animationData)) : null,
loop: a.loop,
autoplay: a.autoplay,
name: a.name,
});
} catch {}
}
} catch (e) {
window.__CLONE_CAPTURE__.lottie.push({ error: String(e) });
}
};
})();
@@ -0,0 +1,147 @@
// Exposes window.__CLONE_LIST_SECTIONS__() which returns a list of candidate top-level
// section bounding boxes for the capture script's scroll planner. We emit one entry per
// large flex/block child of <body>, <main>, or any element classed as section/container/etc.
// The capture script uses these to scroll-to-section instead of fixed-pixel stepping.
(() => {
const SECTIONLIKE_TAGS = new Set(['section', 'header', 'footer', 'nav', 'main', 'article', 'aside']);
const SECTIONLIKE_CLASS_RE =
/(^|\s)(section|hero|container|banner|elementor-section|elementor-element-[^\s]+|e-con|e-parent|wp-block-[^\s]+)(\s|$)/i;
function isViewportWide(el, vw) {
const r = el.getBoundingClientRect();
return r.width >= vw * 0.5 && r.height >= 80;
}
function isVisible(el) {
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
if (parseFloat(cs.opacity) === 0) return false;
return true;
}
function looksLikeSection(el) {
const tag = el.tagName.toLowerCase();
if (SECTIONLIKE_TAGS.has(tag)) return true;
if (el.id && /(hero|banner|section|footer|nav|header)/i.test(el.id)) return true;
if (typeof el.className === 'string' && SECTIONLIKE_CLASS_RE.test(el.className)) return true;
return false;
}
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${el.id}`;
const parts = [];
while (el && el.nodeType === 1 && parts.length < 8) {
let s = el.tagName.toLowerCase();
const p = el.parentNode;
if (p && p.nodeType === 1) {
const sibs = Array.from(p.children).filter((c) => c.tagName === el.tagName);
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
}
parts.unshift(s);
el = el.parentNode;
}
return parts.join(' > ');
}
const SECTION_QUERY =
'section, header, footer, nav, main, article, ' +
'[class*="section"], [class*="elementor-element"], [class*="e-parent"], [class*="e-con"], ' +
'[class*="hero"], [class*="banner"], [id*="section"], [id*="hero"], [id*="banner"]';
function snapshotEntry(el, vw) {
const r = el.getBoundingClientRect();
return {
element: el,
selector: cssPath(el),
tag: el.tagName.toLowerCase(),
id: el.id || null,
className: typeof el.className === 'string' ? el.className.slice(0, 200) : null,
x: r.x,
y: r.y + window.scrollY,
width: r.width,
height: r.height,
looksLikeSection: looksLikeSection(el),
};
}
function collectInScope(scope, vw, out, candidates) {
for (const el of candidates) {
if (!scope.contains(el) || el === scope) continue;
if (!isVisible(el) || !isViewportWide(el, vw)) continue;
// Outermost-wins inside this scope
let skip = false;
for (const o of out) {
if (o.element.contains(el)) {
skip = true;
break;
}
}
if (skip) continue;
for (let i = out.length - 1; i >= 0; i--) {
if (el.contains(out[i].element)) out.splice(i, 1);
}
out.push(snapshotEntry(el, vw));
}
}
window.__CLONE_LIST_SECTIONS__ = () => {
const vw = window.innerWidth;
const vh = window.innerHeight;
const docHeight = Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0);
// Pass 1: known section-like elements
const candidates = Array.from(document.querySelectorAll(SECTION_QUERY));
// Pass 2: large direct children of body/main as a fallback
const root = document.querySelector('main') || document.body;
if (root) {
for (const child of root.children) {
if (!candidates.includes(child)) candidates.push(child);
}
}
// First-pass dedup with outermost-wins
const out = [];
for (const el of candidates) {
if (!isVisible(el) || !isViewportWide(el, vw)) continue;
let skip = false;
for (const o of out) {
if (o.element.contains(el)) {
skip = true;
break;
}
}
if (skip) continue;
for (let i = out.length - 1; i >= 0; i--) {
if (el.contains(out[i].element)) out.splice(i, 1);
}
out.push(snapshotEntry(el, vw));
}
// Pass 3: oversized-wrapper expansion. WordPress / Elementor sites commonly
// wrap the whole page in <main id="main">. Without this step the outer
// wrapper wins and individual sections are dropped, so the smart-scroll
// loop never visits them and lazy backgrounds never fire.
const oversizedThreshold = Math.max(vh * 2, docHeight * 0.5);
const expanded = [];
for (const sec of out) {
if (sec.height < oversizedThreshold) {
expanded.push(sec);
continue;
}
const inner = [];
const innerCandidates = Array.from(sec.element.querySelectorAll(SECTION_QUERY));
collectInScope(sec.element, vw, inner, innerCandidates);
// Only replace the wrapper if recursion produced enough sections to be
// meaningful — otherwise this is a genuine large section, not a wrapper.
if (inner.length >= 3) expanded.push(...inner);
else expanded.push(sec);
}
return expanded
.filter((s) => s.height > 80)
.sort((a, b) => a.y - b.y)
.map(({ element, ...rest }) => rest);
};
})();
+97
View File
@@ -0,0 +1,97 @@
// Extract GLSL from WebGL1 + WebGL2 by intercepting shaderSource / compileShader / linkProgram / useProgram.
// Stashes into window.__CLONE_CAPTURE__.shaders as
// { programId, vertex, fragment, attributes, uniforms, canvasSelector }
(() => {
const CAPTURE = () => window.__CLONE_CAPTURE__;
const shaderMap = new WeakMap(); // WebGLShader -> { type, source }
const programMap = new WeakMap(); // WebGLProgram -> { vertex, fragment }
let nextProgramId = 1;
const contextCanvas = new WeakMap(); // WebGL(2)RenderingContext -> canvas
function patch(ctxProto, isGL2) {
const origShaderSource = ctxProto.shaderSource;
ctxProto.shaderSource = function (shader, source) {
shaderMap.set(shader, { source, type: this.getShaderParameter?.(shader, this.SHADER_TYPE) });
return origShaderSource.call(this, shader, source);
};
const origAttach = ctxProto.attachShader;
ctxProto.attachShader = function (program, shader) {
const info = shaderMap.get(shader);
const t = this.getShaderParameter(shader, this.SHADER_TYPE);
const entry = programMap.get(program) || {};
if (t === this.VERTEX_SHADER) entry.vertex = info?.source || '';
else if (t === this.FRAGMENT_SHADER) entry.fragment = info?.source || '';
programMap.set(program, entry);
return origAttach.call(this, program, shader);
};
const origLink = ctxProto.linkProgram;
ctxProto.linkProgram = function (program) {
const res = origLink.call(this, program);
const entry = programMap.get(program) || {};
if (!entry.id) entry.id = nextProgramId++;
const canvas = contextCanvas.get(this);
const sel = canvas ? cssPath(canvas) : null;
const attributes = [];
const uniforms = [];
try {
const aCount = this.getProgramParameter(program, this.ACTIVE_ATTRIBUTES);
for (let i = 0; i < aCount; i++) {
const info = this.getActiveAttrib(program, i);
if (info) attributes.push({ name: info.name, type: info.type, size: info.size });
}
const uCount = this.getProgramParameter(program, this.ACTIVE_UNIFORMS);
for (let i = 0; i < uCount; i++) {
const info = this.getActiveUniform(program, i);
if (info) uniforms.push({ name: info.name, type: info.type, size: info.size });
}
} catch {}
CAPTURE().shaders.push({
programId: entry.id,
vertex: entry.vertex || '',
fragment: entry.fragment || '',
attributes,
uniforms,
canvasSelector: sel,
isWebGL2: isGL2,
});
return res;
};
}
// Intercept getContext to remember which canvas owns which WebGL context.
const origGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, ...rest) {
const ctx = origGetContext.call(this, type, ...rest);
if (ctx && (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl')) {
contextCanvas.set(ctx, this);
}
return ctx;
};
if (typeof WebGLRenderingContext !== 'undefined') patch(WebGLRenderingContext.prototype, false);
if (typeof WebGL2RenderingContext !== 'undefined') patch(WebGL2RenderingContext.prototype, true);
function cssPath(el) {
if (!el) return null;
const parts = [];
while (el && el.nodeType === 1 && parts.length < 10) {
let s = el.tagName.toLowerCase();
if (el.id) {
s += '#' + el.id;
parts.unshift(s);
break;
}
const parent = el.parentNode;
if (parent) {
const sibs = Array.from(parent.children).filter((c) => c.tagName === el.tagName);
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
}
parts.unshift(s);
el = el.parentNode;
}
return parts.join(' > ');
}
})();
@@ -0,0 +1,130 @@
// Patches THREE.WebGLRenderer to track its canvas + scene root, then serializes the scene graph
// at __CLONE_FINALIZE__. Records geometry types, material types + uniforms, lights, cameras,
// and per-object position/rotation/scale.
(() => {
const renderers = new Set();
const rendererToScene = new WeakMap();
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${el.id}`;
const parts = [];
while (el && el.nodeType === 1 && parts.length < 10) {
let s = el.tagName.toLowerCase();
const p = el.parentNode;
if (p) {
const sibs = Array.from(p.children).filter((c) => c.tagName === el.tagName);
if (sibs.length > 1) s += `:nth-of-type(${sibs.indexOf(el) + 1})`;
}
parts.unshift(s);
el = el.parentNode;
}
return parts.join(' > ');
}
function patchWhenReady() {
if (!window.THREE || !window.THREE.WebGLRenderer) return false;
const WR = window.THREE.WebGLRenderer;
const origRender = WR.prototype.render;
WR.prototype.render = function (scene, camera) {
renderers.add(this);
if (scene) rendererToScene.set(this, { scene, camera });
return origRender.call(this, scene, camera);
};
return true;
}
if (!patchWhenReady()) {
const timer = setInterval(() => {
if (patchWhenReady()) clearInterval(timer);
}, 200);
setTimeout(() => clearInterval(timer), 10_000);
}
function serializeObject(obj) {
const out = {
type: obj.type || obj.constructor?.name || 'Object3D',
name: obj.name || undefined,
position: obj.position ? [obj.position.x, obj.position.y, obj.position.z] : undefined,
rotation: obj.rotation ? [obj.rotation.x, obj.rotation.y, obj.rotation.z] : undefined,
scale: obj.scale ? [obj.scale.x, obj.scale.y, obj.scale.z] : undefined,
visible: obj.visible,
children: [],
};
if (obj.geometry) {
out.geometry = {
type: obj.geometry.type,
parameters: obj.geometry.parameters ? { ...obj.geometry.parameters } : undefined,
};
}
if (obj.material) {
const m = Array.isArray(obj.material) ? obj.material[0] : obj.material;
out.material = {
type: m.type,
color: m.color ? m.color.getHex?.().toString(16) : undefined,
opacity: m.opacity,
transparent: m.transparent,
uniforms: m.uniforms ? summarizeUniforms(m.uniforms) : undefined,
vertexShader: m.vertexShader?.slice(0, 4000),
fragmentShader: m.fragmentShader?.slice(0, 4000),
};
}
if (obj.isLight) {
out.light = {
type: obj.type,
color: obj.color?.getHex?.()?.toString(16),
intensity: obj.intensity,
};
}
if (obj.isCamera) {
out.camera = {
type: obj.type,
fov: obj.fov,
aspect: obj.aspect,
near: obj.near,
far: obj.far,
};
}
if (obj.children?.length) {
out.children = obj.children.map(serializeObject);
}
return out;
}
function summarizeUniforms(u) {
const out = {};
for (const k of Object.keys(u)) {
try {
const v = u[k]?.value;
if (v == null) continue;
if (typeof v === 'number') out[k] = v;
else if (Array.isArray(v)) out[k] = v.slice(0, 16);
else if (v.x != null && v.y != null) out[k] = [v.x, v.y, v.z ?? 0, v.w ?? 0];
else out[k] = String(v).slice(0, 80);
} catch {}
}
return out;
}
const prevFinalize = window.__CLONE_FINALIZE__;
window.__CLONE_FINALIZE__ = function () {
if (prevFinalize)
try {
prevFinalize();
} catch {}
try {
for (const r of renderers) {
const { scene, camera } = rendererToScene.get(r) || {};
const dom = r.domElement;
window.__CLONE_CAPTURE__.threejs.push({
canvasSelector: dom instanceof Element ? cssPath(dom) : null,
size: dom ? [dom.width, dom.height] : null,
scene: scene ? serializeObject(scene) : null,
camera: camera ? serializeObject(camera) : null,
});
}
} catch (e) {
window.__CLONE_CAPTURE__.threejs.push({ error: String(e) });
}
};
})();