> Как отслеживать события мыши на canvas во frontend (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: ООО Свифтсэнд

Стек: JavaScript

> Пример ответа

Короткий ответ

Отслеживание событий мыши на canvas делается через стандартные DOM-события, навешиваемые на сам canvas-элемент: mousemove, mousedown, mouseup, click, mouseenter, mouseleave. Для получения координат относительно canvas используется getBoundingClientRect() и вычитание смещения. Для учёта CSS-масштабирования координаты корректируются отношением canvas.width / rect.width. При высокой частоте событий (например, рисование) применяется throttling или debouncing.

Подробное объяснение

Canvas - это растровый элемент, не хранящий информацию об отдельных объектах. Все события мыши приходят на DOM-узел <canvas>, и разработчик вручную вычисляет позицию курсора в пикселях canvas-координат. Основная сложность - корректный пересчёт координат при наличии CSS-стилей (padding, border, масштабирование) и при devicePixelRatio для HiDPI-экранов.

Для получения точных координат используется следующий подход:

  • canvas.getBoundingClientRect() возвращает размеры элемента в viewport-координатах с учётом CSS.
  • Координаты мыши из события (clientX, clientY) переводятся в локальные: x = event.clientX - rect.left, y = event.clientY - rect.top.
  • Если canvas имеет атрибуты width/height, отличные от CSS-размеров, координаты масштабируются: x *= canvas.width / rect.width.

Для работы с объектами на canvas (например, определение, попал ли клик в круг) применяется ручная проверка - либо через математические формулы, либо через offscreen-контекст с isPointInPath().

На практике

В реальных проектах (рисовалки, графики, игры) важно:

  1. Использовать pointer events вместо mouse events для поддержки touch и пера.
  2. Учитывать devicePixelRatio для чёткого рендеринга на retina-экранах - canvas создаётся с физическим разрешением, а CSS-размер остаётся логическим.
  3. Применять throttling для mousemove (обычно 30-60 FPS), чтобы не перегружать рендеринг.
  4. Для hit-testing сложных фигур использовать вспомогательный offscreen canvas или spatial indexing (quadtree).

Пример кода

JAVASCRIPT
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
function getCanvasCoords(event) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (event.clientX - rect.left) * scaleX,
y: (event.clientY - rect.top) * scaleY
};
}
canvas.addEventListener('click', (event) => {
const { x, y } = getCanvasCoords(event);
// Проверка попадания в круг
const dx = x - 100;
const dy = y - 100;
if (dx * dx + dy * dy <= 50 * 50) {
console.log('Клик по кругу');
}
});
// Для рисования с throttling
let drawing = false;
canvas.addEventListener('pointerdown', () => drawing = true);
canvas.addEventListener('pointerup', () => drawing = false);
canvas.addEventListener('pointermove', (event) => {
if (!drawing) return;
const { x, y } = getCanvasCoords(event);
// отрисовка линии
});

Как отвечать на собеседовании

Начни с базового механизма: событие на canvas, получение координат через getBoundingClientRect. Затем покажи понимание нюансов: учёт CSS-трансформаций, devicePixelRatio, разница между clientX/pageX/offsetX. Упомяни, что offsetX/offsetY не всегда надёжны из-за border-box. Для senior-уровня важно рассказать про производительность: throttling, requestAnimationFrame для рендеринга, избегание лишних перерисовок. Хорошо добавить про pointer events и hit-testing через isPointInPath.

Что проверяет интервьюер

  • Понимание разницы между DOM-координатами и canvas-координатами.
  • Знание проблем с HiDPI и CSS-масштабированием.
  • Умение оптимизировать обработку событий (throttling, RAF).
  • Опыт работы с hit-testing для произвольных фигур.
  • Понимание, что canvas не хранит сцену - всё нужно проверять вручную.

Типичные ошибки

  • Использование event.offsetX/event.offsetY без учёта CSS-масштабирования - эти свойства не учитывают разницу между атрибутами canvas и CSS-размерами.
  • Забывают про devicePixelRatio, из-за чего на retina-экранах координаты мыши не совпадают с пикселями canvas.
  • Не применяют throttling для mousemove, вызывая перерисовку на каждое движение мыши (до 1000 событий/сек).
  • Пытаются получить объект под курсором через DOM-методы - canvas не DOM-дерево.
  • Путают clientX/pageX/screenX при расчёте позиции относительно canvas.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью