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

simplequad

v3.2.0

Published

TypeScript quadtree with SAT collision detection and MTV output. Zero dependencies.

Readme

simplequad

Node.js CI Bundle Size

Bundles quadtree spatial partitioning, SAT intersection testing, and minimum translation vector (MTV) output in a single zero-dependency TypeScript package.

simplequad
Examples →


Who this is for

You're building a canvas game or interactive 2D app and you need to know: what am I colliding with, and by how much? — without pulling in a full physics engine.

const hits = tree.query(myObject);
// hits: Array<{ object: T, mtv: MinimumTranslationVectorInfo }>

One call returns the colliding objects and the overlap vectors you need to push things apart. Supported shapes: BoundingBox, Circle, Point, freely mixed.

If you're using a canvas renderer or something like Pixi.js — which has no built-in collision — simplequad plugs in cleanly.

Object count guide

| Scale | Object count | Example genres | Budget used | | ------ | ------------ | ------------------------------- | ----------- | | Small | < 100 | Casual, puzzle, platformer | < 1.5% | | Medium | 100–500 | Shooter, tower defense, boids | 1.5–10% | | Large | 500–1 000 | Horde survival, light RTS | 10–26% | | XL | 1 000+ | Dense bullet hell, large armies | Not advised |

At 1 000 objects, simplequad uses ~26% of the frame budget — real headroom for game logic and rendering. Drop below 500 and it's nearly free.


When to look elsewhere

  • Beyond 1000 dynamic objects. Stays within the 60fps budget up to 1000 objects; past that, you want something purpose-built for high object counts.
  • Rotation or convex polygons. Only axis-aligned boxes, circles, and points are supported.
  • Continuous collision detection. Intersections are checked at query time — fast-moving objects can tunnel through thin geometry between frames.
  • Full physics simulation. No mass, inertia, joints, or constraints. simplequad tells you that objects overlap and by how much, not how they should physically react.

Performance

Benchmarked using the game-loop pattern (clear → add all → query each object per frame) with AABB objects, averaged across multiple seeds:

| Objects | avg frame time | p95 frame time | 60fps budget used | | ------- | -------------- | -------------- | ----------------- | | 100 | 0.233ms | 0.238ms | 1.4% | | 200 | 0.491ms | 0.501ms | 2.9% | | 300 | 0.861ms | 0.878ms | 5.2% | | 500 | 1.634ms | 1.657ms | 9.8% | | 1000 | 4.271ms | 4.669ms | 25.6% |

The p95 column shows worst-case frame time across test samples; it stays within the 16.7ms budget at all tested object counts.

Mixed-shape note: Scenes with circles run at roughly 1.5–2× the cost of AABB-only scenes at the same object count and query window size.


Installation

npm

npm install simplequad

CDN

<script src="https://unpkg.com/simplequad@latest/dist/simplequad.umd.js"></script>

The window global is SimpleQuad.


Usage

Game loop pattern (recommended for moving objects)

Clear and rebuild the tree every frame. This is the pattern the benchmarks above are based on.

import { createQuadTree } from "simplequad";

const tree = createQuadTree({ x: 0, y: 0, width: 800, height: 600 });

function gameLoop(entities: Entity[]) {
  tree.clear();
  for (const entity of entities) {
    tree.add(entity);
  }
  for (const entity of entities) {
    const hits = tree.query(entity);
    for (const { object, mtv } of hits) {
      // object: the entity this one is colliding with
      // mtv.vector: apply to entity's position to push it out of the overlap
    }
  }
}

Moving a single object

Use move() to atomically remove, mutate, and re-insert an object:

tree.move(entity, (e) => {
  e.x = newX;
  e.y = newY;
});

Returns false if the object wasn't in the tree. Manually doing remove → mutate → add also works, but forgetting the remove first silently leaves a stale entry.

For scenes with many moving objects, the clear+rebuild pattern above is simpler at scale.

MTV semantics

query() returns an MTV for each colliding object. The vector points from the found object toward the querying object, with a magnitude equal to the overlap depth.

To push the querying object out of a collision:

const hits = tree.query(player);
for (const { mtv } of hits) {
  player.x += mtv.vector.x;
  player.y += mtv.vector.y;
}

Physics response patterns

simplequad tells you where objects overlap and by how much. What to do with that depends on your game type.

Platformer — detect floor, ceiling, and wall contacts by MTV direction, then zero the matching velocity component:

const hits = tree.query(player.body);
player.onGround = false;

for (const { mtv } of hits) {
  player.x += mtv.vector.x;
  player.y += mtv.vector.y;

  if (mtv.vector.y < -0.1) {
    // pushed upward → landed on floor
    player.vy = 0;
    player.onGround = true;
  }
  if (mtv.vector.y > 0.1 && player.vy < 0) player.vy = 0; // hit ceiling while moving up
  if (Math.abs(mtv.vector.x) > 0.1) player.vx = 0; // hit wall
}

The 0.1 threshold filters near-zero y values from diagonal MTVs so they don't trigger floor/ceiling detection incorrectly. See Pixi.js Platformer for a full implementation with dt-based gravity and smooth acceleration.

Elastic collision — split the MTV equally between two moving bodies and exchange an impulse along the collision normal:

for (const { object, mtv } of tree.query(body)) {
  // Shared push-out (each body moves half)
  body.x += mtv.vector.x * 0.5;
  body.y += mtv.vector.y * 0.5;
  object.x -= mtv.vector.x * 0.5;
  object.y -= mtv.vector.y * 0.5;

  // Impulse along normal (equal masses, restitution coefficient e)
  const nx = mtv.direction.x,
    ny = mtv.direction.y;
  const dvx = body.vx - object.vx,
    dvy = body.vy - object.vy;
  const dot = dvx * nx + dvy * ny;
  if (dot < 0) {
    // only exchange impulse if approaching
    const impulse = (dot * (1 + e)) / 2;
    body.vx -= impulse * nx;
    body.vy -= impulse * ny;
    object.vx += impulse * nx;
    object.vy += impulse * ny;
  }
}

See Gravity Sandbox.

Soft separation — for crowd simulation where only positions matter, not velocities:

for (const { mtv } of tree.query(entity)) {
  entity.x += mtv.vector.x * 0.5;
  entity.y += mtv.vector.y * 0.5;
}

See Horde Survival — this keeps 500+ enemies from overlapping without any velocity math.


Examples

Live demos at rcasto.github.io/simplequad

| Example | What it shows | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | Query Explorer | All three shape types, live query window, MTV arrows drawn — start here | | Platformer | AABB collision and MTV resolution — player stands on platforms and collects coins | | Top-down Shooter | Circle queries across escalating enemy waves, 100–200 dynamic objects | | Horde Survival | Wave-based survival with escalating enemy counts; stress-tests the tree at high object densities | | Breakout | Mixed shapes — circle ball vs. AABB bricks, MTV drives push-out and reflection angle | | Boids | 150 flocking agents querying spatial neighborhoods every frame; toggle tree overlay | | Gravity Sandbox | Spawn colliding blobs with real-time gravity and drag controls | | Asteroid Field | Split mechanic — rocks fragment on impact, mixed-size circle queries grow each frame | | Tower Defense | Fixed towers query a range circle every frame against many path-following enemies | | Predator-Prey | Prey flock and flee via spatial queries; predators hunt and eat on collision — same tree, two roles | | Pixi.js Platformer | Rolling circle player on a scrolling level; Pixi.js renders, simplequad handles circle-vs-AABB MTV collision | | Image Compression | Upload any image — a quadtree progressively compresses it, subdividing where colors vary most |


API

All schema definitions are also available in schema.ts in the repo.

createQuadTree / create

create is an alias for createQuadTree — both refer to the same function.

Standard form — object type must extend Bound:

export function createQuadTree<T extends Bound>(
  bounds: BoundingBox,
  options?: QuadTreeOptions<T>,
): QuadTree<T>;

Extractor form — use when your objects don't extend Bound directly:

export function createQuadTree<T>(
  bounds: BoundingBox,
  options: QuadTreeOptions<T> & { extractor: (obj: T) => Bound },
): QuadTree<T>;

Creates a quadtree over the given bounds. All objects added should intersect or be contained within these bounds.

  • capacity (default 5) — number of objects a node holds before subdividing. Higher values mean shallower trees; lower values mean finer spatial partitioning. Tune upward for scenes with many objects clustered in small areas.
  • maxDepth (default 8) — maximum subdivision depth. Nodes at this depth store objects regardless of capacity, preventing unbounded recursion when many objects share a small region.
  • extractor — function that derives a Bound from your object. When provided, T can be any type. When omitted, T must extend Bound. See Extractor pattern below.

Positional capacity and maxDepth arguments (createQuadTree(bounds, 5, 8)) remain supported for backwards compatibility but are not the preferred form.

QuadTreeOptions<T>

export interface QuadTreeOptions<T = Bound> {
  capacity?: number;
  maxDepth?: number;
  extractor?: (obj: T) => Bound;
}

When extractor is provided, T can be any type. When extractor is omitted, T must extend Bound.

QuadTree<T>

Mutation

add(object: T): boolean — Adds an object. Returns false if it falls outside the tree's bounds. Subdivides the containing node when capacity is reached.

remove(object: T): boolean — Removes an object by reference equality. Returns false if not found.

clear(): void — Removes all objects and resets all subdivisions.

move(object: T, updateFn: (obj: T) => void): boolean — Atomically removes, calls updateFn to mutate, then re-inserts. Returns false if the object was not in the tree. Use this instead of manual remove → mutate → add to avoid leaving stale entries.

Querying

query(bounds: Bound): Array<QueryResult<T>> — Returns all objects whose bounds intersect bounds, with MTV for each. If the same object reference was added to the tree and passed as bounds, it is automatically excluded. When using an extractor, use queryFor() instead.

queryFor(object: T): Array<QueryResult<T>> — Like query() but takes a T directly. When an extractor is configured, applies it automatically and excludes object from results by reference. Prefer this over query() for entity-vs-world collision checks.

queryBroad(bounds: Bound): T[] — Like query() but skips SAT — returns the objects themselves (no MTV) whose bounds broadly overlap bounds. Faster; use for AI sight ranges, audio zones, spawn checks, and flocking neighbor lookups.

Inspection

getData(): T[] — Returns all objects currently in the tree as a flat deduplicated array.

contains(object: T): boolean — Returns true if the given object reference is in the tree.


QueryResult<T>

export interface QueryResult<T> {
  object: T;
  mtv: MinimumTranslationVectorInfo;
}

MinimumTranslationVectorInfo

export interface MinimumTranslationVectorInfo {
  vector: Point; // full translation vector (direction × magnitude)
  direction: Point; // unit vector
  magnitude: number; // overlap depth
}

The MTV points from the found object toward the querying object. Apply mtv.vector to the querying object's position to push it out of the overlap. The coordinate space assumes x increases left-to-right and y increases top-to-bottom.


Bound types

export type Bound = BoundingBox | Circle | Point;

export interface Point {
  x: number;
  y: number;
}

export interface BoundingBox extends Point {
  width: number;
  height: number;
}

export interface Circle extends Point {
  r: number;
}

Objects added to the tree must either extend one of these types directly, or be used with the extractor pattern described below.


Extractor pattern

By default, simplequad reads spatial bounds directly from the objects you add (x, y, width/height or r). If your objects store their position elsewhere, pass an extractor function at tree creation time instead.

import { create } from "simplequad";

// Works with any object shape
interface Sprite {
  id: string;
  pos: { x: number; y: number };
  w: number;
  h: number;
}

const tree = create<Sprite>(
  { x: 0, y: 0, width: 800, height: 600 },
  {
    extractor: (sprite) => ({
      x: sprite.pos.x,
      y: sprite.pos.y,
      width: sprite.w,
      height: sprite.h,
    }),
  },
);

tree.add(sprite);
tree.remove(sprite);

// query for a specific entity's collisions — extractor and self-exclusion handled automatically:
const hits = tree.queryFor(sprite);

// area query (no specific entity):
const areaHits = tree.query({ x: 100, y: 100, width: 200, height: 200 });

Testing

npm install
npm test

Coverage report:

npm run test:coverage