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

@zakkster/lite-ease-params

v1.0.0

Published

Zero-GC tunable parametric easing factories (Back, Elastic, Bounce). Uses codegen and trigonometry hoisting for allocation-free hot paths.

Readme

@zakkster/lite-ease-params

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

🎚️ What is lite-ease-params?

@zakkster/lite-ease-params is the tunable parametric easing layer for the Lite ecosystem. Where @zakkster/lite-ease ships fixed Penner curves as (t) => t functions, this package ships factories that let you tune the character of Back, Elastic, and Bounce curves: how deep the overshoot dips, how fast the elastic oscillates, how bouncy the bounce settles.

It gives you:

  • 🎯 9 factories — Back, Elastic, Bounce × In/Out/InOut
  • 🎛️ Real parametersovershoot, amplitude, period, bounciness
  • 🧮 Codegen pattern — one allocation at factory time, zero allocations in the hot path
  • Trig hoistingasin, divisions, and breakpoints all computed once at closure init
  • 📐 Bit-exact Penner defaultcreateBounceOut() returns a cached closure that reproduces the canonical Penner curve identically
  • 0️⃣ Zero dependencies, pure ES module, sideEffects: false
  • 🪶 < 2 KB minified (entire library; tree-shakeable per factory)

📐 Why a separate package?

Most easing libraries ship the shape of a curve but not the knob on it. Penner's easeOutBack overshoot is hard-coded at 1.70158. easeOutBounce always uses 7.5625 / 2.75. If you want a snappier overshoot or a slower-settling bounce, you're either rewriting Penner's math or pulling in a 30 KB tween engine.

The naive way to add parameters is to take them on every call:

// ❌ Recomputes asin, divisions, and breakpoints on EVERY tick.
function easeOutElastic(t, amplitude, period) {
    const s = Math.asin(1 / amplitude) * (period / (2 * Math.PI));
    const freq = (2 * Math.PI) / period;
    // ... per-call math
}

lite-ease-params uses the codegen pattern: each factory captures the parameter-derived invariants once, then returns a tight (t) => y closure whose body is only the math the curve fundamentally requires.

// ✅ asin, divisions, breakpoints computed ONCE, when factory is called.
const easeOutElastic = createElasticOut(1.5, 0.4);

// Per-frame: just `pow * sin + 1`. No allocations. No recomputation.
const y = easeOutElastic(t);

For an animation engine running 60–120 fps with hundreds of tweens, this is the difference between thrashing the GC and steady-state silence.

📊 Comparison

| Library | Tunable? | Allocations per call | Default-Penner exact? | Bundle | Approach | |---|---|---|---|---|---| | gsap (CustomEase) | ✅ via SVG path | Some | N/A (custom) | ~30 KB | full tween engine | | popmotion (createGenerator) | ✅ | Closure per call | N/A | ~7 KB | dynamic generator | | bezier-easing | Cubic-bezier only | 0 (after init) | N/A (CSS bezier) | ~1 KB | Newton solver | | @tweenjs/tween.js | ❌ (fixed Penner) | 0 | ✅ | ~9 KB | bundled with engine | | d3-ease | ❌ (fixed) | 0 | ✅ | ~2 KB | static Penner | | eases | ❌ (fixed) | 0 | ✅ | ~2 KB | static Penner | | lite-ease-params | ✅ Back / Elastic / Bounce | 0 in hot path | ✅ bit-exact at default bounciness | < 2 KB | codegen factories |

🚀 Quick start

npm install @zakkster/lite-ease-params
import {
    createBackOut, createElasticOut, createBounceOut,
} from '@zakkster/lite-ease-params';

// 1. Build curves ONCE at app/component init.
const snap   = createBackOut(2.5);          // strong overshoot
const spring = createElasticOut(1.2, 0.4);  // gentle, slow oscillation
const drop   = createBounceOut(0.4);        // bouncier than default

// 2. Use them in your render loop. Zero allocations per call.
function tick(t) {
    el.style.transform = `translateY(${(1 - drop(t)) * 200}px)`;
}

That's it. The factory returns a plain (t: number) => number you can hand to any tweening engine, animation library, or custom RAF loop. It composes with lerp, slerp, Float32Array interpolation, and anything else that takes a normalized t.

🏗️ The codegen pattern

Each factory is split into two phases. The init phase runs once when you call the factory; everything that depends only on the parameters is computed and captured. The hot phase runs on every tick and contains only the t-dependent math.

flowchart LR
    A[createElasticOut<br/>1.5, 0.4]:::factory --> B[Init phase]:::init
    B --> B1[clamp amplitude ≥ 1]:::initStep
    B --> B2[asin 1/amplitude]:::initStep
    B --> B3[derive phase shift s]:::initStep
    B --> B4[derive freq = 2π/period]:::initStep
    B1 & B2 & B3 & B4 --> C[Closure capture<br/><sub>4 captured scalars</sub>]:::capture
    C --> D[returned fn t]:::ret

    D --> E[Hot phase per call]:::hot
    E --> E1[pow 2, -10t]:::hotStep
    E --> E2[sin t-s · freq]:::hotStep
    E --> E3[multiply + add]:::hotStep

    classDef factory fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    classDef init fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef initStep fill:#f0fdf4,stroke:#86efac,color:#15803d
    classDef capture fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
    classDef ret fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef hot fill:#fecaca,stroke:#dc2626,color:#7f1d1d
    classDef hotStep fill:#fef2f2,stroke:#fca5a5,color:#991b1b

Allocation contract:

  • createElasticOut(...) — 1 closure + 4 captured scalars (~64 B)
  • elasticOut(t) — 0 allocations, 0 hidden-class transitions, 0 Math.asin calls

The createBounceOut factory has an additional optimization: when called with the default bounciness = 0.25, it returns a module-level cached closure that uses Penner's bit-exact constants directly. So createBounceOut() === createBounceOut() is true, and the cached path skips even the one Math.sqrt the parametric path needs.

📚 API reference

createBackIn(overshoot?: number) => (t) => y

createBackOut(overshoot?: number) => (t) => y

createBackInOut(overshoot?: number) => (t) => y

overshoot defaults to 1.70158 (Penner's canonical constant). Higher values pull the curve further past 0 (in/inout) or 1 (out/inout) before settling. The InOut variant internally multiplies by 1.525 per Penner's spec.

| t | BackIn(t) (overshoot=1.70158) | BackOut(t) | |---|---|---| | 0 | 0 | 0 | | 0.25 | -0.046 | 0.706 | | 0.5 | -0.088 | 1.088 | | 0.75 | 0.295 | 1.046 | | 1 | 1 | 1 |

createElasticIn(amplitude?, period?) => (t) => y

createElasticOut(amplitude?, period?) => (t) => y

createElasticInOut(amplitude?, period?) => (t) => y

  • amplitude defaults to 1. Internally clamped to >= 1 because the formula uses asin(1 / amplitude) for the phase shift.
  • period defaults to 0.3. Smaller = faster oscillation. Non-positive values fall back to 0.3.

The factory hoists asin, freq = 2π / period, and the phase shift s into closure-captured scalars. Hot path is one pow(2, …) and one sin(…) per call.

createBounceIn(bounciness?) => (t) => y

createBounceOut(bounciness?) => (t) => y

createBounceInOut(bounciness?) => (t) => y

bounciness is the fraction of vertical energy retained per bounce, on (0, 1). Defaults to 0.25 — Penner's canonical curve. Clamped internally to [0.01, 0.99].

| bounciness | Visual character | Notes | |---|---|---| | 0.05 | One long drop, almost no bounces | Heavy ball into mud | | 0.25 (default) | Penner canonical — 3 bounces, decreasing peaks | Bit-exact cached closure | | 0.4 | Slightly bouncier, taller secondary | Tennis ball | | 0.7 | Many small fast bounces | Steel ball on glass | | 0.95 | Very bouncy, slow energy decay | Super-ball |

Geometric guarantees (parametric path):

  • f(0) === 0 and f(1) === 1 (explicit boundary clamps).
  • All segment peaks resolve to exactly 1.
  • All four segments are continuous at boundaries.
  • Valleys hit 1 - b, 1 - b², 1 - b³ at segment centers.

🍳 Recipes

Modal pop-in with snappy overshoot

Strong Back overshoot for that "punched in" entrance feel.

import { createBackOut } from '@zakkster/lite-ease-params';

const punch = createBackOut(2.8);  // tune at component init

modal.animate(t => {
    const s = punch(t);
    modalEl.style.transform = `scale(${0.7 + 0.3 * s})`;
    modalEl.style.opacity   = Math.min(1, t * 2);
}, 320);

Spring-snap drawer

A long-period elastic curve so the drawer feels like a real spring instead of a quick bounce.

import { createElasticOut } from '@zakkster/lite-ease-params';

const spring = createElasticOut(1.1, 0.55);  // amp 1.1, slow period

drawer.slideIn(t => {
    drawerEl.style.transform = `translateX(${(spring(t) - 1) * -320}px)`;
});

Ball drop with tunable bounce

Lower bounciness for a heavy ball; higher for a super-ball. The default 0.25 is the bit-exact Penner curve.

import { createBounceOut } from '@zakkster/lite-ease-params';

const heavyBall = createBounceOut(0.15);  // 1 big drop, tiny tail
const superBall = createBounceOut(0.85);  // many quick bounces
const penner    = createBounceOut();      // === createBounceOut() — cached

ball.animate(t => {
    const y = (1 - superBall(t)) * floorHeight;
    ballEl.style.transform = `translateY(${y}px)`;
});

ECS pattern — one factory, many entities

Build the curve once, share it across thousands of entities. Each entity stores only a t; the curve is a flyweight.

import { createBackOut, createElasticOut } from '@zakkster/lite-ease-params';

const profile = {
    enter:  createBackOut(2.0),
    spring: createElasticOut(1.4, 0.35),
};

// Per-entity state: just t and a profile reference.
const entities = new Float32Array(N * 2);  // [t0, t1, t2, ..., done0, done1, ...]

function tick(dt) {
    for (let i = 0; i < N; i++) {
        const t = entities[i] += dt / 0.4;
        if (t >= 1) continue;
        const y = profile.enter(t);  // zero alloc, hot path
        // ... write y into your transform/SoA buffer
    }
}

Compose with @zakkster/lite-ease

Use the parametric factories alongside the static Penner curves in the rest of your project — they share the same (t) => y shape.

import { easeInQuad, easeOutCubic } from '@zakkster/lite-ease';
import { createBackOut, createBounceOut } from '@zakkster/lite-ease-params';

const intro   = easeOutCubic;              // static
const accent  = createBackOut(2.4);        // tuned
const exit    = createBounceOut(0.6);      // tuned bounce
const settle  = easeInQuad;                // static

// Use them anywhere a (t)=>y is expected.
timeline.add({ duration: 300, ease: intro,  ... });
timeline.add({ duration: 240, ease: accent, ... });

Bake to a LUT for ultimate hot-path purity

Pair with @zakkster/lite-ease-lut to convert any factory into a pre-sampled Float32Array. This is useful for particle systems where you want deterministic frame timing:

import { createElasticOut } from '@zakkster/lite-ease-params';
import { bakeLUT, sampleLUT } from '@zakkster/lite-ease-lut';

const spring = createElasticOut(1.3, 0.4);
const LUT = bakeLUT(spring, 256);  // bake once at init

// Hot path: one array access, no Math.pow, no Math.sin.
const y = sampleLUT(LUT, t);

⚡ Performance characteristics

| Factory | Init cost | Hot-path math | Hot-path allocations | |---|---|---|---| | createBackIn / Out | 1 add | 4 multiplies, 1 add | 0 | | createBackInOut | 2 multiplies, 1 add | 5–6 mults, 1–2 adds, 1 branch | 0 | | createElasticIn / Out | 1 asin, 2 divs | pow + sin + 2 mults + 1 add | 0 | | createElasticInOut | 1 asin, 2 divs | pow + sin + 4 mults + 2 adds + 1 branch | 0 | | createBounceOut(0.25) | 0 (returns cached closure) | 1–2 mults + 1 add per branch | 0 | | createBounceOut(other) | 1 sqrt, 4 mults, 5 adds | 1–3 mults + 1 add + 1 branch | 0 | | createBounceIn / InOut | 1 inner factory call (allocates 1 closure) | inner + 1 sub | 0 |

Reference equality: createBounceOut() === createBounceOut() === createBounceOut(0.25). The default Bounce is a module-level singleton.

🌳 Ecosystem

lite-ease-params is a sibling to the rest of the Lite easing layer:

flowchart TD
    LEP[lite-ease-params<br/><sub>tunable Back / Elastic / Bounce</sub>]:::tunable
    LE[lite-ease<br/><sub>30 fixed Penner curves</sub>]:::fixed
    LCB[lite-cubic-bezier<br/><sub>CSS-compatible bezier runtime</sub>]:::bezier
    LL[lite-ease-lut<br/><sub>bake any (t)=>y to a LUT</sub>]:::lut
    LK[lite-keyframe<br/><sub>multi-segment timelines</sub>]:::timeline

    LEP --> LL
    LE --> LL
    LCB --> LL
    LL --> LK
    LE -.-> LK
    LEP -.-> LK
    LCB -.-> LK

    classDef tunable fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef fixed fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bezier fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef lut fill:#fecaca,stroke:#dc2626,color:#7f1d1d
    classDef timeline fill:#ede9fe,stroke:#7c3aed,color:#4c1d95

Every package speaks the same (t: number) => number interface. Pick the layer you need; they all compose.

🧪 Tests

npm test

The suite covers 79 cases across endpoint guarantees, math correctness for all three families, time-mirror identities (In(t) ≡ 1 - Out(1 - t)), point-symmetry of InOut variants, the bit-exact Penner match for the default Bounce path, regression coverage for the parametric Bounce across 9 bounciness values (continuity, peak heights, in-range output), asin-NaN clamping, period fallback, and reference-equality of the cached default closure.

🟦 TypeScript

import {
    createBackOut,
    createElasticOut,
    createBounceOut,
    type EasingFunction,
} from '@zakkster/lite-ease-params';

const spring: EasingFunction = createElasticOut(1.5, 0.4);
const y: number = spring(0.7);

Full TSDoc on every export, including parameter ranges, clamping behavior, and endpoint guarantees.

📜 License

MIT © Zahary Shinikchiev