Решение для веб-кадровой анимации — принцип и реализация APNG

JavaScript CSS
Решение для веб-кадровой анимации — принцип и реализация APNG

Анимационный цикл статей:

предисловие

Как передовой студент, более или менее получит потребности в анимации. Если это обычная анимация, ее относительно легко реализовать, но если это более сложная покадровая анимация, если мы используем CSS для ее реализации, очень легко вызвать следующую ситуацию: дизайнер — это шоу продавца, а разрабатываем байерское шоу.

image-20210104112052946

Может быть, вы думаете об использовании GIF для достижения этого, но GIF часто имеет разные грани, которые не могут удовлетворить требования дизайнера к сложности. Поэтому нам нужно найти больше анимационных решений, которые позволят нам восстановить 100% эскиза дизайна, обеспечивая при этом уточнение и производительность анимации. В основном автор знакомитAPNGстроить планы.

APNG (Animated Portable Network Graphics) — это формат анимации, основанный на расширении формата PNG, который добавляет поддержку анимированных изображений, а также добавляет поддержку 24-битных изображений и 8-битной альфа-прозрачности, что означает, что анимация будет иметь лучшее качество.

Во-первых, давайте посмотрим на эффект сравнения APNG и GIF:

clock.gifclock.png

Если изображение выше не двигается или чтобы увидеть больше демонстраций, смотрите напрямуюDemo1а такжеDemo2, можно обнаружить, что, хотя размеры APNG и GIF не сильно отличаются, APNG намного четче, чем GIF, и нет разных краев. Это связано с тем, что APNG поддерживает 24-битные изображения и 8-битную альфа-прозрачность. Далее давайте рассмотрим основные принципы и способы использования APNG.

1. Формат данных APNG

1.1 PNG

Перед просмотром формата данных APNG вы должны сначала понять формат данных PNG, ведь APNG основан на расширении формата PNG. Формат данных PNG следующий:

image-20210105200622328

В основном делится на 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 изображениями часов.

image-20210105201547679

Эффект:clock_apng.png(Если вы не двигаетесь, просто посмотрите приведенную выше демонстрацию напрямую)

2. Производительность

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

Но команда APNG также знает об этой проблеме, поэтому они также занимаются оптимизацией кадров:

image-20210105202906640

Как показано в приведенных выше 4 кадрах, видно, что часть набора номера можно использовать повторно, поэтому перед созданием APNG APNG вычислит разницу между кадрами с помощью алгоритма и сохранит разницу только перед кадром, вместо сохранения полного Рамка. Как показано ниже, рамки 2, 3 и 4 не имеют циферблата.

image-20210105202959107

Оптимизированный размер APNG выглядит следующим образом: видно, что данные 2-го, 3-го и 4-го кадров намного меньше, чем данные первого кадра.

image-20210105203025743

Но вот вопрос, как нарисовать кадры 2, 3 и 4? Как узнать, какие элементы использовать повторно? Ответ на этот вопрос будет позже.

3. Анализ исходного кода apng-canvas

Обычно мы используем APNG следующим образом, что очень просто:

<img src="xxx.png" />

но использовать напрямуюimgЕсть 2 проблемы с тегами:

  1. проблемы совместимости,APNG-совместимостьВ настоящее время это все еще нормально, в зависимости от степени совместимости, которую хочет использовать каждая компания.
  2. Очень большая яма.При предварительном просмотре APNG в Safari для iOS (Safari для macOS нормально) количество циклов анимации такое же, как и исходное изображение.loop+1. Например APNG имеет 10 кадров,loopравно 2, то в цикле будет отображаться всего 30 кадров. Это отстой, если наша анимация хочет воспроизвести только один раз.

Обычно мы рекомендуем использоватьapng-canvasэта библиотека. Для работы библиотеки требуется следующая поддержка:

Далее давайте посмотрим, как библиотека apng-canvas реализует обычное воспроизведение APNG, которое в основном делится на 3 шага:

  1. Проанализируйте формат данных APNG (в соответствии с форматом изображения APNG в разделе 1.2).
  2. Разбирает хорошую сортировку данных APNG.
  3. В соответствии с интервалом времени каждого кадра, черезrequestAnimationFrameрисовать каждый кадр.

исходный кодapng-canvas/srcСтруктура каталогов следующая:

├─animation.js // APNG动画逻辑
├─crc32.js // 解码运算相关
├─loader.js //APNG下载
├─main.js // 入口
├─parser.js // 解码
├─support-test.js // 兼容性检查

3.1 Разбор формата данных APNG

Процесс декодирования выглядит следующим образом:

img

Файлы 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 по порядку, черезcreateObjectURLURL созданного изображения сохраняется в кадре для последующего рисования. Код здесь опущен, если вы заинтересованы в просмотре исходного кода самостоятельно.

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 изображениям соответственно Как упоминалось ранее, это оптимизированный эффект:

image-20210105202959107

Также видно, что только первый кадрwidth,height,left,topОтносительно завершено, кадры 2, 3 и 4width,height,left,topВсе разные, потому что они оптимизированы алгоритмом.

ТакblendOpа такжеdisposeOpЧто представляют собой поля? Видно, что у автора нет комментариев Эти два поля упоминаются в разделе 2 выше [Как рисовать кадры 2, 3 и 4? Как узнать, какие элементы использовать повторно? 】Ответ. Как конкретно с этим справиться, ответим в следующем разделе рисунка.

3.3 Нарисуйте каждый кадр

APNG рисуется, в основном, черезrequestAnimationFrameПродолжай звонитьrenderFrameМетод рисует каждый кадр, а изображение, ширина, высота и положение каждого кадра получаются в предыдущем разделе.requestAnimationFrame60 кадров в секунду при нормальных условиях (каждые 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а такжеdisposeOp2 определяют, следует ли повторно использовать данные нарисованного кадра. Информация о параметрах конфигурации, соответствующая этим двум полям, выглядит следующим образом:

  • 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]
}());
  1. Загрузите изображение в кодировке Base64 размером 1 x 1 пиксель. Изображение имеет 2 кадра данных. Разница в том, что последнее значение каждого кадра отличается.

    // frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255]
    // frame 2: [0, 0, 0, 0]
    
  2. нарисовать его на холсте,getImageData()метод получения данных о пикселях изображения, в основном для полученияdata[3]Канал альфа-прозрачности (диапазон значений: 0 - 255). В браузерах, не поддерживающих APNG, будет отображаться только первый кадр, поэтомуdata[3]будет равно 255. Кадр 2 в конечном итоге будет отображаться в браузерах, поддерживающих APNG, поэтомуdata[3]будет равен 0, что означает, что APNG поддерживается.

5. Резюме

  1. Эта статья знакомит с использованием, производительностью, степпингом, совместимостью и обнаружением,apng-canvasАнализ исходного кода библиотеки, в основном для обобщения личного обучения автора.

  2. При фактическом использовании, поскольку Safari для iOSloopбудет автоматически+1, поэтому не подходит для анимаций, которые воспроизводятся только один раз.

  3. Файл APNG будет очень большим для хранения многокадрейных данных, поэтому рекомендуется использовать его на относительно небольших сценах анимации. Если сцена подходит, вы также можете поставить статическое изображение внизу и заменить его после загрузки APNG, но это требует, чтобы первый кадр был отображаться статически для пользователя.

  4. apng-canvasДекодирование занимает много времени, если на странице отображается анимация, это увеличивает время блокировки страницы. Автор попытался разобрать его в Web Worker, что может сэкономить около 100 мс времени.

image-20210106203526901

6. Ссылки

Изображения и соответствующая информация в этой статье взяты из следующих ссылок: