Здравствуйте, друзья!
Предлагаю вам небольшой урок на тему анимации спрайтов с альфаканалом на канве 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. я смнеил для вас фон на менее беспощадный =)