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-cubic-bezier

v1.0.0

Published

High-performance, zero-GC CSS cubic-bezier runtime. Features a DoD-friendly coefficient compiler and Newton-Raphson solver with bisection fallback.

Readme

@zakkster/lite-cubic-bezier

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

📐 What is lite-cubic-bezier?

@zakkster/lite-cubic-bezier is the CSS-compatible cubic-bezier runtime for the Lite ecosystem. You hand it the four control numbers from cubic-bezier(x1, y1, x2, y2) and you get back a stable (t) => number ready to drive any animation.

The library exposes two APIs that share the same math kernel:

  • 🟢 Closure APIcubicBezier(x1, y1, x2, y2) returns a (t) => number. Drop-in for anything that takes an easing function. The 90% case.
  • 🔵 DoD APIcompileBezier() produces a flat coefficient struct, evalBezier(t, c) is a stateless evaluator. For tight inner loops, ECS systems, and anywhere you want to control allocations at the call site.

Both paths run a Newton-Raphson solver with bisection fallback — the same algorithm shape browsers use for cubic-bezier() in CSS animations. Zero allocations on the hot path. Zero dependencies. Sub-1KB min+gzip.

🧬 Where it fits

lite-cubic-bezier is the parametric curve layer of the zero-GC animation pipeline — the bridge between a CSS-style declarative curve specification and a callable easing function:

flowchart LR
    A[Designer / CSS<br/><sub>cubic-bezier x1,y1,x2,y2</sub>]:::input --> B[lite-cubic-bezier<br/><sub>compile + eval</sub>]:::core
    A2[lite-ease<br/><sub>Penner functions</sub>]:::sibling --> C[t => number]:::out
    B --> C
    C --> D[lite-ease-lut<br/><sub>bake to LUT</sub>]:::lut
    C --> E[lite-keyframe<br/><sub>compose segments</sub>]:::keyf
    C --> F[lite-tween<br/><sub>drive over time</sub>]:::tween
    E --> F
    F --> G[your render loop]:::render

    classDef input fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef sibling fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    classDef core fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef out fill:#f0fdf4,stroke:#16a34a,color:#14532d
    classDef lut fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef keyf fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef tween fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
    classDef render fill:#f3f4f6,stroke:#6b7280,color:#1f2937

Every library in the chain is independent. You can use lite-cubic-bezier standalone with raw (t) => number callbacks — no framework lock-in.

🚀 Install

npm install @zakkster/lite-cubic-bezier

🕹️ Quick Start

Closure API — the common case

import { cubicBezier } from '@zakkster/lite-cubic-bezier';

// CSS "ease" — cubic-bezier(0.25, 0.1, 0.25, 1.0)
const ease = cubicBezier(0.25, 0.1, 0.25, 1.0);

ease(0.0);  // → 0
ease(0.5);  // → 0.802
ease(1.0);  // → 1

// Drop it into anything that takes an easing function
mgr.to(player, 'x', 0, 200, 5.0, { ease });

// Linear curves return a shared cached identity — zero closure churn
const linearA = cubicBezier(0, 0, 1, 1);
const linearB = cubicBezier(0.5, 0.5, 0.5, 0.5);
linearA === linearB;  // → true

DoD API — manual struct control

import { compileBezier, evalBezier } from '@zakkster/lite-cubic-bezier';

// Pre-compile once at setup
const c = compileBezier(0.42, 0, 0.58, 1.0);   // CSS "ease-in-out"

// Re-use the struct across millions of evaluations — zero allocations
function tick(progress) {
    const eased = evalBezier(progress, c);
    // ... write to your typed-array buffer, particle system, whatever
}

// Or store many curves in an array of structs (perfectly stable hidden class)
const curves = [
    compileBezier(0.25, 0.1, 0.25, 1.0),
    compileBezier(0.42, 0,   0.58, 1.0),
    compileBezier(0.5, -0.5, 0.5,  1.5),  // back-out with overshoot
];
const value = evalBezier(t, curves[curveIdx]);

🧠 Why this exists

CSS specifies easings declaratively: cubic-bezier(0.25, 0.1, 0.25, 1.0) means "use this exact parametric curve." But evaluating it at runtime is not free — the function doesn't directly give you y(t). The curve is parameterized by an internal variable u, so you have to:

  1. Solve x(u) = t for u (a cubic root-finding problem)
  2. Then compute y(u) using that u

Most implementations close over the four input numbers and allocate fresh objects per call, or compute coefficients on every evaluation. At 60fps with hundreds of timelines, that's a measurable hit on the allocator and the GC.

lite-cubic-bezier separates the two halves of the work:

  1. Setup-time (once per curve): expand the four control numbers into eight cached polynomial coefficients (ax, bx, cx, ay, by, cy plus precomputed derivative coefficients ax3, bx2). One small struct, allocated once.
  2. Hot path (per frame, per element): pure scalar math against that struct. Newton-Raphson converges in 2–3 iterations on every CSS-spec curve. Zero allocations, zero closures created, zero indirection.

The DoD API exposes the struct directly so you can store curves in a typed-array-adjacent layout (one struct per ECS component, one per particle, one per timeline segment) and pass them to evalBezier() without any wrapping function.

🔥 Algorithm

The evaluator runs a two-stage solver: Newton-Raphson first (fast, quadratic convergence in the typical case) with a bisection fallback for the small set of pathological curves where Newton can stall. Both stages share the same compiled coefficients.

flowchart TD
    A[evalBezier t, c]:::start --> B{t ≤ 0<br/>or t ≥ 1?}
    B -->|yes| C[return 0 / 1]:::trivial
    B -->|no| D[Newton-Raphson<br/><sub>up to 8 iterations</sub>]:::newton
    D --> E{|x_u − t|<br/>< 1e-7?}
    E -->|yes ~95%| F[return y_u]:::out
    E -->|no| G{|x' u|<br/>< 1e-6?}
    G -->|yes| H[break Newton<br/><sub>derivative collapsed</sub>]:::brk
    G -->|no| I[u ← u − err/dx<br/>clamp to 0,1]:::iter
    I --> D
    H --> J[Bisection fallback<br/><sub>up to 32 iterations</sub><br/><sub>retains post-Newton u</sub>]:::bisect
    D -->|exhausted 8 iters| J
    J --> K{|x_u − t|<br/>< 1e-6?}
    K -->|yes| F
    K -->|no| L[halve bracket]:::iter
    L --> J

    classDef start fill:#e0f2fe,stroke:#0284c7
    classDef trivial fill:#f3f4f6,stroke:#6b7280
    classDef newton fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef iter fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef brk fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef bisect fill:#fecaca,stroke:#dc2626,color:#7f1d1d
    classDef out fill:#ede9fe,stroke:#7c3aed,color:#4c1d95

Key invariants:

  • The bisection bracket [t0, t1] always contains the true solution because x(u) is monotonic on [0, 1] for any x1, x2 ∈ [0, 1] (per the CSS spec).
  • Newton's u is clamped into [0, 1] after every step to prevent runaway divergence on near-zero derivatives.
  • Convergence is checked at the top of each Newton iteration against fresh xu, so the early-exit branch is the truth, not a stale comparison.
  • Bisection retains the post-Newton u instead of resetting — even when Newton "fails," it gives bisection a much better starting point than t.

🏗️ Coefficient struct layout

compileBezier() returns a flat object with this exact key order:

{
    ax,  bx,  cx,    // cubic, quadratic, linear coefficients of x(u)
    ay,  by,  cy,    // cubic, quadratic, linear coefficients of y(u)
    ax3, bx2,        // precomputed derivative coefficients (3*ax, 2*bx)
}

Why this order: V8 assigns a hidden class based on the order properties are added. By always writing in the same sequence, every coefficient struct in your program shares one hidden class — which means evalBezier's property loads (c.ax, c.bx, …) hit the same inline cache slot every time. Don't add fields conditionally. Don't reorder. Don't delete. The struct is treated as immutable by the evaluator, but the V8-visible "shape" is the actual contract.

The struct is intentionally a plain object, not a Float64Array, because:

  • Property loads on a stable hidden class are as fast as typed-array indexing in modern V8.
  • A plain object lets the evaluator destructure inline if hot enough for the JIT.
  • It keeps the API trivially serializable and inspectable in DevTools.

📊 Comparison

| | lite-cubic-bezier | bezier-easing | popmotion's cubicBezier | hand-rolled inline | |---|---|---|---|---| | Bundle (min+gzip) | <1 KB | ~1.5 KB | bundled with framework | depends | | Hot-path allocations | 0 | depends on impl | depends | usually 0 | | DoD struct API | yes | no | no | manual | | Closure API | yes | yes | yes | manual | | Newton + Bisection | yes | yes | varies | usually no | | Shared linear identity | yes | no | no | no | | TypeScript types | yes (full) | yes | yes | n/a | | ECS / SoA friendly | yes | no | no | yes | | Framework lock-in | none | none | popmotion | none |

lite-cubic-bezier is not trying to replace browser-native CSS easing or full animation libraries. It's the kernel that does one thing well: turn four control numbers into a deterministic, allocation-free (t) => number.

⚙️ API

Closure API

cubicBezier(x1, y1, x2, y2): EasingFunction

Returns a stable (t) => number closure. Coefficients are computed once at construction.

| Param | Type | Description | |---|---|---| | x1 | number | Control point 1 X. Per CSS spec, must be in [0, 1]. | | y1 | number | Control point 1 Y. May be outside [0, 1] for "back" easings. | | x2 | number | Control point 2 X. Per CSS spec, must be in [0, 1]. | | y2 | number | Control point 2 Y. May be outside [0, 1] for overshoot. |

When x1 === y1 && x2 === y2 (mathematically the identity easing), the function returns a shared cached (t) => t instead of a new closure — useful when many call sites construct linear curves.

DoD API

compileBezier(x1, y1, x2, y2): BezierCoefficients

Pre-computes polynomial coefficients into a flat struct with stable hidden class. Call once per curve at setup time.

evalBezier(t: number, c: BezierCoefficients): number

Stateless evaluator. Zero allocations. Requires a struct produced by compileBezier().

| Param | Type | Description | |---|---|---| | t | number | Timeline progression. Values outside [0, 1] clamp to the nearest boundary. NaN propagates. | | c | BezierCoefficients | The compiled coefficient struct. Must come from compileBezier() — passing {x1,y1,x2,y2} will return NaN. |

Types

export type EasingFunction = (t: number) => number;

export interface BezierCoefficients {
    ax: number;  bx: number;  cx: number;
    ay: number;  by: number;  cy: number;
    ax3: number; bx2: number;
}

⚡ Performance characteristics

| Operation | Cost | |---|---| | evalBezier() typical CSS curve | 2–3 Newton iterations, ~10 multiplications | | evalBezier() Newton-converged early exit | 1 polynomial evaluation, ~5 multiplications | | evalBezier() boundary case (t ≤ 0 or t ≥ 1) | 1 comparison | | evalBezier() pathological curve, full bisection | up to 32 iterations | | compileBezier() | 8 multiplications, 1 small allocation (setup, not hot path) | | cubicBezier() linear fast-path | 0 allocations (returns shared IDENTITY) | | Hot-path allocations | zero | | Hidden class instances | one (shared across all compiled structs) |

🛡️ Validation

This library trusts its inputs. There is no runtime validation — bad data produces predictable bad output, not exceptions:

| Input | Behavior | |---|---| | t < 0 or t > 1 | Clamps to 0 / 1 | | t === NaN | Output is NaN (propagates through Newton + Bisection) | | x1 or x2 outside [0, 1] | x(u) may be non-monotonic; solver may return imprecise results | | c not from compileBezier() | evalBezier() returns NaN |

This is deliberate: validation belongs at the boundary of your application, not at the bottom of every animation tick. Validate once when the user (or the CSS parser, or the keyframe loader) hands you the four control numbers.

📦 TypeScript

Full TypeScript declarations included in CubicBezier.d.ts. The BezierCoefficients interface is exported so you can store, pass, and serialize compiled structs with full type safety.

import { cubicBezier, compileBezier, evalBezier } from '@zakkster/lite-cubic-bezier';
import type { EasingFunction, BezierCoefficients } from '@zakkster/lite-cubic-bezier';

const ease: EasingFunction = cubicBezier(0.25, 0.1, 0.25, 1.0);
const coeffs: BezierCoefficients = compileBezier(0.42, 0, 0.58, 1.0);

📚 LLM-friendly documentation

See llms.txt for a structured reference designed for AI coding assistants. Covers the public surface, the algorithm, integration patterns, and common pitfalls in one parseable document.

🧩 Pairs well with

License

MIT © Zahary Shinikchiev