# Embers Physics
[Full-page experience](/experience/embers-physics/) · [Open playground](/playground/#/examples/composition/embers-physics)

Opt into local gravity, wind, and real collider bounces for ember transitions.

## Try it

Burn or materialize, reverse mid-flight, and compare with physics off.

## What to learn

- Connect a transition's particles to batched WASM collision sweeps.
- Preserve airborne motion through reversal and the effect tail.

## Integration boundaries

- Particle collisions are one-way point sweeps; obstacles are not pushed.
- The world and transition receiver share local coordinates.

This is a Pibbl Canvas example. Its mount owns its runtime resources; call its controller's dispose method when removing it. The preview starts automatically unless reduced motion is enabled; Stop disposes the mount. Preserve the source's logical canvas size and aspect ratio. Native HTML is not captured by Canvas filters.

## Related APIs

- [embers](/reference/hooks/use-visibility-transition/)
- [useVisibilityTransition](/reference/hooks/use-visibility-transition/)
- [PhysicsWorld2D](/reference/functions/physics-2d/)
- [physicsObstacles2D](/reference/functions/physics-obstacles/)

## Complete source

Host setup for this source: use a canvas with width 800 and height 600, compile with `jsx: "react-jsx"` and `jsxImportSource: "@pibbl/core"`, import its default `mount`, and call `const controller = mount(canvas)` after attaching the canvas. Call `controller.dispose()` before removing it.

### embers-physics.example.tsx

```tsx
import {
  pibbl,
  Group,
  Rectangle,
  Circle,
  Text,
  embers,
  useVisibilityTransition,
  useSignal,
  useConst,
  type PibblController,
  type PibblVisibilityTransition,
} from "@pibbl/core";
import {
  PhysicsWorld2D,
  pibblPhysicsWorld2D,
  staticBody2D,
  physicsObstacles2D,
} from "@pibbl/core/physics/2d";

function Chamber({
  enabled,
  register,
}: {
  enabled: boolean;
  register: (handle: PibblVisibilityTransition) => void;
}) {
  const world = pibblPhysicsWorld2D();
  const effect = useConst(() =>
    embers({
      region: { x: 180, y: 35, width: 440, height: 110 },
      seed: 9,
      linger: 1800,
      wind: 65,
      physics: enabled
        ? {
            gravity: [0, 650],
            wind: [55, 0],
            drag: 2.5,
            restitution: 0.72,
            obstacles: physicsObstacles2D(world),
          }
        : undefined,
    }),
  );
  const transition = useVisibilityTransition({ enter: effect, duration: 900 });
  register(transition);
  return (
    <Group>
      <Rectangle
        style={{
          left: 25,
          top: 5,
          width: 750,
          height: 370,
          cornerRadius: 16,
          fill: "#101f2d",
        }}
      />
      <Group style={{ transition: transition.binding }}>
        <Rectangle
          style={{
            left: 180,
            top: 35,
            width: 440,
            height: 110,
            cornerRadius: 14,
            fill: "#32677b",
          }}
        />
        <Text
          style={{
            left: 400,
            top: 85,
            textAlign: "center",
            textBaseline: "middle",
            font: "600 30px sans-serif",
            fill: "#fff2d6",
          }}
        >
          FROM FIRE TO ASH
        </Text>
        <Text
          style={{
            left: 400,
            top: 116,
            textAlign: "center",
            font: "14px sans-serif",
            fill: "#badbdc",
          }}
        >
          Ordinary Canvas content. Physical fragments.
        </Text>
      </Group>
      <Circle
        physics={staticBody2D()}
        style={{
          cx: 440,
          cy: 260,
          radius: 46,
          fill: "#305068",
          stroke: "#81acbc",
          strokeWidth: 2,
        }}
      />
      <Rectangle
        physics={staticBody2D()}
        style={{
          left: 140,
          top: 235,
          width: 160,
          height: 18,
          cornerRadius: 3,
          fill: "#305068",
          stroke: "#81acbc",
          strokeWidth: 2,
        }}
      />
      <Rectangle
        physics={staticBody2D()}
        style={{ left: 40, top: 350, width: 720, height: 15, fill: "#466273" }}
      />
      <Rectangle
        physics={staticBody2D()}
        style={{ left: 25, top: 10, width: 15, height: 355, fill: "#263f51" }}
      />
      <Rectangle
        physics={staticBody2D()}
        style={{ left: 760, top: 10, width: 15, height: 355, fill: "#263f51" }}
      />
      <Text
        style={{ left: 70, top: 333, font: "12px monospace", fill: "#86a7b4" }}
      >
        LOCAL COLLIDERS · GRAVITY · WIND · BOUNCE
      </Text>
    </Group>
  );
}
function Demo() {
  const enabled = useSignal(true),
    status = useSignal("Burn it away, then watch the fragments bounce.");
  let handle: PibblVisibilityTransition | undefined;
  const run = async () => {
    if (!handle) return;
    const current = handle;
    status.set(current.visible.get() ? "Burning away…" : "Materializing…");
    const result = await (current.visible.get()
      ? current.hide()
      : current.show());
    if (result.status === "finished")
      status.set("Settled. Toggle again, or compare with physics off.");
  };
  return (
    <Group>
      <Rectangle style={{ width: 800, height: 600, fill: "#091722" }} />
      <Text
        style={{ left: 36, top: 42, font: "12px monospace", fill: "#77cabc" }}
      >
        EMBERS / LOCAL PHYSICS
      </Text>
      <Text
        style={{
          left: 36,
          top: 84,
          font: "600 30px sans-serif",
          fill: "#f4f3e9",
        }}
      >
        A little fire. A little gravity.
      </Text>
      <Text
        style={{ left: 36, top: 113, font: "14px sans-serif", fill: "#96b4c1" }}
      >
        Fragments collide with the shelf, sphere, and floor. Click mid-flight to
        reverse.
      </Text>
      <Group style={{ translateY: 130 }}>
        <PhysicsWorld2D
          key={String(enabled.get())}
          space="local"
          gravity={[0, 650]}
        >
          <Chamber
            enabled={enabled.get()}
            register={(value) => {
              handle = value;
            }}
          />
        </PhysicsWorld2D>
      </Group>
      <Rectangle
        style={{
          left: 36,
          top: 520,
          width: 220,
          height: 42,
          cornerRadius: 7,
          fill: "#d48439",
          cursor: "pointer",
        }}
        onClick={() => {
          void run();
        }}
      />
      <Text
        pointerEvents="none"
        style={{
          left: 146,
          top: 541,
          textAlign: "center",
          textBaseline: "middle",
          font: "600 14px sans-serif",
          fill: "#101e29",
        }}
      >
        BURN / MATERIALIZE
      </Text>
      <Rectangle
        style={{
          left: 275,
          top: 520,
          width: 210,
          height: 42,
          cornerRadius: 7,
          fill: "#294454",
          cursor: "pointer",
        }}
        onClick={() => {
          enabled.update((value) => !value);
          status.set("Fresh subject. Toggle to compare the motion.");
        }}
      />
      <Text
        pointerEvents="none"
        style={{
          left: 380,
          top: 541,
          textAlign: "center",
          textBaseline: "middle",
          font: "600 14px sans-serif",
          fill: "#e8f6f0",
        }}
      >
        PHYSICS: {enabled.get() ? "ON" : "OFF"}
      </Text>
      <Text
        style={{ left: 36, top: 588, font: "13px monospace", fill: "#8be3c7" }}
      >
        {status.get()}
      </Text>
    </Group>
  );
}
export default function mount(canvas: HTMLCanvasElement): PibblController {
  return pibbl(canvas, <Demo />);
}

```

## Documentation version

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.
