// fallow-ignore-file code-duplication export interface StudioManualEditsRenderScriptOptions { activeCompositionPath?: string | null; } export const STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json"; export function createStudioManualEditsRenderBodyScript( manifestContent: string, options: StudioManualEditsRenderScriptOptions = {}, ): string | null { if (!manifestContent.trim()) return null; return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`; } /** * Returns a self-contained IIFE string that re-applies studio position edits * (translate, rotate) after every GSAP seek by querying data attributes baked * into the HTML. Works without a JSON manifest — positions are already inlined * as CSS custom properties on the elements. */ export function createStudioPositionSeekReapplyScript(): string { return `(${studioPositionSeekReapplyRuntime.toString()})();`; } function studioPositionSeekReapplyRuntime(): void { const OFFSET_X_PROP = "--hf-studio-offset-x"; const OFFSET_Y_PROP = "--hf-studio-offset-y"; const WIDTH_PROP = "--hf-studio-width"; const HEIGHT_PROP = "--hf-studio-height"; const ROTATION_PROP = "--hf-studio-rotation"; const PATH_OFFSET_ATTR = "data-hf-studio-path-offset"; const BOX_SIZE_ATTR = "data-hf-studio-box-size"; const ROTATION_ATTR = "data-hf-studio-rotation"; const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate"; const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate"; const MOTION_ATTR = "data-hf-studio-motion"; const MOTION_TL_KEY = "studio-motion"; const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped"; if ( !document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]") ) return; const splitTopLevelWhitespace = (value: string): string[] => { const parts: string[] = []; let depth = 0; let current = ""; for (const char of value.trim()) { if (char === "(") depth += 1; if (char === ")") depth = Math.max(0, depth - 1); if (/\s/.test(char) && depth === 0) { if (current) parts.push(current); current = ""; } else { current += char; } } if (current) parts.push(current); return parts; }; const composeTranslate = (element: HTMLElement, x: string, y: string): string => { const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim(); if (!original || original === "none") return x + " " + y; const parts = splitTopLevelWhitespace(original); if (parts.length === 1) return "calc(" + parts[0] + " + " + x + ") " + y; if (parts.length >= 2) { const z = parts.length >= 3 ? " " + parts[2] : ""; return "calc(" + parts[0] + " + " + x + ") calc(" + parts[1] + " + " + y + ")" + z; } return x + " " + y; }; const isSimpleRotateAngle = (value: string): boolean => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim()); const composeRotation = (element: HTMLElement, rotationValue: string): string => { const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim(); if (!original || original === "none" || !isSimpleRotateAngle(original)) return rotationValue; return "calc(" + original + " + " + rotationValue + ")"; }; let lastSeekTime = 0; let cachedMotionKey = ""; const finiteNum = (v: unknown): number | null => typeof v === "number" && Number.isFinite(v) ? v : null; const computeMotionKey = (motionEls: NodeListOf): string => { let key = ""; for (let i = 0; i < motionEls.length; i++) { const json = (motionEls[i] as HTMLElement).getAttribute?.(MOTION_ATTR); if (json) key += (key ? "\n" : "") + json; } return key; }; const reapplyMotionTimeline = (): void => { const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]"); if (motionEls.length === 0) { cachedMotionKey = ""; return; } const win = window as Window & { gsap?: { timeline?: (opts: Record) => Record; set?: (el: HTMLElement, vars: Record) => void; registerPlugin?: (plugin: unknown) => void; }; CustomEase?: { create?: (id: string, data: string) => void }; __timelines?: Record>; }; const gsap = win.gsap; if (!gsap || typeof gsap.timeline !== "function") return; win.__timelines = win.__timelines || {}; // Cache the timeline keyed by the concatenated motion JSON strings. // On each seek, if the key hasn't changed, just seek the existing timeline // instead of rebuilding it (avoids kill+recreate on every frame). const motionKey = computeMotionKey(motionEls); const existing = win.__timelines[MOTION_TL_KEY]; if ( motionKey && motionKey === cachedMotionKey && existing && typeof existing.totalTime === "function" ) { (existing.totalTime as (t: number, s: boolean) => void)(lastSeekTime, false); return; } if (existing && typeof existing.kill === "function") (existing.kill as () => void)(); const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } }); const fromTo = tl.fromTo as ( el: HTMLElement, from: Record, to: Record, pos: number, ) => void; if (typeof fromTo !== "function") return; let applied = 0; for (let i = 0; i < motionEls.length; i++) { const el = motionEls[i] as HTMLElement; if (!(el instanceof HTMLElement)) continue; const json = el.getAttribute(MOTION_ATTR); if (!json) continue; try { const m = JSON.parse(json) as Record; const start = finiteNum(m.start); const duration = finiteNum(m.duration); if (start == null || duration == null || duration <= 0) continue; const ease = typeof m.ease === "string" ? m.ease : "none"; const from = (m.from && typeof m.from === "object" ? m.from : {}) as Record< string, unknown >; const to = (m.to && typeof m.to === "object" ? m.to : {}) as Record; const customEase = m.customEase as { id?: string; data?: string } | null | undefined; let resolvedEase = ease; if (customEase?.id && customEase?.data && win.CustomEase?.create) { try { gsap.registerPlugin?.(win.CustomEase); win.CustomEase.create(customEase.id, customEase.data); resolvedEase = customEase.id; } catch { /* use default ease */ } } fromTo.call( tl, el, { ...from }, { ...to, duration, ease: resolvedEase, overwrite: "auto", immediateRender: false }, start, ); applied += 1; } catch { /* malformed JSON — skip */ } } if (applied === 0) { cachedMotionKey = ""; if (typeof (tl as { kill?: () => void }).kill === "function") (tl as { kill: () => void }).kill(); return; } cachedMotionKey = motionKey; win.__timelines[MOTION_TL_KEY] = tl; if (typeof tl.pause === "function") (tl.pause as () => void)(); if (typeof tl.totalTime === "function") (tl.totalTime as (t: number, s: boolean) => void)(lastSeekTime, false); }; const stripGsapTranslateFromTransform = (el: HTMLElement): void => { const transform = el.style.getPropertyValue("transform"); if (!transform || transform === "none") return; const win = el.ownerDocument.defaultView as (Window & typeof globalThis) | null; const MatrixCtor = (win as unknown as { DOMMatrix?: typeof DOMMatrix })?.DOMMatrix; if (!MatrixCtor) return; try { const m = new MatrixCtor(transform); if (m.m41 === 0 && m.m42 === 0) return; m.m41 = 0; m.m42 = 0; if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) { el.style.removeProperty("transform"); } else { el.style.setProperty("transform", m.toString()); } } catch { /* non-parseable transform — leave as-is */ } }; const reapplyAll = (): void => { const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]'); for (let i = 0; i < offsetEls.length; i++) { const el = offsetEls[i] as HTMLElement; if (!(el instanceof HTMLElement)) continue; const x = el.style.getPropertyValue(OFFSET_X_PROP); const y = el.style.getPropertyValue(OFFSET_Y_PROP); if (x || y) { el.style.setProperty( "translate", composeTranslate( el, "var(" + OFFSET_X_PROP + ", 0px)", "var(" + OFFSET_Y_PROP + ", 0px)", ), ); stripGsapTranslateFromTransform(el); } } const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]'); for (let i = 0; i < boxSizeEls.length; i++) { const el = boxSizeEls[i] as HTMLElement; if (!(el instanceof HTMLElement)) continue; const w = el.style.getPropertyValue(WIDTH_PROP); const h = el.style.getPropertyValue(HEIGHT_PROP); if (w) el.style.setProperty("width", w); if (h) el.style.setProperty("height", h); } const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]'); for (let i = 0; i < rotEls.length; i++) { const el = rotEls[i] as HTMLElement; if (!(el instanceof HTMLElement)) continue; const rot = el.style.getPropertyValue(ROTATION_PROP); if (rot) { el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)")); stripGsapTranslateFromTransform(el); } } reapplyMotionTimeline(); }; const runtimeWindow = window as Window & { __hf?: Record; __player?: Record; }; const isWrapped = (fn: (time: number) => unknown): boolean => Boolean((fn as unknown as Record)[WRAPPED_PROP]); const markWrapped = (fn: (time: number) => unknown): void => { try { Object.defineProperty(fn, WRAPPED_PROP, { configurable: false, enumerable: false, value: true, }); } catch { try { (fn as unknown as Record)[WRAPPED_PROP] = true; } catch { /* ignore */ } } }; const wrapFn = (get: () => unknown, set: (fn: (time: number) => unknown) => void): boolean => { const fn = get(); if (typeof fn !== "function") return false; const seek = fn as (time: number) => unknown; if (isWrapped(seek)) { reapplyAll(); return true; } const wrapped = function (this: unknown, time: number): unknown { lastSeekTime = typeof time === "number" && Number.isFinite(time) ? Math.max(0, time) : 0; const result = seek.call(this, time); reapplyAll(); return result; }; markWrapped(wrapped); set(wrapped); reapplyAll(); return true; }; const wrapSeekFunctions = (): boolean => { const a = wrapFn( () => runtimeWindow.__hf?.["seek"], (fn) => { if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn; }, ); const b = wrapFn( () => runtimeWindow.__player?.["renderSeek"], (fn) => { if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn; }, ); return a || b; }; const installSeekTrap = ( obj: Record | undefined, key: string, getter: () => unknown, setter: (fn: (time: number) => unknown) => void, ): void => { if (!obj) return; try { let current = obj[key]; Object.defineProperty(obj, key, { configurable: true, enumerable: true, get() { return current; }, set(value: unknown) { current = value; if (typeof value === "function" && !isWrapped(value as (time: number) => unknown)) { wrapFn(getter, setter); } }, }); } catch { /* non-configurable — fall back to polling */ } }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => reapplyAll(), { once: true }); } else { reapplyAll(); } wrapSeekFunctions(); installSeekTrap( runtimeWindow.__hf, "seek", () => runtimeWindow.__hf?.["seek"], (fn) => { if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn; }, ); installSeekTrap( runtimeWindow.__player as Record | undefined, "renderSeek", () => runtimeWindow.__player?.["renderSeek"], (fn) => { if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn; }, ); let remaining = 120; const interval = setInterval(() => { wrapSeekFunctions(); remaining -= 1; if (remaining <= 0) clearInterval(interval); }, 50); } function studioManualEditsRenderRuntime( manifestContent: string, activeCompositionPath: string | null, ): void { const OFFSET_X_PROP = "--hf-studio-offset-x"; const OFFSET_Y_PROP = "--hf-studio-offset-y"; const WIDTH_PROP = "--hf-studio-width"; const HEIGHT_PROP = "--hf-studio-height"; const ROTATION_PROP = "--hf-studio-rotation"; const PATH_OFFSET_ATTR = "data-hf-studio-path-offset"; const BOX_SIZE_ATTR = "data-hf-studio-box-size"; const ROTATION_ATTR = "data-hf-studio-rotation"; const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate"; const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate"; const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped"; const ROTATION_TRANSFORM_ORIGIN = "center center"; const finiteNumber = (value: unknown): number | null => typeof value === "number" && Number.isFinite(value) ? value : null; const objectRecord = (value: unknown): Record | null => value && typeof value === "object" ? (value as Record) : null; const runtimeWindow = window as Window & { __hf?: { seek?: (time: number) => unknown }; __hfStudioManualEditsApply?: () => number; __player?: { renderSeek?: (time: number) => unknown }; }; const parsedManifest = (() => { try { return objectRecord(JSON.parse(manifestContent)); } catch { return null; } })(); const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : []; if (manifestEdits.length === 0) return; const sourceFileForElement = (element: HTMLElement): string => { let current: HTMLElement | null = element; while (current) { const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src"); if (sourceFile) return sourceFile; current = current.parentElement; } return activeCompositionPath ?? "index.html"; }; const elementMatchesSourceFile = (element: HTMLElement, sourceFile: string): boolean => sourceFileForElement(element) === sourceFile; const styleUsesStudioOffset = (value: string): boolean => value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP); const styleUsesStudioRotation = (value: string): boolean => value.includes(ROTATION_PROP); const splitTopLevelWhitespace = (value: string): string[] => { const parts: string[] = []; let depth = 0; let current = ""; for (const char of value.trim()) { if (char === "(") depth += 1; if (char === ")") depth = Math.max(0, depth - 1); if (/\s/.test(char) && depth === 0) { if (current) parts.push(current); current = ""; } else { current += char; } } if (current) parts.push(current); return parts; }; const composeTranslate = (element: HTMLElement, x: string, y: string): string => { const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim(); if (!original || original === "none") return `${x} ${y}`; const parts = splitTopLevelWhitespace(original); if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`; if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`; if (parts.length === 3) { return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`; } return `${x} ${y}`; }; const readStyleOrComputed = (element: HTMLElement, property: string): string => { try { return ( element.style.getPropertyValue(property) || getComputedStyle(element).getPropertyValue(property) ); } catch { return element.style.getPropertyValue(property); } }; const readTransformLonghandBase = ( element: HTMLElement, property: "translate" | "rotate", ): string => { const value = readStyleOrComputed(element, property).trim(); return value === "none" ? "" : value; }; const preparePathOffsetBase = (element: HTMLElement): void => { const currentTranslate = readTransformLonghandBase(element, "translate"); const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR); const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate); if (!hasMarker) { element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : ""); } else if (wasResetByAnimation) { element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate); } }; const prepareRotationBase = (element: HTMLElement): void => { const currentRotate = readTransformLonghandBase(element, "rotate"); const hasMarker = element.hasAttribute(ROTATION_ATTR); const wasResetByAnimation = !styleUsesStudioRotation(currentRotate); if (!hasMarker) { element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : ""); } else if (wasResetByAnimation) { element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate); } }; const querySelectorCandidates = (selector: string): HTMLElement[] => { const isCandidate = (element: Element): element is HTMLElement => element instanceof HTMLElement; const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1]; if (className) { return Array.from(document.getElementsByTagName("*")).filter( (element): element is HTMLElement => isCandidate(element) && element.classList.contains(className), ); } if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) { return Array.from(document.getElementsByTagName(selector)).filter(isCandidate); } return Array.from(document.querySelectorAll(selector)).filter(isCandidate); }; const resolveTarget = (edit: Record): HTMLElement | null => { const targetRecord = objectRecord(edit.target); if (!targetRecord) return null; const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : ""; if (!sourceFile) return null; const id = typeof targetRecord.id === "string" ? targetRecord.id : ""; if (id) { const byId = document.getElementById(id); if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId; const matchesById = [ document.documentElement, ...Array.from(document.getElementsByTagName("*")), ].filter( (element): element is HTMLElement => element instanceof HTMLElement && element.id === id && elementMatchesSourceFile(element, sourceFile), ); if (matchesById[0]) return matchesById[0]; } const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : ""; if (!selector) return null; try { const matches = querySelectorCandidates(selector).filter((element) => elementMatchesSourceFile(element, sourceFile), ); const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0; return matches[Math.max(0, Math.floor(selectorIndex))] ?? null; } catch { return null; } }; const roundRotationAngle = (angle: number): number => Math.round(angle * 10) / 10; const isSimpleRotateAngle = (value: string): boolean => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim()); const composeRotation = (element: HTMLElement, rotationValue: string): string => { const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim(); if (!original || original === "none" || !isSimpleRotateAngle(original)) { return rotationValue; } return `calc(${original} + ${rotationValue})`; }; const applyPathOffset = (element: HTMLElement, edit: Record): void => { const x = finiteNumber(edit.x); const y = finiteNumber(edit.y); if (x == null || y == null) return; preparePathOffsetBase(element); element.setAttribute(PATH_OFFSET_ATTR, "true"); element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`); element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`); element.style.setProperty( "translate", composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`), ); }; const readParentFlexBasisPixels = ( element: HTMLElement, size: { width: number; height: number }, ): number | null => { const parent = element.parentElement; if (!parent) return null; const styles = getComputedStyle(parent); if (styles.display !== "flex" && styles.display !== "inline-flex") return null; return Math.round( Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width), ); }; const applyBoxSize = (element: HTMLElement, edit: Record): void => { const width = finiteNumber(edit.width); const height = finiteNumber(edit.height); if (width == null || height == null || width <= 0 || height <= 0) return; const rounded = { width: Math.round(Math.max(1, width)), height: Math.round(Math.max(1, height)), }; element.setAttribute(BOX_SIZE_ATTR, "true"); element.style.setProperty(WIDTH_PROP, `${rounded.width}px`); element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`); element.style.setProperty("box-sizing", "border-box"); element.style.setProperty("width", `${rounded.width}px`); element.style.setProperty("height", `${rounded.height}px`); element.style.setProperty("min-width", "0px"); element.style.setProperty("min-height", "0px"); element.style.setProperty("max-width", "none"); element.style.setProperty("max-height", "none"); const flexBasis = readParentFlexBasisPixels(element, rounded); if (flexBasis != null) { element.style.setProperty("flex-basis", `${flexBasis}px`); element.style.setProperty("flex-grow", "0"); element.style.setProperty("flex-shrink", "0"); } if (getComputedStyle(element).display === "inline") { element.style.setProperty("display", "inline-block"); } }; const applyRotation = (element: HTMLElement, edit: Record): void => { const angle = finiteNumber(edit.angle); if (angle == null) return; prepareRotationBase(element); element.setAttribute(ROTATION_ATTR, "true"); element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`); element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN); element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`)); }; const applyManifest = (): number => { let applied = 0; for (const edit of manifestEdits) { const editRecord = objectRecord(edit); if (!editRecord) continue; const element = resolveTarget(editRecord); if (!element) continue; if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord); if (editRecord.kind === "box-size") applyBoxSize(element, editRecord); if (editRecord.kind === "rotation") applyRotation(element, editRecord); applied += 1; } return applied; }; runtimeWindow.__hfStudioManualEditsApply = applyManifest; const markWrapped = (fn: (time: number) => unknown): void => { try { Object.defineProperty(fn, WRAPPED_SEEK_PROP, { configurable: false, enumerable: false, value: true, }); } catch { try { (fn as unknown as Record)[WRAPPED_SEEK_PROP] = true; } catch { // Ignore non-extensible functions. } } }; const isWrapped = (fn: (time: number) => unknown): boolean => Boolean((fn as unknown as Record)[WRAPPED_SEEK_PROP]); const wrapFunction = ( get: () => ((time: number) => unknown) | undefined, set: (fn: (time: number) => unknown) => void, ): boolean => { const fn = get(); if (!fn) return false; const seek = fn as (time: number) => unknown; if (isWrapped(seek)) { applyManifest(); return true; } const wrappedSeek = function (this: unknown, time: number): unknown { const result = seek.call(this, time); applyManifest(); return result; }; markWrapped(wrappedSeek); set(wrappedSeek); applyManifest(); return true; }; const wrapSeekFunctions = (): boolean => { const wrappedHfSeek = wrapFunction( () => runtimeWindow.__hf?.seek, (fn) => { if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn; }, ); const wrappedPlayerRenderSeek = wrapFunction( () => runtimeWindow.__player?.renderSeek, (fn) => { if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn; }, ); return wrappedHfSeek || wrappedPlayerRenderSeek; }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true }); } else { applyManifest(); } wrapSeekFunctions(); let remainingSeekWrapAttempts = 120; const seekWrapInterval = setInterval(() => { wrapSeekFunctions(); remainingSeekWrapAttempts -= 1; if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval); }, 50); }