mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
Per-keyframe speed-curve editing, velocity-based ease fitting, and Gaussian gesture smoothing. Easy Ease presets, per-segment KeyframeEaseList with a bezier editor, AE-convention ease fitting, position-only set-tween rows, and AnimationCard extraction.
287 lines
8.6 KiB
TypeScript
287 lines
8.6 KiB
TypeScript
import { useCallback } from "react";
|
|
import type { Composition } from "@hyperframes/sdk";
|
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
import { roundTo3 } from "../utils/rounding";
|
|
import {
|
|
sdkGsapTweenPersist,
|
|
sdkGsapDeleteAllForSelectorPersist,
|
|
sdkAddWithKeyframesPersist,
|
|
sdkReplaceWithKeyframesPersist,
|
|
type CutoverDeps,
|
|
} from "../utils/sdkCutover";
|
|
import {
|
|
assignGsapTargetAutoIdIfNeeded,
|
|
ensureElementAddressable,
|
|
} from "./gsapScriptCommitHelpers";
|
|
import type { CommitMutation, SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
|
|
|
|
interface SdkAnimationDeps {
|
|
sdkSession?: Composition | null;
|
|
sdkDeps?: CutoverDeps | null;
|
|
}
|
|
|
|
interface GsapAnimationOpsParams extends SdkAnimationDeps {
|
|
projectIdRef: React.MutableRefObject<string | null>;
|
|
activeCompPath: string | null;
|
|
commitMutation: CommitMutation;
|
|
commitMutationSafely: SafeGsapCommitMutation;
|
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
}
|
|
|
|
export function useGsapAnimationOps({
|
|
projectIdRef,
|
|
activeCompPath,
|
|
commitMutation,
|
|
commitMutationSafely,
|
|
showToast,
|
|
sdkSession,
|
|
sdkDeps,
|
|
}: GsapAnimationOpsParams) {
|
|
const updateGsapMeta = useCallback(
|
|
async (
|
|
selection: DomEditSelection,
|
|
animationId: string,
|
|
updates: { duration?: number; ease?: string; easeEach?: string; position?: number },
|
|
) => {
|
|
if (sdkSession && sdkDeps) {
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const handled = await sdkGsapTweenPersist(
|
|
targetPath,
|
|
{ kind: "set", animationId, properties: updates },
|
|
sdkSession,
|
|
sdkDeps,
|
|
{ label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
|
|
);
|
|
if (handled) return;
|
|
}
|
|
commitMutationSafely(
|
|
selection,
|
|
{ type: "update-meta", animationId, updates },
|
|
{ label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta`, softReload: true },
|
|
);
|
|
},
|
|
[commitMutationSafely, activeCompPath, sdkSession, sdkDeps],
|
|
);
|
|
|
|
const deleteGsapAnimation = useCallback(
|
|
async (selection: DomEditSelection, animationId: string) => {
|
|
if (sdkSession && sdkDeps) {
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const handled = await sdkGsapTweenPersist(
|
|
targetPath,
|
|
{ kind: "remove", animationId },
|
|
sdkSession,
|
|
sdkDeps,
|
|
{ label: "Delete GSAP animation" },
|
|
);
|
|
if (handled) return;
|
|
}
|
|
commitMutationSafely(
|
|
selection,
|
|
{ type: "delete", animationId, stripStudioEdits: true },
|
|
{ label: "Delete GSAP animation" },
|
|
);
|
|
},
|
|
[commitMutationSafely, activeCompPath, sdkSession, sdkDeps],
|
|
);
|
|
|
|
const deleteAllForSelector = useCallback(
|
|
async (selection: DomEditSelection, targetSelector: string) => {
|
|
if (sdkSession && sdkDeps) {
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const handled = await sdkGsapDeleteAllForSelectorPersist(
|
|
targetPath,
|
|
targetSelector,
|
|
sdkSession,
|
|
sdkDeps,
|
|
{ label: "Delete all animations for element" },
|
|
);
|
|
if (handled) return;
|
|
}
|
|
void commitMutation(
|
|
selection,
|
|
{ type: "delete-all-for-selector", targetSelector },
|
|
{ label: "Delete all animations for element", softReload: true },
|
|
);
|
|
},
|
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
);
|
|
|
|
// fallow-ignore-next-line complexity
|
|
const addGsapAnimation = useCallback(
|
|
// fallow-ignore-next-line complexity
|
|
async (
|
|
selection: DomEditSelection,
|
|
method: "to" | "from" | "set" | "fromTo",
|
|
_currentTime?: number,
|
|
) => {
|
|
const { selector, autoId } = ensureElementAddressable(selection);
|
|
|
|
if (autoId) {
|
|
const pid = projectIdRef.current;
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
if (!pid) return;
|
|
const assigned = await assignGsapTargetAutoIdIfNeeded({
|
|
projectId: pid,
|
|
targetPath,
|
|
selection,
|
|
autoId,
|
|
showToast,
|
|
});
|
|
if (!assigned) return;
|
|
}
|
|
|
|
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
const position = roundTo3(elStart);
|
|
const duration = roundTo3(elDuration);
|
|
const toDefaults: Record<string, Record<string, number>> = {
|
|
from: { opacity: 0 },
|
|
to: { x: 0, y: 0, opacity: 1 },
|
|
set: { opacity: 1 },
|
|
fromTo: { x: 0, y: 0, opacity: 1 },
|
|
};
|
|
|
|
// Skip SDK path when an id was just assigned server-side (autoId): the
|
|
// SDK session hasn't reloaded that write yet, so persisting its
|
|
// serialization would clobber the new id — let the server add the tween
|
|
// atomically with the id it wrote.
|
|
if (!autoId && selection.hfId && sdkSession && sdkDeps) {
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const spec = {
|
|
method,
|
|
position,
|
|
...(method !== "set" ? { duration, ease: "power2.out" as const } : {}),
|
|
properties: toDefaults[method] ?? { opacity: 1 },
|
|
...(method === "fromTo" ? { fromProperties: { opacity: 0 } } : {}),
|
|
};
|
|
const handled = await sdkGsapTweenPersist(
|
|
targetPath,
|
|
{ kind: "add", target: selection.hfId, spec },
|
|
sdkSession,
|
|
sdkDeps,
|
|
{ label: `Add GSAP ${method} animation` },
|
|
);
|
|
if (handled) return;
|
|
}
|
|
|
|
await commitMutation(
|
|
selection,
|
|
{
|
|
type: "add",
|
|
targetSelector: selector,
|
|
method,
|
|
position,
|
|
duration: method === "set" ? undefined : duration,
|
|
ease: method === "set" ? undefined : "power2.out",
|
|
properties: toDefaults[method] ?? { opacity: 1 },
|
|
fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
|
|
},
|
|
{ label: `Add GSAP ${method} animation` },
|
|
);
|
|
},
|
|
[activeCompPath, commitMutation, projectIdRef, showToast, sdkSession, sdkDeps],
|
|
);
|
|
|
|
type KeyframeEntry = {
|
|
percentage: number;
|
|
properties: Record<string, number | string>;
|
|
ease?: string;
|
|
auto?: boolean;
|
|
};
|
|
|
|
const addWithKeyframes = useCallback(
|
|
async (
|
|
selection: DomEditSelection,
|
|
targetSelector: string,
|
|
position: number,
|
|
duration: number,
|
|
keyframes: KeyframeEntry[],
|
|
ease?: string,
|
|
label = "Add animation with keyframes",
|
|
) => {
|
|
if (sdkSession && sdkDeps) {
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const handled = await sdkAddWithKeyframesPersist(
|
|
targetPath,
|
|
targetSelector,
|
|
position,
|
|
duration,
|
|
keyframes,
|
|
ease,
|
|
sdkSession,
|
|
sdkDeps,
|
|
{ label },
|
|
);
|
|
if (handled) return;
|
|
}
|
|
void commitMutation(
|
|
selection,
|
|
{
|
|
type: "add-with-keyframes",
|
|
targetSelector,
|
|
position,
|
|
duration,
|
|
keyframes,
|
|
...(ease ? { ease } : {}),
|
|
},
|
|
{ label, softReload: true },
|
|
);
|
|
},
|
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
);
|
|
|
|
const replaceWithKeyframes = useCallback(
|
|
async (
|
|
selection: DomEditSelection,
|
|
animationId: string,
|
|
targetSelector: string,
|
|
position: number,
|
|
duration: number,
|
|
keyframes: KeyframeEntry[],
|
|
ease?: string,
|
|
label = "Replace animation with keyframes",
|
|
) => {
|
|
if (sdkSession && sdkDeps) {
|
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
const handled = await sdkReplaceWithKeyframesPersist(
|
|
targetPath,
|
|
animationId,
|
|
targetSelector,
|
|
position,
|
|
duration,
|
|
keyframes,
|
|
ease,
|
|
sdkSession,
|
|
sdkDeps,
|
|
{ label },
|
|
);
|
|
if (handled) return;
|
|
}
|
|
void commitMutation(
|
|
selection,
|
|
{
|
|
type: "replace-with-keyframes",
|
|
animationId,
|
|
targetSelector,
|
|
position,
|
|
duration,
|
|
keyframes,
|
|
...(ease ? { ease } : {}),
|
|
},
|
|
{ label, softReload: true },
|
|
);
|
|
},
|
|
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
|
);
|
|
|
|
return {
|
|
updateGsapMeta,
|
|
deleteGsapAnimation,
|
|
deleteAllForSelector,
|
|
addGsapAnimation,
|
|
addWithKeyframes,
|
|
replaceWithKeyframes,
|
|
};
|
|
}
|