/**
* Чистая математика one-shot эффектов: сэмплы состояния (радиус/альфа/смещение)
* в момент t. Без Pixi — тестируется в Vitest headless; адаптеры в этом каталоге
* только применяют сэмплы к вьюхам.
*/
import { backOut, linear, quadOut, type EaseFn } from '../../core/easing';
/** Спецификация кольца/волны; repeat > 1 — телеграф зоны. */
export interface FxRingSpec {
/** Цвет обводки (0xRRGGBB). */
color: number;
/** Стартовый радиус, px. */
from: number;
/** Конечный радиус, px. */
to: number;
/** Секунд на один проход. */
duration: number;
/** Альфа в начале/конце прохода (по умолчанию 1 → 0). */
alphaFrom?: number;
alphaTo?: number;
/** Толщина обводки, px (по умолчанию 1). */
width?: number;
/** Сглаживание радиуса (по умолчанию linear). */
ease?: EaseFn;
/** Число проходов (по умолчанию 1; Infinity — зацикленный телеграф). */
repeat?: number;
/** Пауза между проходами, сек (по умолчанию 0). */
gap?: number;
/** Форма зоны: круг или «footprint»-ромб (квадрат, повёрнутый на 45°). */
shape?: 'circle' | 'diamond';
/** Цвет заливки зоны (телеграф); 0 — без заливки. */
fill?: number;
/** Альфа заливки (по умолчанию 0.25). */
fillAlpha?: number;
}
/** Состояние кольца в момент t. */
export interface FxRingSample {
radius: number;
alpha: number;
}
/** Радиус и альфа кольца в момент t; repeat > 1 — проходы с паузой gap. */
export function sampleRing(t: number, spec: FxRingSpec): FxRingSample {
const dur = Math.max(spec.duration, 1e-6);
const gap = spec.gap ?? 0;
const repeat = spec.repeat ?? 1;
const cycle = dur + gap;
const total = repeat === Infinity ? Infinity : repeat * cycle - gap;
// Прошли все проходы — стоим в конце последнего (mod не должен завернуть границу).
const ended = total !== Infinity && t >= total;
const k = repeat === Infinity ? t % cycle : Math.min(t, total);
const u = ended ? 1 : Math.min(1, Math.max(0, (k % cycle) / dur));
const ease = spec.ease ?? linear;
const aFrom = spec.alphaFrom ?? 1;
const aTo = spec.alphaTo ?? 0;
return {
radius: spec.from + (spec.to - spec.from) * ease(u),
alpha: aFrom + (aTo - aFrom) * u
};
}
/** Полная длительность эффекта, сек (Infinity при repeat: Infinity). */
export function ringDuration(spec: FxRingSpec): number {
const repeat = spec.repeat ?? 1;
if (repeat === Infinity) return Infinity;
return repeat * (spec.duration + (spec.gap ?? 0)) - (spec.gap ?? 0);
}
/** Спецификация всплывающего текста (урон, тосты, подборы). */
export interface FxFloatSpec {
/** Полная длительность, сек. */
duration: number;
/** Плато после подъёма, сек (тосты — «держать, пока читается»). */
hold?: number;
/** Подъём вверх, px (по умолчанию 8; отрицательный — вниз). */
rise?: number;
/** Дрейф по X, px (число урона «уезжает» вбок). */
driftX?: number;
/** Сек нарастания альфы в начале (по умолчанию 0.06). */
fadeIn?: number;
/** Сек затухания в конце (по умолчанию 0.4). */
fadeOut?: number;
/** Ease подъёма (по умолчанию quadOut). */
ease?: EaseFn;
/** Пик овершута масштаба (0 — без «пружинки»). */
pop?: number;
/** Стартовый масштаб при pop (по умолчанию 0.6). */
fromScale?: number;
}
/** Состояние текста в момент t (x/y — смещение от стартовой точки). */
export interface FxFloatSample {
x: number;
y: number;
alpha: number;
scale: number;
}
/** Положение, альфа и масштаб всплывающего текста в момент t. */
export function sampleFloat(t: number, spec: FxFloatSpec): FxFloatSample {
const rise = spec.rise ?? 8;
const fadeIn = spec.fadeIn ?? 0.06;
const fadeOut = spec.fadeOut ?? 0.4;
const hold = spec.hold ?? 0;
const travel = Math.max(spec.duration - hold, 1e-6);
const u = Math.min(1, Math.max(0, (t - hold) / travel));
const ease = spec.ease ?? quadOut;
const alphaIn = Math.min(1, t / Math.max(fadeIn, 1e-6));
const alphaOut = Math.min(1, (spec.duration - t) / Math.max(fadeOut, 1e-6));
const pop = spec.pop ?? 0;
const fromScale = spec.fromScale ?? 0.6;
const scale = pop > 0
? fromScale + (1 - fromScale) * Math.min(1, t / Math.max(pop, 1e-6))
: 1;
return {
x: (spec.driftX ?? 0) * u,
y: 0 - rise * ease(u), // 0 − (…) вместо -(…): не даём «минус ноль» на старте
alpha: Math.max(0, Math.min(alphaIn, alphaOut)),
scale
};
}
/**
* Время чтения строки (для тостов): base + perChar*len, не больше max.
* Дефолты совпадают с историческим тостом демо: 1.4 + 0.045/символ, не дольше 6.
*/
export function readSeconds(
text: string,
opts?: { base?: number; perChar?: number; max?: number }
): number {
const base = opts?.base ?? 1.4;
const perChar = opts?.perChar ?? 0.045;
const max = opts?.max ?? 6;
return Math.min(max, base + text.length * perChar);
}
/** Спецификация растворения/проявления произвольной вьюхи. */
export interface FxFadeSpec {
/** Секунд до конца. */
duration: number;
/** Целевая альфа (по умолчанию 0 — растворение). */
to?: number;
/** Задержка старта, сек. */
delay?: number;
/** Ease альфы (по умолчанию quadOut). */
ease?: EaseFn;
}
/** Альфа вьюхи в момент t: from → to за duration после delay. */
export function sampleFade(t: number, from: number, spec: FxFadeSpec): number {
const delay = spec.delay ?? 0;
const u = Math.min(1, Math.max(0, (t - delay) / Math.max(spec.duration, 1e-6)));
const ease = spec.ease ?? quadOut;
const to = spec.to ?? 0;
return from + (to - from) * ease(u);
}
/** Спецификация появления с овершутом (pop/squash). */
export interface FxPopSpec {
/** Секунд (по умолчанию 0.25). */
duration?: number;
/** Стартовый множитель масштаба (по умолчанию 0.5). */
fromScale?: number;
/** Пик овершута (по умолчанию 1.12; 1 — без пружинки). */
overshoot?: number;
/** Ease масштаба (по умолчанию backOut). */
ease?: EaseFn;
}
/** Множитель масштаба в момент t (1 в покое): from → overshoot → 1. */
export function samplePop(t: number, spec: FxPopSpec = {}): number {
const dur = spec.duration ?? 0.25;
const from = spec.fromScale ?? 0.5;
const over = spec.overshoot ?? 1.12;
const ease = spec.ease ?? backOut;
const u = Math.min(1, Math.max(0, t / Math.max(dur, 1e-6)));
if (u >= 1) return 1;
// Две фазы: разгон from → overshoot (70% времени), затем плавный возврат к 1.
const peakT = 0.7;
if (u < peakT) {
return from + (over - from) * ease(u / peakT);
}
const settle = (u - peakT) / (1 - peakT);
return over + (1 - over) * settle;
}