npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

anim-engine

v0.6.0

Published

JavaScript library for animating numbers

Readme

NPM Version License CI

Renderer-agnostic animation for JavaScript runtimes. Pure-numeric, tree-shakeable, zero dependencies.

npm install anim-engine
import { createAnimation, getTicker } from "anim-engine";

getTicker().start();

const anim = createAnimation({
  from: 0,
  to: 100,
  durationMs: 1000,
  ease: "outCubic",
  onUpdate: (value) => (sprite.x = value),
});

await anim.play();

AI agents: this package includes a SKILL.md with in-depth usage guidance for agent-assisted development.

Design

Anim Engine is built around the same concepts used in animation applications, applied to pure numbers.

Timed motion — a fixed path evaluated when play() is called.

  • Tween — animate from value A to value B over a duration with easing.
  • Keyframes — multi-segment motion describing a value at specific points in time.
  • Timeline — orchestrate multiple keyframe animations in parallel, sequence, or staggered offset.

Continuous motion — chases a live target every frame.

  • Spring — mass-spring-damper physics for bouncy or elastic movement.
  • Smooth damp — natural deceleration toward a target (Unity-style).
  • Lerp — exponential approach for smooth asymptotic tracking.
  • Inertia — velocity decay for drag-to-throw gestures (fling, spin).
  • Smooth scroll — delta-based momentum scroll with clamped bounds.
  • Smooth clamp — asymptotic saturation for capping velocity, torque, or force.

Color helpers — Oklab-based composable interpolation.

  • lerpRgba — interpolates between two RGBA colors in perceptually uniform space.
  • hexToRgba — parse hex color strings (#RGB, #RRGGBB, etc.) to normalized RGBA.

Deliberately decoupled

Anim Engine works strictly with numbers to stay renderer-agnostic. Each function accepts an onUpdate callback with the latest value and velocity — assign it to a DOM element, WebGL object, or any other target. Read-only value and velocity properties are also exposed for polling inside a game loop.


Primitives

Two families with complementary shapes:

| Family | Functions | Lifespan | | -------------- | --------------------------------------------------------------------------------------- | ----------------------------------------- | | Timed | createAnimation, createTimeline | Fixed motion path, runs once per play() | | Continuous | createSpring, createSmoothDamp, createLerp, createInertia, createSmoothScroll | Chases a live target until stopped |

All return a control handle with value, velocity, status, and lifecycle methods.

createSmoothClamp is a pure function factory (no handle — returns (input: number) => number).

createInertia and createSmoothScroll are gesture-driven continuous primitives with distinct control handles (see below).

At a glance

Timed — Animation

type Animation = {
  play: () => Promise<void>;
  pause: () => void;
  resume: () => void;
  stop: () => void;
  skipToEnd: () => void;
  setProgress: (value: number) => void;

  value: number;
  velocity: number;
  progress: number; // 0 → 1
  status: "playing" | "paused" | "stopped";
  durationMs: number;
};

Options — single tween:

type TweenOptions = {
  from: DynamicValue;
  to: DynamicValue;
  durationMs: DynamicValue;
  ease?: EaseName | EaseFunction;
  onStarted?: () => void;
  onUpdate?: (value: number, velocity: number) => void;
  onProgress?: (progress: number) => void;
  onEnded?: () => void;
  ticker?: ExternalTicker;
};

Options — keyframes:

type KeyframeOptions = {
  keyframes: Keyframe[];
  onStarted?: () => void;
  onUpdate?: (value: number, velocity: number) => void;
  onProgress?: (progress: number) => void;
  onEnded?: () => void;
  ticker?: ExternalTicker;
};

type Keyframe = {
  value: DynamicValue;
  gap?: DynamicValue; // ms from previous keyframe
  ease?: EaseName | EaseFunction;
};

Timed — Timeline

Layers multiple keyframe animations in parallel or sequence.

type Timeline = {
  play: () => Promise<void>;
  pause: () => void;
  resume: () => void;
  stop: () => void;
  skipToEnd: () => void;
  setProgress: (value: number) => void;

  values: number[]; // one per layer
  velocities: number[]; // one per layer
  progress: number;
  status: "playing" | "paused" | "stopped";
  durationMs: number;
};
type TimelineLayer =
  | { animation: KeyframeOptions; at: DynamicValue } // absolute position
  | { animation: KeyframeOptions; gap: number }; // relative to previous layer end

Continuous — Interpolation

type Interpolation = {
  resume: () => void;
  stop: () => void;
  setValue: (value: number) => void;

  value: number;
  velocity: number;
  status: "active" | "inactive";
};

Options:

type SpringOptions = {
  to: () => number
  stiffness?: DynamicValue   // default 180
  damping?: DynamicValue     // default 12
  mass?: DynamicValue        // default 1
  precision?: number         // default 0.01
  onUpdate?: (value: number, velocity: number) => void
  onEnded?: () => void
  ticker?: ExternalTicker
}

type SmoothDampOptions = {
  to: () => number
  smoothTimeMs: DynamicValue
  maxSpeed?: DynamicValue
  precision?: number         // default 0.01
  onUpdate?: (value: number, velocity: number) => void
  onEnded?: () => void
  ticker?: ExternalTicker
}

type LerpOptions = {
  to: () => number
  smoothTimeMs: DynamicValue
  precision?: number         // default 0.01
  onUpdate?: (value: number, velocity: number) => void
  onEnded?: () => void
  ticker?: ExternalTicker
}
```

### Common option patterns

| | `from` | `to` / target | `onUpdate` values | `durationMs` / `smoothTimeMs` |
|---|---|---|---|---|
| `createAnimation` (tween) | ✅ `DynamicValue` | ✅ `DynamicValue` | single `(value, velocity)` | ✅ `DynamicValue` |
| `createAnimation` (keyframes) | — (first keyframe) | — (last keyframe) | single `(value, velocity)` | — (sum of gaps) |
| `createTimeline` | — | — | multi `(values[], velocities[])` | — (computed) |
| `createSpring` | — (chases current) | ✅ `() => number` | single `(value, velocity)` | — (physics params) |
| `createSmoothDamp` | — (chases current) | ✅ `() => number` | single `(value, velocity)` | ✅ `DynamicValue` |
| `createLerp` | — (chases current) | ✅ `() => number` | single `(value, velocity)` | ✅ `DynamicValue` |

### Shared options

```ts
type DynamicValue = number | (() => number);

type ExternalTicker = {
  add: (handler: TickHandler) => void;
  remove: (handler: TickHandler) => void;
};
```

---

## Examples

### Single tween

```ts
const anim = createAnimation({
  from: 0,
  to: 100,
  durationMs: 2000,
  ease: "outElastic",
  onUpdate: (v) => (sprite.x = v),
});
await anim.play();
```

### Keyframes

```ts
createAnimation({
  keyframes: [{ value: 0 }, { value: 50, gap: 300, ease: "outCubic" }, { value: 100, gap: 400 }],
  onUpdate: (v) => (sprite.x = v),
}).play();
```

### Repeat & yoyo

```ts
const anim = createAnimation({
  from: () => (forward ? 1 : 1.3),
  to: () => (forward ? 1.3 : 1),
  durationMs: 600,
  ease: "inOutSine",
  onUpdate: (v) => sprite.scale.set(v),
});

for (let i = 0; i < 6; i++) {
  forward = !forward;
  await anim.play();
}
```

### Timeline

```ts
createTimeline(
  [
    {
      at: 0,
      animation: {
        keyframes: [{ value: 0 }, { value: 1, gap: 500 }],
        onEnded: () => console.log("layer 1 done"),
      },
    },
    {
      at: 0,
      animation: {
        keyframes: [{ value: -100 }, { value: 0, gap: 800, ease: "outBack" }],
        onUpdate: (v) => (sprite.y = v),
      },
    },
  ],
  {
    // values[i] / velocities[i] correspond to layer i in definition order
    onUpdate: (values, velocities) => {
      sprite.x = values[0];
      sprite.y = values[1];
    },
    onProgress: (p) => console.log(p),
  },
);
```

### Spring

```ts
const spring = createSpring({
  to: () => mouseX,
  stiffness: 180,
  damping: 12,
  onUpdate: (v) => (sprite.x = v),
});
```

### Smooth damp

```ts
createSmoothDamp({
  to: () => 100,
  smoothTimeMs: 300,
  onUpdate: (v) => (sprite.x = v),
});
```

### Lerp

```ts
createLerp({
  to: () => 100,
  smoothTimeMs: 300,
  onUpdate: (v) => (sprite.x = v),
});
```

### Smooth clamp

```ts
const clamp = createSmoothClamp(45);
clamp(1000); // ≈ 44.96 (asymptotic to 45)
```

### Inertia (drag-to-throw)

Call `track()` every frame while dragging, then `release()` to coast.

```ts
const inertia = createInertia({
  decelerationMs: 300,
  precision: 0.5,
  onUpdate: (v) => (block.style.left = `${v}px`),
});

function onPointerDown(e) {
  inertia.track(e.clientX);
}
function onPointerMove(e) {
  if (e.buttons) inertia.track(e.clientX);
}
function onPointerUp() {
  inertia.release();
}

// Teleport and reset velocity instantly
inertia.setValue(30);
```

### Smooth scroll (momentum scroll)

Delta-driven momentum scroll with optional bounds. Target is clamped on write so there's no dead zone when coming back from overscroll.

```ts
const scroll = createSmoothScroll({
  smoothTimeMs: 30,
  min: 0,
  max: 650,
  onUpdate: (v) => (track.style.transform = `translateX(${-v}px)`),
});

// Wheel
viewport.addEventListener("wheel", (e) => {
  scroll.addDelta(e.deltaX || e.deltaY);
});

// Pointer drag
function onPointerDown(e) {
  scroll.setValue(scroll.value); // re-sync on grab
}
function onPointerMove(e) {
  if (e.buttons) scroll.addDelta(e.clientX - lastX);
}
```

---

## Easing

31 named presets plus custom cubic bezier:

```ts
type EaseName =
  | "linear"
  | "inQuad"
  | "outQuad"
  | "inOutQuad"
  | "inCubic"
  | "outCubic"
  | "inOutCubic"
  | "inQuart"
  | "outQuart"
  | "inOutQuart"
  | "inQuint"
  | "outQuint"
  | "inOutQuint"
  | "inSine"
  | "outSine"
  | "inOutSine"
  | "inExpo"
  | "outExpo"
  | "inOutExpo"
  | "inCirc"
  | "outCirc"
  | "inOutCirc"
  | "inBack"
  | "outBack"
  | "inOutBack"
  | "inElastic"
  | "outElastic"
  | "inOutElastic"
  | "inBounce"
  | "outBounce"
  | "inOutBounce";

type EaseFunction = (t: number) => number;
```

Custom bezier:

```ts
const ease = cubicBezier(0.25, 0.1, 0.25, 1);
createAnimation({ from: 0, to: 100, durationMs: 1000, ease, ... });
```

---

## Dynamic values

`DynamicValue` (`number | (() => number)`) is resolved at different frequencies depending on the primitive:

- **Timed animations** — resolved once per `play()` call and cached for the run.
- **Continuous primitives** — resolved every frame (targets are live).

```ts
// Resolved at play time, cached for duration
createAnimation({
  from: () => getX(),
  durationMs: () => 500 / speed,
  keyframes: [{ value: 0 }, { value: () => targetY, gap: 300 }],
}).play();

// Resolved every frame
createSpring({ to: () => mouseX, ... });
```

---

## Color

Oklab (perceptually uniform) color interpolation via `lerpRgba`.

```ts
type RgbaTuple = [number, number, number, number]

hexToRgba(hex: string): RgbaTuple
lerpRgba(from: RgbaTuple, to: RgbaTuple, t: number): RgbaTuple
```

```ts
const from = hexToRgba("#ff6b6b"); // [1, 0.42, 0.42, 1]
const to = hexToRgba("#4ecdc4");

createAnimation({
  from: 0,
  to: 1,
  durationMs: 2000,
  onUpdate: (t) => {
    const [r, g, b, a] = lerpRgba(from, to, t);
    sprite.setColor(r, g, b, a);
  },
});
```

Accepts `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` formats.

---

## Ticker

Does **not** auto-start. Explicit control. **Call `getTicker().start()` once per application** — at your entry point, not inside a component or per-animation.

```ts
import { getTicker } from "anim-engine";

// rAF mode
getTicker().start();

// Custom game loop
const ticker = getTicker();
function loop(dt: number) {
  ticker.update(dt);
}
```

Stop with `getTicker().stop()`.

---

## Rendering Library integration

### PixiJS

```ts
import { createAnimation, getTicker } from "anim-engine";
const ticker = getTicker();

app.ticker.add((delta) => ticker.update(delta.deltaMS));

createAnimation({
  from: 0,
  to: 300,
  durationMs: 2000,
  ease: "outElastic",
  onUpdate: (x) => (sprite.x = x),
}).play();
```

### ThreeJS

```ts
const clock = new THREE.Clock();
const ticker = getTicker();

function animate() {
  requestAnimationFrame(animate);
  ticker.update(clock.getDelta() * 1000);
  renderer.render(scene, camera);
}
animate();
```

---

## Benchmarks

vs GSAP (vitest bench, Apple Silicon M-series, Node 24). Matched easing functions.

| Benchmark                           | anim-engine  | GSAP         | Ratio |
| ----------------------------------- | ------------ | ------------ | ----- |
| Single tween (cubic, 1000 frames)   | 40,974 ops/s | 10,656 ops/s | 3.8×  |
| Single tween (bezier, 1000 frames)  | 20,190 ops/s | 11,948 ops/s | 1.7×  |
| Keyframe (3 segments, 1000 frames)  | 26,661 ops/s | 3,578 ops/s  | 7.5×  |
| 50 concurrent tweens (500 frames)   | 866 ops/s    | 436 ops/s    | 2.0×  |
| 200 concurrent tweens (500 frames)  | 187 ops/s    | 112 ops/s    | 1.7×  |
| 1000 concurrent tweens (500 frames) | 37 ops/s     | 13 ops/s     | 2.8×  |
| 50-layer timeline (500 frames)      | 684 ops/s    | 406 ops/s    | 1.7×  |
| 50 tweens re-play (500 frames)      | 523 ops/s    | 133 ops/s    | 3.9×  |

vs GSAP `onUpdate` (apples-to-apples): **2.8–4.5× faster**.

Run locally: `npm run bench`

---

## License

MIT