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.

Utility nodes handle time, audio, animation playback, and logical grouping. Unlike the 2D nodes, utility nodes are non-spatial — they do not extend Node2D and have no world position. They extend the base Node class directly, which means they inherit id, zIndex, deltaIncrease, lifecycle events (started, updated, destroyed), and tree methods (addChild, destroy, etc.) but have no position or globalPosition.

Nodes

Class: Group · Enum: PrimaryNode.Group · JSX tag: <group>Group is a lightweight logical container. It organises child nodes in the hierarchy without contributing any spatial offset. Unlike <transform>, it does not translate its children — use it purely for tree structure, batch-destruction, shared deltaIncrease, or script attachment.<List> uses Transform internally as an anchor for keyed reconciliation when rendering arrays of nodes.
import { useNode, useMount } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function EnemyWave() {
  const wave = useNode(PrimaryNode.Group)

  useMount(() => {
    // Destroy all enemies in the wave at once
    wave.node.destroy()
  })

  return (
    <group ref={wave}>
      <Enemy position={[100, 200]} />
      <Enemy position={[200, 200]} />
      <Enemy position={[300, 200]} />
    </group>
  )
}
Slowing a subset of nodes:
// All children of this group run at half speed
<group deltaIncrease={0.5}>
  <SlowProjectile />
  <SlowEnemy />
</group>

Props

id
string | symbol
Node identifier for child() path lookups.
zIndex
number
Z-order. Higher values render on top of siblings.
deltaIncrease
number
Speed multiplier for this node and all descendants. Defaults to 1.
children
Node[]
Child nodes to nest inside this group.

Events

EventuseEvent nameCallbackDescription
started'started'() => voidFires once when the group and all its children have started.
updated'updated'(delta: number) => voidFires every frame.
destroyed'destroyed'() => voidFires when the group (and all children) are destroyed.

Notes

  • Group has no visual representation and no position property.
  • For nodes that need a shared world position, use <transform> instead.
Class: Timer · Enum: PrimaryNode.Timer · JSX tag: <timer>Timer counts up from 0 to duration (in seconds) and fires a timeout event when it completes. Each frame the internal counter advances by delta (the elapsed time in seconds since the last frame), so timers are framerate-independent. The timeChanged event fires every frame while the timer is running, providing the current elapsed value.See the Nodes guide for usage patterns.
import { useNode, useEvent } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function CooldownTimer() {
  const timer = useNode(PrimaryNode.Timer)

  useEvent(timer, 'timeout', () => {
    console.log('Cooldown finished!')
  })

  useEvent(timer, 'timeChanged', (elapsed) => {
    // elapsed is the current elapsed time in seconds
    updateCooldownUI(elapsed)
  })

  return <timer ref={timer} duration={5} autoPlay />
}
Starting a timer on demand:
import { useNode, useMount, useEvent } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function RespawnTimer() {
  const timer = useNode(PrimaryNode.Timer)

  useEvent(timer, 'timeout', () => {
    spawnPlayer()
  })

  function onPlayerDied() {
    timer.node.play(0) // start from the beginning
  }

  return <timer ref={timer} duration={3} />
}

Props

duration
number | Reactive<number>
required
Total duration in seconds. When the elapsed time reaches this value, timeout fires and the timer stops. Accepts a SignalGetter<number> for reactive duration.
autoPlay
boolean
When true, the timer starts immediately when the node is created. Defaults to false.
id
string | symbol
Node identifier for child() path lookups.
deltaIncrease
number
Speed multiplier. 2 makes the timer count at double speed. Defaults to 1.

Events

EventuseEvent nameCallbackDescription
timeout'timeout'() => voidFires once when elapsed >= duration. The timer stops automatically.
timeChanged'timeChanged'(elapsed: number) => voidFires every frame while the timer is playing. elapsed is the current time in seconds.

Methods

MethodSignatureDescription
play(from?: number) => voidStarts or resumes the timer. Optional from sets the starting time in seconds (clamped to [0, duration]).
pause() => voidPauses the timer without resetting elapsed time.
stop() => voidStops the timer and resets elapsed time to 0.

Instance properties

PropertyTypeDescription
durationnumberCurrent duration in seconds. Mutable.
Class: AudioPlayer · Enum: PrimaryNode.AudioPlayer · JSX tag: <audio-player>AudioPlayer plays audio buffers loaded with loadSound(). Internally it manages a Web Audio API AudioBufferSourceNode and a GainNode for volume control. The AudioContext is created lazily on first use (required by browser autoplay policies).For full audio documentation see the Audio guide.
import { useNode, useEvent } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'
import { loadSound } from 'fraxel/assets'

const SHOOT_SFX = await loadSound('/assets/shoot.mp3')

function Gun() {
  const audio = useNode(PrimaryNode.AudioPlayer)
  const clickable = useNode(PrimaryNode.Clickable)

  useEvent(clickable, 'clicked', () => {
    audio.node.play()
  })

  return (
    <sprite ref={clickable} textureId={GUN_TEXTURE}>
      <clickable size={[48, 48]} />
      <audio-player ref={audio} soundId={SHOOT_SFX} volume={0.8} />
    </sprite>
  )
}
One-shot sound with persistUntilEnd:
function Explosion({ position }: { position: VectorLike }) {
  const explode = useNode(PrimaryNode.Transform)
  const collider = useNode(PrimaryNode.Collider)
  const audio = useNode(PrimaryNode.AudioPlayer)

  useEvent(collider, 'colliderEntered', () => {
    audio.node.play()
    explode.node.destroy() // node is destroyed, but audio survives until end
  })

  return (
    <transform ref={explode} position={position}>
      <collider ref={collider} shape={shapes.circle(24)} group={['explosion']} collidesWith={['enemy']} />
      <audio-player ref={audio} soundId={EXPLOSION_SFX} persistUntilEnd />
    </transform>
  )
}

Props

soundId
symbol
required
The symbol returned by loadSound(). The sound buffer must be loaded before playback is attempted.
loop
boolean
When true, the sound loops indefinitely until stop() is called. Defaults to false.
volume
number
Playback volume 01. 0 = silent, 1 = full volume. Defaults to 1.
playbackRate
number
Playback speed multiplier. 1 = normal speed, 2 = double speed, 0.5 = half speed. Defaults to 1.
persistUntilEnd
boolean
When true, calling destroy() on the node while audio is playing defers actual destruction until playback ends. Useful for one-shot sounds on components that self-destruct. Defaults to false.

Events

EventuseEvent nameCallbackDescription
ended'ended'() => voidFires when non-looping playback reaches the end of the buffer.
error'error'(err: Error) => voidFires if playback fails (e.g. AudioContext unavailable, buffer missing).

Methods

MethodSignatureDescription
play(offset?: number) => voidStarts playback. Optional offset sets the start time in seconds. Resumes from the pause position if called after pause().
pause() => voidPauses playback, preserving the current position for resumption via play().
stop() => voidStops playback and resets position to 0.

Instance properties (getters/setters)

PropertyTypeDescription
isPlayingbooleantrue if audio is currently playing (read-only).
volumenumberGet/set the playback volume. Updates the GainNode immediately if playing.
playbackRatenumberGet/set the playback rate. Updates the AudioBufferSourceNode immediately if playing.
Class: AnimationPlayer · Enum: PrimaryNode.AnimationPlayer · JSX tag: <animation-player>AnimationPlayer drives frame-based animations. It maintains a map of named Animation objects, each containing a list of keyframe functions and an FPS value. The player advances through keyframes each frame based on delta * fps, making animations framerate-independent.The animations prop is a function called at node start — not at construction time. This deferred evaluation allows keyframes to reference sibling Sprite nodes that may not yet exist when the scene JSX is first evaluated.The currentAnim prop accepts both a static string and a reactive SignalGetter<string>, enabling declarative animation switching via signals.For full animation documentation see the Animation guide.
import { useNode, useComputed, useSignal } from 'fraxel/hooks'
import { PrimaryNode, Vector2 } from 'fraxel'
import { keyframesFromSheet } from 'fraxel/animation'

const PLAYER_IDLE = await loadTexture('/assets/idle.png')
const PLAYER_WALK = await loadTexture('/assets/walk.png')

function Player() {
  const sprite = useNode(PrimaryNode.Sprite)
  const anim = useNode(PrimaryNode.AnimationPlayer)
  const [isWalking] = useSignal(false)

  const currentAnim = useComputed(() => (isWalking() ? 'walk' : 'idle'))

  return (
    <sprite ref={sprite} textureId={PLAYER_IDLE} sourceSize={new Vector2(16, 16)}>
      <animation-player
        ref={anim}
        animations={() => ({
          idle: {
            fps: 4,
            keyframes: keyframesFromSheet(sprite.node, PLAYER_IDLE, 4),
            loop: true,
          },
          walk: {
            fps: 8,
            keyframes: keyframesFromSheet(sprite.node, PLAYER_WALK, 6),
            loop: true,
          },
        })}
        currentAnim={currentAnim}
      />
    </sprite>
  )
}
One-shot animation with destroyOnEnd:
function HitEffect() {
  return (
    <animation-player
      animations={() => ({
        hit: {
          fps: 12,
          keyframes: keyframesFromSheet(sprite.node, HIT_TEXTURE, 5),
          loop: false,
        },
      })}
      currentAnim="hit"
      destroyOnEnd
    />
  )
}

Props

animations
(() => Record<string, Animation>) | Reactive<Record<string, Animation>>
A function returning a record of animation definitions. Called when the node starts (deferred), not at construction time. This allows referencing sibling Sprite nodes that may not exist yet at JSX evaluation time.
currentAnim
string | Reactive<string>
The name of the animation to play. Accepts a static string or a SignalGetter<string> for reactive animation switching. When the signal value changes, the player automatically transitions to the new animation.
destroyOnEnd
boolean
When true, the node is automatically destroyed after the current (non-looping) animation ends and animationEnded has fired. Defaults to false.

Events

EventuseEvent nameCallbackDescription
animationEnded'animationEnded'(animName: string) => voidFires when a non-looping animation finishes. animName is the name of the completed animation.
animationChanged'animationChanged'(newAnim: string, oldAnim: string | null) => voidFires when play() switches to a different animation.
animationIndexChanged'animationIndexChanged'(index: number) => voidFires each time the frame index advances.
animationStopped'animationStopped'(animName: string) => voidFires when stop() is called on the current animation.

Methods

MethodSignatureDescription
play(animName: string, index?: number) => voidStarts the named animation, optionally from a specific frame index.
stop() => voidStops the current animation and resets the frame index to 0.
setNext(animName: string | null) => voidQueues an animation to play after the current one ends. Pass null to clear the queue.
add(animName: string, animation: Animation) => thisAdds a single animation by name. Returns this for chaining.
define(animations: Record<string, Animation>) => thisAdds multiple animations at once. Returns this for chaining.

Instance properties

PropertyTypeDescription
currentAnimstring | nullThe name of the currently playing animation (read-only). null if stopped.
indexnumberThe current integer frame index (read-only).

Animation type

interface Animation {
  /** Frames per second */
  fps: number
  /** Array of keyframe functions */
  keyframes: AnimationKeyframe[]
  /** Whether the animation loops. Defaults to undefined (no loop). */
  loop?: boolean
}

/** Called each frame with the fractional time within the current frame (0–1). */
type AnimationKeyframe = (time: number) => void

JSX Components

These are not nodes but JSX components exported from fraxel/jsx. They orchestrate how scenes, games, and lists are composed.
The root component. Renders the game canvas and bootstraps the engine. Must wrap all other Fraxel content.
import { Game, Scene } from 'fraxel/jsx'

createRoot(document.getElementById('app')!).render(
  <Game width={800} height={600} defaultScene="main">
    <Scene name="main" component={MainScene} />
  </Game>
)

Props

width
number
required
Canvas width in pixels.
height
number
required
Canvas height in pixels.
defaultScene
string
required
The name of the <Scene> to load on startup.
testOptions
Partial<TestOptions>
Development-only options for visualising colliders, ray-casts, and clickable areas. See TestOptions in game-config.ts.
theme
Theme
Global theme applied to all <text> nodes. Overrides TextStyle.DEFAULT.
inputOptions
InputOptions
Options forwarded to the input system (e.g. custom pointer event handling).
Declares a named scene. Each <Scene> maps a string name to a component function. Scenes are rendered inside <Game> and can be switched at runtime. The component prop supports both direct component references and dynamic imports for code splitting.
import { Scene } from 'fraxel/jsx'

// Direct reference
<Scene name="gameplay" component={GameplayScene} />

// Lazy import (code splitting)
<Scene name="menu" component={() => import('./scenes/menu.js')} />

Props

name
string
required
Unique name for this scene. Referenced by defaultScene on <Game> and by the scene-switching API.
component
(() => Promise<Component | { default: Component }>) | Component
required
The scene root component function, or a dynamic import that resolves to one. Called when the scene becomes active. Use a dynamic import (() => import('./scene.js')) for lazy loading.
Renders a reactive array of nodes with keyed reconciliation. When the signal changes, <List> adds, removes, and reorders child nodes efficiently — only mutating what has changed.<List> uses a hidden <transform> node internally as an anchor for reconciliation.
import { List } from 'fraxel/jsx'
import { useSignal } from 'fraxel/hooks'

function EnemyList() {
  const [enemies, setEnemies] = useSignal<Enemy[]>([])

  return (
    <List
      array={enemies}
      itemKey={(enemy) => enemy.id}
      empty={<NoEnemiesLabel />}
    >
      {(enemy) => (
        <EnemyNode key={enemy.id} position={enemy.position} />
      )}
    </List>
  )
}

Props

array
SignalGetter<T[]>
required
A reactive array signal. <List> subscribes to this signal and reconciles the rendered output whenever the array changes.
children
(item: T, index: number, arr: T[]) => JSX.Element
required
Render function called for each item in the array. Receives the item value, its index, and the full array. Must return a single JSX node.
itemKey
(item: T, index: number, arr: T[]) => string | symbol
Key extractor for stable reconciliation. When provided, <List> uses the key to match old and new items, avoiding unnecessary node recreation. Strongly recommended for mutable arrays.
empty
JSX.Element
Optional node to render when the array is empty.
Logically groups multiple JSX nodes without adding a wrapper node to the scene tree. Identical in behaviour to React/JSX fragments.
import { Fragment } from 'fraxel/jsx'

function HUD() {
  return (
    <Fragment>
      <ScoreLabel />
      <HealthBar />
      <MiniMap />
    </Fragment>
  )
}
Shorthand syntax (if your JSX config supports it):
function HUD() {
  return (
    <>
      <ScoreLabel />
      <HealthBar />
      <MiniMap />
    </>
  )
}

Props

<Fragment> accepts no props. It is a pure logical grouping mechanism and adds no node to the scene tree.

Build docs developers (and LLMs) love