How to Bind Svelte Stores to Matter.js Updates

Integrating Matter.js with Svelte allows you to drive reactive web interfaces using real-world 2D physics. This guide demonstrates how to synchronize Matter.js engine updates directly with Svelte reactive stores using Matter.js event hooks. By bridging the physics runner's tick lifecycle with Svelte's writable stores, you can build declarative components that react automatically to physical movements, collisions, and state changes.

The Synchronization Architecture

Matter.js operates on an imperative loop driven by Matter.Runner or requestAnimationFrame, continuously updating physical properties such as position, velocity, and rotation. Svelte operates declaratively, rendering updates when internal stores change.

To connect the two systems efficiently:

  1. Initialize a Svelte writable store containing the state of the physics objects you want to track.
  2. Hook into the Matter.js engine lifecycle using Matter.Events.on(engine, 'afterUpdate', callback).
  3. Read the transformed coordinates and rotation angles from target Matter.Body instances on every frame.
  4. Push those updates into the Svelte store using store.set(), triggering fine-grained reactivity in the DOM or an SVG/Canvas layer.

Implementation Example

The following pattern demonstrates how to bind physics bodies to a Svelte store inside a component's lifecycle:

<script>
  import { onMount } from 'svelte';
  import { writable } from 'svelte/store';
  import Matter from 'matter-js';

  const { Engine, Runner, Bodies, Composite, Events } = Matter;

  // Create a reactive store to hold body transformations
  const boxState = writable({ x: 0, y: 0, angle: 0 });

  onMount(() => {
    // 1. Create engine and world
    const engine = Engine.create();
    const world = engine.world;

    // 2. Create physics bodies
    const box = Bodies.rectangle(200, 100, 50, 50, { restitution: 0.8 });
    const ground = Bodies.rectangle(200, 400, 400, 40, { isStatic: true });
    Composite.add(world, [box, ground]);

    // 3. Bind engine updates to the Svelte store
    Events.on(engine, 'afterUpdate', () => {
      boxState.set({
        x: box.position.x,
        y: box.position.y,
        angle: box.angle
      });
    });

    // 4. Start the simulation
    const runner = Runner.create();
    Runner.run(runner, engine);

    // 5. Cleanup on component destroy
    return () => {
      Runner.stop(runner);
      Events.off(engine, 'afterUpdate');
      Composite.clear(world, false);
      Engine.clear(engine);
    };
  });
</script>

<!-- Render reactive element driven by the store -->
<div class="viewport">
  <div 
    class="physics-box"
    style="transform: translate({$boxState.x}px, {$boxState.y}px) rotate({$boxState.angle}rad);"
  ></div>
  <div class="ground"></div>
</div>

<style>
  .viewport {
    position: relative;
    width: 400px;
    height: 400px;
    overflow: hidden;
    border: 1px solid #ccc;
  }

  .physics-box {
    position: absolute;
    top: -25px; /* Offset center point */
    left: -25px;
    width: 50px;
    height: 50px;
    background: #ff3e00;
    will-change: transform;
  }

  .ground {
    position: absolute;
    top: 380px;
    left: 0;
    width: 400px;
    height: 40px;
    background: #333;
  }
</style>

Performance Considerations

While updating a Svelte store every frame works smoothly for a small number of bodies, high object counts can degrade performance due to frequent reactivity cycles. To optimize throughput: