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

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 instantiates Pixi's Application class to create the WebGL renderer and canvas.

Key configuration choices target crisp pixel art rendering:

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 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:

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 (lines 31-45) and processes each tileset URL from the active theme bundle.

Tiled Map Parsing and Sprite Generation

The 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:

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 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 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:

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:

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 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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →