С помощью HTML5 элемента canvas и нескольких строк JS-кода несложно сделать спрайт-анимацию для игр или интерактивных приложений. Этим мы сегодня и займемся.

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

Что такое спрайт-анимация

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

See the Pen sprite-animation by FurryCat (@mohnatus-the-lessful) on CodePen.

Так как все кадры находятся в одной картинке (sprite sheet), нам потребуются возможности метода drawImage – он позволяет обрезать изображение, то есть выбирать нужный фрейм.

Спрайт

Спрайты на HTML5 холсте те же самые, что мы используем в CSS – это просто несколько разных картинок, объединенных в одном файле. Например, в этом спрайте 10 кадров. Ширина всего спрайта составляет 460 пикселей, соответственно, каждый фрейм занимает 460/10 – 46 пикселей.

Начинаем анимировать

Сначала загрузим спрайт с фреймами анимации монетки. Для этого создадим новый объект Image и установим нужное значение для его свойства src:

let coinImage = new Image();
coinImage.src = 'coin-sprite-animation.png';

Теперь создадим класс спрайтов. Его объектам потребуется 4 свойства:

  • контекст для рисования;
  • ширина изображения;
  • высота изображения;
  • само изображение с фреймами.
class Sprite {
    constructor(options) {
        this.ctx = options.ctx;

        this.image = options.image;

        this.width = options.width;
        this.height = options.height;
    }
}

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

<canvas id="canvas"></canvas>
let canvas = document.getElementById('canvas');
canvas.width = 100;
canvas.height = 100;

Создадим реальный объект спрайт-анимации монетки с нужными параметрами:

let sprite = new Sprite({
  ctx: canvas.getContext('2d'),
  image: coinImage,
  width: 1000,
  height: 100,
  numberOfFrames: 10,
  ticksPerFrame: 4,
})

Метод drawImage

В основе спрайт-анимации на холсте – метод drawImage. Он позволяет резать изображение как вздумается и отрисовывать нужный кусок. Этот метод принимает целую кучу параметров:

context.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
  • img – исходное изображение-спрайт;
  • sx – x-координата верхнего левого угла нужного фрейма на спрайте;
  • sy – y-координата верхнего левого угла нужного фрейма на спрайте;
  • sw – ширина фрейма;
  • sh – высота фрейма;
  • dx – x-координата точки на холсте, где начнется отрисовка фрейма (его верхний левый угол);
  • dy – y-координата точки на холсте, где начнется отрисовка фрейма;
  • dw – ширина отрисованного на холсте фрейма;
  • dh – высота отрисованного на холсте фрейма.

Таким образом, параметры, начинающиеся с s-, относятся к исходному изображению (source), а параметры, начинающиеся с d-, относятся к холсту (destination).

Метод контекста холста drawImage будет использоваться в методе render класса Sprite.

Render

Пора рисовать. Сначала просто выведем на холст первый фрейм, обрезав спрайт по нужным размерам:

class Sprite {
    constructor(options) {
        // ...
        this.render(); // рендерим спрайт после создания
    }

    render() {
        this.ctx.drawImage(
            this.image,
            0,
            0,
            this.width,
            this.height,
            0,
            0,
            this.width,
            this.height
        )
    }
}

Теперь посмотрим, что получилось.

Статичная картинка. Хм, но где анимация?

Update

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

  • frameIndex – индекс активного фрейма;
  • tickCount – количество обновлений, произошедших после первого вывода текущего фрейма;
  • ticksPerFrame – количество обновлений, которые должны произойти до смены фреймов.

Можно было бы обновлять frameIndex при каждом вызове метода update, но тогда нельзя будет регулировать скорость анимации. Поэтому будем отслеживать тики обновления. Например, если игра запущена со скоростью 60 кадров в секунду, а ticksPerFrame = 4, скорость анимации будет равна 15 кадров в секунду.

Каждый вызов метода update будет инкрементировать количество тиков обновления (tickCount). Если оно достигнет значения tickPerFrame, то будет сброшено, а индекс активного фрейма изменится.

class Sprite {
    constructor(options) {
        // ...

        this.frameIndex = 0;
        this.tickCount = 0;
        this.ticksPerFrame = options.ticksPerFrame || 0;
        
        this.update();
        this.render();
    }

    update() {
        this.tickCount++;

        if (this.tickCount > this.ticksPerFrame) {
            this.tickCount = 0;
            this.frameIndex++;
        }
    }
}

Добавим еще одно свойство для класса Sprite: numberOfFrames – количество фреймов в спрайте. Метод render теперь может сдвинуть границы, отрезающие активный фрейм, на основе значения frameIndex. Поправим параметры отрисовки для метода drawImage:

class Sprite {
    constructor(options) {
        // ...

        this.numberOfFrames = options.numberOfFrames || 1;

        this.update();
        this.render();
    }

    render() {
        this.ctx.drawImage(
            this.image,
            this.frameIndex * this.width / this.numberOfFrames,
            0,
            this.width / this.numberOfFrames,
            this.height,
            0,
            0,
            this.width / this.numberOfFrames,
            this.height
        )
    }
}

Что делать, если кадры спрайта кончились? Запустить ее с самого начала:

class Sprite {
    update() {
        this.tickCount++;

        if (this.tickCount > this.ticksPerFrame) {
            this.tickCount = 0;
            if (this.frameIndex < this.numberOfFrames - 1) {
                this.frameIndex++;
            } else {
                this.frameIndex = 0;
            }
        }
    }
}

RequestAnimationFrame

Метод requestAnimationFrame позволяет привязать перерисовку анимации к циклу обновления браузера. Его реализация в разных браузерах несколько различается, поэтому стоит пользоваться кроссбраузерным вариантом:

let requestAnimationFrame = window.requestAnimationFrame || 
                            window.mozRequestAnimationFrame ||
                            window.webkitRequestAnimationFrame || 
                            window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;

Создадим отдельный метод start для запуска анимации:

class Sprite {
    constructor(options) {
        // ...

        this.start();
    }

    start() {
        let loop = () => {
            this.update();
            this.render();

            window.requestAnimationFrame(loop);
        }

        window.requestAnimationFrame(loop);
    }
}

See the Pen sprite-animation-coin-1 by FurryCat (@mohnatus-the-lessful) on CodePen.

Уже очень близко к идеалу, осталась лишь одна маленькая деталь.

Очистка холста

Метод clearRect позволяет очистить область холста от предыдущего кадра. Он принимает 4 параметра:

context.clearRect(x, y, width, height)
  • x – x-координата верхнего левого угла очищаемого прямоугольника;
  • y – y-координата верхнего левого угла очищаемого прямоугольника;
  • width – ширина очищаемой области;
  • height – высота очищаемой области.

Теперь наша спрайт-анимация работает так, как нужно.

See the Pen sprite-animation-coin-2 by FurryCat (@mohnatus-the-lessful) on CodePen.

Полный код:

class Sprite {
    constructor(options) {
        this.ctx = options.ctx;

        this.image = options.image;

        this.frameIndex = 0;
        this.tickCount = 0;
        this.ticksPerFrame = options.ticksPerFrame || 0;
        this.numberOfFrames = options.numberOfFrames || 1;

        this.width = options.width;
        this.height = options.height;

        this.start();
    }

    update() {
        this.tickCount++;

        if (this.tickCount > this.ticksPerFrame) {
            this.tickCount = 0;
            if (this.frameIndex < this.numberOfFrames - 1) {
                this.frameIndex++;
            } else {
                this.frameIndex = 0;
            }
        }
    }

    render() {
      this.ctx.clearRect(0, 0, this.width / this.numberOfFrames, this.height);
        this.ctx.drawImage(
            this.image,
            this.frameIndex * this.width / this.numberOfFrames,
            0,
            this.width / this.numberOfFrames,
            this.height,
            0,
            0,
            this.width / this.numberOfFrames,
            this.height
        )
    }

    start() {
        let loop = () => {
            this.update();
            this.render();

            window.requestAnimationFrame(loop);
        }

        window.requestAnimationFrame(loop);
    }
}

let canvas = document.getElementById('canvas');
canvas.width = 100;
canvas.height = 100;

let coinImage = new Image();
coinImage.src = 'https://www.cat-in-web.ru/wp-content/uploads/coin-sprite-animation.png';

let sprite = new Sprite({
  ctx: canvas.getContext('2d'),
  image: coinImage,
  width: 1000,
  height: 100,
  numberOfFrames: 10,
  ticksPerFrame: 4,
})

1 комментарий

Оставить комментарий

*Доступные HTML-теги: a, abbr, blockquote, code, pre, del, i, em, strong, b, strike
*Не будет опубликован