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

Written by

in

Ever wanted to create your own digital canvas? Perhaps you’ve envisioned a place where you can sketch ideas, brainstorm visually, or simply unwind with some digital doodling. In this tutorial, we’ll dive into building a simple, yet functional, drawing application using Next.js. This project is perfect for beginners and intermediate developers looking to expand their skills with modern web technologies. We’ll explore the core concepts of Next.js, learn how to handle user input, and master the art of manipulating the HTML canvas element. Let’s get started!

Why Build a Drawing App?

Building a drawing app might seem like a niche project, but it’s an excellent way to learn fundamental web development concepts. It allows you to:

  • Master Canvas Manipulation: You’ll gain hands-on experience working with the HTML canvas, a powerful tool for creating graphics and animations.
  • Understand Event Handling: You’ll learn how to capture and respond to user interactions like mouse clicks, mouse movements, and touch events.
  • Explore State Management: You’ll practice managing the application’s state to track drawing data, colors, and brush sizes.
  • Apply Next.js Fundamentals: You’ll solidify your understanding of Next.js components, routing, and styling.

Plus, it’s a fun and engaging project that lets you express your creativity while learning.

Prerequisites

Before we begin, make sure you have the following:

  • Node.js and npm (or yarn): Installed on your system. These are essential for running Next.js projects.
  • A Code Editor: Such as VS Code, Sublime Text, or Atom.
  • Basic HTML, CSS, and JavaScript knowledge: Familiarity with these languages will be helpful.
  • Next.js knowledge: Familiarity with Next.js fundamentals is an advantage.

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

This command will set up a new Next.js project named “drawing-app”. Navigate into your project directory:

cd drawing-app

Now, start the development server:

npm run dev

Open your web browser and go to http://localhost:3000. You should see the default Next.js welcome page.

Creating the Drawing Component

The heart of our application will be a drawing component. This component will handle user input, draw lines on the canvas, and manage the application’s state. Create a new file called `DrawingCanvas.js` inside the `components` directory (create it if it doesn’t exist).

Here’s the basic structure of the `DrawingCanvas.js` component:

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

function DrawingCanvas() {
  const canvasRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [color, setColor] = useState('black');
  const [size, setSize] = useState(5);
  const [paths, setPaths] = useState([]);

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

    // Set initial canvas size
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // Function to draw a line
    const drawLine = (path) => {
      context.strokeStyle = path.color;
      context.lineWidth = path.size;
      context.lineCap = 'round';

      context.beginPath();
      context.moveTo(path.points[0].x, path.points[0].y);
      for (let i = 1; i  {
    setIsDrawing(true);
    const rect = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setPaths(prevPaths => [
      ...prevPaths,
      { color, size, points: [{ x, y }] }
    ]);
  };

  const draw = (e) => {
    if (!isDrawing) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setPaths(prevPaths => {
      const lastPath = prevPaths[prevPaths.length - 1];
      const newPaths = [...prevPaths];
      if (lastPath) {
        newPaths[newPaths.length - 1] = {
          ...lastPath,
          points: [...lastPath.points, { x, y }]
        }
      }
      return newPaths;
    });
  };

  const stopDrawing = () => {
    setIsDrawing(false);
  };

  return (
    <div>
      
    </div>
  );
}

export default DrawingCanvas;

Let’s break down this code:

  • Import Statements: We import `useRef`, `useEffect`, and `useState` from React.
  • `canvasRef` : A reference to the HTML canvas element.
  • `isDrawing` : A boolean state variable indicating whether the user is currently drawing.
  • `color` : A string state variable representing the current drawing color.
  • `size` : A number state variable representing the current brush size.
  • `paths` : An array of objects, where each object represents a path drawn by the user. Each path consists of points, color, and size.
  • `useEffect` : This hook runs after the component renders. It gets the 2D rendering context of the canvas and sets the canvas dimensions to match the window size. It also handles redrawing existing paths.
  • `drawLine` : This function takes a path object as input and draws the line on the canvas.
  • `startDrawing` : This function is called when the user clicks the mouse on the canvas. It sets `isDrawing` to `true` and records the starting point of the drawing.
  • `draw` : This function is called when the mouse moves while the mouse button is pressed. It adds a new point to the current path and redraws the line.
  • `stopDrawing` : This function is called when the user releases the mouse button or moves the mouse outside of the canvas. It sets `isDrawing` to `false`.
  • Canvas Element: The `<canvas>` element is where the drawing will take place. We attach event listeners for `onMouseDown`, `onMouseUp`, `onMouseOut`, and `onMouseMove` to handle user interactions.

Integrating the Drawing Component into Your App

Now, let’s integrate this component into our main page. Open `pages/index.js` and replace the existing code with the following:

import DrawingCanvas from '../components/DrawingCanvas';

function HomePage() {
  return (
    <div>
      
    </div>
  );
}

export default HomePage;

This code imports the `DrawingCanvas` component and renders it on the page. Now, when you visit http://localhost:3000, you should see a blank canvas. Try clicking and dragging your mouse on the canvas; nothing will happen yet, since we haven’t implemented the drawing logic.

Implementing Drawing Logic

Let’s add the drawing functionality to the `DrawingCanvas.js` component. We’ll start by modifying the `startDrawing`, `draw`, and `stopDrawing` functions.

Inside the `DrawingCanvas.js` component, update the `startDrawing` function to capture the starting point of the line:

const startDrawing = (e) => {
  setIsDrawing(true);
  const rect = canvasRef.current.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  setPaths(prevPaths => [
    ...prevPaths,
    { color, size, points: [{ x, y }] }
  ]);
};

Next, update the `draw` function to add new points to the current path:

const draw = (e) => {
  if (!isDrawing) return;
  const rect = canvasRef.current.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  setPaths(prevPaths => {
    const lastPath = prevPaths[prevPaths.length - 1];
    const newPaths = [...prevPaths];
    if (lastPath) {
      newPaths[newPaths.length - 1] = {
        ...lastPath,
        points: [...lastPath.points, { x, y }]
      }
    }
    return newPaths;
  });
};

Finally, the `useEffect` hook will handle drawing the lines based on the `paths` state. Inside the `useEffect`, add the `drawLine` function to draw on the canvas. The `drawLine` function will iterate over the `paths` array and draw each path.


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

    // Set initial canvas size
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // Function to draw a line
    const drawLine = (path) => {
      context.strokeStyle = path.color;
      context.lineWidth = path.size;
      context.lineCap = 'round';

      context.beginPath();
      context.moveTo(path.points[0].x, path.points[0].y);
      for (let i = 1; i < path.points.length; i++) {
        context.lineTo(path.points[i].x, path.points[i].y);
      }
      context.stroke();
    };

    // Redraw existing paths on component mount
    paths.forEach(drawLine);
  }, [paths]);

Now, save the file and refresh your browser. You should be able to draw on the canvas! Try clicking, dragging, and releasing the mouse to create lines.

Adding Color and Brush Size Controls

Let’s add some UI elements to control the drawing color and brush size. We’ll add two select elements for color and size. Modify the `DrawingCanvas.js` component:

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

function DrawingCanvas() {
  const canvasRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [color, setColor] = useState('black');
  const [size, setSize] = useState(5);
  const [paths, setPaths] = useState([]);

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

    // Set initial canvas size
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // Function to draw a line
    const drawLine = (path) => {
      context.strokeStyle = path.color;
      context.lineWidth = path.size;
      context.lineCap = 'round';

      context.beginPath();
      context.moveTo(path.points[0].x, path.points[0].y);
      for (let i = 1; i  {
    setIsDrawing(true);
    const rect = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setPaths(prevPaths => [
      ...prevPaths,
      { color, size, points: [{ x, y }] }
    ]);
  };

  const draw = (e) => {
    if (!isDrawing) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setPaths(prevPaths => {
      const lastPath = prevPaths[prevPaths.length - 1];
      const newPaths = [...prevPaths];
      if (lastPath) {
        newPaths[newPaths.length - 1] = {
          ...lastPath,
          points: [...lastPath.points, { x, y }]
        }
      }
      return newPaths;
    });
  };

  const stopDrawing = () => {
    setIsDrawing(false);
  };

  return (
    <div>
      <div>
        <label>Color:</label>
         setColor(e.target.value)} value={color}>
          Black
          Red
          Blue
          Green
        
      </div>
      <div>
        <label>Size:</label>
         setSize(Number(e.target.value))} value={size}>
          1
          3
          5
          7
        
      </div>
      
    </div>
  );
}

export default DrawingCanvas;

In this code, we added two `select` elements: one for color and another for size. We use the `onChange` event to update the `color` and `size` state variables when the user selects a different option. The `value` prop is used to set the selected option for each select element.

Now, when the user changes the color or size, the corresponding state variables are updated, and the drawing will reflect these changes.

Adding a Clear Button

Let’s add a button to clear the canvas. Add the following code inside the `DrawingCanvas.js` component, just before the `<canvas>` element:


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

Then, add the button to the return statement:


  <button onClick={clearCanvas}>Clear</button>

The `clearCanvas` function resets the `paths` state to an empty array and uses `clearRect` to clear the entire canvas.

Styling the Application

Let’s add some basic styling to make the app look better. You can use CSS modules, styled-components, or any other styling method you prefer. For simplicity, we’ll use inline styles in this example.

Here’s an example of how you can add some basic styles to the `DrawingCanvas.js` component:

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

function DrawingCanvas() {
  const canvasRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [color, setColor] = useState('black');
  const [size, setSize] = useState(5);
  const [paths, setPaths] = useState([]);

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

    // Set initial canvas size
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    // Function to draw a line
    const drawLine = (path) => {
      context.strokeStyle = path.color;
      context.lineWidth = path.size;
      context.lineCap = 'round';

      context.beginPath();
      context.moveTo(path.points[0].x, path.points[0].y);
      for (let i = 1; i  {
    setIsDrawing(true);
    const rect = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setPaths(prevPaths => [
      ...prevPaths,
      { color, size, points: [{ x, y }] }
    ]);
  };

  const draw = (e) => {
    if (!isDrawing) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setPaths(prevPaths => {
      const lastPath = prevPaths[prevPaths.length - 1];
      const newPaths = [...prevPaths];
      if (lastPath) {
        newPaths[newPaths.length - 1] = {
          ...lastPath,
          points: [...lastPath.points, { x, y }]
        }
      }
      return newPaths;
    });
  };

  const stopDrawing = () => {
    setIsDrawing(false);
  };

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

  return (
    <div style={{ padding: '20px' }}>
      <div style={{ marginBottom: '10px' }}>
        <label htmlFor="color" style={{ marginRight: '5px' }}>Color:</label>
        <select id="color" onChange={(e) => setColor(e.target.value)} value={color} style={{ marginRight: '10px' }}>
          <option value="black">Black</option>
          <option value="red">Red</option>
          <option value="blue">Blue</option>
          <option value="green">Green</option>
        </select>
        <label htmlFor="size" style={{ marginRight: '5px' }}>Size:</label>
        <select id="size" onChange={(e) => setSize(Number(e.target.value))} value={size} style={{ marginRight: '10px' }}>
          <option value="1">1</option>
          <option value="3">3</option>
          <option value="5">5</option>
          <option value="7">7</option>
        </select>
        <button onClick={clearCanvas} style={{ padding: '5px 10px', backgroundColor: '#eee', border: '1px solid #ccc', cursor: 'pointer' }}>Clear</button>
      </div>
      <canvas
        ref={canvasRef}
        onMouseDown={startDrawing}
        onMouseUp={stopDrawing}
        onMouseOut={stopDrawing}
        onMouseMove={draw}
        style={{ border: '1px solid black', cursor: 'crosshair' }}
      />
    </div>
  );
}

export default DrawingCanvas;

This code adds some basic styles for padding, margins, and borders to improve the look and feel of the app. Feel free to customize the styles to match your preferences.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Canvas Not Rendering: Make sure you’ve imported and rendered the `DrawingCanvas` component correctly in your `pages/index.js` file.
  • Drawing Not Working: Double-check your event listeners (`onMouseDown`, `onMouseUp`, `onMouseMove`) and the logic within the `startDrawing`, `draw`, and `stopDrawing` functions. Ensure that `isDrawing` is toggling correctly.
  • Incorrect Coordinates: Verify that you’re correctly calculating the mouse coordinates relative to the canvas using `getBoundingClientRect()`.
  • Lines Not Appearing: Ensure that the `drawLine` function is correctly drawing lines on the canvas. Check the `strokeStyle`, `lineWidth`, and `lineCap` properties.
  • Canvas Size Issues: The canvas size should be dynamically adjusted to window size.
  • Performance Issues: For more complex drawing applications, consider optimizing the drawing process to improve performance. For example, you can use techniques like double buffering or requestAnimationFrame.

Key Takeaways

In this tutorial, you’ve learned how to build a basic drawing app using Next.js. You’ve gained experience with:

  • Setting up a Next.js project.
  • Creating and using a Canvas element.
  • Handling mouse events.
  • Managing state with `useState`.
  • Drawing lines on the canvas.
  • Adding color and size controls.
  • Clearing the canvas.

This project provides a solid foundation for understanding how to work with the HTML canvas and build interactive web applications. You can extend this app further by adding features such as:

  • Different brush types (e.g., pen, eraser, spray).
  • Color pickers.
  • Saving and loading drawings.
  • Zoom and pan functionality.
  • Touch support for mobile devices.

FAQ

Q: How can I add touch support for mobile devices?

A: You can add touch support by adding event listeners for `onTouchStart`, `onTouchMove`, and `onTouchEnd` to the canvas element. In these event handlers, you’ll need to use `e.touches[0]` to get the touch coordinates.

Q: How can I save the user’s drawings?

A: You can save the user’s drawings by converting the canvas content to a data URL using `canvas.toDataURL()`. You can then store this data URL in local storage or send it to a server to save it to a database.

Q: How can I add different brush types?

A: You can add different brush types by creating different drawing functions for each brush type. For example, you could create a pen brush that draws a solid line, an eraser brush that clears the canvas, and a spray brush that creates a dotted effect.

Q: How do I deploy this application?

A: You can deploy your Next.js application to various platforms like Vercel, Netlify, or AWS. You can follow the deployment guides provided by these platforms.

Q: How can I improve the performance of the drawing application?

A: You can improve the performance by optimizing the drawing process. For instance, you can use techniques like double buffering, requestAnimationFrame, and limiting the frequency of drawing updates.

Building this drawing application is just the beginning. The skills you’ve acquired can be applied to a wide range of projects, from simple games to complex data visualization tools. Experiment with the code, try adding new features, and most importantly, have fun exploring the possibilities of web development.