// Public re-exports — consumers import from this file as before. export { STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP, STUDIO_WIDTH_PROP, STUDIO_ROTATION_PROP, type StudioBoxSizeSnapshot, type StudioRotationSnapshot, type StudioPathOffsetSnapshot, } from "./manualEditsTypes"; export { readStudioFileChangePath } from "./manualEditsParsing"; export { beginStudioManualEditGesture, endStudioManualEditGesture, isStudioManualEditGestureCurrent, readStudioPathOffset, readAppliedStudioPathOffset, readStudioBoxSize, readStudioRotation, applyStudioPathOffset, applyStudioPathOffsetDraft, applyStudioBoxSize, applyStudioBoxSizeDraft, applyStudioRotation, applyStudioRotationDraft, reapplyPositionEditsAfterSeek, } from "./manualEditsDom"; export { captureStudioBoxSize, captureStudioRotation, captureStudioPathOffset, restoreStudioBoxSize, restoreStudioRotation, restoreStudioPathOffset, clearStudioPathOffset, clearStudioRotation, clearStudioBoxSize, } from "./manualEditsSnapshot"; import type { StudioManualEditSeekWindow } from "./manualEditsTypes"; import { STUDIO_MANUAL_EDITS_APPLY_PROP, STUDIO_MANUAL_EDITS_WRAPPED_PROP, STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP, } from "./manualEditsTypes"; import { finiteNumber } from "./manualEditsParsing"; /* ── Seek/play reapply wrappers ───────────────────────────────────── */ function markWrapped(fn: (...args: unknown[]) => unknown): void { try { Object.defineProperty(fn, STUDIO_MANUAL_EDITS_WRAPPED_PROP, { configurable: false, enumerable: false, value: true, }); } catch { try { (fn as unknown as Record)[STUDIO_MANUAL_EDITS_WRAPPED_PROP] = true; } catch { // Ignore non-extensible functions. } } } function isWrapped(fn: (...args: unknown[]) => unknown): boolean { return Boolean((fn as unknown as Record)[STUDIO_MANUAL_EDITS_WRAPPED_PROP]); } function wrapSeekReapplyFunction( win: StudioManualEditSeekWindow, owner: Record | undefined, key: string, ): boolean { const fn = owner?.[key]; if (!owner || typeof fn !== "function") return false; const seek = fn as (...args: unknown[]) => unknown; if (isWrapped(seek)) return true; const wrappedSeek = function (this: unknown, ...args: unknown[]): unknown { const result = seek.apply(this, args); win.__hfStudioManualEditsApply?.(); return result; }; markWrapped(wrappedSeek); try { owner[key] = wrappedSeek; } catch { return false; } return true; } function readOwnerNumber(owner: Record, key: string): number | null { const fn = owner[key]; if (typeof fn !== "function") return null; try { return finiteNumber(fn.call(owner)); } catch { return null; } } function hasRemainingTimelineTime(owner: Record): boolean { const duration = readOwnerNumber(owner, "duration") ?? readOwnerNumber(owner, "getDuration"); if (duration == null) return true; if (duration <= 0) return false; const time = readOwnerNumber(owner, "time") ?? readOwnerNumber(owner, "totalTime") ?? readOwnerNumber(owner, "getTime"); if (time == null) return true; return time < duration; } function isTimelinePlaying(owner: Record | undefined): boolean { if (!owner) return false; const isPlaying = owner.isPlaying; if (typeof isPlaying === "function") { try { return Boolean(isPlaying.call(owner)); } catch { return false; } } const paused = owner.paused; if (typeof paused === "function") { try { if (paused.call(owner)) return false; } catch { return false; } const isActive = owner.isActive; if (typeof isActive === "function") { try { if (isActive.call(owner)) return true; } catch { return false; } } return hasRemainingTimelineTime(owner); } const isActive = owner.isActive; if (typeof isActive === "function") { try { return Boolean(isActive.call(owner)); } catch { return false; } } return false; } function isStudioManualEditPlaybackActive(win: StudioManualEditSeekWindow): boolean { if (isTimelinePlaying(win.__player)) return true; if (isTimelinePlaying(win.__timeline)) return true; return Object.values(win.__timelines ?? {}).some(isTimelinePlaying); } function startStudioManualEditPlaybackReapply(win: StudioManualEditSeekWindow): void { win.__hfStudioManualEditsApply?.(); if (win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] != null) return; const tick = () => { win.__hfStudioManualEditsApply?.(); if (!isStudioManualEditPlaybackActive(win)) { win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] = null; return; } win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] = win.requestAnimationFrame(tick); }; win[STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP] = win.requestAnimationFrame(tick); } function wrapPlayReapplyFunction( win: StudioManualEditSeekWindow, owner: Record | undefined, key: string, ): boolean { const fn = owner?.[key]; if (!owner || typeof fn !== "function") return false; const play = fn as (...args: unknown[]) => unknown; if (isWrapped(play)) return true; const wrappedPlay = function (this: unknown, ...args: unknown[]): unknown { const result = play.apply(this, args); startStudioManualEditPlaybackReapply(win); return result; }; markWrapped(wrappedPlay); try { owner[key] = wrappedPlay; } catch { return false; } return true; } function wrapApplyAfterFunction( win: StudioManualEditSeekWindow, owner: Record | undefined, key: string, ): boolean { const fn = owner?.[key]; if (!owner || typeof fn !== "function") return false; const applyAfter = fn as (...args: unknown[]) => unknown; if (isWrapped(applyAfter)) return true; const wrappedApplyAfter = function (this: unknown, ...args: unknown[]): unknown { const result = applyAfter.apply(this, args); win.__hfStudioManualEditsApply?.(); return result; }; markWrapped(wrappedApplyAfter); try { owner[key] = wrappedApplyAfter; } catch { return false; } return true; } export function installStudioManualEditSeekReapply(win: Window, apply: () => void): boolean { const studioWin = win as StudioManualEditSeekWindow; studioWin[STUDIO_MANUAL_EDITS_APPLY_PROP] = apply; const wrappedHfSeek = wrapSeekReapplyFunction(studioWin, studioWin.__hf, "seek"); const wrappedPlayerSeek = wrapSeekReapplyFunction(studioWin, studioWin.__player, "seek"); const wrappedPlayerRenderSeek = wrapSeekReapplyFunction( studioWin, studioWin.__player, "renderSeek", ); const wrappedTimelineSeek = wrapSeekReapplyFunction(studioWin, studioWin.__timeline, "seek"); wrapSeekReapplyFunction(studioWin, studioWin.__timeline, "totalTime"); const wrappedPlayerPlay = wrapPlayReapplyFunction(studioWin, studioWin.__player, "play"); const wrappedTimelinePlay = wrapPlayReapplyFunction(studioWin, studioWin.__timeline, "play"); const wrappedPlayerPause = wrapApplyAfterFunction(studioWin, studioWin.__player, "pause"); const wrappedTimelinePause = wrapApplyAfterFunction(studioWin, studioWin.__timeline, "pause"); let wrappedNamedTimelineSeek = false; let wrappedNamedTimelinePlay = false; let wrappedNamedTimelinePause = false; for (const timeline of Object.values(studioWin.__timelines ?? {})) { wrappedNamedTimelineSeek = wrapSeekReapplyFunction(studioWin, timeline, "seek") || wrappedNamedTimelineSeek; wrapSeekReapplyFunction(studioWin, timeline, "totalTime"); wrappedNamedTimelinePlay = wrapPlayReapplyFunction(studioWin, timeline, "play") || wrappedNamedTimelinePlay; wrappedNamedTimelinePause = wrapApplyAfterFunction(studioWin, timeline, "pause") || wrappedNamedTimelinePause; } // Auto-wrap timelines registered AFTER this install runs. GSAP compositions // register via `window.__timelines[id] = tl` which may happen after the // Studio hook runs. The Proxy intercepts new registrations and wraps // seek/play/pause immediately, closing the gap that causes translate doubling. if (studioWin.__timelines && !(studioWin.__timelines as Record).__proxied) { const original = studioWin.__timelines; studioWin.__timelines = new Proxy(original, { set(target, prop, value) { target[prop as string] = value; if (typeof value === "object" && value !== null) { const tl = value as Record; wrapSeekReapplyFunction(studioWin, tl, "seek"); wrapSeekReapplyFunction(studioWin, tl, "totalTime"); wrapPlayReapplyFunction(studioWin, tl, "play"); wrapApplyAfterFunction(studioWin, tl, "pause"); studioWin.__hfStudioManualEditsApply?.(); } return true; }, }); (studioWin.__timelines as Record).__proxied = true; } if (isStudioManualEditPlaybackActive(studioWin)) { startStudioManualEditPlaybackReapply(studioWin); } return ( wrappedHfSeek || wrappedPlayerSeek || wrappedPlayerRenderSeek || wrappedTimelineSeek || wrappedPlayerPlay || wrappedTimelinePlay || wrappedPlayerPause || wrappedTimelinePause || wrappedNamedTimelineSeek || wrappedNamedTimelinePlay || wrappedNamedTimelinePause ); }