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.
<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>
The simulated pointer lights Salt and Saffron; the Clockmaker's button has focus, so its cover wears the ring.
<!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>