Build a Next.js Interactive Web-Based Simple Drawing App

Written by

in

Ever wanted to create your own digital canvas and bring your artistic visions to life? In this comprehensive tutorial, we’ll dive into the world of Next.js and build a simple, yet functional, interactive drawing application. This project is perfect for beginners and intermediate developers looking to hone their skills in front-end development, React, and Next.js. We will explore how to handle user input, manipulate the DOM, and create a smooth and responsive drawing experience.

Why Build a Drawing App?

Building a drawing app provides a fantastic opportunity to learn several key concepts in web development. You’ll get hands-on experience with:

  • Event Handling: Understanding how to capture and respond to user interactions (mouse clicks, mouse movements, etc.).
  • DOM Manipulation: Dynamically creating and modifying elements on the webpage.
  • State Management: Managing the application’s state, such as the current color, line width, and drawing history.
  • Canvas API: Utilizing the HTML Canvas element to render graphics.
  • Responsive Design: Ensuring the app works well on different screen sizes.

Furthermore, a drawing app is a fun and engaging project. You can immediately see the results of your code, which is highly motivating for learning!

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js and npm: You’ll need Node.js and npm (Node Package Manager) installed on your system. You can download them from nodejs.org.
  • Code Editor: A code editor like Visual Studio Code (VS Code), Sublime Text, or Atom.
  • Basic Knowledge of JavaScript, HTML, and CSS: Familiarity with these languages will be helpful but not strictly necessary. We will explain everything step by step.

Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app drawing-app
cd drawing-app

This command creates a new Next.js project named “drawing-app”. After the project is created, navigate into the project directory using the cd command.

Project Structure

Your project directory should look something like this:


drawing-app/
├── node_modules/
├── pages/
│   ├── _app.js
│   └── index.js
├── public/
├── .gitignore
├── next.config.js
├── package.json
├── yarn.lock
└── README.md

The pages directory is where we’ll create our pages. The index.js file inside the pages directory will be the main page of our drawing app.

Building the Drawing App: Step-by-Step

1. Setting Up the Canvas and Basic Structure

Let’s start by modifying the index.js file. Replace the existing code with the following:

import { useRef, useEffect, useState } from 'react';

export default function Home() {
  const canvasRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [color, setColor] = useState('black');
  const [lineWidth, setLineWidth] = useState(5);

  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');

    // Set initial canvas properties
    context.lineCap = 'round';
    context.lineJoin = 'round';

    // Handle resizing the canvas to fit the window
    const resizeCanvas = () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight - 100; // Subtract a bit for UI elements
    };

    resizeCanvas(); // Initial resize
    window.addEventListener('resize', resizeCanvas);

    return () => window.removeEventListener('resize', resizeCanvas);
  }, []);

  const startDrawing = (e) => {
    setIsDrawing(true);
    draw(e);
  };

  const stopDrawing = () => {
    setIsDrawing(false);
    // Optional: Save the drawing here
  };

  const draw = (e) => {
    if (!isDrawing) return;
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    const x = e.clientX - canvas.offsetLeft;
    const y = e.clientY - canvas.offsetTop;

    context.strokeStyle = color;
    context.lineWidth = lineWidth;
    context.lineTo(x, y);
    context.stroke();
    context.beginPath(); // Start a new path for the next line segment
    context.moveTo(x, y);
  };

  return (
    <div style="{{">
      

      {/* UI Elements (Color Picker, Line Width) will go here */}

    </div>
  );
}

Let’s break down this code:

  • Import Statements: We import useRef, useEffect, and useState from React.
  • State Variables:
    • isDrawing: A boolean that indicates whether the user is currently drawing.
    • color: The current drawing color (default: black).
    • lineWidth: The current line width (default: 5 pixels).
  • canvasRef: A React ref that holds a reference to the canvas element. This allows us to access and manipulate the canvas element directly.
  • useEffect Hook:
    • Gets the canvas context.
    • Sets initial properties for the canvas.
    • Handles resizing the canvas to fit the window.
    • Adds and removes a resize event listener to update the canvas size on window resize.
  • Event Handlers:
    • startDrawing: Called when the user clicks the mouse button. It sets isDrawing to true and starts the drawing process.
    • stopDrawing: Called when the user releases the mouse button or moves the mouse outside the canvas. It sets isDrawing to false.
    • draw: The core drawing function. It draws lines on the canvas based on mouse movements.
  • JSX:
    • A div to contain the canvas and UI elements.
    • The canvas element, which is where the drawing will happen.
    • Event listeners (onMouseDown, onMouseUp, onMouseOut, onMouseMove) to capture user interactions.

2. Adding Basic Styling

Let’s add some basic styling to make the app look better. Add the following CSS to the index.js file, inside a <style> tag, or create a separate CSS file and import it. For simplicity, we’ll use inline styles:


  return (
    <div style="{{">
      

      {/* UI Elements (Color Picker, Line Width) will go here */}

      
        {
          `.color-picker {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 10;
            display: flex;
            align-items: center;
          }
          .color-swatch {
            width: 20px;
            height: 20px;
            border: 1px solid #ccc;
            margin-right: 5px;
            cursor: pointer;
          }
          .line-width-slider {
            position: absolute;
            top: 10px;
            left: 150px;
            z-index: 10;
          }
          `}
      
    </div>
  );

This CSS sets up the basic layout and positioning of the canvas, color picker, and line width slider. The canvas is positioned absolutely to fill the screen, and the UI elements will be positioned absolutely on top of the canvas.

3. Adding UI Elements (Color Picker & Line Width)

Now, let’s add the UI elements for selecting the color and line width. Add the following code inside the main <div> element, after the <canvas> element:


  return (
    <div style="{{">
      

      <div>
        <div style="{{"> setColor('black')}
        ></div>
        <div style="{{"> setColor('red')}
        ></div>
        <div style="{{"> setColor('green')}
        ></div>
        <div style="{{"> setColor('blue')}
        ></div>
      </div>

      <div>
        <label>Line Width:</label>
         setLineWidth(parseInt(e.target.value, 10))}
        />
      </div>

      
        {
          `.color-picker {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 10;
            display: flex;
            align-items: center;
          }
          .color-swatch {
            width: 20px;
            height: 20px;
            border: 1px solid #ccc;
            margin-right: 5px;
            cursor: pointer;
          }
          .line-width-slider {
            position: absolute;
            top: 10px;
            left: 150px;
            z-index: 10;
          }
          `}
      
    </div>
  );

Here’s what we added:

  • Color Picker: A div with a class name “color-picker” that contains color swatches (div elements). Each swatch has an inline style for its background color and an onClick event handler that calls setColor to update the current drawing color.
  • Line Width Slider: An input element of type “range” that allows the user to select the line width. The onChange event handler updates the lineWidth state variable.

4. Implementing Clear Canvas Functionality

Let’s add a button to clear the canvas. Add the following code block, inside the main <div> element, after the line width slider:


  const clearCanvas = () => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    context.clearRect(0, 0, canvas.width, canvas.height);
  };

  return (
    <div style="{{">
      

      <div>
        <div style="{{"> setColor('black')}
        ></div>
        <div style="{{"> setColor('red')}
        ></div>
        <div style="{{"> setColor('green')}
        ></div>
        <div style="{{"> setColor('blue')}
        ></div>
      </div>

      <div>
        <label>Line Width:</label>
         setLineWidth(parseInt(e.target.value, 10))}
        />
      </div>

      <button style="{{">Clear</button>

      
        {
          `.color-picker {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 10;
            display: flex;
            align-items: center;
          }
          .color-swatch {
            width: 20px;
            height: 20px;
            border: 1px solid #ccc;
            margin-right: 5px;
            cursor: pointer;
          }
          .line-width-slider {
            position: absolute;
            top: 10px;
            left: 150px;
            z-index: 10;
          }
          `}
      
    </div>
  );

Here’s what’s new:

  • clearCanvas Function: This function clears the entire canvas by using context.clearRect().
  • Clear Button: A button that, when clicked, calls the clearCanvas function.

5. Adding Undo/Redo Functionality (Optional but Recommended)

Undo/Redo functionality significantly enhances the user experience. Here’s how you can implement it:

First, add the following state variables to keep track of the drawing history:


  const [drawingHistory, setDrawingHistory] = useState([]);
  const [historyIndex, setHistoryIndex] = useState(-1); // -1 means no history

Next, modify the draw function to save the canvas state after each stroke:


  const draw = (e) => {
    if (!isDrawing) return;
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    const x = e.clientX - canvas.offsetLeft;
    const y = e.clientY - canvas.offsetTop;

    context.strokeStyle = color;
    context.lineWidth = lineWidth;
    context.lineTo(x, y);
    context.stroke();
    context.beginPath();
    context.moveTo(x, y);

    // Save the current state to the history, after the stroke
    saveCanvasState();
  };

Implement the saveCanvasState, undo, and redo functions:


  const saveCanvasState = () => {
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);

    // Remove any future states
    const newHistory = drawingHistory.slice(0, historyIndex + 1);
    setDrawingHistory([...newHistory, imageData]);
    setHistoryIndex(historyIndex + 1);
  };

  const undo = () => {
    if (historyIndex  {
    if (historyIndex >= drawingHistory.length - 1) return; // No more history to redo
    const canvas = canvasRef.current;
    const context = canvas.getContext('2d');
    setHistoryIndex(historyIndex + 1);
    context.putImageData(drawingHistory[historyIndex + 1], 0, 0);
  };

Finally, add the Undo/Redo buttons to the UI:


  return (
    <div style="{{">
      

      <div>
        <div style="{{"> setColor('black')}
        ></div>
        <div style="{{"> setColor('red')}
        ></div>
        <div style="{{"> setColor('green')}
        ></div>
        <div style="{{"> setColor('blue')}
        ></div>
      </div>

      <div>
        <label>Line Width:</label>
         setLineWidth(parseInt(e.target.value, 10))}
        />
      </div>

      <button style="{{">Clear</button>
      <button onClick={undo} style={{ position: 'absolute', top: 10, right: 50, zIndex: 10 }} disabled={historyIndex Undo</button>
      <button style="{{" disabled="{historyIndex">= drawingHistory.length - 1}>Redo</button>

      
        {
          `.color-picker {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 10;
            display: flex;
            align-items: center;
          }
          .color-swatch {
            width: 20px;
            height: 20px;
            border: 1px solid #ccc;
            margin-right: 5px;
            cursor: pointer;
          }
          .line-width-slider {
            position: absolute;
            top: 10px;
            left: 150px;
            z-index: 10;
          }
          `}
      
    </div>
  );

Here’s what’s added:

  • drawingHistory: An array to store the canvas states (ImageData objects).
  • historyIndex: An index to track the current position in the history.
  • saveCanvasState: Saves the current canvas state to the history.
  • undo: Undoes the last drawing action.
  • redo: Redoes the undone drawing action.
  • Undo/Redo Buttons: Buttons that call undo and redo. They are disabled when there’s no history to undo or redo.

6. Adding Download Functionality (Optional)

Allowing users to download their drawings is a great addition. Here’s how you can implement it:

Add a download button:


  const downloadCanvas = () => {
    const canvas = canvasRef.current;
    const image = canvas.toDataURL('image/png'); // Get the image data
    const link = document.createElement('a');
    link.href = image;
    link.download = 'drawing.png'; // Set the filename
    link.click(); // Trigger the download
  };

  return (
    <div style="{{">
      

      <div>
        <div style="{{"> setColor('black')}
        ></div>
        <div style="{{"> setColor('red')}
        ></div>
        <div style="{{"> setColor('green')}
        ></div>
        <div style="{{"> setColor('blue')}
        ></div>
      </div>

      <div>
        <label>Line Width:</label>
         setLineWidth(parseInt(e.target.value, 10))}
        />
      </div>

      <button style="{{">Clear</button>
      <button onClick={undo} style={{ position: 'absolute', top: 10, right: 100, zIndex: 10 }} disabled={historyIndex Undo</button>
      <button style="{{" disabled="{historyIndex">= drawingHistory.length - 1}>Redo</button>
      <button style="{{">Download</button>

      
        {
          `.color-picker {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 10;
            display: flex;
            align-items: center;
          }
          .color-swatch {
            width: 20px;
            height: 20px;
            border: 1px solid #ccc;
            margin-right: 5px;
            cursor: pointer;
          }
          .line-width-slider {
            position: absolute;
            top: 10px;
            left: 150px;
            z-index: 10;
          }
          `}
      
    </div>
  );

Here’s what’s added:

  • downloadCanvas Function: This function creates a download link for the canvas as a PNG image.
  • Download Button: A button that, when clicked, calls the downloadCanvas function.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Canvas Not Displaying:
    • Problem: The canvas might not be visible.
    • Solution: Double-check the width and height attributes. Ensure the canvas has a background color (e.g., white) or is positioned correctly.
  • Drawing Not Working:
    • Problem: The drawing functionality might not be working.
    • Solution: Ensure that the event listeners (onMouseDown, onMouseUp, onMouseMove) are correctly attached to the canvas element. Verify that the isDrawing state is being updated correctly. Check the console for any JavaScript errors.
  • Incorrect Mouse Coordinates:
    • Problem: The lines are not drawn where you expect them to be.
    • Solution: Make sure you are subtracting the offsetLeft and offsetTop of the canvas element from the mouse coordinates (clientX and clientY) to get the correct position within the canvas.
  • Canvas Resizing Issues:
    • Problem: The canvas doesn’t resize correctly on window resize.
    • Solution: Ensure your resize event listener is correctly implemented and that the canvas dimensions are updated within the useEffect hook.
  • Performance Issues:
    • Problem: The drawing app feels slow or laggy.
    • Solution: Optimize the drawing loop. Avoid unnecessary calculations within the draw function. If implementing complex features, consider using techniques like requestAnimationFrame.

Key Takeaways

  • You’ve learned how to create a basic drawing application using Next.js, React, and the HTML Canvas API.
  • You’ve gained hands-on experience with event handling, DOM manipulation, state management, and basic UI element creation.
  • You’ve learned how to handle user interactions (mouse events) to draw on the canvas.
  • You’ve implemented features like color selection, line width adjustments, clearing the canvas, and optional undo/redo and download functionality.
  • You’ve learned to use React refs to access the DOM element.

FAQ

Here are some frequently asked questions:

  1. Can I add more colors to the color picker? Yes! Simply add more <div> elements with the class name “color-swatch” and set their background colors and onClick event handlers.
  2. How can I implement different brush styles? You can modify the context.lineCap and context.lineJoin properties of the canvas context to change the brush style. You can also explore more advanced techniques like using images for custom brush shapes.
  3. How can I add the ability to save the drawing? You can use the canvas.toDataURL() method to get the data URL of the canvas as an image and then send it to your server to save it. You can also use browser local storage to save drawings.
  4. How do I deploy this app? You can deploy your Next.js app to platforms like Vercel, Netlify, or other hosting providers. You typically just need to run npm run build and then deploy the contents of the .next directory.
  5. Can I add touch support? Yes, you can add touch support by listening for touch events (onTouchStart, onTouchMove, onTouchEnd) and adapting the code to handle touch input. Replace e.clientX and e.clientY with e.touches[0].clientX and e.touches[0].clientY.

This tutorial has shown you the fundamentals of creating a simple drawing app. You can extend this project by adding features such as different brush sizes, shapes, and the ability to save and load drawings. The possibilities are vast, and you can let your creativity guide you. Experiment, explore, and most importantly, have fun building!

Remember that this is just the beginning. The world of front-end development is constantly evolving, and there’s always something new to learn. Keep practicing, keep building, and don’t be afraid to experiment with new technologies and techniques. The more you code, the better you will become.