Введение в spritesheet анимацию

12 октября 2011 в 12:00

Анимация спрайтов при помощи CSS, JS и Canvas

  • Разработка веб-сайтов ,
  • Canvas

Всем привет. Пару дней назад совершенно случайно наткнулся в залежах дисков на «Космические рейнджеры 2: Доминаторы». Устанавливать не стал, так как сейчас не хватает времени, чтобы как следует в неё погрузиться. А решил посмотреть, что на диске находится. Посмотрел «Фан-Арт» и там увидел программку для ковыряния ресурсов рейнджеров. Вот и решил посмотреть, из чего же сделаны наши доминаторы. Покликав немного, нашел файлики с анимацией в формате GAI. Начал любоваться той анимацией. Захотел их сохранить в «гифки», но не как не давала та программка сохранить анимацию? Можно либо сохранить текущий кадр, либо все файлы в PNG. Я решил сохранить все кадры, а их было - 150. Картинки все есть, а почему бы не сделать с ними ту же анимацию.

Спрайт

Для того чтобы порадовать себя такой анимацией взял в помощь CSS + JS. А что мне делать со 150-ю файлами изображений? Их вес не критичен, хоть и весят они в сумме больше мегабайта. Главные проблемы с их одновременной загрузкой и манипуляцией. Поэтому я решил «склеить» их в одну. Грузить только одну, да и при помощи CSS + JS придется только правильно позиционировать её.
Осталось выбрать метод «склейки». Я же программист, а мы все ленивые:), поэтому я сразу отбросил ручную склейку в графическом редакторе. Первым делом я бросился, как обычно, к PHP и библиотеке GD. Но она оставляет желать лучшего по работе с полупрозрачными PNG. Дальше я подумал, а чем же ещё можно «склеить» картинки? И остановился на том, что сейчас у всех на слуху, что считается сейчас модным - HTML5. За работу с графикой у него отвечает - Canvas, причем мне очень нравится эта «вкусняшка». Вот поэтому все же решил «клеить» на «холсте».
И так добавлю я ка этот тег в HTML:

Не поддерживается
В JS укажу маску для имени картинки, первую картинку и номер последней (мне не пришлось переименовывать картинки, так как они все идут по порядку). Это должно выглядеть примерно так:

Var first = "000"; //номерная часть имени первой картинки var last = 49; //номер последней картинки var num = 0; //итератор var maskFileName = ["2HULL_PEOPLE_P_A_", ".png"];//маска имени картинки var dir = "ship"; //директория, в которой лежат картинки

Теперь можно указать размеры спрайта который должен получиться, и получить его контекст:

Var canvas = document.getElementById("sprite"); //выберем наш канвас canvas.width = (last + 1) * 75; //задаем ширину, в зависимости от количества canvas.height = 75; var width = 0; //переменная, в которую будем записывать сдвиг var context = canvas.getContext("2d"); //получаем контекст

Для облегчения перевода из числа в строку (аналог str_pad из PHP) написал функцию-преобразователь с диким названием - zerofikator():

Function zerofikator(int, length) { //на вход получаем число и длину строки var prefix = ""; for (var i = num.toString().length; i < length; i++) { prefix += "0"; } return prefix + num; }

Function draw() { var img = document.createElement("img"); /* каждый раз при вызове этой функции мы создаем новый обьект-изображение */ img.onload = function () { //когда изображение загрузится рисуем им на канвасе context.drawImage(img, width, 0); width += 75; //при этом каждый раз сдвигаем на ширину изображения, чтобы рисовать справа от предыдущего рисунка, а не на нем if (zerofikator(num, first.length) != zerofikator(last, first.length)) { //проверяем достигли ли мы последнего рисунка num++; //увиличуем итератор draw(); //и запускаем функцию вновь } } img.src = dir + "/" + maskFileName + zerofikator(num, first.length) + maskFileName; //собираем имя файла картинки для загрузки } draw(); //вызываем функцию впервые

После запуска такой страницы мы увидим широкое покадровое изображение, которое, если мы сохраним - сможем назвать спрайтом. Кстати, сохраненный спрайт весит 615 KB, а 150 картинок 1 189 KB, хм, вот и ещё один плюс:).

Решил добавить преобразование сразу в файл по клику на канвас (решает проблему сохранения для некоторых браузеров):

Canvas.onclick = function () { window.location = context.canvas.toDataURL("image/png"); };

Анимация

Ну а теперь, можно приступить и к анимации.
В HTML добавляем пару «дивчиков», с которыми мы дальше будем работать:

Var styles = {}; styles.cursor = "pointer"; //чтобы видно было что это наш элемент, меняем курсор styles.width = "75px"; //размеры элемента styles.height = "75px"; var elementId = "gun"; // id элемента, в котором будет анимация var imgName = "canvas.png"; // имя файла спрайта

Function spriteAnimation(elementId, imgName, styles) { var img = document.createElement("img"); var offset = 0; img.onload = function () { //как только спрайт загружается var element = document.getElementById(elementId); element.style.cursor = styles.cursor; element.style.width = styles.width; element.style.height = styles.height; element.style.background = "url("" + imgName + "") " + offset + "px 50%"; //меняем стили для нашего элемента var i = 0; element.onmouseover = function() { //вешаем обработчик на наведение мыши interval = setInterval(function() { //запускаем интервал if (offset < img.width) { //для смены позиции изображения i++; // если дошли до конца спрайта } else { i = 0; // то возвращаемся к началу } offset = 75 * i; //сдвиг по слайду element.style.background = "url("" + imgName + "") " + offset + "px 50%"; //меняем позиционирование спрайта } , 1000/24) //24 кадра в секунду } element.onmouseout = function(){ //вешаем обработчик на убирание курсора мыши clearInterval(interval) //удаляем интервал (прекращаем анимацию) } } img.src = imgName; //даем имя нашего спрайта }

Ах да, нужно ведь ещё вызвать эту функцию:

SpriteAnimation(elementId, imgName, styles); spriteAnimation("ship", "ship.png", styles);

И, чтобы смотрелось к месту, можно добавить картинку с фоном и правильно спозиционировать:

Cпрайты и спрайтовую анимацию (sprite animation) использовали в видеоиграх еще в 90-е годы. Некоторые даже полностью состояли из спрайтов, позже ими заменяли только отдельные вещи (например взрывы, внутриигровую растительность, даже источники света были стилизованы под спрайты). Но кто бы мог подумать, что в 21 веке мы все еще будем сталкиваться с ними? Разберемся сегодня на !

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

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

Frame { width: 280px; height: 456px; background: url("http://thewebivore.com/demos/collie/deer-sprite.png"); animation: play 1s steps(7) infinite; -webkit-animation: play 1s steps(7) infinite; /* Ключевой момент здесь steps(7), мы указываем количество шагов, за которые нужно полностью прокрутить ленту кадров. Если мы этого не сделаем, а просто оставим infinite, то не получится ощущение анимации и будет просто быстро текущий поток рисунков */ } @-webkit-keyframes play { from { background-position: 0px; } to { background-position: -1956px; } } @-moz-keyframes play { from { background-position: 0px; } to { background-position: -1956px; } } @-o-keyframes play { from { background-position: 0px; } to { background-position: -1956px; } } @keyframes play { from { background-position: 0px; } to { background-position: -1956px; } }

Sprite animation VS gif-файлы

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

  • Gif-изображения ограничены 256 цветами – невозможно сделать красивые изображения с большим количеством полутонов.
  • «Гифки» автоматически начинают проигрываться, даже если не загрузились до конца, тогда как спрайты можно предзагрузить и убедиться что анимация будет плавной.
  • Спрайтовую анимацию можно полностью контролировать через Javascript и стилизовать с помощью CSS: останавливать, проигрывать частично, воспроизводить только после какого-то события, а также добавлять рамки, заливки и многое другое.
  • Ну и конечно же со спрайтами возможен частичный рендеринг. То есть у стоячего на фоне леса человечка можно анимировать только волосы и руки, все остальное будет статичным – это экономит определенное количество ресурсов.

Реализация sprite animation через css-анимации может похвастаться неплохой поддержкой браузеров.

На самом деле, многие праздничные дудлы от Google выполнены спрайтовой анимацией, компания Adobe недавно выкатила экспериментальный сайт, посвященный css-анимациям – . Я уверен, что этот функционал станет еще более популярным в ближайшие пару лет.

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

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

Когда компьютеры были большими, спрайты были совсем крошечными, размером 8 на 8 пикселей и могли быть раскрашены в 4-8 цветов. Самым известным героем первых спрайтовых игр был всеми любимый Марио. Из-за крохотных размеров спрайта нарисовать этому персонажу рот было невозможно: так появились характерные усы Марио, герой стал узнаваемым.

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

На рубеже тысячелетий традиционная анимация начала уступать место 3D-анимации. То же самое происходило и в мире компьютерных игр - спрайтовые персонажи стали вытесняться полигональными. Однако спрайтовая анимация ещё не собирается уходить со сцены. Она продолжает широко использоваться в оформлении (вспомните "помощников" из более ранних версий Office), в обучающих программах, а также в играх - главным образом, для мобильных устройств. Кроме того, в наши дни спрайты используются во Flash и gif-анимации, только хранятся не в виде битмэпа, а в формате многослойного графического файла, где каждый кадр анимации расположен на отдельном слое. В Adobe After Effects можно поместить в сцену анимацию, созданную из последовательности графических файлов. Строго говоря, это тоже спрайтовая анимация, только источником спрайтов является не многослойная картинка или битмэп, а отдельная папка на компьютере. Так что спрайты, как волшебные феи, всегда готовы прийти на выручку аниматору, особенно при создании игр!

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

Давайте на какое-то время отвлечемся от игры и поговорим об анимации вообще. Представим, что нам требуется нарисовать человечка, который шагает слева направо по экрану. Как это можно реализовать? Обычно новичкам эта задача кажется непомерно трудной. На самом деле здесь нет ничего сложного. Идея взята из кинематографа. У нас должен быть набор изображений, представляющий собой "фотографии" нашего человечка в разные, достаточно близкие, моменты времени. Быстро меняя кадры, мы увидим, что наша картинка начала двигаться. Ситуацию с походкой упрощает тот факт, что она носит периодический характер. Грубо говоря, чтобы получить красивую и достоверную анимацию нам достаточно иметь кадры с момента, когда человечек опирается, скажем, на левую ногу, до момента, когда он сделав два шага вновь на нее обопрется.

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

В компьютерной графике и gamedev-е широко используется понятиеспрайт (sprite ). В современной трактовке оно означает графический объект, который может перемешаться по экрану и при этом изменять свой вид. Со спрайтом связан рисунок, содержащий раскадровку и, в зависимости от ситуации, спрайт имеет вид того или иного кадра. Ниже приведет пример рисунка с последовательностью кадров, который будет использоваться нами для создания спрайта.

Это изображение имеет ширину в 150 точек, и содержит 5 кадров, то есть ширина каждого кадра составляет 30 пикселей.

Чтобы создать анимацию мы можем загрузить каждый кадр, как отдельную картинку и затем через равные интервалы времени выводить их последовательно на экран. Можно сделать по-другому: загрузить одну картинку, содержащую все кадры, а затем налету разбивать ее на кадры и выводить требуемый в данный момент кадр. На самом деле это довольно просто. Мы знаем, что наша картинка содержит 5 кадров шириной 30 пикселей. Определим прямоугольник, имеющий ширину кадра (30 точек) и высоту всего изображения. На приведенном ниже рисунке синими прямоугольниками отмечены первые два кадра.

Давайте продолжим разработку нашей игры. Создадим проект. За основу возьмем разработанный на предыдущих уроках пример Droid_03. Добавим в папку res/drawable-mdpi проекта файл Создадим новый класс для нашего персонажа. Поскольку в Monkey Island персонаж зовут Elaine, назовем класс ElaineAnimated.

public class ElaineAnimated{ private static final String TAG= ElaineAnimated. class . getSimpleName() ; private Bitmap bitmap; // Картинка с анимационной последовательностью private Rect sourceRect; // Прямоугольная область в bitmap, которую нужно нарисовать private int frameNr; // Число кадров в анимации private int currentFrame; // Текущий кадр private long frameTicker; // время обновления последнего кадра private int framePeriod; // сколько миллисекунд должно пройти перед сменой кадра (1000/fps) private int spriteWidth; // ширина спрайта (одного кадра) private int spriteHeight; // высота спрайта private int x; // X координата спрайта (верхний левый угол картинки) private int y; // Y координата спрайта (верхний левый угол картинки) }

Здесь bitmap - png рисунок, содержащий все кадры; sourceRect - прямоугольная область, которая "очерчивает" в рисунке границы текущего кадра; frameTicker - переменная, содержащая время, которое прошло с момента последной смены кадра анимации. Указанная в комментарии переменная fps обозначает не fps игры, а fps спрайта, то есть сколько раз изменяется кадр спрайта за секунду. Для плавной анимации это значение должно иметь величину порядка 25-30, однако для наших учебных целей подойдет и более скромное число 5. Мы не можем поставить здесь большее значение, поскольку у нас всего 5 кадров в рисунке. framePeriod - период между сменой кадра в миллисекундах. Для нашего случая, когда мы хотим менять 5 кадров в секунду эта величина равна 200 мс.

Напишем конструктор

public ElaineAnimated(Bitmap bitmap, int x, int y, int width, int height, int fps, int frameCount) { this. bitmap= bitmap; this. x= x; this. y= y; currentFrame= 0 ; frameNr= frameCount; spriteWidth= bitmap. getWidth() / frameCount; spriteHeight= bitmap. getHeight() ; sourceRect= new Rect(0 , 0 , spriteWidth, spriteHeight) ; framePeriod= 1000 / fps; frameTicker= 0l; }

Подразумевается, что кадры имеют одинаковую ширину, поэтому spriteWidth вычисляется, как общая ширина рисунков поделенная на число кадров. В конструктор передается параметр fps, который обозначает число кадров спрайта в секунду.

Добавим в конструктор класса MainGamePanel строку, создающую объект для нашего нового анимированного персонажа

public MainGamePanel(Context context) { super(context) ; // Сообщаем, что обработчик событий от поверхности будет реализован // в этом классе. getHolder() . addCallback(this) ; elaine= new ElaineAnimated( BitmapFactory. decodeResource(getResources() , R. drawable. walk_elaine) , 10 , 50 // начальное положение , 30 , 47 // ширина и высота спрайта , 5 , 5 ) ; // FPS и число кадров в анимации ...

Добавим в класс метод update, который будет изменять текущий кадр в зависимости от текущего времени. Мы специально не привязываемся к частоте обновления игрового цикла, чтобы не нарушать процессор лишней работой. Например, допустим, что наша программа запущена на очень продвинутом телефоне, который обеспечивает проход итерации игрового цикла за 20 мс, допустим также, что менять картинку спрайта нужно раз в 200 мс, таким образом, мы должны обновлять спрайт каждый десятый шаг игрового цикла.

public void update(long gameTime) { if (gameTime> frameTicker+ framePeriod) { frameTicker= gameTime; // увеличиваем номер текущего кадра currentFrame++; //если текущий кадр превышает номер последнего кадра в // анимационной последовательности, то переходим на нулевой кадр if (currentFrame>= frameNr) { currentFrame= 0 ; } } // Определяем область на рисунке с раскадровкой, соответствующую текущему кадру this. sourceRect. left= currentFrame* spriteWidth; this. sourceRect. right= this. sourceRect. left+ spriteWidth; }

Этот метод получает в качестве параметра текущее время и если это время превышает сумму времени последнего обновления (frameTicker) и длительности показа кадра (framePeriod), то необходимо перейти к показу следующего кадра. Для этого увеличиваем на единицу значение переменная currentFrame, а затем на основании ее значения вычисляем заново границы кадра (sourceRect.left и sourceRect.right).

Внесем изменение в метод update класса MainGamePanel

public void update() { elaine. update(System . currentTimeMillis() ) ; ...

Теперь у нашего спрайта меняются кадры, но мы не видим этого. Добавим в класс ElaineAnimated метод draw, который будет выводить на экран текущий кадр

public void draw(Canvas canvas) { // область, где рисуется спрайт Rect destRect= new Rect(x, y, x+ spriteWidth, y+ spriteHeight) ; //комманда вывода рисунка на экран. canvas. drawBitmap(bitmap, sourceRect, destRect, null ) ; }

Команда canvas.drawBitmap рисует прямоугольную область sourceRect из рисунка bitmap в прямоугольной области destRect.

Изменим также метод onDraw, добавив туда вызов метода перерисовки спрайта

protected void onDraw(Canvas canvas) { // Заливаем canvas черным цветом canvas. drawColor(Color. BLACK) ; //рисуем анимированный спрайт elaine. draw(canvas) ; ...

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

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

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

Когда компьютеры были большими, спрайты были совсем крошечными, размером 8 на 8 пикселей и могли быть раскрашены в 4-8 цветов. Самым известным героем первых спрайтовых игр был всеми любимый Марио. Из-за крохотных размеров спрайта нарисовать этому персонажу рот было невозможно: так появились характерные усы Марио, герой стал узнаваемым.

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

Образцы спрайтов из игр серии Street Fighter

Однако ещё более популярным стал жанр "платформенной аркады", где спрайтовая анимация по-настоящему правила бал. На рубеже девяностых на студии Дисней наступила эпоха возрождения: из года в год рождались новые шедевры. Небывалым спросом пользовались игры по мотивам знаменитых мультфильмов студии. Возможности спрайтовой анимации возросли настолько, что в аркадной игре можно было буквально погрузиться в атмосферу мультфильма. Игры были подлинными шедеврами - под стать оригинальным анимационным полнометражкам! "Алладин", "Король лев", "Геркулес", "Книга джунглей" и многие другие игры с логотипом Disney навсегда вошли в золотую коллекцию аркад. Это был настоящий расцвет спрайтовой анимации.

Аркада The Lion King (1994 год)

Все спрайты, образующие анимацию, хранятся в специальном файле в виде одной большой растровой карты (битмэпа). "Картой" это изображение называют потому, что игровая программа постоянно ищет нужный спрайт по координатам. Допустим, область анимации составляет 16 на 32 пикселя, а отправной точкой на битмэпе в требуемый момент времени является 84-й пиксель по горизонтали, 460-й - по вертикали. Эти данные заносятся программистом в код игры. Таким образом, битмэп является своеобразной "раскадровкой" спрайтовой анимации, "зашитой" в код программы и скрытой от постороннего зрителя. Вот как, например, выглядит эта "раскадровка" для игры "Король лев", персонаж - маленький Симба:

Ещё одним популярным жанром игр, где использовались спрайты, были анимационные квесты. Стоит отметить, что на сегодня это, пожалуй, единственный жанр игр для настольных компьютеров, где спрайтовая анимация всё ещё широко применяется. Многим из нас знакомы игры серии "Петька и Василий Иванович", "Братья пилоты" и другие популярные отечественные мультипликационные квесты. Они тоже основаны на спрайтах.

Скриншот из игры "Братья пилоты: по следам полосатого слона" (1997 год)

На рубеже тысячелетий традиционная анимация начала уступать место 3D-анимации. То же самое происходило и в мире компьютерных игр - спрайтовые персонажи стали вытесняться полигональными. Даже игры по мотивам новых диснеевских мультфильмов, выполненных в технике классической анимации, были перенесены в трёхмерное пространство. Примеры - "Тарзан", "Лило и Стич", "Братец Медвежонок", "Принцесса и лягушка".

Скриншот из игры "Братец медвежонок" (2002 год)

Однако спрайтовая анимация ещё не собирается уходить со сцены. Она продолжает широко использоваться в оформлении (вспомните "помощников" из более ранних версий Microsoft Office), в обучающих программах, а также в играх - главным образом, для мобильных устройств. Кроме того, в наши дни спрайты используются во Flash и gif-анимации, только хранятся не в виде битмэпа, а в формате многослойного графического файла, где каждый кадр анимации расположен на отдельном слое. В Adobe After Effects можно поместить в сцену анимацию, созданную из последовательности графических файлов. Строго говоря, это тоже спрайтовая анимация, только источником спрайтов является не многослойная картинка или битмэп, а отдельная папка на компьютере. Так что спрайты, как волшебные феи, всегда готовы прийти на выручку аниматору, особенно при создании игр! :)

Спрайты из игры Angry Birds (2009 год)


Спрайты из игры Dust: an Elysian Tail (2012 год)