mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-06 02:01:07 +00:00
108 lines
4.4 KiB
TypeScript
108 lines
4.4 KiB
TypeScript
/**
|
|
* Reading a composition file's GSAP tweens into the keyframe cache: fetch and
|
|
* selector -> element id resolution.
|
|
* Split from useGsapTweenCache to keep that file under the 600-line limit.
|
|
*/
|
|
import type { GsapAnimation, GsapKeyframesData, ParsedGsap } from "@hyperframes/core/gsap-parser";
|
|
import { isStudioHoldSet } from "@hyperframes/core/gsap-parser";
|
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
import { replaceKeyframeCacheForFile } from "./gsapKeyframeCacheHelpers";
|
|
import { resolveClipTimingBasis, resolveSelectorElementIds, toClipKeyframes } from "./gsapShared";
|
|
import {
|
|
deduplicateKeyframes,
|
|
isStaticPositionHold,
|
|
synthesizeFlatTweenKeyframes,
|
|
type MergeableKeyframe,
|
|
} from "./gsapTweenSynth";
|
|
|
|
export { resolveSelectorElementIds };
|
|
|
|
/**
|
|
* The slice of the parse response callers actually read. The endpoint returns
|
|
* the full `ParsedGsap` (preamble/postamble and all), but nothing downstream of
|
|
* this fetch touches the source-text fields, so the guard below only has to
|
|
* vouch for what gets used.
|
|
*/
|
|
type ParsedGsapAnimations = Pick<
|
|
ParsedGsap,
|
|
"animations" | "multipleTimelines" | "unsupportedTimelinePattern"
|
|
>;
|
|
|
|
/**
|
|
* A proxy, an error page, or a stale server can answer 200 with something that
|
|
* has no `animations` array — the case where the old blind cast crashed on
|
|
* `.animations.filter`.
|
|
*/
|
|
function hasAnimations(value: unknown): value is ParsedGsapAnimations {
|
|
return (
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
"animations" in value &&
|
|
Array.isArray(value.animations)
|
|
);
|
|
}
|
|
|
|
export async function fetchParsedAnimations(
|
|
projectId: string,
|
|
sourceFile: string,
|
|
): Promise<ParsedGsapAnimations | null> {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/projects/${encodeURIComponent(projectId)}/gsap-animations/${encodeURIComponent(sourceFile)}`,
|
|
// Always re-read the freshly-parsed source; no per-call timestamp (which
|
|
// would defeat caching forever and is a deterministic-render no-no).
|
|
{ cache: "no-store" },
|
|
);
|
|
if (!res.ok) return null;
|
|
const parsed: unknown = await res.json();
|
|
if (!hasAnimations(parsed)) return null;
|
|
// Studio-emitted pre-keyframe hold `set`s are an internal runtime detail (they
|
|
// hold an element's first keyframe before its tween). They must not surface as
|
|
// user animations — otherwise they pollute the keyframe cache / timeline diamonds.
|
|
return { ...parsed, animations: parsed.animations.filter((a) => !isStudioHoldSet(a)) };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read one composition file's tweens into the keyframe cache. Split out of the
|
|
* hook so the effect can run it per file without re-nesting the whole body.
|
|
*/
|
|
// fallow-ignore-next-line complexity
|
|
export async function populateKeyframeCacheFromAst(
|
|
projectId: string,
|
|
sf: string,
|
|
doc: Document | null | undefined,
|
|
): Promise<void> {
|
|
const parsed = await fetchParsedAnimations(projectId, sf);
|
|
if (!parsed) return;
|
|
const { elements, domClipChildren } = usePlayerStore.getState();
|
|
const mergedByElement = new Map<string, GsapKeyframesData<MergeableKeyframe>>();
|
|
const sourceByElement = new Map<string, GsapAnimation[]>();
|
|
for (const anim of parsed.animations) {
|
|
if (anim.hasUnresolvedKeyframes) continue;
|
|
if (isStaticPositionHold(anim)) continue;
|
|
const kfData = anim.keyframes ?? synthesizeFlatTweenKeyframes(anim);
|
|
if (!kfData) continue;
|
|
// Attribute the tween to every element it animates (handles class /
|
|
// group / descendant selectors, not just `#id`).
|
|
for (const id of resolveSelectorElementIds(anim.targetSelector, doc)) {
|
|
// kfData is already resolved (real keyframes OR a synthesized flat
|
|
// tween), so a flat tween joins the store like a keyframed one. No
|
|
// property-group filter: this map must cover every tween the cache
|
|
// below records, or expanded lanes have nothing to render.
|
|
sourceByElement.set(id, [...(sourceByElement.get(id) ?? []), anim]);
|
|
const { elStart, elDuration } = resolveClipTimingBasis(id, sf, elements, domClipChildren);
|
|
const clipKeyframes = toClipKeyframes(kfData.keyframes, anim, elStart, elDuration);
|
|
const existing = mergedByElement.get(id);
|
|
if (existing) {
|
|
existing.keyframes = deduplicateKeyframes([...existing.keyframes, ...clipKeyframes]);
|
|
} else {
|
|
mergedByElement.set(id, { ...kfData, keyframes: clipKeyframes });
|
|
}
|
|
}
|
|
}
|
|
replaceKeyframeCacheForFile(sf, mergedByElement, sourceByElement);
|
|
}
|