HTML5 Canvas Draw and Animate Tutorial

HTML5 Canvas. Рисуй и анимируй!

HTML5 уже давно на дворе, а мы в этом блоге ещё ни разу ничего на холсте (canvas) не нарисовали :) И чтобы исправить эту несправедливость, я сегодня на достаточно простом примере расскажу и покажу, как рисовать и анимировать с помощью этого интересного элемента web-страницы. Эти знания пригодятся вам, например, при разработке HTML5-игр. Сразу скажу, что сегодня мы не будем использовать никаких библиотек по работе с canvas’ом, предоставляющих более удобный интерфейс взаимодействия с элементом, чтобы не затачиваться на какой-то конкретный инструмент, а иметь общее понимание принципа работы с canvas. В результате мы с вами сегодня сделаем анимированные круги, хаотично перемещающиеся по области холста, и добавим немного интерактивности. Ссылки на демо и исходники вы найдёте в конце статьи. Что ж, давайте начнём с общих определений.

Анимация (от фр. animation — оживление, одушевление) — компьютерная имитация движения с помощью изменения координат и форм объектов, а также их перерисовка с определённым интервалом.

Canvas (в переводе с англ. — холст) — это элемент из стандарта HTML5, который даёт разработчикам возможность создания динамической графики при помощи JavaScript. С первых моментов его существования было понятно, что Flash в скором времени будет чахнуть и вымирать, уступая позиции амбициозному и перспективному новичку, решающего во многом похожие задачи. Собственно говоря, мы всё это уже увидели. Поддержка Flash’а в современных браузерах сходит на нет. В то же время, поддержка canvas’а в современных браузерах вполне нас радует. Поэтому можем воплощать свои идеи на холсте, как истинные IT-хужожники :) В тело этого тэга можно добавить текст, отображаемый в браузерах, которые его не поддерживают. Но сам по себе canvas — это обычный DOM-элемент, область для рисования, ничего сама по себе особого не умеющая.

И тут самое время рассказать про canvas-контекст. У DOM-элемента canvas’а есть метод getContext, получающий на вход тип контекста, с которым мы хотим работать. Так вот, контекст — это вторая часть технологии canvas, представляющая из себя набор операций и свойств, позволяющий нам создавать растровую графику на холсте. Мы будем пользоваться двумерным (2d) контекстом (CanvasRenderingContext2D), который позволит нам работать с 2D-свойствами и методами для рисования и манипулирования медиа-контентом на холсте. Единица измерения в операциях контекста — пиксель. Начало координат находится в левом верхнем углу холста. У каждого отображаемого элемента должны быть координаты x и y.

А теперь мы изучим ещё одну функцию, которую будем использовать для анимации. requestAnimationFrame сообщает браузеру, что планируется анимация и просит его поставить в очередь перерисовку в следующем кадре. Параметром requestAnimationFrame получает функцию, вызываемую прямо перед перерисовкой. Каждый вызов функции должен самостоятельно вызывать requestAnimationFrame. В противном случае, циклический вызов функции вместе с анимацией приостановятся. Каждый раз, когда вам требуется произвести перерисовку холста, вам нужно вызывать этот метод. Обновление может происходить до 60 раз в секунду (60 FPS, если частота вашего экрана 60 Гц), но браузер своими механизмами оптимизации эту частоту для неактивных или невидимых вкладок может снизить. Во всех браузерах, поддерживающих этот метод, callback на вход получит аргумент со значением запланированного времени анимации, с помощью которого вы можете расчитывать позиции отображаемых объектов.

Теперь, ознакомившись с теоретической частью, можем приступать к практике!

Для начала давайте напишем содержимое главного файла разметки:

<!doctype html>
<html>
<head>
    <title>Canvas Example</title>
</head>
<body>
<canvas id='canvas' width=500 height=500>
</body>
</html>

В теле страницы мы добавили элемент холста и идентификатором canvas с определёнными размерами — 500 на 500 пикселей.

Теперь приступим непосредственно к написанию кода, который будет отображать графику на холсте.

var canvas = document.getElementById('canvas');
var canvasLeftOffset = canvas.offsetLeft;
var canvasTopOffset = canvas.offsetTop;
var ctx = canvas.getContext('2d');
var circles = [];
var mouse = {x: null, y: null, EFFECT_RADIUS: 100};

Объявляем переменные, в которые сохраняем ссылку на DOM-элемент холста, его левый и верхний отступы для дальнейших расчётов на страницы и получаем 2d-контекст. Также объявляем массив наших кружков, которые будут блуждать по нашему холсту и создаём объект, хранящий информацию о действиях пользователя мышью — координаты и переменную радиуса влияния мыши, значение которой мы увидим позже.

Теперь напишем функцию, вызываемую при перерисовке каждого нового кадра.

function draw() {
	ctx.clearRect(0, 0, canvas.width, canvas.height);
	circles.forEach(function (circle) {
		circle.move();
		circle.draw();
	});
	requestAnimationFrame(draw);
}
requestAnimationFrame(draw);

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

Реализуем JS-класс, который будет содержать львиную долю логики нашего примера. Держите самый большой листинг на сегодня:

function Circle(params) {
	var self = this;
	params = params || {};
	self.x = params.x || Math.round(Math.random() * canvas.width);
	self.y = params.y || Math.round(Math.random() * canvas.height);
	self.v = getRandomVelocity();
	self.initRadius = params.radius || 5 + Math.round(Math.random()) * 10;
	self.radius = self.initRadius;
	self.fillColor = params.fillColor || getRandomColor();
	self.strokeColor = params.strokeColor || getRandomColor();
	self.randomMotion = params.randomMotion || false;

	if (self.randomMotion) {
		cyclicChangeVelocityVector();
	}

	function cyclicChangeVelocityVector() {
		self.v = getRandomVelocity();
		setTimeout(cyclicChangeVelocityVector, 200);
	}
}

Circle.prototype.move = function () {
	this.x += this.v.x;
	this.y += this.v.y;

	var radiusDiff = 0;
	if (mouse.x !== null && mouse.y !== null) {
		var mouseCircleRadius = Math.sqrt(Math.pow(mouse.x - this.x, 2) + Math.pow(mouse.y - this.y, 2));
		radiusDiff = mouse.EFFECT_RADIUS - mouseCircleRadius;
	}
	this.radius = this.initRadius + Math.max(0.7 * radiusDiff, 0);

	if (this.x - this.radius < 0 || this.x + this.radius > canvas.width) {
		this.v.x = -this.v.x;
	}
	if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
		this.v.y = -this.v.y;
	}
};

Circle.prototype.draw = function () {
	ctx.beginPath();
	ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
	ctx.strokeStyle = this.strokeColor;
	ctx.lineWidth = 1;
	ctx.fillStyle = this.fillColor;
	ctx.fill();
	ctx.stroke();
};

В конструкторе класса мы определяем все необходимые свойства на основании пришедшего объекта параметров, устанавливая значение по умолчанию, если нужного свойства в объекте не нашли. Некоторые свойства мы заполняем случайными значениями нужного типа, чтобы добавить хаотичности и разнообразия движению наших фигур.

Помимо этого в конструкторе в зависимости от свойства randomMotion может запускаться циклическое изменение вектора скорости. т.е. такие фигуры каждые 200 мс будут менять направление и скорость своего движения.

В методе move происходит изменение позиции фигуры. Также здесь производится расчёт пересечения фигурой границы холста. При обнаружении такого пересечения значение скорости по одной из осей меняется на противоположное. А ещё в этой функции происходит расчёт радиуса окружности в зависимости от удалённости от курсора мыши, находящегося на холсте. Это сделано для того, чтобы добавить немного интерактивности нашему примеру.

А в методе draw производится непосредственная отрисовка фигуры в контексте, определённом для нашего класса глобально. Конечно, по-хорошему, от глобальной зависимости нужно избавиться, но для нашего примера это некритично.

Поскольку мы сегодня говорим о canvas’е, я подробнее опишу функцию отрисовки. Сначала мы при помощи метода beginPath начинаем новый контур, после чего методом arc формируем контур нашей окружности с заданным центром. При помощи свойств strokeStyle, lineWidth и fillStyle задаём цвет обводки контура, толщину обводки и цвет заливки соответственно. И в завершении методом fill заполняем окружность заливкой и обводим контур с помощью метода stroke.

Ниже приведён код наших утилитных функций, используемых для определения некоторых свойств наших фигур. Думаю, для людей, знакомых с основами JS, объяснения излишни.

function getRandomColor() {
	var colors = ['blue', 'orange', 'black', 'brown', 'yellow', 'magento', 'white', 'red'];
	return colors[Math.floor(Math.random() * colors.length)];
}

function getRandomMinusOrPlusUnit() {
	return Math.random() < 0.5 ? -1 : 1;
}

function getRandomVelocity() {
	return {
		x: getRandomMinusOrPlusUnit() * Math.round(Math.random() * 2),
		y: getRandomMinusOrPlusUnit() * Math.round(Math.random() * 2)
	};
}

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

canvas.addEventListener('click', function (event) {
	var x = event.pageX - canvasLeftOffset,
		y = event.pageY - canvasTopOffset;
	circles.push(new Circle({x: x, y: y, randomMotion: Math.random() > 0.5}));
});

canvas.addEventListener('mousemove', function (event) {
	mouse.x = event.pageX - canvasLeftOffset;
	mouse.y = event.pageY - canvasTopOffset;
});

canvas.addEventListener('mouseout', function () {
	mouse.x = null;
	mouse.y = null;
});

При клике по холсту мы вычисляем позицию клика и создаём на этом месте новый кружок. При движении курсора мыши по нашему canvas'у мы устанавливаем координаты объекта мыши в соответствующие значения, а при выводе курсора с элемента зануляем их.

Ну и последний штрих нашей сегодняшней работы - населяем наш холст первой сотней фигур, чтоб сделать наш холст оживлённее:

for (var i = 0; i < 100; i++) {
	circles.push(new Circle({randomMotion: Math.random() > 0.5}));
}

А теперь, после того, как вы дочитали до конца наше сегодняшнее занятие, держите обещанные ссылки на DEMO и ИСХОДНИКИ.

В итоге, мы получили множество шаров-"молекул", бороздящих простор нашей страницы. Броуновским движением это, конечно, не назвать, но всё равно выглядит весело :) На базе наших сегодняшних результатов можно сделать интересные эффекты, но для этого нужны доработки.

На этом наш сегодняшний урок закончен! Всё, как видите, просто! Буду рад увидеть ваши комментарии, вопросы и отзывы по написанному материалу. Надеюсь, что тема canvas'а для вас стала гораздо понятнее, чем это было до прочтения. Возможности 2D-контекста гораздо шире тех, что мы сегодня увидели. К сожалению, всё в одном уроке не объять, поэтому остальное при желании изучите уже самостоятельно. Либо в моих будущих выпусках!

На этом всё! До новых статей! :)