Gravity
A set of wrapper components for creating physics-based gravity animations with Matter.js.
components made with:
Setting up Matter.js for creating physics-based animations can be a bit tricky and cumbersome, especially with React. This component simplifies the process by wrapping your content with a physics world, and transforming your React components into Matter.js bodies.
Source code
First, install the following dependencies.
We use the matter-js
library to create the physics simulation. The poly-decomp
package is used to decompose bodies into a set of vertices, which is required for the svg
body type. The svg-path-commander
package is used to parse SVG paths and convert them into a set of vertices (since the built-in feature for this in matter-js
is outdated).
npm install matter-js @types/matter-js poly-decomp svg-path-commander
Then, create two utility functions for the component.
One is for parsing SVG paths into a set of vertices:
import SVGPathCommander from 'svg-path-commander';
// Function to convert SVG path "d" to vertices
export function parsePathToVertices(path: string, sampleLength = 15) {
// Convert path to absolute commands
const commander = new SVGPathCommander(path);
const points: { x: number, y: number }[] = [];
let lastPoint: { x: number, y: number } | null = null;
// Get total length of the path
const totalLength = commander.getTotalLength();
let length = 0;
// Sample points along the path
while (length < totalLength) {
const point = commander.getPointAtLength(length);
// Only add point if it's different from the last one
if (!lastPoint || point.x !== lastPoint.x || point.y !== lastPoint.y) {
points.push({ x: point.x, y: point.y });
lastPoint = point;
}
length += sampleLength;
}
// Ensure we get the last point
const finalPoint = commander.getPointAtLength(totalLength);
if (lastPoint && (finalPoint.x !== lastPoint.x || finalPoint.y !== lastPoint.y)) {
points.push({ x: finalPoint.x, y: finalPoint.y });
}
return points;
}
The other is for calculating the position of an element based on its container, and a posiiton value
export function calculatePosition(
value: number | string | undefined,
containerSize: number,
elementSize: number
) {
if (typeof value === "string" && value.endsWith("%")) {
const percentage = parseFloat(value) / 100;
return containerSize * percentage;
}
return typeof value === "number"
? value
: elementSize - containerSize + elementSize / 2;
}
Then, create the component:
Usage
First, you need to wrap your scene / content with the Gravity
component. Set the gravity direction vector in the gravity
prop (default is {x: 0, y: 1}
). Then, in order to transform your regular HTML elements into Matter bodies, you need to wrap them with the MatterBody
component. You need to set each bodies x
and y
position, either as a percentage of your container size, or as a number. You do not need to set the width and height manually, everything else is taken care of by component :). High-level example:
<Gravity>
<MatterBody x="50%" y="50%">
<div>Hello world!</div>
</MatterBody>
<MatterBody x="10%" y="10%">
<div>fancy!</div>
</MatterBody>
</Gravity>
Understanding the component
Gravity component
At its core, the Gravity component creates and manages a Matter.js physics world. It handles:
-
Physics Setup: Creates a canvas and initializes the Matter.js physics engine with:
- A physics engine to calculate forces and collisions
- A renderer to visualize the physics (when debug mode is enabled)
- A runner to step the physics simulation forward
- Mouse constraints to enable dragging of elements
-
Animation Loop: Continuously updates the positions of your HTML elements to match their physics bodies in the Matter.js world. This creates the illusion that your DOM elements are actually affected by physics.
-
Controls: Exposes three main methods:
start()
: Begins the physics simulationstop()
: Pauses the physics simulationreset()
: Returns all elements to their starting positions
-
Debug Mode: When enabled via the
debug
prop, shows the actual Matter.js physics bodies as overlays, which is super helpful for development.
MatterBody component
The MatterBody
component transforms regular HTML elements into physics-enabled elements. Key features:
- Positioning: Set initial positions with
x
andy
props
<MatterBody x="50%" y="100px">
<div>I'm physics-enabled!</div>
</MatterBody>
-
Body Types: Choose between different physics shapes:
rectangle
: Default, good for most elementscircle
: Perfect for round elementssvg
: For complex custom shapes
-
Physics Properties: Customize how elements behave with
matterBodyOptions
. The most commonly used options are:
<MatterBody
matterBodyOptions={{
friction: 0.5, // How slippery it is
restitution: 0.7, // How bouncy it is
density: 0.001, // How heavy it is
isStatic: false, // If true, the element won't move but can be collided with
force: { x: 0, y: 0 } // Initial force applied to the body
... // More options
}}
>
<div>I'm bouncy!</div>
</MatterBody>
For a complete list of options, check out the Matter.js Body documentation. You can fine-tune everything from angular velocity to mass to create exactly the physics behavior you want.
Context
The components use React Context to communicate. When you wrap an element with MatterBody
, it registers itself with the parent Gravity
component. The registration process:
- Creates a Matter.js physics body matching your element's size and shape
- Adds the body to the physics world
- Sets up a sync system where the HTML element's position updates to match its physics body
Examples
Non-draggable bodies
By default, the MatterBody makes its element draggable. You can disable this behavior by setting the isDraggable
prop to false
. (Under the hood, we just add back the pointer-events to the elements, so they will be clickable, hover-able, etc, but the Matter body underneath will not receive any pointer events). This can be handy to create creative footers with clickable links for example:
CONTACT
Different body types
With the bodyType
prop, you can choose between different types of bodies. The available types are circle
, rectangle
, and svg
.
In this example, we have a mixed of circle
and rectangle
bodies. Again, you do not need to define the sizes on the MatterBody
component, you can define them on your component level, eg. adding w-12 h12
to your tailwind classes. Then, the component will calculate the size for the matter.js engine.
icons from lucide.dev
SVGs
The third bodyType
option is svg
, which allows you to create physics bodies from SVG elements. This is particularly useful for creating custom-shaped physics objects that match your SVG graphics.
Here's how it works:
- The component takes your SVG element and extracts the path data
- It converts the path into a series of vertices (points) that outline the shape (with a custom converter using the
svg-path-commander
package) - These vertices are then converted into polygons by matter.js (with the help of the
poly-decomp
package). - The resulting polygons are then used to create Matter.js bodies
As you can see in the demo above, SVG bodies can produce varying results. Simple shapes like the stars translate well, maintaining their shapes in the physics simulation. More complex shapes like the fancy text at the bottom (which is an SVG path, and not an HTML element) end up with rougher approximations.
This variance in quality stems from the challenging process of converting SVG paths to physics bodies. Therefore, there are a few caveats to keep in mind:
-
SVG Requirements:
- Keep them simple. The simpler the SVG, the better the decomposition, and the simulation.
- It's only tested with single-path SVGs, and it probably won't work with nested paths.
- Avoid shapes with holes or complex curves, or shapes that are seem to be too complex to decompose into polygons.
-
Performance Impact:
- Complex SVGs create more detailed physics bodies, which can slow down the simulation
- More vertices mean more calculations
- The initial path-to-vertices conversion can be slow.
If you're not getting the desired results, you have several options:
- Break down complex SVGs into simpler shapes
- Use basic physics bodies (rectangles/circles) with the SVG as a visual overlay
- Fine-tune the vertex sampling with the
sampleLength
prop
While the demo's fancy text on the bottom worked well by chance for me, you more than likely will need to experiment with different settings to get the desired results. Use the debug
prop to visualize the physics bodies and their vertices, and adjust the sampleLength
prop to control the accuracy of the conversion.
For more details on the decomposition process, refer to the poly-decomp documentation, the Matter.js documentation, and to the SVG path commander documentation.
Props
Gravity
Prop | Type | Default | Description |
---|---|---|---|
children* | React.ReactNode | - | The content to be displayed and animated |
debug | boolean | false | Whether to show the physics bodies and their vertices |
gravity | { x: number; y: number } | { x: 0, y: 1 } | The direction of gravity |
resetOnResize | boolean | true | Whether to reset the physics world when the window is resized |
grabCursor | boolean | true | Whether to show grab/grabbing cursor when interacting with bodies |
addTopWall | boolean | true | Whether to add a wall at the top of the canvas |
autoStart | boolean | true | Whether to automatically start the physics simulation |
className | string | - | Additional CSS classes to apply to the container |
MatterBody
Prop | Type | Default | Description |
---|---|---|---|
children* | React.ReactNode | - | The content to be displayed and animated |
matterBodyOptions | Matter.IBodyDefinition | { friction: 0.1, restitution: 0.1, density: 0.001, isStatic: false } | Matter.js body configuration options |
bodyType | "rectangle" | "circle" | "svg" | "rectangle" | The type of physics body to create |
isDraggable | boolean | true | Whether the body can be dragged with the mouse |
sampleLength | number | 15 | The sampling distance for SVG path vertices |
x | number | string | 0 | Initial x position (can be percentage string) |
y | number | string | 0 | Initial y position (can be percentage string) |
angle | number | 0 | Initial rotation angle in degrees |
className | string | - | Additional CSS classes to apply to the container |