callbackhunter

Спрайты для анимаций

Подготовил: Андрей Косяк Дата публикации: 23.12.2013
Последнее обновление: 30.12.2013

Задача:

Реализовать анимацию персонажа.

Решение:

Дан спрайт:

Спрайт для анимации

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

Общепринятой частотой кадров для мультфильмов является частота 24 к/сек.

В сети эта величина составляет 12-18 к/сек. Чем больше частота, тем плавнее и качественнее изображение.

Наш спрайт рассчитан на воспроизведение длительностью 0.6 секунды, а значит его частота 7 кадров / 0.6 сек ~ 12 к/сек.

Для того, чтобы определить необходимое кол-во кадров, которое должно быть отрисовано для анимации нужно:

  • определится с частотой кадров анимации. Допустим, остановились на 14 к/сек.
  • определиться с длительностью анимации. Допустим, остановились на 0.8 сек.
  • на базе частоты и времени рассчитать кол-во кадров: 0.8 сек * 14 к/сек = 11 кадров

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

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

Инструменты:

  • ShoeBox (http://renderhjs.net/shoebox/ ) – инструмент позволяет разрезать, собирать, склеивать, вырезать спрайты по контуру, конвертировать в разные форматы, выделять альфа маски, разбивать на тайлы, работать со шрифтами и много чего еще. Это бесплатное приложение для Adobe Air( должен быть установлен на машине ). Плюс еще масса интересных возможностей и настроек.

Подготовка спрайта.

Отрисованный персонаж может попасть к нам в руки в двух состояниях: в виде одного спрайта со всеми кадрами:

Спрайт для анимации

либо набор файлов-изображений, в котором каждый файл - отдельный кадр:

Файл1:

Спрайт для анимации

Файл2:

Спрайт для анимации

и т. д.

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

Будем считать, что дизайнер нарисовал нам персонажа и прислал его набором файлов:

0000.png
0001.png
0002.png
0003.png
0004.png
0005.png
0006.png

Итого 7 кадров.

Собираем спрайт.

Открываем ShoeBox → Animation:

Спрайт для анимации

Перетаскиваем нужные файлы-кадры на иконку “Frame Sheet”:

Спрайт для анимации

В итоге открывается еще одно окно с уже собранным анимационным спрайтом:

Спрайт для анимации

Нажимаем кнопку “Save” и получаем в той же директории, откуда мы перетянули файлы, еще один файл с именем sheet_7x1.png, где 7 – это кол-во кадров в одной строке, а 1 – кол-во строк в файле. В итоге имеем 7x1=7 кадров анимации.

Теперь нужно узнать размер кадра. Смотрим на св-ва полученного файла:

Спрайт для анимации

Отсюда ширина кадра - 462 / 7 = 66px, высота 135. Т.е. размер одного кадра 66 x 135.

Для задачи я написал небольшой объект XAnimation, при помощи которого мы и оживим наш спрайт. Основная задача объекта возвращать в нужное время нужный кадр анимации:

function XAnimate( params ){
	var	self = this;

    // опции по-умолчанию
	self.options = {
        frames: 1,
        duration: 1000,
        repeat: -1,
        yoyo: false,
        sequence: [ 0 ]
	};
	$.extend( self.options, params );

	self.init( params );
	
	return this;
};


XAnimate.prototype = {
    init: function( params ){
        var	self = this,
            options = self.options;

        // флаги состояния анимации
        self.paused = false;
        self.stopped = false;
        self.playing = false;
        self.positiveDirection = true;

        // ширина весго спрайт листа
        self.totalWidth = options.width * options.frames;
        self.resetTimers();
        self.play();
        return this;
    },

    // сброс таймеров
    resetTimers: function(){
        var self = this;
        self.timeStart = 0;             // время старта
        self.timePause = 0;             // время длительности паузы
        self.timePauseStart = 0;        // время постановки на паузу
        self.timeDelta = 0;             // разница времени от старта
        self.progress = 0;              // общий прогресс
        self.currentFrame = 0;          // текущий кадр
        self.repeated = 0;              // текущее кол-во повторов анимации
        self.positiveDirection = true;  // текущее направление анимации ( вперед/назад )
    },

    // пересчет прогресса и обновление таймеров
    update: function(){
        var self = this,
            options = self.options;

        // если пауза - считаем ее длительность
        if ( self.paused ) {
            self.timePause = new Date().getTime() - self.timePauseStart;
            return;
        }

        // считаем разницу времени и прогресс процесса
        self.timeDelta = new Date().getTime() - self.timeStart + self.timePause;
        progress = self.timeDelta / options.duration;
        self.progress = self.positiveDirection ? progress : 1 - progress;


        // просчет и выдача текущего результата анимации
        function actions( test ){
            self.currentFrame = options.sequence[ options.sequence.length * self.progress >> 0 ] >> 0;
            options.onUpdate( {
                currentFrame: self.currentFrame,
                progress: self.progress,
                positiveDirection: self.positiveDirection
            } );

        };

        // если анимация не остановлена
        if ( !self.stopped ) {

            // если анимационный цикл закончен
            if ( progress >= 1 ) {

                // прогресс имеет максимальное значение (100%)
                self.progress = 1;

                // переключаем направление анимации, если стоит флаг
                if ( options.yoyo ) {
                    self.positiveDirection = !self.positiveDirection;
                }

                // если кол-во повторов в опциях не соответствует текущему - запускаем процесс заново
                if ( self.repeated != options.repeat ) {
                    self.repeated++;
                    self.timeStart = new Date().getTime();
                    self.reqTimer = window.requestAnimFrame( function(){ self.update() } );
                // иначе останавливаем процесс
                } else {
                    self.stop();
                }

            // если анимационный цикл не закончен
            } else {
                // обновляем текущие данные
                actions();
                // продолжаем процесс
                self.reqTimer = window.requestAnimFrame( function(){ self.update() } );
            }

        // если анимация остановлена
        } else {
            // сброс всех таймеров
            self.resetTimers();
            // установка анимации в исходную
            actions( true );
        }

    },

    // запуск анимации
    play: function(){
        var self = this;

        if ( !self.playing ) {

            // установка флагов состояния
            self.paused = false;
            self.stopped = false;
            self.playing = true;

            // сдвиг времени старта анимации на длительность паузы
            if ( !self.paused ) {
                self.timeStart += ( new Date().getTime() - self.timePauseStart );
                self.timePause = 0;
                self.update();
            }
        }

    },

    // пауза анимации
    pause: function(){
        var self = this;

        if ( !self.stopped ) {
            // триггер паузы
            self.paused = !self.paused;

            // если пауза - считаем длительность
            if ( self.paused ) {
                self.timePauseStart = new Date().getTime();
                self.timePause = new Date().getTime();
                self.playing = false;
            // иначе запуск анимации
            } else {
                self.play();
            }
        }

    },

    // остановка анимации
    stop: function(){
        var self = this;

        // установка флагов состояния
        self.playing = false;
        self.stopped = true;
        self.paused = false;

        // последний кадр
        self.update();
        window.cancelRequestAnimFrame( self.reqTimer );
    }
};


window.cancelRequestAnimFrame = ( function() {
    return window.cancelAnimationFrame          ||
        window.webkitCancelRequestAnimationFrame    ||
        window.mozCancelRequestAnimationFrame       ||
        window.oCancelRequestAnimationFrame     ||
        window.msCancelRequestAnimationFrame        ||
        clearTimeout
} )();

window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame    ||
        window.oRequestAnimationFrame      ||
        window.msRequestAnimationFrame     ||
        function(/* function */ callback, /* DOMElement */ element){
            return window.setTimeout(callback, 1000 / 60);
        };
})();

Скачать можно тут:

xanimation-1.0.js

xanimation-1.0.min.js

Начнем с анимации элемента DOM. Создадим сам элемент и кнопки управления анимацией:

<div class="example">
        <div class="entity-wrap">
            <div class="entity"></div>
        </div>

        <div>
            <input type="button" id="pause1" value="Pause">
            <input type="button" id="stop1" value="Stop">
            <input type="button" id="play1" value="Play">
        </div>
    </div>

и стили для него:

.entity {
    width: 66px;
    height: 135px;
    background: url(../img/sheet_7x1.png) no-repeat;
    position: absolute;
    bottom: 0;
    left: 0;
    overflow: hidden;
}

.entity-wrap {
    width: 200px;
    height: 200px;
    margin-bottom: 20px;
    position: relative;
}

.example {
    margin-bottom: 30px;
}

Создаем анимацию для него:

// DOM
    var animation = new XAnimate( {
        width: 66,
        height: 135,
        frames: 7,
        duration: 600,
        repeat: -1,
        yoyo: true,
        sequence: [ 0, 1, 2, 3, 4, 5, 6 ],
        onUpdate: function( params ){
            $entity.css( {
                backgroundPosition: -params.currentFrame * 66 +"px 0"
            } );
        }
    } );
    $("#stop1").on( {
        click: function(){
            animation.stop();
        }
    } );
    $("#play1").on( {
        click: function(){
            animation.play();
        }
    } );
    $("#pause1").on( {
        click: function(){
            animation.pause();
        }
    } );

Результат.

демо-пример.

Теперь сделаем то же самое для канвы. Добавляем в html канву:

<div class="example">
        <div class="entity-wrap">
            <div class="entity"></div>
        </div>

        <div>
            <input type="button" id="pause1" value="Pause">
            <input type="button" id="stop1" value="Stop">
            <input type="button" id="play1" value="Play">
        </div>
    </div>


    <div class="example">
        <div class="entity-wrap">
            <canvas id="canvas" width="200" height="200"></canvas>
        </div>

        <div>
            <input type="button" id="pause2" value="Pause">
            <input type="button" id="stop2" value="Stop">
            <input type="button" id="play2" value="Play">
        </div>
    </div>

Создаем анимацию для персонажа на канве:

// DOM
    var animation = new XAnimate( {
        width: 66,
        height: 135,
        frames: 7,
        duration: 600,
        repeat: -1,
        yoyo: true,
        sequence: [ 0, 1, 2, 3, 4, 5, 6 ],
        onUpdate: function( params ){
            $entity.css( {
                backgroundPosition: -params.currentFrame * 66 + "px 0"
            } );
        }
    } );
    $("#stop1").on( {
        click: function(){
            animation.stop();
        }
    } );
    $("#play1").on( {
        click: function(){
            animation.play();
        }
    } );
    $( "#pause1").on( {
        click: function(){
            animation.pause();
        }
    } );



    // canvas
    var canvas = $("#canvas")[ 0 ],
        ctx = canvas.getContext("2d"),
        img = new Image();
    $( img ).load( function(){
        var animationCanvas = new XAnimate( {
            width: 66,
            height: 135,
            frames: 7,
            duration: 600,
            repeat: -1,
            yoyo: true,
            sequence: [ 0, 1, 2, 3, 4, 5, 6 ],
            onUpdate: function( params ){
                ctx.clearRect( 0, 0, canvas.width, canvas.height );
                ctx.save();
                ctx.translate( 33, canvas.height );
                ctx.drawImage( img, params.currentFrame * 66, 0, 66, 135, -33, -135, 66, 135 );
                ctx.restore();
            }
        } );


        $("#stop2").on( {
            click: function(){
                animationCanvas.stop();
            }
        } );
        $("#play2").on( {
            click: function(){
                animationCanvas.play();
            }
        } );
        $("#pause2").on( {
            click: function(){
                animationCanvas.pause();
            }
        } );

    }).attr("src", "../img/sheet_7x1.png" );

Результат.

демо-пример.

В итоге получились две независимые друг от друга анимации.

Показать комментарии