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