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

@vworlds/box2d3-wasm

v7.0.0

Published

Box2D v3 for the web and Node as WebAssembly: a flat C-style embind API plus a zero-allocation fast path.

Readme

@vworlds/box2d3-wasm

Box2D v3 compiled to WebAssembly for browsers and Node.js, with two ways to call it:

  • a flat, C-style embind API (b2CreateWorld, b2World_Step, b2Body_GetPosition, ...), and
  • a zero-allocation fast path (b2x_* raw exports and a small helper, @vworlds/box2d3-wasm/fastpath) that skips embind's marshalling for the calls a game makes every frame.

Both run the same Box2D code, v3.2.0 since 7.0.0, and give bit-identical results: npm run test:reference checks every step of seven scenes, through both APIs and both flavours, against hashes recorded for the pinned Box2D. Coming from 6.x (Box2D 3.1.1+): docs/box2d-3.2-migration.md.

Origin

This is a fork of Birch-san/box2d3-wasm by Alex Birch and Erik Sombroek, which wraps Erin Catto's Box2D. The upstream project is no longer maintained; this one is maintained by vworlds and its consumer is the vecs ECS library.

What differs from upstream 5.2.0:

  • Flat C API only. The object-oriented classes (World, Body, Shape, the joint classes, ...) are gone, and so is the box2cpp submodule.
  • No threading. No TaskSystem, no b2CreateThreadedWorld, no pthread build, no SharedArrayBuffer requirement. Memory is not shared. b2CreateWorld forces workerCount to 1 (Box2D 3.2 would otherwise start its own thread pool).
  • No debug draw. No b2DebugDraw, b2World_Draw or the ./utils/* export.
  • Box2D v3.2.0 since 7.0.0 (upstream 5.2.0 ships v3.1.1 plus 18 commits).
  • The fast path (below), plus a small patch to Box2D (patches/box2d/) that adds a runtime switch for its profile timers.

Install

npm install @vworlds/box2d3-wasm

The package is not published yet. Until it is, build it from source (see Build), run npm pack and install the tarball. It is ESM only.

Usage

The flat API mirrors Box2D's C API; see Box2D's documentation for what each function does.

import Box2DFactory from "@vworlds/box2d3-wasm";

const b2 = await Box2DFactory();

const worldDef = b2.b2DefaultWorldDef();
worldDef.gravity.y = -10;
const worldId = b2.b2CreateWorld(worldDef);
worldDef.delete();

const bodyDef = b2.b2DefaultBodyDef();
bodyDef.type = b2.b2BodyType.b2_dynamicBody;
bodyDef.position.y = 10;
const bodyId = b2.b2CreateBody(worldId, bodyDef);
bodyDef.delete();

const shapeDef = b2.b2DefaultShapeDef();
shapeDef.density = 1;
const box = b2.b2MakeBox(0.5, 0.5);
b2.b2CreatePolygonShape(bodyId, shapeDef, box);
shapeDef.delete();

for (let i = 0; i < 60; i++) b2.b2World_Step(worldId, 1 / 60, 4);

const p = b2.b2Body_GetPosition(bodyId);
console.log(p.x, p.y); // 0 4.979168891906738
p.delete();

b2.b2DestroyWorld(worldId);

Objects that embind returns or that you construct (b2WorldDef, b2Vec2, ...) are handles into wasm memory; call .delete() when you are done with them.

Fast path

Ids cross as plain integers, scalars cross directly and structs are written into wasm memory you own. After each step one call exports the pose of every body that moved.

import Box2DFactory from "@vworlds/box2d3-wasm";
import { createFastPath } from "@vworlds/box2d3-wasm/fastpath";

const b2 = await Box2DFactory();
const fp = createFastPath(b2);

// Worlds, bodies and shapes are created through the flat embind API.
const worldDef = b2.b2DefaultWorldDef();
worldDef.gravity.y = -10;
const worldId = b2.b2CreateWorld(worldDef);
worldDef.delete();
const bodyDef = b2.b2DefaultBodyDef();
bodyDef.type = b2.b2BodyType.b2_dynamicBody;
const bodyId = b2.b2CreateBody(worldId, bodyDef);
bodyDef.delete();
const shapeDef = b2.b2DefaultShapeDef();
shapeDef.density = 1;
b2.b2CreatePolygonShape(bodyId, shapeDef, b2.b2MakeBox(0.5, 0.5));
shapeDef.delete();

// Packed ids for the raw calls.
const world = fp.world(worldId);
const index1 = bodyId.index1;
const rev = fp.rev(bodyId);

const moves = fp.createMoveExporter(256);
const M = fp.layout.move;
for (let i = 0; i < 60; i++) {
  fp.module._b2x_World_Step(world, 1 / 60, 4);
  const count = moves.exportMoves(world, 0); // also refreshes the typed-array views
  for (let n = 0, base = moves.base; n < count; n++, base += moves.stride) {
    const y = fp.f32[base + (M.y >> 2)];
    if (i === 59) console.log("y", y); // y -5.020830154418945
  }
}

// A struct result goes into memory you own.
const out = fp.alloc(64);
fp.module._b2x_Body_GetPose(index1, rev, out);
console.log(fp.f32[(out + fp.layout.pose.y) >> 2]);
fp.free(out);

The full reference (exports, memory-growth rule, layouts, events, profile timers) is in docs/fastpath.md.

Flavours

dist/ holds two builds of the same code:

| flavour | built with | used when | |---|---|---| | deluxe | WebAssembly SIMD (-msimd128 -msse2) | the runtime validates a SIMD module | | compat | BOX2D_DISABLE_SIMD=ON | otherwise |

dist/entry.mjs (the "." export) checks WebAssembly.validate on a tiny SIMD module and imports one of them. In the scenes of npm run test:reference both flavours produce the same hashes. Both carry the fast path, and @vworlds/box2d3-wasm/fastpath works with either.

Build

Requirements: emsdk 4.0.18, TypeScript 5 as tsc (emcc --emit-tsd breaks on TypeScript 6), cmake, make, git, gawk, Node.js (and Python 3 for npm run gen, gen:check and check:bindings). The output depends on the emsdk version; do not change it together with anything else.

git clone --recurse-submodules https://github.com/vworlds/box2d3-wasm
cd box2d3-wasm

With Docker (nothing else to install):

npm run build:docker   # builds scripts/Dockerfile into an image on first use, then builds in it

With a local emsdk (activated in the shell, source emsdk_env.sh):

npm run build

Either writes dist/. FLAVOURS="deluxe" limits the build to one flavour; TARGET_TYPE=Debug or RelWithDebInfo builds those instead of Release. Intermediate output goes to build/.

The build applies patches/box2d/*.patch to the box2d submodule, so the submodule shows as modified afterwards; that is expected. It does not depend on where the repository is checked out: the same commit built at two different paths gives byte-identical files in dist/.

Test

npm test           # fast-path differential test + bindings test (both flavours) + integration test, on dist/
npm run test:reference # step-by-step hashes equal to the recorded reference for the pinned Box2D, embind and fast path
npm run gen:check  # the generated fast-path code is current
npm run check:bindings  # every C function is bound or excepted (see API coverage)
npm run test:release    # the release-version script (scripts/release-version.test.mjs)
npm run check:pack      # npm pack would publish only the expected files (needs dist/)

npm run gen regenerates src/fastpath_body.gen.cpp and src/fastpath_body.gen.d.ts from box2d/include/box2d/box2d.h.

Benchmark

npm run bench                              # dist/, both flavours
node bench/engine.mjs <distA> <distB>      # compare two builds (each a dist/ directory)

bench/engine.mjs times b2World_Step (1/60 s, 4 sub-steps) on five scenes modelled on Box2D's own benchmark suite: large_pyramid (5050 boxes), many_pyramids (5500 boxes in 100 islands), joint_grid (3600 bodies, ~7000 revolute joints), tumbler (2025 boxes in a motorised drum) and sparse_movers (2000 bodies that never touch). Each measurement is a fresh process and builds alternate within a round; it prints the median ms per step. Box2D 3.2 against the previous pin (3.1.1+18), Node 24, x86-64, ms per step:

| scene | deluxe 3.1.1+ | deluxe 3.2.0 | compat 3.1.1+ | compat 3.2.0 | |---|---|---|---|---| | large_pyramid | 4.24 | 2.09 (0.49×) | 7.56 | 4.63 (0.61×) | | many_pyramids | 4.06 | 1.87 (0.46×) | 7.37 | 4.18 (0.57×) | | joint_grid | 1.81 | 1.59 (0.88×) | 1.82 | 1.65 (0.91×) | | tumbler | 3.21 | 1.38 (0.43×) | 4.80 | 2.76 (0.57×) | | sparse_movers | 0.156 | 0.148 (0.95×) | 0.158 | 0.157 (0.99×) |

API coverage

Rule: every function of Box2D's public C API (the B2_API declarations in box2d.h, types.h, collision.h, math_functions.h, base.h) is callable through the flat embind API under its C name, except the ones listed with a reason in scripts/bindings-exceptions.txt. The fast path is an optional shortcut to some of them and does not count towards the rule.

Today that is 443 of 506 declared functions bound; the 63 exceptions are:

  • debug draw (b2World_Draw, b2DefaultDebugDraw), removed on purpose;
  • allocator, assert and log hooks (b2SetAllocator, b2SetAssertCallback, b2SetLogCallback, b2InternalAssert), which take C function pointers;
  • b2World_SetWorkerCount (this build has no threads);
  • b2GetMillisecondsAndReset (in/out uint64_t*; use b2GetTicks and b2GetMilliseconds), b2ComputeShapeDistance (needs the b2DistanceInput/b2DistanceOutput/b2Simplex value classes);
  • the 31 recording and replay functions and the 3 world snapshot functions (new in Box2D 3.2: byte buffers, files and raw handles; not bound yet);
  • the 19 standalone dynamic tree functions (raw pointers and callbacks);
  • b2World_DumpMemoryStats (writes a file; the build has no file system, so the call throws).

Conventions for the awkward ones:

  • User data (b2World_/b2Body_/b2Shape_/b2Joint_ SetUserData/GetUserData) is an unsigned 32-bit integer stored in the void*, not an object. It is the same storage as _b2x_Body_SetUserData / _b2x_Shape_SetUserData, so both APIs see the same value; keep your own table if you need to attach objects.
  • Names: b2Body_SetName(id, string) / b2Body_GetName(id) (Box2D 3.2 keeps at most 10 characters, B2_NAME_LENGTH).
  • Out-parameters come back as value objects: b2Joint_GetConstraintTuning(id) returns {hertz, dampingRatio}, b2DistanceJoint_GetSpringForceRange(id) returns {lowerForce, upperForce}.
  • uint64_t values (b2Shape_SetUserMaterial, b2GetTicks) are BigInt.
  • b2ContactData gained a contactId property, which b2Contact_GetData and b2Contact_IsValid take.
  • Callbacks: bound are the world queries b2World_OverlapAABB, b2World_OverlapShape, b2World_CastRay, b2World_CastShape, b2World_CollideMover (a JS function per call), b2World_SetCustomFilterCallback, b2World_SetPreSolveCallback (free the returned handle with b2World_DeleteCallback) and b2World_SetFrictionCallback / b2World_SetRestitutionCallback. Not bound: the task system and the allocator/assert hooks above, and debug draw.

npm run check:bindings enforces the rule. It parses the B2_API declarations (comments and preprocessor lines stripped, multi-line declarations handled) and the function("...") names in src/*.cpp, prints the totals per header, and exits 1 when a declared function is neither bound nor excepted, when an exception or a Box2D-looking binding names a function the headers no longer declare (so a Box2D update that removes or renames one is noticed), or when an exception is also bound. Lines starting with + in the exceptions file list package helpers that have no C twin (b2Body_GetPointer, ...). Run it after changing glue.cpp or moving the Box2D submodule.

Demo

demo/ is a small Phaser 4 site that runs this package on the main thread (flat embind API to create bodies, the b2x_ fast path to step and read poses): a pile, a joints scene, sensors and hit events, and 3500 always-moving bodies. It loads whichever flavour entry.mjs picks and shows it in the stats readout. It imports the package from this repository's dist/, so build the package first.

npm run build          # the package, into dist/
npm --prefix demo ci     # once
npm run demo:dev       # http://localhost:5173
npm run demo:build     # static site in demo/dist-site/

The demo is not part of the published package (it has its own package.json). CI builds it and deploys demo/dist-site/ to GitHub Pages from master. See demo/README.md.

CI

.github/workflows/ci.yml runs on pull requests to dev and master and on pushes to dev. It builds both flavours once, then runs gen:check, check:bindings, test:release, npm test, npm run test:reference, a check of the npm pack content, the demo build and the vecs test suites (physics, phaser-server, phaser) against the vecs commit pinned in ci/vecs-ref. The vecs repository is private: the suites need the VECS_READ_SECRET secret and are skipped, with a warning, when it is missing. .github/workflows/vecs-nightly.yml runs the same suites against vecs's dev every night and opens a pull request that moves the pin when they pass. To run them yourself:

npm pack
scripts/run-vecs-suites.sh <vecs-checkout> vworlds-box2d3-wasm-*.tgz

Releasing

Merge dev into master. .github/workflows/publish.yml builds, tests, and publishes @vworlds/box2d3-wasm to npm with provenance. If the version in package.json is not on npm yet it is released as is, otherwise the patch version is bumped (scripts/release-version.mjs, --dry-run shows the decision), committed to master with [skip ci] and tagged v<version>. For a minor or major release, set the version in package.json on dev first. pages.yml deploys the demo to GitHub Pages on the same push. docs/releasing.md lists the one-time setup.

Repository layout

| path | contents | |---|---| | src/ | glue.cpp (embind bindings), fastpath.cpp and fastpath_body.gen.cpp (b2x_ exports), fastpath.mjs / .d.ts (helper), entry.mjs / .d.ts (flavour selection), Box2D.template.d.ts | | scripts/ | build.sh, build-in-docker.sh, Dockerfile, gen_fastpath.py, check_bindings.py, bindings-exceptions.txt, modify_emscripten_dts.awk, release-version.mjs (+ test), check-pack.mjs, run-vecs-suites.sh | | .github/, ci/ | workflows, the build action, vecs-ref (the pinned vecs commit) | | patches/box2d/ | the patch applied to the Box2D submodule | | box2d/ | Box2D (git submodule) | | tests/ | fastpath-differential.mjs, bindings.mjs, integration.mjs, reference-differential/ | | bench/ | engine.mjs (engine benchmark, compares builds) | | demo/ | the Phaser 4 demo site (own package.json, not published) | | docs/ | fastpath.md, box2d-3.2-migration.md, vecs-migration.md, box2d-update.md | | dist/, build/ | build output (not committed) |

Documentation

Licence

MIT. Box2D is MIT licensed too (Erin Catto), and parts of the build output come from Emscripten and wasm-feature-detect; see THIRD_PARTY_NOTICES.md.