diff --git a/README.md b/README.md index e2510ac..cbed200 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ CDN usage: | Procedural generation | `perlin`, `perlin3D`, `simplex2D`, `simplex3D`, `worley`, `worleySample`, `waveFunctionCollapse`, `cellularAutomataCave`, `poissonDiskSampling`, `computeVoronoiDiagram`, `diamondSquare`, `generateLSystem`, `generateBspDungeon`, `generateRecursiveMaze`, `generatePrimMaze`, `generateKruskalMaze`, `generateWilsonMaze`, `generateAldousBroderMaze`, `generateRecursiveDivisionMaze` | `procedural/*.ts` | `examples/simplex.ts`, `examples/worley.ts`, `examples/waveFunctionCollapse.ts`, `examples/cellularAutomata.ts`, `examples/poissonDisk.ts`, `examples/voronoi.ts`, `examples/diamondSquare.ts`, `examples/lSystem.ts`, `examples/dungeonBsp.ts`, `examples/mazeRecursive.ts`, `examples/mazePrim.ts`, `examples/mazeKruskal.ts`, `examples/mazeWilson.ts`, `examples/mazeAldous.ts`, `examples/mazeDivision.ts` | | Spatial queries & collision | `Quadtree`, `aabbCollision`, `aabbIntersection`, `satCollision`, `circleRayIntersection`, `sweptAABB` | `spatial/*.ts` | `examples/sat.ts` | | AI behaviours & crowds | `seek`, `flee`, `arrive`, `pursue`, `wander`, `updateBoids`, `BehaviorTree`, `rvoStep` | `ai/steering.ts`, `ai/boids.ts`, `ai/behaviorTree.ts`, `ai/rvo.ts` | `examples/steering.ts`, `examples/boids.ts`, `examples/rvo.ts` | -| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `fisherYatesShuffle`, `createFixedTimestepLoop` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/fisherYates.ts`, `examples/fixedTimestep.ts` | +| Web performance & UI | `debounce`, `throttle`, `LRUCache`, `memoize`, `deduplicateRequest`, `clearRequestDedup`, `calculateVirtualRange`, `createWeightedAliasSampler`, `createObjectPool`, `createDeltaTimeManager`, `createFixedTimestepLoop`, `fisherYatesShuffle` | `util/*.ts` | `examples/requestDedup.ts`, `examples/virtualScroll.ts`, `examples/weightedAlias.ts`, `examples/objectPool.ts`, `examples/deltaTime.ts`, `examples/fixedTimestep.ts`, `examples/fisherYates.ts` | | Search & text | `fuzzySearch`, `fuzzyScore`, `Trie`, `binarySearch`, `levenshteinDistance` | `search/*.ts` | `examples/search.ts` | | Data & diff pipelines | `diff`, `deepClone`, `groupBy`, `diffJson`, `applyJsonDiff` | `data/*.ts` | `examples/jsonDiff.ts` | | Graph algorithms | `graphBFS`, `graphDFS`, `topologicalSort` | `graph/traversal.ts` | `examples/graph.ts` | diff --git a/ROADMAP.md b/ROADMAP.md index 06202cb..815e494 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -45,7 +45,7 @@ - [ ] Maze algorithms pack (Recursive backtracking ✅, Prim's ✅, Kruskal's ✅, Wilson's ✅, Aldous–Broder ✅, Recursive Division ✅) - Gameplay systems & utilities: - [x] Fixed-timestep game loop utility with interpolation helpers - - [ ] Delta-time manager for frame-independent timing + - [x] Delta-time manager for frame-independent timing - [x] Object pool helper for reusable entities - [x] Weighted random selector (alias method) - [x] Fisher–Yates shuffle implementation diff --git a/docs/index.d.ts b/docs/index.d.ts index 41cc4cd..63a02ff 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -853,6 +853,35 @@ export interface ObjectPool { */ export function createObjectPool(options: ObjectPoolOptions): ObjectPool; +/** + * Delta-time manager configuration. + * Use for: clamping frame spikes, tuning smoothing windows. + * Import: util/deltaTime.ts + */ +export interface DeltaTimeOptions { + maxDelta?: number; + smoothing?: number; +} + +/** + * Delta-time manager API. + * Use for: sampling frame durations and resetting loops. + * Import: util/deltaTime.ts + */ +export interface DeltaTimeManager { + update(timestamp: number): number; + getDelta(): number; + reset(): void; +} + +/** + * Creates a delta-time manager that smooths and clamps frame durations. + * Use for: game loops, animation systems, interpolation factors. + * Performance: O(1) per update with small smoothing window maintenance. + * Import: util/deltaTime.ts + */ +export function createDeltaTimeManager(options?: DeltaTimeOptions): DeltaTimeManager; + /** * Shuffles an array in place using Fisher–Yates. * Use for: unbiased permutations, testing, random ordering. diff --git a/examples/deltaTime.ts b/examples/deltaTime.ts new file mode 100644 index 0000000..e48b739 --- /dev/null +++ b/examples/deltaTime.ts @@ -0,0 +1,7 @@ +import { createDeltaTimeManager } from '../src/index.js'; + +const manager = createDeltaTimeManager({ smoothing: 2, maxDelta: 0.05 }); + +console.log('Delta 0:', manager.update(0)); +console.log('Delta 1:', manager.update(16)); +console.log('Delta 2:', manager.update(32)); diff --git a/src/index.ts b/src/index.ts index 9cfe608..da8ea9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,6 +87,7 @@ export const examples = { calculateVirtualRange: 'examples/virtualScroll.ts', createWeightedAliasSampler: 'examples/weightedAlias.ts', createObjectPool: 'examples/objectPool.ts', + createDeltaTimeManager: 'examples/deltaTime.ts', fisherYatesShuffle: 'examples/fisherYates.ts', createFixedTimestepLoop: 'examples/fixedTimestep.ts', }, @@ -425,6 +426,13 @@ export { createWeightedAliasSampler } from './util/weightedAlias.js'; */ export { createObjectPool } from './util/objectPool.js'; +/** + * Delta-time manager that clamps spikes and smooths frame durations. + * + * Example file: examples/deltaTime.ts + */ +export { createDeltaTimeManager } from './util/deltaTime.js'; + /** * Fisher–Yates shuffling utility for unbiased permutations. * @@ -448,6 +456,11 @@ export type { VirtualScrollOptions, } from './util/virtualScroll.js'; +/** + * Delta-time manager types for smoothing configuration and runtime control. + */ +export type { DeltaTimeOptions, DeltaTimeManager } from './util/deltaTime.js'; + // ============================================================================ // 🔍 SEARCH & STRING UTILITIES // ============================================================================ diff --git a/src/util/deltaTime.ts b/src/util/deltaTime.ts new file mode 100644 index 0000000..804d789 --- /dev/null +++ b/src/util/deltaTime.ts @@ -0,0 +1,96 @@ +/** + * Configuration options for {@link createDeltaTimeManager}. + * Useful for: bounding large frame spikes and smoothing jitter. + */ +export interface DeltaTimeOptions { + /** Maximum delta in seconds, defaults to 1/10 (100 ms). */ + maxDelta?: number; + /** Number of samples to smooth, defaults to 1 (no smoothing). */ + smoothing?: number; +} + +/** + * Runtime interface returned by {@link createDeltaTimeManager}. + * Useful for: polling delta inside fixed or variable timestep loops. + */ +export interface DeltaTimeManager { + /** Updates the manager with the latest timestamp (ms) and returns smoothed delta (seconds). */ + update(timestamp: number): number; + /** Returns the most recent smoothed delta (seconds). */ + getDelta(): number; + /** Resets internal state and clears accumulated samples. */ + reset(): void; +} + +/** + * Creates a delta-time manager for animation/gameplay loops. + * Useful for: smoothing frame time, clamping spikes, and driving interpolation factors. + * + * @example + * ```ts + * const manager = createDeltaTimeManager({ maxDelta: 0.05, smoothing: 4 }); + * const delta = manager.update(performance.now()); + * ``` + */ +export function createDeltaTimeManager({ + maxDelta = 0.1, + smoothing = 1, +}: DeltaTimeOptions = {}): DeltaTimeManager { + if (typeof maxDelta !== 'number' || Number.isNaN(maxDelta) || !Number.isFinite(maxDelta)) { + throw new Error('maxDelta must be a finite number.'); + } + if (maxDelta <= 0) { + throw new Error('maxDelta must be greater than zero.'); + } + if (typeof smoothing !== 'number' || Number.isNaN(smoothing) || smoothing < 1) { + throw new Error('smoothing must be a number >= 1.'); + } + if (!Number.isInteger(smoothing)) { + throw new Error('smoothing must be an integer.'); + } + + let lastTimestamp: number | undefined; + const samples: number[] = []; + let currentDelta = 0; + + function update(timestamp: number): number { + if (typeof timestamp !== 'number' || Number.isNaN(timestamp) || !Number.isFinite(timestamp)) { + throw new Error('timestamp must be a finite number.'); + } + + if (lastTimestamp === undefined) { + lastTimestamp = timestamp; + currentDelta = 0; + return currentDelta; + } + + let delta = (timestamp - lastTimestamp) / 1000; + lastTimestamp = timestamp; + + if (delta > maxDelta) { + delta = maxDelta; + } else if (delta < 0) { + delta = 0; + } + + samples.push(delta); + if (samples.length > smoothing) { + samples.shift(); + } + + currentDelta = samples.reduce((sum, value) => sum + value, 0) / samples.length; + return currentDelta; + } + + function getDelta(): number { + return currentDelta; + } + + function reset(): void { + lastTimestamp = undefined; + samples.length = 0; + currentDelta = 0; + } + + return { update, getDelta, reset }; +} diff --git a/tests/deltaTime.test.ts b/tests/deltaTime.test.ts new file mode 100644 index 0000000..c734507 --- /dev/null +++ b/tests/deltaTime.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; + +import { createDeltaTimeManager } from '../src/index.js'; + +describe('createDeltaTimeManager', () => { + it('clamps delta by maxDelta and smooths samples', () => { + const manager = createDeltaTimeManager({ maxDelta: 0.05, smoothing: 2 }); + expect(manager.update(0)).toBe(0); + expect(manager.update(10)).toBeCloseTo(0.01, 5); + expect(manager.update(200)).toBeCloseTo(0.03, 5); // (0.01 + clamp 0.05) /2 + expect(manager.getDelta()).toBeCloseTo(0.03, 5); + }); + + it('reset clears internal state', () => { + const manager = createDeltaTimeManager(); + manager.update(0); + manager.update(16); + manager.reset(); + expect(manager.getDelta()).toBe(0); + expect(manager.update(100)).toBe(0); + }); + + it('validates options and inputs', () => { + expect(() => createDeltaTimeManager({ maxDelta: 0 })).toThrow(/greater than zero/i); + expect(() => createDeltaTimeManager({ smoothing: 0 })).toThrow(/>= 1/i); + expect(() => createDeltaTimeManager({ smoothing: 1.5 })).toThrow(/integer/i); + + const manager = createDeltaTimeManager(); + expect(() => manager.update(Number.NaN)).toThrow(/finite number/i); + }); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index 66a1903..9ce2d7f 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -87,6 +87,7 @@ describe('package entry point', () => { | 'calculateVirtualRange' | 'createWeightedAliasSampler' | 'createObjectPool' + | 'createDeltaTimeManager' | 'fisherYatesShuffle' | 'createFixedTimestepLoop' >();