import { describe, it, expect } from 'vitest';
import {
sampleRing, ringDuration, sampleFloat, readSeconds, sampleFade, samplePop
} from '../fxSim';
import { quadOut } from '../../../core/easing';
describe('sampleRing', () => {
const spec = { color: 0xffffff, from: 8, to: 96, duration: 0.4 };
it('границы прохода: t=0 → from/alphaFrom, t=duration → to/alphaTo', () => {
expect(sampleRing(0, spec)).toEqual({ radius: 8, alpha: 1 });
expect(sampleRing(0.4, spec)).toEqual({ radius: 96, alpha: 0 });
});
it('ease применяется к радиусу, но не к альфе', () => {
const eased = sampleRing(0.2, { ...spec, ease: quadOut });
// quadOut(0.5) = 0.75 → радиус дальше середины
expect(eased.radius).toBeCloseTo(8 + 88 * 0.75, 5);
expect(eased.alpha).toBeCloseTo(0.5, 5);
});
it('repeat с gap: второй проход начинается с from после паузы', () => {
const s = { ...spec, repeat: 3, gap: 0.1 };
// конец первого прохода + пауза — радиус снова from (начало второго)
const second = sampleRing(0.4 + 0.1 + 0.01, s);
expect(second.radius).toBeCloseTo(8 + 88 * 0.025, 1);
// после всех проходов (3*0.4 + 2*0.1 = 1.4) — конец
expect(ringDuration(s)).toBeCloseTo(1.4, 5);
expect(sampleRing(1.4, s).alpha).toBeCloseTo(0, 5);
});
it('repeat: Infinity — бесконечная длительность, циклы повторяются', () => {
const s = { ...spec, repeat: Infinity, gap: 0.1 };
expect(ringDuration(s)).toBe(Infinity);
// t = 0.5 (заход во второй цикл) — как t = 0.5 - 0.5 цикла... проверяем цикличность
const a = sampleRing(0.4 + 0.1 + 0.0, s);
const b = sampleRing(0.4 + 0.1 + 0.5 + 0.4 + 0.1, s);
expect(a.radius).toBeCloseTo(b.radius, 5);
expect(a.alpha).toBeCloseTo(b.alpha, 5);
});
it('отрицательный t клампится к началу', () => {
expect(sampleRing(-1, spec).radius).toBe(8);
});
});
describe('sampleFloat', () => {
it('границы: t=0 → y=0/alpha=0, конец → y=-rise/alpha=0', () => {
const spec = { duration: 1, rise: 8 };
expect(sampleFloat(0, spec)).toEqual({ x: 0, y: 0, alpha: 0, scale: 1 });
const end = sampleFloat(1, spec);
expect(end.y).toBeCloseTo(-8, 5);
expect(end.alpha).toBe(0);
});
it('в середине — alpha 1, подъём по quadOut', () => {
const s = sampleFloat(0.5, { duration: 1, rise: 8 });
expect(s.alpha).toBe(1);
expect(s.y).toBeCloseTo(-8 * 0.75, 5); // quadOut(0.5)
});
it('hold: плато позиции и альфы до конца hold', () => {
const spec = { duration: 2, hold: 1, rise: 10 };
const mid = sampleFloat(0.5, spec);
expect(mid.y).toBe(0);
expect(mid.alpha).toBe(1);
});
it('driftX смещает x пропорционально прогрессу', () => {
const s = sampleFloat(1, { duration: 1, driftX: 12 });
expect(s.x).toBeCloseTo(12, 5);
});
it('pop: масштаб растёт от fromScale к 1 в течение pop', () => {
const s = sampleFloat(0.15, { duration: 1, pop: 0.3, fromScale: 0.6 });
expect(s.scale).toBeCloseTo(0.6 + 0.4 * 0.5, 5);
expect(sampleFloat(0.4, { duration: 1, pop: 0.3, fromScale: 0.6 }).scale).toBe(1);
});
});
describe('readSeconds', () => {
it('база + perChar за символ', () => {
expect(readSeconds('')).toBeCloseTo(1.4, 5);
expect(readSeconds('ab')).toBeCloseTo(1.4 + 2 * 0.045, 5);
});
it('не больше max', () => {
expect(readSeconds('x'.repeat(500))).toBe(6);
});
});
describe('sampleFade', () => {
it('границы: from → to', () => {
expect(sampleFade(0, 1, { duration: 0.5 })).toBe(1);
expect(sampleFade(0.5, 1, { duration: 0.5 })).toBeCloseTo(0, 5);
});
it('delay сдвигает старт', () => {
const spec = { duration: 0.5, delay: 0.25 };
expect(sampleFade(0.25, 1, spec)).toBe(1);
expect(sampleFade(0.5, 1, spec)).toBeCloseTo(0.25, 5); // половина пути
expect(sampleFade(0.75, 1, spec)).toBeCloseTo(0, 5);
});
it('кастомная цель: проявление до 0.8', () => {
expect(sampleFade(1, 0.2, { duration: 1, to: 0.8 })).toBeCloseTo(0.8, 5);
});
});
describe('samplePop', () => {
it('границы: t=0 → fromScale, конец → ровно 1', () => {
expect(samplePop(0, {})).toBeCloseTo(0.5, 5);
expect(samplePop(0.25, {})).toBe(1);
expect(samplePop(10, {})).toBe(1);
});
it('пик у overshoot в районе 70% времени', () => {
const peak = samplePop(0.175, { duration: 0.25 }); // u = 0.7
expect(peak).toBeCloseTo(1.12, 5);
});
it('кастомные параметры honored', () => {
const spec = { duration: 1, fromScale: 0.2, overshoot: 1.5 };
expect(samplePop(0, spec)).toBeCloseTo(0.2, 5);
expect(samplePop(0.7, spec)).toBeCloseTo(1.5, 5);
});
});
describe('чистота сэмплов', () => {
it('двойной прогон даёт одинаковые значения (нет Math.random)', () => {
const ringSpec = { color: 0xd99a32, from: 8, to: 96, duration: 0.4, repeat: 3, gap: 0.1 };
const floatSpec = { duration: 2, hold: 0.8, driftX: 5, pop: 0.3 };
const a = [0, 0.13, 0.5, 1.23, 2].map((t) => JSON.stringify(sampleFloat(t, floatSpec)));
const b = [0, 0.2, 0.5, 0.9, 1.4].map((t) => JSON.stringify(sampleRing(t, ringSpec)));
const a2 = [0, 0.13, 0.5, 1.23, 2].map((t) => JSON.stringify(sampleFloat(t, floatSpec)));
const b2 = [0, 0.2, 0.5, 0.9, 1.4].map((t) => JSON.stringify(sampleRing(t, ringSpec)));
expect(a).toEqual(a2);
expect(b).toEqual(b2);
});
});