# fraxel

> **Declarative 2D game engine powered by JSX and fine-grained reactivity.**

[![CI](https://github.com/sanchedev/fraxel/actions/workflows/ci.yml/badge.svg)](https://github.com/sanchedev/fraxel/actions)
[![npm version](https://img.shields.io/npm/v/fraxel)](https://www.npmjs.com/package/fraxel)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

**Fraxel** is a declarative 2D game engine for the browser built around a custom JSX runtime.

Instead of rendering HTML, JSX compiles directly into a scene graph rendered on a `<canvas>`. Signals update node properties automatically, eliminating reconciliation, virtual DOM diffing and component re-renders.

Designed for developers who enjoy declarative APIs while keeping the performance and architecture expected from a real-time game engine.

## Why Fraxel?

- 🎮 Declarative scene graph powered by JSX
- ⚡ Fine-grained reactivity with signals
- 🧩 Typed node references through `ref()`
- 💥 Built-in physics and collision detection
- 🎥 Camera, audio, animation and asset loading
- 📦 TypeScript-first API
- 🚫 No React
- 🚫 No virtual DOM
- 🚫 No component re-renders

---

# Install

```bash
npm install fraxel
```

or

```bash
pnpm add fraxel
```

or

```bash
yarn add fraxel
```

> 💡 **Recommended:** use Fraxel together with [`@fraxel/vite-plugin`](https://www.npmjs.com/package/@fraxel/vite-plugin). Its two headline features are the asset import pipeline (`?texture` / `?sound` queries that resolve to ready-to-use symbol IDs) and automatic attribute derivation (non-literal JSX props are wrapped in `derived()` so they stay reactive without manual wiring). See the [plugin README](https://www.npmjs.com/package/@fraxel/vite-plugin) for setup.

---

# Quick Example

A complete player with movement, jumping, collisions and physics.

Create a new project with `npx create-fraxel my-game -t platformer` (available templates: `empty`, `platformer`, `top-down`, `coin-box`).

Paste it in src/main.tsx

```tsx
import {
  Actions,
  loadTexture,
  CollisionLayer,
  shapes,
  vector,
  ref,
  signal,
  mirror,
  createGame,
  useActions,
  useEffect,
  type CollisionOwner,
} from 'fraxel'

const PLAYER = await loadTexture('/player.png')

const Left = Actions.create('KeyA')
const Right = Actions.create('KeyD')
const Jump = Actions.create('Space')

const actions = { Left, Right, Jump }

const Layers = {
  Player: CollisionLayer.create(1),
  Ground: CollisionLayer.create(2),
} as const

function Player() {
  const body = ref()
  const grounded = mirror<CollisionOwner | null>(null)
  const velocity = signal(vector(0))

  const { justPressed, getAxis } = useActions()

  useEffect(() => {
    velocity.set((old) => vector(getAxis(Left, Right) * 120, old.y))

    if (justPressed(Jump) && grounded() != null) {
      body.current?.applyImpulse(vector(0, -400))
    }
  })

  return (
    <rigidbody
      ref={body}
      position={vector(80, 40)}
      mass={1}
      layer={Layers.Player}
      mask={Layers.Ground}
      velocity={velocity}
    >
      <sprite textureId={PLAYER} />
      <collider shape={shapes.rectangle(16, 16)} />
      <raycast
        position={vector(8, 16)}
        direction={vector(0, 2)}
        mask={Layers.Ground}
        target={grounded}
      />
    </rigidbody>
  )
}

function MainScene() {
  return (
    <>
      <Player />

      <rigidbody position={vector(0, 200)} isStatic layer={Layers.Ground} mask={Layers.Player}>
        <collider shape={shapes.rectangle(400, 16)} />
      </rigidbody>
    </>
  )
}

createGame({
  viewportSize: vector(320, 240),
  mainScene: 'main',
  actions,
  scenes: {
    main: async () => MainScene,
  },
})
  .atRoot(document.querySelector('#app')!)
  .play()
```

Do `npm run dev` and play.

---

# Setup

Enable Fraxel's JSX runtime.

```json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "fraxel"
  }
}
```

For a complete Vite workflow — especially the `?texture` / `?sound` asset imports and the automatic JSX attribute derivation — add `@fraxel/vite-plugin` (see its README).

---

# What makes Fraxel different?

Fraxel combines ideas from modern frontend frameworks with the architecture of a traditional game engine.

Instead of rendering HTML, JSX creates nodes inside a scene graph.

Instead of component state, signals update node properties directly.

Instead of DOM refs, `ref()` exposes strongly typed node references with methods, reactive state and events.

The result feels familiar to frontend developers while remaining purpose-built for real-time rendering.

---

# Core Concepts

## Declarative Scene Graph

Every JSX element becomes a node.

```tsx
<rigidbody>
  <sprite />
  <collider />
</rigidbody>
```

becomes

```text
RigidBody
├── Sprite
└── Collider
```

Function components (like `Player`) don't create their own node — they return nodes that attach directly to the parent, so they add no wrapper to the tree.

The scene graph drives rendering, physics, audio, animation and every engine subsystem.

---

## Fine-grained Reactivity

Properties accept signals directly.

```tsx
const body = ref()
const velocity = signal(vector(0))

const rotation = computed(() => velocity().x * 0.01)

return (
  <rigidbody ref={body} rotation={rotation} velocity={velocity}>
    <sprite textureId={PLAYER} />
  </rigidbody>
)
```

Only the affected property updates.

No render loop.

No reconciliation.

No virtual DOM.

---

## Typed Node References

`ref()` exposes typed references to engine nodes.

```tsx
const sprite = ref()

sprite.current?.position()
sprite.current?.rotation()
sprite.current?.flipX.set(true)
sprite.current?.opacity.set(0.8)

return <sprite ref={sprite} textureId={PLAYER} />
```

Each reference combines:

- reactive state
- imperative methods
- strongly typed events

---

## Trigger System

Triggers allow nodes to communicate without tight coupling.

```tsx
import { trigger } from 'fraxel'

const onDead = trigger<[]>()

const disconnect = onDead(() => {
  console.log('Player died')
})

onDead.emit()
disconnect()
```

---

# Features

- ✅ Custom JSX runtime
- ✅ Declarative scene graph
- ✅ Fine-grained reactivity
- ✅ Signals, computed, derived & mirrors
- ✅ Reactive stores
- ✅ `<For>`, `<Show>` and `<Dynamic>` components
- ✅ Typed node references via `ref()`
- ✅ Trigger system with linking
- ✅ Physics engine
- ✅ Collision detection & raycasts
- ✅ Camera system
- ✅ Audio playback
- ✅ Sprite animations
- ✅ Tweening (`number`, `Color`, `Vector`, and signals)
- ✅ `<clickable>` appearance & pointer events
- ✅ Drag & drop (`<draggable>` / `<droparea>`)
- ✅ Asset loading
- ✅ Input actions
- ✅ State machines
- ✅ Pixel-art rendering
- ✅ TypeScript-first

---

# Package Structure

```tsx
import {
  // Engine
  loadTexture,
  Actions,
  shapes,
  vector,
  createGame,
  NodeType,
  tween,

  // JSX runtime
  Fragment,
  For,
  Show,
  Dynamic,

  // Reactivity
  signal,
  computed,
  derived,
  mirror,
  store,

  // Hooks
  useActions,
  useHost,
  useEffect,
  useGameControls,
  useGameScenes,
  useStateMachine,

  // State machines
  stateMachine,

  // Refs
  ref,
} from 'fraxel'
```

---

# Documentation

[https://fraxel.mintlify.app/](https://fraxel.mintlify.app/)

---

# Philosophy

Fraxel is designed around one simple idea:

> **Games should be described, not synchronized.**

JSX describes the scene.

Signals describe the state.

Refs and effects interact with nodes.

Fraxel keeps everything in sync.

---

# License

MIT
