# Анимация: клипы, аниматоры, оживители, реестр тика

Все анимации движка тикаются от фиксированного шага (60 Гц) — Pixi-тикер
не используется (`sharedTicker: false`), Pixi `AnimatedSprite` не применяется.
Чистая математика (клипы, оживители, симуляция частиц) живёт отдельно от
тонких Pixi-адаптеров и тестируется в Vitest без браузера.

Модули:

```
anim/clip.ts           чистый степпер клипов (loop/once/pingpong)
anim/SpriteAnimator.ts кадры → Sprite, именованные клипы, события
anim/FrameAnimation.ts @deprecated-обёртка совместимости над SpriteAnimator
```

## Реестр тика: `engine.fx`

Эффекты, аниматоры и вспышки не требуют ручного `update(dt)` из сцены —
добавьте объект в `engine.fx` (`core/Updatables`), движок тикает его сам
между твин-ами и логикой сцены:

```ts
engine.fx.add(emitter);          // возвращает тот же объект
engine.fx.add(new SpriteMotion(view, { bob: {...} }, { seed: 7 }));
engine.fx.remove(fx);            // редко: обычно достаточно destroyed
engine.fx.clear();               // при выходе из сцены, если она не разрушает дерево
```

Самозачистка: объект с `destroyed === true` вычищается из реестра после
очередного прохода (Pixi `destroy()` выставляет флаг сам — эффекты,
живущие в разрушаемом дереве мира, отпадают без кода). Допускается и
локальный инстанс `Updatables` внутри сцены (тот же класс, свой тик).

## Клипы и SpriteAnimator

```ts
import { SpriteAnimator } from '@rpg/engine';

const animator = new SpriteAnimator(sprite, {
    walk_down: { frames: assets.frames('chars/hero_sheet.json', 'hero_down'), fps: 6 },
    idle:      { frames: [assets.frames('chars/hero_sheet.json', 'hero_down')[0]], loop: 'once' }
}, 'walk_down', { randomPhase: seed });   // seed — рассинхрон толпы

animator.play('walk_down', { restart: true }); // тот же клип без restart — продолжение
animator.pause();          // ... resume(), showStep(0) — заморозить на шаге
animator.onFrame = (step) => ...;    // смена шага
animator.onFinish = (clip) => ...;   // конец once/pingpong-прохода
animator.setClipFrames('walk_down', otherFrames); // кадры на лету (вариации окраса)
```

Режимы клипа (`ClipLoop`): `'loop'` — цикл; `'once'` — один раз и стоп
на последнем кадре (после конца аниматор сам паузится); `'pingpong'` —
туда-обратно без дубля крайних кадров. Математика — чистая `stepClip`
в `anim/clip.ts`: время накапливается в «шагах», dt больше периода
проматывает промежуточные шаги (сообщается только финальный) — по
аналогии с пересечением `delay` в Tween.

`sprite.texture` пишется только при смене шага. Флип и anchor — зона
владельца спрайта (см. рецепт ходьбы в `recipes.md`).

`FrameAnimation(sprite, frames, fps, loop)` — прежний API одного клипа,
оставлен как тонкая обёртка (`@deprecated`): `setFrames(f, reset)`
продолжает с текущего шага без reset и стартует с нуля с ним.