mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
fix(runtime): immediateRender for set tweens + array timeline normalization (#1692)
* chore(studio): remove all console.* calls from studio package * chore(studio): address review — remove dead stubs, restore consent notice - Delete empty if-blocks left after console removal (snapTargetCollection, Player asset-poll, useTimelineSyncCallbacks 5s probe, useGestureRecording dev guard + now-unused isDevBuild) and the stale "surface in dev" comment. - Drop the dangling no-console pragma + dead duplicate-id branch in sourcePatcher. - Restore the one-time telemetry consent disclosure in showNoticeOnce (kept behind a pragma — it is a user-facing notice, not debug noise). - Remove the missed timelineIcons console.warn while preserving the `tag || "div"` null-safety fallback. - Route caption auto-save failures (a data-loss path) through telemetry instead of swallowing silently. - Restore the accidentally-clobbered css-var-fonts output.mp4 fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(runtime): immediateRender for set tweens + array timeline normalization - Set tweens now emit immediateRender:true so they render on page load without requiring the runtime to seek past position 0 - Runtime IIFE normalizes array timelines (window.__timelines = [tl]) to keyed objects, and auto-adds data-start on root elements - Drag teardown clears translate:none to prevent #1673 fly-off - Position-only set tweens hidden from timeline diamonds (3 cache paths) - Parser: ease-only keyframe update preserves existing properties * fix(runtime): address review — restore perf gate, debug surface, scrub restore - Restore the #1651 skipForInjectedVideo gate in media.ts that was dropped on restack — avoids ~2400 wasted per-tick seeks on video-heavy renders. - Restore the console.debug body + docstring bullet of swallow() in diagnostics.ts: the __hfDebug opt-in debug surface had been gutted to an empty if-block. - Rebind: after the progress-cycle set() kick, seek to state.currentTime via totalTime() instead of snapping to 0, so a rebind after scrub / soft-reload restore keeps the playhead. - Array __timelines normalization + data-start default now resolve the root via a shared findRootCompositionEl() that honors data-root="true" first (matches resolveRootCompositionElement, which now delegates to it). - Ease-only keyframe update leaves a primitive (non-object) keyframe value untouched instead of wiping it to {}; add a preservation unit test. - Document the boundDuration<=0 progress(1) kick + restore the STATIC-case comment in gsapRuntimeBridge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
adb40321d6
commit
6987447a75
@@ -72,6 +72,46 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
// Resolve the root composition element with the same priority the rest of
|
||||
// the runtime uses (explicit `data-root` marker first, then the topmost
|
||||
// non-nested composition, then first in DOM order). Defined here so the
|
||||
// array-normalization + data-start defaults below pick the same root the
|
||||
// closure-based `resolveRootCompositionElement` does on multi-comp pages.
|
||||
const findRootCompositionEl = (): HTMLElement | null => {
|
||||
const explicitRoot = document.querySelector('[data-composition-id][data-root="true"]');
|
||||
if (explicitRoot instanceof HTMLElement) return explicitRoot;
|
||||
const nodes = Array.from(document.querySelectorAll("[data-composition-id]")) as HTMLElement[];
|
||||
return (
|
||||
nodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ??
|
||||
nodes[0] ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
// Agents often write `window.__timelines = [tl]` (array) instead of the
|
||||
// keyed-by-composition-id object the runtime expects. Normalize at init so
|
||||
// the rest of the pipeline can assume a Record<string, timeline>.
|
||||
if (Array.isArray(window.__timelines)) {
|
||||
const arr = window.__timelines as unknown[];
|
||||
const rootId = findRootCompositionEl()?.getAttribute("data-composition-id") ?? "root";
|
||||
const normalized: Record<string, unknown> = {};
|
||||
if (arr.length === 1) {
|
||||
normalized[rootId] = arr[0];
|
||||
} else {
|
||||
for (let i = 0; i < arr.length; i++) normalized[`tl-${i}`] = arr[i];
|
||||
}
|
||||
(window as Record<string, unknown>).__timelines = normalized;
|
||||
}
|
||||
|
||||
// Agents sometimes omit data-start on the root composition element. The
|
||||
// runtime skips timed-visibility for elements without it, making clips
|
||||
// invisible and timelines non-seekable. Default to 0 for the root.
|
||||
const rootComp = findRootCompositionEl();
|
||||
if (rootComp && !rootComp.hasAttribute("data-start")) {
|
||||
rootComp.setAttribute("data-start", "0");
|
||||
}
|
||||
|
||||
const registerRuntimeCleanup = (callback: () => void) => {
|
||||
runtimeCleanupCallbacks.push(callback);
|
||||
};
|
||||
@@ -218,23 +258,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
return `${parsed}px`;
|
||||
};
|
||||
|
||||
const resolveRootCompositionElement = (): HTMLElement | null => {
|
||||
// 1. Explicit root marker takes priority
|
||||
const explicitRoot = document.querySelector('[data-composition-id][data-root="true"]');
|
||||
if (explicitRoot instanceof HTMLElement) {
|
||||
return explicitRoot;
|
||||
}
|
||||
// 3. Topmost composition element (not nested inside another)
|
||||
const compositionNodes = Array.from(
|
||||
document.querySelectorAll("[data-composition-id]"),
|
||||
) as HTMLElement[];
|
||||
if (compositionNodes.length === 0) return null;
|
||||
return (
|
||||
compositionNodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ??
|
||||
compositionNodes[0] ??
|
||||
null
|
||||
);
|
||||
};
|
||||
const resolveRootCompositionElement = (): HTMLElement | null => findRootCompositionEl();
|
||||
|
||||
const applyCompositionSizing = () => {
|
||||
const rootEl = resolveRootCompositionElement();
|
||||
@@ -1003,16 +1027,38 @@ export function initSandboxRuntimeModular(): void {
|
||||
state.capturedTimeline.timeScale(state.playbackRate);
|
||||
}
|
||||
const boundDuration = getSafeTimelineDurationSeconds(state.capturedTimeline, 0);
|
||||
if (boundDuration <= 0) {
|
||||
// No resolvable duration (e.g. a set()-only timeline, or one whose
|
||||
// duration isn't known yet). Kick GSAP off the creation position so the
|
||||
// set() renders. For a finite-but-zero timeline progress(1) === progress(0);
|
||||
// for an infinite-repeat timeline this lands on the first iteration's end
|
||||
// frame, which is the best we can do without a known cycle length.
|
||||
if (typeof state.capturedTimeline.progress === "function") {
|
||||
state.capturedTimeline.progress(1, true);
|
||||
state.capturedTimeline.progress(0, false);
|
||||
state.capturedTimeline.pause();
|
||||
}
|
||||
}
|
||||
if (boundDuration > 0) {
|
||||
try {
|
||||
clock.setDuration(boundDuration);
|
||||
} catch {
|
||||
// clock not yet initialized — duration will be set during TransportClock setup
|
||||
}
|
||||
state.capturedTimeline.pause();
|
||||
const seekTime = Math.max(0, state.currentTime || 0);
|
||||
|
||||
if (typeof state.capturedTimeline.totalTime === "function") {
|
||||
// GSAP won't render tl.set() at position 0 when the paused timeline
|
||||
// starts there — play/pause/seek/totalTime are all no-ops at the
|
||||
// creation position. Force the set to render by cycling progress past
|
||||
// 0 (when the timeline implements it), then seek to the prior playhead
|
||||
// (state.currentTime) so a rebind after a user scrub or soft-reload
|
||||
// restore doesn't snap back to 0.
|
||||
if (typeof state.capturedTimeline.progress === "function") {
|
||||
state.capturedTimeline.progress(0.0001, true);
|
||||
}
|
||||
const seekTime = Math.max(0, state.currentTime || 0);
|
||||
state.capturedTimeline.totalTime(seekTime, false);
|
||||
state.capturedTimeline.pause();
|
||||
}
|
||||
|
||||
// GSAP bakes the CSS `translate` into style.transform on seek.
|
||||
|
||||
Reference in New Issue
Block a user