Skip to content

Lab 6: Paint Application

bobby reed edited this page Nov 14, 2024 · 1 revision

Building a React Paint App

A Step-by-Step Implementation Guide


1. Project Overview

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

2. Initial Setup and Styling

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

3. Transitioning to Tailwind

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

4. Core Components Structure

Our app consists of three main parts:

  1. Main App component (canvas management)
  2. Brush Settings Form (controls)
  3. Styled Menu (UI wrapper)

5. Setting Up the Canvas

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

6. Drawing Functions

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

7. Styled Components

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

8. Form Implementation

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

9. Putting It All Together

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

9. Potential Feature Adds

1. Undo/Redo Functionality

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:

  1. Create two arrays: undoStack and redoStack using useState
  2. Modify the draw function to save canvas state after each complete stroke
  3. Implement handleUndo and handleRedo functions that pop/push states between stacks

2. Save/Load Features

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:

  1. Add "Save" button that converts canvas to PNG using toDataURL()
  2. Implement download functionality using <a> tag with download attribute
  3. Create a data structure to store drawing history and settings

3. Different Brush Types

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:

  1. Create a Brush base class with core drawing methods
  2. Implement an AirBrush class that uses multiple small circles for spray effect
  3. Add brush type selector to the settings form

4. Layer System

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:

  1. Create a Layer component that manages an individual canvas
  2. Implement layer list UI with visibility toggles
  3. Add layer ordering functionality with drag-and-drop

5. Color Picker with Presets

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:

  1. Create a ColorHistory component that stores recently used colors
  2. Implement custom palette storage using localStorage
  3. Add RGB/HSL sliders for precise color control

6. Touch Screen Support

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:

  1. Replace mouse events with pointer events (pointerdown, pointermove, etc.)
  2. Add pressure sensitivity support using pressure property
  3. Implement pinch-to-zoom gesture handling

7. Image Stamp Tool

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:

  1. Create an image upload system with preview thumbnails
  2. Implement drawImage functionality with transformation controls
  3. 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.