Учебник по JavaScript: Начните работу с анимацией Canvas


Статья написана Майклом Кареном, автором образовательного курса «Разработка игр на JavaScript: Создание Тетриса».

Я люблю играть в игры. И я также люблю кодировать. И вот однажды я подумал, почему бы не использовать эти навыки кодирования для создания игры? Но это звучит сложно. Как вообще можно начать?

С маленьких шагов.

В этой статье вы научитесь рисовать и анимировать объекты с помощью HTML5 Canvas и JavaScript, а затем мы оптимизируем их для производительности.

«Анимация — это не искусство рисунков, которые движутся, а искусство движений, которые рисуются». — Норман Макларен

Вот что мы рассмотрим сегодня:

  • История Canvas
  • Что такое элемент canvas?
  • Что такое контекст холста?
  • Рисование объекта
  • Анимация объекта
  • Анимация нескольких объектов
  • Оптимизация анимации
  • Завершение

История Canvas

Компания Apple представила Canvas в 2004 году для работы с приложениями и браузером Safari. Несколько лет спустя он был стандартизирован WHATWG. Он обеспечивает более тонкий контроль над рендерингом, но за это приходится расплачиваться необходимостью управлять каждой деталью вручную. Другими словами, он может работать со многими объектами, но нам нужно подробно все кодировать.

Холст имеет 2D контекст рисования, используемый для рисования фигур, текста, изображений и других объектов. Сначала мы выбираем цвет и кисть, а затем рисуем. Мы можем менять кисть и цвет перед каждым новым рисунком или продолжать рисовать тем, что есть.

В Canvas используется немедленный рендеринг: Когда мы рисуем, рисунок сразу же отображается на экране. Но это система «забей и забудь». После того как мы что-то нарисовали, холст забывает об объекте и знает его только как пиксели. Таким образом, нет объекта, который мы могли бы переместить. Вместо этого мы должны нарисовать его снова.

Создание анимации на Canvas похоже на показ фильма. В каждом кадре нужно немного перемещать объекты, чтобы анимировать их.

Что такое элемент canvas?

Элемент HTML <canvas> представляет собой пустой контейнер, на котором мы можем рисовать графику. Мы можем рисовать на нем фигуры и линии с помощью API Canvas, который позволяет рисовать графику с помощью JavaScript.

Холст — это прямоугольная область на HTML-странице, которая по умолчанию не имеет границ и содержимого. По умолчанию размер холста составляет 300 пикселей × 150 пикселей (ширина × высота). Однако пользовательские размеры могут быть определены с помощью свойств HTML height и width:

<canvas id="canvas" width="600" height="300"></canvas>
Вход в полноэкранный режим Выход из полноэкранного режима

Укажите атрибут id, чтобы иметь возможность ссылаться на него из сценария. Чтобы добавить рамку, используйте атрибут style или используйте CSS с атрибутом class:

<canvas id="canvas" width="600" height="300" style="border: 2px solid"></canvas>
<button onclick="animate()">Play</button>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, когда мы добавили границу, мы видим размер нашего пустого холста на экране.
У нас также есть кнопка с событием onclick для запуска нашей функции animate() при нажатии на нее.

Мы можем разместить наш код JavaScript в элементах <script>, которые мы помещаем в документ <body> после элемента <canvas>:

<script type="text/javascript" src="canvas.js"></script>
Вход в полноэкранный режим Выход из полноэкранного режима

Мы получаем ссылку на элемент HTML <canvas> в DOM (Document Object Model) с помощью метода getElementById():

const canvas = document.getElementById('canvas');
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть элемент canvas, но мы не можем рисовать непосредственно на нем. Вместо этого у холста есть контексты рендеринга, которые мы можем использовать.

Что такое контекст холста?

Холст имеет контекст 2D рисования, используемый для рисования фигур, текста, изображений и других объектов. Сначала мы выбираем цвет и кисть, а затем рисуем. Мы можем менять кисть и цвет перед каждым новым рисунком или продолжать рисовать тем, что есть.

Метод HTMLCanvasElement.getContext() возвращает контекст рисования, в котором мы отрисовываем графику. Указав в качестве аргумента '2d', мы получим контекст 2D-рендеринга холста:

const ctx = canvas.getContext('2d');
Вход в полноэкранный режим Выйти из полноэкранного режима

Существуют и другие доступные контексты, например webgl для трехмерного контекста рендеринга, которые выходят за рамки этой статьи.

Контекст CanvasRenderingContext2D имеет множество методов для рисования линий и фигур на холсте. Для задания цвета линии используется strokeStyle, а для задания толщины — lineWidth:

ctx.strokeStyle = 'black';
ctx.lineWidth = 5;
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы готовы нарисовать нашу первую линию на холсте. Но прежде чем мы это сделаем, нам нужно понять, как мы указываем холсту, где рисовать. Холст HTML представляет собой двухмерную сетку. Верхний левый угол холста имеет координаты (0, 0).

   X →
Y [(0,0), (1,0), (2,0), (3,0), (4,0), (5,0)]
↓ [(0,1), (1,1), (2,1), (3,1), (4,1), (5,1)]
  [(0,2), (1,2), (2,2), (3,2), (4,2), (5,2)]
Вход в полноэкранный режим Выход из полноэкранного режима

Поэтому, когда мы говорим, что хотим moveTo(4, 1) на холсте, это означает, что мы начинаем с левого верхнего угла (0,0) и перемещаемся на четыре колонки вправо и на одну строку вниз.

Рисуем свой объект 🔵

После того как у нас есть контекст холста, мы можем рисовать на нем, используя API контекста холста. Метод lineTo() добавляет прямую линию к текущему подпути, соединяя его последнюю точку с указанными координатами (x, y).

Как и другие методы, изменяющие текущий путь, этот метод ничего непосредственно не отрисовывает. Чтобы нарисовать путь на холсте, можно использовать методы fill() или stroke().

ctx.beginPath();      // Start a new path
ctx.moveTo(100, 50);  // Move the pen to x=100, y=50.
ctx.lineTo(300, 150); // Draw a line to x=300, y=150.
ctx.stroke();         // Render the path
Вход в полноэкранный режим Выход из полноэкранного режима

Мы можем использовать fillRect(), чтобы нарисовать заполненный прямоугольник. Установка fillStyle определяет цвет, используемый при заливке нарисованных фигур:

ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 30, 30); // (x, y, width, height);
Вход в полноэкранный режим Выход из полноэкранного режима

Рисуется заполненный прямоугольник синего цвета:

Анимация вашего объекта 🎥

Теперь давайте посмотрим, сможем ли мы заставить наш блок двигаться на холсте. Для начала установим size квадрата на 30. Затем мы можем перемещать значение x вправо с шагом size и рисовать объект снова и снова. Следующий код перемещает блок вправо, пока он не достигнет конца холста:

const size = 30;
ctx.fillStyle = 'blue';

for (let x = 0; x < canvas.width; x += size) {
  ctx.fillRect(x, 50, size, size);
}
Войти в полноэкранный режим Выход из полноэкранного режима

Хорошо, мы смогли нарисовать квадрат так, как хотели. Но у нас есть две проблемы:

  1. Мы не убираем за собой.
  2. Мы слишком быстро видим анимацию.

Нам нужно убрать старый блок. Что мы можем сделать, так это стереть пиксели в прямоугольной области с помощью clearRect(). Используя ширину и высоту холста, мы можем очищать его между красками.

for (let x = 0; x < canvas.width; x += size) {
  ctx.clearRect(0, 0, canvas.width, canvas.height); // Clean up
  ctx.fillRect(x, 50, size, size);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Отлично! Мы устранили первую проблему. Теперь давайте попробуем замедлить процесс рисования, чтобы мы могли увидеть анимацию.

Возможно, вы знакомы с setInterval(function, delay). Она начинает многократно выполнять указанную функцию каждые delay миллисекунд. Я установил интервал в 200 мс, что означает, что код выполняется пять раз в секунду.

let x = 0;
const id = setInterval(() => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);      
  ctx.fillRect(x, 50, size, size);
  x += size;

  if (x >= canvas.width) {
    clearInterval(id);
  }
}, 200);    
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы остановить таймер, созданный с помощью setInterval(), нужно вызвать clearInterval() и указать ему идентификатор интервала, который нужно отменить. Используемый идентификатор — это тот, который возвращает setInterval(), и именно поэтому нам нужно его сохранить.

Теперь мы видим, что если мы нажмем на кнопку, то получим квадрат, который перемещается слева направо. Но если мы нажмем кнопку play несколько раз, то увидим, что существует проблема анимации нескольких квадратов одновременно.

Каждый квадрат получает свой интервал, который очищает доску и закрашивает квадрат.

И так по всему периметру! Давайте посмотрим, как мы можем это исправить.

Анимация нескольких объектов

Чтобы иметь возможность запускать анимацию для нескольких блоков, нам нужно переосмыслить логику. На данный момент каждый блок получает свой метод анимации с помощью setInterval(). Вместо этого мы должны управлять движущимися объектами, прежде чем отправлять их на отрисовку, причем все сразу.

Мы можем добавить переменную started, чтобы запускать setInterval() только при первом нажатии кнопки. Каждый раз, когда мы нажимаем кнопку play, мы добавляем новое значение 0 в массив squares. Этого достаточно для этой простой анимации, но для чего-то более сложного мы можем создать объект Square с координатами и возможными другими свойствами, например цветом.

let squares = [];
let started = false;

function play() {
  // Add 0 as x value for object to start from the left.
  squares.push(0);

  if (!started) {
      started = true;
      setInterval(() => {
        tick();
      }, 200)
  }
}

function tick() {
  // Clear canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Paint objects
  squares.forEach(x => ctx.fillRect(x, 50, size, size));

  squares = squares.map(x => x += size) // move x to right
      .filter(x => x < canvas.width);  // remove when at end
}
Вход в полноэкранный режим Выход из полноэкранного режима

Функция tick() очищает экран и закрашивает все объекты в массиве каждые 200 мс. А благодаря тому, что интервал всего один, мы избегаем мерцания, которое было раньше. И теперь мы получаем более качественную анимацию:

То, что мы сделали здесь, — это первый шаг к созданию игрового цикла. Этот цикл — сердце любой игры. Это управляемый бесконечный цикл, который поддерживает работу вашей игры; это место, где все ваши маленькие части обновляются и рисуются на экране.

Оптимизируйте анимацию 🏃.

Другой вариант анимации — использовать requestAnimationFrame(). Она сообщает браузеру, что вы хотите выполнить анимацию, и просит браузер вызвать функцию для обновления анимации перед следующей перерисовкой. Другими словами, мы говорим браузеру: «В следующий раз, когда ты будешь рисовать на экране, также запусти эту функцию, потому что я тоже хочу что-нибудь нарисовать».

Способ анимирования с помощью requestAnimationFrame() заключается в создании функции, которая рисует кадр, а затем планирует свой повторный вызов. Таким образом, мы получаем асинхронный цикл, который выполняется, когда мы рисуем на холсте. Мы вызываем метод animate снова и снова, пока не решим остановиться. Теперь вместо этого мы вызываем функцию animate():

function play() {
  // Add 0 as x value for object to start from the left.
  squares.push(0);

  if (!started) {
      animate();
  }
}

function animate() {
  tick();
  requestAnimationFrame(animate);  
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Если мы попробуем это, то заметим, что мы можем видеть анимацию, чего не было в случае с setInterval(), несмотря на то, что это супербыстро. Количество обратных вызовов обычно составляет 60 раз в секунду.

Метод requestAnimationFrame() возвращает id, который мы используем для отмены запланированного кадра анимации. Чтобы отменить запланированный кадр анимации, можно использовать метод cancelAnimationFrame(id).

Для замедления анимации нам нужен таймер, который будет проверять прошедшее время с момента последнего вызова функции tick(). Чтобы помочь нам, функции обратного вызова передается аргумент, DOMHighResTimeStamp, указывающий момент времени, когда requestAnimationFrame() начинает выполнять функции обратного вызова.

let start = 0;

function animate(timestamp) {    
  const elapsed  = timestamp - start;
  if (elapsed > 200) {
    start = timestamp;
    tick();
  }
  requestAnimationFrame(animate);  
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Таким образом, мы имеем ту же функциональность, что и ранее с помощью setInterval().

Итак, в заключение, почему мы должны использовать requestAnimationFrame() вместо setInterval()?

  • Она позволяет оптимизировать браузеры.
  • Она обрабатывает частоту кадров.
  • Анимация запускается только тогда, когда она видна.

Подведение итогов

В этой статье мы создали HTML5 Canvas и использовали JavaScript и его контекст 2D рендеринга для рисования на холсте. Мы познакомились с некоторыми методами, доступными в контексте холста, и использовали их для визуализации различных фигур.

Наконец, мы смогли анимировать несколько объектов на холсте. Мы узнали, как использовать setInterval() для создания цикла анимации, который управляет и рисует объекты на экране.
Мы также узнали, как оптимизировать анимацию с помощью requestAnimationFrame().

Теперь, когда вы изучили некоторые базовые анимации, вы сделали первые шаги в разработке игр. Следующим шагом для вас будет проект для начинающих разработчиков игр, чтобы узнать, как:

  • разрабатывать игровые циклы
  • Реализовывать интерактивные элементы управления с помощью addEventListener
  • Определять столкновения объектов
  • Отслеживать очки

Чтобы помочь вам, я создал книгу «Разработка игр на JavaScript: Создание Тетриса. Этот курс научит вас применять свои навыки работы с JavaScript для создания своей первой игры. Создав «Тетрис», вы изучите основы разработки игр и создадите свое профессиональное портфолио, на которое сможете ссылаться на следующем собеседовании.

Счастливого обучения!

Продолжить чтение о JavaScript и разработке игр на Educative

  • Самоучитель JavaScript: Создайте Тетрис с помощью современного JavaScript
  • Самоучитель JavaScript по игре «Змейка»: создайте простую интерактивную игру
  • Введение в полнофункциональную разработку на JavaScript

Начните обсуждение

Какие игры вы надеетесь разрабатывать? Была ли эта статья полезной? Сообщите нам об этом в комментариях ниже!

Оцените статью
devanswers.ru
Добавить комментарий