mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(studio): keep gsapAnimations in sync with the keyframe cache
An ungrouped tween (mixed property groups classify to propertyGroup undefined) fed keyframeCache but was skipped by every gsapAnimations writer, so the collapsed row drew diamonds the expanded lanes had no source animation to render. Drop the property-group gate at all three writers; lane consumers already filter by group. Also route the same-percentage merge in updateKeyframeCacheFromParsed through deduplicateKeyframes so the easeAmbiguous rule has one owner.
This commit is contained in:
@@ -195,6 +195,30 @@ describe("updateKeyframeCacheFromParsed", () => {
|
|||||||
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#box")).toEqual([animation]);
|
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#box")).toEqual([animation]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("records an ungrouped tween in gsapAnimations too, so the two stores agree", () => {
|
||||||
|
// `{ x, opacity }` spans two property groups, so the parser leaves
|
||||||
|
// propertyGroup undefined. Skipping it here used to cache diamonds with no
|
||||||
|
// source animation behind them: the collapsed row drew keyframes the
|
||||||
|
// expanded lanes could not render.
|
||||||
|
const animation: GsapAnimation = {
|
||||||
|
id: "mixed-box",
|
||||||
|
targetSelector: "#box",
|
||||||
|
method: "to",
|
||||||
|
position: 0,
|
||||||
|
properties: { x: 100, opacity: 0 },
|
||||||
|
duration: 1,
|
||||||
|
resolvedStart: 0,
|
||||||
|
};
|
||||||
|
usePlayerStore.setState({
|
||||||
|
elements: [{ id: "box-clip", domId: "box", tag: "div", start: 0, duration: 1, track: 0 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
updateKeyframeCacheFromParsed([animation], "scene.html", "box", {});
|
||||||
|
|
||||||
|
expect(cache().has("scene.html#box")).toBe(true);
|
||||||
|
expect(usePlayerStore.getState().gsapAnimations.get("scene.html#box")).toEqual([animation]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not cache a flat tween without animatable numeric properties", () => {
|
it("does not cache a flat tween without animatable numeric properties", () => {
|
||||||
const animation: GsapAnimation = {
|
const animation: GsapAnimation = {
|
||||||
id: "flat-box",
|
id: "flat-box",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||||
import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
|
import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
|
||||||
import { toAbsoluteTime } from "./gsapShared";
|
import { toAbsoluteTime } from "./gsapShared";
|
||||||
import { synthesizeFlatTweenKeyframes } from "./gsapTweenSynth";
|
import { deduplicateKeyframes, synthesizeFlatTweenKeyframes } from "./gsapTweenSynth";
|
||||||
|
|
||||||
export function updateKeyframeCacheFromParsed(
|
export function updateKeyframeCacheFromParsed(
|
||||||
animations: GsapAnimation[],
|
animations: GsapAnimation[],
|
||||||
@@ -23,9 +23,12 @@ export function updateKeyframeCacheFromParsed(
|
|||||||
anim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(anim)?.keyframes ?? [];
|
anim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(anim)?.keyframes ?? [];
|
||||||
if (!id || kfSource.length === 0) continue;
|
if (!id || kfSource.length === 0) continue;
|
||||||
idsWithKeyframes.add(id);
|
idsWithKeyframes.add(id);
|
||||||
if (anim.propertyGroup) {
|
// Every tween that fed keyframeCache also lands in gsapAnimations, group or
|
||||||
sourceAnimations.set(id, [...(sourceAnimations.get(id) ?? []), anim]);
|
// not: a mixed-group tween (`{ x, opacity }` classifies to undefined) used to
|
||||||
}
|
// cache diamonds with no source animation behind them, so the collapsed row
|
||||||
|
// drew keyframes the expanded lanes couldn't render. Lane consumers do the
|
||||||
|
// group filtering themselves (animationContributesLane).
|
||||||
|
sourceAnimations.set(id, [...(sourceAnimations.get(id) ?? []), anim]);
|
||||||
|
|
||||||
// Convert tween-relative percentages to clip-relative so diamonds
|
// Convert tween-relative percentages to clip-relative so diamonds
|
||||||
// render at the correct position within the timeline clip.
|
// render at the correct position within the timeline clip.
|
||||||
@@ -51,28 +54,10 @@ export function updateKeyframeCacheFromParsed(
|
|||||||
|
|
||||||
const existing = merged.get(id);
|
const existing = merged.get(id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const byPct = new Map<number, (typeof existing.keyframes)[0]>();
|
// deduplicateKeyframes owns the same-% merge (including the easeAmbiguous
|
||||||
for (const kf of [...existing.keyframes, ...clipKeyframes]) {
|
// flag downstream lanes read); a second copy of that rule here is how the
|
||||||
const prev = byPct.get(kf.percentage);
|
// two writers drift.
|
||||||
if (prev) {
|
existing.keyframes = deduplicateKeyframes([...existing.keyframes, ...clipKeyframes]);
|
||||||
prev.properties = { ...prev.properties, ...kf.properties };
|
|
||||||
// Mirror deduplicateKeyframes: a same-% collision across different
|
|
||||||
// source animations is an ambiguous merged segment (the button can
|
|
||||||
// only target one arbitrary animation). Flag it so the collapsed row
|
|
||||||
// suppresses the inline ease button there.
|
|
||||||
if (
|
|
||||||
prev.animationId !== undefined &&
|
|
||||||
kf.animationId !== undefined &&
|
|
||||||
prev.animationId !== kf.animationId
|
|
||||||
) {
|
|
||||||
prev.easeAmbiguous = true;
|
|
||||||
}
|
|
||||||
if (kf.ease) prev.ease = kf.ease;
|
|
||||||
} else {
|
|
||||||
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existing.keyframes = Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
|
|
||||||
} else {
|
} else {
|
||||||
merged.set(id, {
|
merged.set(id, {
|
||||||
...anim.keyframes,
|
...anim.keyframes,
|
||||||
|
|||||||
@@ -329,9 +329,9 @@ export function useGsapAnimationsForElement(
|
|||||||
// fallow-ignore-next-line complexity
|
// fallow-ignore-next-line complexity
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!elementId) return;
|
if (!elementId) return;
|
||||||
|
// No property-group filter: ungrouped tweens are recorded here as well.
|
||||||
const sourceAnimations = animations.filter(
|
const sourceAnimations = animations.filter(
|
||||||
(animation) =>
|
(animation) => animation.keyframes || synthesizeFlatTweenKeyframes(animation),
|
||||||
animation.propertyGroup && (animation.keyframes || synthesizeFlatTweenKeyframes(animation)),
|
|
||||||
);
|
);
|
||||||
if (sourceAnimations.length > 0)
|
if (sourceAnimations.length > 0)
|
||||||
writeGsapAnimationsForElement(sourceFile, elementId, sourceAnimations);
|
writeGsapAnimationsForElement(sourceFile, elementId, sourceAnimations);
|
||||||
@@ -493,10 +493,10 @@ export function usePopulateKeyframeCacheForFile(
|
|||||||
// group / descendant selectors, not just `#id`).
|
// group / descendant selectors, not just `#id`).
|
||||||
for (const id of resolveSelectorElementIds(anim.targetSelector, doc)) {
|
for (const id of resolveSelectorElementIds(anim.targetSelector, doc)) {
|
||||||
// kfData is already resolved (real keyframes OR a synthesized flat
|
// kfData is already resolved (real keyframes OR a synthesized flat
|
||||||
// tween), so a grouped flat tween joins the store like a keyframed one.
|
// tween), so a flat tween joins the store like a keyframed one. No
|
||||||
if (anim.propertyGroup) {
|
// property-group filter: this map must cover every tween the cache
|
||||||
sourceByElement.set(id, [...(sourceByElement.get(id) ?? []), anim]);
|
// 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 { elStart, elDuration } = resolveClipTimingBasis(id, sf, elements, domClipChildren);
|
||||||
const clipKeyframes = kfData.keyframes.map((kf) => {
|
const clipKeyframes = kfData.keyframes.map((kf) => {
|
||||||
const absTime = toAbsoluteTime(tweenPos, tweenDur, kf.percentage);
|
const absTime = toAbsoluteTime(tweenPos, tweenDur, kf.percentage);
|
||||||
|
|||||||
Reference in New Issue
Block a user