mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
feat(core): add deterministic keyframe ease runtime
This commit is contained in:
@@ -52,6 +52,7 @@ import type {
|
||||
import type { PlayerAPI } from "../core.types";
|
||||
import { swallow } from "./diagnostics";
|
||||
import { shouldAttemptPeriodicTimelineBind } from "./timelineRebindPolicy";
|
||||
import { installStudioCustomEase } from "./customEase";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
@@ -114,6 +115,9 @@ function resolveExportRenderFps(): ExportRenderFpsResolution {
|
||||
|
||||
export function initSandboxRuntimeModular(): void {
|
||||
const state = createRuntimeState();
|
||||
// Own the analytics bridge before any best-effort runtime installation so
|
||||
// early failures are observable instead of disappearing before player setup.
|
||||
initRuntimeAnalytics(postRuntimeMessage as (payload: unknown) => void);
|
||||
// SDK moveElement edits must render even when no usable GSAP timeline ever
|
||||
// binds (CSS/WAAPI-animated or fully static compositions) — apply at init.
|
||||
// This runs at DOMContentLoaded, after inline composition scripts have
|
||||
@@ -142,6 +146,16 @@ export function initSandboxRuntimeModular(): void {
|
||||
const runtimeCleanupCallbacks: Array<() => void> = [];
|
||||
const postedDiagnosticKeys = new Set<string>();
|
||||
let rootStageDiagnosticRafId: number | null = null;
|
||||
const reportedRuntimeIssues = new Set<string>();
|
||||
const reportRuntimeIssueOnce = (
|
||||
key: string,
|
||||
event: "auto_marker_install_failed" | "custom_ease_install_failed",
|
||||
properties: Record<string, string>,
|
||||
): void => {
|
||||
if (reportedRuntimeIssues.has(key)) return;
|
||||
reportedRuntimeIssues.add(key);
|
||||
emitAnalyticsEvent(event, properties);
|
||||
};
|
||||
if (typeof window.__hfRuntimeTeardown === "function") {
|
||||
try {
|
||||
window.__hfRuntimeTeardown();
|
||||
@@ -172,11 +186,38 @@ export function initSandboxRuntimeModular(): void {
|
||||
try {
|
||||
g.registerPlugin({ name: "_auto", init: () => false });
|
||||
w.__hfAutoNoopRegistered = true;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
reportRuntimeIssueOnce("auto_marker_install_failed", "auto_marker_install_failed", {
|
||||
reason: "threw",
|
||||
});
|
||||
swallow("runtime.autoMarker.install", err);
|
||||
// a stray warning is preferable to a broken runtime
|
||||
}
|
||||
};
|
||||
const ensureStudioCustomEase = (): void => {
|
||||
const g = window.gsap;
|
||||
if (!g) {
|
||||
reportRuntimeIssueOnce("custom_ease_missing_gsap", "custom_ease_install_failed", {
|
||||
reason: "missing_gsap",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!installStudioCustomEase(g)) {
|
||||
reportRuntimeIssueOnce("custom_ease_no_parse_ease", "custom_ease_install_failed", {
|
||||
reason: "no_parseEase",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
reportRuntimeIssueOnce("custom_ease_install_threw", "custom_ease_install_failed", {
|
||||
reason: "threw",
|
||||
});
|
||||
swallow("runtime.customEase.install", err);
|
||||
// falling back to GSAP's default ease is preferable to a broken runtime
|
||||
}
|
||||
};
|
||||
ensureAutoMarkerNoop();
|
||||
ensureStudioCustomEase();
|
||||
// Normalize html/body so browser defaults (8px margin, white background) never
|
||||
// bleed into renders as white bars. Runs in both preview and render contexts,
|
||||
// eliminating the preview/render parity gap that existed when only the React
|
||||
@@ -1207,8 +1248,52 @@ export function initSandboxRuntimeModular(): void {
|
||||
// (setTimeout(0)). Scripts using requestAnimationFrame or longer delays may
|
||||
// not be discovered.
|
||||
let childrenBound = false;
|
||||
// A GSAP keyframes tween (`{ keyframes: {...}, ease }`) builds an INNER timeline
|
||||
// whose own `_ease` GSAP resolves ONCE, at build time, via the internal
|
||||
// `_parseEase(vars.ease)` (gsap-core: `tl._ease = _parseEase(keyframes.ease ||
|
||||
// vars.ease || "none")`). On render it calls that inner `timeline._ease(...)`.
|
||||
// The composition's inline `<script>` runs and builds these tweens BEFORE this
|
||||
// runtime finishes registering the custom eases (hold/spring/wiggle/custom) in
|
||||
// GSAP's internal ease map — so for a custom container ease the inner `_ease`
|
||||
// bakes to `undefined`, and the first render throws "_ease is not a function"
|
||||
// (a masked cross-origin Script error). Registering the eases afterward can't
|
||||
// retro-fix that already-baked value, so re-resolve every keyframes tween's
|
||||
// inner `_ease` here, once the eases are registered.
|
||||
const repairKeyframeInnerEase = (tlLike: unknown): void => {
|
||||
const g = (window as unknown as { gsap?: { parseEase?: (e: unknown) => unknown } }).gsap;
|
||||
const tl = tlLike as { getChildren?: (a: boolean, b: boolean, c: boolean) => unknown[] } | null;
|
||||
if (!tl || typeof tl.getChildren !== "function" || !g || typeof g.parseEase !== "function")
|
||||
return;
|
||||
for (const child of tl.getChildren(true, true, true)) {
|
||||
const k = child as {
|
||||
timeline?: { _ease?: unknown };
|
||||
vars?: { ease?: unknown; keyframes?: unknown };
|
||||
};
|
||||
const inner = k.timeline;
|
||||
if (!inner || !("_ease" in inner) || typeof inner._ease === "function") continue;
|
||||
const kf = k.vars?.keyframes;
|
||||
const kfEase = kf && !Array.isArray(kf) ? (kf as { ease?: unknown }).ease : undefined;
|
||||
const ease = kfEase ?? k.vars?.ease ?? "none";
|
||||
try {
|
||||
const resolved = g.parseEase(ease);
|
||||
if (typeof resolved === "function") inner._ease = resolved;
|
||||
} catch (err) {
|
||||
emitAnalyticsEvent("keyframe_ease_repair_failed", {
|
||||
ease: typeof ease === "string" ? ease : String(ease),
|
||||
});
|
||||
swallow("runtime.keyframeEase.repair", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
// fallow-ignore-next-line complexity
|
||||
const bindRootTimelineIfAvailable = (): boolean => {
|
||||
// Custom eases (hold/spring/wiggle/custom) must be registered in GSAP's
|
||||
// internal ease map BEFORE this function's prime render (progress/totalTime
|
||||
// below), or a keyframe segment using one resolves to a non-function ease
|
||||
// and GSAP throws "_ease is not a function" at render. The one-shot call in
|
||||
// init runs early, but if GSAP wasn't ready then (load-order race) it's a
|
||||
// no-op with no retry — so re-assert here, at the render site. Idempotent.
|
||||
ensureStudioCustomEase();
|
||||
if (!externalCompositionsReady) return false;
|
||||
const currentTimeline = state.capturedTimeline;
|
||||
const currentDuration = getTimelineDurationSeconds(currentTimeline);
|
||||
@@ -1229,6 +1314,8 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (typeof state.capturedTimeline.timeScale === "function") {
|
||||
state.capturedTimeline.timeScale(state.playbackRate);
|
||||
}
|
||||
// Repair keyframe inner-timeline eases before any prime render (see helper above).
|
||||
repairKeyframeInnerEase(state.capturedTimeline);
|
||||
const boundDuration = getSafeTimelineDurationSeconds(state.capturedTimeline, 0);
|
||||
if (boundDuration <= 0) {
|
||||
// No resolvable duration (e.g. a set()-only timeline, or one whose
|
||||
@@ -2371,8 +2458,6 @@ export function initSandboxRuntimeModular(): void {
|
||||
window.__player = createPlayerApiCompat(player);
|
||||
window.__playerReady = true;
|
||||
|
||||
// Wire analytics event emission through the bridge
|
||||
initRuntimeAnalytics(postRuntimeMessage as (payload: unknown) => void);
|
||||
emitAnalyticsEvent("composition_loaded", {
|
||||
duration: player.getDuration(),
|
||||
compositionId:
|
||||
|
||||
Reference in New Issue
Block a user