Map Editor
Advanced map editor features and UX patterns
Phase: 03-phase-1-basic-maps Status: ✅ Complete (Tasks 1-26)
🎨 Visual Layout
The map editor provides a full-screen editing experience with a clean, professional layout:
┌─────────────────────────────────────────────────────────────────┐
│ ← [Map Title] [Save Status] [Replace Image] │ Header
├────────────┬────────────────────────────────────────────────────┤
│ │ │
│ Content │ ┌─────────────┐ │
│ Library │ │ Toolbar │ │
│ │ └─────────────┘ │
│ [Search] │ │
│ │ │
│ ┌────────┐ │ Map Canvas │
│ │ NPC │ │ │
│ │ Token │ │ [Grid overlay if enabled] │
│ └────────┘ │ │
│ │ [Placed tokens] │
│ ┌────────┐ │ │
│ │Monster │ │ [Measurement lines if active] │
│ │ Token │ │ │
│ └────────┘ │ │
│ │ │
│ [Hide ◀] │ │
│ │ │
└────────────┴────────────────────────────────────────────────────┘
🎯 Key UI Components
1. Header Bar
- Back Button: Returns to campaign view
- Map Title: Displays current map name
- Save Status Badge:
- ☁️ Saved (green)
- 🔄 Saving... (with spinner)
- 💾 Unsaved (yellow outline)
- ☁️❌ Save Failed (red)
- Replace Image Button: Re-upload map image (when map exists)
2. Content Library Sidebar (Collapsible)
- Search Bar: Filter content by name/type
- Content Items: Drag-and-drop NPCs, monsters, items onto map
- Content Types:
- ⚔️ NPCs
- 📍 Locations
- 💀 Monsters
- 💎 Treasures
- 👥 Factions
- Toggle Button: Collapse/expand sidebar with smooth animation
3. Floating Toolbar (Center-top)
Tools Section:
┌─────────────────────────────────────────────────────────────────┐
│ [👆 Select] [📏 Measure] │ [# Grid] │ [◐ Contrast] │ │
│ │ │ │ │
│ [🔍- 75% 🔍+] [⊡ Fit] │ [⚙️ Settings] │
└─────────────────────────────────────────────────────────────────┘
Tool Buttons:
- Select Tool (V): Click tokens to select, drag to move
- Measure Tool (R): Click points to measure distance (D&D 5e rules)
- Grid Toggle (G): Show/hide grid overlay
- High Contrast (C): Accessibility mode with WCAG-compliant colors
- Zoom Controls: - / % / + buttons
- Fit to Viewport: Auto-zoom to fit entire map
- Settings: Open grid calibration dialog
Keyboard Shortcuts (All tools have keyboard support):
V- Select toolR- Measure toolG- Toggle gridC- Toggle high contrast+/-- Zoom in/out0- Fit to viewport- Arrow keys - Move selected token
Delete- Remove selected tokenEscape- Clear selection / Clear measurementCtrl+Z- Remove last measurement point
4. Map Canvas
Rendering Layers (bottom to top):
- Background Image: Uploaded battle map
- Grid Overlay: Configurable 5ft or 10ft grid with opacity control
- Token Layer: Draggable content tokens with selection highlights
- Measurement Layer: Distance measurement lines with labels
Canvas Features:
- Pan: Click-drag to pan around map
- Zoom: Scroll wheel or toolbar buttons (25%-400%)
- Token Selection: Click token to select (yellow outline)
- Token Movement: Drag tokens to new positions
- Grid Snapping: Tokens snap to grid squares (when calibrated)
- High Contrast Mode:
- Grid: Pure black/white
- Token outlines: 4px black borders
- Selection: Yellow (21:1 contrast)
- Measurements: Red lines (5.25:1 contrast)
🎨 Design System Integration
Colors
Normal Mode:
- Grid: Black with configurable opacity
- Token outlines: Dark gray (selected: yellow)
- Measurements: Blue lines with white text backgrounds
High Contrast Mode (WCAG 2.1 AA):
- Grid: #000000 / #FFFFFF
- Selection: #FFFF00 (21:1 ratio)
- Measurements: #FF0000 (5.25:1 ratio)
- Token outlines: #000000 (4px width)
Typography
- Map title:
font-semibold - Content items:
text-sm - Toolbar labels: Icon-first design
- Measurement labels: Bold white text on dark backgrounds
Spacing
- Header: 56px (h-14)
- Sidebar: 256px (w-64) when open, 0 when collapsed
- Toolbar: Floating with shadow, centered at top
- Content padding: 16px (p-4)
♿ Accessibility Features (WCAG 2.1 AA)
Visual
- High Contrast Mode: System preference detection + manual toggle
- ARIA Labels: All toolbar buttons, canvas regions
- Focus Indicators: Visible keyboard focus on all interactive elements
- Color Independence: No information conveyed by color alone
Keyboard Navigation
- Full Keyboard Control: All features accessible without mouse
- Tab Navigation: Logical tab order through UI
- Arrow Key Navigation: Token movement with grid awareness
- Screen Reader Support:
- Live regions announce token moves
- ARIA labels describe all tools
- Token positions announced as grid coordinates
Screen Reader Announcements
// Example announcements
"Goblin moved to 5, 7"
"Wizard selected"
"Selection cleared"
"Measurement started at 3, 4"
"Distance: 25 feet (5 squares)"
🎮 Interactive Features
Token Management
Drag-and-Drop from Library:
- Search content library
- Click content item to add to map center
- Or drag item onto specific map location
Token Interactions:
- Click: Select token (highlights with yellow border)
- Drag: Move token to new position
- Right-click: Context menu (future: visibility, rotation)
- Keyboard: Arrow keys move 1 square, Shift+arrow moves 5 squares
- Delete: Remove token from map
Token Rendering:
- Size: Scales based on creature size (Tiny, Small, Medium, Large, etc.)
- Visibility:
- Visible (normal opacity)
- GM Only (semi-transparent, red outline)
- Hidden (not shown to players)
- Rotation: Future feature (already in data model)
- Elevation: Future feature for 3D positioning
Measurement Tool
Click-to-measure workflow:
- Activate measure tool (R)
- Click first point on map
- Click additional points to add segments
- Each segment shows:
- Distance in feet
- Grid squares
- Running total
- Uses D&D 5e distance rules (Chebyshev or 5-10-5)
Measurement Display:
- Blue line connecting points
- White label with distance
- "25 ft (5 sq)" format
- Keyboard shortcuts:
Escape: Clear all measurementsBackspace: Remove last point
Grid Calibration
Calibration Dialog:
┌─────────────────────────────────────┐
│ Grid Calibration │
├─────────────────────────────────────┤
│ │
│ [Method: Two-point] [Quick] │
│ │
│ Click two points on the map that │
│ are a known distance apart. │
│ │
│ Point 1: (x: 150, y: 100) │
│ Point 2: (x: 450, y: 100) │
│ │
│ Distance: [10] squares │
│ │
│ Grid Size: ○ 5ft ● 10ft │
│ │
│ [Cancel] [Apply] │
└─────────────────────────────────────┘
Calibration Methods:
- Two-Point: Click two map points, specify distance
- Quick Width: Enter map width in squares
Grid Settings:
- Grid size: 5ft or 10ft
- Grid opacity: 0-100% (default 50%)
- Grid color: Always black (varies with opacity)
Battle Map Lifecycle
Battle maps in CritForge can be created through two distinct paths and experienced in different viewing contexts depending on how they were generated.
Path 1: Standalone Map Generator
Route: /generate/map/battle
The Map Generator wizard lets GMs create standalone battle maps by specifying location type, biome, scale, and tactical requirements. The AI generates a complete map with terrain, enemy positions, hazards, and read-aloud text.
After generation, the map is stored as content_type='map' in the database and the GM is redirected to the Map Detail Page (/maps/[mapId]), which has two tabs:
| Tab | Purpose |
|---|---|
| Details | Read-aloud text, tactical description, terrain features, hazards, enemy positions (list form), entry/exit points, world setting |
| Visual Preview | Interactive 2D canvas (Pixi.js) showing the rendered grid with enemy tokens, terrain zones, and drag-and-drop repositioning |
Available actions in Visual Preview:
- Pan and zoom the map
- Drag enemy tokens to reposition (persisted via API)
- Right-click enemies for count adjustment and removal
- Switch style presets (parchment, tactical, blueprint)
- Fit-to-view (
Homekey)
What's NOT available: Reveal/hide controls (no player view concept in standalone maps), mark-as-defeated, initiative tracking.
Path 2: Encounter Generator
Route: /generate/encounter
The Encounter Generator creates a complete combat encounter — enemies, tactics, loot, and an embedded battle map. The map data lives inside the encounter's content_data (not as a separate map record).
After generation, the encounter is stored as content_type='encounter'. The GM can view it at /encounters/[id] and access the tactical views:
| View | Route | Purpose |
|---|---|---|
| Tactical Grid | /encounters/[id]/tactical | Full-screen encounter prep — layer controls, reveal/hide, count adjustment, remove, style switching |
| Combat Mode | /encounters/[id]/tactical/combat | Live play — initiative tracking, HP/condition management, reveal/hide, mark-as-defeated |
The "Go Live →" button in Tactical Grid transitions to Combat Mode.
Feature Comparison
| Feature | Visual Preview | Tactical Grid | Combat Mode |
|---|---|---|---|
| Pan & zoom | ✅ | ✅ | ✅ |
| Drag reposition | ✅ | — | — |
| Count adjustment | ✅ | ✅ | ✅ |
| Remove enemy | ✅ | ✅ | — |
| Reveal/hide enemies | — | ✅ | ✅ |
| Bulk reveal/hide all | — | — | ✅ |
| Mark as defeated | — | — | ✅ |
| Layer controls | — | ✅ | — |
| Initiative sidebar | — | — | ✅ |
| Style presets | ✅ | — | — |
| Keyboard shortcuts | Home | F C Shift+H | F C Shift+H |
| Data persistence | API (PATCH/DELETE) | Zustand store | Zustand store |
Data Flow
Standalone Map Encounter Map
───────────── ──────────────
/generate/map/battle /generate/encounter
│ │
▼ ▼
content_type='map' content_type='encounter'
│ │
▼ ▼
MapDataAdapter encounterToRenderableMap()
│ │
▼ ▼
RenderableMap RenderableMap
│ ┌──────┴──────┐
▼ ▼ ▼
MapVisualPreview TacticalGridView CombatGridView
(map detail page) (encounter prep) (live combat)
Both paths produce a RenderableMap — the shared type consumed by Map2DCanvas (the Pixi.js renderer). The difference is the adapter layer and what UI chrome wraps the canvas.
Enemy Token Management
Visual Preview Mode (Map Detail Page)
When viewing a generated battle map in the Visual Preview tab, enemy tokens appear as colored discs with count badges (e.g., "x6"). The GM can manage enemies directly on the canvas:
Right-Click Context Menu:
- Right-click any enemy token to open a context menu
- Co-located enemies: When multiple enemy types share the same cell, ONE right-click shows ALL enemies in a single grouped menu with independent controls per type
- Count Stepper: Adjust the number of enemies ([-] N [+]) — changes are session-only until removed
- Remove from Map: Hard-deletes the enemy from the map (persisted via API)
Visual Group Indicators: When 2+ enemy types share a grid cell, a subtle background plate and "N types · M total" badge appear behind the group, making it easy to spot clustered enemies at a glance.
Drag-and-Drop Repositioning:
- Drag enemy tokens to reposition them on the grid
- Tokens snap to grid cells by default; hold Alt/Option for free placement
- Changes are persisted automatically via API
Keyboard Shortcuts:
| Action | Key |
|---|---|
| Fit to view | Home |
Tactical Grid View (Encounter Prep)
The Tactical Grid View (/encounters/[id]/tactical) provides a full-screen map experience for encounter preparation:
Features:
- Full context menu with reveal/hide controls (for player view management)
- Count adjustment and remove actions
- Layer controls (grid, tactical overlay, player view toggle)
- "Go Live" button transitions to Combat Mode
Keyboard Shortcuts:
| Action | Key |
|---|---|
| Fit all tokens | F |
| Fit combatants | C |
| Return to map | Home |
| Hide all enemies | Shift+H |
Combat Mode (Live Play)
Combat Mode (/encounters/[id]/tactical/combat) is designed for running live encounters:
Enemy Management:
- Reveal/Hide: Control which enemies players can see
- Reveal All / Hide All: Bulk visibility controls (top-left buttons)
- Count Stepper: Adjust enemy counts mid-combat
- Mark as Defeated: Non-destructive alternative to removal — token shows as grayed out with a red X overlay, kept on map for reference
- Hidden enemy count badge shows how many enemies are still hidden from players
Initiative Sidebar:
- Set initiative order before combat starts
- Track HP, conditions, and turn order
- "Next Turn" button advances initiative
Keyboard Shortcuts:
| Action | Key |
|---|---|
| Fit all tokens | F |
| Fit combatants | C |
| Hide all enemies | Shift+H |
🔧 Technical Implementation
State Management
// React state hooks
const [currentTool, setCurrentTool] = useState<'select' | 'measure'>('select');
const [selectedTokenId, setSelectedTokenId] = useState<string | null>(null);
const [gridEnabled, setGridEnabled] = useState(true);
const [zoom, setZoom] = useState(1.0);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved' | 'error'>('saved');
Data Fetching (SWR)
const { data: map, mutate: mutateMap } = useSWR(`/api/maps/${mapId}`, fetcher);
const { data: tokens, mutate: mutateTokens } = useSWR(`/api/maps/${mapId}/tokens`, fetcher);
const { data: content } = useSWR(`/api/campaigns/${campaignId}/content`, fetcher);
Auto-Save with Debounce
const AUTO_SAVE_DELAY = 2000; // 2 seconds
// Debounced save on calibration/settings changes
useEffect(() => {
const timer = setTimeout(() => {
handleSaveSettings();
}, AUTO_SAVE_DELAY);
return () => clearTimeout(timer);
}, [calibration, gridOpacity, gridSize]);
Canvas Rendering (HTML5 Canvas)
// Layers rendered in order:
1. drawBackground(imageUrl)
2. drawGrid(calibration, opacity)
3. drawTokens(tokens, selectedId)
4. drawMeasurements(points, distances)
5. drawHighContrast(colors) // if enabled
📱 Responsive Design
Desktop (1920x1080+):
- Full sidebar + canvas
- Large toolbar with all controls visible
- Optimal for map editing
Tablet (768px - 1920px):
- Collapsible sidebar (default open)
- Full toolbar
- Touch-friendly buttons
Mobile (< 768px):
- Sidebar hidden by default
- Compact toolbar (icons only)
- Touch gestures for pan/zoom
- Simplified token placement
🎯 User Workflows
Workflow 1: Create New Map
- Click "New Map" in campaign
- Enter map title
- Upload battle map image (PNG/JPG/WebP, max 10MB)
- Calibrate grid (two-point or quick method)
- Adjust grid opacity/size as needed
- Map ready for token placement
Workflow 2: Place Tokens
- Open map editor
- Search content library (e.g., "goblin")
- Click content item or drag onto map
- Token appears at map center or drag location
- Drag token to final position
- Auto-saves after 2 seconds
Workflow 3: Measure Distance
- Activate measure tool (R key or toolbar)
- Click starting point on map
- Click destination point
- See distance in feet and grid squares
- Click additional points for multi-segment measurements
- Press Escape to clear
Workflow 4: High Contrast Mode
- Toggle high contrast in toolbar (C key)
- Or: System detects
prefers-contrast: more - Grid, tokens, measurements switch to high contrast colors
- Preference saved to localStorage
🚀 Performance Optimizations
Canvas Rendering
- Memoized Drawing Functions: Only redraw changed layers
- Request Animation Frame: Smooth 60fps rendering
- Viewport Culling: Only render visible tokens
- Image Caching: Background map cached in memory
State Updates
- Debounced Auto-save: Prevents excessive API calls
- SWR Caching: Efficient data fetching with automatic revalidation
- Optimistic Updates: Token moves show instantly, sync in background
Image Loading
- Progressive Loading: Show placeholder while loading
- Lazy Load: Sidebar content loads on-demand
- Compression: Server-side image optimization
- Thumbnails: Generate thumbnails for large maps
🐛 Error Handling
Map Not Found
┌─────────────────────────────┐
│ ⚠️ │
│ Map Not Found │
│ │
│ The map you're looking for │
│ doesn't exist or you don't │
│ have access. │
│ │
│ [← Back to Campaign] │
└─────────────────────────────┘
Upload Errors
- File too large (>10MB)
- Invalid format (not PNG/JPG/WebP)
- Magic number validation failures
- Network errors
Save Errors
- Network disconnection
- Session expired
- Server errors
- Quota exceeded
Error Toast Notifications:
- Position: Bottom right
- Duration: 5 seconds
- Types: Error (red), Success (green), Info (blue)
📊 Analytics & Tracking
Events Tracked (Future)
- Map created
- Map image uploaded
- Grid calibrated
- Token placed
- Token moved
- Measurement taken
- High contrast toggled
- Time spent in editor
🔜 Future Enhancements
Shipped
- Token health/status indicators — shipped as "Mark as Defeated" (grayed token with red X overlay, non-destructive)
- Player view mode — shipped via Tactical Grid View and Combat Mode
- GM controls (reveal/hide areas) — shipped as per-enemy reveal/hide controls in Tactical and Combat views
Phase 2 Features
- Token rotation controls
- Elevation/height visualization
- Fog of war
- Dynamic lighting
- Line of sight calculations
- Token collision detection
- Multi-select tokens
- Copy/paste tokens
- Undo/redo history
Advanced Tools
- Drawing tools (freehand, shapes)
- Text annotations
- Area markers (radius, cone, cube)
- Terrain effects (difficult terrain, hazards)
- Initiative tracker integration
- Dice roller integration
Collaboration
- Real-time multiplayer editing
- Shared cursors
- Chat integration
📝 Developer Notes
Key Files
- Page:
src/app/[locale]/campaigns/[id]/maps/[mapId]/page.tsx - Canvas:
src/components/maps/MapCanvas.tsx - Toolbar:
src/components/maps/MapToolbar.tsx - Grid:
src/components/maps/GridOverlay.tsx - Measurement:
src/components/maps/MeasurementTool.tsx - Accessibility:
src/components/maps/useTokenKeyboardNavigation.ts - High Contrast:
src/components/maps/useHighContrast.ts - Enemy Context Menu:
src/components/map/enemy-context-menu.tsx - Visual Preview:
src/components/map/map-visual-preview.tsx - Canvas Orchestrator:
src/components/map/map-canvas-orchestrator.tsx - 2D Canvas:
src/components/map/map-2d-canvas.tsx - Pixi Token Layer:
src/lib/map/renderer/pixi/pixi-token-layer.ts - Battle Map Store:
src/stores/battle-map-layer-store.ts - Combat Grid View:
src/components/tactical/CombatGridView.tsx - Tactical Grid View:
src/components/tactical/TacticalGridView.tsx - Enemy Positions API:
src/app/api/maps/[id]/enemy-positions/route.ts
Grid Utilities
- Coordinate Conversion:
src/lib/maps/grid-utils.ts - Distance Calculation:
src/lib/maps/measurement-utils.ts - File Validation:
src/lib/maps/map-validation-service.ts
API Routes
GET /api/maps- List mapsPOST /api/maps- Create mapGET /api/maps/[id]- Get map detailsPATCH /api/maps/[id]- Update settingsDELETE /api/maps/[id]- Delete mapPOST /api/maps/[id]/upload- Upload imageGET /api/maps/[id]/tokens- List tokensPOST /api/maps/[id]/tokens- Add tokenPATCH /api/maps/[id]/tokens/[tokenId]- Update tokenDELETE /api/maps/[id]/tokens/[tokenId]- Remove tokenPATCH /api/maps/[id]/enemy-positions- Update enemy positionDELETE /api/maps/[id]/enemy-positions- Remove enemy from map
Last Updated: 2026-03-06 Status: Production Ready Test Coverage: 147 unit tests (100% of critical utilities)