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:
Abdullah M. Nazif
2026-10-09 09:15:02 +00:00
committed by GitHub
parent 7a84d29df1
commit a03d709a52
9 changed files with 142 additions and 29 deletions
+4 -3
View File
@@ -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.
+1 -1
View File
@@ -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 },
});
```
+4 -4
View File
@@ -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 },
},
});
+3 -2
View File
@@ -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">
+3 -7
View File
@@ -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();
}
+4 -5
View File
@@ -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 });
}
+5 -6
View File
@@ -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);
}
+117
View File
@@ -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();
}
});
});
+1 -1
View File
@@ -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;