Canvas JS: создание графики и обработка событий
Основные возможности 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>
Результат: после загрузки изображения оно отображается в тонах сепии (коричневатый оттенок).