Mouse and Hit Testing

Tracking the Mouse and Hit Testing

Pointer events cover mouse, pen and touch. Subtract the canvas's getBoundingClientRect() from clientX and clientY, then test shapes from the top down with isPointInPath(); setPointerCapture() keeps a drag's events coming. Focusable buttons inside <canvas> are invisible but reach keyboards and screen readers, and drawFocusIfNeeded(path, element) rings a shape while its button has focus. booknest-data.js defines BOOKS, the six books with their colors.

Hover by hit testing, and a focus ring for the keyboardHTML
<canvas id="c" style="width:600px; height:84px"></canvas>
<script src="hidpi.js"></script><script src="booknest-data.js"></script>
<script>
  const ctx = hidpi(c), r = devicePixelRatio;  let hover = null;
  const covers = BOOKS.map((book, i) => {
    const path = new Path2D();  path.roundRect(8 + i * 98, 6, 84, 72, 6);
    const button = c.appendChild(document.createElement('button'));   // fallback content
    button.textContent = book.title;  button.onfocus = button.onblur = draw;
    return { ...book, path, button };
  });
  function draw() {
    ctx.clearRect(0, 0, 600, 84);
    for (const k of covers) {
      ctx.globalAlpha = k === hover ? 1 : 0.45;  ctx.fillStyle = k.color;  ctx.fill(k.path);
      ctx.globalAlpha = 1;  ctx.drawFocusIfNeeded(k.path, k.button);
    }
  }
  c.addEventListener('pointermove', e => {
    const box = c.getBoundingClientRect(), x = e.clientX - box.left, y = e.clientY - box.top;
    hover = covers.findLast(k => ctx.isPointInPath(k.path, x * r, y * r)) ?? null;  draw();
  });
  const o = c.getBoundingClientRect();                         // simulate a hover, then a Tab
  c.dispatchEvent(new PointerEvent('pointermove', { clientX: o.x + 250, clientY: o.y + 40 }));
  covers[4].button.focus({ preventScroll: true });  __done = true;
</script>
Browser output of Listing 2.22
Browser output of 22

The simulated pointer lights Salt and Saffron; the Clockmaker's button has focus, so its cover wears the ring.

Pointer tracking and isPointInPath hit testing on covers, with keyboard focus ringsHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
  canvas { width: 600px; max-width: 100%; height: 120px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<p id="info">Hover a cover, or press Tab to focus one.</p>
<script>
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  const BOOKS = [
    { title: 'The Quiet Harbor', color: '#1f5f8b' }, { title: 'Patterns of the Deep Web', color: '#5b3f99' },
    { title: 'Salt and Saffron', color: '#e09a10' }, { title: 'Small Steps to Big Summits', color: '#3f7d3a' },
    { title: "The Clockmaker's Paradox", color: '#b5452f' }, { title: 'Gardens in Glass', color: '#2a9d8f' }
  ];
  const c = document.getElementById('c');
  const ctx = hidpi(c), r = devicePixelRatio;
  let hover = null;

  const covers = BOOKS.map((book, i) => {
    const path = new Path2D();  path.roundRect(8 + i * 98, 10, 84, 100, 6);
    // Fallback content: invisible buttons that keyboards and screen readers can reach
    const button = c.appendChild(document.createElement('button'));
    button.textContent = book.title;
    button.onfocus = button.onblur = draw;
    return { ...book, path, button };
  });

  function draw() {
    ctx.clearRect(0, 0, 600, 120);
    for (const k of covers) {
      ctx.globalAlpha = k === hover ? 1 : 0.45;
      ctx.fillStyle = k.color;  ctx.fill(k.path);
      ctx.globalAlpha = 1;
      ctx.strokeStyle = '#263238';  ctx.lineWidth = 3;
      ctx.drawFocusIfNeeded(k.path, k.button);      // ring only while its button has focus
    }
    const focused = covers.find(k => k.button === document.activeElement);
    document.getElementById('info').textContent =
      `hover: ${hover ? hover.title : '–'} · focus: ${focused ? focused.title : '–'}`;
  }

  c.addEventListener('pointermove', e => {
    // Client coordinates minus the canvas box give CSS pixels; isPointInPath wants bitmap pixels
    const box = c.getBoundingClientRect(), x = e.clientX - box.left, y = e.clientY - box.top;
    hover = covers.findLast(k => ctx.isPointInPath(k.path, x * r, y * r)) ?? null;
    draw();
  });
  c.addEventListener('pointerleave', () => { hover = null; draw(); });

  // Simulate a hover over Salt and Saffron and keyboard focus on The Clockmaker's Paradox
  const o = c.getBoundingClientRect();
  c.dispatchEvent(new PointerEvent('pointermove', { clientX: o.x + 250, clientY: o.y + 50 }));
  covers[4].button.focus({ preventScroll: true });
</script>