Interaction
Mouse, touch, and keyboard input: coordinate mapping, hit detection, dragging, and building interactive tools.
Everything you have built so far is passive; the user watches but cannot interact. This lesson changes that. You will respond to mouse clicks, track cursor movement, detect which shape was clicked, and build drag-and-drop tools. By the end, your canvas will feel like an application, not a picture.
What Are Browser Events?
Before we make our canvas interactive, let’s understand how browsers tell your code that something happened. Every time a user clicks, moves the mouse, presses a key, scrolls, or touches the screen, the browser creates an event object and sends it to your code. Think of it like a mail carrier delivering a letter every time something happens; your code is the mailbox.
Analogy: Imagine you are sitting in a room with a doorbell. You do not constantly check the door. Instead, you listen for the bell. When it rings, you go check who is there. Browser events work the same way: you register a “listener” function, and the browser calls it when that event happens.
Your code The browser
┌──────────┐ ┌───────────────┐
│ listener │◄───────│ "click" event │
│ function │ │ happened! │
└──────────┘ └───────────────┘
│
▼
"Oh! The user
clicked at
x=150, y=80" When we do addListener(canvas, ‘mousemove’, function(e) { ... }), we are telling the browser: “whenever the mouse moves over the canvas, run this function and give me the details in e.” The e parameter (the event object) contains everything about what happened: where the mouse is, which button was pressed, whether Shift was held, and more.
The Event Object
Every event handler receives an event object packed with useful properties. Here are the ones you will use most often for Canvas interaction:
Key event properties for mouse events:
e.clientX, e.clientY → position relative to the browser window e.pageX, e.pageY → position relative to the entire page e.offsetX, e.offsetY → position relative to the target element e.button → which mouse button (0=left, 1=middle, 2=right) e.buttons → bitmask of currently pressed buttons e.shiftKey → true if Shift is held e.ctrlKey → true if Ctrl/Cmd is held e.altKey → true if Alt is held e.preventDefault() → stop the browser default action e.target → the element the event fired on
Let’s build a demo that shows you the event object in real time. Move your mouse, click, and hold modifier keys to see the properties change.
Try this: Hold Shift while moving the mouse to see modifier key detection. Then try changing the circle color from cyan to something based on position, like 'hsl(' + (x / w * 360) + ', 80%, 60%)'.
Understanding Coordinate Systems
This is the number one source of confusion for beginners. When the browser reports a mouse position, it uses the browser window’s coordinate system. But your canvas has its own coordinate system. If the canvas is not at the top-left corner of the page, these numbers will be different. You must convert.
Three coordinate systems at play:
Page coordinates (e.pageX, e.pageY) ┌─────────────────────────────────────────┐ │ Browser window (may be scrolled) │ │ │ │ Client coordinates │ │ (e.clientX, e.clientY) │ │ ┌───────────────────────────┐ │ │ │ Visible viewport │ │ │ │ │ │ │ │ ┌────────────┐ │ │ │ │ │ CANVAS │ ◄── Canvas coords │ │ │ │ (0,0) │ │ │ │ │ │ · click │ │ │ │ │ └────────────┘ │ │ │ │ │ │ │ └───────────────────────────┘ │ └─────────────────────────────────────────┘ To convert from client → canvas: canvasX = e.clientX - canvas.getBoundingClientRect().left canvasY = e.clientY - canvas.getBoundingClientRect().top
canvas.getBoundingClientRect() returns an object with left, top, width, and height, representing the canvas’s position and size in the viewport. Subtracting left and top from the mouse’s client position gives you the position relative to the canvas’s top-left corner.
DPI and devicePixelRatio
On high-DPI screens (Retina displays, modern phones), one CSS pixel might actually be 2 or 3 real screen pixels. The CanvasDemo component in this tutorial already handles DPI scaling for you, but you need to understand it for your own projects.
The DPI scaling problem and solution:
Without DPI scaling (blurry on Retina): ┌───────────────────────┐ │ canvas.width = 500 │ CSS size: 500px │ canvas.height = 400 │ Pixel buffer: 500x400 │ Looks blurry on 2x! │ (half the pixels needed) └───────────────────────┘ With DPI scaling (sharp): ┌───────────────────────┐ │ canvas.width = 1000 │ CSS size: 500px │ canvas.height = 800 │ Pixel buffer: 1000x800 │ ctx.scale(2, 2) │ (we draw at 500x400 scale, │ Sharp on Retina! │ rendered at 2x resolution) └───────────────────────┘ The code: var dpr = window.devicePixelRatio || 1; canvas.width = desiredWidth * dpr; canvas.height = desiredHeight * dpr; canvas.style.width = desiredWidth + 'px'; canvas.style.height = desiredHeight + 'px'; ctx.scale(dpr, dpr);
After this setup, you draw using your original coordinates (e.g. 500x400), and the canvas renders at the correct resolution.
When converting mouse coordinates on a DPI-scaled canvas, you must account for the difference between the CSS size and the drawing coordinate space:
// Full coordinate conversion formula: var rect = canvas.getBoundingClientRect(); var canvasX = (e.clientX - rect.left) * (logicalWidth / rect.width); var canvasY = (e.clientY - rect.top) * (logicalHeight / rect.height); // logicalWidth = your drawing coordinate width (e.g. 500) // rect.width = actual CSS rendered width on screen // The ratio handles any scaling (CSS resize, DPI, etc.)
Touch Events for Mobile
Mouse events do not fire on phones and tablets. Instead, you get touch events. Touch events are similar to mouse events but support multiple simultaneous touches (fingers). The three core touch events are:
Touch events vs. mouse events:
Mouse events: Touch events: mousedown → touchstart mousemove → touchmove mouseup → touchend Key difference: touch events store positions in e.touches[] (active touches) and e.changedTouches[] (touches that changed). Getting the first touch position: var touch = e.touches[0]; var x = touch.clientX; var y = touch.clientY;
The demo below works with both mouse and touch. Try it on a phone, or use browser dev tools to simulate touch.
Tip: Always call e.preventDefault() in touch handlers on a canvas. Without it, the browser will try to scroll the page when the user drags their finger, fighting your drawing code.
Keyboard Events on Canvas
Canvas elements do not receive keyboard events by default because they are not “focusable” elements (like input fields). To make a canvas keyboard-interactive, you need to give it a tabindex attribute and then focus it. Here we listen on the window for simplicity; use the arrow keys to move the square.
Why track keys in an object? If you just move in the keydown handler, the player jumps once per key press. By storing which keys are pressed and checking every frame, movement is smooth and continuous. This pattern is standard in game development.
Circle Hit Detection (Distance Formula)
How do you know if a click landed on a circle? You need the distance formula from math class. If the distance from the click to the circle’s center is less than the circle’s radius, the click is inside.
The distance formula:
click point (cx, cy)
·
/|
/ |
distance / │ dy = cy - circle.y
/ │
/ │
circle center ·──────┘
(circle.x, circle.y)
dx = cx - circle.x
distance = sqrt(dx*dx + dy*dy) ← Pythagorean theorem!
if (distance < circle.radius) → HIT!
if (distance >= circle.radius) → MISS! Circle-Circle Collision Detection
What about detecting when two circles overlap (collide)? The idea is the same as click detection: measure the distance between their centers. If that distance is less than the sum of their radii, they are overlapping.
Circle A (r=30) Circle B (r=20)
┌───┐ ┌──┐
╱ A ╲ ╱ B ╲
│ ·───┼─────────────┼──· │
╲ ╱ distance ╲ ╱
└───┘ └──┘
◄──────────────►
dist
Collision if: dist < A.r + B.r (30 + 20 = 50)
No collision if: dist >= A.r + B.r Rectangle Collision (AABB)
AABB stands for “Axis-Aligned Bounding Box,” a rectangle that is not rotated. This is the most common collision check in 2D games. Instead of distance, you check whether two rectangles overlap on both the X and Y axes.
AABB overlap logic:
Two rectangles overlap if and only if they overlap
on BOTH the X axis AND the Y axis.
Box A Box B
┌─────────┐ ┌─────────┐
│ Ax, Ay │ │ Bx, By │
│ │ │ │
│ Ax+Aw┤ │ Bx+Bw┤
└─────────┘ └─────────┘
Ay+Ah By+Bh
Overlap on X axis:
A's left edge < B's right edge AND
A's right edge > B's left edge
Overlap on Y axis:
A's top edge < B's bottom edge AND
A's bottom edge > B's top edge
In code:
Ax < Bx + Bw && Ax + Aw > Bx &&
Ay < By + Bh && Ay + Ah > By
If ALL FOUR are true → collision! Dragging Objects
Dragging combines three events: mousedown (start), mousemove (update), and mouseup (stop). The key trick is storing the offset, the distance between the click point and the object’s corner, so the object does not “jump” to the cursor.
Step 1: mousedown ┌───────────┐ │ BOX │ Click happens at (clickX, clickY) │ · ◄─────┼── offset = click - box position └───────────┘ Step 2: mousemove Mouse moves to (newX, newY) box.x = newX - offsetX ← preserves the grab point box.y = newY - offsetY Step 3: mouseup Stop tracking.
Hover Effects and Cursor Styles
You can make canvas feel more like a real UI by changing the cursor when the mouse is over clickable things. The canvas.style.cursor property lets you set any CSS cursor: ‘pointer’, ‘grab’, ‘crosshair’, etc.
Tooltips on hover: To add tooltips, combine hover detection (above) with ctx.roundRect() to draw a background rectangle, then ctx.fillText() for the label. Position the tooltip near the hovered item and check bounds to keep it on screen.
Zoom and Pan (Wheel + Drag)
For data visualization or maps, you often need to let users zoom in/out with the mouse wheel and pan by dragging. This uses canvas translate() and scale() transforms, which shift and resize the coordinate system before drawing. If you covered transforms in an earlier lesson, this is the same idea applied to user input. We also use ctx.save() and ctx.restore() to isolate the transform so it does not affect UI overlays drawn afterward.
How zoom and pan work together:
Without transform: With translate + scale: ┌──────────────┐ ┌──────────────┐ │ · · · · · · │ │ │ │ · · · · · · │ zoom in │ · · · · │ │ · · · · · · │ ───────→ │ · · · · │ │ · · · · · · │ │ · · · · │ └──────────────┘ └──────────────┘ All dots visible Fewer dots, bigger ctx.translate(panX, panY); // move the "camera" ctx.scale(zoom, zoom); // zoom level // then draw everything at normal coordinates
Building Canvas Buttons
HTML buttons are great, but sometimes you need buttons drawn directly on the canvas (for a game UI, for example). You build them by drawing rectangles with text, then checking if clicks land inside.
Marquee selection: To build a drag-to-select box, combine the drag pattern (above) with AABB collision: track mousedown/mousemove to define a rectangle, then on mouseup check which objects fall inside it. Use ctx.setLineDash([4, 4]) for the dashed selection border.
Practical Example: Color Picker
Let’s build something useful: a hue/saturation color picker drawn entirely on canvas. This shows how interaction, gradients, and real-time feedback come together.
Practical Example: Mini Paint App
Let’s combine everything into a tiny paint application. It has a color bar, brush size control, and a drawing canvas, all rendered with Canvas 2D.
What you learned in this lesson:
- Browser events deliver information through the event object (
e) - Convert page/client coordinates to canvas coordinates with
getBoundingClientRect() - DPI scaling: multiply canvas size by
devicePixelRatio, thenctx.scale() - Touch events mirror mouse events but use
e.touches[0] - Keyboard input: track pressed keys in an object, check every frame
- Circle hit detection:
distance < radius(Pythagorean theorem) - Circle-circle collision:
distance < radiusA + radiusB - AABB collision: four overlap checks on X and Y axes
- Drag = mousedown (start + offset) → mousemove (update) → mouseup (stop)
- Cursor styles:
canvas.style.cursor = ‘pointer’ - Zoom/pan:
ctx.translate()+ctx.scale()with wheel events - Canvas can be used to build complete UI elements like buttons and color pickers