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:
- Draw free hand lines on the canvas (Part I)
- Draw resizable rectangles to highlight a section of the image (Part II)
- Set a background image on the canvas that we can annotate
- 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 withtype="file"
- A new button which allows the user to Export PNG
- A new script on the page, loaded from
js/image.js
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
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
- You can access a hosted version of the drawing tool here
- The GitHub repo to accompany this series of blog posts
- The version of the tool built in this tutorial can be found on this branch
- The revealing module pattern for modular Javascript
- MDN docs for FileReader API
- MDN docs for globalCompositeOperation
- MDN docs for drawImage
- 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 …