HTML5 Canvas - анимация

139

Нарисовать одну картинку может казаться достаточно обескураживающей задачей. Поэтому не удивительно, что даже закаленные веб-разработчики рассматривают с определенным трепетом идею рисования нескольких десятков изображений каждую секунду. Основная трудность с анимацией состоит в необходимости обновлять отображение содержимого холста достаточно быстро, чтобы оно выглядело натурально движущимся или изменяющимся.

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

Анимацию также можно использовать для привлечения внимания к изменениям содержимого, например, постепенно вводить новый вид или создавать графики и диаграммы, "вырастающие" в требуемую позицию. Такие способы использования анимации являются мощным средством для придания глянца веб-приложениям.

Простая анимация

Сделать анимацию из элемента на холсте HTML5 достаточно просто. Для этого устанавливается таймер, который постоянно вызывает элемент, обычно 30 или 40 раз в секунду. При каждом вызове код полностью обновляет содержимое всего холста. Если код написан правильно, постоянно сменяющиеся кадры сольются в плавную, реалистичную анимацию.

JavaScript предоставляет два способа для управления этим повторяющимся обновлением содержимого холста:

Функция setTimeout()

Эта функция дает указание браузеру подождать несколько миллисекунд, а потом исполнить фрагмент кода, в данном случае код для обновления содержимого холста. По окончании исполнения кода функция setTimeout() выполняется опять, вновь вызывая код обновления холста, и так до тех пор, пока анимацию не нужно завершить.

Функция setInterval()

Эта функция дает указание браузеру исполнять фрагмент кода через регулярный интервал времени, например каждые 20 мс. Эффект от этой функции, по большому счету, такой же, как и от функции setTimeout(), но функцию setInterval() нужно вызывать только один раз. Чтобы остановить повторяющийся вызов кода браузером, исполняется функция clearInterval().

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

При вызове функции setTimeout() передаются два параметра: название функции, которою нужно выполнить, и период времени ожидания перед исполнением этой функции. Время ожидания указывается в миллисекундах (т.е. тысячных секунды), таким образом, 20 мс (обычная задержка при анимации) равняется 0,02 с. Далее показан пример такого кода анимации с использованием функции setTimeout():

var canvas;
var context;

window.onload = function() {
	   // Определение контекста рисования
	   canvas = document.getElementById("drawingCanvas");
	   context = canvas.getContext("2d");  
		 
	   // Обновляем холст через 0.02 секунды
	   setTimeout("drawFrame()", 20);
}

Этот вызов функции setTimeout() является ключевой частью любой задачи анимации. Допустим, что мы хотим создать анимацию квадрата, падающего сверху вниз страницы. Для этого нам нужно отслеживать позицию квадрата, используя две глобальные переменные. После нам надо просто изменять позицию при каждом исполнении функции drawFrame(), а потом перерисовывать квадрат в новой позиции:

// Устанавливаем начальную позицию квадрата
var squarePosition_x = 10;
var squarePosition_y = 0;

function drawFrame() {
	// Очистить холст
	context.clearRect(0, 0, canvas.width, canvas.height);
	
	// Вызываем метод beginPath(), чтобы убедиться,
	// что мы не рисуем часть уже нарисованного содержимого холста
	context.beginPath();
	
	// Рисуем квадрат размером 10x10 пикселов в текущей позиции
	context.rect(squarePosition_x, squarePosition_y, 10, 10);
	context.lineStyle = "#109bfc";
	context.lineWidth = 1;
	context.stroke();
	
	// Перемещаем квадрат вниз на 1 пиксел (где он будет 
	// прорисован в следующем кадре)
	squarePosition_y += 1;
	
	// Рисуем следующий кадр через 20 миллисекунд
	setTimeout("drawFrame()", 20);
}

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

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

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

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

Анимация Canvas

Эта программа анимации позволяет добавлять в холст неограниченное количество мячиков. Программа предоставляет опцию выбора размера мячика (радиус по умолчанию — 15 пикселов) и соединения мячиков линиями, как показано справа на рисунке. Каждый добавленный в холст мячик двигается независимо от других, падая с ускорением, пока не столкнется с нижним краем холста, потом отскакивает от него и начинает двигаться в другом направлении.

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

Добавьте базовую разметку для примера:

<div class="CanvasContainer">
    <canvas id="drawingCanvas" width="500" height="340"></canvas>
</div>
<div>
    <button onclick="addBall()">Добавить мячик</button>
    <button onclick="clearBalls()">Очистить холст</button>
</div>
<div>
    Размер мячика:<input id="ballSize" type="number" min="0" max="50" value="15">
    <input id="connectedBalls" type="checkbox">Добавить соединяющие линии<br>
</div>

Для управления всеми этими шариками мы воспользуемся пользовательским объектом. В данном случае нам нужно отслеживать массив объектов Ball и кроме позиции (представляемой свойствами x и y) для каждого мячика нужно еще отслеживать и скорость (представляемую свойствами dx и dy):

// Тип данных, представляющий отдельный мячик
function Ball(x, y, dx, dy, radius) {
    this.x = x;
    this.y = y;
    this.dx = dx;
    this.dy = dy;
    this.radius = radius;
    this.strokeColor = "black";
    this.fillColor = "red";
}

// Массив, содержащий информацию обо всех мячиках на холсте
var balls = [];

В математике выражение dx обозначает скорость изменения абсциссы, а dy — скорость изменения ординаты. Поэтому по мере падения мячика значение x для каждого кадра увеличивается на величину dx, а значение y — на величину dy.

При нажатии кнопки "Добавить мячик" простой код создает новый объект Ball и сохраняет его в массиве balls:

function addBall() {
    // Устанавливаем размер мячика
    var radius = parseFloat(document.getElementById("ballSize").value);

    // Создаем новый мячик
    var ball = new Ball(50,50,1,1,radius);

    // Сохраняем его в массиве
    balls.push(ball);
}

Кроме очистки холста, кнопка "Очистить холст" также очищает массив balls:

function clearBalls() {
  // Удаляем все мячики
  balls = [];
}

Но ни функция addBall(), ни функция clearBalls() в действительности не только ничего не рисуют, но даже не вызывают функцию для рисования. Вместо этого код страницы устроен таким образом, чтобы вызывать функцию drawFrame(), которая прорисовывает холст каждые 20 мс:

window.onload = function() {
	   // Определение контекста рисования
	   canvas = document.getElementById("drawingCanvas");
	   context = canvas.getContext("2d");
		 
	   // Обновляем холст через 0.02 секунды
	   setTimeout("drawFrame()", 20);
}

Функция drawFrame() является ключевой частью этого примера. Она не только рисует мячики на холсте, но также и вычисляет их текущую позицию и скорость. В функции drawFrame() выполняется несколько разных вычислений, чтобы более реалистично эмулировать движение мячиков, например, ускорение мячиков при падении и замедление, когда они отскакивают от препятствий. Полный код функции выглядит так:

function drawFrame() {
    // Очистить холст
    context.clearRect(0, 0, canvas.width, canvas.height);

    // Вызываем метод beginPath(), чтобы убедиться,
    // что мы не рисуем часть уже нарисованного содержимого холста
    context.beginPath();

    // Перебираем все мячики
    for(var i=0; i<balls.length; i++) {
        // Перемещаем каждый мячик в его новую позицию
        var ball = balls[i];
        ball.x += ball.dx;
        ball.y += ball.dy;

        // Добавляем эффект "гравитации", который ускоряет падение мячика
        if ((ball.y) < canvas.height) ball.dy += 0.22;

        // Добавляем эффект "трения", который замедляет движение мячика
        ball.dx = ball.dx * 0.998;

        // Если мячик натолкнулся на край холста, отбиваем его
        if ((ball.x + ball.radius > canvas.width) || (ball.x - ball.radius < 0)) {
            ball.dx = -ball.dx;
        }

        // Если мячик упал вниз, отбиваем его, но слегка уменьшаем скорость
        if ((ball.y + ball.radius > canvas.height) || (ball.y - ball.radius < 0)) { 
            ball.dy = -ball.dy*0.96; 
        }

        // Проверяем, хочет ли пользователь соединительные линии
        if (!document.getElementById("connectedBalls").checked) {
            context.beginPath();
            context.fillStyle = ball.fillColor;
        }
        else {
            context.fillStyle = "white";
        }

        // Рисуем мячик
        context.arc(ball.x, ball.y, ball.radius, 0, Math.PI*2);
        context.lineWidth = 1;
        context.fill();
        context.stroke(); 
    }

    // Рисуем следующий кадр через 20 миллисекунд
    setTimeout("drawFrame()", 20);
}

Хотя весь этот объем кода может вызывать легкий страх, общий подход остался прежним. Код исполняет следующие операции:

  1. Очищает холст.

  2. Перебирает в цикле мячики в массиве.

  3. Корректирует позицию и скорость каждого мячика.

  4. Рисует каждый мячик на холсте.

  5. Устанавливает время ожидания (функция setTimeout) для вызова метода drawFrame() опять 20 мс.

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

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

window.onload = function() {
	   ... 
	   
	   canvas.onmousedown = canvasClick; 
       
       ...
}

function canvasClick(e) {
  // Координаты щелчка мышью
  var clickX = e.pageX - canvas.offsetLeft;
  var clickY = e.pageY - canvas.offsetTop;

  for(var i in balls)
  {
    var ball = balls[i];
	
	// Проверка попадания
    if ((clickX > (ball.x-ball.radius)) && (clickX < (ball.x+ball.radius)))
    {
      if ((clickY > (ball.y-ball.radius)) && (clickY < (ball.y+ball.radius)))
      {
        // Изменить скорость мячика
        ball.dx -= 2;
        ball.dy -= 3;
        return;
      }
    }
  }
}

Довольно впечатляющую версию этого примера, можно исследовать на странице HTML5 Canvas Google Bouncing Balls. Здесь наведение курсора мыши на мячики разбрасывает их в разные стороны (как именно, зависит от того, каким образом наводится на них курсор), а если отвести курсор, мячики собираются в слово "Google".

Производительность анимации

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

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

Но производительность холста может быть проблемой в случае маломощных мобильных устройств, таких как iPhone или устройства с операционной системой Android. Результаты тестов показывают, что анимация, которая может выполняться на настольном компьютере со скоростью 60 кадр/с (кадров в секунду), будет исполняться на среднем смартфоне рывками, с максимальной скоростью 10 кадр/с.

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

Анимация на холсте для ленивых

Мне действительно нужно выполнять все вычисления самому?

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

А если требуется анимировать одновременно несколько предметов разными способами, объем и сложность необходимых для этого вычислений могут очень быстро выйти из под контроля. Поэтому намного легче жизнь разработчиков, использующих подключаемый модуль, такой как Flash или Silverlight. Обе технологии имеют встроенную систему анимации, которая позволяет разработчикам давать команды наподобие "переместить эту фигуру из этой точки в ту за 45 секунд" или, еще лучше, "переместить эту фигуру от верхнего края окна к нижнему, применяя эффект ускорения, вследствие которого фигура слегка отскакивает, достигнув нижнего края".

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

Пройди тесты
Лучший чат для C# программистов