@zakkster/lite-ease
v1.1.0
Published
30 Penner easing functions as pure (t)=>t. Tree-shakeable, zero-dependency, composable with any lerp.
Maintainers
Readme
@zakkster/lite-ease
🎢 What is lite-ease?
@zakkster/lite-ease is the complete set of Robert Penner easing equations as individual, tree-shakeable ES module exports — plus a small, zero-allocation composition layer.
It gives you:
- 🎢 30 easing functions (10 families × 3 variants)
- 🧮 Pure math:
(t: number) => numberwhere t ∈ [0, 1] - 🔗 Composable:
lerp(a, b, easeOutBounce(t)) - 🛡️
clamp01()wrapper for Back/Elastic overshoot - 🔄
reverse(ease)creates the inverse curve - 🎼 v1.1:
mirror,chain,blend,scale— zero-GC composition factories - 📖
easingslookup map for string-based access - 0️⃣ Zero dependencies, pure math, no allocation in hot path
- 🪶 < 1 KB minified (entire library)
Part of the @zakkster/lite-* ecosystem — micro-libraries built for deterministic, cache-friendly game development.
🚀 Install
npm i @zakkster/lite-ease🕹️ Quick Start
import { easeOutBounce, easeInOutElastic, clamp01, reverse } from '@zakkster/lite-ease';
import { lerp } from '@zakkster/lite-lerp';
// Compose with any interpolation
const y = lerp(startY, endY, easeOutBounce(t));
// Clamp overshoot (Back/Elastic can exceed 1.0)
const safe = lerp(0, 255, clamp01(easeOutBack(t)));
// Create inverse curve
const myEaseOut = reverse(easeInCubic);
// String-based lookup (useful for configs/JSON)
import { easings } from '@zakkster/lite-ease';
const ease = easings[config.easing]; // 'easeOutBounce' → function📊 Comparison
| Library | Size | Functions | Format | Install |
|---------|------|-----------|--------|---------|
| bezier-easing | ~3 KB | Custom curves | Class | npm i bezier-easing |
| eases | ~2 KB | 31 | CommonJS | npm i eases |
| lite-ease | < 1 KB | 31 + composition | ESM, tree-shakeable | npm i @zakkster/lite-ease |
⚙️ API
10 Families × 3 Variants
| Family | easeIn | easeOut | easeInOut |
|--------|--------|---------|-----------|
| Sine | easeInSine | easeOutSine | easeInOutSine |
| Quad | easeInQuad | easeOutQuad | easeInOutQuad |
| Cubic | easeInCubic | easeOutCubic | easeInOutCubic |
| Quart | easeInQuart | easeOutQuart | easeInOutQuart |
| Quint | easeInQuint | easeOutQuint | easeInOutQuint |
| Expo | easeInExpo | easeOutExpo | easeInOutExpo |
| Circ | easeInCirc | easeOutCirc | easeInOutCirc |
| Back | easeInBack | easeOutBack | easeInOutBack |
| Elastic | easeInElastic | easeOutElastic | easeInOutElastic |
| Bounce | easeInBounce | easeOutBounce | easeInOutBounce |
Helpers
linear(t)— Identity functionclamp01(t)— Clamp to [0, 1]. Back/Elastic overshoot safely.reverse(ease)— Create the inverse:reverse(easeIn) ≈ easeOuteasings—Record<string, EasingFunction>for dynamic lookup
🎼 Composition Helpers (v1.1)
Each helper is a factory: it pre-computes any constants and returns a closure that does zero allocation per call. The returned function is what you call in your hot path.
⚠️ Build once, call many. Don't put
mirror(...),chain(...),blend(...), orscale(...)inside yourrequestAnimationFrameloop — that allocates a fresh closure every frame and defeats the design. Build the composed easing in init/module scope, then call the returned function each frame.
mirror(ease, clamp?)
Plays the curve forward in the first half, then backward — a seamless ping-pong (0 → 1 → 0).
import { mirror, easeInOutQuad } from '@zakkster/lite-ease';
const pingPong = mirror(easeInOutQuad);
// pingPong(0) === 0
// pingPong(0.5) ≈ 1
// pingPong(1) === 0clamp: true clamps each half to [0, 1] — useful when wrapping easeOutBack or easeOutElastic to suppress overshoot.
chain(easeA, easeB, clamp?)
Sequential chain: easeA drives t ∈ [0, 0.5], easeB drives t ∈ [0.5, 1].
import { chain, easeInQuad, easeOutBounce } from '@zakkster/lite-ease';
const windAndDrop = chain(easeInQuad, easeOutBounce);
// First half: smooth wind-up. Second half: bounce settle.clamp: true is recommended when either curve overshoots — clamps each half to its [0, 0.5] / [0.5, 1] range so overshoot can't bleed across the midpoint.
blend(easeA, easeB, weight)
Linear crossfade between two curves. weight is clamped to [0, 1] once at init and stored — no per-call clamp cost.
import { blend, linear, easeOutElastic } from '@zakkster/lite-ease';
const subtleElastic = blend(linear, easeOutElastic, 0.3);
// 70% linear + 30% elastic — keeps the bounce flavor without the full overshoot.scale(ease, fromT, toT, clamp?)
Remaps an easing to a sub-segment of the timeline. Outside the segment, output is locked to the boundary value (pre-computed at init). Throws if toT <= fromT.
import { scale, easeOutBounce } from '@zakkster/lite-ease';
const dropAt40Percent = scale(easeOutBounce, 0.4, 0.9);
// dropAt40Percent(t) → 0 for t <= 0.4
// dropAt40Percent(t) → 1 for t >= 0.9
// drops/bounces between 0.4 and 0.9 of the master timelineIdeal for staggered animation sequences where multiple elements share a master t and each starts/ends at a different point.
🧪 Benchmark
31 easing functions, 1M calls each:
All functions: < 0.01ms per call (pure arithmetic)
No allocation, no branching (except Bounce/Elastic)
Tree-shakeable: unused functions are eliminated by bundlers
Composition helpers (mirror/chain/blend/scale):
Factory cost paid once at init.
Returned closure: zero allocation per call, one inner ease() invocation.📦 TypeScript
Full declarations included in LiteEase.d.ts. The EasingFunction type alias is exported for your own composed easings.
📚 LLM-Friendly Documentation
See llms.txt for AI-optimized metadata and usage examples.
License
MIT
