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-lut

v1.0.1

Published

High-performance, zero-GC lookup table generator for easing functions. Eliminates Math.sin and Math.pow overhead in hot paths.

Readme

@zakkster/lite-ease-lut

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

📐 What is lite-ease-lut?

@zakkster/lite-ease-lut bakes any easing function (t: number) => number into a pre-computed lookup table backed by a Float32Array. The result is a drop-in replacement closure that returns the same shape of curve, but with deterministic, allocation-free cost on the hot path.

It gives you:

  • 📐 Drop-in replacement: bakeLUT(easeOutElastic) returns (t) => number
  • ⚡ Deterministic ~15 ns/call on V8, regardless of curve complexity
  • 🧊 Zero allocations on hot path — no GC pressure during animation
  • 🏗️ bakeAll() packs an entire animation system into one contiguous buffer for cache locality
  • 🔬 Smart segment-count heuristics by family (Bounce: 128, Elastic: 256, Sine/Circ/Expo/Back: 64)
  • 🛡️ Minifier-safe via the hint parameter — survives aggressive name mangling
  • ♻️ dispose() zeros owned memory for clean teardown in long-running apps
  • 🪶 < 1 KB minified, zero dependencies

Part of the @zakkster/lite-* ecosystem — micro-libraries built for deterministic, cache-friendly game development.

🤔 Why a LUT?

Not all easings are created equal. easeInQuad is one multiplication. easeInElastic is Math.pow + Math.sin. In a frame budget, that variance creates jank when you switch curves.

A baked LUT replaces the curve evaluation with a Float32Array index + a linear interpolation. The cost is:

  • The same ~15 ns regardless of which family you pick. No more "elastic is the slow one."
  • No Math.sin / Math.pow / branching in animation hot paths — important for older mobile WebViews and constrained runtimes (Twitch Extensions, embedded <canvas>).
  • Cache-friendly when batched. With bakeAll(), every active easing lives in the same Float32Array, so all your curve evaluations hit the same hot cache lines.

This isn't a universal speedup. If your only easing is easeInQuad, raw math wins (it's already 4 ns). LUTs win when you have a mix of cheap and expensive curves and want the cost to be predictable.

🚀 Install

npm i @zakkster/lite-ease-lut

Pairs naturally with @zakkster/lite-ease:

npm i @zakkster/lite-ease @zakkster/lite-ease-lut

🕹️ Quick Start

Single curve

import { bakeLUT } from '@zakkster/lite-ease-lut';
import { easeOutElastic } from '@zakkster/lite-ease';

// Bake once, anywhere outside the hot path
const ease = bakeLUT(easeOutElastic);

// Drop-in replacement: same signature, faster + deterministic cost
function animate(t) {
    return ease(t); // (t: number) => number, just like easeOutElastic
}

Batched (recommended for production)

import { bakeAll } from '@zakkster/lite-ease-lut';
import * as eases from '@zakkster/lite-ease';

// One contiguous Float32Array for all your curves
const { easings, shared, dispose } = bakeAll({
    inOutSine:    eases.easeInOutSine,
    outBounce:    eases.easeOutBounce,
    inOutElastic: eases.easeInOutElastic,
});

// Hot path: zero allocations, predictable cost, cache-friendly
const y = lerp(start, end, easings.outBounce(t));

// On teardown (long-running apps, page unload, etc.)
dispose();

Minified bundle (production)

When your bundler renames easeOutBounce to e or o2, segment heuristics need a hint:

import { bakeAll } from '@zakkster/lite-ease-lut';
import * as e from '@zakkster/lite-ease'; // renamed import

const { easings } = bakeAll(
    { primary: e.easeOutBounce, secondary: e.easeInElastic },
    { hints: { primary: 'bounce', secondary: 'elastic' } }
);

🏗️ The bakeAll pattern

bakeAll is the difference between "I have ten LUTs scattered in heap" and "I have one cache line with everything I'll ever touch."

Without bakeAll:                With bakeAll:
┌────────┐                      ┌─────────────────────────┐
│ LUT A  │ ← heap allocation    │ shared Float32Array     │
└────────┘                      │ ┌──┐┌──────┐┌────┐┌───┐ │
┌──────────────────┐            │ │ A││  B   ││ C  ││ D │ │
│      LUT B       │ ← another  │ └──┘└──────┘└────┘└───┘ │
└──────────────────┘            └─────────────────────────┘
┌──────┐                          ↑ contiguous, cache-friendly
│LUT C │ ← another
└──────┘

You can also bring your own buffer (e.g. a slice of a WebAssembly.Memory or a pool you manage yourself):

import { bakeAll } from '@zakkster/lite-ease-lut';

const myPool = new Float32Array(8192);

const { easings } = bakeAll(easingsMap, {
    shared: myPool,
    offset: 1024,           // Write starting at index 1024
    segments: 64,           // Force every curve to 64 segments
});

⚙️ API

bakeLUT(easeFn, segments?, hint?)

Primary single-curve API. Returns a closure that's a drop-in replacement for easeFn.

| Param | Type | Description | |---|---|---| | easeFn | (t: number) => number | Pure easing function | | segments | number? | Override the recommended count | | hint | string \| { family: string }? | Minifier-safe family hint |

Returns a BakedEasing — a callable (t) => number with these introspection properties:

| Property | Type | Description | |---|---|---| | lut | Float32Array \| null | The underlying LUT (null when linear is short-circuited) | | segments | number | Segment count | | offset | number | Start index in the LUT (always 0 for bakeLUT) |

Linear short-circuit: When the function name or hint contains 'linear' and segments === 2, no LUT is allocated and the closure becomes the identity function.


bakeAll(easingsMap, options?)

Batch processor. Bakes a dictionary of easings into one contiguous Float32Array.

| Option | Type | Description | |---|---|---| | segments | number? | Force every curve to this segment count | | shared | Float32Array? | Bring your own buffer | | offset | number? | Starting index when using a custom buffer (default 0) | | hints | Record<string, string \| { family: string }>? | Per-key family hints for minifier safety |

Returns:

| Property | Type | Description | |---|---|---| | easings | Record<string, BakedEasing> | Drop-in closures, keyed by your map keys | | shared | Float32Array | The contiguous LUT data | | offsets | Int32Array | Start index of each curve in shared | | segments | Int32Array | Segment count of each curve | | dispose | () => void | Zero this batch's slice + null out closure .lut references |


recommendedSegments(ease, hint?)

Returns the heuristic segment count for a function or family name.

| Family | Default | Why | |---|---|---| | linear | 2 | Identity — only endpoints needed | | sine / circ / expo / back | 64 | Smooth curves — moderate sampling | | bounce | 128 | Multi-cusp piecewise — needs density | | elastic | 256 | High-frequency oscillation — needs the most | | (unrecognized) | 32 | Safe default |

Throws when the function name appears minified (name.length <= 2) and no hint is provided. This is intentional — it prevents you from accidentally shipping a bundle where everything falls back to 32 segments.


evalLUT(lut, offset, segments, t)

The raw evaluator used internally by baked closures. Exposed for advanced use:

  • Building your own pooling layer
  • Sharing a LUT between multiple call sites
  • Embedding LUT data in a binary asset
import { fillLUT, evalLUT } from '@zakkster/lite-ease-lut';

const buffer = new Float32Array(33);
fillLUT(easeOutBounce, buffer, 0, 32);

// In your hot path — completely allocation-free:
const y = evalLUT(buffer, 0, 32, t);

Handles t <= 0, t >= 1, and NaN without branching to allocation.


fillLUT(easeFn, outFloat32, offset, segments)

Caller-owned memory variant of the bake step. Writes segments + 1 values into outFloat32 starting at offset. Returns the count written. Throws if the buffer is too small or segments < 1.

The last vertex is always easeFn(1) exactly — no float drift from i * (1/segments) accumulating to slightly-less-than-1.

🧪 Benchmark

5,000,000 calls each on Node 22, V8 13.x, x64 Linux, t cycling through 1000 values:

| Easing | Raw math | LUT | Verdict | |---|---:|---:|---| | easeOutBounce (128 seg) | 3.75 ns/call | 13.83 ns/call | Raw wins — bounce is just multiplications | | easeInOutSine (64 seg) | 18.75 ns/call | 15.14 ns/call | LUT modest win — Math.cos is the cost | | easeInElastic (256 seg) | 72.16 ns/call | 14.84 ns/call | LUT 5× faster — Math.pow + Math.sin are expensive |

Read this honestly: if your animations only use polynomial easings, you don't need this library. If you use Elastic, Sine, Expo, Circ, or any custom curve with trigonometry, every active easing now costs the same predictable ~15 ns — and that's the win.

Numbers will vary by engine. Mobile WebViews and older JavaScriptCore versions tend to show larger LUT advantages because Math.sin/Math.pow are slower there.

🛡️ Minifier safety

If you minify with Terser/esbuild/SWC, function names get mangled. Without help, recommendedSegments(myMinifiedFn) would throw, and named imports like _a.easeOutBounce could become _a.b — losing the family signal.

Two escape hatches:

// Per-call hint on bakeLUT
bakeLUT(easeFn, undefined, 'elastic');
bakeLUT(easeFn, undefined, { family: 'bounce' });

// Per-key hints on bakeAll
bakeAll(map, { hints: { foo: 'sine', bar: 'expo' } });

// Or just override segments entirely (skips heuristics)
bakeAll(map, { segments: 64 });

Hints are matched as case-insensitive substrings against the family name (e.g. 'CustomBounceV2' matches 'bounce').

📦 TypeScript

Full declarations included in EaseLut.d.ts:

import type { BakedEasing, BakeAllResult, EasingHint } from '@zakkster/lite-ease-lut';

const ease: BakedEasing = bakeLUT(easeOutBounce);
ease.lut;       // Float32Array | null
ease.segments;  // number
ease.offset;    // number
ease(0.5);      // number — still callable

🤝 Pairs well with

License

MIT