Home About Me

Making a Canvas Respond: Clicks, Drags, Buttons, and Arrow Keys

Canvas input starts with a coordinate problem. A pointer event reports position relative to the viewport, while drawing commands expect position relative to the canvas. getBoundingClientRect() supplies the canvas's position, and subtracting rect.left and rect.top from clientX and clientY gives the correct point inside the canvas.

The first example draws a circle wherever the canvas is clicked:

var canvas = document.getElementById("canvas"),
    context = canvas.getContext("2d");

context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);

context.strokeStyle = '#abcdef';

canvas.addEventListener("click", function (event) {
  var rect = canvas.getBoundingClientRect(),
      x = event.clientX - rect.left,
      y = event.clientY - rect.top;

  console.log(event.clientX);
  console.log(rect.left);
  console.log(rect.top);
  context.beginPath();
  context.arc(x, y, 10, 0, Math.PI * 2);
  context.stroke();
});

The white fill clears the canvas before drawing, the stroke color controls the circle outline, and the console.log calls are only there to inspect the coordinate values. Once the click point is known, arc() can place a circle at that location.

Dragging turns the canvas into a simple drawing surface. mousedown records the starting point. While the mouse is down, mousemove draws a line from the previous point to the current one and updates the stored position. The mouseup listener is attached to document.body, not just the canvas, so releasing the pointer outside the canvas still ends the stroke.

var canvas = document.getElementById("canvas"),
    context = canvas.getContext("2d");

var rect,
    mouseX = 0,
    mouseY = 0;

context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);

context.strokeStyle = '#B31313';

canvas.addEventListener("mousedown", onMouseDown);

function onMouseDown(event) {
  rect = canvas.getBoundingClientRect();
  mouseX = event.clientX - rect.left;
  mouseY = event.clientY - rect.top;

  canvas.addEventListener("mousemove", onMouseMove);
  document.body.addEventListener("mouseup", onMouseUp);

}

function onMouseMove(event) {
  context.beginPath();
  context.moveTo(mouseX, mouseY);
  rect = canvas.getBoundingClientRect();
  mouseX = event.clientX - rect.left;
  mouseY = event.clientY - rect.top;
  context.lineTo(mouseX, mouseY);
  context.stroke();
}

function onMouseUp(event) {
  canvas.removeEventListener("mousemove", onMouseMove);
  document.body.removeEventListener("mouseup", onMouseUp);
}

A rectangular button is mostly a hit test and a redraw. The button state lives in a plain object with x, y, w, h, and selected. A click checks whether the pointer is inside that rectangle and flips selected. A separate drawButton() function chooses a fill color based on the state.

var canvas = document.getElementById("canvas"),
    context = canvas.getContext("2d");

btn = {
  x: 50,
  y: 50,
  w: 100,
  h: 50,
  selected: false
}

drawButton();

canvas.addEventListener('click', function () {
  var rect = canvas.getBoundingClientRect(),
      x = event.clientX - rect.left,
      y = event.clientY - rect.top;

  if (x >= btn.x && x <= btn.x + btn.y &&
      y >= btn.y && y <= btn.y + btn.h) {
    btn.selected = !btn.selected;
    drawButton();
  }
});

canvas.addEventListener('mousemove', function () {
  var rect = canvas.getBoundingClientRect(),
      x = event.clientX - rect.left,
      y = event.clientY - rect.top;

  if (x >= btn.x && x <= btn.x + btn.y &&
      y >= btn.y && y <= btn.y + btn.h) {
    btn.selected = !btn.selected;
    canvas.style.cursor = 'pointer';
  } else {
    canvas.style.cursor = 'auto';
  }
});

function drawButton() {
  context.fillStyle = btn.selected ? '#388E3C' : '#8BC34A';
  context.fillRect(btn.x, btn.y, btn.w, btn.h);
}

The horizontal bounds in this example use btn.y where a width would normally be expected (btn.w). The code is left as written, but that detail matters when adapting the hit test. The mousemove handler also toggles selected and sets the cursor, which is useful for seeing how redraws work even though a real button would usually separate hover styling from click state.

Keyboard input follows the same redraw pattern. keydown listens for the left and right arrow key codes, changes the circle's x coordinate, and calls draw(). That function clears the canvas and draws a fresh circle at the updated position.

var canvas = document.getElementById("canvas"),
    context = canvas.getContext("2d"),
    x = 50,
    y = 50;

document.body.addEventListener("keydown", function (event) {
    switch (event.keyCode) {
      case 37: // left
        x--;
        draw();
        break;
      case 39: // right
        x++;
        draw();
        break;
    }
});

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.beginPath();
  context.arc(x, y, 20, 0, Math.PI * 2);
  context.fill()
}

context.fillStyle = '#C75696';

draw();

Across these examples, the canvas itself keeps no persistent scene. Interaction changes variables, and a drawing function rebuilds what should be visible. Coordinate correction, event attachment, and clearing before redrawing are the pieces that make the surface feel responsive.