@penner/responsive-easing
v0.2.1
Published
Responsive Easing is a library that dynamically generates easing functions for motion design that intelligently responds to varying conditions.
Downloads
397
Maintainers
Readme
@penner/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
The library supports a broad set of easing kinds. Most can be used as either head or tail, and you can freely mix head/tail pairs to design custom motion.
| Kind | Parameters | Character |
| ------------- | --------------------------- | -------------------------------------------------------- |
| power | exponent | Smooth acceleration (quadratic, cubic, etc.) |
| back | overshoot | Anticipation — dips backward before moving forward |
| flex | exponent, overshoot | Combined power + back |
| bezier | x1, y1, x2, y2 | Cubic Bézier (same param space as CSS) |
| exponential | decay | Exponential decay (tail only) |
| spring | bounces, decay | Damped oscillation (tail only) |
| innerSpring | bounces, decay | Spring tuned for inOut-style internal joining |
| bounce | bounces, decay | Hard-surface rebound (tail only) |
| swim | strokes, effort, drag | Rhythmic propulsion — pulsed force through viscous fluid |
| sine | — | Sinusoidal ease profile |
| circular | — | Circular arc-style ease profile |
| smoothstep | — | Polynomial smoothstep profile |
Also available for specialized workflows:
cubic-hermite(follower-only)cruise(constant-velocity segment)hold(constant-position segment)
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 | FuseEasingDef | — | Head easing definition (required) |
| tail | FuseEasingDef | — | 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 },
});
// 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,
});
// Swim head + bounce tail
fuse({
head: { kind: "swim", strokes: 3.3, effort: 0.33, drag: 13 },
tail: { kind: "bounce", bounces: 1, decay: 0 },
});
// Symmetric curve via mirror
fuse({
head: { kind: "flex", exponent: 2, overshoot: 0.1 },
tail: { kind: "power", exponent: 2 }, // ignored when mirror=true
mirror: true,
});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 { FlexRem, SwimRem, getEasingRem } from "@penner/responsive-easing";
// Factory creation
const rem = getEasingRem("flex", { 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
FlexRem.EXPONENT_KNOB; // { key: 'exponent', type: 'number', min: 0.1, max: 10, ... }
SwimRem.DRAG_KNOB; // { key: 'drag', type: 'number', min: 0.5, max: 30, ... }Available REM classes: PowerRem, BackRem, FlexRem, BezierRem, ExponentialRem, SpringRem, BounceRem, SwimRem.
Additional easing kinds such as innerSpring, sine, circular, and smoothstep are available via getEasingRem(kind, params) and fuse() config kinds.
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.
Advanced/Experimental
The package also exports lower-level modules for specialized use cases:
- Interrupt easing:
createInterruptEasing,createSpringInterruptEasing,createBackInterruptEasing— generate easing functions for interrupted/redirected animations - Interruptible motion (2D):
InterruptibleMotion,computeInterrupt,linearDuration,positionAtTime— manage 2D animations that can be interrupted and smoothly redirected mid-flight - Interruptible scalar (1D):
InterruptibleScalar,computeScalarInterrupt,scalarAtTime— same concept for single-axis motion - Bezier constraints:
clampBezierY2ForTail,clampBezierY1ForHead,calculateTailP1FromHeadP2,calculateHeadP2FromTailP1— enforce continuity constraints on Bézier control points - Flex math:
getLocalMinimumPosition,findExponentForMinimumPosition,solveFlexStrength,evalFlexIn— internal math for flex easing curves - Parameter domains:
paramDomains,isInDomain,clampToDomain— math truth (inherent inequality bounds + canonical default) for each REM parameter. UI knob specs (slider ranges, labels, percent vs number) live in@penner/fuse-editor-coreaseditorKnobSpecs.
Related Packages
@penner/easing— the underlying easing functions used by this package@penner/classic-easing— the original Penner equations with classic naming conventions
License
MIT © Robert Penner
