Technical notes for the Gravit browser physics sandbox.
Gravit is a client-side browser app built with plain HTML, CSS, and JavaScript. The main workspace is an HTML canvas, with the UI layered over it using fixed-position controls.
The project does not rely on a packaged physics engine. The physics, collision detection, collision response, rendering, camera, terrain, history, hotkeys, and settings systems are all implemented locally in JavaScript.
At a high level, Gravit is organized around:
gravit/
├── index.html
├── styles.css
├── app.js
├── core.js
├── math.js
├── geometry.js
├── fixtures.js
├── bodies.js
├── physics.js
├── integrator.js
├── collision-detection.js
├── collision-response.js
├── terrain.js
├── renderer.js
├── camera.js
├── grid.js
├── history.js
├── hotkeys.js
├── settings.js
├── ui/
│ ├── input.js
│ └── toolbar.js
└── shapes/
├── registry.js
├── circle.js
├── box.js
├── triangle.js
├── oval.js
├── octagon.js
└── l-block.js
| Area | Files | Purpose |
|---|---|---|
| App bootstrap | app.js |
Starts the app, initializes controls, loads settings, binds input, and runs the animation loop |
| Shared state | core.js |
Stores DOM references and the main mutable world state |
| Math helpers | math.js |
Vector math, dot/cross products, rotation, clamping, and segment helpers |
| Geometry helpers | geometry.js |
Polygon axes, projections, shape vertices, hit tests, and drawing helpers |
| Fixtures | fixtures.js |
Collision primitives, transforms, AABBs, hit tests, and inertia approximations |
| Bodies | bodies.js |
Dynamic body creation, selection, deletion, retrieval, and inspector sync |
| Physics loop | physics.js |
Coordinates integration, grabbing, collisions, and boundary handling |
| Integration | integrator.js |
Applies gravity, velocity, angular motion, grab forces, and bounds behavior |
| Collision detection | collision-detection.js |
Detects fixture-pair collisions and builds contact manifolds |
| Collision response | collision-response.js |
Resolves contacts with positional correction, impulses, bounce, friction, and angular response |
| Terrain | terrain.js |
Creates static terrain segments and handles erase behavior |
| Rendering | renderer.js |
Draws the world, bodies, terrain, drafts, grid, debug overlays, and navigation widget |
| Camera | camera.js |
Handles pan, zoom, rotation, screen/world coordinate conversion, and view constraints |
| Grid | grid.js |
Handles grid snapping and snapped pointer helpers |
| History | history.js |
Snapshot-based undo/redo |
| Hotkeys | hotkeys.js |
Configurable command hotkeys |
| Settings | settings.js |
Saves UI, camera, grid, gravity, and tool preferences to local storage |
| UI input | ui/input.js |
Pointer, wheel, keyboard, canvas, and navigation-widget interactions |
| Toolbar UI | ui/toolbar.js |
Toolbar controls, inspector controls, display settings, and point-terrain actions |
| Shapes | shapes/* |
Shape registration, draft previews, drawing, and fixture configuration |
The app is centered around a shared state object. This object tracks the current world, UI settings, camera state, selected object, active tool, pointer position, history, and physics objects.
Important state areas include:
This keeps the app simple to reason about: most systems read from and write to the same world state.
Gravit renders with requestAnimationFrame, but physics uses a fixed timestep.
The loop caps large frame gaps, accumulates elapsed time, advances physics at 1 / 180 second intervals, then renders the current state.
const dt = 1 / 180;
while (state.accumulator >= dt) {
step(dt);
state.accumulator -= dt;
}
draw();
requestAnimationFrame(loop);
This is more stable than using raw frame time directly for physics, especially when the browser has inconsistent frame pacing.
Each physics step follows this general order:
1. Clear previous contact data
2. Integrate body movement
3. Apply active grab/drag forces
4. Solve collisions
5. Apply boundary behavior
The main step function is intentionally small:
function step(dt) {
state.contacts = [];
integrateBodies(dt);
applyGrab(dt);
solveCollisions();
keepBodiesNearWorld();
}
Collisions are solved in multiple passes:
for (let iteration = 0; iteration < 5; iteration += 1) {
solveFixtureCollisions(iteration);
}
The first pass also records contact data for the debug overlay.
Bodies store the physical state of objects in the world.
A body contains:
Fixtures define the collision geometry attached to a body.
Supported fixture types:
This separation makes it possible for a body to have multiple collision shapes. For example, the L-block shape is built as a compound body using multiple polygon fixtures.
Shapes are registered through a small shape registry.
Each shape module provides:
typelabelconfigure()getDraftSpec()draw()drawDraft()Implemented shape modules include:
This keeps shape-specific behavior out of the main physics and rendering systems. Adding a new shape means adding a new module that registers itself and defines its fixtures, draft behavior, and drawing logic.
Collision detection is fixture-pair based.
The system first performs broad rejection using AABBs, then runs more specific fixture collision logic.
Supported collision paths include:
Polygon collision uses projection axes, similar to a Separating Axis Theorem approach. The engine projects shapes onto candidate axes, checks for overlap, and uses the smallest overlap as the contact depth.
Collision results are represented as manifolds containing:
Collision response uses impulse resolution.
The solver applies:
Bounce is based on the lower bounce value of the two colliding bodies.
Friction is based on the geometric mean of both body friction values.
The response also accounts for angular motion by calculating velocity at the contact point, not just the center of the body.
Movement integration applies gravity and updates linear and angular motion.
Each dynamic body:
Gravity is scaled from a user-facing value into canvas units using a metersToPixels value.
The gravity mode can be either:
Gravit supports multiple edge behaviors.
Bodies leaving one side of the world reappear on the opposite side.
Bodies collide with the world edges and reverse velocity based on their bounce value.
Bodies are clamped inside the world and their edge velocity is zeroed.
Bodies that leave the world are reset near the top-center of the screen.
Gravit supports several object interaction styles.
Moves the body directly toward the pointer and derives velocity from pointer movement.
Applies a spring-like force toward the pointer.
Applies force at the grabbed anchor point, allowing the object to rotate around that point.
Dampens the body while held, then applies an impulse when released.
Holding Shift forces direct dragging.
Terrain is represented as static segment fixtures.
Both freehand terrain and point-to-point terrain eventually become connected line segments. Each segment is stored as a static body with a segment fixture.
The erase tool removes terrain or bodies near the pointer using a fixed erase radius.
Rendering is handled through the canvas.
The draw order is:
1. Clear canvas
2. Apply camera transform
3. Draw grid
4. Draw terrain
5. Draw bodies
6. Draw spawn preview
7. Draw terrain preview
8. Draw grab guide
9. Draw debug world overlay
10. Restore camera transform
11. Draw navigation widget
12. Draw debug text
The renderer uses the same body, fixture, and shape data used by the physics system. This keeps the visual shape close to the collision shape.
The camera system supports:
The camera transform is applied before drawing world objects.
The app also includes a small navigation widget that acts like a minimap and camera control surface.
The grid system supports:
Snap behavior is split by tool so terrain, shape placement, and dragging can be controlled separately.
Undo and redo are snapshot-based.
The history system serializes:
Actions record a before and after snapshot.
Examples of history-tracked actions:
The current history limit is 50 entries.
Gravit stores local preferences in browser localStorage.
Saved settings include:
Hotkey bindings are also stored locally.
Current state: usable prototype.
Implemented pieces include: