mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
* fix(core): drive adapter seeks when composition has no GSAP timeline renderSeek returned early when deps.getTimeline() was null, skipping the onDeterministicSeek call that drives all frame adapters (CSS, WAAPI, Lottie, Three.js). That meant compositions using any non-GSAP animation primitive froze on their initial frame during capture. Now we still quantize the seek time and fire onDeterministicSeek even without a timeline, so each adapter gets a chance to advance. GSAP compositions are unaffected — timeline-driven seek still takes the same path it did before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(producer): auto-fallback screenshot capture for raf and iframes Co-Authored-By: Codex <codex@openai.com> * test(producer): add render compatibility regression fixtures Co-Authored-By: Codex <codex@openai.com> * fix(core): scrub CSS animations via WAAPI currentTime Co-Authored-By: Codex <codex@openai.com> * test(producer): cover css keyframe renders Co-Authored-By: Codex <codex@openai.com> * fix(producer): propagate virtual time into iframe documents Co-Authored-By: Codex <codex@openai.com> * test(producer): refresh iframe docker golden Co-Authored-By: Codex <codex@openai.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Codex <codex@openai.com>
126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
import type { RuntimeDeterministicAdapter } from "../types";
|
|
|
|
export function createCssAdapter(params?: {
|
|
resolveStartSeconds?: (element: Element) => number;
|
|
}): RuntimeDeterministicAdapter {
|
|
let entries: Array<{
|
|
el: HTMLElement;
|
|
baseDelay: string;
|
|
basePlayState: string;
|
|
}> = [];
|
|
|
|
const getAnimationsForElement = (el: HTMLElement): Animation[] => {
|
|
if (typeof el.getAnimations !== "function") return [];
|
|
try {
|
|
return el.getAnimations();
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const seekAnimations = (animations: Animation[], timeMs: number) => {
|
|
for (const animation of animations) {
|
|
try {
|
|
animation.currentTime = timeMs;
|
|
} catch {
|
|
// ignore animations that reject currentTime writes
|
|
}
|
|
try {
|
|
animation.pause();
|
|
} catch {
|
|
// infinite unresolved animations can throw on pause before currentTime sticks
|
|
}
|
|
}
|
|
};
|
|
|
|
const playAnimations = (animations: Animation[]) => {
|
|
for (const animation of animations) {
|
|
try {
|
|
animation.play();
|
|
} catch {
|
|
// ignore animation edge-cases
|
|
}
|
|
}
|
|
};
|
|
|
|
const pauseAnimations = (animations: Animation[]) => {
|
|
for (const animation of animations) {
|
|
try {
|
|
animation.pause();
|
|
} catch {
|
|
// ignore animation edge-cases
|
|
}
|
|
}
|
|
};
|
|
|
|
const restoreInlineStyles = (entry: (typeof entries)[number]) => {
|
|
if (entry.baseDelay) {
|
|
entry.el.style.animationDelay = entry.baseDelay;
|
|
} else {
|
|
entry.el.style.removeProperty("animation-delay");
|
|
}
|
|
if (entry.basePlayState) {
|
|
entry.el.style.animationPlayState = entry.basePlayState;
|
|
} else {
|
|
entry.el.style.removeProperty("animation-play-state");
|
|
}
|
|
};
|
|
|
|
return {
|
|
name: "css",
|
|
discover: () => {
|
|
entries = [];
|
|
const all = document.querySelectorAll("*");
|
|
for (const rawEl of all) {
|
|
if (!(rawEl instanceof HTMLElement)) continue;
|
|
const style = window.getComputedStyle(rawEl);
|
|
if (!style.animationName || style.animationName === "none") continue;
|
|
entries.push({
|
|
el: rawEl,
|
|
baseDelay: rawEl.style.animationDelay || "",
|
|
basePlayState: rawEl.style.animationPlayState || "",
|
|
});
|
|
}
|
|
},
|
|
seek: (ctx) => {
|
|
const time = Number(ctx.time) || 0;
|
|
for (const entry of entries) {
|
|
if (!entry.el.isConnected) continue;
|
|
const start = params?.resolveStartSeconds
|
|
? params.resolveStartSeconds(entry.el)
|
|
: Number.parseFloat(entry.el.getAttribute("data-start") ?? "0") || 0;
|
|
const localTimeMs = Math.max(0, time - start) * 1000;
|
|
const animations = getAnimationsForElement(entry.el);
|
|
if (animations.length > 0) {
|
|
seekAnimations(animations, localTimeMs);
|
|
continue;
|
|
}
|
|
|
|
// Fallback for environments without WAAPI-backed CSS animation handles.
|
|
entry.el.style.animationPlayState = "paused";
|
|
entry.el.style.animationDelay = `-${(localTimeMs / 1000).toFixed(3)}s`;
|
|
}
|
|
},
|
|
pause: () => {
|
|
for (const entry of entries) {
|
|
if (!entry.el.isConnected) continue;
|
|
const animations = getAnimationsForElement(entry.el);
|
|
if (animations.length > 0) {
|
|
pauseAnimations(animations);
|
|
}
|
|
restoreInlineStyles(entry);
|
|
}
|
|
},
|
|
play: () => {
|
|
for (const entry of entries) {
|
|
if (!entry.el.isConnected) continue;
|
|
restoreInlineStyles(entry);
|
|
playAnimations(getAnimationsForElement(entry.el));
|
|
}
|
|
},
|
|
revert: () => {
|
|
entries = [];
|
|
},
|
|
};
|
|
}
|