mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
- Gate stripStudioEditsFromTarget/bakeVisibilityOnDelete behind a stripStudioEdits flag on the delete mutation type so they only fire on user-initiated deletes, not on internal delete-then-recreate drags. - Add bakeVisibilityOnDelete to the remove-all-keyframes handler so elements with CSS opacity:0 stay visible after collapsing keyframes. - Fix integer rounding in readAllAnimatedProperties: use 3-decimal precision for visual properties (opacity, scale, rotation) instead of Math.round which corrupted mid-fade values to 0. - Guard VISUAL_BASELINE against cross-tween contamination by querying __timelines for properties animated by other tweens on the same element. - Harden bakeVisibilityOnDelete: reverse-scan keyframes for the last one containing opacity, guard against relative values (+=/-=/*=), and add Number.isFinite check. - Fix falsy-zero doubling in drag commit: replace || fallback with Number.isFinite so a base GSAP position of 0 is correctly preserved. - Fix gesture recording sign inversion: remove pointerElementOffset subtraction from dx/dy formula and instead apply it once to basePosition so the element center tracks the pointer. - Fix TypeScript build errors in gsapSoftReload.ts (6 double-casts). - Strip all diagnostic logs from production code.
127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
type IframeWindow = Window & {
|
|
__timelines?: Record<string, { kill?: () => void; pause?: () => void }>;
|
|
__player?: { getTime?: () => number; seek?: (t: number) => void };
|
|
__hfForceTimelineRebind?: () => void;
|
|
__hfSuppressSceneMutations?: <T>(fn: () => T) => T;
|
|
__hfStudioManualEditsApply?: () => void;
|
|
gsap?: {
|
|
timeline?: (...args: unknown[]) => unknown;
|
|
registerPlugin?: (...plugins: unknown[]) => unknown;
|
|
};
|
|
MotionPathPlugin?: unknown;
|
|
};
|
|
|
|
function isGsapScript(text: string): boolean {
|
|
return (
|
|
text.includes("gsap.timeline") ||
|
|
text.includes("__timelines") ||
|
|
text.includes(".to(") ||
|
|
text.includes(".set(")
|
|
);
|
|
}
|
|
|
|
function findGsapScriptElements(doc: Document): HTMLScriptElement[] {
|
|
const results: HTMLScriptElement[] = [];
|
|
const scripts = doc.querySelectorAll<HTMLScriptElement>("script:not([src])");
|
|
for (const script of scripts) {
|
|
if (isGsapScript(script.textContent || "")) results.push(script);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Replace the GSAP script in the live iframe without reloading. This preserves
|
|
* the WebGL context and shader transition cache.
|
|
*
|
|
* Scoped to root-document GSAP scripts only — scripts inside `<template>`
|
|
* elements (sub-compositions) are not visible to `querySelectorAll` and will
|
|
* fall back to a full iframe reload.
|
|
*
|
|
* Returns false (triggering a full reload fallback) when:
|
|
* - The iframe or GSAP runtime isn't available
|
|
* - Multiple GSAP scripts are found (ambiguous which to replace)
|
|
* - No matching GSAP script element exists in the live DOM
|
|
*/
|
|
export function applySoftReload(iframe: HTMLIFrameElement | null, scriptText: string): boolean {
|
|
if (!iframe || !scriptText) return false;
|
|
|
|
const win = iframe.contentWindow as IframeWindow | null;
|
|
const doc = iframe.contentDocument;
|
|
if (!win || !doc) return false;
|
|
if (!win.gsap || !win.__hfForceTimelineRebind) return false;
|
|
|
|
const gsapScripts = findGsapScriptElements(doc);
|
|
if (gsapScripts.length !== 1) return false;
|
|
const oldScriptEl = gsapScripts[0]!;
|
|
|
|
const currentTime = win.__player?.getTime?.() ?? 0;
|
|
|
|
const doReload = () => {
|
|
const timelines = win.__timelines;
|
|
if (timelines) {
|
|
for (const key of Object.keys(timelines)) {
|
|
try {
|
|
const tl = timelines[key] as {
|
|
kill?: () => void;
|
|
getChildren?: (deep: boolean) => Array<{ targets?: () => Element[] }>;
|
|
};
|
|
const allTargets: Element[] = [];
|
|
if (tl?.getChildren) {
|
|
try {
|
|
for (const child of tl.getChildren(true)) {
|
|
if (typeof child.targets === "function") {
|
|
for (const t of child.targets()) {
|
|
allTargets.push(t);
|
|
delete (t as unknown as Record<string, unknown>)._gsap;
|
|
}
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
tl?.kill?.();
|
|
} catch {}
|
|
delete timelines[key];
|
|
}
|
|
}
|
|
|
|
oldScriptEl.remove();
|
|
|
|
const executeScript = () => {
|
|
if (win.MotionPathPlugin && win.gsap?.registerPlugin) {
|
|
win.gsap.registerPlugin(win.MotionPathPlugin);
|
|
}
|
|
const s = doc.createElement("script");
|
|
s.textContent = `(function(){${scriptText}\n})();`;
|
|
doc.body.appendChild(s);
|
|
win.__hfForceTimelineRebind?.();
|
|
win.__player?.seek?.(currentTime);
|
|
win.__hfStudioManualEditsApply?.();
|
|
};
|
|
|
|
// Load MotionPathPlugin on demand if the script uses motionPath.
|
|
// Uses the same CDN as composition templates (GSAP_CDN in constants.ts).
|
|
const needsMotionPath = /motionPath\s*[:{]/.test(scriptText);
|
|
if (needsMotionPath && !win.MotionPathPlugin && win.gsap) {
|
|
const pluginScript = doc.createElement("script");
|
|
pluginScript.src = "https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/MotionPathPlugin.min.js";
|
|
pluginScript.onload = () => executeScript();
|
|
pluginScript.onerror = () => executeScript();
|
|
doc.head.appendChild(pluginScript);
|
|
return;
|
|
}
|
|
|
|
executeScript();
|
|
};
|
|
|
|
try {
|
|
if (win.__hfSuppressSceneMutations) {
|
|
win.__hfSuppressSceneMutations(doReload);
|
|
} else {
|
|
doReload();
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|