Здравствуйте, друзья!
Предлагаю вам небольшой урок на тему анимации спрайтов с альфаканалом на канве HTML5.
Для начала нарисуем нечто

Почему круглое и желтое? Потому что у Дугласа Адамса в «Автостопом по галактике» есть такой Слартибартфаст — очень трогательный дядя, имеет приз за береговые линии при строительстве Земли. Поэтому на всякий случай будем анимировать желтую звезду.
Далее анимируем звезду в последовательность из 256 кадров с размером кадра 256х256 пикселей.
Те 256, эти 256 и вон те 256 взяты только ради примера. Анимация может быть и короче и мельче, впрочем как и длиннее и крупнее — все зависит от ваших целей. Я исхожу из того что 256х256 пикселей нынче это такой вполне себе нормальный размер для спрайта: не слишком большой и не слишком маленький. Конечно раньше, когда трава была зеленее, и спрайты были скромнее 16х16 или 32х32, но мы будем как будто бы на вырост экспериментировать.
256 кадров это тоже не так чтобы много но и не так чтобы мало: в зависимости от частоты кадров можно получить 10-20-30 секундную последовательность.
До сих пор все без неожиданностей, пока не перемножить размеры сторон кадра, длительность и глубину:
256 х 256 х 256 х 4 = 64 МБ
Однако! 64 мегабайта без сжатия это немного жирновато для спрайта. Если выполнить просчет всех 256 кадров в матрицу 16х16 кадров и сохранить эту рыбу в PNG с альфа-каналом (все происходит в Adobe After Effects) то на выходе получим 18.6 МБ что хоть и не 64 но все-же немало. Для тех, кому интересно взглянуть на этот PNG вот ссылочка.
Неужели After Effect так небрежно относится к сжатию PNG? Чтобы привести полученную сетку к вменяемому размеру, я попытался оптимизировать отрендеренный PNG с помощью утилит OptiPNG и PNGOut. Обе утилиты запускались в с различными опциями вплоть до экстремальных, но в результате в самом лучшем случае сжатие оптимизировалось на феерические 1.5 процента, что явно не соответсвует ожиданиям. Поэтому эта анимация была пересчитана в пару файлов без потери качества (тот же самый PNG) но раздельно RGB и Alpha, и далее с помощью Image Optimizer они были конвертированы в пару JPEG-ов. Чтобы облегчить работу компрессору JPG (это правильное слово?), фон на той последовательности что содержит RGB-слои перед рендерингом принудительно заливаем космически-желтым цветом вместо космически-черного.
В зависимости от степени сжатия получаем разные по весу пары JPEG-ов:
Вот эти размеры уже ближе к желаемому. Для меня оказалось полной неожиданностью что файл с прозрачностью оказывается во всех случаях тяжелее чем файл с цветами, вероятно все дело в контрасте. Визуально-же, все что сжато сильнее 50% похоже на кирпичи а не на косомс. Друзья, если среди вас есть кто-то кто сможет сделать хорошо сжатый и одновременно хорошо различимый JPEG — поделитесь пожалуйста своим опытом, я с удовольствием обновлю статью. А пока дальнейшие эксперименты будем проводить с парой файлов сжатых до 75% которые тянут в сумме на 1547KB что ощутимо лучше (легче) чем 18.6MB для PNG и уж тем более 64MB для несжатого формата.
Теперь эти раздельные JPEG-и нам будет отдавать сервер а на клиенте мы их будем принимать и склеивать в одно общее изображение с прозрачностью.
Разметка-же будет самая что ни наесть пустая:
Как впрочем и стили:
В блоке скрипта по окончании формирования документа вызываем функцию загрузки изображений.
Она очень простая (лежит в файле js/main.js):
В двух словах: объявим два объекта Image и дождавшись их загрузки (т.е. когда число img_count будет 2). В моем скрипте этот момент сделан несколько через одно место — после загрузки любого из файлов проверяется а не загружен ли УЖЕ другой и если да, то вызывается функция сборки двух изображений в одно. Я не так чтобы хорошо разбираюсь в JavaScript просто чуствую что что-то не так, но несмотря на мои чувства оно работает. И поскольку ожидается (в примере) всегда именно 2 изображения, то функцию загрузки пока оставим в покое и напишем функции сборки.
Тут тоже нет ничего сложного — в первом условии проверется наличие размеров а во втором их соответсвие. потом объявляем три канвы: для цвета, для прозрачности и для анимации. Как положено проверяем Наличие поддержки механизма Canvas в браузере (я тестировал в Mozilla Firefox и Google Chrome, тут все в порядке а у IE8 случилась истерика и я оставил его в покое. Как подсказывает dasm32: «На Opera работает», а так-же у sashagil «в IE 10 (это который пока только в Win8), нормально показывает», но у Krovosos «На IPad Safari не сработало, даже фон не показался» ) и генерируем для них контексты. А дальше начинается магия:
эти двумя строчками вытягиваем RGBA-слои от контекстов файла цвета и файла прозрачности, каждый из которых разворачивается в 64-мегабайтный массив, несмотря на то что в случае с файлом цвета отсутствует составляющая прозрачности а в случае Gray-Mode файла прозрачности достаточно вообще одного 8-битного слоя, но такова селяви — канва всегда 4 байта в глубину. По байту на каждый из трех цветов и еще один для прозрачности. Попав на канву монохромный файл прозрачности «разворачивается в глубину» на 32 бита таким образом что R=G=B а прозрачность=255.
обходим в цикле все пиксели этих массивов и
копируем значение байта красного цвета из канвы прозрачности в слой прозрачности (data[i] — красный, data[i+1] — зеленый, data[i+2] — синий, data[i+3] — альфа)
С этого момента у нас есть огромная (4096х4096) портянка состоящая из 256 отдельных кадров каждый из которых 256х256 пикселей размером. Кадры выложены в сетку 16х16 сверху-вниз и слева-направо. Теперь самое логичное экспортировать эту сетку из канвы в документ как Base64 инстанс и скормить его свойству Background-Image в DIV соответсвующего размера и сдвигать начальную точку по таймеру на размер кадра. Эксперимент показал что это ужасный вариант в плане загрузки процессора, мой DualCore 2.2GHz не смог выдать выше 5 FPS. Поэтому разрежем эти 16х16 солнышек на кадры и сложим их в массив.
далее запускаем таймер 16 раз в секунду (можно и скорее и помедленнее) и видим анимацию, или скачиваем и смотрим локально.
Внимание! Хром не станет локально «закачивать» файлы по соображениям безобразности, поэтому остается ФФ.
UPD SHVV: "… чтобы Хром смог открыть локальные файлы, его надо запустить с ключом --disable-web-security".
Если же вы смотрите онлайн, то должен сказать что загрузка (1.5МБ) происходит незаметно, а вот склеивание замерзает примерно секунд на 5, но потом отмерзает и анимация работает. Причем процессор загружен на 1% что дает повод провести дальнейший эксперимент с парой таких солнышек на предмет «посмотреть как они пересекутся».
для этого модифицируем CSS
Сделаем абсолютное позиционирование чтобы блок мог летать.
А также JavaScript. Во-первых сделаем две звезды, соответственно два DIV-а, вызовем две функции склейки —
одна такая-же как и ранее (compileRGBA) а вторая модифиуированная (compileGGAA) она отличается от первой лишь немного более вольным обращением с каналами цвета-прозрачности:
как видно тут в цикле значение красного канала заменяется на значение зеленого, а синий и прозрачность копируются из прозрачности второго файла. Звезда синеет. А так-же в таймере анимации сдвинуто значение стартового кадра на сотню. Это чтобы не получилось синхронного плавания.
Во-вторых зададим таймеры для перемещения этих звезд по эллипсу с дельтой фаз в 180 градусов:
в-третьих удалим промежуточные структуры:
Тут сложно сказать есть-ли толк от этих удалений, по-крайней мере Task Manager никакой разницы не заметил, что с удалением что без удаления.
Таким образом получается две по-переменно затменные звезды.
— А можно всех посмотреть?
— Да, конечно. Скачивать будете?
На этом эксперимент/урок закончен. Ощущение двоякое: с одной стороны принципиально все работает. С другой — этот дикий тормоз на момент склейки портит радость победы. И что с этим делать пока не понятно. Если у вас есть предложения — охотно выслушаю и внесу коррективы.
Спасибо за внимание, друзья!
P.S. я смнеил для вас фон на менее беспощадный =)
Предлагаю вам небольшой урок на тему анимации спрайтов с альфаканалом на канве HTML5.
Преамбула.
Для начала нарисуем нечто

Почему круглое и желтое? Потому что у Дугласа Адамса в «Автостопом по галактике» есть такой Слартибартфаст — очень трогательный дядя, имеет приз за береговые линии при строительстве Земли. Поэтому на всякий случай будем анимировать желтую звезду.
Далее анимируем звезду в последовательность из 256 кадров с размером кадра 256х256 пикселей.
Те 256, эти 256 и вон те 256 взяты только ради примера. Анимация может быть и короче и мельче, впрочем как и длиннее и крупнее — все зависит от ваших целей. Я исхожу из того что 256х256 пикселей нынче это такой вполне себе нормальный размер для спрайта: не слишком большой и не слишком маленький. Конечно раньше, когда трава была зеленее, и спрайты были скромнее 16х16 или 32х32, но мы будем как будто бы на вырост экспериментировать.
256 кадров это тоже не так чтобы много но и не так чтобы мало: в зависимости от частоты кадров можно получить 10-20-30 секундную последовательность.
До сих пор все без неожиданностей, пока не перемножить размеры сторон кадра, длительность и глубину:
256 х 256 х 256 х 4 = 64 МБ
Однако! 64 мегабайта без сжатия это немного жирновато для спрайта. Если выполнить просчет всех 256 кадров в матрицу 16х16 кадров и сохранить эту рыбу в PNG с альфа-каналом (все происходит в Adobe After Effects) то на выходе получим 18.6 МБ что хоть и не 64 но все-же немало. Для тех, кому интересно взглянуть на этот PNG вот ссылочка.
Сжать!
Неужели After Effect так небрежно относится к сжатию PNG? Чтобы привести полученную сетку к вменяемому размеру, я попытался оптимизировать отрендеренный PNG с помощью утилит OptiPNG и PNGOut. Обе утилиты запускались в с различными опциями вплоть до экстремальных, но в результате в самом лучшем случае сжатие оптимизировалось на феерические 1.5 процента, что явно не соответсвует ожиданиям. Поэтому эта анимация была пересчитана в пару файлов без потери качества (тот же самый PNG) но раздельно RGB и Alpha, и далее с помощью Image Optimizer они были конвертированы в пару JPEG-ов. Чтобы облегчить работу компрессору JPG (это правильное слово?), фон на той последовательности что содержит RGB-слои перед рендерингом принудительно заливаем космически-желтым цветом вместо космически-черного.
В зависимости от степени сжатия получаем разные по весу пары JPEG-ов:
Сжатие | 10% | 15% | 25% | 50% | 75% | 85% | 90% |
---|---|---|---|---|---|---|---|
RGB | 139KB | 198KB | 250KB | 425KB | 691KB | 992KB | 1337KB |
Alpha | 363KB | 459KB | 524KB | 689KB | 856KB | 1067KB | 1273KB |
Вот эти размеры уже ближе к желаемому. Для меня оказалось полной неожиданностью что файл с прозрачностью оказывается во всех случаях тяжелее чем файл с цветами, вероятно все дело в контрасте. Визуально-же, все что сжато сильнее 50% похоже на кирпичи а не на косомс. Друзья, если среди вас есть кто-то кто сможет сделать хорошо сжатый и одновременно хорошо различимый JPEG — поделитесь пожалуйста своим опытом, я с удовольствием обновлю статью. А пока дальнейшие эксперименты будем проводить с парой файлов сжатых до 75% которые тянут в сумме на 1547KB что ощутимо лучше (легче) чем 18.6MB для PNG и уж тем более 64MB для несжатого формата.
Переслать!
Теперь эти раздельные JPEG-и нам будет отдавать сервер а на клиенте мы их будем принимать и склеивать в одно общее изображение с прозрачностью.
Разметка-же будет самая что ни наесть пустая:
Разметка
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Canvas Example: Using canvas</title>
<link rel="stylesheet" href="css/main.css" type="text/css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script src="js/main.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
loadRGBA("jpeg_16x16/render_16_16_rgb_75.jpg", "jpeg_16x16/render_16_16_aplha_75.jpg");
});
</script>
</head>
<body>
<div id="star1" class="base64"></div>
<div id="star2" class="base64"></div>
</body>
</html>
Как впрочем и стили:
Стили
* {
margin: 0;
padding: 0;
}
html {
background: #888 url(../backs/back64dark.png);
font-size: 14px;
font-family: 'Helvetica', Helvetica, sans-serif;
}
h1 {
font-size: 1.5em;
text-align: center;
margin: 1em 0;
color: #ddd;
}
#base64{
background: transparent;
color: #bbb;
width: 256px;
height: 265px;
margin: 0 auto;
}
В блоке скрипта по окончании формирования документа вызываем функцию загрузки изображений.
Она очень простая (лежит в файле js/main.js):
function loadRGBA()
function loadRGBA(url_rgb, url_alpha){
var img_rgb = new Image();
var img_alpha = new Image();
var img_count = 0;
var img_rgba = '';
img_rgb.src = url_rgb;
img_alpha.src = url_alpha;
img_rgb.onload = function(){
++img_count;
if(2 == img_count){
img_rgba = compileRGBA(img_rgb, img_alpha);
}
}
img_alpha.onload = function(){
++img_count;
if(2 == img_count){
img_rgba = compileRGBA(img_rgb, img_alpha);
}
}
}
В двух словах: объявим два объекта Image и дождавшись их загрузки (т.е. когда число img_count будет 2). В моем скрипте этот момент сделан несколько через одно место — после загрузки любого из файлов проверяется а не загружен ли УЖЕ другой и если да, то вызывается функция сборки двух изображений в одно. Я не так чтобы хорошо разбираюсь в JavaScript просто чуствую что что-то не так, но несмотря на мои чувства оно работает. И поскольку ожидается (в примере) всегда именно 2 изображения, то функцию загрузки пока оставим в покое и напишем функции сборки.
Собрать!
compileRGBA()
function compileRGBA(raw_rgb, raw_alpha){
if (!raw_rgb.width || !raw_rgb.height || !raw_alpha.width || !raw_alpha.height){
return;
}
if (raw_rgb.width !== raw_alpha.width || raw_rgb.height !== raw_alpha.height){
alert('Размеры RGB и прозрачности не сходятся')
return;
}
var canvas_rgb = document.createElement("canvas");
var canvas_alpha = document.createElement("canvas");
var canvas_frame = document.createElement("canvas");
if (!canvas_rgb || !canvas_rgb.getContext('2d')
|| !canvas_alpha || !canvas_alpha.getContext('2d')
|| !canvas_frame || !canvas_frame.getContext('2d')){
alert('Та-а-а-а-а, насяльника... та-а-а-а, канва, насяльника');
return;
}
canvas_rgb.width = raw_rgb.width;
canvas_rgb.height = raw_rgb.height;
canvas_alpha.width = raw_alpha.width;
canvas_alpha.height = raw_alpha.height;
canvas_frame.width = 256;
canvas_frame.height = 256;
var context_rgb = canvas_rgb.getContext('2d');
var context_alpha = canvas_alpha.getContext('2d');
var context_frame = canvas_frame.getContext('2d');
context_rgb.drawImage(raw_rgb, 0, 0);
context_alpha.drawImage(raw_alpha, 0, 0);
var pix_rgb = context_rgb.getImageData(0, 0, raw_rgb.width, raw_rgb.height);
var pix_alpha = context_alpha.getImageData(0, 0, raw_alpha.width, raw_alpha.height);
for (var i = 0, n = pix_rgb.width * pix_rgb.height * 4; i < n; i += 4){
pix_rgb.data[i+3] = pix_alpha.data[i];
}
context_rgb.putImageData(pix_rgb, 0, 0);
var img_arr = [];
var frames = [];
for(var i=0; i<=15; i++){
for(var j=0; j<=15; j++){
frames[j*16 + i] = context_rgb.getImageData(i*256, j*256, 256, 256);
}
}
var frame = 0;
$("#base64").append(canvas_frame);
var intFPS = setInterval(function(){
++frame;
if (frame > 255){
frame = 0;
}
context_frame.putImageData(frames[frame], 0, 0)
}, 1000 / 16);
}
Тут тоже нет ничего сложного — в первом условии проверется наличие размеров а во втором их соответсвие. потом объявляем три канвы: для цвета, для прозрачности и для анимации. Как положено проверяем Наличие поддержки механизма Canvas в браузере (я тестировал в Mozilla Firefox и Google Chrome, тут все в порядке а у IE8 случилась истерика и я оставил его в покое. Как подсказывает dasm32: «На Opera работает», а так-же у sashagil «в IE 10 (это который пока только в Win8), нормально показывает», но у Krovosos «На IPad Safari не сработало, даже фон не показался» ) и генерируем для них контексты. А дальше начинается магия:
var pix_rgb = context_rgb.getImageData(0, 0, raw_rgb.width, raw_rgb.height);
var pix_alpha = context_alpha.getImageData(0, 0, raw_alpha.width, raw_alpha.height);
эти двумя строчками вытягиваем RGBA-слои от контекстов файла цвета и файла прозрачности, каждый из которых разворачивается в 64-мегабайтный массив, несмотря на то что в случае с файлом цвета отсутствует составляющая прозрачности а в случае Gray-Mode файла прозрачности достаточно вообще одного 8-битного слоя, но такова селяви — канва всегда 4 байта в глубину. По байту на каждый из трех цветов и еще один для прозрачности. Попав на канву монохромный файл прозрачности «разворачивается в глубину» на 32 бита таким образом что R=G=B а прозрачность=255.
обходим в цикле все пиксели этих массивов и
pix_rgb.data[i+3] = pix_alpha.data[i]
копируем значение байта красного цвета из канвы прозрачности в слой прозрачности (data[i] — красный, data[i+1] — зеленый, data[i+2] — синий, data[i+3] — альфа)
С этого момента у нас есть огромная (4096х4096) портянка состоящая из 256 отдельных кадров каждый из которых 256х256 пикселей размером. Кадры выложены в сетку 16х16 сверху-вниз и слева-направо. Теперь самое логичное экспортировать эту сетку из канвы в документ как Base64 инстанс и скормить его свойству Background-Image в DIV соответсвующего размера и сдвигать начальную точку по таймеру на размер кадра. Эксперимент показал что это ужасный вариант в плане загрузки процессора, мой DualCore 2.2GHz не смог выдать выше 5 FPS. Поэтому разрежем эти 16х16 солнышек на кадры и сложим их в массив.
var frames = [];
for(var i=0; i<=15; i++){
for(var j=0; j<=15; j++){
frames[j*16 + i] = context_rgb.getImageData(i*256, j*256, 256, 256);
}
}
далее запускаем таймер 16 раз в секунду (можно и скорее и помедленнее) и видим анимацию, или скачиваем и смотрим локально.
Внимание! Хром не станет локально «закачивать» файлы по соображениям безобразности, поэтому остается ФФ.
UPD SHVV: "… чтобы Хром смог открыть локальные файлы, его надо запустить с ключом --disable-web-security".
Если же вы смотрите онлайн, то должен сказать что загрузка (1.5МБ) происходит незаметно, а вот склеивание замерзает примерно секунд на 5, но потом отмерзает и анимация работает. Причем процессор загружен на 1% что дает повод провести дальнейший эксперимент с парой таких солнышек на предмет «посмотреть как они пересекутся».
Космос 2.0
для этого модифицируем CSS
Новые стили
.base64{
background: transparent;
position: absolute;
width: 256px;
height: 265px;
margin: 0;
}
Сделаем абсолютное позиционирование чтобы блок мог летать.
А также JavaScript. Во-первых сделаем две звезды, соответственно два DIV-а, вызовем две функции склейки —
модификация loadRGBA()
img_rgb.onload = function(){
++img_count;
if(2 == img_count){
compileRGBA(img_rgb, img_alpha, "star1");
compileGGAA(img_rgb, img_alpha, "star2");
}
}
img_alpha.onload = function(){
++img_count;
if(2 == img_count){
compileRGBA(img_rgb, img_alpha, "star1");
compileGGAA(img_rgb, img_alpha, "star2");
}
}
одна такая-же как и ранее (compileRGBA) а вторая модифиуированная (compileGGAA) она отличается от первой лишь немного более вольным обращением с каналами цвета-прозрачности:
pix_rgb.data[i] = pix_rgb.data[i+1];
pix_rgb.data[i+2] = pix_rgb.data[i+3] = pix_alpha.data[i];
как видно тут в цикле значение красного канала заменяется на значение зеленого, а синий и прозрачность копируются из прозрачности второго файла. Звезда синеет. А так-же в таймере анимации сдвинуто значение стартового кадра на сотню. Это чтобы не получилось синхронного плавания.
Во-вторых зададим таймеры для перемещения этих звезд по эллипсу с дельтой фаз в 180 градусов:
таймеры
var intRotate = setInterval(function(){
phase += .01;
if (phase >= 6.28319) {
phase = .0;
}
$("#star1.base64").css('top', (doc_h + 50*Math.sin(phase)) + 'px' );
$("#star2.base64").css('top', (doc_h + 50*Math.sin(phase + 3.14159)) + 'px' );
$("#star1.base64").css('left', (doc_w + 100*Math.cos(phase)) + 'px' );
$("#star2.base64").css('left', (doc_w + 100*Math.cos(phase + 3.14159)) + 'px' );
$("#star1.base64").css('z-index', (phase < 3.14159) ? '1001':'1000' );
$("#star2.base64").css('z-index', (phase < 3.14159) ? '1000':'1001' );
}, 1000 / 24);
в-третьих удалим промежуточные структуры:
удаляем мусор
delete pix_rgb;
delete pix_alpha;
delete context_rgb;
delete canvas_rgb;
delete context_alpha;
delete canvas_alpha;
Тут сложно сказать есть-ли толк от этих удалений, по-крайней мере Task Manager никакой разницы не заметил, что с удалением что без удаления.
Таким образом получается две по-переменно затменные звезды.
— А можно всех посмотреть?
— Да, конечно. Скачивать будете?
Резюме
На этом эксперимент/урок закончен. Ощущение двоякое: с одной стороны принципиально все работает. С другой — этот дикий тормоз на момент склейки портит радость победы. И что с этим делать пока не понятно. Если у вас есть предложения — охотно выслушаю и внесу коррективы.
Спасибо за внимание, друзья!
P.S. я смнеил для вас фон на менее беспощадный =)