> Как отслеживать события мыши на 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().
На практике
В реальных проектах (рисовалки, графики, игры) важно:
- Использовать
pointer eventsвместо mouse events для поддержки touch и пера. - Учитывать
devicePixelRatioдля чёткого рендеринга на retina-экранах - canvas создаётся с физическим разрешением, а CSS-размер остаётся логическим. - Применять throttling для
mousemove(обычно 30-60 FPS), чтобы не перегружать рендеринг. - Для hit-testing сложных фигур использовать вспомогательный offscreen canvas или spatial indexing (quadtree).
Пример кода
JAVASCRIPTconst 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('Клик по кругу');}});// Для рисования с throttlinglet 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.
> Похожие задачи по frontend
Как работает Redux Saga
Как работают редьюсеры в Redux
Каким методом промиса получить результат первого выполненного запроса
Какие инструменты позволяют убрать нагрузку с основного потока в JavaScript
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью