Interaction

Mouse, touch, and keyboard input: coordinate mapping, hit detection, dragging, and building interactive tools.

Lesson 7 of 10

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.

Event Object Inspector paused

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.

Mouse Position Tracking paused

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.

Mouse + Touch Drawing paused

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.

Keyboard Control: Arrow Keys to Move paused

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!
Click to Change Colors paused

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
Circle-Circle Collision: Drag the Blue Circle paused

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!
AABB Collision: Drag the Blue Box paused

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.
Drag and Drop paused

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.

Hover: Grow on Mouseover + Cursor Change paused

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
Scroll to Zoom, Drag to Pan paused

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.

Canvas UI Buttons paused

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.

Canvas Color Picker paused

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.

Mini Paint App paused

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, then ctx.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