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:
characterContaineris added last to ensure avatars appear on top of furniture - State-driven animation: The central Redux-like store (
useStore) feeds agent state toCharacterSprite, 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
webglcontextlostevents 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, andGraphics - TiledMapRenderer.ts converts JSON map exports into efficient sprite batches with sub-texture atlasing
- Pixel-perfect rendering is enforced via
scaleMode: 'nearest'androundPixels: true - Electron compatibility requires
import 'pixi.js/unsafe-eval'to bypass CSP restrictions - Animation and simulation are synchronized through a single
app.tickercallback that updates all visual systems - Dynamic UI elements use
Graphicsfor procedural shapes andSpritefor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →