feat(core): add anime.js runtime adapter (#569)

## Summary
- Adds a `RuntimeDeterministicAdapter` for anime.js v4+ alongside existing Lottie, Three.js, WAAPI, and CSS adapters
- Enables frame-accurate rendering of anime.js animations — the adapter converts seek time (seconds) to milliseconds and calls `.seek(timeMs)` on registered instances
- Auto-discovers running instances via `anime.running`; compositions can also register manually via `window.__hfAnime`

## Usage in compositions

```html
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
<script>
  const anim = anime({
    targets: '.box',
    translateX: 250,
    rotate: '1turn',
    duration: 2000,
    autoplay: false,
  });
  window.__hfAnime = window.__hfAnime || [];
  window.__hfAnime.push(anim);
</script>
```

## Files changed
- `packages/core/src/runtime/adapters/animejs.ts` — adapter implementation
- `packages/core/src/runtime/adapters/animejs.test.ts` — 15 unit tests
- `packages/core/src/runtime/init.ts` — register adapter in runtime init
- `packages/core/src/runtime/window.d.ts` — add `anime` and `__hfAnime` globals

## Test plan
- [x] All 15 unit tests pass (`bun run --cwd packages/core test -- --run adapters/animejs`)
- [x] Build passes (`bun run build`)
- [x] Pre-commit hooks pass (lint, format, typecheck, commitlint)
- [x] Manual test: render a composition using anime.js animations
This commit is contained in:
Miguel Ángel
2026-04-29 21:30:17 +02:00
committed by GitHub
parent bcd7230557
commit f9d38a1542
8 changed files with 561 additions and 0 deletions
@@ -0,0 +1,13 @@
{
"name": "animejs-adapter",
"description": "Regression guard for the anime.js v4 runtime adapter. Verifies that anime.createTimeline(), stagger(), and seek() render deterministically via the __hfAnime adapter bridge.",
"tags": ["regression", "adapter"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:68dbadce0d0178668532a07cf8b68a81956039ccc3c1628d958e10152d8c142f
size 272293
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background: #0B132B; }
[data-composition-id="animejs-test"] { position: relative; overflow: hidden; font-family: monospace; }
.box { width: 120px; height: 120px; position: absolute; border-radius: 12px; }
.dot { width: 16px; height: 16px; border-radius: 50%; background: #1C2541; position: absolute; }
#label { position: absolute; top: 40px; left: 60px; color: #BBFBFF; font-size: 28px; font-weight: bold; opacity: 0; }
</style>
</head>
<body>
<div id="root" data-composition-id="animejs-test" data-width="1920" data-height="1080" data-start="0" data-duration="6">
<div id="label">anime.js adapter test</div>
<!-- Boxes for transform tests -->
<div class="box" id="box-a" style="left:200px;top:300px;background:#5409DA;opacity:0;"></div>
<div class="box" id="box-b" style="left:400px;top:300px;background:#FF6D00;opacity:0;"></div>
<div class="box" id="box-c" style="left:600px;top:300px;background:#4E71FF;opacity:0;"></div>
<!-- Dot grid for stagger test -->
<div id="dot-container" style="position:absolute;right:100px;top:200px;"></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__hfAnime = window.__hfAnime || [];
// Generate dot grid
const container = document.getElementById('dot-container');
for (let row = 0; row < 6; row++) {
for (let col = 0; col < 8; col++) {
const dot = document.createElement('div');
dot.className = 'dot';
dot.style.left = (col * 40) + 'px';
dot.style.top = (row * 40) + 'px';
container.appendChild(dot);
}
}
// Timeline 1: label + box entrances (03s)
const tl1 = anime.createTimeline({ autoplay: false });
tl1
.add('#label', { opacity: [0, 1], translateY: [-30, 0], duration: 400, ease: 'out(4)' }, 200)
.add('#box-a', { opacity: [0, 1], scale: [0, 1], rotate: [-90, 0], duration: 500, ease: 'out(5)' }, 500)
.add('#box-b', { opacity: [0, 1], scale: [0, 1], rotate: [90, 0], duration: 500, ease: 'out(5)' }, 700)
.add('#box-c', { opacity: [0, 1], scale: [0, 1], translateY: [80, 0], duration: 500, ease: 'out(3)' }, 900);
// Timeline 2: CSS transforms on boxes (25s)
const tl2 = anime.createTimeline({ autoplay: false });
tl2
.add('#box-a', { rotate: 360, duration: 2000, ease: 'inOut(2)' }, 0)
.add('#box-b', { skewX: 25, scale: 1.3, duration: 1500, ease: 'inOut(3)' }, 200)
.add('#box-c', { rotate: -180, translateX: 100, duration: 1800, ease: 'inOut(2)' }, 400);
// Timeline 3: stagger wave on dots (36s)
const tl3 = anime.createTimeline({ autoplay: false });
tl3.add('.dot', {
backgroundColor: '#5409DA',
scale: [1, 2.5, 1],
duration: 600,
ease: 'out(2)',
delay: anime.stagger(30, { grid: [8, 6], from: 'center' }),
}, 0);
tl3.add('.dot', {
backgroundColor: '#FF6D00',
scale: [1, 2, 1],
duration: 500,
ease: 'in(2)',
delay: anime.stagger(25, { grid: [8, 6], from: 'edges' }),
}, 1500);
// Scene mapping for the adapter
const scenes = [
{ tl: tl1, start: 0, end: 3 },
{ tl: tl2, start: 2, end: 5 },
{ tl: tl3, start: 3, end: 6 },
];
window.__hfAnime = [{
seek: function(globalTimeMs) {
const t = globalTimeMs / 1000;
for (const s of scenes) {
if (t >= s.start && t < s.end) {
s.tl.seek((t - s.start) * 1000);
} else if (t >= s.end) {
s.tl.seek((s.end - s.start) * 1000);
} else {
s.tl.seek(0);
}
}
},
pause: function() { for (const s of scenes) s.tl.pause(); },
play: function() { for (const s of scenes) s.tl.play(); },
}];
const tl = gsap.timeline({ paused: true });
window.__timelines['animejs-test'] = tl;
</script>
</div>
</body>
</html>