Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sanchedev/fraxel/llms.txt

Use this file to discover all available pages before exploring further.

Collision detection in Fraxel is automatic — you declare <collider> nodes with shapes and groups in JSX, then subscribe to events via useEvent(). The engine runs a two-phase pipeline every frame: a spatial-hash broadphase narrows down candidate pairs, then a narrowphase performs exact shape overlap tests. For physics simulation with gravity and forces, add a <rigid-body> node alongside the collider. See the Physics guide for a full walkthrough.

shapes factory

The shapes factory creates typed collision shape descriptors. Pass the result directly to the shape prop of a <collider> node.
import { shapes } from 'fraxel'

<collider shape={shapes.rectangle(32, 48)} group={['player']} collidesWith={['enemy']} />
<collider shape={shapes.circle(16)} group={['projectile']} collidesWith={['zombie']} />

shapes.rectangle(width, height)

width
number
required
Width of the rectangle in pixels.
height
number
required
Height of the rectangle in pixels.
Returns RectangleShape:
PropertyTypeDescription
type'rectangle'Discriminant — always 'rectangle'
sizeVector2Width (size.x) and height (size.y) of the rectangle

shapes.circle(radius)

radius
number
required
Radius of the circle in pixels.
Returns CircleShape:
PropertyTypeDescription
type'circle'Discriminant — always 'circle'
radiusnumberRadius of the circle in pixels

Shape union type

type Shape = RectangleShape | CircleShape
Use shape.type to discriminate:
function getArea(shape: Shape): number {
  if (shape.type === 'rectangle') {
    return shape.size.x * shape.size.y
  }
  return Math.PI * shape.radius * shape.radius
}

Collider Groups

Every <collider> node has two group arrays that control which colliders interact:
PropTypeDescription
groupstring[]The groups this collider belongs to
collidesWithstring[]The groups this collider reacts to
A collision pair is processed only when at least one group from collider A’s collidesWith list matches a group in collider B’s group list. Both colliders receive the collision events — you don’t need symmetric declarations.
// The player reacts to enemies
<collider shape={shapes.rectangle(24, 32)} group={['player']} collidesWith={['enemy', 'hazard']} />

// The enemy reacts to projectiles only
<collider shape={shapes.rectangle(24, 32)} group={['enemy']} collidesWith={['projectile']} />
group and collidesWith values are read-only after construction. Set them as JSX props — they cannot be changed at runtime.

Collision Events

Three events are emitted on a Collider node during each frame’s collision pass. Subscribe with useEvent():
import { useEvent, useNode } from 'fraxel/hooks'
import { PrimaryNode, shapes } from 'fraxel'

function Projectile() {
  const collider = useNode(PrimaryNode.Collider)

  useEvent(collider, 'colliderEntered', (otherCollider) => {
    // Fires once — the first frame the overlap begins
    otherCollider.parent.script.applyDamage(20)
  })

  useEvent(collider, 'collided', (otherCollider) => {
    // Fires every frame while the overlap persists
  })

  useEvent(collider, 'colliderExited', (otherCollider) => {
    // Fires once — the first frame the overlap ends
  })

  return (
    <collider
      ref={collider}
      shape={shapes.circle(4)}
      group={['projectile']}
      collidesWith={['enemy']}
    />
  )
}
EventCallback signatureWhen it fires
colliderEntered(otherCollider: Collider) => voidOnce — first frame two colliders begin overlapping
collided(otherCollider: Collider) => voidEvery frame — while two colliders remain overlapping
colliderExited(otherCollider: Collider) => voidOnce — first frame two colliders stop overlapping
Events are dispatched on both colliders in a pair. If A enters B, both A.colliderEntered(B) and B.colliderEntered(A) fire, provided the group filters allow it.

RayCast

A <ray-cast> node projects a ray from its global position in a given direction and detects the nearest collider it intersects.
import { useNode, useEvent } from 'fraxel/hooks'
import { PrimaryNode, Vector2 } from 'fraxel'

function GroundDetector() {
  const ray = useNode(PrimaryNode.RayCast)

  useEvent(ray, 'colliderEntered', (collider) => {
    console.log('Ray hit:', collider)
  })

  useEvent(ray, 'colliderExited', (collider) => {
    console.log('Ray cleared:', collider)
  })

  return (
    <ray-cast
      ref={ray}
      direction={new Vector2(0, 64)}
      collidesWith={['ground']}
    />
  )
}

RayCast props

direction
Vector2
required
The ray’s direction and length as a Vector2. The ray extends from the node’s global position to globalPosition + direction. A direction of new Vector2(0, 64) casts 64 pixels downward.
collidesWith
string[]
required
Groups the ray tests against. Only colliders whose group intersects this list are considered.

RayCast methods

MethodReturnsDescription
getCollider()Collider | nullReturns the currently detected nearest collider, or null if nothing is hit

RayCast events

EventCallback signatureWhen it fires
colliderEntered(collider: Collider) => voidFirst frame a new collider is hit (or the hit target changes)
colliderExited(collider: Collider) => voidFirst frame the previously hit collider is no longer detected
Raycasts query collider groups directly (bypassing the spatial hash broadphase). This ensures raycasts work correctly even when the spatial hash hasn’t been rebuilt for a given frame. Only collider-vs-collider detection uses the spatial hash.

Narrowphase

The Narrowphase class performs precise shape-vs-shape intersection tests. It is used internally by CollisionSystem, but is also available for manual queries.

Narrowphase.detect(a, b)

import { Narrowphase } from 'fraxel'

const hit = Narrowphase.detect(playerCollider, enemyCollider)
if (hit) console.log('Shapes overlap!')
a
Collider
required
The first collider.
b
Collider
required
The second collider.
Returns booleantrue if the two shapes overlap. The method dispatches across all four shape combinations:
PairAlgorithm
rectanglerectangleAABB overlap (four boundary comparisons)
circlecircleSum-of-radii distance test
rectanglecircleClosest point on AABB to circle centre
circlerectangleSame as above, arguments swapped

PhysicsSystem

PhysicsSystem is a singleton that applies gravity, integrates velocity into position, and resolves physics collisions each frame. It runs automatically after CollisionSystem.update() in the game loop.

PhysicsSystem.gravity

import { PhysicsSystem, Vector2 } from 'fraxel'

// Read current gravity
console.log(PhysicsSystem.gravity) // Vector2 { x: 0, y: 980 }

// Override — e.g. low-gravity level
PhysicsSystem.gravity = new Vector2(0, 200)

// Zero gravity — space level
PhysicsSystem.gravity = new Vector2(0, 0)
PropertyTypeDefaultDescription
PhysicsSystem.gravityVector2new Vector2(0, 980)Global gravity vector applied to every non-static body that has useGravity enabled
The default 980 is pixels/second² — tuned to feel realistic at typical game scales. Adjust it freely to match your scene’s pixel density.

PhysicsBody

PhysicsBody is the kinematic state container attached to a <rigid-body> node. Access it via body.node.physicsBody:
import { useNode } from 'fraxel/hooks'
import { PrimaryNode, Vector2 } from 'fraxel'

function Player() {
  const body = useNode(PrimaryNode.RigidBody)

  function jump() {
    // Instant upward velocity change
    body.node.physicsBody.applyImpulse(new Vector2(0, -400))
  }

  function applyWind() {
    // Continuous rightward force (accumulated this frame)
    body.node.physicsBody.applyForce(new Vector2(50, 0))
  }

  return (
    <transform>
      <sprite textureId={PLAYER_TEX} />
      <collider shape={shapes.rectangle(24, 32)} group={['player']} collidesWith={['ground']} />
      <rigid-body ref={body} mass={1} bounce={0.2} />
    </transform>
  )
}

Methods

applyForce(force)

force
Vector2
required
Force vector in pixels/second². Accumulated into the body’s internal acceleration buffer and applied during the next physics integration step. Resets to zero after each frame — call it every frame for continuous effects like thrust or wind.

applyImpulse(impulse)

impulse
Vector2
required
Impulse vector in pixels/second. Added directly to velocity — instant effect, no accumulation. Ideal for one-shot events like jumps, explosions, or knockback. Has no effect on static bodies (isStatic === true).

setVelocity(v)

v
Vector2
required
Sets the body’s velocity directly, overriding any accumulated velocity. Use for teleport-style resets or scripted movement.

PhysicsBody properties

PropertyTypeDefaultDescription
velocityVector2(0, 0)Current velocity in pixels/second
massnumber1Body mass — higher values resist forces and impulses more
frictionnumber0.1Friction coefficient (0–1) applied on collision contact
bouncenumber0Restitution coefficient (0 = no bounce, 1 = perfect elastic)
isStaticbooleanfalseIf true, the body does not move and has infinite effective mass
useGravitybooleantrueIf false, global gravity is not applied to this body
isGroundedbooleanfalseSet by the resolver — true when the body is resting on a surface

rigid-body JSX props

Configure PhysicsBody properties directly as JSX attributes on <rigid-body>:
<rigid-body
  mass={1}
  friction={0.3}
  bounce={0.6}
  isStatic={false}
  useGravity={true}
/>

Spatial Hash (broadphase)

The SpatialHash divides the world into a fixed-size grid and tracks which colliders occupy each cell. It is used internally by CollisionSystem for broadphase candidate culling — you do not interact with it directly in normal game code.
The spatial hash only handles collider-vs-collider broadphase. Raycasts query #colliderGroups directly, bypassing the hash entirely.
The hash is marked dirty whenever a collider’s position changes (CollisionSystem.setDirty()) and is rebuilt at the start of each frame’s collision pass.

Related guides

  • Physics guide — rigid bodies, gravity, forces, and collision response
  • Nodes 2D API<collider>, <rigid-body>, and <ray-cast> node props
  • Math APIVector2 used for direction and applyForce

Build docs developers (and LLMs) love