/**
* Pixi-адаптер оживителей: применяет выборку sampleMotion к view (position
* относительно базовой, rotation, alpha, scale). База снимается в конструкторе
* — не позиционируйте view после создания. Тикается через engine.fx.
*/
import type { Container } from 'pixi.js';
import type { Updatable } from '../core/Updatables';
import { createRng } from '../math/rng';
import { sampleMotion, type MotionParams } from './motion';
export interface SpriteMotionOptions {
/** Seed — детерминированная случайная фаза (рассинхрон кустов/искр). */
seed?: number;
}
export class SpriteMotion implements Updatable {
/** Живые параметры: меняйте на лету (колокол качается только при звоне). */
params: MotionParams;
private readonly view: Container;
private readonly baseX: number;
private readonly baseY: number;
private readonly baseAlpha: number;
private readonly baseScaleX: number;
private readonly baseScaleY: number;
private t = 0;
constructor(view: Container, params: MotionParams, opts: SpriteMotionOptions = {}) {
this.view = view;
this.params = params;
this.baseX = view.x;
this.baseY = view.y;
this.baseAlpha = view.alpha;
this.baseScaleX = view.scale.x;
this.baseScaleY = view.scale.y;
if (opts.seed !== undefined) {
// Фаза из seed: доля периода, детерминированная (без Math.random).
const rng = createRng(opts.seed);
const phase = (rng.int(0, 1000) / 1000);
this.t = phase * (params.bob?.period ?? params.sway?.period ?? params.pulse?.period ?? params.blink?.period ?? 1);
}
}
update(dt: number): void {
this.t += dt;
const s = sampleMotion(this.params, this.t);
this.view.x = this.baseX;
this.view.y = this.baseY + s.y;
this.view.rotation = s.angle;
this.view.alpha = this.baseAlpha * s.alpha;
this.view.scale.set(this.baseScaleX * s.scale, this.baseScaleY * s.scale);
}
/** Вернуть исходные position/rotation/alpha/scale (при выходе из сцены). */
detach(): void {
this.view.x = this.baseX;
this.view.y = this.baseY;
this.view.rotation = 0;
this.view.alpha = this.baseAlpha;
this.view.scale.set(this.baseScaleX, this.baseScaleY);
}
}