Create a drawing app with HTML and JavaScript

No Comments
Published: 08.05.2022

Do you want to learn how to create a drawing app with HTML and JavaScript? In this post, we will do exactly that, and we will build upon the basics that we discussed in my last post (here).

The drawing app consists of a color selection and multiple interesting tools, like the polygon or rectangle tool!

To create the app, we will follow the following steps:

So let’s get started!

Setup the project

For this project, we will use vite and tailwind. You do not need them, but they ease the process of developing the application. Vite is used as a development environment and a bundler for the final application. Tailwind CSS is a CSS framework that allows you to create User Interfaces faster than basic CSS, as I showed in this post here.

I also created a script that I use to set up my projects. You can download that for subscribing to my newsletter πŸ™‚

Need help or want to share feedback? Join my discord community!

If you decide to do that, you can skip to this section.

Setup without Script

module.exports = {
  content: [
    "./index.html"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
@tailwind base;
@tailwind components;
@tailwind utilities;

With that, we set up Tailwind CSS. Now we will make some more changes that are project-specific. First, we create multiple folders. The folder assets for assets, css for CSS files and lastly js for JavaScript files. According to that, we move the style.css file into the folder css and the file main.js into the folder js. Additionally, we will move the favicon.svg file into the assets folder. Lastly, we will update the contents of index.html to:

KOFI Logo

If this guide is helpful to you and you like what I do, please support me with a coffee!

<!DOCTYPE html>
<html lang="en" class="h-full">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Drawing App</title>
  </head>
  <body>

    <script type="module" src="./js/main.js"></script>
  </body>
</html>

and the contents of css/style.css to:

import '../css/style.css'

With that, we have the following final structure:

drawing-app
β”œβ”€β”€ node_modules
β”œβ”€β”€ assets
β”‚   └── favicon.svg
β”œβ”€β”€ css
β”‚   └── style.css
β”œβ”€β”€ js
β”‚   └── main.js
β”œβ”€β”€ .gitignore
β”œβ”€β”€ index.html
β”œβ”€β”€ package.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ postcss.config.js
└── tailwind.config.js

To finish the setup, continue reading here.

Setup with Script

To set up the project with the script, do the following steps:

<!DOCTYPE html>
<html lang="en" class="h-full">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Drawing App</title>
  </head>
  <body>

    <script type="module" src="./js/main.js"></script>
  </body>
</html>

For both

For the project, we need multiple different graphics. Therefore download the following zip and unzip it inside the folder assets.

The resulting structure should look like this:

assets
β”œβ”€β”€ big.svg
β”œβ”€β”€ clear.svg
β”œβ”€β”€ favicon.svg
β”œβ”€β”€ medium.svg
β”œβ”€β”€ path.svg
β”œβ”€β”€ pen.svg
β”œβ”€β”€ polygon.svg
β”œβ”€β”€ small.svg
└── square.svg

Create the interface for the drawing app

Now that we finished setting up our project, we can start creating the drawing app! For that, we first have to create the basic HTML structure:

<body>
    <canvas id="canvas"></canvas>
    <div>
        <div id="colors">
            <button id="black"></button>
            <button></button>
            <button></button>
            <button></button>
            <button></button>
            <button></button>
            <button></button>
            <button></button>
        </div>
        <div id="settings">
            <button id="pen">
                <img src="./assets/pen.svg" alt="pen">
            </button>
            <button id="small">
                <img src="./assets/small.svg" alt="small">
            </button>
            <button id="medium">
                <img src="./assets/medium.svg" alt="medium">
            </button>
            <button id="big">
                <img src="./assets/big.svg" alt="big">
            </button>
            <button id="path">
                <img src="./assets/path.svg" alt="path">
            </button>
            <button id="polygon">
                <img src="./assets/polygon.svg" alt="polygon">
            </button>
            <button id="rect">
                <img src="./assets/square.svg" alt="square">
            </button>
            <button id="clear">
                <img src="./assets/clear.svg" alt="clear">
            </button>
        </div>
    </div>
    <script type="module" src="./js/main.js"></script>
</body>

and then add Tailwind CSS classes to the markup to achieve the wanted result:

<body class="flex flex-col items-center justify-center h-full py-8 px-8 w-full lg:w-4/5 xl:w-3/4 2xl:w-2/3 mx-auto">
    <canvas id="canvas" class="w-full max-h-full aspect-video border-2 border-black border-solid mb-8"></canvas>
    <div class="w-full flex flex-row justify-between">
        <div id="colors" class="grid grid-rows-2 grid-cols-4 gap-1 justify-self-start">
            <button id="black" class="bg-black setting"></button>
            <button class="bg-red-500 setting"></button>
            <button class="bg-green-500 setting"></button>
            <button class="bg-blue-500 setting"></button>
            <button class="bg-white setting"></button>
            <button class="bg-yellow-500 setting"></button>
            <button class="bg-orange-500 setting"></button>
            <button class="bg-violet-500 setting"></button>
        </div>
        <div id="settings" class="grid grid-rows-2 grid-cols-4 gap-1 justify-self-end">
            <button id="pen" class="setting tool">
                <img src="./assets/pen.svg" alt="pen">
            </button>
            <button id="small" class="setting size">
                <img src="./assets/small.svg" alt="small">
            </button>
            <button id="medium" class="setting size">
                <img src="./assets/medium.svg" alt="medium">
            </button>
            <button id="big" class="setting size">
                <img src="./assets/big.svg" alt="big">
            </button>
            <button id="path" class="setting tool">
                <img src="./assets/path.svg" alt="path">
            </button>
            <button id="polygon" class="setting tool">
                <img src="./assets/polygon.svg" alt="polygon">
            </button>
            <button id="rect" class="setting tool">
                <img src="./assets/square.svg" alt="square">
            </button>
            <button id="clear" class="setting">
                <img src="./assets/clear.svg" alt="clear">
            </button>
        </div>
    </div>
    <script type="module" src="./js/main.js"></script>
</body>

As you may see, we added the class setting to all the different colors and tools to make them look the same. But to make the class do something, we need to add custom classes to css/style.css like this:

@layer components {
  .setting {
    @apply h-8 lg:h-12 aspect-square border-2 border-solid border-black;
  }

  .setting > img {
      @apply h-full w-full;
  }

  .setting.selected {
      @apply border-4;
  }

  #black.setting.selected {
    @apply border-4 border-white outline outline-1 outline-black;
  }

  .setting.selected.hide-select {
    @apply border-2;
  }
}

With that, our drawing app already looks like we want it to look, but there is no functionality yet. That is what we will cover in the next section!

Create the tools for the drawing app

For the drawing app, we will create multiple different tools. A pen, a path, a polygon, and a rectangle tool! I orient myself on the HTML Canvas basics we discussed in this post here to create them.

Each tool is based on different mouse events. To change between the different settings, we will create a mode switcher in the last part of this section.

But before we can create the tools, we need to initialize the canvas with the following lines (add them to main.js):

import '../css/style.css'

const container = document.getElementById("canvas-container");
const canvas = document.getElementById("canvas");
const width = 1920;
const height = 1080;

// context of the canvas
const context = canvas.getContext("2d");
context.imageSmoothingEnabled = true;

// resize canvas (CSS does scale it up or down)
canvas.height = height;
canvas.width = width;

function getMousePos(canvas, evt) {
  var rect = canvas.getBoundingClientRect(), 
    scaleX = canvas.width / rect.width, 
    scaleY = canvas.height / rect.height;

  return {
    x: (evt.clientX - rect.left) * scaleX,
    y: (evt.clientY - rect.top) * scaleY
  }
}

Pen Tool

As we already saw in the basics post, we need three different events for the pen tool to work. These are mousedown, mouseup, and mousemove. For each of the different events, we need a function that can be executed.

// --- Pen ---
let drawing = false;

function startDraw(e) {
  drawing = true;
  context.beginPath();
  draw(e)
}

function endDraw(e) {
  drawing = false;
}

function draw(e) {
  if (!drawing) return;

  let { x, y } = getMousePos(canvas, e);

  context.lineTo(x, y);
  context.stroke();

  // for smoother drawing
  context.beginPath();
  context.moveTo(x, y);
}

To test if the pen tool works, you can add the following lines:

window.addEventListener("mousedown", startDraw);
window.addEventListener("mouseup", endDraw);
window.addEventListener("mousemove", draw);

After you see that it worked, I suggest you have to remove it.

Sizes

With this functionality, you can switch between the three different sizes (small, medium, and big). We will first set the new size and then handle the class of the selected size to show that it is selected!

const sizes = {
  'small': 5,
  'medium': 10,
  'big': 15
}

function setSize(e, size) {
  context.lineWidth = size;
  selectSize(e);
}

function selectSize(e) {
  if (mode === 'rect')
    return;

  const sizes = document.getElementsByClassName("size");
  for (const size of sizes) {
    size.classList.remove('selected');
  }
  
  if (e === undefined)
  return;
  
  e.target.parentElement.classList.add('selected');
}

The changed size affects the pen, path, and polygon tool!

Path Tool

The path tool is used to draw a straight line from point a to point b. Thus we only need two functions for the mousedown and mouseup event. When you press your mouse down, you set the start position, and when you lift your finger, you set the end position and draw a line between the two!

// --- Path ---

function startPath(e) {
  drawing = true;
  context.beginPath();
  draw(e)
}

function endPath(e) {
  drawing = false;
  let { x, y } = getMousePos(canvas, e);
  
  context.lineTo(x, y);
  context.stroke();
}

To test if the path tool works, you can add the following lines (remove it afterward):

window.addEventListener("mousedown", startPath);
window.addEventListener("mouseup", endPath);

Polygon Tool

With the polygon tool, you create a closed polygon with multiple edges. It works as follows:

Therefore we again need only two functions for mousedown and mouseup.

// --- Polygon ---

let poly = false;
let polyTimeout = undefined;

function startPolygon(e) {
  if (e.target.id !== 'canvas')
    return;

  drawing = true;

  if (poly) {
    polygon(e);
  }
  else {
    context.beginPath();
    draw(e);
  }
  poly = true;
}

function endPolygon(e) {
  if (!poly)
    return;

  polyTimeout = setTimeout(() => {
    drawing = false;
    context.closePath();
    context.stroke();

    poly = false;
  }, 1000);
}

function polygon(e) {
  if (!drawing) return;
  clearTimeout(polyTimeout);

  let { x, y } = getMousePos(canvas, e);
  
  context.lineTo(x, y);
  context.stroke();
}

To test if the polygon tool works, you can add the following lines (remove them afterward):

window.addEventListener("mousedown", startPolygon);
window.addEventListener("mouseup", endPolygon);

Rectangle Tool

Lastly, we have the rectangle tool. It is used to create rectangles on the canvas, and it works similarly to the path tool. But instead of a straight line between the two points, it creates a rectangle between them.

// --- Rect ---
let start = {}

function startRect(e) {
    start = getMousePos(canvas, e);
}

function endRect(e) {
    let { x, y } = getMousePos(canvas, e);
    context.fillRect(start.x, start.y, x - start.x, y - start.y);
}

To test if the polygon tool works, you can add the following lines (remove them afterward):

window.addEventListener("mousedown", startRect);
window.addEventListener("mouseup", endRect);

Clear the canvas

The last tool in our toolbar is clear. It is used to reset the canvas.

// --- Clear ---

function clearCanvas() {
  context.clearRect(0, 0, canvas.width, canvas.height);
}

Select Tool/Mode

Lastly, let’s add functionality to swap between the different tools. Therefore we need to remove the events of the current tool and assign the ones of the new tool. In addition to that, we also need to change some of the classes to visualize the selected tools!

let mode = 'draw';

function selectMode(e, newMode) {
  const tools = document.getElementsByClassName("tool");
  for (const tool of tools) {
    tool.classList.remove('selected');
  }
  
  const size = document.querySelector(".size.selected");
  if (size !== null)
  {
    size.classList.remove('hide-select');
    if (newMode === 'rect')
      size.classList.add('hide-select');
  }
    
  
  e.target.parentElement.classList.add('selected');

  mode = newMode;
}

const activeEvents = {
  "mousedown": undefined,
  "mouseup": undefined,
  "mousemove": undefined
};

function setMode(e, mode) {
  for (const event in activeEvents) {
    window.removeEventListener(event, activeEvents[event]);
    activeEvents[event] = undefined;
  }

  switch (mode) {
    case 'pen':
      window.addEventListener("mousedown", startDraw);
      window.addEventListener("mouseup", endDraw);
      window.addEventListener("mousemove", draw);

      activeEvents['mousedown'] = startDraw;
      activeEvents['mouseup'] = endDraw;
      activeEvents['mousemove'] = draw;
      break;
    case 'path':
      window.addEventListener("mousedown", startPath);
      window.addEventListener("mouseup", endPath);

      activeEvents['mousedown'] = startPath;
      activeEvents['mouseup'] = endPath;
      break;
    case 'polygon':
      window.addEventListener("mousedown", startPolygon);
      window.addEventListener("mouseup", endPolygon);

      activeEvents['mousedown'] = startPolygon;
      activeEvents['mouseup'] = endPolygon;
      break;
    case 'rect':
      window.addEventListener("mousedown", startRect);
      window.addEventListener("mouseup", endRect);

      activeEvents['mousedown'] = startRect;
      activeEvents['mouseup'] = endRect;
      break;

    default:
      break;
  }

  selectMode(e, mode);
}

The created function does not yet work with the buttons. We will take care of that after creating the color selection!

Create the colors for the drawing app

The next thing we need is to create the color selection of the app. We create a colors.js file containing a JSON with the different color codes needed (they represent the tailwind -500 colors that we also assigned the buttons).

export const colors = {
    "black": "#000000",
    "white": "#ffffff",
    "red": "#ef4444",
    "green": "#22c55e",
    "blue": "#3b82f6",
    "yellow": "#eab308",
    "orange": "#f97316",
    "violet": "#8b5cf6"
}

Next, we need to import this into our main.js file like this:

//...
import { colors } from "./colors";
//...

And with that, we can create the color selection. For that, we first change the drawing color and then apply the selected style to the selected color:

function setColor(e, color) {
  context.strokeStyle = colors[color];
  context.fillStyle = colors[color];
  selectColor(e);
}

function selectColor(e) {
  const colors = document.getElementById("colors").children;
  for (const color of colors) {
    color.classList.remove('selected');
  }

  e.target.classList.add('selected');
}

We created all the functions with that, and there is only one step left. To initialize the whole thing.

Initialize the drawing app

Inside this step, we will assign the different functions to the correct buttons and set the default settings for the drawing app.

function initialize() {
  const colorButtons = document.getElementById('colors').children;
  for (const colorButton of colorButtons) {
    colorButton.addEventListener('click', (e) => { setColor(e, colorButton.classList.value.replace(/bg-(\w*).*/, '$1'))} );
  }

  const tools = document.getElementsByClassName('tool');
  for (const tool of tools) {
    tool.addEventListener('click', (e) => { setMode(e, tool.id)} );
  }

  const sizeButtons = document.getElementsByClassName('size');
  for (const sizeButton of sizeButtons) {
    sizeButton.addEventListener('click', (e) => { setSize(e, sizes[sizeButton.id])} );
  }

  document.getElementById('clear').addEventListener('click', clearCanvas);

  // set default settings
  context.lineCap = 'round';
  document.getElementById('small').firstElementChild.click();
  document.getElementById('pen').firstElementChild.click();
  document.getElementById('black').click();
}

initialize();

With that, we completed the drawing app, and you can create a final version of it by running:

npm run build

Conclusion

We created a drawing app with basic HTML and JavaScript from scratch in this post! For that, we learned how to change settings like the size or color, and we learned how to create different tools based on the functionality the HTML Canvas provides!

You can find the final app in this repository here.

I hope you enjoyed this post and it was helpful for you! If you have any questions feel free to ask through the chat window or send me a message on Twitter @programonaut (or email etc.).

In case you are interested in more posts like this, consider subscribing to my newsletter, where I publish a monthly update on all the new posts!

Discussion (0)

Add Comment

Your email address will not be published. Required fields are marked *