mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(sdk): correct animation calls in examples and docs (#5247)
* fix(sdk): correct animation calls in editor examples * docs(sdk): align entrance snippets with tween properties
This commit is contained in:
@@ -113,7 +113,7 @@ const animId = comp.addGsapTween("hf-title", {
|
||||
position: 0.5,
|
||||
duration: 0.6,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 40 },
|
||||
properties: { opacity: 0, y: 40 },
|
||||
});
|
||||
// animId is the newly-assigned animation ID — store it for subsequent edits
|
||||
```
|
||||
@@ -137,13 +137,14 @@ const animId = comp.addGsapTween("hf-title", {
|
||||
GSAP ease string, e.g. `"power2.inOut"`.
|
||||
</ParamField>
|
||||
<ParamField path="fromProperties" type="Record<string, unknown>">
|
||||
Starting properties — used with `"from"` and `"fromTo"`.
|
||||
Starting properties for `"fromTo"` tweens.
|
||||
</ParamField>
|
||||
<ParamField path="toProperties" type="Record<string, unknown>">
|
||||
Ending properties — used with `"fromTo"`.
|
||||
</ParamField>
|
||||
<ParamField path="properties" type="Record<string, unknown>">
|
||||
Animation target properties — used with `"to"` tweens.
|
||||
Starting values for `"from"`, target values for `"to"`, or immediate values for `"set"`.
|
||||
Also supplies the destination of `"fromTo"` when `toProperties` is omitted.
|
||||
</ParamField>
|
||||
<ParamField path="repeat" type="number">
|
||||
Number of repeats (`-1` = infinite). Maps to GSAP's `repeat` option.
|
||||
|
||||
@@ -266,7 +266,7 @@ const animId = comp.addGsapTween("hf-title", {
|
||||
position: 0,
|
||||
duration: 0.6,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 40 },
|
||||
properties: { opacity: 0, y: 40 },
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ comp.dispatch({
|
||||
position: 0,
|
||||
duration: 0.8,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 40 },
|
||||
properties: { opacity: 0, y: 40 },
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -354,9 +354,9 @@ comp.dispatch({
|
||||
| `position` | `number \| string` | Timeline position. Accepts numbers (seconds) or label-relative strings (`"intro+=0.5"`). |
|
||||
| `duration` | `number` | Tween duration in seconds. |
|
||||
| `ease` | `string` | GSAP ease string (e.g. `"power3.out"`). |
|
||||
| `fromProperties` | `Record<string, unknown>` | Start-state properties (used by `from` and `fromTo`). |
|
||||
| `fromProperties` | `Record<string, unknown>` | Start-state properties for `fromTo` tweens. |
|
||||
| `toProperties` | `Record<string, unknown>` | End-state properties (used by `fromTo`). |
|
||||
| `properties` | `Record<string, unknown>` | Animated properties for `to` tweens. |
|
||||
| `properties` | `Record<string, unknown>` | Starting values for `from`, target values for `to`, immediate values for `set`, or the `fromTo` destination when `toProperties` is omitted. |
|
||||
| `repeat` | `number` | Repeat count (`-1` = infinite). |
|
||||
| `yoyo` | `boolean` | Reverse on alternating repeats. |
|
||||
| `stagger` | `number \| Record<string, unknown>` | Stagger config for multi-target tweens. |
|
||||
@@ -501,7 +501,7 @@ comp.dispatch({
|
||||
method: "from",
|
||||
position: "outro-=0.5",
|
||||
duration: 0.4,
|
||||
fromProperties: { opacity: 0 },
|
||||
properties: { opacity: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ interface GsapTweenSpec {
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="fromProperties" type="Record<string, unknown>">
|
||||
Start-state property map — used by `"from"` and `"fromTo"` methods.
|
||||
Start-state property map for `"fromTo"` tweens.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="toProperties" type="Record<string, unknown>">
|
||||
@@ -276,7 +276,8 @@ interface GsapTweenSpec {
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="properties" type="Record<string, unknown>">
|
||||
Animated property map — used by `"to"` method.
|
||||
Starting values for `"from"`, target values for `"to"`, or immediate values for `"set"`.
|
||||
Also supplies the destination of `"fromTo"` when `toProperties` is omitted.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="repeat" type="number">
|
||||
|
||||
@@ -111,20 +111,16 @@ export async function addStaggeredEntrance(html: string, staggerDelay = 0.15): P
|
||||
|
||||
const textEls = comp.find({ tag: "div" });
|
||||
|
||||
// Phase 3b feature-detect: addGsapTween throws UnsupportedOpError until the
|
||||
// parser-backed engine lands — skip animation rather than crash the job.
|
||||
// Compositions without an editable GSAP timeline must remain usable.
|
||||
const probeTween = {
|
||||
method: "from",
|
||||
position: 0,
|
||||
duration: 0.5,
|
||||
ease: "power3.out",
|
||||
fromProperties: { opacity: 0, y: 30 },
|
||||
properties: { opacity: 0, y: 30 },
|
||||
} as const;
|
||||
const first = textEls[0];
|
||||
if (
|
||||
!first ||
|
||||
!comp.can({ type: "addGsapTween", target: first, id: "preflight", tween: probeTween })
|
||||
) {
|
||||
if (!first || !comp.can({ type: "addGsapTween", target: first, tween: probeTween }).ok) {
|
||||
return comp.serialize();
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ export function onClipDrag(comp: Composition, id: string, start: number, duratio
|
||||
|
||||
// ── GSAP animation panel ──────────────────────────────────────────────────────
|
||||
|
||||
// Phase 3b: GSAP ops throw UnsupportedOpError until the parser-backed engine
|
||||
// lands — feature-detect with can() and disable the panel control if false.
|
||||
// Disable animation controls when the composition cannot support the edit.
|
||||
|
||||
export function addBounceIn(comp: Composition, targetId: string): string | null {
|
||||
const tween = {
|
||||
@@ -96,14 +95,14 @@ export function addBounceIn(comp: Composition, targetId: string): string | null
|
||||
position: 0,
|
||||
duration: 0.5,
|
||||
ease: "bounce.out",
|
||||
fromProperties: { y: 40, opacity: 0 },
|
||||
properties: { y: 40, opacity: 0 },
|
||||
} as const;
|
||||
if (!comp.can({ type: "addGsapTween", target: targetId, id: "preflight", tween })) return null;
|
||||
if (!comp.can({ type: "addGsapTween", target: targetId, tween }).ok) return null;
|
||||
return comp.addGsapTween(targetId, tween);
|
||||
}
|
||||
|
||||
export function updateEase(comp: Composition, animationId: string, ease: string): void {
|
||||
if (!comp.can({ type: "setGsapTween", animationId, properties: { ease } })) return;
|
||||
if (!comp.can({ type: "setGsapTween", animationId, properties: { ease } }).ok) return;
|
||||
comp.setGsapTween(animationId, { ease });
|
||||
}
|
||||
|
||||
|
||||
@@ -102,8 +102,7 @@ export function applyOpFromJson(comp: Composition, opJson: unknown): void {
|
||||
|
||||
// ── GSAP operations ───────────────────────────────────────────────────────────
|
||||
|
||||
// NOTE (Phase 3b): GSAP ops require the parser-backed engine and throw
|
||||
// UnsupportedOpError until it lands. Feature-detect with can() first.
|
||||
// Compositions without an editable GSAP timeline must remain usable.
|
||||
|
||||
export function addFadeIn(comp: Composition, targetId: string, delay = 0): string | null {
|
||||
const tween: GsapTweenSpec = {
|
||||
@@ -111,9 +110,9 @@ export function addFadeIn(comp: Composition, targetId: string, delay = 0): strin
|
||||
position: delay,
|
||||
duration: 0.4,
|
||||
ease: "power2.out",
|
||||
fromProperties: { opacity: 0 },
|
||||
properties: { opacity: 0 },
|
||||
};
|
||||
if (!comp.can({ type: "addGsapTween", target: targetId, id: "preflight", tween })) return null;
|
||||
if (!comp.can({ type: "addGsapTween", target: targetId, tween }).ok) return null;
|
||||
return comp.addGsapTween(targetId, tween);
|
||||
}
|
||||
|
||||
@@ -127,10 +126,10 @@ export function addBounce(
|
||||
position: 0,
|
||||
duration: 0.6,
|
||||
ease: "bounce.out",
|
||||
fromProperties: { y: 60, opacity: 0 },
|
||||
properties: { y: 60, opacity: 0 },
|
||||
...overrides,
|
||||
};
|
||||
if (!comp.can({ type: "addGsapTween", target: targetId, id: "preflight", tween })) return null;
|
||||
if (!comp.can({ type: "addGsapTween", target: targetId, tween }).ok) return null;
|
||||
return comp.addGsapTween(targetId, tween);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { parseGsapScript } from "@hyperframes/parsers";
|
||||
import { addStaggeredEntrance } from "../examples/headless-agent.js";
|
||||
import { addBounceIn, updateEase } from "../examples/react-embed.js";
|
||||
import { addBounce, addFadeIn } from "../examples/vanilla-editor.js";
|
||||
import { openComposition } from "./session.js";
|
||||
|
||||
const HTML =
|
||||
'<!DOCTYPE html>\n<html><head><style>html,body{margin:0} [data-hf-root]{width:640px;height:360px;background:#101820;color:#fff} #headline{position:absolute;left:80px;top:100px;font:48px Arial}</style></head>\n<body><section data-composition-id="example" data-hf-id="hf-stage" data-hf-root data-width="640" data-height="360" data-duration="2">\n<div id="headline" class="clip" data-hf-id="hf-text" data-start="0" data-duration="2" data-track-index="0">SDK example</div>\n{{SCRIPT}}\n</section></body></html>';
|
||||
const SCRIPT =
|
||||
'<script>var tl = gsap.timeline({ paused: true }); tl.to("#headline", { x: 0, duration: 2 }, 0); window.__timelines = { example: tl };</script>';
|
||||
const GSAP_HTML = HTML.replace("{{SCRIPT}}", SCRIPT);
|
||||
const STATIC_HTML = HTML.replace("{{SCRIPT}}", "");
|
||||
|
||||
const helpers = [
|
||||
{ name: "vanilla fade", add: addFadeIn, properties: { opacity: 0 }, duration: 0.4 },
|
||||
{ name: "vanilla bounce", add: addBounce, properties: { y: 60, opacity: 0 }, duration: 0.6 },
|
||||
{ name: "React bounce", add: addBounceIn, properties: { y: 40, opacity: 0 }, duration: 0.5 },
|
||||
];
|
||||
|
||||
describe("SDK animation examples", () => {
|
||||
it.each(helpers)("$name skips compositions without a GSAP script", async ({ add }) => {
|
||||
const comp = await openComposition(STATIC_HTML);
|
||||
const before = comp.serialize();
|
||||
try {
|
||||
expect(add(comp, "hf-text")).toBeNull();
|
||||
expect(comp.serialize()).toBe(before);
|
||||
} finally {
|
||||
comp.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(helpers)("$name skips targets missing from a GSAP composition", async ({ add }) => {
|
||||
const comp = await openComposition(GSAP_HTML);
|
||||
const before = comp.serialize();
|
||||
try {
|
||||
expect(add(comp, "hf-missing")).toBeNull();
|
||||
expect(comp.serialize()).toBe(before);
|
||||
} finally {
|
||||
comp.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(helpers)(
|
||||
"$name emits an entrance tween with starting values",
|
||||
async ({ add, properties, duration }) => {
|
||||
const comp = await openComposition(GSAP_HTML);
|
||||
try {
|
||||
const animationId = add(comp, "hf-text");
|
||||
expect(animationId).toBeTruthy();
|
||||
const scripts = /<script>([\s\S]*?)<\/script>/.exec(comp.serialize());
|
||||
const animations = parseGsapScript(scripts?.[1] ?? "").animations;
|
||||
expect(animations.find((animation) => animation.id === animationId)).toMatchObject({
|
||||
method: "from",
|
||||
properties,
|
||||
duration,
|
||||
});
|
||||
} finally {
|
||||
comp.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("the headless agent skips a composition without a writable timeline", async () => {
|
||||
const expected = await openComposition(STATIC_HTML);
|
||||
try {
|
||||
expect(await addStaggeredEntrance(STATIC_HTML)).toBe(expected.serialize());
|
||||
} finally {
|
||||
expected.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("the headless agent preserves stagger positions and starting values", async () => {
|
||||
const html = GSAP_HTML.replace(
|
||||
"</section>",
|
||||
'<div data-hf-id="hf-second">Second</div></section>',
|
||||
);
|
||||
const output = await addStaggeredEntrance(html, 0.25);
|
||||
const script = /<script>([\s\S]*?)<\/script>/.exec(output);
|
||||
const entrances = parseGsapScript(script?.[1] ?? "").animations.filter(
|
||||
(animation) => animation.method === "from",
|
||||
);
|
||||
expect(entrances).toHaveLength(2);
|
||||
expect(entrances.map((animation) => animation.position)).toEqual([0, 0.25]);
|
||||
for (const animation of entrances)
|
||||
expect(animation.properties).toMatchObject({ opacity: 0, y: 30 });
|
||||
});
|
||||
|
||||
it("the React easing control skips unsupported edits", async () => {
|
||||
const comp = await openComposition(STATIC_HTML);
|
||||
const dispatch = vi.spyOn(comp, "setGsapTween");
|
||||
try {
|
||||
expect(() => updateEase(comp, "missing", "power2.in")).not.toThrow();
|
||||
expect(dispatch).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
comp.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("the React easing control updates an existing animation", async () => {
|
||||
const comp = await openComposition(GSAP_HTML);
|
||||
try {
|
||||
const animationId = addBounceIn(comp, "hf-text");
|
||||
expect(animationId).toBeTruthy();
|
||||
if (!animationId) return;
|
||||
updateEase(comp, animationId, "power2.in");
|
||||
const script = /<script>([\s\S]*?)<\/script>/.exec(comp.serialize());
|
||||
expect(
|
||||
parseGsapScript(script?.[1] ?? "").animations.find(
|
||||
(animation) => animation.id === animationId,
|
||||
)?.ease,
|
||||
).toBe("power2.in");
|
||||
} finally {
|
||||
comp.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -298,7 +298,7 @@ export interface GsapTweenSpec {
|
||||
ease?: string;
|
||||
fromProperties?: Record<string, unknown>;
|
||||
toProperties?: Record<string, unknown>;
|
||||
/** For 'to' tweens — the properties to animate toward */
|
||||
/** Tween values for 'from', 'to', and 'set'; destination values for 'fromTo'. */
|
||||
properties?: Record<string, unknown>;
|
||||
repeat?: number;
|
||||
yoyo?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user