mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
feat(studio): add keyframe timeline state
This commit is contained in:
@@ -5,14 +5,26 @@ import type {
|
||||
} from "@hyperframes/core/gsap-parser";
|
||||
import { PROPERTY_DEFAULTS } from "./gsapShared";
|
||||
|
||||
export function deduplicateKeyframes(
|
||||
keyframes: GsapPercentageKeyframe[],
|
||||
): GsapPercentageKeyframe[] {
|
||||
const byPct = new Map<number, GsapPercentageKeyframe>();
|
||||
export function deduplicateKeyframes<
|
||||
T extends GsapPercentageKeyframe & { animationId?: string; easeAmbiguous?: boolean },
|
||||
>(keyframes: T[]): T[] {
|
||||
const byPct = new Map<number, T>();
|
||||
for (const kf of keyframes) {
|
||||
const existing = byPct.get(kf.percentage);
|
||||
if (existing) {
|
||||
existing.properties = { ...existing.properties, ...kf.properties };
|
||||
// Two DIFFERENT source animations with a keyframe at the same clip %: a
|
||||
// single inline ease button can only target one of them, and which one is
|
||||
// arbitrary (each may also inherit a different easeEach/animation ease, so
|
||||
// comparing raw keyframe eases isn't enough). Flag it so the collapsed row
|
||||
// hides the button there and the user edits per-lane instead.
|
||||
if (
|
||||
existing.animationId !== undefined &&
|
||||
kf.animationId !== undefined &&
|
||||
existing.animationId !== kf.animationId
|
||||
) {
|
||||
existing.easeAmbiguous = true;
|
||||
}
|
||||
if (kf.ease) existing.ease = kf.ease;
|
||||
} else {
|
||||
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
||||
@@ -41,29 +53,40 @@ export function synthesizeFlatTweenKeyframes(anim: GsapAnimation): GsapKeyframes
|
||||
const fromProps = anim.fromProperties;
|
||||
if (!toProps || Object.keys(toProps).length === 0) return null;
|
||||
|
||||
const startProps: Record<string, number | string> = {};
|
||||
const endProps: Record<string, number | string> = {};
|
||||
const rawStart: Record<string, number | string> = {};
|
||||
const rawEnd: Record<string, number | string> = {};
|
||||
|
||||
if (anim.method === "from") {
|
||||
for (const [k, v] of Object.entries(toProps)) {
|
||||
startProps[k] = v;
|
||||
endProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
rawStart[k] = v;
|
||||
rawEnd[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
}
|
||||
} else if (anim.method === "fromTo" && fromProps) {
|
||||
Object.assign(startProps, fromProps);
|
||||
Object.assign(endProps, toProps);
|
||||
Object.assign(rawStart, fromProps);
|
||||
Object.assign(rawEnd, toProps);
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(toProps)) {
|
||||
startProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
endProps[k] = v;
|
||||
rawStart[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
||||
rawEnd[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// Only numeric props are keyframe-interpolatable — a flat tween of a
|
||||
// non-numeric prop (e.g. backgroundColor: "#fff") can't be a 2-keyframe lane.
|
||||
const numericKeys = Object.keys(rawEnd).filter(
|
||||
(k) => typeof rawStart[k] === "number" && typeof rawEnd[k] === "number",
|
||||
);
|
||||
if (numericKeys.length === 0) return null;
|
||||
const startProps = Object.fromEntries(numericKeys.map((k) => [k, rawStart[k]]));
|
||||
const endProps = Object.fromEntries(numericKeys.map((k) => [k, rawEnd[k]]));
|
||||
|
||||
return {
|
||||
format: "percentage",
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: startProps },
|
||||
{ percentage: 100, properties: endProps },
|
||||
// Segment ease lives on the destination keyframe (Figma/AE model) so the
|
||||
// lane + cache surface it; also kept data-level for useGsapTweenCache.
|
||||
{ percentage: 100, properties: endProps, ...(anim.ease ? { ease: anim.ease } : {}) },
|
||||
],
|
||||
...(anim.ease ? { ease: anim.ease } : {}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user