Table of Contents
Input events (mouse and touch) on your canvas are the first step towards interactivity. Here we’ll go over event coordinates, context click (aka right click or tap hold), and double clicking. We’ll also cover common input problems.
Pointer input events are a slightly newer class of HTMLElement
event than their predecessors, Mouse and Touch input events. The idea was that any input ought to be handled by the same code path, and the Input event itself should contain information about the kind of device that gave the input (a mouse, a touch, a stylus pen, etc). Pointer events also contain optional information about the input tilt, pressure, and other variables.
Here’s a small HTML Canvas that you can draw on with mouse our touch input, and the code to make it work:
hovering over fills gray rectangles, pressing down creates a green rectangle, lifting up creates a red circle
<!-- Our HTML, just a Canvas. We'll set width/height in the JS -->
<canvas id="yourCanvasId"></canvas>
// Setup the canvas
const width = 400;
const height = 180;
const can = document.getElementById('yourCanvasId');
can.width = width;
can.height = height;
can.style.width = width + 'px';
can.style.height = height + 'px';
const ctx = can.getContext('2d');
// Given a MouseEvent or PointerEvent and a canvas reference, return
// x/y coordinates as a JavaScript object { x: number, y: number }
function getCoords(e, canvas) {
const bbox = canvas.getBoundingClientRect();
const mx = e.clientX - bbox.left * (canvas.width / bbox.width);
const my = e.clientY - bbox.top * (canvas.height / bbox.height);
return { x: mx, y: my };
}
can.addEventListener('pointerdown', function(e) {
var pt = getCoords(e, can);
// Fill a 6-pixel rectangle on the center of the coordinate:
ctx.fillStyle = 'lime';
ctx.fillRect(pt.x - 4, pt.y - 4, 8, 8);
});
can.addEventListener('pointermove', function(e) {
var pt = getCoords(e, can);
// Fill a 4-pixel rectangle on the center of the coordinate:
ctx.fillStyle = 'gray';
ctx.fillRect(pt.x - 2, pt.y - 2, 4, 4);
});
can.addEventListener('pointerup', function(e) {
var pt = getCoords(e, can);
// Fill a 6-pixel rectangle on the center of the coordinate:
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.ellipse(pt.x, pt.y, 8, 8, 0, 0, 2 * Math.PI);
ctx.fill();
});
note: If your canvas is not going to change size, you could cache the bounding rect in function getCoords
The code adds three event listeners to the Canvas element, and all three use the same getCoords
function, passing in their PointerEvent
.
Suppose instead we want to draw only when the mouse is clicked, instead we would write this:
let pointerHeld = false;
can.addEventListener('pointerdown', function(e) {
pointerHeld = true;
});
can.addEventListener('pointermove', function(e) {
// If the mouse or touch is not down, quit!
if (!pointerHeld) return;
var pt = getCoords(e, can);
// Fill a 4-pixel rectangle on the center of the coordinate:
ctx.fillStyle = 'gray';
ctx.fillRect(pt.x - 2, pt.y - 2, 4, 4);
});
can.addEventListener('pointerup', function(e) {
pointerHeld = false;
});
To such an effect:
click and hold to draw
If you right-click on the canvas above, you’ll see the browser’s built-in context menu (Save Image as… etc). If you right click below, it will instead draw a circle and tell you the PointerEvent.button
value of your click or tap:
this canvas text may look a little blurry - see Pixel Density for how to fix that
To achieve the above we need to prevent the browser’s default behavior. We can do this by adding an event listener to the canvas for the 'contextmenu'
event, and prevent it:
can.addEventListener('contextmenu', function(e) {
e.preventDefault(); // stop the context menu
});
Then, in a 'pointerup'
event we can look at the value of e.button
and decide what to do differently when the value is 2
:
can.addEventListener('pointerup', function(e) {
let color = "lime";
if (e.button === 2) color = "red";
var pt = getCoords(e, can);
// Fill a 6-pixel rectangle on the center of the coordinate:
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(pt.x, pt.y, 8, 8, 0, 0, 2 * Math.PI);
ctx.fill();
// Write the value of PointerEvent.button:
ctx.fillStyle = 'black';
ctx.fillText(`button: ${e.button}`, pt.x, pt.y);
});
Like with context clicking, there is somewhat-annoying default browser behavior: Double-clicking on a canvas will select text from the next HTML element underneath it. Tap-hold on mobile devices will do the same thing. Thankfully, this can be stopped in a similar fashion by preventing the 'selectstart'
event on the canvas:
can.addEventListener('selectstart', function(e) {
e.preventDefault(); // stop double click text selection
});
Double clicking is simple if you want it to be. You can listen for the 'dblclick'
event just like any of the pointer events.
can.addEventListener('dblclick', function(e) {
var pt = getCoords(e, can);
ctx.fillStyle = "teal";
ctx.beginPath();
ctx.ellipse(pt.x, pt.y, 8, 8, 0, 0, 2 * Math.PI);
ctx.fill();
});
This 'dblclick'
event works on mobile too. However, it’s not very flexible, for instance it cannot detect right-button double clicks. So you might want to consider implementing your own with a timer, depending on your needs.
Note that CSS padding
and border
values on the canvas will offset your coordinates, making them inaccurate:
<canvas id="canvas2" style="border: solid 36px lightcoral;"></canvas>
border set on the canvas takes up canvas area, and makes input coordinates inaccurate near the edges.
Instead, you should consider putting any border
on a surrounding DIV. This will keep coordinates accurate, without having to involve extra offsetting math in function getCoords
code:
<div style="border: solid 36px lightgreen; width: fit-content;">
<canvas id="canvas3"></canvas>
</div>
For more interesting usage of input coordinates, see the tutorial on making movable, selectable shapes.