-
Notifications
You must be signed in to change notification settings - Fork 7
Lab 6: Paint Application
Before we dive into the code, let's understand what we're building:
- A web-based paint application using React
- Canvas-based drawing functionality
- Real-time brush customization
- Form handling with validation
- Styled components using Tailwind CSS
First, let's set up our font and base styling:
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
/* Base App Container */
.App {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
}
/* Drawing Area */
.draw-area {
width: 1280px;
height: 720px;
border: 2px solid #808080;
position: relative;
background-color: white;
}
/* Menu Container */
.Menu {
width: 650px;
height: 50px;
display: flex;
justify-content: space-evenly;
border-radius: 5px;
align-items: center;
background-color: #a3a3a32d;
margin: auto;
margin-top: 10px;
}
Key Setup Points:
- Custom font import for stylized headings
- Flexible container layout
- Consistent sizing for drawing area
- Clean, minimal menu design
We'll refactor our CSS to use Tailwind utility classes:
Before:
.App {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
}
After:
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex flex-col items-center">
Benefits of Tailwind:
- Inline styling without CSS files
- Consistent design system
- Rapid development
- Easy responsive design
- No class naming conflicts
Our app consists of three main parts:
- Main App component (canvas management)
- Brush Settings Form (controls)
- Styled Menu (UI wrapper)
First, let's implement the basic canvas functionality:
const App = () => {
const canvasRef = useRef(null);
const ctxRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
// Basic brush properties
const [lineWidth, setLineWidth] = useState(5);
const [lineColor, setLineColor] = useState("black");
const [lineOpacity, setLineOpacity] = useState(0.1);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.globalAlpha = lineOpacity;
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
ctxRef.current = ctx;
}, [lineColor, lineOpacity, lineWidth]);
}
Key Implementation Points:
- We use
useRef
to maintain references to the canvas element and its context -
useState
manages drawing state and brush properties -
useEffect
initializes and updates the canvas context when brush properties change
Next, we implement the core drawing functionality:
const startDrawing = (e) => {
ctxRef.current.beginPath();
ctxRef.current.moveTo(
e.nativeEvent.offsetX,
e.nativeEvent.offsetY
);
setIsDrawing(true);
};
const endDrawing = () => {
ctxRef.current.closePath();
setIsDrawing(false);
};
const draw = (e) => {
if (!isDrawing) return;
ctxRef.current.lineTo(
e.nativeEvent.offsetX,
e.nativeEvent.offsetY
);
ctxRef.current.stroke();
};
Implementation Details:
-
startDrawing
: Begins a new path when mouse is pressed -
endDrawing
: Closes the path when mouse is released -
draw
: Continues the path while mouse is moving - Uses native mouse event coordinates for precise drawing
Let's create our styled components using Tailwind:
const StyledMenu = ({ className, children }) => (
<div className={`${className} bg-gray-200/20 rounded-md p-4 mb-4 w-[650px] flex justify-evenly items-center`}>
{children}
</div>
);
const ErrorAlert = ({ children }) => (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-2 rounded relative text-sm">
{children}
</div>
);
Key Styling Concepts:
- Component-based styling approach
- Reusable UI components
- Tailwind utility classes for rapid styling
- Composition through className props
Now for the brush settings form with validation:
const BrushSettingsForm = ({ setLineColor, setLineWidth, setLineOpacity }) => {
const [formValues, setFormValues] = useState({
color: "#000000",
width: "5",
opacity: "10"
});
const [errors, setErrors] = useState({});
const validateForm = (name, value) => {
switch (name) {
case 'width':
return value >= 3 && value <= 20 ? '' : 'Width must be between 3 and 20';
case 'opacity':
return value >= 1 && value <= 100 ? '' : 'Opacity must be between 1 and 100';
default:
return '';
}
};
const handleChange = (e) => {
const { name, value } = e.target;
const error = validateForm(name, value);
setFormValues(prev => ({
...prev,
[name]: value
}));
setErrors(prev => ({
...prev,
[name]: error
}));
if (!error) {
// Update canvas settings
switch (name) {
case 'color':
setLineColor(value);
break;
case 'width':
setLineWidth(value);
break;
case 'opacity':
setLineOpacity(value / 100);
break;
}
}
};
}
Form Implementation Highlights:
- Controlled form inputs
- Real-time validation
- Error state management
- Input range constraints
- Immediate feedback
- Clean error handling
Finally, let's look at how everything connects:
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex flex-col items-center">
<h1 className="font-['Lobster'] text-5xl text-blue-600 my-6">Paint App</h1>
<div className="bg-white border-2 border-gray-300 rounded-lg p-4">
<BrushSettingsForm
setLineColor={setLineColor}
setLineWidth={setLineWidth}
setLineOpacity={setLineOpacity}
/>
<canvas
onMouseDown={startDrawing}
onMouseUp={endDrawing}
onMouseMove={draw}
ref={canvasRef}
width={1280}
height={720}
className="border border-gray-200"
/>
</div>
</div>
);
Final Implementation Notes:
- Clean component hierarchy
- Event handler attachment
- Canvas size definition
- Responsive layout
- Aesthetic styling
Description: Allow users to step backwards and forwards through their drawing actions, enabling error recovery and experimentation.
Implementation Narrative: We'll use a command pattern to store drawing actions in two stacks - one for undo and one for redo. Each drawing action will save the canvas state using toDataURL()
. When users undo/redo, we'll restore the appropriate state.
First Steps:
- Create two arrays:
undoStack
andredoStack
usinguseState
- Modify the
draw
function to save canvas state after each complete stroke - Implement
handleUndo
andhandleRedo
functions that pop/push states between stacks
Description: Enable users to save their artwork locally or to the cloud, and load it back for continued editing.
Implementation Narrative: We'll start with local saving using the browser's download capabilities, then add cloud storage using Firebase. We'll save both the final image and a JSON representation of the drawing commands.
First Steps:
- Add "Save" button that converts canvas to PNG using
toDataURL()
- Implement download functionality using
<a>
tag with download attribute - Create a data structure to store drawing history and settings
Description: Expand beyond basic lines to include brushes like airbrush, calligraphy pen, and pattern stamp.
Implementation Narrative: We'll create a brush system that modifies the canvas context's drawing behavior. Each brush type will be a class with its own draw method and settings.
First Steps:
- Create a
Brush
base class with core drawing methods - Implement an
AirBrush
class that uses multiple small circles for spray effect - Add brush type selector to the settings form
Description: Allow users to work with multiple layers, similar to professional drawing software.
Implementation Narrative: We'll create multiple canvas elements stacked on top of each other, each representing a layer. Users can reorder, show/hide, and adjust opacity of each layer.
First Steps:
- Create a
Layer
component that manages an individual canvas - Implement layer list UI with visibility toggles
- Add layer ordering functionality with drag-and-drop
Description: Enhanced color selection with saved palettes, color history, and advanced color manipulation.
Implementation Narrative: We'll build a color picker that includes RGB/HSL controls, recently used colors, and the ability to save custom palettes.
First Steps:
- Create a
ColorHistory
component that stores recently used colors - Implement custom palette storage using localStorage
- Add RGB/HSL sliders for precise color control
Description: Optimize the drawing experience for touch devices with pressure sensitivity and gesture controls.
Implementation Narrative: We'll use the Pointer Events API to handle both mouse and touch input, adding support for pressure sensitivity on compatible devices.
First Steps:
- Replace mouse events with pointer events (
pointerdown
,pointermove
, etc.) - Add pressure sensitivity support using
pressure
property - Implement pinch-to-zoom gesture handling
Description: Allow users to import and use images as stamps, with rotation, scaling, and opacity controls.
Implementation Narrative: We'll create a system for users to upload images and use them as repeatable stamps on the canvas. The stamps can be rotated, scaled, and applied with varying opacity.
First Steps:
- Create an image upload system with preview thumbnails
- Implement
drawImage
functionality with transformation controls - Add stamp settings panel (rotation, scale, opacity) to the form
Each of these features builds upon our base application, adding professional-grade functionality. They can be implemented independently or combined for a more powerful drawing application.