логотип PurpleSchool
логотип PurpleSchool

Работа с Canvas во Vue

Автор

Олег Марков

Введение

Canvas — мощный инструмент для работы с растровой графикой в браузере. Он позволяет создавать интерактивные графики, динамическую анимацию, игры и визуализации, работая низкоуровнево с пикселями на странице. Если вы используете Vue для построения пользовательских интерфейсов, интеграция Canvas предоставляет огромные возможности для создания визуальных и креативных решений прямо внутри ваших компонентов.

В этой статье я покажу, как вы можете использовать Canvas во Vue, на что стоит обратить внимание при организации кода, какие сложности могут возникнуть, и какие паттерны обеспечивают удобную и производительную работу с графикой. Мы разберём базовые примеры, а также советы по интеграции событий и реактивности, чтобы сделать рисунки интерактивными.

Рендеринг Canvas во Vue-компоненте

Базовая интеграция Canvas

Давайте начнем с простого: добавим элемент canvas на страницу через Vue-компонент.

<template>
  <canvas ref="myCanvas" width="400" height="300"></canvas>
</template>

<script>
export default {
  mounted() {
    // Получаем ссылку на DOM-элемент canvas
    const canvas = this.$refs.myCanvas;
    // Получаем 2D контекст для рисования
    const ctx = canvas.getContext('2d');
    // Простейшее рисование: красный прямоугольник
    ctx.fillStyle = 'red';
    ctx.fillRect(10, 10, 100, 80); // x, y, width, height
  }
};
</script>

Как видите, всё просто: мы получаем доступ к canvas через ref, а в mounted() можем сразу работать с ним, ведь компонент уже добавлен в DOM.

Управление размерами и стилями Canvas

Очень важно правильно задавать размеры canvas — это влияет на его плотность пикселей:

  • Атрибуты width и height задают внутренний размер полотна (в пикселях!).
  • CSS-свойства определяют отображаемый размер.

Если вы не сделаете их одинаковыми, изображение может быть растянутым или размытым:

<canvas 
  ref="mainCanvas" 
  width="600"    // внутренний размер
  height="400"   // внутренний размер
  style="width: 600px; height: 400px;" // CSS размер
></canvas>

Если вы хотите сделать canvas адаптивным, ширину и высоту стоит выставлять программно из кода — через props или computed значения, которые вычисляют нужные параметры по размеру окна.

Давайте рассмотрим адаптивный пример:

<template>
  <canvas
    ref="responsiveCanvas"
    :width="canvasWidth"
    :height="canvasHeight"
    style="width: 100%; height: auto"
  ></canvas>
</template>

<script>
export default {
  data() {
    return {
      canvasWidth: 0,
      canvasHeight: 0
    }
  },
  mounted() {
    this.updateCanvasSize();
    window.addEventListener('resize', this.updateCanvasSize);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.updateCanvasSize);
  },
  methods: {
    updateCanvasSize() {
      const width = window.innerWidth * 0.8;
      const height = width * 0.6;
      this.canvasWidth = Math.floor(width);
      this.canvasHeight = Math.floor(height);
    }
  }
}
</script>

Использование Canvas API в Vue

Работа с 2d-контекстом

Большинство задач реализуется через 2d context, у которого огромный набор методов:

  • fillRect, strokeRect — прямоугольники,
  • beginPath, moveTo, lineTo, arc, closePath, stroke, fill — произвольная графика,
  • drawImage — отрисовка изображений,
  • getImageData, putImageData — работа с пикселями, создание фильтров.

Пример рисования круга и линии:

mounted() {
  const canvas = this.$refs.myCanvas;
  const ctx = canvas.getContext('2d');
  
  // Черная линия
  ctx.beginPath();
  ctx.moveTo(120, 80); // Начальная точка
  ctx.lineTo(340, 230); // Конечная точка
  ctx.strokeStyle = '#000';
  ctx.lineWidth = 4;
  ctx.stroke();
  ctx.closePath();

  // Синяя окружность
  ctx.beginPath();
  ctx.arc(200, 150, 60, 0, 2 * Math.PI); // x, y, radius, startAngle, endAngle
  ctx.fillStyle = 'blue';
  ctx.fill();
  ctx.closePath();
}

Обратите внимание: чтобы избежать "наслоения" путей, вызывайте beginPath() перед рисованием и closePath() после.

Использование Canvas и реактивности во Vue

Canvas по своей сути не реактивен — любые изменения состояния Vue не приводят к перерисовке канваса. Чтобы иметь динамическую графику, нужно явно вызывать функцию отрисовки каждый раз, когда данные меняются.

Рассмотрим пример: рисуем круг, радиус которого управляется через элемент управления (input):

<template>
  <div>
    <input type="range" min="10" max="150" v-model="radius" @input="draw" />
    <canvas ref="circleCanvas" width="300" height="300"></canvas>
  </div>
</template>

<script>
export default {
  data() {
    return {
      radius: 50
    }
  },
  mounted() {
    this.draw();
  },
  methods: {
    draw() {
      const canvas = this.$refs.circleCanvas;
      const ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, canvas.width, canvas.height); // Очищаем холст

      ctx.beginPath();
      ctx.arc(150, 150, this.radius, 0, 2 * Math.PI);
      ctx.strokeStyle = 'green';
      ctx.lineWidth = 8;
      ctx.stroke();
      ctx.closePath();
    }
  }
}
</script>

Теперь изменение слайдера input приводит к перерисовке круга с новым радиусом. Такой подход работает всегда: любые реактивные данные Vue могут быть визуализированы на canvas, если запускать draw-функцию при каждом изменении.

Взаимодействие с событиями мыши

Canvas из коробки не имеет разметки DOM, поэтому чтобы обработать клик или движение мыши, мы вешаем события на сам canvas и вычисляем координаты вручную.

Допустим, вы хотите рисовать линии, следуя за мышкой:

<template>
  <canvas ref="drawCanvas" width="500" height="350"
      @mousedown="onMouseDown"
      @mousemove="onMouseMove"
      @mouseup="onMouseUp"></canvas>
</template>

<script>
export default {
  data() {
    return {
      isDrawing: false,
      lastX: 0,
      lastY: 0
    }
  },
  methods: {
    getMousePos(e) {
      const rect = this.$refs.drawCanvas.getBoundingClientRect();
      return {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top
      }
    },
    onMouseDown(e) {
      const pos = this.getMousePos(e);
      this.isDrawing = true;
      this.lastX = pos.x;
      this.lastY = pos.y;
    },
    onMouseMove(e) {
      if (!this.isDrawing) return;
      const canvas = this.$refs.drawCanvas;
      const ctx = canvas.getContext('2d');
      const pos = this.getMousePos(e);

      ctx.beginPath();
      ctx.moveTo(this.lastX, this.lastY);
      ctx.lineTo(pos.x, pos.y);
      ctx.strokeStyle = 'blue';
      ctx.lineWidth = 3;
      ctx.stroke();
      ctx.closePath();

      this.lastX = pos.x;
      this.lastY = pos.y;
    },
    onMouseUp() {
      this.isDrawing = false;
    }
  }
}
</script>

Этот компонент позволяет рисовать линии мышью. Я добавил функцию для пересчета координат мыши относительно canvas, потому что клики бывают в разных координатах, если страница прокручена или у canvas есть отступы.

Инкапсуляция Canvas-логики

Чтобы поддерживать ваш код в чистоте и порядке, лучше инкапсулировать взаимодействие с canvas в отдельные методы или даже выделять отдельные компоненты для разных задач (например, один компонент для рисования линий, другой — для complex visualization).

Вы можете также использовать provide/inject или отдельные сервисы (например, классы или composable-функции во Vue 3).

Пример использования composable во Vue 3

Допустим, у вас есть простая функция для инициализации canvas, которую вы хотите повторно использовать:

// useCanvas.js
import { ref, onMounted, onUnmounted } from "vue";

export default function useCanvas(drawFn) {
  const canvasRef = ref(null);

  onMounted(() => {
    if (canvasRef.value && typeof drawFn === 'function') {
      // Передаем ctx в drawFn
      const ctx = canvasRef.value.getContext('2d');
      drawFn(ctx);
    }
  });

  return {
    canvasRef
  }
}

Используется так:

<template>
  <canvas ref="canvasRef" width="200" height="200"></canvas>
</template>

<script>
import useCanvas from './useCanvas';

export default {
  setup() {
    // Передаем функцию, описывающую отрисовку
    const { canvasRef } = useCanvas((ctx) => {
      ctx.fillStyle = 'orange';
      ctx.fillRect(40, 40, 120, 120);
    });

    return { canvasRef };
  }
}
</script>

Отрисовка изображений, работа с пикселями и продвинутая графика

Рисование изображений

Вы можете рисовать изображения и работать с ними на canvas — для этого используйте метод drawImage. Важно дождаться загрузки изображения, прежде чем рисовать его на canvas.

mounted() {
  const canvas = this.$refs.imageCanvas;
  const ctx = canvas.getContext('2d');
  const img = new Image();
  img.src = 'https://placekitten.com/400/300';
  img.onload = () => {
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  }
}

Работа с пикселями

Canvas позволяет манипулировать каждым пикселем с помощью методов getImageData и putImageData. Это можно использовать для создания фильтров — например, инвертировать цвета изображения:

methods: {
  invertColors() {
    const canvas = this.$refs.imageCanvas;
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    // Инвертируем каждый пиксель
    for (let i = 0; i < data.length; i += 4) {
      data[i] = 255 - data[i];       // Красный
      data[i + 1] = 255 - data[i+1]; // Зеленый
      data[i + 2] = 255 - data[i+2]; // Синий
    }

    ctx.putImageData(imageData, 0, 0);
  }
}

Вызывайте этот метод кнопкой, и изображение на canvas поменяет цвета на противоположные.

Анимация и render loop в Vue

Если вы делаете анимацию, используйте requestAnimationFrame, чтобы обеспечить плавность и максимально эффективный рендеринг.

data() {
  return {
    x: 0
  }
},
mounted() {
  this.animate();
},
methods: {
  animate() {
    const canvas = this.$refs.animCanvas;
    const ctx = canvas.getContext('2d');

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = 'purple';
    ctx.beginPath();
    ctx.arc(this.x, 50, 30, 0, 2 * Math.PI);
    ctx.fill();

    // Увеличение x, чтобы шар двигался вправо
    this.x = (this.x + 2) % canvas.width;

    // Рекурсивный вызов на следующий кадр
    requestAnimationFrame(this.animate);
  }
}

В этом примере по канвасу бегает шарик, и за счет рекурсивного вызова animate анимация остаётся плавной и не блокирует основной поток.

Интеграция Canvas с жизненным циклом Vue

Рекомендуется всегда инициализировать canvas на стадии mounted — так он гарантированно существует в DOM. Если вам нужно сбрасывать/перерисовывать canvas при обновлении props, используйте watch для отслеживания изменений и вызывайте функцию отрисовки при каждом изменении.

watch: {
  someProp() {
    this.draw();
  }
}

Помните, что если компонент уничтожается, отписывайтесь от global-событий или отменяйте animation loop, чтобы избежать утечек памяти.

Популярные библиотеки для Canvas и Vue

  • vue-konva — обертка над популярной библиотекой Konva (для сложной графики, drag&drop, групп и слоев).
  • vue-canvas — облегчённый способ подключения canvas с поддержкой реактивных свойств.
  • Fabric.js — не vue-специфичный, но отлично дружит с Vue при интеграции через refs.

Использование библиотеки обосновано, если вам нужна многослойная графика, drag&drop объектов или масштабирование. Однако даже "чистый Canvas" легко интегрируется в ваше приложение на Vue — главное соблюдать принципы управления состоянием, чтобы ваша отрисовка всегда отображала актуальные данные.

Итог

Реализация Canvas во Vue — это отличный способ создавать динамическую и интерактивную графику, объединяя мощь низкоуровневых API браузера с реактивностью Vue. Вы можете использовать базовые механики для рисования, а также продвинутые техники: анимации, обработка изображений, интеграция с lifecycle хуками. Благодаря гибкой структуре компонентов и жизненному циклу Vue любые нестандартные фичи легко реализуются, если правильно подключать канвас, контролировать событие и поддерживать актуальность данных для рендера.

Частозадаваемые технические вопросы по теме статьи и ответы на них

1. Как сохранить изображение из Canvas на диск пользователя?
Вам нужно получить данные из canvas методом toDataURL(), а затем создать ссылку (<a>) для скачивания: javascript const canvas = this.$refs.myCanvas; const dataURL = canvas.toDataURL('image/png'); const link = document.createElement('a'); link.href = dataURL; link.download = 'canvas-image.png'; link.click();

2. Как очистить canvas полностью?
Используйте метод clearRect: javascript const ctx = this.$refs.myCanvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); Это удаляет ВСЕ нарисованное на canvas.

3. Как получить координаты клика относительно канваса при скролле или сдвиге страницы?
Вам поможет метод getBoundingClientRect: javascript const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top;

4. Как оптимизировать перерисовку при частых изменениях данных?
Используйте requestAnimationFrame для отрисовки, и устраивайте redraw только при необходимости (например, debounce/fps limit для событий). Обновляйте только те части canvas, которые действительно изменились.

5. Почему изображение выглядит размытым на устройствах с высокой плотностью пикселей (Retina)?
Canvas нужно увеличивать пропорционально devicePixelRatio: javascript const ratio = window.devicePixelRatio || 1; canvas.width = width * ratio; canvas.height = height * ratio; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; ctx.setTransform(ratio, 0, 0, ratio, 0, 0); // Масштабирование Так изображение выйдет четким.

Стрелочка влево3 способа интеграции Chart.js с Vue для создания графиковИнструкция по реализации календаря во VueСтрелочка вправо

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
Управление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueРабота с teleport для управления DOM во VueПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиРуководство по nextTick для работы с DOMСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний