The Play Canvas is a professional football play diagramming tool built with React and Konva.js. It provides coaches with an infinite canvas for creating, editing, and managing football play diagrams with a component-based reusable system.
Location: /src/components/PlayCanvas.tsx
Type System: /src/types/visualPlaybook.ts
Dependencies: React, Konva, react-konva, Lucide React icons
- Konva.js: 2D canvas library providing the rendering engine
- react-konva: React wrapper for Konva.js enabling declarative canvas elements
- TypeScript: Full type safety for all canvas elements and interactions
- Lucide React: Icon library for tool buttons and UI elements
PlayCanvas Component (Main)
├── State Management (useState hooks)
├── Event Handlers
├── UI Components
│ ├── Top Toolbar
│ │ ├── Undo/Redo Controls
│ │ ├── Component Management
│ │ ├── View Controls (Grid, Snap)
│ │ ├── Zoom Controls
│ │ └── Actions (Copy, Delete, Export)
│ ├── Side Toolbar
│ │ ├── Selection Tool
│ │ ├── Shape Tools (Square, Circle, Text)
│ │ └── Line Tools (Straight, Arrow, Curved)
│ └── Canvas Area
│ ├── Konva Stage
│ ├── Konva Layer
│ ├── Grid System
│ ├── Selection Rectangle
│ ├── Canvas Elements
│ └── Transformer
├── Floating Components
│ ├── Component List
│ └── Help Text
└── Helper Functions// Tool Selection
const [tool, setTool] = useState<Tool>('select');
// Tools: 'select' | 'pan' | 'square' | 'circle' | 'text' | 'line' | 'arrow' | 'curved'
// Canvas Elements
const [elements, setElements] = useState<CanvasElement[]>([]);
// Stores all drawn elements on the canvas
// Selection System
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [isSelecting, setIsSelecting] = useState(false);
const [selectionRect, setSelectionRect] = useState({
x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0
});
// Component System
const [components, setComponents] = useState<PlaybookComponent[]>([]);
// Saved reusable component groups
// Canvas View
const [scale, setScale] = useState(1); // Zoom level
const [stagePos, setStagePos] = useState({ x: 400, y: 50 }); // Canvas position (updated for field view)
// Grid and Field System
const [showGrid, setShowGrid] = useState(true);
const [snapToGrid, setSnapToGrid] = useState(false);
const [showField, setShowField] = useState(false); // NEW: NCAA football field overlay
const [fieldScale, setFieldScale] = useState(2.5); // NEW: Field size multiplier
const gridSize = 20; // Fixed 20px grid
// Drawing State
const [isDrawing, setIsDrawing] = useState(false);
const [linePoints, setLinePoints] = useState<number[]>([]);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
// History System
const [history, setHistory] = useState<CanvasElement[][]>([[]]);
const [historyIndex, setHistoryIndex] = useState(0);
// Keyboard Modifiers
const [isShiftPressed, setIsShiftPressed] = useState(false);
const [isSpacePressed, setIsSpacePressed] = useState(false);
// Clipboard
const [copiedElements, setCopiedElements] = useState<CanvasElement[]>([]);interface CanvasElement {
id: string; // Unique identifier
type: ElementType; // 'square' | 'circle' | 'text' | 'line' | 'arrow' | 'curved'
x: number; // X position
y: number; // Y position
// Shape-specific properties
width?: number; // For squares
height?: number; // For squares
radius?: number; // For circles
text?: string; // For text elements
fontSize?: number; // For text
fontFamily?: string; // For text
fontStyle?: string; // For text
// Line-specific properties
points?: number[]; // Array of [x1, y1, x2, y2, ...] for lines
tension?: number; // Curve tension for curved lines
pointerLength?: number; // Arrow head length
pointerWidth?: number; // Arrow head width
// Styling
fill?: string; // Fill color
stroke?: string; // Stroke color
strokeWidth?: number; // Stroke width
cornerRadius?: number; // Rounded corners for squares
rotation?: number; // Rotation in degrees
// Visual effects
dash?: number[]; // Dashed line pattern
opacity?: number; // Element opacity
shadowColor?: string; // Shadow color
shadowBlur?: number; // Shadow blur radius
shadowOpacity?: number; // Shadow opacity
}interface PlaybookComponent {
id: string; // Unique identifier
name: string; // Component name (uppercase)
elements: CanvasElement[]; // Array of elements in component
createdAt: Date; // Creation timestamp
updatedAt: Date; // Last update timestamp
}- Purpose: Initiates selection rectangle, element placement, or line drawing
- Behavior:
- In select mode + empty canvas: Starts selection rectangle
- In line tools + empty canvas: NEW Starts line drawing (click-and-drag)
- Prevents canvas dragging while selecting
- Sets up selection rectangle coordinates or line start point
- Purpose: Places new elements on canvas
- Behavior:
- Only fires when not in select mode
- Places shapes/text at click position
- Handles line drawing (start/end points)
- Respects snap-to-grid if enabled
- Purpose: Updates selection rectangle and line preview
- Behavior:
- Updates selection rectangle dimensions
- Calculates element intersections
- Shows line preview while drawing
- Handles Shift constraint for 45° angles
- Purpose: Completes selection rectangle or line drawing
- Behavior:
- Finalizes multi-select operation
- NEW Completes line drawing (click-and-drag behavior)
- Applies minimum length check for lines (10px minimum)
- Elements remain selected
Select All: Ctrl+A
Copy: Ctrl+C
Paste: Ctrl+V
Duplicate: Ctrl+D
Undo: Ctrl+Z
Redo: Ctrl+Y
Delete: Delete key
Pan Canvas: Space (hold) + drag
Constrain: Shift (hold) - constrains lines to 45° angles
Cancel: Escape - cancels current operation
- Mouse Wheel: Zoom in/out centered on cursor
- Zoom Buttons: Fixed percentage zoom
- Scale Range: 10% to 500%
- Algorithm: Maintains cursor position during zoom
- Pan Tool: NEW Dedicated hand tool for canvas navigation
- Space + Drag: Pan the canvas view (legacy method)
- Visual Feedback: Cursor changes to grab hand
- State: Uses
isSpacePressedflag ortool === 'pan' - Stage Draggable: Enabled when pan tool is active or space is pressed
- Multi-select: Drag rectangle to select multiple
- Single select: Click individual elements
- Add to selection: Shift+click
- Move elements: Drag selected elements
- Pan canvas: Space + drag
- Purpose: Navigate the infinite canvas
- Interaction: Click and drag to move view
- Cursor: Shows grab hand
- Alternative: Space + drag in select mode
- Purpose: Linemen (O-Line/D-Line)
- Default: 40x40px, white fill, black stroke
- Features: Rounded corners (4px radius)
- Purpose: Skill position players
- Default: 20px radius, white fill, black stroke
- Features: Perfect circles for players
- Purpose: Position labels (QB, RB, WR, etc.)
- Default: 20px font, bold, system font
- Input: Prompt dialog for text entry
- Purpose: Basic routes and blocking
- Interaction: UPDATED Click and drag (not click-start, click-end)
- Shift Constraint: 45° angle snapping
- Minimum Length: 10px to prevent accidental dots
- Purpose: Pass routes with direction
- Features: Arrow head (10px)
- Width: 3px stroke
- Interaction: UPDATED Click and drag
- Preview: Shows dashed arrow while dragging
- Purpose: Motion and complex routes
- Tension: 0.5 for smooth curves
- Interaction: UPDATED Click and drag
- Auto-Curve: NEW Curves perpendicular to drag direction (30px offset)
- Preview: Shows curved preview while dragging
- Select multiple elements (drag selection)
- Click "Save" button
- Enter component name (auto-uppercase)
- Elements saved with relative positioning
- Toast notification confirms save
- Stored in localStorage
- Type component name(s) in input
- Click "Load" or press Enter
- Multiple components space-separated
- Places at offset position (100, 100)
- Automatically selected after load
- Location: Browser localStorage
- Key: 'playbookComponents'
- Format: JSON stringified array
- Persistence: Survives page refresh
- Size: 20px squares
- Color: #e5e7eb (light gray)
- Toggle: Grid button in toolbar
- Performance: Non-interactive (listening: false)
- Toggle: Lock/Unlock button
- Behavior: Rounds to nearest 20px
- Applies to: All placement and movement
- Visual: Lock icon when enabled
- Toggle: Green football icon (Activity) in toolbar
- Orientation: Vertical (end zones at top/bottom)
- Dimensions:
- Width: 53⅓ yards (NCAA regulation)
- Length: 120 yards (100 field + 2×10 end zones)
- Scale: Adjustable 1.0x to 5.0x via slider
- Components:
- Field boundary with light gray stroke
- End zones with light shading
- Yard lines every 5 yards (thicker every 10)
- Yard numbers (10, 20, 30, 40, 50)
- Hash marks every yard at NCAA positions (20 yards from sidelines)
- Auto-Zoom: Automatically zooms out to 50% when field is enabled
- Field Scale Slider: Appears when field is active for size adjustment
- Use Case: Provides realistic proportions for play diagramming
- Initiation: Mouse down on empty canvas
- Visual: Blue dashed rectangle with semi-transparent fill
- Calculation: Intersection detection for all elements
- Completion: Mouse up finalizes selection
getElementBounds(element) {
switch(type):
'square': { x, y, width, height }
'circle': { x: centerX - radius, y: centerY - radius, width: radius*2, height: radius*2 }
'text': { x, y, width: ~100px, height: fontSize }
'line/arrow/curved': { bounding box of all points }
}- Algorithm: AABB (Axis-Aligned Bounding Box)
- Efficiency: O(n) for n elements
- Updates: Real-time during drag
- Structure: Array of canvas states
- Index: Current position in history
- Save Points: After each modification
- Limit: Unbounded (consider adding max)
undo(): Moves to previous state
redo(): Moves to next state
saveToHistory(): Adds current state- Uses Konva's
toDataURL()method - Creates download link
- Auto-downloads as 'playbook.png'
- Includes all visible elements
- Batching: Transformer batch updates
- Listening: Grid lines non-interactive
- Preview elements: Non-interactive during draw
- Selective updates: Only modified elements
- ID generation: Timestamp + random for uniqueness
- Local operations: No server calls
- Selected elements: Blue stroke (#0066ff)
- Increased stroke width: 3px when selected
- Shadow effect: Blue shadow on selection
- Active tool: Blue background in toolbar
- Cursor changes: Based on current tool/mode
- Help text: Context-sensitive instructions
- Lines: Dashed preview while drawing
- Color: Gray (#666666)
- Updates: Follow mouse position
- Origin: Top-left (0, 0)
- Relative positioning: Uses
getRelativePointerPosition() - Fixes shape jumping: Accurate coordinate calculation
- Canvas stability: No auto-centering
- No auto-switch: Tools stay selected
- Rapid placement: Multiple shapes without interruption
- Clean state: Tool switches clear in-progress operations
- Initial offset: (100, 100) for better workspace
- Pan control: Only with Space+drag
- No view snapping: Canvas stays where positioned
{
"playbookComponents": [
{
"id": "element_123456_abc",
"name": "TRIPS RIGHT",
"elements": [...],
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
]
}- Route: Accessible via palette icon in navigation
- Component: Standalone, no external dependencies
- State: Self-contained, no global state
- Play Library: Save diagrams with play calls
- Practice Scripts: Attach diagrams to plays
- Export Options: SVG, PDF formats
- Collaboration: Real-time multi-user editing
- Animation: Play progression/motion
- Add to
Tooltype invisualPlaybook.ts - Add tool button in side toolbar
- Add handler in
handleStageClick - Add element rendering in render section
- Add help text for tool
- Update
CanvasElementinterface - Add property handling in element creation
- Update rendering to use property
- Consider bounds calculation impact
- Grid size: Change
gridSizeconstant - Grid color: Modify stroke in grid rendering
- Snap behavior: Adjust
snapValuefunction
-
Shapes jumping on placement
- Check coordinate calculation
- Verify
getRelativePointerPosition()usage - Ensure no stage position updates
-
Selection not working
- Verify
isSelectingstate - Check mouse event handlers
- Ensure proper event propagation
- Verify
-
Canvas moving unexpectedly
- Check
draggableprop conditions - Verify
isSpacePressedlogic - Review stage position updates
- Check
-
Tools not switching
- Check tool button onClick handlers
- Verify state cleanup on switch
- Ensure no blocking conditions
- Always use
getRelativePointerPosition()for accurate coordinates - Clear in-progress operations when switching tools
- Provide visual feedback for all interactions
- Save to history after modifications
- Use non-interactive elements for previews/guides
- Maintain tool state for rapid workflows
- Test with multiple elements for performance
- Update types in
visualPlaybook.ts - Add state variables if needed
- Implement event handlers
- Add UI controls
- Update rendering logic
- Add help text
- Test all interactions
- Multi-select works correctly
- Snap to grid functions properly
- Undo/redo maintains consistency
- Components save/load correctly
- Export produces valid images
- Keyboard shortcuts work
- Touch support functions
- Performance with 100+ elements
- ✅ Pan Tool: Dedicated hand tool for canvas navigation
- ✅ NCAA Football Field: Regulation field overlay with adjustable scaling
- ✅ Click-and-Drag Lines: Updated all line tools to use click-and-drag interaction
- ✅ Auto-Curve System: Curved lines automatically bend perpendicular to drag direction
- ✅ Field Scale Control: Dynamic field sizing with 1.0x-5.0x range
- ✅ Improved UX: Better initial positioning and auto-zoom for field view
- Freehand drawing tool
- Shape library (triangles, polygons)
- Text editing in-place
- Line style options (dotted, thick)
- Copy/paste between field and non-field views
- Layers system
- Grouping without components
- Alignment tools
- Measurement tools
- Copy formatting
- Connect to Play Library
- Template system
- Import from image
- Video export for animations
- Collaborative editing
This component is a core part of the Football Play/Roster Manager system. For questions or issues, refer to the main CLAUDE.md documentation.
Last Updated: January 2025 Version: 1.1.0 - Navigation and Field Enhancements