@penner/responsive-easing
v0.1.0
Published
Responsive Easing is a library that dynamically generates easing functions for motion design that intelligently responds to varying conditions.
Downloads
27
Maintainers
Readme
Responsive Easing
Mix and merge easing curves to create motion that feels exactly right.
Install
npm install @penner/responsive-easingOverview
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 to visually design curves, tweak parameters in real time, and copy out CSS easing strings or config values for the JS API.
Quick start
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:
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.
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
// 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.
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, andEasingFnare powered by@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
