Анимационный цикл статей:
- Решение для веб-кадровой анимации — принцип и реализация APNG
- Решение для веб-фреймовой анимации — анализ исходного кода lottie-web
- Решение для веб-кадровой анимации — WebGl реализует прозрачное рисование видео.
предисловие
Как передовой студент, более или менее получит потребности в анимации. Если это обычная анимация, ее относительно легко реализовать, но если это более сложная покадровая анимация, если мы используем CSS для ее реализации, очень легко вызвать следующую ситуацию: дизайнер — это шоу продавца, а разрабатываем байерское шоу.
Может быть, вы думаете об использовании GIF для достижения этого, но GIF часто имеет разные грани, которые не могут удовлетворить требования дизайнера к сложности. Поэтому нам нужно найти больше анимационных решений, которые позволят нам восстановить 100% эскиза дизайна, обеспечивая при этом уточнение и производительность анимации. В основном автор знакомитAPNGстроить планы.
APNG (Animated Portable Network Graphics) — это формат анимации, основанный на расширении формата PNG, который добавляет поддержку анимированных изображений, а также добавляет поддержку 24-битных изображений и 8-битной альфа-прозрачности, что означает, что анимация будет иметь лучшее качество.
Во-первых, давайте посмотрим на эффект сравнения APNG и GIF:
Если изображение выше не двигается или чтобы увидеть больше демонстраций, смотрите напрямуюDemo1а такжеDemo2, можно обнаружить, что, хотя размеры APNG и GIF не сильно отличаются, APNG намного четче, чем GIF, и нет разных краев. Это связано с тем, что APNG поддерживает 24-битные изображения и 8-битную альфа-прозрачность. Далее давайте рассмотрим основные принципы и способы использования APNG.
1. Формат данных APNG
1.1 PNG
Перед просмотром формата данных APNG вы должны сначала понять формат данных PNG, ведь APNG основан на расширении формата PNG. Формат данных PNG следующий:
В основном делится на 4 части:
-
Подпись PNG — это идентификатор файла, используемый для проверки того, является ли файл форматом PNG. Содержимое фиксируется как:
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a // 这里为下文打下基础,通过校验前8个字节是否为这个内容,判断是否为png
-
IHDR — это блок данных заголовка файла, который содержит основную информацию об изображениях PNG, такую как ширина и высота изображения.
-
IDAT — это блок данных изображения, ядро, в котором хранятся определенные данные изображения.
-
IEND — конечный блок данных, отмечающий конец изображения.
1.2 APNG
Разобравшись с форматом данных PNG, давайте посмотрим на формат данных APNG. Как показано ниже:
Как видите, APNG добавляет в PNG 3 модуля acTL, fcTL и fdAT.
-
acTL: должен предшествовать первому блоку IDAT, используется для того, чтобы сообщить синтаксическому анализатору, что это PNG в формате анимации, содержащий информацию об общем количестве кадров анимации и количестве циклов,Это означает, что вы можете судить, является ли это формат изображения для APNG через это поле..
-
fcTL: Блок управления кадром, который необходим для каждого кадра, относится к вспомогательному блоку в спецификации PNG и содержит порядковый номер текущего кадра, а также ширину и высоту изображения.
-
fdAT: Блок данных кадра, который имеет то же значение, что и IDAT, представляет собой данные изображения. Но у него больше серийных номеров кадров, чем у IDAT, потому что в анимации несколько кадров. На рисунке видно, что данные изображения первого кадра по-прежнему называются IDAT, а после второго кадра они называются fdAT, потому что формат первого кадра и данных PNG остается прежним. В браузерах, не поддерживающих APNG, его можно понизить до статического изображения, показывающего только первый кадр.
Чтобы лучше понять формат данных APNG, заинтересованные учащиеся могут использовать приведенное ниже программное обеспечение APNGb для самостоятельного создания анимации APNG. Демо ниже создано с 4 изображениями часов.
Эффект:(Если вы не двигаетесь, просто посмотрите приведенную выше демонстрацию напрямую)
2. Производительность
После изучения формата данных APNG и с помощью приведенной выше демонстрации мы можем обнаружить, что анимация часов хранит 4 кадра данных изображения часов, а это означает, что анимация APNG должна быть очень большой. Если есть десятки кадров, это еще более невообразимо. Анимация загрузки страницы очень медленная, но она плохо воспринимается пользователем, а анимация не имеет смысла.
Но команда APNG также знает об этой проблеме, поэтому они также занимаются оптимизацией кадров:
Как показано в приведенных выше 4 кадрах, видно, что часть набора номера можно использовать повторно, поэтому перед созданием APNG APNG вычислит разницу между кадрами с помощью алгоритма и сохранит разницу только перед кадром, вместо сохранения полного Рамка. Как показано ниже, рамки 2, 3 и 4 не имеют циферблата.
Оптимизированный размер APNG выглядит следующим образом: видно, что данные 2-го, 3-го и 4-го кадров намного меньше, чем данные первого кадра.
Но вот вопрос, как нарисовать кадры 2, 3 и 4? Как узнать, какие элементы использовать повторно? Ответ на этот вопрос будет позже.
3. Анализ исходного кода apng-canvas
Обычно мы используем APNG следующим образом, что очень просто:
<img src="xxx.png" />
но использовать напрямуюimg
Есть 2 проблемы с тегами:
- проблемы совместимости,APNG-совместимостьВ настоящее время это все еще нормально, в зависимости от степени совместимости, которую хочет использовать каждая компания.
- Очень большая яма.При предварительном просмотре APNG в Safari для iOS (Safari для macOS нормально) количество циклов анимации такое же, как и исходное изображение.
loop
+1. Например APNG имеет 10 кадров,loop
равно 2, то в цикле будет отображаться всего 30 кадров. Это отстой, если наша анимация хочет воспроизвести только один раз.
Обычно мы рекомендуем использоватьapng-canvasэта библиотека. Для работы библиотеки требуется следующая поддержка:
Далее давайте посмотрим, как библиотека apng-canvas реализует обычное воспроизведение APNG, которое в основном делится на 3 шага:
- Проанализируйте формат данных APNG (в соответствии с форматом изображения APNG в разделе 1.2).
- Разбирает хорошую сортировку данных APNG.
- В соответствии с интервалом времени каждого кадра, черезrequestAnimationFrameрисовать каждый кадр.
исходный кодapng-canvas/srcСтруктура каталогов следующая:
├─animation.js // APNG动画逻辑
├─crc32.js // 解码运算相关
├─loader.js //APNG下载
├─main.js // 入口
├─parser.js // 解码
├─support-test.js // 兼容性检查
3.1 Разбор формата данных APNG
Процесс декодирования выглядит следующим образом:
Файлы APNG, загруженныеXMLHttpRequest
скачать, см. ниже/src/loader.js, без объяснения причин.
Логика декодирования в основном в/src/parser.js, сначала преобразуйте APNG вarraybuffer
Загрузите ресурсы в формате и проверьте, является ли формат файла PNG и APNG, оперируя двоичными данными.
Проверьте, что формат PNG проверенPNG Signature
Блок, упомянутый в разделе 1.1 формата данных PNG, ключевая реализация которого выглядит следующим образом:
const bufferBytes = new Uint8Array(buffer);
const PNG_SIGNATURE_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
for (let i = 0; i < PNG_SIGNATURE_BYTES.length; i++) {
if (PNG_SIGNATURE_BYTES[i] !== bufferBytes[i]) {
reject('Not a PNG file (invalid file signature)');
return;
}
}
Проверка формата APNG заключается в том, чтобы определить, существует ли файл с типомacTL
, как указано в подразделе 1.2 Формат данных APNG. Прочитайте каждый блок в файле последовательно и получите такие данные, как тип блока.Код решения выглядит следующим образом:
let isAnimated = false;
parseChunks(bufferBytes, (type) => {
if (type === 'acTL') {
isAnimated = true;
return false;
}
return true;
});
if (!isAnimated) {
reject('Not an animated PNG');
return;
}
Процесс декодирования и сортировки каждого кадра данных показан в следующем коде. передачаparseChunks
Прочитайте каждый блок по очереди, обработайте и сохраните его в соответствии с данными, шириной, высотой, соответствующей позицией и размером байта, содержащимися в каждом типе блока.
let preDataParts = [], // 存储 其他辅助块
postDataParts = [], // 存储 IEND块
headerDataBytes = null; // 存储 IHDR块
const anim = anim = new Animation();
let frame = null; // 存储 每一帧
parseChunks(bufferBytes, (type, bytes, off, length) => {
let delayN,
delayD;
switch (type) {
case 'IHDR':
headerDataBytes = bytes.subarray(off + 8, off + 8 + length);
anim.width = readDWord(bytes, off + 8); // 画布宽
anim.height = readDWord(bytes, off + 12); // 画布高
break;
case 'acTL':
anim.numPlays = readDWord(bytes, off + 8 + 4); // 循环次数
break;
case 'fcTL':
if (frame) anim.frames.push(frame); // 上一帧数据
frame = {}; // 新的一帧
frame.width = readDWord(bytes, off + 8 + 4); // 当前帧的宽度
frame.height = readDWord(bytes, off + 8 + 8); // 当前帧的高度
frame.left = readDWord(bytes, off + 8 + 12); // 距离画布左侧位置
frame.top = readDWord(bytes, off + 8 + 16); // 距离画布顶部位置
delayN = readWord(bytes, off + 8 + 20);
delayD = readWord(bytes, off + 8 + 22);
if (delayD === 0) delayD = 100;
frame.delay = 1000 * delayN / delayD; // 当前帧播放时长
anim.playTime += frame.delay; // 累加播放总时长
frame.disposeOp = readByte(bytes, off + 8 + 24);
frame.blendOp = readByte(bytes, off + 8 + 25);
frame.dataParts = [];
break;
case 'fdAT':
// 图像数据
if (frame) frame.dataParts.push(bytes.subarray(off + 8 + 4, off + 8 + length));
break;
case 'IDAT':
// 图像数据
if (frame) frame.dataParts.push(bytes.subarray(off + 8, off + 8 + length));
break;
case 'IEND':
postDataParts.push(subBuffer(bytes, off, 12 + length));
break;
default:
preDataParts.push(subBuffer(bytes, off, 12 + length));
}
});
if (frame) anim.frames.push(frame); // 依次存储每一帧帧数据
После обработки ширины, высоты, положения, времени воспроизведения и т. д. каждого кадра изображения выше обрабатываются данные кадра каждого кадра.dataParts
составить ресурс изображения PNG по порядку, черезcreateObjectURL
URL созданного изображения сохраняется в кадре для последующего рисования. Код здесь опущен, если вы заинтересованы в просмотре исходного кода самостоятельно.
const url = URL.createObjectURL(new Blob(bb, { type: 'image/png' }));
frame.img = document.createElement('img');
frame.img.src = url;
frame.img.onload = function () {
URL.revokeObjectURL(this.src);
createdImages++;
if (createdImages === anim.frames.length) { //全部解码完成
resolve(anim);
}
};
Расшифровка этого произведения довольно скучна, если вы хотите узнать больше, вы можете прочитать эту статью в колонке @Netease Cloud Music~декодирование APNG, Автор в основном берет всех для разъяснения идеи.
3.2 Организация проанализированных данных APNG
Из раздела 3.1 видно, что проанализированные данные хранятся последовательно вanim.frames
В а, вышеупомянутыйЧасыРезультаты анализа случая следующие:
anim.frames =
[
// 第1帧
{
blendOp: 0
delay: 1000 // 每一帧持续时间
disposeOp: 0
height: 150 // 高度
img: img // 当前帧的图片数据
left: 0 // 距离画布左侧位置
top: 0 // 距离画布顶部位置
width: 150 // 宽度
},
// 第2帧
{
blendOp: 1
delay: 1000
disposeOp: 0
height: 58
img: img
left: 46
top: 31
width: 73
},
// 第3帧
{
blendOp: 1
delay: 1000
disposeOp: 2
height: 66
img: img
left: 46
top: 53
width: 73
},
// 第4帧
{
blendOp: 1
delay: 1000
disposeOp: 0
height: 30
img: img
left: 31
top: 53
width: 89
}
]
Приведенные выше 4 кадра данных соответствуют следующим 4 изображениям соответственно Как упоминалось ранее, это оптимизированный эффект:
Также видно, что только первый кадрwidth
,height
,left
,top
Относительно завершено, кадры 2, 3 и 4width
,height
,left
,top
Все разные, потому что они оптимизированы алгоритмом.
ТакblendOp
а такжеdisposeOp
Что представляют собой поля? Видно, что у автора нет комментариев Эти два поля упоминаются в разделе 2 выше [Как рисовать кадры 2, 3 и 4? Как узнать, какие элементы использовать повторно? 】Ответ. Как конкретно с этим справиться, ответим в следующем разделе рисунка.
3.3 Нарисуйте каждый кадр
APNG рисуется, в основном, черезrequestAnimationFrame
Продолжай звонитьrenderFrame
Метод рисует каждый кадр, а изображение, ширина, высота и положение каждого кадра получаются в предыдущем разделе.requestAnimationFrame
60 кадров в секунду при нормальных условиях (каждые 16,7 мс или около того), упомянутые в предыдущем разделе.playTime
Это поле представляет собой время отрисовки каждого кадра. Итак, неrequestAnimationFrame
будет идти рисовать каждый раз, но поplayTime
рассчитатьnextRenderTime
(следующее время рисования), а затем рисовать, когда это время будет достигнуто. Избегайте бесполезного рисования, которое влияет на производительность. код показывает, как показано ниже:
const renderFrame = function (now) {
if (nextRenderTime === 0) nextRenderTime = now;
while (now > nextRenderTime + ani.playTime) nextRenderTime += ani.playTime;
nextRenderTime += frame.delay;
};
const tick = function (now) {
while (played && nextRenderTime <= now) renderFrame(now);
if (played) requestAnimationFrame(tick);
};
Конкретный рисунок реализуется с помощью Canvas 2D API.
const renderFrame = function (now) {
const f = fNum++ % ani.frames.length;
const frame = ani.frames[f];
if (prevF && prevF.disposeOp === 1) { // 清空上一帧区域的底图
ctx.clearRect(prevF.left, prevF.top, prevF.width, prevF.height);
} else if (prevF && prevF.disposeOp === 2) { // 恢复为上一帧绘制之前的底图
ctx.putImageData(prevF.iData, prevF.left, prevF.top);
} // 0 则直接绘制
const {
left, top, width, height,
img, disposeOp, blendOp
} = frame;
prevF = frame;
prevF.iData = null;
if (disposeOp === 2) { // 存储当前的绘制底图,用于下一帧绘制前恢复该数据
prevF.iData = ctx.getImageData(left, top, width, height);
}
if (blendOp === 0) { // 清空当前帧区域的底图
ctx.clearRect(left, top, width, height);
}
ctx.drawImage(img, left, top); // 绘制当前帧图片
// 下一帧的绘制时间
if (nextRenderTime === 0) nextRenderTime = now;
nextRenderTime += frame.delay; // delay为帧间隔时间
};
Из приведенного выше кода рисования мы можем видетьblendOp
а такжеdisposeOp
2 определяют, следует ли повторно использовать данные нарисованного кадра. Информация о параметрах конфигурации, соответствующая этим двум полям, выглядит следующим образом:
-
disposeOp
Определяет операцию над буфером перед отрисовкой следующего кадра.- 0: не очищать холст и отображать новые данные изображения напрямую в области, указанной холстом.
- 1: очистить холст в области текущего кадра перед рендерингом следующего кадра с цветом фона по умолчанию.
- 2: Перед рендерингом следующего кадра восстановить текущую область кадра холста до результата отрисовки предыдущего кадра
-
blendOp
Определяет операцию над буфером перед отрисовкой текущего кадра- 0: Указывает, что текущая область очищается, а затем рисуется
- 1: Указывает, что текущая область рисуется напрямую без очистки, а изображение накладывается
Процесс рисования соответствующих часов 4 кадра выглядит следующим образом:
-
Первый кадр:
- blendOp: 0 Перед рисованием текущего кадра очистите текущую область, а затем рисуйте
- disposeOp: 0 не очищает холст, а напрямую отображает новые данные изображения в области, указанной холстом.
-
Второй кадр:
- blendOp: 1 Перед отрисовкой текущего кадра означает, что текущая область рисуется напрямую без очистки, а изображение накладывается
- disposeOp: 0 не очищает холст, а напрямую отображает новые данные изображения в области, указанной холстом.
-
Третий кадр:
- blendOp: 1 Перед отрисовкой текущего кадра означает, что текущая область рисуется напрямую без очистки, а изображение накладывается
- disposeOp: 2 Перед рендерингом следующего кадра восстановить текущую область кадра холста до результата отрисовки предыдущего кадра (т.к. четвертая картинка перекрывает красную линию второй картинки, поэтому третью картинку надо вернуть после перемещения к кадру 2)
-
Четвертый кадр:
- blendOp: 1 Перед отрисовкой текущего кадра означает, что текущая область рисуется напрямую без очистки, а изображение накладывается
- disposeOp: 0 не очищает холст, а напрямую отображает новые данные изображения в области, указанной холстом.
слишком далекоapng-canvasПроцесс рисования завершен, и заинтересованные студенты могут больше поразмышлять над исходным кодом~
4. Проверка совместимости APNG
Как определить, достаточно ли браузер поддерживает APNG в практическом применении, можно использовать следующие методы:
(function() {
"use strict";
var apngTest = new Image(),
ctx = document.createElement("canvas").getContext("2d");
apngTest.onload = function () {
ctx.drawImage(apngTest, 0, 0);
self.APNG = ( ctx.getImageData(0, 0, 1, 1).data[3] === 0 );
};
apngTest.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAAcMq2TYAAAANSURBVAiZY2BgYPgPAAEEAQB9ssjfAAAAGmZjVEwAAAAAAAAAAQAAAAEAAAAAAAAAAAD6A+gBAbNU+2sAAAARZmRBVAAAAAEImWNgYGBgAAAABQAB6MzFdgAAAABJRU5ErkJggg==";
// frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255]
// frame 2: [0, 0, 0, 0]
}());
-
Загрузите изображение в кодировке Base64 размером 1 x 1 пиксель. Изображение имеет 2 кадра данных. Разница в том, что последнее значение каждого кадра отличается.
// frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255] // frame 2: [0, 0, 0, 0]
-
нарисовать его на холсте,getImageData()метод получения данных о пикселях изображения, в основном для получения
data[3]
Канал альфа-прозрачности (диапазон значений: 0 - 255). В браузерах, не поддерживающих APNG, будет отображаться только первый кадр, поэтомуdata[3]
будет равно 255. Кадр 2 в конечном итоге будет отображаться в браузерах, поддерживающих APNG, поэтомуdata[3]
будет равен 0, что означает, что APNG поддерживается.
5. Резюме
-
Эта статья знакомит с использованием, производительностью, степпингом, совместимостью и обнаружением,apng-canvasАнализ исходного кода библиотеки, в основном для обобщения личного обучения автора.
-
При фактическом использовании, поскольку Safari для iOS
loop
будет автоматически+1
, поэтому не подходит для анимаций, которые воспроизводятся только один раз. -
Файл APNG будет очень большим для хранения многокадрейных данных, поэтому рекомендуется использовать его на относительно небольших сценах анимации. Если сцена подходит, вы также можете поставить статическое изображение внизу и заменить его после загрузки APNG, но это требует, чтобы первый кадр был отображаться статически для пользователя.
-
apng-canvasДекодирование занимает много времени, если на странице отображается анимация, это увеличивает время блокировки страницы. Автор попытался разобрать его в Web Worker, что может сэкономить около 100 мс времени.
6. Ссылки
Изображения и соответствующая информация в этой статье взяты из следующих ссылок: