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

v1.0.0

Published

Zero-GC, high-performance multi-segment keyframe timelines. Animate UI and particle systems with typed-array backed state and O(1) cursor-advanced evaluation.

Downloads

69

Readme

@zakkster/lite-keyframe

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

🎞️ What is lite-keyframe?

@zakkster/lite-keyframe is a multi-segment easing curve. A regular easing function takes one value and bends it across [0, 1]. A keyframe timeline does the same thing — but across an arbitrary sequence of waypoints, with a different ease per segment.

The whole library is two classes built on a single shared evaluator:

  • 🟢 Keyframe — one timeline per instance. Best for hero animations, scripted UI reveals, one-off complex sequences.
  • 🟣 KeyframePool — N timelines packed in an SoA layout. Best for mass animation: particle systems, grids, multi-element UIs.

Both produce a stable (t) => number reference that drops directly into anything that takes an easing function — lite-tween, GSAP, your own RAF loop. Zero allocations on the hot path. Zero dependencies. Under 2 KB min+gzip.

🧬 Where it fits

lite-keyframe is the composition layer of the zero-GC animation pipeline:

flowchart LR
    A[lite-ease<br/><sub>raw math</sub>]:::raw --> B[lite-ease-lut<br/><sub>bake to LUT</sub>]:::lut
    B --> C[lite-keyframe<br/><sub>compose segments</sub>]:::keyf
    A --> C
    C --> D[lite-tween<br/><sub>drive over time</sub>]:::tween
    D --> E[your render loop]:::render

    classDef raw fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e
    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

Each library is independent. You can use lite-keyframe standalone with raw (t) => number callbacks — no framework lock-in.

🚀 Install

npm install @zakkster/lite-keyframe

🕹️ Quick Start

Single timeline — Keyframe

import Keyframe from '@zakkster/lite-keyframe';
import { easeOutBounce, easeInQuad } from '@zakkster/lite-ease';

// Reusable easing bank shared by the timeline
const tl = new Keyframe(8).setBank([easeOutBounce, easeInQuad]);

// (time, value, easeIdx) — easeIdx -1 = linear, 0..127 = bank index
tl.addKey(0.0,   0, -1);  // start
tl.addKey(0.4, 100,  0);  // bounce in
tl.addKey(0.7,  60,  1);  // quad-ease back down
tl.addKey(1.0, 200, -1);  // linear settle

// tl.eval is a stable arrow ref — drop it anywhere a (t) => number is wanted
mgr.to(player, 'x', 0, 200, 5.0, { ease: tl.eval });

// Or use it directly
console.log(tl.eval(0.5));  // → 80 (interpolated through segment 1)

Mass animation — KeyframePool

import { KeyframePool } from '@zakkster/lite-keyframe';
import { easeOutCubic } from '@zakkster/lite-ease';

// 500 independent timelines, max 4 keys each (~21 KB total)
const pool = new KeyframePool(500, 4).setBank([easeOutCubic]);

// Build different timelines per particle
for (let i = 0; i < 500; i++) {
    pool.addKey(i, 0,   spawnY[i],     -1);
    pool.addKey(i, 0.3, peakY[i],       0);  // ease into peak
    pool.addKey(i, 1.0, groundY[i],    -1);
}

// In your update loop — pure typed-array reads, zero allocation
function update(t) {
    for (let i = 0; i < 500; i++) {
        particles[i].y = pool.eval(i, t);
    }
}

🧠 Why this exists

Most animation libraries collapse "the curve" into a single easing function. That works for simple from → to tweens but falls apart the moment you need a path that bends multiple times: bounce → settle → drift → snap. The usual workarounds — chained tweens, nested timelines, after-completion callbacks — all allocate, all schedule, all add jank.

A keyframe timeline solves this by treating the whole path as one object you can evaluate at any normalized time t. The curve becomes a pure function: t → value. Combine that with the data-oriented constraint that hot-path code must allocate zero memory and you get this library.

The design ladder:

  1. Drop-in (t) => number. Anywhere you'd pass easeOutBounce, you can pass tl.eval. No framework knows or cares. The integration contract is one type signature.
  2. Cursor-tracked O(1) eval. Steady-state forward play (the 90%+ case) doesn't search for the active segment — it stays put. The cold path is reserved for scrubbing and rewinds.
  3. SoA pool for mass animation. When you need 500 timelines, allocating 500 instances is pointless overhead. KeyframePool packs everything into shared typed arrays — same eval primitive, same API, vastly less memory pressure.
  4. Validate at setup, not at frame 60. Bad data (NaN time, missing bank, out-of-range index) throws when you insert it, not when something silently returns garbage three minutes into a session.

🔥 Hot-path architecture

The whole library exists to make eval() cheap. Inside the evaluator, a 4-branch cascade picks the cheapest possible answer:

flowchart TD
    A[eval&#40;t&#41;]:::start --> B{n &lt; 1?<br/>n &lt; 2?}
    B -->|yes| C[return early]:::trivial
    B -->|no| D{NaN or<br/>out of range?}
    D -->|yes| E[clamp to first/last]:::trivial
    D -->|no| F{cursor's<br/>current segment<br/>still valid?}
    F -->|yes ~90%| G[STAY<br/><sub>0 reads</sub>]:::stay
    F -->|no| H{advance 1?}
    H -->|yes| I[c++ <br/><sub>per-key boundary</sub>]:::adv
    H -->|no| J{advance 2?}
    J -->|yes| K[c += 2<br/><sub>frame skip</sub>]:::adv
    J -->|no| L[COLD PATH<br/>linear or binary scan]:::cold
    G --> M[interpolate t0 → t1]:::out
    I --> M
    K --> M
    L --> M

    classDef start fill:#e0f2fe,stroke:#0284c7
    classDef trivial fill:#f3f4f6,stroke:#6b7280
    classDef stay fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef adv fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef cold fill:#fecaca,stroke:#dc2626,color:#7f1d1d
    classDef out fill:#ede9fe,stroke:#7c3aed,color:#4c1d95

The cold-path scan is itself adaptive: linear scan for n ≤ 16 (faster than binary search at small n thanks to the branch predictor and cache prefetch), binary search above that.

🏗️ SoA memory layout (KeyframePool)

KeyframePool is a Structure-of-Arrays packing of N timelines. Each per-key field lives in its own typed array, contiguous across all timelines:

            ┌─ row 0 ─┬─ row 1 ─┬─ row 2 ─┬─ ... ─┐
times[]   = │ t₀ t₁ … │ t₀ t₁ … │ t₀ t₁ … │  …    │  Float32Array
values[]  = │ v₀ v₁ … │ v₀ v₁ … │ v₀ v₁ … │  …    │  Float32Array
easings[] = │ e₀ e₁ … │ e₀ e₁ … │ e₀ e₁ … │  …    │  Int8Array
            └────────┴─────────┴─────────┴───────┘
              stride    stride    stride

cursors[] = [ c₀, c₁, c₂, … ]  Int32Array (one per row)
counts[]  = [ n₀, n₁, n₂, … ]  Int32Array (one per row)

Why this layout: when you're evaluating row i, the keys for row i sit in one contiguous cache line. The cursor and count for row i sit at cursors[i] and counts[i] — also contiguous in their own arrays. No chasing pointers across heap-allocated instances.

Memory cost: capacity × stride × 9 bytes for keyframe data + capacity × 8 bytes for cursors and counts. 500 timelines × 4 keys ≈ 21 KB total.

📊 Comparison

| | lite-keyframe | GSAP Timeline | Theatre.js | raw chained tweens | |---|---|---|---|---| | Bundle size | <2 KB | ~30 KB (gsap core) | ~250 KB | depends | | Hot-path allocations | 0 | several per frame | many | several per frame | | Mass animation API | SoA pool built in | manual | not the use case | manual | | Evaluator type | pure (t) => number | Timeline object | Studio-bound | controller object | | Multi-segment ease | 1 timeline | nested tweens | yes (visual) | chain + callbacks | | Setup-time validation | strict | lenient | strict | none | | Framework lock-in | none | GSAP runtime | Theatre.js studio | tween library's own |

lite-keyframe is not a replacement for GSAP or Theatre — those are full animation systems with timelines, easings, callbacks, plugins, scrubbing UIs. This is the kernel that does one thing well: turn a sequence of (time, value, ease) keys into a fast pure function.

⚙️ API

class Keyframe

new Keyframe(maxKeys?: number)        // default 16

| Method / Property | Description | |---|---| | .setBank(bank) | Set the easing bank (array of (t) => number). Max 127 fns. Returns this. | | .addKey(time, value, easeIdx?) | Insertion-sorted key add. Returns slot index, or -1 if full. | | .setKey(idx, time, value, easeIdx?) | Direct slot write — caller owns the sort invariant. | | .eval(t) | Stable arrow ref. Pre-bound for use as a callback. | | .clear() | Reset to zero keys; buffers retained. | | .destroy() | Null out typed arrays. Safe for GC. | | .keyCount | Active key count (getter). | | .maxKeys | Capacity (getter). |

class KeyframePool

new KeyframePool(capacity: number, maxKeysPerTimeline?: number)  // default stride 8

| Method / Property | Description | |---|---| | .setBank(bank) | Shared easing bank for the entire pool. Returns this. | | .addKey(rowIdx, time, value, easeIdx?) | Add to row rowIdx. Returns slot index, or -1 if full. | | .setKey(rowIdx, slotIdx, time, value, easeIdx?) | Direct write into a row's slot. | | .eval(rowIdx, t) | Evaluate timeline at row rowIdx. Hot path — no rowIdx bounds check. | | .clear(rowIdx) | Reset one row. | | .clearAll() | Reset every row. | | .keyCount(rowIdx) | Active keys in one row. | | .destroy() | Null out all typed arrays. | | .capacity | Number of rows (getter). | | .stride | Max keys per row (getter). |

Validation rules

The library throws loudly at setup so you don't debug silent garbage at frame 60:

| Method | Throws on | |---|---| | setBank | non-array bank, or bank.length > 127 | | addKey | NaN / Infinity time, non-int easeIdx, easeIdx outside [-1, 127], or easeIdx >= 0 with no/insufficient bank | | setKey | NaN / Infinity time | | KeyframePool.{addKey, setKey, clear, keyCount} | rowIdx outside [0, capacity) |

KeyframePool.eval() is the only method that does not bounds-check rowIdx — it's the hot path, and validation belongs to setup.

⚡ Performance characteristics

| Operation | Cost | |---|---| | eval() steady-state forward play | ~3 typed-array reads + 1 multiply-add | | eval() per-key advance | ~5 typed-array reads + 1 multiply-add | | eval() cold path (rewind, scrub) | linear scan O(n) for n≤16, binary search O(log n) above | | addKey() | O(n) worst case (insertion sort), O(1) appends | | setKey() | O(1) | | eval reference identity | stable for the lifetime of the instance | | Hot-path allocations | zero |

The cursor lives in a length-1 Int32Array (not a number field) so the access site inside the evaluator is monomorphic across Keyframe and KeyframePool — both classes hit the same typed-array indexing pattern, keeping V8's inline cache happy.

📦 TypeScript

Full TypeScript declarations included in Keyframe.d.ts. All validation behaviors are surfaced in JSDoc so IntelliSense shows you exactly what each method throws.

import Keyframe, { KeyframePool, EasingFunction } from '@zakkster/lite-keyframe';

const myEase: EasingFunction = (t) => t * t;

📚 LLM-friendly documentation

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

🧩 Pairs well with

License

MIT © Zahary Shinikchiev