Canvas – A Simple Drawing Application

on in Canvas
Last modified on

This is a simple drawing application that allows the user to select a colour and thickness for their brush and draw on a canvas element. The user can also save and clear the canvas as needed. The colour options include red, green, and blue, and the thickness options include thin, normal, and thick. The application uses JavaScript to handle the various brush and canvas functions.

Preparing to draw

When the page loads, the code gets the canvas object and hooks up functions to it to handle several JavaScript events for different mouse movements: onMouseDown, onMouseUp, onMouseOut, and onMouseMove. As we’ll see later, these events control the drawing process. At the same time, the page stores the canvas in the canvas global variable and the drawing context in another global context variable. Thus, these objects will be available to the rest of the code:

const canvas = document.getElementById("drawingCanvas");
const context = canvas.getContext("2d");

window.onload = () => {
    canvas.onmousedown = startDrawing;
    canvas.onmouseup = stopDrawing;
    canvas.onmouseout = stopDrawing;
    canvas.onmousemove = draw;
}

To begin drawing, the user selects the colour and line weight by clicking on the required icons in the toolbar at the top of the drawing window. These toolbars are created using simple <div> elements formatted with a light blue background and containing multiple click-activated <img> elements:

<div class="toolbar-wrapper">
    <div class="toolbar">
        Color
        <svg xmlns="http://www.w3.org/2000/svg" id="redPen" viewBox="0 0 400 400" alt="Red Brush" onclick="changeColor('rgb(212,21,29)', this)" style="height:48px">
            <circle cx="200" cy="200" r="100" fill="red" stroke="#000" stroke-width="10" />
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" id="greenPen" viewBox="0 0 400 400" alt="Green Brush" onclick="changeColor('rgb(131,190,61)', this)" style="height:48px">
            <circle cx="200" cy="200" r="100" fill="green" stroke="#000" stroke-width="10" />
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" id="bluePen" viewBox="0 0 400 400" alt="Blue Brush" onclick="changeColor('rgb(0,86,166)', this)" style="height:48px">
            <circle cx="200" cy="200" r="100" fill="blue" stroke="#000" stroke-width="10" />
        </svg>
    </div>

    <div class="toolbar">
        Thickness
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" alt="Thin Brush" onclick="changeThickness(1, this)" style="height:32px">
            <circle cx="200" cy="200" r="100" fill="grey" stroke="#000" stroke-width="10" />
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" alt="Thin Brush" onclick="changeThickness(5, this)" style="height:48px">
            <circle cx="200" cy="200" r="100" fill="grey" stroke="#000" stroke-width="10" />
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" alt="Thin Brush" onclick="changeThickness(10, this)" style="height:64px">
            <circle cx="200" cy="200" r="100" fill="grey" stroke="#000" stroke-width="10" />
        </svg>
    </div>
</div>

<div class="CanvasContainer">
    <canvas id="drawingCanvas" width="600" height="300"></canvas>
</div>
<div class="Toolbar">
    <button onclick="saveCanvas()">Save Canvas Content</button>
    <button onclick="clearCanvas()">Clear Canvas</button>
    <div id="savedCopyContainer">
        <img id="savedImageCopy"><br>
        Right click to save
    </div>
</div>
* {
    box-sizing: border-box;
}

body {
    background: white;
}

.toolbar-wrapper {
    display: flex;
    width: 600px;
}
.toolbar {
    display: flex;
    align-items: center;
    flex-basis: 50%;
    font-size: 14px;
    background: #f2f7fe;
    padding: 16px;
    margin-bottom: 1px;
    margin-right: 1px;
    border: 1px solid #7b899b;
}

canvas {
    border: 1px solid #7b899b;
}

svg {
    padding: 2px;
    border: 2px solid #f2f7fe;
}

svg:hover {
    border: 2px groove #e4f0fe;
    background: white;
}

svg.Selected {
    border: 2px groove #e4f0fe;
}

#savedCopyContainer {
    display: none;
}

#savedCopyContainer img {
    width: 250px;
    height: 150px;
}

An important part of this markup is the onclick attribute of the <img> element. When the user clicks on a pen icon of a certain colour, the <img> element calls the changeColor() function. This function takes two parameters – a new colour that matches the colour of the selected icon, and a reference to the clicked <img> element. The function code looks like this:

const previousColorElement = null;
let isDrawing = false;

const changeColor = (color, imgElement) => {
    // Change the current drawing color to the specified color
    context.strokeStyle = color;

    // Change the class of the clicked img element to "Selected"
    imgElement.className = "Selected";

    // Reset the class of the previously selected img element to its default state
    // Set the previousColorElement variable to the current img element
    if (previousColorElement !== null) {
        previousColorElement.className = "";
    }

    previousColorElement = imgElement;
};

The changeColor() function code does two main things. First, it sets the strokeStyle context property to the appropriate new colour. It only takes one line of code to do this. Second, the code changes the appearance of the clicked <img> element by enclosing it in a border so that you can see which colour is the current one. This operation requires more work because you need to keep track of the previously clicked colour <img> element to remove its border.

The changeThickness() function does almost identical work, only it changes the value of the lineWidth context property according to the selected line thickness:

// Track the <img> element for the line thickness that was previously clicked on
var previousThicknessElement;

function changeThickness (thickness, imgElement) {
    // Change the current line thickness
    context.lineWidth = thickness;

    // Change the style of the <img> element that was clicked on
    imgElement.className = "Selected";

    // Return the previously selected <img> element to its normal state
    if (previousThicknessElement != null)
        previousThicknessElement.className = "";

    previousThicknessElement = imgElement;
}

The above code prepares the canvas for drawing, but it’s too early to draw. To do this, you need to add the code that actually does the drawing, which is done in the next section.

Painting on Canvas

The drawing process begins when the user clicks on the canvas. To keep track of when drawing is in progress, the application uses the isDrawing global variable to inform the rest of the code whether the drawing context can be used.

As we saw earlier, the onMouseDown event is associated with the startDrawing() function. This function sets the isDrawing variable, creates a new path, and then sets the drawing start position, thus preparing the canvas for drawing:

function startDrawing(e) {
    // Start drawing
    isDrawing = true;

    // Create a new path (with the current color and line thickness)
    context.beginPath();

    // By clicking the left mouse button, we place the "brush" on the canvas
    context.moveTo(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop);
}

For our application to work correctly, drawing must start at the current position, i.e. where the mouse pointer is when the user presses the left button. But the task of obtaining the correct coordinates of this point is somewhat complicated.

The onMouseDown event provides the coordinates of the mouse cursor (via the pageX and pageY properties shown in this example), but these are coordinates relative to the entire page. To calculate the corresponding canvas coordinates, you need to subtract the distance to the upper left corner of the canvas from the coordinates of the upper left corner of the browser window.

The actual drawing happens when the user moves the mouse while holding down the left button. Each time the mouse moves, even one pixel, the onMouseMove event is fired and the draw() function is executed. If the isDrawing variable is set, the code calculates the current coordinates of the canvas (i.e. the coordinates of the point where the mouse is currently located), and then calls the lineTo() method, which adds the path of the corresponding line segment, after which the stroke() method is called, which draws this line:

function draw(event) {
    // Determine current mouse pointer coordinates
    if (isDrawing == true) {
        var x = event.pageX - canvas.offsetLeft;
        var y = event.pageY - canvas.offsetTop;


	// Draw line to new coordinates
	context.lineTo(x, y);
	context.stroke();
    }
}

If the user continues to move the mouse, the draw() function is called again, again adding a segment to the already drawn line. This line is so short—only a pixel or two long—that it doesn’t even look like a straight line when you start drawing.

Finally, when the user releases the mouse button or moves the cursor off the canvas, the onMouseUp or onMouseOut event fires, respectively. Both of these events trigger the stopDrawing() function, which tells the application to stop drawing:

function stopDrawing() {
    isDrawing = false;	
}

At this stage, we have considered all aspects of drawing:

Now let’s move on to discuss the functions of clearing the canvas or saving the created drawing. There are two buttons on the toolbar at the bottom of the canvas for this purpose. Clicking the “Clear Canvas” button calls the clearCanvas() function, which completely clears the surface of the canvas using the context’s clearRect() method:

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

The operation of saving the contents of the canvas is more complex, and we will devote a separate section to it.

Saving Canvas Content

The task of saving canvas content requires many options to be considered. First, you need to decide how to get the drawing data. Canvas provides three possible approaches to solve this problem:

Use Data URL

With this approach, the content of the canvas is converted into an image file, which is then translated into a sequence of characters formatted as a URL. This allows for a neat and compact way to move image data (for example, it can be passed to an <img> element and sent to a web server). In our drawing application, we will use this approach.

Use the getImageData() method

This approach allows you to get “raw” pixel data, which can then be manipulated as you like.

Keep a list of “steps”

For example, you can organize an array containing a list of all the lines drawn on the canvas. This data can then be saved and used to reproduce the image. This approach requires less space to store the image, and also provides more flexibility for later work with it.

But that is not all. Once you’ve decided on the type of content to save, you need to decide where to save the content. The following options are available, among others:

  • In an image file. For example, the content of a canvas is saved as a PNG or JPEG file on the artist’s local hard drive. This is the approach that our paint application takes, and we’ll look at it next.
  • In the local storage system.
  • On the web server. After the data is transmitted to the web server, the latter can store it in a file or database and provide it the next time the user visits the page.

Our paint application uses a feature called data URL to store the contents of the canvas. To get the URL for the current data, we simply use the canvas’s toDataURL() method. If we call the toDataURL() method without passing any parameters to it, we will get an image in PNG format. Alternatively, you can specify the required image format or mime type to the method:

var url = canvas.toDataURL("image/jpeg");

But if the browser cannot provide the required format, it will return a PNG image converted to a long string again.

What is a data URL? Technically, it’s just a long string of Base64 encoded characters that starts with the text: data:image/png;base64. It looks like nonsense, at least to people, because. This data is intended to be understood by computer programs such as browsers. The data URL for the canvas content looks something like this:

data:image/png;base64, iVBORwOKGgoAAAANSUhEUgAAAfQAAAEsCAYAAAA2uOHXAAAAA ...

Thus, the task of converting image data into URL data is not difficult. But what can you do next with this data URL? You can send it to a web server for long-term storage.

But the possibilities for saving data on the client side are somewhat limited. Some browsers allow you to go directly to the data URL. This means that you can use the following code to navigate to an image:

window.location = canvas.toDataURL();

A more reliable method would be to pass the URL of the data to the <img> element, which is what our paint application does:

function saveCanvas() {
    // Find the <img> element
    var imageCopy = document.getElementById("savedImageCopy");


    // Display canvas data in the <img> element
    imageCopy.src = canvas.toDataURL();

    // Show the <div> element, making the image visible
    var imageContainer = document.getElementById("savedCopyContainer");
    imageContainer.style.display = "block";
}

This code doesn’t exactly store the image data, because the image is not yet saved in the file. But this requires only one step – just right-click on the thumbnail image in the panel below the canvas and select “Save Image As” from the context menu that opens. It’s not as convenient as a file upload or the Save dialog, but it’s the only way that works reliably in all browsers.

Related posts