npm package: @penner/responsive-easing
Description: Responsive Easing is a library that dynamically generates easing functions for motion design that intelligently responds to varying conditions.
Author: Robert Penner
Latest version: 0.1.0

# Responsive Easing

Mix and merge easing curves to create motion that feels exactly right.

[![npm version](https://img.shields.io/npm/v/@penner/responsive-easing)](https://www.npmjs.com/package/@penner/responsive-easing)

## Install

```bash
npm install @penner/responsive-easing
```

## Overview

Responsive Easing opens up new creative possibilities by splitting easing curves into two independent phases:

- **Head** — the first phase of the curve, easing away from the start
- **Tail** — the second phase, easing into the destination

The `fuse()` function seamlessly joins them at a configurable point, producing a single smooth easing function. Because each phase is independent, you can mix any head with any tail — anticipation into a bounce, a Bézier swoop into a spring, or anything in between — creating curves that would be impossible with a single easing function.

Use the **[Fuse Editor](https://robertpenner.com/fuse)** to visually design curves, tweak parameters in real time, and copy out CSS easing strings or config values for the JS API.

## Quick start

```ts
import { fuse } from '@penner/responsive-easing';

// Backwards anticipation into a bouncy landing
const { easing, easingFn } = fuse({
  head: { kind: 'back', overshoot: 0.2 }, // backwards by 20%
  tail: { kind: 'spring', bounces: 4, decay: 0.95 },
  joinTime: 0.7, // head is 70% of the curve, tail is 30%
});

// `easing` is a CSS linear() string - for CSS transitions, animations and web animations
element.animate(keyframes, { duration: 500, easing });

// `easingFn` is a mathematical function — for JS-driven animation or direct evaluation
const y = easingFn(0.5);
```

The `.easing` property is a CSS `linear()` string, so it also works directly in style assignments:

```ts
element.style.transition = `transform 600ms ${easing}`;
element.style.animationTimingFunction = easing;
```

## Easing definitions

Eight easing types, each with its own tunable parameters. Any head can be paired with any tail, giving you a wide palette of motion curves to design with.

| Kind         | Parameters                  | Character                                                |
| ------------ | --------------------------- | -------------------------------------------------------- |
| `power`      | `exponent`                  | Smooth acceleration (quadratic, cubic, etc.)             |
| `back`       | `overshoot`                 | Anticipation — dips backward before moving forward       |
| `power-back` | `exponent`, `overshoot`     | Combined power + back                                    |
| `bezier`     | `x1`, `y1`, `x2`, `y2`      | Cubic Bézier (same param space as CSS)                   |
| `expo`       | `decay`                     | Exponential decay (tail only)                            |
| `spring`     | `bounces`, `decay`          | Damped oscillation (tail only)                           |
| `bounce`     | `bounces`, `decay`          | Hard-surface rebound (tail only)                         |
| `swim`       | `strokes`, `effort`, `drag` | Rhythmic propulsion — pulsed force through viscous fluid |

## The `fuse()` API

`fuse()` takes a plain config object describing head and tail easings and returns an `EasingKit` — a bundle containing the composed easing function, its CSS `linear()` string, and a velocity function.

```ts
function fuse(config: FuseConfig): EasingKit;

interface EasingKit {
  easing: CSSEasing; // CSS linear() string for transitions, animations, WAAPI
  easingFn: EasingFn; // (t: number) => number, [0,1] → [0,1]
  velocityFn: VelocityFn; // speed of the easing at any point in time
  meta: Record<string, unknown>;
}
```

### FuseConfig

| Property   | Type                      | Default        | Description                                           |
| ---------- | ------------------------- | -------------- | ----------------------------------------------------- |
| `head`     | `AnyEasingDef`            | —              | Head easing definition (required)                     |
| `tail`     | `AnyEasingDef`            | —              | Tail easing definition (required)                     |
| `joinTime` | `number`                  | `0.5`          | Where head meets tail, in [0, 1]                      |
| `movement` | `'transition' \| 'pulse'` | `'transition'` | Transition goes A→B; pulse goes A→B→A                 |
| `mirror`   | `boolean`                 | `false`        | Tail = reversed head (symmetric inOut curve)          |
| `maxSpeed` | `number`                  | —              | Cap join velocity; inserts a cruise phase if exceeded |

## Examples

```ts
// Smooth power curve (equivalent to cubic ease-in-out when mirrored)
fuse({
  head: { kind: 'power', exponent: 3 },
  tail: { kind: 'power', exponent: 3 },
}).easingFn;

// Bézier head + bounce tail
fuse({
  head: { kind: 'bezier', x1: 0.4, y1: 1.6, x2: 0.8, y2: -0.4 },
  tail: { kind: 'bounce', bounces: 3, decay: 0.95 },
  joinTime: 0.3,
}).easingFn;

// Swim head + bounce tail
fuse({
  head: { kind: 'swim', strokes: 3.3, effort: 0.33, drag: 13 },
  tail: { kind: 'bounce', bounces: 1, decay: 0 },
}).easingFn;

// Symmetric curve via mirror
fuse({
  head: { kind: 'power-back', exponent: 2, overshoot: 0.1 },
  tail: { kind: 'power', exponent: 2 }, // ignored when mirror=true
  mirror: true,
}).easingFn;
```

## Responsive Easing Modules (REMs)

Under the hood, each easing definition resolves to an `EasingRem` (Responsive Easing Module) — an object that bundles the easing math with metadata for building UIs like sliders and parameter editors.

```ts
import { PowerBackRem, SwimRem, getEasingRem } from '@penner/responsive-easing';

// Factory creation
const rem = getEasingRem('power-back', { exponent: 3, overshoot: 0.15 });

// Immutable update
const updated = rem.with({ overshoot: 0.3 });

// Evaluate
const y = rem.easeIn(0.5);

// Static knob specs for UI generation
PowerBackRem.EXPONENT_KNOB; // { key: 'exponent', type: 'number', min: 0.1, max: 10, ... }
SwimRem.DRAG_KNOB; // { key: 'drag', type: 'number', min: 0.5, max: 30, ... }
```

Available REMs: `PowerRem`, `BackRem`, `PowerBackRem`, `BezierRem`, `ExpoRem`, `SpringRem`, `BounceRem`, `SwimRem`.

## Technical notes

- The fuse config is a plain serializable object — no class instances or functions, just data. Easy to store in a database, share via URL, or pass between workers.
- Types like `NormalizedTime`, `CSSEasing`, and `EasingFn` are powered by [`@penner/smart-primitive`](https://www.npmjs.com/package/@penner/smart-primitive), which catches unit mix-ups at compile time with zero runtime cost.
- Each easing definition resolves to an immutable `EasingRem` (Responsive Easing Module) that encapsulates the math, velocity computation, and UI knob metadata for a single easing type. REMs can be created, updated, and evaluated directly for advanced use cases.

## Mathematical notes

The join between head and tail maintains **C¹ continuity** — both position and velocity (first derivative) match at the join point. This is what makes the transition feel smooth rather than abrupt.

Internally, `fuse()` scales each phase (head or tail) to fit its portion of the [0, 1] time range. The head is stretched or compressed to fill the interval `[0, joinTime]`, and the tail fills `[joinTime, 1]`. Velocity at the join point is computed from the head's ending slope, and the tail's amplitude and time scale are adjusted so its starting slope matches — guaranteeing a seamless handoff regardless of which easing types are combined.

For easing types like `spring`, `bounce`, and `swim` where analytical derivatives aren't available, velocity is computed numerically.

## License

MIT © [Robert Penner](https://github.com/robertpenner)