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

v1.0.0

Published

Zero-GC 3D transform math: quaternion + 4x4 column-major matrix. Float32Array-only, fully unrolled, three.js-compatible compose order. Designed for tight per-frame budgets (Twitch extensions, WebGL/WebGPU, game loops).

Readme

@zakkster/lite-math

npm version Zero-GC npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

Zero-GC 3D transform math — quaternion + 4×4 matrix, nothing else. Two namespaces (quat, mat4) over plain Float32Array buffers. Fully unrolled, no helper-call boundary crossings, three.js-compatible compose order. ~440 lines of code, zero dependencies.

import { quat, mat4 } from '@zakkster/lite-math';

// Allocate once at setup.
const rotation  = quat.create();
const position  = new Float32Array([0, 0, 0]);
const scale     = new Float32Array([1, 1, 1]);
const world     = mat4.create();
const tmpQuat   = quat.create();
const tmpTarget = quat.create();
quat.fromEuler(tmpTarget, 0, Math.PI, 0);

// In the per-frame loop — zero allocations.
function tick(t) {
    quat.slerp(rotation, tmpQuat, tmpTarget, t);
    position[0] += velocityX;
    mat4.compose(world, position, rotation, scale);
    // gl.uniformMatrix4fv(uWorld, false, world);
}

Contents


Why

You almost certainly already have a math library. So why this one?

Because most math libraries allocate, and you can't see them doing it. gl-matrix is the gold standard for the zero-alloc approach in browser JS, but it's also five separate namespaces (vec2, vec3, vec4, mat3, mat4, quat, quat2) and ships with class-based wrappers, dual-quat support, and SIMD scaffolding you almost never need. Three.js's Matrix4/Quaternion classes are ergonomic but allocate scratch objects internally — fine for general 3D, fatal for a 60Hz Twitch extension with 1 MB of bundle budget and a 3 s cold-start.

@zakkster/lite-math is what's left after you strip everything you can:

  • Two namespaces. quat and mat4. That's it. If you need vec3 ops, three lines of inline arithmetic will outperform any function call.
  • Float32Array everything. No { x, y, z, w } object wrappers. No class Quaternion. Just flat buffers laid out for V8's typed-array fast path.
  • Every op writes into out. No exceptions. create() and clone() are the only allocators — call them at setup, never in the loop.
  • Fully unrolled. No .set(), no .fill(), no for loops crossing the C++ boundary in inner ops. copy writes 16 fields by hand because that's what TurboFan inlines best.
  • One file. ~440 lines. You can read the whole library in five minutes and audit every byte you ship.

It is not a replacement for gl-matrix if you need vec3.cross, mat3.normalFromMat4, or quaternion-from-axis-angle. It is what you reach for when you've already decided that allocations matter and you want the smallest possible surface that still handles a TRS pipeline.


Install

npm install @zakkster/lite-math

Zero dependencies. ESM only. Node ≥14, all modern browsers.


Quick start

A typical animated transform — interpolate rotation, build a world matrix, upload to the GPU:

import { quat, mat4 } from '@zakkster/lite-math';

// === Setup (allocate once) ===
const startRot = quat.create();
const endRot   = quat.create();
quat.fromEuler(endRot, 0, Math.PI / 2, 0);     // 90° around Y

const rot      = quat.create();
const pos      = new Float32Array([0, 0, -5]); // 5 units in front of camera
const scale    = new Float32Array([1, 1, 1]);
const world    = mat4.create();

// === Per-frame (zero allocations) ===
function tick(elapsedSec) {
    const t = Math.min(elapsedSec / 2.0, 1.0);     // 2-second blend
    quat.slerp(rot, startRot, endRot, t);
    mat4.compose(world, pos, rot, scale);
    // gl.uniformMatrix4fv(uWorld, false, world);
}

For a camera view matrix:

// view = inverse(camera-world)
const camWorld = mat4.create();
const view     = mat4.create();
mat4.compose(camWorld, camPos, camRot, ONE_SCALE);
if (!mat4.invert(view, camWorld)) {
    // singular — bad camera state, skip frame
}

Conventions

| | | |---|---| | Quaternion layout | [x, y, z, w] (Hamilton, w-last) — same as gl-matrix and three.js | | Matrix layout | Float32Array(16), column-major — same as WebGL / WebGPU / gl-matrix | | Compose order | mat4.compose(out, pos, rot, scale) produces T * R * S — same as three.js | | Rotation order | quat.fromEuler(out, x, y, z) is YXZ (yaw-pitch-roll) — same as three.js default | | Angle units | All angles in radians | | Coordinate system | Right-handed |

If you've used three.js, all four signatures match. If you've used gl-matrix, the layouts match but composition is (pos, rot, scale) instead of (rot, pos, scale) — see the comparison table.


API reference: quat

A quaternion is a Float32Array(4) laid out as [x, y, z, w].

| Method | Allocates? | Returns | Description | |---|---|---|---| | create(x?, y?, z?, w?) | ✅ once | Quat | New quaternion, defaults to identity (0,0,0,1). | | clone(a) | ✅ once | Quat | New independent copy of a. | | copy(out, a) | no | out | Copies a into out. | | set(out, x, y, z, w) | no | out | Writes explicit components. | | identity(out) | no | out | Writes (0, 0, 0, 1). | | multiply(out, a, b) | no | out | Hamilton product a * b. Aliasing-safe. | | normalize(out, a) | no | out | Unit-length; returns identity if a has zero length. | | dot(a, b) | no | number | Dot product. |dot| measures rotation closeness; sign indicates hemisphere. | | conjugate(out, a) | no | out | (-x, -y, -z, w). For unit quaternions this is the rotational inverse. | | slerp(out, a, b, t) | no | out | Spherical linear interpolation along the shortest arc. Falls back to normalized lerp when cos(θ/2) > 0.9995. Aliasing-safe. | | fromEuler(out, x, y, z) | no | out | YXZ-order Euler → unit quaternion. Angles in radians. |

Notes

slerp shortest-path correction. When dot(a, b) < 0 the two quaternions are in opposite hemispheres of the 4-sphere (they represent the same rotation but rotate "the long way around"). slerp negates b internally so you always interpolate along the shorter arc — the standard fix, but worth knowing if you ever try to manually unpack the math.

fromEuler order. Quaternions don't have a single canonical Euler-angle decomposition; somebody has to pick a convention. We use YXZ (apply yaw, then pitch, then roll) because it matches three.js's default Euler.order and is the most intuitive for free-look cameras and tumbling objects.


API reference: mat4

A matrix is a Float32Array(16) in column-major order. Element layout:

            | column 0 | column 1 | column 2 | column 3 |
            |   X axis |   Y axis |   Z axis | translation
indices:    |  0  1  2 |  4  5  6 |  8  9 10 |  12 13 14
            |  3 (pad) |  7 (pad) | 11 (pad) |  15 (= 1) |

| Method | Allocates? | Returns | Description | |---|---|---|---| | create() | ✅ once | Mat4 | New identity matrix via fast typed-array memcpy. | | clone(a) | ✅ once | Mat4 | New independent copy of a. | | copy(out, a) | no | out | Copies a into out. | | identity(out) | no | out | Writes the identity matrix. | | multiply(out, a, b) | no | out | a * b. Aliasing-safe. | | invert(out, a) | no | out \| null | Full 4×4 inverse, or null if singular (det ≈ 0). | | fromQuat(out, q) | no | out | Pure rotation matrix from quaternion. | | compose(out, pos, rot, scale) | no | out | T * R * S from translation [x,y,z], quaternion [x,y,z,w], scale [x,y,z]. |

Notes

invert returns null on singular matrices. Always check the return value before using out. A common cause: a camera with zero scale, or a hierarchy where a parent has collapsed to zero size.

fromQuat assumes a unit quaternion. If q isn't normalized, the resulting matrix has scale. Normalize first if you're not certain — slerp and fromEuler already produce unit quaternions, so you only need to worry about hand-set inputs.


Aliasing rules

Every binary operation is safe when out aliases an input:

quat.multiply(a, a, b);     // ✓ a *= b
mat4.multiply(world, world, local);  // ✓ accumulate transform
quat.slerp(rot, rot, target, t);     // ✓ interpolate in place

The unrolled implementations read every input slot into a local before writing any output, so there's no read-after-write hazard. copy, normalize, conjugate, invert, fromQuat, and compose all support out === input for the same reason.


Composition order: why T * R * S

mat4.compose(out, pos, rot, scale) produces:

                world = T * R * S

Applied to a point p, this evaluates right-to-left: scale first, then rotate, then translate. That's the order you almost always want — local space p gets sized, oriented, then placed in the world.

flowchart LR
    p["p (local)"] --> S["S — scale"]
    S --> R["R — rotate"]
    R --> T["T — translate"]
    T --> world["p (world)"]

    style p fill:#1e293b,color:#e2e8f0,stroke:#475569
    style S fill:#334155,color:#e2e8f0,stroke:#475569
    style R fill:#334155,color:#e2e8f0,stroke:#475569
    style T fill:#334155,color:#e2e8f0,stroke:#475569
    style world fill:#1e293b,color:#e2e8f0,stroke:#475569

The parameter order (pos, rot, scale) matches three.js's Matrix4.compose, not gl-matrix's fromRotationTranslationScale(out, q, v, s). If you're porting from gl-matrix, the output matrix is the same — only the call-site argument order changes.


Recipes

Build a perspective projection

@zakkster/lite-math deliberately doesn't ship perspective — write it once, inline, and you'll have something tighter than any library call:

function perspective(out, fovYRad, aspect, near, far) {
    const f = 1 / Math.tan(fovYRad / 2);
    const nf = 1 / (near - far);
    out[0]  = f / aspect;  out[1]  = 0;  out[2]  = 0;                    out[3]  = 0;
    out[4]  = 0;           out[5]  = f;  out[6]  = 0;                    out[7]  = 0;
    out[8]  = 0;           out[9]  = 0;  out[10] = (far + near) * nf;    out[11] = -1;
    out[12] = 0;           out[13] = 0;  out[14] = 2 * far * near * nf;  out[15] = 0;
    return out;
}

MVP matrix from world + view + projection

const mvp = mat4.create();
mat4.multiply(mvp, projection, view); // VP
mat4.multiply(mvp, mvp, world);       // VP * W = MVP

Smooth rotation toward a target

const blendSpeed = 4; // higher = snappier

function update(dt) {
    const t = 1 - Math.exp(-blendSpeed * dt); // framerate-independent damping
    quat.slerp(rot, rot, targetRot, t);
}

Inverse of a rigid transform

For a matrix that's only translation + rotation (no scale, no skew), you don't need the full invert — but if you're not 100% sure your matrix is rigid, just use mat4.invert. The cost is constant (~30 ns) and the code is one line.


Comparison with gl-matrix and three.js

| | @zakkster/lite-math | gl-matrix | three.js | |---|---|---|---| | Quat layout | [x, y, z, w] | [x, y, z, w] | { x, y, z, w } getters | | Mat4 layout | column-major Float32Array | column-major Float32Array | column-major Float32Array | | Compose call | compose(out, pos, rot, scale) | fromRotationTranslationScale(out, q, v, s) | m.compose(pos, rot, scale) | | Compose math | T * R * S | T * R * S | T * R * S | | Vec ops | inline yourself | vec2, vec3, vec4 namespaces | Vector3 class | | Per-frame allocs | zero | zero | depends on method | | Lines of code (mat4+quat) | ~440 | ~2000+ | ~1500+ | | Class wrappers | no | optional (glMatrix.glMatrix) | yes (default API) |

If you need vector arithmetic, axis-angle constructors, dual quaternions, or mat3.normalFromMat4, use gl-matrix. If you need scene-graph ergonomics, use three.js. If you need just the TRS pipeline with the smallest possible footprint, this is it.


Testing

npm test

Runs the suite under Node with --expose-gc:

  • 50 unit tests across quat (identity / multiply / normalize / dot / conjugate / slerp / fromEuler) and mat4 (identity / multiply / invert / fromQuat / compose).
  • Hamilton-product, shortest-arc slerp, near-parallel slerp fallback, singular-matrix null return.
  • Aliasing safety (multiply(a, a, b), conjugate(q, q), etc.) on every binary op.
  • A 100,000-iteration hot loop running slerp + compose + multiply per iteration, verifying total heap growth stays under 512 KB (typical run: ~10 KB of V8 internal churn). If anything were allocating, you'd see megabytes.

License

MIT © Zahary Shinikchiev