Building Interactive Drawing Tools with React Three Fiber
Learn how to create a complete 2D drawing application using React Three Fiber with multiple tools, snap-to-grid, real-time measurements, and interactive object management.
Building Interactive Drawing Tools with React Three Fiber
Creating interactive drawing tools in a web browser traditionally requires complex 2D canvas manipulation or SVG handling. However, React Three Fiber (R3F) offers a unique approach - leveraging 3D graphics capabilities to build sophisticated 2D drawing applications with excellent performance and developer experience.
In this tutorial, we'll build a complete drawing application featuring multiple tools, snap-to-grid functionality, real-time measurements, and object management - all within a 2D orthographic view.
Demo
Why React Three Fiber for 2D Drawing?
While it might seem counterintuitive to use a 3D library for 2D drawing, R3F provides several advantages:
- Declarative approach: Components map naturally to drawing objects
- Built-in interaction: Pointer events work seamlessly
- Performance: WebGL acceleration handles complex scenes efficiently
- Coordinate system: Precise mathematical positioning
- Text rendering: Built-in text components with proper positioning
Core Architecture
Our drawing tool consists of several key components:
1. Tool Management System
const [currentTool, setCurrentTool] = useState('line')
const [isDrawing, setIsDrawing] = useState(false)
const [drawingState, setDrawingState] = useState(null)
const [drawnObjects, setDrawnObjects] = useState([])
Each tool maintains its own state during the drawing process, then creates a permanent object when completed.
2. Interactive Canvas
The drawing surface is a plane mesh that captures pointer events:
<mesh
  ref={planeRef}
  position={[0, 0, 0]}
  onPointerMove={handlePointerMove}
  onPointerOut={handlePointerOut}
  onClick={handleCanvasClick}
>
  <planeGeometry args={[20, 20]} />
  <meshBasicMaterial transparent opacity={0.1} color="lightblue" />
</mesh>
3. Coordinate Transformation
Converting from screen space to drawing coordinates requires careful handling:
const localPoint = planeRef.current.worldToLocal(e.point.clone())
if (snapEnabled) {
  localPoint.x = Math.round(localPoint.x / snapValue) * snapValue
  localPoint.y = Math.round(localPoint.y / snapValue) * snapValue
}
const drawingPoint = { x: localPoint.x, y: localPoint.y, z: 0 }
Drawing Tools Implementation
Line Tool
The line tool demonstrates the basic drawing pattern:
- First click: Set start point and enter drawing mode
- Mouse move: Update preview line endpoint
- Second click: Finalize line and exit drawing mode
if (currentTool === 'line') {
  if (!isDrawing) {
    setIsDrawing(true)
    setDrawingState({ start: snappedPoint, end: snappedPoint })
  } else {
    setDrawnObjects(prev => [...prev, {
      type: 'line',
      id: Date.now(),
      start: drawingState.start,
      end: snappedPoint
    }])
    setIsDrawing(false)
    setDrawingState(null)
  }
}
Shape Tool (Polygon)
The shape tool handles multi-point drawing with completion logic:
if (currentTool === 'shape') {
  if (!isDrawing) {
    setIsDrawing(true)
    setDrawingState({ points: [snappedPoint] })
  } else {
    const newPoints = [...drawingState.points, snappedPoint]
    setDrawingState({ points: newPoints })
  }
}
Shapes can be completed by:
- Clicking the starting point (green indicator)
- Pressing the ESC key
Rectangle Tool
Rectangles use a corner-to-corner approach:
if (currentTool === 'rectangle') {
  if (!isDrawing) {
    setIsDrawing(true)
    setDrawingState({ corner1: snappedPoint, corner2: snappedPoint })
  } else {
    setDrawnObjects(prev => [...prev, {
      type: 'rectangle',
      id: Date.now(),
      corner1: drawingState.corner1,
      corner2: snappedPoint
    }])
    setIsDrawing(false)
    setDrawingState(null)
  }
}
Advanced Features
Snap-to-Grid
Grid snapping ensures precise positioning:
const snapToGrid = useCallback((point) => {
  if (!snapEnabled) return point
  return {
    x: Math.round(point.x / snapValue) * snapValue,
    y: Math.round(point.y / snapValue) * snapValue,
    z: 0
  }
}, [snapEnabled, snapValue])
The grid is visualized using line components:
for (let i = -divisions; i <= divisions; i++) {
  const pos = i * snapValue
  lines.push(
    <Line 
      key={`h${i}`}
      points={[[-size/2, pos, 0], [size/2, pos, 0]]}
      color="gray"
      lineWidth={0.5}
    />
  )
}
Real-Time Measurements
Length calculations use the distance formula:
const calculateDistance = (p1, p2) => {
  return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2))
}
Measurements are displayed at segment midpoints:
const midpoint = {
  x: (start.x + end.x) / 2,
  y: (start.y + end.y) / 2
}
return (
  <Text
    position={[midpoint.x, midpoint.y, 0.1]}
    fontSize={0.2}
    color="black"
    anchorX="center"
    anchorY="middle"
  >
    {length.toFixed(2)}
  </Text>
)
Delete Tool
Objects become clickable when the delete tool is active:
const commonProps = isDeleteMode ? {
  onPointerEnter: () => setIsHovered(true),
  onPointerLeave: () => setIsHovered(false),
  onClick: (e) => {
    e.stopPropagation()
    onDelete()
  }
} : {}
const color = isDeleteMode && isHovered ? 'red' : undefined
Camera Configuration
For 2D drawing, we use an orthographic camera positioned directly above:
<Canvas 
  orthographic 
  camera={{ zoom: 50, near: 0.1, far: 200, position: [0, 0, 15] }}
>
This eliminates perspective distortion and provides true-to-scale measurements.
Rendering Strategy
Each drawing object type has its own component:
switch (object.type) {
  case 'line':
    return <DrawnLine {...object} />
  case 'shape':
    return <DrawnShape {...object} />  
  case 'rectangle':
    return <DrawnRectangle {...object} />
}
This modular approach makes it easy to add visual enhancements like edge lines:
{/* Edge lines for definition */}
<Line 
  points={[
    [minX, minY, 0.01], [maxX, minY, 0.01],
    [maxX, maxY, 0.01], [minX, maxY, 0.01],
    [minX, minY, 0.01]
  ]}
  color="black"
  lineWidth={1}
/>
Key Takeaways
Building drawing tools with React Three Fiber demonstrates several important concepts:
- State management: Complex interactions require careful state coordination
- Event handling: Pointer events need proper propagation control
- Coordinate systems: Understanding world vs local coordinates is crucial
- Performance: Declarative updates handle dynamic scenes efficiently
- User experience: Visual feedback makes tools intuitive to use
The resulting application provides a professional drawing experience with features comparable to desktop CAD software, all running in a web browser with smooth 60fps performance.
Next Steps
This foundation could be extended with additional features:
- Import/Export: Save drawings as JSON or export to SVG
- Layers: Organize objects on separate layers
- Styling: Line weights, colors, and fill patterns
- Dimensions: Automatic dimensioning with leader lines
- Templates: Predefined shapes and symbols
React Three Fiber's flexibility makes these enhancements straightforward to implement while maintaining good performance and code organization.
Jeremy Atkinson
Jeremy is a structural engineer, researcher, and developer from BC. He works on Calcs.app and writes at Kinson.io