import { EaseFn, linear } from './easing';
/**
* Твины и таймеры: плавное изменение числовых свойств объекта и отложенные вызовы.
* Менеджер обновляется игровым циклом (движок делает это сам в Engine.tick).
*/
export interface TweenOptions {
/** Длительность в секундах. */
duration: number;
/** Функция сглаживания (по умолчанию linear). */
ease?: EaseFn;
/** Задержка до старта в секундах. */
delay?: number;
/** Вызывается по завершении. */
onDone?: () => void;
}
/** Управление конкретным твином/таймером: можно отменить. */
export interface TweenHandle {
cancel(): void;
}
interface ActiveTween {
target: Record<string, number>;
props: Record<string, number>;
from: Record<string, number> | null; // снимается при старте (после delay)
elapsed: number;
options: TweenOptions;
cancelled: boolean;
}
interface ActiveDelay {
remaining: number;
fn: () => void;
cancelled: boolean;
}
export class TweenManager {
private tweens: ActiveTween[] = [];
private delays: ActiveDelay[] = [];
/**
* Плавно привести числовые свойства target к заданным значениям.
* Начальные значения снимаются в момент старта (после delay).
*/
to(target: object, props: Record<string, number>, options: TweenOptions): TweenHandle {
const tween: ActiveTween = {
target: target as Record<string, number>,
props,
from: null,
elapsed: 0,
options: { ease: linear, ...options },
cancelled: false
};
this.tweens.push(tween);
return { cancel: () => (tween.cancelled = true) };
}
/** Вызвать fn через seconds секунд. */
delay(seconds: number, fn: () => void): TweenHandle {
const d: ActiveDelay = { remaining: seconds, fn, cancelled: false };
this.delays.push(d);
return { cancel: () => (d.cancelled = true) };
}
/** Отменить все твины, изменяющие target (таймеры не привязаны к target). */
cancelFor(target: object): void {
for (const t of this.tweens) {
if (t.target === target) t.cancelled = true;
}
this.tweens = this.tweens.filter((t) => !t.cancelled);
}
/** Отменить всё. */
clear(): void {
for (const t of this.tweens) t.cancelled = true;
for (const d of this.delays) d.cancelled = true;
this.tweens = [];
this.delays = [];
}
/** dt в секундах (фиксированный шаг). */
update(dt: number): void {
// Твины
for (const t of this.tweens) {
if (t.cancelled) continue;
const delay = t.options.delay ?? 0;
let dtLeft = dt;
if (t.elapsed < delay) {
const skipped = Math.min(dtLeft, delay - t.elapsed);
t.elapsed += skipped;
dtLeft -= skipped;
if (dtLeft <= 0) continue;
}
if (!t.from) {
t.from = {};
for (const key of Object.keys(t.props)) {
t.from[key] = t.target[key] ?? 0;
}
}
t.elapsed += dtLeft;
const raw = Math.min(1, (t.elapsed - (t.options.delay ?? 0)) / t.options.duration);
const k = t.options.ease!(raw);
for (const key of Object.keys(t.props)) {
t.target[key] = t.from[key] + (t.props[key] - t.from[key]) * k;
}
if (raw >= 1) {
t.cancelled = true;
t.options.onDone?.();
}
}
this.tweens = this.tweens.filter((t) => !t.cancelled);
// Таймеры
for (const d of this.delays) {
if (d.cancelled) continue;
d.remaining -= dt;
if (d.remaining <= 0) {
d.cancelled = true;
d.fn();
}
}
this.delays = this.delays.filter((d) => !d.cancelled);
}
get activeCount(): number {
return this.tweens.filter((t) => !t.cancelled).length + this.delays.filter((d) => !d.cancelled).length;
}
}