Newer
Older
rpg / packages / engine / src / anim / motion.ts
/**
 * Процедурные «оживители» статичных объектов — чистая математика без Pixi:
 * синусные волны, композиция до четырёх одновременно. Фазы — в долях
 * периода; случайная фаза берётся из seed снаружи (детерминизм).
 */

export interface BobParams {
    /** Амплитуда, px (округляется до целого — пиксель-арт не дрожит). */
    amplitude: number;
    /** Полный период, сек. */
    period: number;
    /** Фаза, доли периода (0..1). */
    phase?: number;
}

export interface SwayParams {
    /** Амплитуда, рад (вокруг основания view). */
    amplitude: number;
    period: number;
    phase?: number;
}

export interface PulseParams {
    /** Нижняя и верхняя граница пульса (alpha или scale). */
    min: number;
    max: number;
    period: number;
    phase?: number;
}

export interface BlinkParams {
    period: number;
    /** Доля периода в «включённом» состоянии (0..1), по умолчанию 0.5. */
    duty?: number;
    phase?: number;
}

export interface MotionParams {
    bob?: BobParams;
    sway?: SwayParams;
    pulse?: PulseParams;
    blink?: BlinkParams;
}

/** Выборка оживителя в момент t (сек) относительно базового состояния. */
export interface MotionSample {
    /** Смещение по Y, px (целое). */
    y: number;
    /** Поворот, рад. */
    angle: number;
    /** Множитель альфы (произведение пульса и строба). */
    alpha: number;
    /** Множитель масштаба. */
    scale: number;
}

const TAU = Math.PI * 2;

/** Фаза волны 0..1 в момент t для периода period и стартовой фазы phase. */
function wave(t: number, period: number, phase = 0): number {
    return ((t / period + phase) % 1 + 1) % 1;
}

/** Выборка всех оживителей в момент t; параметры независимы, эффекты перемножаются. */
export function sampleMotion(p: MotionParams, t: number): MotionSample {
    let y = 0;
    let angle = 0;
    let alpha = 1;
    let scale = 1;
    if (p.bob) {
        const w = wave(t, p.bob.period, p.bob.phase);
        // Синус фазы 0..1 → -1..1; округление до целого px.
        y = Math.round(Math.sin(w * TAU) * p.bob.amplitude);
    }
    if (p.sway) {
        const w = wave(t, p.sway.period, p.sway.phase);
        angle = Math.sin(w * TAU) * p.sway.amplitude;
    }
    if (p.pulse) {
        const w = wave(t, p.pulse.period, p.pulse.phase);
        // Треугольная волна 0..1..0 — пульс без резких скачков фазы.
        const tri = 1 - Math.abs(w * 2 - 1);
        const k = p.pulse.min + (p.pulse.max - p.pulse.min) * tri;
        alpha *= k;
        scale *= k;
    }
    if (p.blink) {
        const w = wave(t, p.blink.period, p.blink.phase);
        alpha *= w < (p.blink.duty ?? 0.5) ? 1 : 0;
    }
    return { y, angle, alpha, scale };
}