import type { AnimationAsset, TextureAsset } from './Asset'; import { textOf } from './Properties'; export const PLAYBACKS = ['loop', 'once', 'pingpong'] as const; export type Playback = (typeof PLAYBACKS)[number]; export interface FrameAt { /** -1 for an animation without frames. */ index: number; /** Only `once` ever finishes; it then holds its last frame. */ finished: boolean; } /** Slack for `time * fps`: six ticks of 1/60 s sum to 0.09999999999999999, which at 10 fps is frame 1. */ const STEP_EPSILON = 1e-6; /** Which frame shows at `time` seconds after the animation started. */ export function frameAt(count: number, fps: number, playback: Playback, time: number): FrameAt { if (count <= 0) return { index: -1, finished: false }; if (!Number.isFinite(fps) || fps <= 0 || !(time > 0)) return { index: 0, finished: false }; const step = Math.floor(time * fps + STEP_EPSILON); switch (playback) { case 'loop': return { index: step % count, finished: false }; case 'once': return step >= count ? { index: count - 1, finished: true } : { index: step, finished: false }; case 'pingpong': { if (count === 1) return { index: 0, finished: false }; const period = 2 * count - 2; const at = step % period; return { index: at < count ? at : period - at, finished: false }; } } } export function playbackOf(asset: AnimationAsset): Playback { const value = textOf(asset.props.playback ?? 'loop'); return (PLAYBACKS as readonly string[]).includes(value) ? (value as Playback) : 'loop'; } export function fpsOf(asset: AnimationAsset): number { const fps = Number(asset.props.fps); return Number.isFinite(fps) && fps > 0 ? fps : 0; } export function animationFrameAt(asset: AnimationAsset, time: number): FrameAt { return frameAt(asset.frames.length, fpsOf(asset), playbackOf(asset), time); } /** Seconds for one pass through the frames. */ export function animationDuration(asset: AnimationAsset): number { const fps = fpsOf(asset); return fps > 0 ? asset.frames.length / fps : 0; } /** What an animation made from a texture plays: its frames in order, the whole picture when it has none. */ export function framesOfTexture(texture: TextureAsset): string[] { return texture.frames.length > 0 ? texture.frames.map((f) => f.id) : [texture.id]; }