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

v1.0.0

Published

Zero-GC 2D axis-aligned bounding box primitives. Flat Float32Array(4) format [minX, minY, maxX, maxY], every op writes into a caller-provided out buffer — no per-frame allocation. ~100 lines, zero deps, ESM only.

Downloads

77

Readme

@zakkster/lite-aabb

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

Zero-GC 2D axis-aligned bounding-box primitives. Twelve operations on a flat Float32Array(4)[minX, minY, maxX, maxY]. Every op that returns a box writes into a caller-provided out buffer. No new in your hot loop. No object graphs. ~120 lines of code.

import { aabb2 } from '@zakkster/lite-aabb';

// Allocate once at setup.
const playerBox = aabb2.create(0, 0, 32, 32);
const wall      = aabb2.create(40, 0, 48, 32);
const swept     = aabb2.create();      // scratch buffer for the hot path

// In the per-frame loop — zero allocations.
aabb2.set(playerBox, x, y, x + 32, y + 32);
aabb2.fatten(swept, playerBox, 4);        // proximity check radius

if (aabb2.intersects(swept, wall)) {
    // resolve collision...
}

Contents


Why

The first AABB API every JS dev writes looks like this:

class AABB {
    constructor(minX, minY, maxX, maxY) { /* ... */ }
    intersects(other) { /* ... */ }
    union(other) { return new AABB(/* ... */); }
}

It's pleasant to use. And at 10k bounding-box ops per frame, it allocates ~10k objects per frame, every one of which gets handed to the garbage collector. Tilemap scrolling stutters. Particle systems hitch. Twitch extension overlays — which have a strict 60 fps budget and a 1 MB bundle cap — fail their cold-start review.

The fix isn't faster math; it's no allocation at all. Every operation here writes into a buffer you already own:

// Per frame — every call is one indexed store loop. Zero allocations.
aabb2.set(out, x, y, x + w, y + h);
aabb2.fatten(out, out, margin);
const hit = aabb2.intersects(out, target);

Total cost: four out[i] = … writes per set, branchless Math.min/max for merge, four comparisons for intersects. That's it.

What this is not

  • Not a collision resolver. It tells you boxes touch; it doesn't compute MTV, contact points, or swept-AABB hits.
  • Not a spatial index. For broadphase across many boxes, use @zakkster/lite-bvh — same flat-array format, plug-in compatible.
  • Not 3D. This is (minX, minY, maxX, maxY). A lite-aabb3 would be a different package.

Install

npm i @zakkster/lite-aabb

ESM only. Zero dependencies. Ships TypeScript definitions alongside the source.

import { aabb2 } from '@zakkster/lite-aabb';

You can also drop Aabb.js into your project directly — it's one file with no imports.


Quick start

import { aabb2 } from '@zakkster/lite-aabb';

// ---- Setup phase: allocate everything you'll need ----
const box  = aabb2.create(0, 0, 10, 10);
const next = aabb2.create();                   // scratch for transformed bounds
const fat  = aabb2.create();                   // scratch for broadphase

// ---- Hot loop: zero allocations ----
function update(dx, dy) {
    aabb2.set(next, box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy);
    aabb2.fatten(fat, next, 1);                // 1-pixel slop for picking
    return aabb2.intersects(fat, worldBox);
}

Common idioms

// Initialize a "min bounds" reducer (a box that will be merged into).
const bounds = aabb2.create(Infinity, Infinity, -Infinity, -Infinity);
for (const point of points) {
    bounds[0] = Math.min(bounds[0], point.x);
    bounds[1] = Math.min(bounds[1], point.y);
    bounds[2] = Math.max(bounds[2], point.x);
    bounds[3] = Math.max(bounds[3], point.y);
}

// Union N boxes into one (in-place reduction).
aabb2.copy(out, boxes[0]);
for (let i = 1; i < boxes.length; i++) {
    aabb2.extend(out, boxes[i]);
}

// Sweep test (cheap approximation of swept-AABB).
aabb2.merge(swept, prevPosBox, nextPosBox);
const candidates = bvh.query(swept, hitBuffer);

The flat-array convention

An AABB is just a Float32Array of length 4 — no class wrapper, no prototype:

index:   0       1       2       3
value:   minX    minY    maxX    maxY

This format is chosen on purpose:

| Property | Why it matters | |---|---| | Plain Float32Array | V8 stores typed-array data off-heap; reads compile to a single load instruction. No hidden classes, no megamorphic property access. | | Contiguous layout | Modern CPUs prefetch consecutive 16-byte chunks for free. Reading min and max is one cache line. | | No method lookup | Module-level functions inline cleanly in V8/JSC's TurboFan. Class methods through this add an extra indirection. | | Transferable-safe | An AABB can be sent through postMessage to a Worker by transferring its .buffer. | | Compatible with WebGL / WebGPU | The same Float32Array can be fed directly to a uniform or storage buffer if you're packing bounds for the GPU. |

Float32 is not free

The trade-off: Float32Array has ~7 decimal digits of precision. For world-space bounds at typical game-engine scales (a few thousand units), this is fine. If your scene spans millions of units (planetary terrain, geographic mapping), use Float64Array and pay the doubled memory cost — but you'll need to swap the type yourself in Aabb.js.


API reference

All functions are static (no this), live on the aabb2 namespace, and return out for chaining.

Allocation — don't call in a hot loop

| Function | Returns | Description | |---|---|---| | aabb2.create(minX?, minY?, maxX?, maxY?) | Float32Array(4) | Allocates a new AABB. Defaults are zero. | | aabb2.clone(a) | Float32Array(4) | Allocates a copy of a. |

Assignment — zero allocation

| Function | Returns | Description | |---|---|---| | aabb2.copy(out, a) | out | out ← a. | | aabb2.set(out, minX, minY, maxX, maxY) | out | Writes explicit bounds. |

Composition — zero allocation

| Function | Returns | Description | |---|---|---| | aabb2.merge(out, a, b) | out | out ← bounding box enclosing both a and b. Safe when out aliases a or b. | | aabb2.extend(out, b) | out | Enlarges out in place to include b. Equivalent to merge(out, out, b) with one fewer indirection. | | aabb2.fatten(out, a, margin) | out | Expands a by margin on every side and writes into out. Negative margins shrink. Safe when out === a. |

Measurement

| Function | Returns | Description | |---|---|---| | aabb2.perimeter(a) | number | 2 * (width + height). The Surface Area Heuristic cost in 2D BVHs. | | aabb2.area(a) | number | width * height. | | aabb2.overlapArea(a, b) | number | Area of the intersection. 0 if they don't overlap. Touching edges produce 0. |

Predicates

| Function | Returns | Description | |---|---|---| | aabb2.intersects(a, b) | boolean | True if a and b overlap. Touching edges count as overlap (>= comparison). | | aabb2.contains(a, b) | boolean | True if a fully contains b. Touching edges count as contained. |


Aliasing rules

The out buffer can safely alias the input buffer in every function that takes both:

// All of these are correct and produce the right result:
aabb2.merge(a, a, b);          // a ← merge(a, b)
aabb2.merge(b, a, b);          // b ← merge(a, b)
aabb2.fatten(a, a, 2);         // grow a in place
aabb2.copy(a, a);              // no-op

This works because every operation reads each input slot exactly once before writing the corresponding output slot. For four-element arrays this is trivially safe in any order.


Compatibility with @zakkster/lite-bvh

@zakkster/lite-bvh uses the same Float32Array(4) AABB format as the leaf input to insertLeaf / updateLeaf / query:

import { aabb2 } from '@zakkster/lite-aabb';
import { DynamicBVH2D } from '@zakkster/lite-bvh';

const tree = new DynamicBVH2D(4096);
const tight = aabb2.create();
const fat   = aabb2.create();

// Insert entities with a fattening margin baked in.
aabb2.set(tight, x, y, x + w, y + h);
aabb2.fatten(fat, tight, 4);
const nodeId = tree.insertLeaf(fat, entityId);

// Move an entity. The BVH internally checks if the tight bounds still fit
// inside the fat bounds and only restructures the tree if they don't.
aabb2.set(tight, newX, newY, newX + w, newY + h);
tree.updateLeaf(nodeId, tight, 4);

// Query a viewport rectangle.
aabb2.set(fat, viewX, viewY, viewX + viewW, viewY + viewH);
const hits = tree.query(fat, hitBuffer);

There is no runtime dependency between the two packages — they just agree on the buffer format. Use either one alone or both together.


Testing

npm test
# or: node --expose-gc Aabb.test.js

Runs 37 deterministic assertions covering:

| Group | What's tested | |---|---| | Construction + copy | defaults, explicit values, independence of clones, return-out contract | | merge / extend | non-overlapping, overlapping, contained, negative coords, aliasing | | perimeter / area | unit, rectangle, zero-size | | overlapArea | identical, partial, disjoint, touching edges, containment | | intersects / contains | overlap, touching, disjoint, axis-separated, self-containment, symmetry | | fatten | positive/zero/negative margins, aliasing | | Zero-allocation guarantee | 500 000 mixed ops → heap growth < 256 KB under --expose-gc |

The zero-alloc test requires the --expose-gc flag — without it the test is skipped (with a yellow warning) rather than failing, so CI runs without flags still go green.

A clean run ends with 37 passed, 0 failed and exit code 0. Any failure prints the assertion plus the expected/actual values.


License

MIT © Zahary Shinikchiev