⚔️CritForge
🗺️

Map Editor

Advanced map editor features and UX patterns

mapseditoruxdesign

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 tool
  • R - Measure tool
  • G - Toggle grid
  • C - Toggle high contrast
  • +/- - Zoom in/out
  • 0 - Fit to viewport
  • Arrow keys - Move selected token
  • Delete - Remove selected token
  • Escape - Clear selection / Clear measurement
  • Ctrl+Z - Remove last measurement point

4. Map Canvas

Rendering Layers (bottom to top):

  1. Background Image: Uploaded battle map
  2. Grid Overlay: Configurable 5ft or 10ft grid with opacity control
  3. Token Layer: Draggable content tokens with selection highlights
  4. 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:

  1. Search content library
  2. Click content item to add to map center
  3. 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:

  1. Activate measure tool (R)
  2. Click first point on map
  3. Click additional points to add segments
  4. Each segment shows:
    • Distance in feet
    • Grid squares
    • Running total
  5. 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 measurements
    • Backspace: 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:

  1. Two-Point: Click two map points, specify distance
  2. 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:

TabPurpose
DetailsRead-aloud text, tactical description, terrain features, hazards, enemy positions (list form), entry/exit points, world setting
Visual PreviewInteractive 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 (Home key)

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:

ViewRoutePurpose
Tactical Grid/encounters/[id]/tacticalFull-screen encounter prep — layer controls, reveal/hide, count adjustment, remove, style switching
Combat Mode/encounters/[id]/tactical/combatLive play — initiative tracking, HP/condition management, reveal/hide, mark-as-defeated

The "Go Live →" button in Tactical Grid transitions to Combat Mode.

Feature Comparison

FeatureVisual PreviewTactical GridCombat 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 shortcutsHomeF C Shift+HF C Shift+H
Data persistenceAPI (PATCH/DELETE)Zustand storeZustand 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:

ActionKey
Fit to viewHome

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:

ActionKey
Fit all tokensF
Fit combatantsC
Return to mapHome
Hide all enemiesShift+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:

ActionKey
Fit all tokensF
Fit combatantsC
Hide all enemiesShift+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

  1. Click "New Map" in campaign
  2. Enter map title
  3. Upload battle map image (PNG/JPG/WebP, max 10MB)
  4. Calibrate grid (two-point or quick method)
  5. Adjust grid opacity/size as needed
  6. Map ready for token placement

Workflow 2: Place Tokens

  1. Open map editor
  2. Search content library (e.g., "goblin")
  3. Click content item or drag onto map
  4. Token appears at map center or drag location
  5. Drag token to final position
  6. Auto-saves after 2 seconds

Workflow 3: Measure Distance

  1. Activate measure tool (R key or toolbar)
  2. Click starting point on map
  3. Click destination point
  4. See distance in feet and grid squares
  5. Click additional points for multi-segment measurements
  6. Press Escape to clear

Workflow 4: High Contrast Mode

  1. Toggle high contrast in toolbar (C key)
  2. Or: System detects prefers-contrast: more
  3. Grid, tokens, measurements switch to high contrast colors
  4. 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 maps
  • POST /api/maps - Create map
  • GET /api/maps/[id] - Get map details
  • PATCH /api/maps/[id] - Update settings
  • DELETE /api/maps/[id] - Delete map
  • POST /api/maps/[id]/upload - Upload image
  • GET /api/maps/[id]/tokens - List tokens
  • POST /api/maps/[id]/tokens - Add token
  • PATCH /api/maps/[id]/tokens/[tokenId] - Update token
  • DELETE /api/maps/[id]/tokens/[tokenId] - Remove token
  • PATCH /api/maps/[id]/enemy-positions - Update enemy position
  • DELETE /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)