docs(skills): fix anime.js v3 syntax in v4 adapter guidance (#3064)

* docs(skills): fix anime.js v3 syntax in v4 adapter guidance

The animejs adapter docs teach v3 syntax against a v4 build, so the examples
cannot run as written: every v4 bundle assigns a *namespace object* to the
global `anime`, making v3's `anime({ targets })` call form a TypeError. `easing:`
is now `ease:`, ease names lost their `ease` prefix, and `timeline.add()` takes
`(targets, parameters, position)`.

- Rewrite `skills/hyperframes-animation/adapters/animejs.md` for v4:
  `anime.animate()` / `anime.createTimeline()`, `ease:` names, targets-first
  `add()`, position shorthands, and a note that the producer fixtures pin
  4.0.2 (`lib/`) while 4.1+ moved bundles to `dist/bundles/`.
- Fix the same v3 `anime.timeline({ targets })` snippet in
  `skills/hyperframes-keyframes/references/keyframe-patterns.md`.
- Stop advertising `anime.running` auto-discovery as a safety net. No v4 build
  exports `running` (checked 4.0.2 and 4.5.0), so `discover()` returns
  immediately and any instance a composition forgets to push onto
  `window.__hfAnime` is silently never seeked. Marked v3-only/inert in the
  skill page and the adapter docstring; explicit registration is now stated
  as mandatory.
- Add render-safety notes the page lacked: `createSeededRandom()` as the
  deterministic replacement for `Math.random()`, and why
  `autoplay: onScroll(...)`, `createDraggable`, and pointer-driven
  `createAnimatable` cannot work under headless seek rendering.
- Regenerate skills-manifest.json.

Runtime behaviour is unchanged: the `packages/core` edit is comment-only, and
the seek path already works on v4 (registered instances expose
seek/pause/play). The now-dead `anime.running` branch in `discover()` is left
in place — it is guarded and try/caught, and removing it is a behaviour change
that belongs in its own PR.

* fix(core): align Anime.js globals with v4

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
This commit is contained in:
focsuer
2026-08-16 14:01:38 -04:00
committed by GitHub
co-authored by Miguel Ángel
parent fecaf72d1f
commit 67edb01bf4
7 changed files with 147 additions and 58 deletions
+1 -1
View File
@@ -48,7 +48,7 @@
"player:perf": "bun run --filter @hyperframes/player perf", "player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .", "format:check": "oxfmt --check .",
"knip": "knip", "knip": "knip",
"test:scripts": "node --import tsx --test scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs && vitest run scripts/catalog/", "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs && vitest run scripts/catalog/",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json", "typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "node --test 'skills/**/*.test.mjs'", "test:skills": "node --test 'skills/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts", "generate:previews": "tsx scripts/generate-template-previews.ts",
+20 -14
View File
@@ -8,14 +8,18 @@ import { swallow } from "../diagnostics";
* *
* ## Usage in a composition * ## Usage in a composition
* *
* The v4 global `anime` is a namespace object, not a callable — `anime.animate()`,
* `anime.createTimeline()`. v3's `anime({ targets })` form does not exist in v4.
* (4.1+ also moved the bundles from `lib/` to `dist/bundles/`.)
*
* ```html * ```html
* <script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script> * <script src="https://cdn.jsdelivr.net/npm/animejs@4.5.0/dist/bundles/anime.umd.min.js"></script>
* <script> * <script>
* const anim = anime({ * const anim = anime.animate('.box', {
* targets: '.box', * x: 250,
* translateX: 250,
* rotate: '1turn', * rotate: '1turn',
* duration: 2000, * duration: 2000,
* ease: 'outExpo',
* autoplay: false, * autoplay: false,
* }); * });
* window.__hfAnime = window.__hfAnime || []; * window.__hfAnime = window.__hfAnime || [];
@@ -23,13 +27,13 @@ import { swallow } from "../diagnostics";
* </script> * </script>
* ``` * ```
* *
* Timelines work the same way: * Timelines work the same way — note `add(targets, params, position)`:
* *
* ```html * ```html
* <script> * <script>
* const tl = anime.timeline({ autoplay: false }); * const tl = anime.createTimeline({ autoplay: false });
* tl.add({ targets: '.a', opacity: [0, 1], duration: 500 }) * tl.add('.a', { opacity: [0, 1], duration: 500 })
* .add({ targets: '.b', translateY: [-40, 0], duration: 400 }); * .add('.b', { y: [-40, 0], duration: 400 });
* window.__hfAnime = window.__hfAnime || []; * window.__hfAnime = window.__hfAnime || [];
* window.__hfAnime.push(tl); * window.__hfAnime.push(tl);
* </script> * </script>
@@ -37,10 +41,11 @@ import { swallow } from "../diagnostics";
* *
* Multiple instances are supported — all are seeked in sync. * Multiple instances are supported — all are seeked in sync.
* *
* ## Auto-discovery * ## Auto-discovery (v3 only — inert on v4)
* *
* The adapter also checks `anime.running` for active instances * `discover()` checks `anime.running`, which v4 no longer exports, so it always
* (useful for compositions that forget to register manually). * returns empty against a v4 build. Compositions MUST push every instance onto
* `window.__hfAnime` themselves; an unregistered instance is never seeked.
*/ */
export function createAnimeJsAdapter(): RuntimeDeterministicAdapter { export function createAnimeJsAdapter(): RuntimeDeterministicAdapter {
return { return {
@@ -133,9 +138,10 @@ interface AnimeInstance {
} }
interface AnimeGlobal { interface AnimeGlobal {
(params: unknown): AnimeInstance; animate?: (targets: unknown, params?: unknown) => AnimeInstance;
timeline?: (params?: unknown) => AnimeInstance; createTimeline?: (params?: unknown) => AnimeInstance;
running: AnimeInstance[]; /** Legacy v3 registry retained for backward-compatible discovery. */
running?: AnimeInstance[];
} }
interface AnimeWindow extends Window { interface AnimeWindow extends Window {
+7 -5
View File
@@ -97,13 +97,15 @@ declare global {
}; };
THREE?: ThreeLike; THREE?: ThreeLike;
/** /**
* Global anime.js instance (set by including the anime.iife.min.js script). * Global Anime.js v4 namespace (set by the UMD or IIFE bundle).
* The adapter uses `anime.running` for auto-discovery. * Register returned instances on `window.__hfAnime`; v4 has no
* `anime.running` auto-discovery registry.
*/ */
anime?: { anime?: {
(params: unknown): unknown; animate?: (targets: unknown, params?: unknown) => unknown;
timeline?: (params?: unknown) => unknown; createTimeline?: (params?: unknown) => unknown;
running: unknown[]; /** Legacy v3 registry retained for backward-compatible discovery. */
running?: unknown[];
}; };
/** /**
* anime.js instances registered by compositions. * anime.js instances registered by compositions.
+63
View File
@@ -0,0 +1,63 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const guidance = [
{
path: "packages/core/src/runtime/adapters/animejs.ts",
required: ["anime.animate", "anime.createTimeline"],
},
{
path: "skills/hyperframes-animation/adapters/animejs.md",
required: ["anime.animate", "anime.createTimeline"],
},
{
path: "skills/hyperframes-keyframes/references/keyframe-patterns.md",
required: ["anime.createTimeline"],
},
];
function read(path) {
return readFileSync(join(repoRoot, path), "utf8");
}
function fencedCode(source) {
const normalized = source.replace(/^\s*\* ?/gm, "");
return [...normalized.matchAll(/```[^\n]*\n([\s\S]*?)```/g)].map((match) => match[1]).join("\n");
}
for (const { path, required } of guidance) {
test(`${path} uses Anime.js v4 call sites in executable examples`, () => {
const snippets = fencedCode(read(path));
for (const call of required) {
assert.match(snippets, new RegExp(`\\b${call.replace(".", "\\.")}\\s*\\(`));
}
assert.doesNotMatch(snippets, /\banime\s*\(\s*\{/);
assert.doesNotMatch(snippets, /\banime\.timeline\s*\(/);
});
}
test("the window anime global advertises the v4 namespace API", () => {
const source = read("packages/core/src/runtime/window.d.ts");
const global = source.match(/\n\s*anime\?: \{([\s\S]*?)\n\s*\};/);
assert.ok(global, "found Window.anime declaration");
assert.match(global[1], /\banimate\??:\s*\(/);
assert.match(global[1], /\bcreateTimeline\??:\s*\(/);
assert.doesNotMatch(global[1], /^\s*\(params:/m);
assert.doesNotMatch(global[1], /\btimeline\??:/);
});
test("the adapter's local anime global shape matches v4 while retaining legacy discovery", () => {
const source = read("packages/core/src/runtime/adapters/animejs.ts");
const global = source.match(/interface AnimeGlobal \{([\s\S]*?)\n\}/);
assert.ok(global, "found AnimeGlobal declaration");
assert.match(global[1], /\banimate\??:\s*\(/);
assert.match(global[1], /\bcreateTimeline\??:\s*\(/);
assert.match(global[1], /\brunning\??:/);
assert.doesNotMatch(global[1], /^\s*\(params:/m);
assert.doesNotMatch(global[1], /\btimeline\??:/);
});
+2 -2
View File
@@ -22,7 +22,7 @@
"files": 17 "files": 17
}, },
"hyperframes-animation": { "hyperframes-animation": {
"hash": "5bc2ce098387a547", "hash": "2ce5ca7dbf361e27",
"files": 121 "files": 121
}, },
"hyperframes-audio": { "hyperframes-audio": {
@@ -42,7 +42,7 @@
"files": 78 "files": 78
}, },
"hyperframes-keyframes": { "hyperframes-keyframes": {
"hash": "a568c05c01b27461", "hash": "d00744ff0e669624",
"files": 3 "files": 3
}, },
"hyperframes-registry": { "hyperframes-registry": {
@@ -7,28 +7,39 @@ description: Anime.js adapter patterns for HyperFrames. Use when writing Anime.j
HyperFrames can seek Anime.js instances through its `animejs` runtime adapter. The composition owns the animation objects; HyperFrames owns the clock. HyperFrames can seek Anime.js instances through its `animejs` runtime adapter. The composition owns the animation objects; HyperFrames owns the clock.
**This page targets v4 (examples pinned to 4.5.0, MIT).** v4 is a hard break from v3 — there is no callable `anime()`, `easing:` is now `ease:`, and ease names lost their `ease` prefix. Writing v3 from memory produces a composition that throws or silently animates nothing.
The repo's own producer fixtures pin `animejs@4.0.2/lib/anime.iife.min.js`, which still resolves — but that build predates `splitText` / `scrambleText` / `createSeededRandom` / `createLayout` used below, and 4.1+ moved the bundles to `dist/bundles/`, so a version bump needs the path changed too.
## Contract ## Contract
- Create animations or timelines synchronously during composition initialization. - Create animations or timelines synchronously during composition initialization.
- Set `autoplay: false` so Anime.js does not advance on its own clock. - Set `autoplay: false` so Anime.js does not advance on its own clock.
- Register every returned animation or timeline on `window.__hfAnime`. - Register every returned animation or timeline on `window.__hfAnime`**explicitly. There is no working auto-discovery on v4** (see Avoid).
- Use finite durations and loop counts. - Use finite durations and loop counts.
- Avoid callbacks that mutate DOM based on wall-clock time, network state, or unseeded randomness. - Avoid callbacks that mutate DOM based on wall-clock time, network state, or unseeded randomness.
The adapter seeks every registered instance with `instance.seek(timeMs)`, where `timeMs` is HyperFrames time in milliseconds. The adapter seeks every registered instance with `instance.seek(timeMs)`, where `timeMs` is HyperFrames time **in milliseconds** (`ctx.time` seconds × 1000). It also calls `pause()` and `play()` on each instance; anything exposing those three methods works, whatever created it.
## Loading v4
```html
<!-- UMD: the global `anime` is a NAMESPACE OBJECT, not a function -->
<script src="https://cdn.jsdelivr.net/npm/animejs@4.5.0/dist/bundles/anime.umd.min.js"></script>
```
`anime.animate(...)`, `anime.createTimeline(...)`, `anime.utils.*`, `anime.svg.*`, `anime.stagger(...)`. **Calling `anime(...)` is a TypeError** — every v4 build (UMD and IIFE alike) assigns a namespace object to the global, so v3's `anime({ targets })` form cannot work no matter which v4 file you load.
## Basic Pattern ## Basic Pattern
```html ```html
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
<script> <script>
const anim = anime({ const anim = anime.animate(".mark", {
targets: ".mark", x: 280, // v4 shorthand for translateX
translateX: 280,
rotate: "1turn", rotate: "1turn",
opacity: [0, 1], opacity: [0, 1],
duration: 1200, duration: 1200,
easing: "easeOutExpo", ease: "outExpo", // NOT easing: "easeOutExpo"
autoplay: false, autoplay: false,
}); });
@@ -39,55 +50,56 @@ The adapter seeks every registered instance with `instance.seek(timeMs)`, where
## Timeline Pattern ## Timeline Pattern
`createTimeline` replaces `anime.timeline`, and `add()` takes **targets as its first argument**`add(targets, parameters, position)`:
```html ```html
<script> <script>
const tl = anime.timeline({ const tl = anime.createTimeline({
autoplay: false, autoplay: false,
easing: "easeOutCubic", defaults: { ease: "outCubic" }, // per-timeline defaults, not a bare `easing`
}); });
tl.add({ tl.add(".title", { y: [40, 0], opacity: [0, 1], duration: 650 });
targets: ".title", tl.add(".accent", { scaleX: [0, 1], duration: 450 }, 250); // 250 = time position
translateY: [40, 0],
opacity: [0, 1],
duration: 650,
}).add(
{
targets: ".accent",
scaleX: [0, 1],
duration: 450,
},
250,
);
window.__hfAnime = window.__hfAnime || []; window.__hfAnime = window.__hfAnime || [];
window.__hfAnime.push(tl); window.__hfAnime.push(tl);
</script> </script>
``` ```
Position accepts a number, a label, `"+=250"` / `"-=100"`, `"<"` (previous **end**) and `"<<"` (previous **start**).
## Module Builds ## Module Builds
If you use an ES module build, the adapter does not care how the instance was created. It only needs the returned object to expose `seek()`, `pause()`, and preferably `play()`: The adapter does not care how the instance was created only that it exposes `seek()`, `pause()`, and `play()`:
```html ```html
<script type="module"> <script type="module">
import { animate } from "https://cdn.jsdelivr.net/npm/animejs/+esm"; import { animate } from "https://cdn.jsdelivr.net/npm/animejs@4.5.0/+esm";
const anim = animate(".chip", { const anim = animate(".chip", { x: "18rem", duration: 900, autoplay: false });
x: "18rem",
duration: 900,
autoplay: false,
});
window.__hfAnime = window.__hfAnime || []; window.__hfAnime = window.__hfAnime || [];
window.__hfAnime.push(anim); window.__hfAnime.push(anim);
</script> </script>
``` ```
## Determinism
v4 ships `createSeededRandom(seed)` — use it instead of `Math.random()` when a composition needs scatter/jitter, so the same frame renders the same on every pass:
```js
const rnd = anime.createSeededRandom(1337);
anime.animate(".dot", { y: () => -40 * rnd(), duration: 800, autoplay: false });
```
`anime.utils.random()` / `randomPick()` / `shuffle()` are **not** seeded — they break frame-to-frame reproducibility.
## Good Uses ## Good Uses
- Small SVG and DOM flourishes where Anime.js syntax is compact. - Small SVG and DOM flourishes where Anime.js syntax is compact.
- Imported Anime.js examples that can be made seek-driven. - Free `splitText` / `scrambleText` (Motion puts these behind Motion+; GSAP SplitText is the other free option).
- `svg.createDrawable` / `svg.morphTo` / `svg.createMotionPath` line-draw and path work.
- Multiple independent micro-animations pushed into the same registry. - Multiple independent micro-animations pushed into the same registry.
Use GSAP for complex scene sequencing unless the user specifically asks for Anime.js. GSAP is still the primary HyperFrames authoring path. Use GSAP for complex scene sequencing unless the user specifically asks for Anime.js. GSAP is still the primary HyperFrames authoring path.
@@ -95,8 +107,11 @@ Use GSAP for complex scene sequencing unless the user specifically asks for Anim
## Avoid ## Avoid
- Leaving `autoplay` at the Anime.js default. - Leaving `autoplay` at the Anime.js default.
- Depending on `anime.running` auto-discovery instead of explicit `window.__hfAnime.push(...)`. - **Relying on the adapter's `anime.running` auto-discovery — it cannot work on v4.** `running` is not among v4.5.0's exports (verified against the published bundle), so `discover()` returns immediately and any instance you did not `push()` is never seeked. Explicit registration is mandatory, not a nicety.
- Infinite loops. Compute a finite repeat count from the composition duration. - `autoplay: onScroll(...)` — there is no scroll in a headless seek render, so the animation would never advance. Drive it off composition time instead.
- `waapi.animate()` for anything the adapter must seek — the adapter seeks via `.seek()`, and whether WAAPI-backed instances honor it is **unverified**. Use the JS engine (`animate`) for rendered compositions; `waapi` is an off-main-thread optimization for live pages.
- `createDraggable`, and any pointer-driven `createAnimatable` loop — input does not exist at render time.
- Infinite loops. Compute a finite repeat count from the composition duration (v4 `loop` counts **repeats**: `loop: 1` plays twice).
- Building animations in timers, promises, event handlers, or after async asset loads. - Building animations in timers, promises, event handlers, or after async asset loads.
## Validation ## Validation
@@ -105,10 +120,11 @@ After editing a composition that uses Anime.js:
```bash ```bash
npx hyperframes lint npx hyperframes lint
npx hyperframes check npx hyperframes validate
``` ```
## Credits And References ## Credits And References
- HyperFrames adapter source: `packages/core/src/runtime/adapters/animejs.ts`. - HyperFrames adapter source: `packages/core/src/runtime/adapters/animejs.ts`.
- Anime.js documentation for `autoplay`, `pause()`, and `seek()`: https://animejs.com/documentation/ - Anime.js v4 docs: https://animejs.com/documentation/
- v3 → v4 migration (not on animejs.com): https://github.com/juliangarnier/anime/wiki/Migrating-from-v3-to-v4
@@ -42,8 +42,10 @@ CSS:
Anime.js: Anime.js:
```js ```js
const animation = anime.timeline({ autoplay: false }); const animation = anime.createTimeline({ autoplay: false });
animation.add({ targets: "<selector>" /* derived channels */ }); animation.add("<selector>", {
/* derived channels */
});
window.__hfAnime = window.__hfAnime || []; window.__hfAnime = window.__hfAnime || [];
window.__hfAnime.push(animation); window.__hfAnime.push(animation);
``` ```