This series of posts will explore how we can use the HTML canvas element to build a simple browser-based drawing tool. In this post we look at adding images to, and exporting images from our canvas.

Introduction

Welcome to the third post in this series, the goal of which is to build a canvas-based drawing tool from scratch with zero dependencies. The tool should allow the user to upload an existing image and to embellish it with free-hand drawing and fixed shapes, before exporting the image again. This tool is being built for instructional purposes, but for convenience I have made it available here, if you would like to have a play around.

If you intend to follow the tutorial for yourself I would encourage you to grab the source code from the GitHub repo. The version of the code we build today will extend upon what we covered in Part I and Part 2 of this tutorial series, so I would also encourage you to have a look back over these earlier tutorials if you come across any concepts or constructs that look unfamiliar.

In this post we will look at setting a background image on our drawing canvas, and we investigate how we can export our canvas creation to an image format (PNG). The new functionality we are aiming for is briefly demonstrated in this video:

Let's recap what we are hoping to implement, and where we are:

  1. Draw free hand lines on the canvas (Part I)
  2. Draw resizable rectangles to highlight a section of the image (Part II)
  3. Set a background image on the canvas that we can annotate
  4. Add text captions

The source code for the version of the tool that we build in this tutorial can be found on this branch of the GitHub repo.

Page markup and initial setup

The markup introduced in the previous posts will require a few more elements for us to hang this new functionality. A stripped back version of the HTML relevant for this post is shown here:

    
    
    <div id="tool_wrapper">
      <div id="control_panel">
        <div class="control_panel_section">
          <h3>Background</h3>
          <div class="control_option">
            <label>
              <div>Select a background image:</div>
              <input type="file" id="image_upload" accept="image/*">
            </label>
          </div>
        </div>

        <div class="control_panel_section">
          <h3>Line styles</h3>
          …

        <div class="control_panel_section">
          <div class="control_option">
            <button class="btn" id="export">Export PNG</button>
          </div>

          <div class="control_option">
            <button class="btn btn-danger" id="reset">Clear</button>
          </div>
        </div>
      </div>
      <div id="draw_panel_wrapper">
        <canvas id="draw_panel"></canvas>
      </div>
    </div>
    <script src="js/page.js"></script>
    <script src="js/image.js"></script>
    <script src="js/eraser.js"></script>
    <script src="js/pencil.js"></script>
    <script src="js/rectangle.js"></script>
    <script src="js/shape.js"></script>
    
  

The significant elements that have been added to the page markup for this tutorial are:

  • A control panel section for the background image, which includes an input element with type="file"
  • A new button which allows the user to Export PNG
  • A new script on the page, loaded from js/image.js
Before we examine the contents of the js/image.js we will note that the general page initialization looks like this:

    
window.PAGE = (function(page){
  …
  page.init = function(canvas_id){
    // Other setup discussed in previous tutorials
    …

    if(typeof window.IMAGE !== "undefined"){
      window.IMAGE.init(page.ctx);
    }
    …
  };
  return page;
})({});

document.addEventListener("DOMContentLoaded", function(){
  PAGE.init("draw_panel");
});
    
  

We skip over the other elements of page initialization that were introduced in the earlier tutorials, focussing on what is relevant for the current tutorial. We see that, on page-load, we look for the IMAGE member on the global window object, and if this is defined we call IMAGE.init. This IMAGE object is defined in the js/image.js file using the revealing module pattern.

    
window.IMAGE = (function(i){
  let canvas = null,
    ctx = null,
    img_input = null;

  i.init = function(context){
    ctx = context;
    canvas = ctx.canvas;
    init_file_upload_handlers();
    init_export_button();
  };
  …

  return i;
})({});
    
 

The init method takes a reference to the drawing context, which we store along with a reference to the canvas itself. We then call separate functions for setting up the file-upload and export buttons. Let's look at each in turn.

Adding a background image to our canvas

To support the upload of images to our canvas we need to attach a change handler to the file input element, which we identify by its ID, image_upload. This event handling is set up in the init_file_upload_handlers function:

    
  const init_file_upload_handlers = function(){
    img_input = document.getElementById("image_upload");
    img_input.addEventListener('change', function(e) {
      if(e.target.files) {
        const reader = new FileReader();
        reader.readAsDataURL(e.target.files[0]);
        reader.onloadend = function (e) {
          const my_image = new Image();
          my_image.src = e.target.result;
          my_image.onload = function() {
            const orig_composite_op = ctx.globalCompositeOperation;
            ctx.globalCompositeOperation = 'destination-over';
            ctx.beginPath();
            draw_image_to_canvas(my_image, ctx);
            ctx.globalCompositeOperation = orig_composite_op;
          }
        }
      }
    });
  };
    
 

On handling the change event we can access the files member on the input element, which allows us to get a reference to the local file that was selected by the user, i.e. e.target.files[0].

We read this user-selected file by creating a browser-native FileReader object and calling readAsDataURL, passing the reference to the selected file. This will read the file data and, once complete, will make it availabe as a data URL. In the onloadend handler we can create an Image object for the user-selected image, and we set the image src attribute to equal this data URL.

To this newly created Image object we attach an onload handler, which will be triggered once the image source has been loaded. Within the onloadend handler we call the draw_image_to_canvas function, but before we do this we temporarily set the globalCompositeOperation property on the drawing context. Setting this property to have value destination-over means that new shapes are drawn behind the existing canvas content. This ensures that when we draw our image to the canvas, it will lie beneath any existing markings or shapes that we have already drawn on the canvas. The actual drawing of the image to the canvas takes place in the draw_image_to_canvas function:

    
  const draw_image_to_canvas = function(image, context){
    const canvas = context.canvas,
      horizontal_ratio = canvas.width/image.width,
      vertical_ratio = canvas.height/image.height,
      ratio = Math.min(horizontal_ratio, vertical_ratio),
      horizontal_offset = ( canvas.width - image.width*ratio ) / 2,
      vertical_offset = ( canvas.height - image.height*ratio ) / 2;

    context.drawImage(
      image,    // Reference to Image
      0,    // Source origin x
      0,    // Source origin y
      image.width,    // Source width to include
      image.height,    // Source height to include
      horizontal_offset,    // Destination x coordinate on canvas
      vertical_offset,    // Destination y coordinate on canvas
      image.width*ratio,    // Width on canvas
      image.height*ratio);    // Height on canvas
  };
    
  

Given the intrinsic size of our image, we determine which dimension (image width or height) is largest relative to the corresponding canvas dimension. We can then use this ratio to scale both width and height of the image equally, maintaining the aspect ratio and ensuring that both dimensions will fit within the canvas boundary. Drawing the image to the canvas is achieved with the help of the drawImage method on the drawing context. There are quite a few parameters in this method call: the first is the reference to the Image, the next four parameters define the part of the original image you wish to add to the canvas. In our case we want to add the whole image to the canvas, so we start at the image origin, (0,0) and include the full image.width and image.height. The next four parameters define where we want to place this image on our drawing context. We use the scaled width and height (based on ratio just calculated) and we also calculate an x- and y-offset on the canvas. This offset is based on the difference between the new scaled image dimensions and the width and height of our containing canvas. Honestly, the equations say it much more succinctly than words!

The functions detailed above should take our local file and draw a scaled and centered image on the background of our primary canvas.

Exporting our canvas as an image

As well as uploading images to our canvas, we also want to be able to export our canvas in some image format. We have included an Export PNG' button in our HTML, with ID export. The init_export_button is responsible for attaching a click event handler to this button to process the export request.

    
  const init_export_button = function(){
    const export_button = document.getElementById("export");
    export_button.addEventListener("click", function(e){
      const $temp_canvas = set_up_temp_canvas_for_export(),
        $link = document.createElement('a');

      $link.href = "" + $temp_canvas.toDataURL('image/png');
      $link.download = `vector-logic-${random()}.png`;
      $link.style.display = "none";
      document.body.appendChild($link);
      $link.click();
      document.body.removeChild($link);
      document.body.removeChild($temp_canvas);
    });
  };
    
  

We grab a reference to the export button and attach a click-handler. This handler uses a pretty standard trick to download the canvas image:

  • Create an anchor tag (<a>-tag)
  • Set the href attribute to equal the data URL for our canvas image
  • Set the download attribute, which should set the name of the downloaded file
  • Set style="display: none" so our link remains invisible to the user
  • Add the link to the DOM
  • Click the link
This set of steps will cause the image to be downloaded by the user's browser when they click the button. The main outstanding question is: how do we get the data URL for our canvas image?

When we have a single canvas this is relatively straightforward, we can call toDatatURL on the canvas element. This returns a string containing the data URL for the canvas image. But in our case we could have multipe canvases, owing to each rectangle being rendered on its own overlapping canvas. To address the problem we introduce the set_up_temp_canvas_for_export function.

    
  const set_up_temp_canvas_for_export = function(){
    const $temp_canvas = document.createElement('canvas'),
        $temp_ctx = $temp_canvas.getContext('2d');
    $temp_canvas.width = window.getComputedStyle(canvas, null)
      .getPropertyValue("width")
      .replace(/px$/, '');
    $temp_canvas.height = window.getComputedStyle(canvas, null)
      .getPropertyValue("height")
      .replace(/px$/, '');
    document.body.appendChild($temp_canvas);
    canvas.parentNode.querySelectorAll("canvas").forEach(function($canvas){
      $temp_ctx.drawImage($canvas, 0, 0);
    });
    return $temp_canvas;
  };
    
  

This method creates a new canvas element with dimensions matching our drawing canvas, and it appends this element to the page. The function then loops through the existing canvas elements and draws each of them, in turn, on to our single merged canvas. We can then call toDataURL on this single $temp_canvas, a composite of all the others. The export click-handler does exactly that: it gets the data URL from the $temp_canvas to set the link href, it clicks the link to download the file and finally it cleans up after itself by removing both the link and the $temp_canvas from the DOM.

Summary

Relative to Part 2 this tutorial was a lot shorter. We implemented functionality allowing the user to select a local image to apply to our canvas as a background, looking specifically at how one could scale and centre that image on the canvas. We also discussed how we could export our canvas creations as a PNG. We demonstrated a technique of setting up an invisible link and setting the href attribute to a data URL representing our canvas. We get this data URL by setting up a temporary canvas upon which we merge all the other canvases that we have accumulated during our drawing activities.

This image-export funcationality, in particular, required us to leverage some native browser functionality including the toDataUrl method on the canvas element, the drawImage and globalCompositeOperation properties on the rendering context and the FileReader object.

Hopefully you found this tutorial useful. If you have any feedback or questions please leave a comment.

In Part IV we will look at adding text captions to our canvas.

References

  1. You can access a hosted version of the drawing tool here
  2. The GitHub repo to accompany this series of blog posts
  3. The version of the tool built in this tutorial can be found on this branch
  4. The revealing module pattern for modular Javascript
  5. MDN docs for FileReader API
  6. MDN docs for globalCompositeOperation
  7. MDN docs for drawImage
  8. MDN docs for toDataURL on the canvas element

Comments

There are no existing comments

Got your own view or feedback? Share it with us below …

×

Subscribe

Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.