# How Munder Difflin Uses Pixi.js for Its 2D Office Floor Visualization

> Discover how Munder Difflin leverages Pixi.js for dynamic 2D office floor visualizations. Learn about Tiled JSON maps, animated sprites, and real-time updates.

- Repository: [Chaitanya Giri/munder-difflin](https://github.com/chaitanyagiri/munder-difflin)
- Tags: how-to-guide
- Published: 2026-08-28

---

**Munder Difflin renders its animated office floor with Pixi.js v8 by loading Tiled JSON maps into textured sprites, driving agent animations with `AnimatedSprite`, and updating dynamic overlays through Pixi's `Ticker` system.**

The `chaitanyagiri/munder-difflin` repository implements a complete 2D office visualization for an AI-agent observability tool. The rendering pipeline combines Pixi's low-level scene graph APIs with a custom Tiled-map parser, producing pixel-perfect tile layers, animated avatars, and interactive UI elements inside an Electron window. This article breaks down exactly how Pixi.js powers each visual component.

## Pixi.js Application Setup and WebGL Initialization

The entry point [`OfficeFloor.tsx`](https://github.com/chaitanyagiri/munder-difflin/blob/main/OfficeFloor.tsx) instantiates Pixi's `Application` class to create the WebGL renderer and canvas.

Key configuration choices target crisp pixel art rendering:

```tsx
import { Application } from 'pixi.js';
import 'pixi.js/unsafe-eval';

const app = new Application({
  background: 0xeeeeee,
  antialias: false,
  roundPixels: true,
  resolution: Math.max(window.devicePixelRatio || 1, 2),
  autoDensity: true,
  width: host.clientWidth,
  height: host.clientHeight,
});

host.appendChild(app.canvas);

```

The `unsafe-eval` import is **required** for Electron compatibility. Pixi uses `new Function()` internally, which Electron's Content-Security-Policy blocks by default. This workaround is applied at the top of [`OfficeFloor.tsx`](https://github.com/chaitanyagiri/munder-difflin/blob/main/OfficeFloor.tsx) before any other Pixi imports.

## Loading and Configuring Tileset Textures

Theme assets are loaded through a custom `loadTexture` helper that wraps HTMLImageElement loading with Pixi's `Texture.from`:

```ts
import { Texture } from 'pixi.js';

function loadTexture(url: string): Promise<Texture> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      const tex = Texture.from(img);
      tex.source.scaleMode = 'nearest';  // Pixel-perfect scaling
      resolve(tex);
    };
    img.onerror = () => reject(new Error('failed to load ' + url));
    img.src = url;
  });
}

```

The `scaleMode: 'nearest'` setting is critical for the retro pixel-art aesthetic—it prevents bilinear blurring when textures are scaled. This implementation appears in [`OfficeFloor.tsx`](https://github.com/chaitanyagiri/munder-difflin/blob/main/OfficeFloor.tsx) (lines 31-45) and processes each tileset URL from the active theme bundle.

## Tiled Map Parsing and Sprite Generation

The [`TiledMapRenderer.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/TiledMapRenderer.ts) file transforms Tiled JSON exports into a Pixi scene graph hierarchy. It processes four tile layers in order: `floor`, `walls`, `furniture-below`, and `furniture-above`.

### Building Tile Layers with Sub-Texture Extraction

Each tile layer becomes a `Container` filled with `Sprite` instances. The renderer extracts sub-textures from tileset atlases using `Rectangle` frames:

```ts
import { Container, Sprite, Texture, Rectangle } from 'pixi.js';

for (const layerName of TILE_LAYERS) {
  const layer = this.findLayer(layerName, 'tilelayer');
  const container = new Container();
  
  if (layer?.data) {
    for (let y = 0; y < this.height; y++) {
      for (let x = 0; x < this.width; x++) {
        const raw = layer.data[y * this.width + x];
        if (!raw) continue;

        // Decode Tiled flip flags
        const flippedH = (raw & FLIPPED_H_FLAG) !== 0;
        const flippedV = (raw & FLIPPED_V_FLAG) !== 0;
        const flippedD = (raw & FLIPPED_D_FLAG) !== 0;
        const tileId   = raw & TILE_ID_MASK;

        const resolved = this.resolveTileset(tileId);
        if (!resolved) continue;

        const { tileset, texture } = resolved;
        const cols = tileset.columns ?? 16;
        const tw = tileset.tilewidth ?? this.tileSize;
        const th = tileset.tileheight ?? this.tileSize;
        const localId = tileId - tileset.firstgid;
        
        // Calculate source rectangle in tileset atlas
        const srcX = (localId % cols) * tw;
        const srcY = Math.floor(localId / cols) * th;
        const frame = new Rectangle(srcX, srcY, tw, th);

        const sprite = new Sprite(
          new Texture({ source: texture.source, frame })
        );

        // Apply Tiled transformations
        if (flippedH) sprite.scale.x = -1;
        if (flippedV) sprite.scale.y = -1;
        if (flippedD) { /* rotation handling */ }

        sprite.x = x * this.tileSize;
        sprite.y = y * this.tileSize;
        container.addChild(sprite);
      }
    }
  }
  this.rootContainer.addChild(container);
}

```

This code, located at [`TiledMapRenderer.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/TiledMapRenderer.ts) lines 91-122, demonstrates Pixi's flexible texture system: a single loaded image serves as the source for hundreds of sprites through frame rectangle mapping.

## Character Animation with AnimatedSprite

Agents (office workers) render above all tile layers in a dedicated `characterContainer`. The [`CharacterSprite.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/CharacterSprite.ts) module wraps Pixi's `AnimatedSprite` to handle walking, idle, and coffee-sipping animations.

Key architectural decisions:

- **Layer ordering**: `characterContainer` is added last to ensure avatars appear on top of furniture
- **State-driven animation**: The central Redux-like store (`useStore`) feeds agent state to `CharacterSprite`, which selects appropriate animation frames
- **Anchor manipulation**: Sprite anchors are adjusted for each animation type to maintain proper positioning during state transitions

## Dynamic Overlays and Interactive Elements

Beyond static tiles and characters, Munder Difflin uses Pixi's `Graphics` and `Ticker` for real-time visual effects.

### Per-Frame Update System

The simulation loop attaches to Pixi's ticker in [`OfficeFloor.tsx`](https://github.com/chaitanyagiri/munder-difflin/blob/main/OfficeFloor.tsx):

```ts
app.ticker.add((delta) => {
  const dt = delta / app.ticker.FPS; // Convert to seconds
  
  updateCoffeeRuns(dt);
  updateCafeteria(dt);
  updateErrands(dt);
  updateBossAura(dt);
  updateDeskLife(dt);
  // Additional per-frame systems...
});

```

This unified tick handler keeps all visual state synchronized with the backend simulation.

### Vector Graphics for UI Elements

Thought bubbles, message envelopes, and tool indicators combine `Graphics` primitives with `Sprite` composition:

```ts
import { Graphics } from 'pixi.js';

const bubble = new Graphics();
bubble.beginFill(0xffffff, 0.9);
bubble.drawRoundedRect(0, 0, 80, 30, 8);
bubble.endFill();
bubble.position.set(x, y);
container.addChild(bubble);

```

The [`MessageEnvelope.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/MessageEnvelope.ts) implementation limits concurrent envelopes to 16, pooling `Container`/`Sprite` pairs that travel along bezier paths between desks.

## Camera and Viewport Management

A lightweight `Camera` class in [`Camera.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/Camera.ts) manipulates the world container's `position` property to follow agents and restrict the visible area. It clamps coordinates against `Application.screen` dimensions to prevent showing empty space beyond map boundaries.

## Resilience: WebGL Context Recovery

The [`glRecovery.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/glRecovery.ts) module adds production reliability:

- Detects `webglcontextlost` events on the canvas
- Gracefully destroys and rebuilds the Pixi `Application`
- Preserves simulation state while restoring visual output

This prevents the entire UI from freezing when GPU resources are reclaimed by the operating system.

## Summary

- **Pixi.js v8** provides the complete rendering foundation through `Application`, `Container`, `Sprite`, `AnimatedSprite`, and `Graphics`
- **TiledMapRenderer.ts** converts JSON map exports into efficient sprite batches with sub-texture atlasing
- **Pixel-perfect rendering** is enforced via `scaleMode: 'nearest'` and `roundPixels: true`
- **Electron compatibility** requires `import 'pixi.js/unsafe-eval'` to bypass CSP restrictions
- **Animation and simulation** are synchronized through a single `app.ticker` callback that updates all visual systems
- **Dynamic UI elements** use `Graphics` for procedural shapes and `Sprite` for textured objects

## Frequently Asked Questions

### What Pixi.js version does Munder Difflin use?

Munder Difflin targets **Pixi.js v8**, the latest major version at time of development. The codebase uses the modern package structure with individual subpath imports like `pixi.js/unsafe-eval` and the new `Texture` constructor API with `{ source, frame }` options.

### How does the Tiled map loader handle flipped tiles?

[`TiledMapRenderer.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/TiledMapRenderer.ts) decodes Tiled's bit-packed flip flags (`FLIPPED_H_FLAG`, `FLIPPED_V_FLAG`, `FLIPPED_D_FLAG`) from each tile's GID. Horizontal and vertical flips apply negative `scale.x`/`scale.y` values to the sprite. Diagonal flips (rotation) require additional rotation transformation logic to match Tiled's rendering behavior.

### Why is the `characterContainer` added last to the scene graph?

Pixi renders children in insertion order. By adding `characterContainer` after all tile layers (`floor`, `walls`, `furniture-below`, `furniture-above`), agents automatically appear on top of furniture and environmental details. This simple depth-sorting approach works because the office floor uses a fixed orthogonal perspective without complex Z-buffering requirements.

### How does the application handle high-DPI displays?

The `Application` constructor sets `resolution: Math.max(window.devicePixelRatio || 1, 2)` and `autoDensity: true`. This creates a canvas with physical pixel dimensions matching the device, then automatically scales CSS dimensions to maintain logical size. The `roundPixels: true` setting prevents sub-pixel rendering artifacts on high-density screens.