Skip to content

Getting started

This guide takes you from an empty project to a bouncing ball rendered on screen.

Terminal window
pnpm add jolt-ts
# or: npm install jolt-ts / yarn add jolt-ts

jolt-ts ships the compiled Jolt WASM builds inside the package, so there is nothing else to download. The default wasm-compat build embeds the WASM binary directly in its JavaScript, which means it works without any asset copying or locateFile configuration — ideal for bundlers and static sites. See Loading & WASM builds for the other variants.

Everything starts with a World. Creating one is asynchronous because it initializes the WASM runtime the first time.

import { World, Body, Shape } from "jolt-ts";
const world = await World.create({
gravity: [0, -9.81, 0],
});

World.create() loads the default wasm-compat build for you. To reuse one runtime across many worlds (recommended if you create more than one), load it once and pass it in:

import { loadJolt, World } from "jolt-ts";
const runtime = await loadJolt(); // wasm-compat by default
const world = await World.create({ runtime });

A Body is a rigid body with a Shape. Create bodies from a plain options object…

const floor = world.createBody({
type: "static",
shape: Shape.box({ halfExtents: [50, 0.5, 50] }),
position: [0, -0.5, 0],
layer: "static",
});

…or with the fluent Body descriptor builder, whichever reads better:

const ball = world.createBody(
Body.dynamic()
.shape(Shape.sphere(0.5))
.translation(0, 8, 0)
.layer("moving")
.restitution(0.5),
);

Both styles accept the same fields. Object literals are easy to serialize and generate; the builder is nice for handwritten code. See Bodies & motion.

Advance the world by a fixed timestep. A real app runs this from a fixed-timestep loop; for a quick test, just call it:

for (let i = 0; i < 120; i++) {
world.step(1 / 60);
}
console.log(ball.translation()); // → { x, y, z }, now resting on the floor

step(deltaTime, collisionSteps?) runs Jolt’s update. Use a fixed deltaTime (e.g. 1/60) for stable, reproducible physics — don’t feed it a variable frame delta directly.

Bodies expose plain-JS getters, plus zero-allocation …Into() variants for hot loops:

const p = ball.translation(); // { x, y, z }
const r = ball.rotation(); // { x, y, z, w }
// Hot-loop friendly: write into a buffer you own.
const position = new Float32Array(3);
ball.translationInto(position);

jolt-ts is renderer-agnostic — it only computes physics. To draw a frame, copy each body’s transform onto a mesh. Here is the essence of what every demo on this site does:

import * as THREE from "three";
import { World, Shape } from "jolt-ts";
const world = await World.create({ gravity: [0, -9.81, 0] });
world.createBody({ type: "static", shape: Shape.box({ halfExtents: [25, 0.5, 25] }), position: [0, -0.5, 0], layer: "static" });
const ball = world.createBody({ type: "dynamic", shape: Shape.sphere(0.5), position: [0, 6, 0], layer: "moving" });
const mesh = new THREE.Mesh(new THREE.SphereGeometry(0.5), new THREE.MeshStandardMaterial());
scene.add(mesh);
const position: [number, number, number] = [0, 0, 0];
const rotation: [number, number, number, number] = [0, 0, 0, 1];
function frame() {
world.step(1 / 60);
ball.translationInto(position);
ball.rotationInto(rotation);
mesh.position.set(...position);
mesh.quaternion.set(...rotation);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
frame();

When you’re done with a world, dispose it. That releases every native object it owns — bodies, shapes, query collectors, and the Jolt interface itself.

world.dispose();

There is no per-body or per-shape bookkeeping to remember: world.dispose() cleans up everything the world created. If you use using, World also implements Symbol.dispose:

{
using world = await World.create();
// …
} // world.dispose() runs automatically here