Canvas JS: создание графики и обработка событий

Раздел: JavaScript -> Рисование

Основные возможности Canvas

Элемент <canvas> предоставляет растровую область для рисования при помощи JavaScript. Получив 2D контекст, программист может создавать фигуры, текст, изображения и анимации. Ниже разобраны ключевые техники с примерами и типичными ошибками.

Как создать и настроить холст?

Самый надёжный способ - задать размеры через атрибуты width и height и получить контекст методом getContext('2d').


<canvas id='myCanvas' width='400' height='300'></canvas>
<script>
  const canvas = document.getElementById('myCanvas');
  const ctx = canvas.getContext('2d');
  // теперь ctx - объект CanvasRenderingContext2D
</script>
  

Canvas js (canvas в javascript)

Если холст уже есть, размер можно изменить через CSS, но при этом его внутреннее разрешение (width/height) останется прежним, что приведёт к растяжению. Лучше устанавливать оба параметра в JS:


canvas.width = 800;
canvas.height = 600;
  

Варианты с getContext('webgl') для 3D или getContext('bitmaprenderer') для высокопроизводительного отображения ImageBitmap не рассматриваются в рамках 2D.

Ошибка: холст отображается, но рисунка нет - скорее всего, забыли вызвать getContext или указали неправильный идентификатор. Решение: проверьте наличие элемента в DOM перед скриптом. Проблема: при изменении размеров через CSS контекст сбрасывается - после смены ширины/высоты атрибута нужно переустановить все настройки (стили линий, цвета).

Как рисовать прямоугольники?

Прямоугольники - самые простые фигуры. Используйте fillRect(x, y, width, height) и strokeRect(x, y, width, height).


ctx.fillStyle = '#3498db';
ctx.fillRect(10, 10, 100, 50);
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 3;
ctx.strokeRect(10, 10, 100, 50);
  

Для очистки области используйте clearRect(x, y, w, h). Чтобы создать прямоугольник с градиентом, задайте fillStyle как градиент:


const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
  

Типичная ошибка: при рисовании нескольких прямоугольников они накладываются в том порядке, в котором вызываются. Чтобы «задний фон» не перекрывал передний, рисуйте сначала фон, а потом детали. Ещё: fillRect сразу рендерит на холст, его нельзя отменить - используйте ctx.save()/ctx.restore() для изоляции стилей.

Как рисовать пути и сложные фигуры?

Для произвольных контуров используйте beginPath(), moveTo(), lineTo(), arc() и завершайте closePath() при необходимости. Затем fill() или stroke().


ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.lineTo(100, 150);
ctx.closePath();
ctx.fillStyle = 'green';
ctx.fill();
ctx.strokeStyle = 'black';
ctx.stroke();
  

Так создаётся треугольник.

Для кривых: quadraticCurveTo(cpx, cpy, x, y) и bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y). Пример кубической кривой:


ctx.beginPath();
ctx.moveTo(20, 120);
ctx.bezierCurveTo(20, 20, 180, 220, 180, 120);
ctx.stroke();
  

Также можно комбинировать arcTo() для закруглений.

Ошибка: если не вызвать closePath(), то при fill() контур автоматически закрывается, но при stroke() останется разрыв. Решение: явно закрывайте путь, если нужна замкнутая линия. Проблема: при заливке самопересекающихся фигур может применяться правило non-zero - для сложных случаев используйте ctx.fill('evenodd').

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

Текст выводится методами fillText(text, x, y) и strokeText(text, x, y). Укажите шрифт через ctx.font.


ctx.font = '30px Arial';
ctx.fillStyle = 'purple';
ctx.fillText('Привет, Canvas!', 50, 100);
ctx.strokeStyle = 'orange';
ctx.lineWidth = 1;
ctx.strokeText('Привет, Canvas!', 50, 100);
  

Для выравнивания используйте textAlign и textBaseline. Чтобы узнать ширину текста (например, для центрирования), вызовите ctx.measureText(text).width.


const text = 'Canvas';
ctx.font = '20px Verdana';
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
ctx.fillText(text, (canvas.width - textWidth) / 2, 50);
  

Для многострочного текста придётся разбивать строку вручную, так как встроенного переноса нет.

Типичная ошибка: текст не отображается из-за неправильного формата font - строка должна содержать размер и семейство, например '16px sans-serif'. Проблема: если fillText вызывается до загрузки шрифта, может отобразиться шрифт по умолчанию - используйте document.fonts.ready или загружайте шрифт через CSS @font-face с ожиданием.

Как работать с изображениями?

Изображение рисуется через drawImage(image, dx, dy) или drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) для вырезания части. Объект image - это HTMLImageElement, SVGImageElement, HTMLVideoElement или другой Canvas.


const img = new Image();
img.src = 'https://example.com/image.png';
img.onload = () => {
  ctx.drawImage(img, 0, 0);
};
  

Важно дождаться загрузки.

Можно использовать canvas.toDataURL() для экспорта или ctx.getImageData()/putImageData() для попиксельной обработки. Пример инвертирования:


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];     // R
  data[i+1] = 255 - data[i+1]; // G
  data[i+2] = 255 - data[i+2]; // B
}
ctx.putImageData(imageData, 0, 0);
  

Также возможна отрисовка кадров видео: каждые 10 мс вызывайте drawImage(video, 0, 0).

Ошибка CORS: если изображение загружено с другого домена и сервер не отдаёт заголовок Access-Control-Allow-Origin, то toDataURL() или getImageData() вызовут исключение безопасности. Решение: использовать прокси или изображения со своего домена. Проблема: если drawImage используется до загрузки изображения, ничего не нарисуется - всегда проверяйте onload или img.complete.

Как создать анимацию?

Лучший способ - использовать requestAnimationFrame(callback). Он синхронизирует отрисовку с обновлением экрана.


let x = 0;
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(x, 50, 50, 50);
  x += 2;
  requestAnimationFrame(animate);
}
animate();
  

В каждом кадре очищаем холст и рисуем новое положение.

Можно использовать setInterval или setTimeout, но они не привязаны к частоте обновления монитора, что может вызывать торможение или лишнюю нагрузку. requestAnimationFrame автоматически приостанавливается, когда вкладка неактивна.


// Менее эффективно:
setInterval(() => {
  // отрисовка
}, 16); // ~60fps
  

Для сложных анимаций можно управлять временем через performance.now().

Ошибка: забыли вызвать clearRect - предыдущий кадр остаётся, анимация превращается в размазывание. Проблема: при изменении размеров окна холст очищается, и контекст сбрасывает состояние - сохраняйте настройки перед началом анимации или восстанавливайте после каждой смены.

Как обрабатывать события мыши на холсте?

События добавляются на элемент canvas. Координаты мыши относительно холста получаем через event.offsetX и event.offsetY.


canvas.addEventListener('mousemove', (e) => {
  const x = e.offsetX;
  const y = e.offsetY;
  // рисуем, например, точку
  ctx.fillRect(x, y, 5, 5);
});
  

Это даёт плавное рисование каракулями.

Если холст имеет CSS-трансформацию или размеры не совпадают с атрибутами, координаты нужно корректировать. Используйте getBoundingClientRect() для расчёта:


canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const x = (e.clientX - rect.left) * (canvas.width / rect.width);
  const y = (e.clientY - rect.top) * (canvas.height / rect.height);
  // теперь x и y в координатах canvas
});
  

Также можно обрабатывать касания для мобильных устройств через touchstart, touchmove.

Типичная ошибка: offsetX и offsetY не поддерживаются в старых браузерах - используйте pageX - canvas.offsetLeft с учётом скролла. Проблема: при CSS-размерах, отличных от атрибутов (width: 100%), координаты в пикселях CSS не совпадают с пикселями холста - нужно масштабировать, как показано в варианте.

Дополнительные примеры

1. Интерактивное рисование линий (белая доска)

Реализуем инструмент для рисования произвольных линий с помощью событий мыши.

Пример

<canvas id='draw' width='600' height='400' style='border:1px solid black'></canvas>
<script>
const canvas = document.getElementById('draw');
const ctx = canvas.getContext('2d');
let drawing = false;

canvas.addEventListener('mousedown', (e) => {
  drawing = true;
  ctx.beginPath();
  ctx.moveTo(e.offsetX, e.offsetY);
});

canvas.addEventListener('mousemove', (e) => {
  if (!drawing) return;
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.strokeStyle = 'blue';
  ctx.lineWidth = 3;
  ctx.stroke();
});

canvas.addEventListener('mouseup', () => drawing = false);
canvas.addEventListener('mouseleave', () => drawing = false);
</script>
Результат: при зажатой кнопке мыши на холсте рисуется синяя линия. При отпускании рисование прекращается.

2. Анимация системы частиц

Создадим множество маленьких шариков, движущихся по случайным траекториям.

Пример

<canvas id='particles' width='500' height='500'></canvas>
<script>
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
const particles = [];
for (let i = 0; i < 100; i++) {
  particles.push({
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    vx: (Math.random() - 0.5) * 2,
    vy: (Math.random() - 0.5) * 2,
    radius: Math.random() * 5 + 2,
    color: `hsl(${Math.random() * 360}, 80%, 60%)`
  });
}

function animateParticles() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;
    // отражение от границ
    if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
    if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
    ctx.fillStyle = p.color;
    ctx.fill();
  });
  requestAnimationFrame(animateParticles);
}
animateParticles();
</script>
Результат: 100 цветных частиц движутся внутри холста, отскакивая от краёв.

3. Обработка пикселей: сепия и размытие

Загружаем изображение и применяем эффект сепии через манипуляции с ImageData.

Пример

<canvas id='sepiaCanvas' width='400' height='300'></canvas>
<script>
const canvas = document.getElementById('sepiaCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://picsum.photos/400/300';
img.onload = () => {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i+1];
    const b = data[i+2];
    data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189));
    data[i+1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168));
    data[i+2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131));
  }
  ctx.putImageData(imageData, 0, 0);
};
</script>
Результат: после загрузки изображения оно отображается в тонах сепии (коричневатый оттенок).

Canvas в JavaScript - comments

En
Canvas js (javascript)