Skip to content

Commit f6e187c

Browse files
committed
fix(studio): address PR review — html-escape attrs, cache timeline, migrate sidecar, add tests
Blocker: JSON attribute values are now HTML-entity-escaped before being written into source HTML. Read-back unescapes automatically. Perf: motion timeline is cached between seeks at render — only rebuilt when the concatenated JSON key changes, not on every frame. Migration: on mount, empties legacy .hyperframes/studio-motion.json so the legacy render script no-ops. Tests: 46 new tests for motion read/write/clear round-trips, JSON attribute escaping, and source patcher entity handling. Nits: removed unused activeCompositionPath param; tightened htmlCompiler attribute substring check.
1 parent 793c25b commit f6e187c

8 files changed

Lines changed: 816 additions & 28 deletions

File tree

packages/core/src/studio-api/helpers/manualEditsRenderScript.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,26 @@ function studioPositionSeekReapplyRuntime(): void {
8181
};
8282

8383
let lastSeekTime = 0;
84+
let cachedMotionKey = "";
8485

8586
const finiteNum = (v: unknown): number | null =>
8687
typeof v === "number" && Number.isFinite(v) ? v : null;
8788

89+
const computeMotionKey = (motionEls: NodeListOf<Element>): string => {
90+
let key = "";
91+
for (let i = 0; i < motionEls.length; i++) {
92+
const json = (motionEls[i] as HTMLElement).getAttribute?.(MOTION_ATTR);
93+
if (json) key += (key ? "\n" : "") + json;
94+
}
95+
return key;
96+
};
97+
8898
const reapplyMotionTimeline = (): void => {
8999
const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]");
90-
if (motionEls.length === 0) return;
100+
if (motionEls.length === 0) {
101+
cachedMotionKey = "";
102+
return;
103+
}
91104
const win = window as Window & {
92105
gsap?: {
93106
timeline?: (opts: Record<string, unknown>) => Record<string, unknown>;
@@ -100,7 +113,22 @@ function studioPositionSeekReapplyRuntime(): void {
100113
const gsap = win.gsap;
101114
if (!gsap || typeof gsap.timeline !== "function") return;
102115
win.__timelines = win.__timelines || {};
116+
117+
// Cache the timeline keyed by the concatenated motion JSON strings.
118+
// On each seek, if the key hasn't changed, just seek the existing timeline
119+
// instead of rebuilding it (avoids kill+recreate on every frame).
120+
const motionKey = computeMotionKey(motionEls);
103121
const existing = win.__timelines[MOTION_TL_KEY];
122+
if (
123+
motionKey &&
124+
motionKey === cachedMotionKey &&
125+
existing &&
126+
typeof existing.totalTime === "function"
127+
) {
128+
(existing.totalTime as (t: number, s: boolean) => void)(lastSeekTime, false);
129+
return;
130+
}
131+
104132
if (existing && typeof existing.kill === "function") (existing.kill as () => void)();
105133
const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
106134
const fromTo = tl.fromTo as (
@@ -151,10 +179,12 @@ function studioPositionSeekReapplyRuntime(): void {
151179
}
152180
}
153181
if (applied === 0) {
182+
cachedMotionKey = "";
154183
if (typeof (tl as { kill?: () => void }).kill === "function")
155184
(tl as { kill: () => void }).kill();
156185
return;
157186
}
187+
cachedMotionKey = motionKey;
158188
win.__timelines[MOTION_TL_KEY] = tl;
159189
if (typeof tl.pause === "function") (tl.pause as () => void)();
160190
if (typeof tl.totalTime === "function")

packages/producer/src/services/htmlCompiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ export async function compileForRender(
10031003
const HF_POSITION_ATTRS = [
10041004
'data-hf-studio-path-offset="true"',
10051005
'data-hf-studio-rotation="true"',
1006-
"data-hf-studio-motion=",
1006+
'data-hf-studio-motion="',
10071007
];
10081008
const hasPositionEdits = HF_POSITION_ATTRS.some((attr) => htmlWithAssets.includes(attr));
10091009
const html = hasPositionEdits

packages/studio/src/components/editor/studioMotion.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,7 @@ export function applyStudioMotionManifest(
230230
* builds a GSAP timeline, and seeks to the current time.
231231
* This replaces the manifest-based `applyStudioMotionManifest` for the studio preview.
232232
*/
233-
export function applyStudioMotionFromDom(
234-
document: Document,
235-
activeCompositionPath?: string | null,
236-
currentTime?: number,
237-
): number {
233+
export function applyStudioMotionFromDom(document: Document, currentTime?: number): number {
238234
const win = document.defaultView as StudioMotionWindow | null;
239235
if (!win) return 0;
240236
const gsap = win.gsap;

0 commit comments

Comments
 (0)