最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Web 帧动画解决方案 - APNG原理与实现

    正文概述 掘金(青舟同学)   2021-01-07   699

    前言

    作为前端同学,或多或少都会接到动画需求。如果是有规律性的动画还是相对容易实现的,但如果是比较复杂的帧动画,我们用 CSS 实现的话,就非常容易造成如下情况,设计师是卖家秀,我们开发的是买家秀。

    Web 帧动画解决方案 - APNG原理与实现

    或许你会想到用 GIF 实现,但是 GIF 经常会有杂边,无法满足设计师对精致度的要求。所以我们需要寻找更多的动画方案,能够让我们 100% 还原设计稿,又保证动画的精致度和性能。本文笔者主要是介绍的是 APNG 方案。

    APNG(Animated Portable Network Graphics)是基于 PNG 格式扩展的一种动画格式,增加了对动画图像的支持,同时加入了 24 位图像和 8 位 Alpha 透明度的支持,这意味着动画将拥有更好的质量。

    首先来看下 APNG 和 GIF 的对比效果:

    Web 帧动画解决方案 - APNG原理与实现Web 帧动画解决方案 - APNG原理与实现

    上面的图不动的话,或者查看更多 Demo 请直接看 Demo1 和 Demo2,可以发现 APNG 和 GIF 的大小虽然相差不大,但是 APNG 要比 GIF 清晰的多,并且没有杂边。这是因为 APNG 拥有 24 位图像和 8 位 Alpha 透明度的支持。接下来一起看看 APNG 的主要原理和使用吧。

    1. APNG 数据格式

    1.1 PNG

    在查看 APNG 数据格式前,要先了解下 PNG 的数据格式,毕竟 APNG 是基于 PNG 格式扩展的。PNG 的数据格式如下:

    Web 帧动画解决方案 - APNG原理与实现

    主要分为 4 部分:

    • PNG Signature 是文件标识,用于校验文件格式是否为 PNG。内容固定为:

      0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
      // 这里为下文打下基础,通过校验前8个字节是否为这个内容,判断是否为png
      
    • IHDR 是文件头数据块,包含 PNG 图像的基本信息,例如图像的宽高等信息

    • IDAT 是图像数据块,最核心,存储具体的图像数据

    • IEND 是结束数据块,标示图像结束

    1.2 APNG

    在了解 PNG 的数据格式后,再来看下 APNG 的数据格式。如下图所示:

    Web 帧动画解决方案 - APNG原理与实现

    可以看到,APNG 在 PNG 的基础上增加了 acTL、fcTL 和 fdAT 3 种模块

    • acTL:必须在第一个 IDAT 块之前,用于告诉解析器这是一个动画格式的 PNG,包含动画帧总数和循环次数的信息,意味着可以通过这个字段来判断是否为 APNG 的图像格式

    • fcTL:帧控制块,每一帧都必须有的,属于 PNG 规范中的辅助块,包含了当前帧的序列号、图像的宽高。

    • fdAT:帧数据块,和 IDAT 意义相同,都是图像数据。但是比 IDAT 多了帧的序列号,因为动画存在多帧。图中可以看到第一帧的图像数据依然叫做 IDAT,第 2 帧以后才叫 fdAT,这是因为第一帧和 PNG 数据的格式保持相同。在不支持 APNG 的浏览器上,可以降级为静态图片,只展示第一帧。

    为了更好的理解 APNG 数据格式,感兴趣的同学可以通过下方 APNGb 这个软件,自己生成 APNG 动画。下面的 DEMO 是用 4 张时钟图片生成。

    Web 帧动画解决方案 - APNG原理与实现

    效果:Web 帧动画解决方案 - APNG原理与实现(不动的话就直接看上述 Demo)

    2. 性能

    学习完 APNG 的数据格式,以及通过上面的 Demo 我们可以发现,一个时钟动画存储了 4 帧时钟图像数据,意味着一张 APNG 动画必然很大。如果有几十帧,那更是不敢想象了。页面加载动画很慢,反而造成不好的用户体验,那动画也没什么存在的意义了。

    但是 APNG 的团队也意识到这个问题,因此也会进行帧优化:

    Web 帧动画解决方案 - APNG原理与实现

    如上这 4 帧,可以看出表盘部分是可以复用的,因此在生成 APNG 前,APNG 会通过算法计算帧之间的差异,只存储帧之前的差异,而不是存储全帧。如下,第 2、3、4 帧都没有表盘部分了。

    Web 帧动画解决方案 - APNG原理与实现

    优化后的 APNG 大小如下,可以看出第 2、3、4 帧数据要比第一帧小了很多。

    Web 帧动画解决方案 - APNG原理与实现

    但是这里有个问题便是,第 2、3、4 帧如何绘制呢?如何知道复用哪些元素呢?这个问题会在后面解答。

    3. apng-canvas 源码分析

    平时我们使用 APNG 方式如下,非常简单:

    <img src="xxx.png" />
    

    但是直接使用 img 标签存在 2 个问题:

    1. 兼容性问题,APNG 兼容性目前来看还算可以,取决于各个公司希望的兼容程度使用。
    2. 一个非常大的坑,在 Safari for iOS(Safari for macOS正常)预览 APNG 的时候,动图的循环次数为对应原图的 loop + 1。比如 APNG 有 10帧,loop 为 2,那么会循环总计展示 30 帧。如果我们的动画只想播放一次的话,那就糟糕了。

    所以一般我们推荐 使用 apng-canvas 这个库。该库需要以下支持才能运行:

    • Canvas
    • Typed Arrays
    • Blob URLs
    • requestAnimationFrame

    接下来带大家看一下 apng-canvas 库是如何实现 APNG 正常播放的,主要分为 3 个步骤:

    1. 解析 APNG 数据格式(按照 1.2 小节的 APNG 图片格式)。
    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 数据格式

    解码流程如下:

    Web 帧动画解决方案 - 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 存储到frame中,用于后续绘制。这里代码省略,感兴趣自行查看源码。

    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);
        }
    };
    

    解码这一块比较无聊,想了解更详细的可以看@网易云音乐专栏的这篇文章哈~ 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 张图,前面提到过这是优化后的效果:

    Web 帧动画解决方案 - APNG原理与实现

    也可以看出只有第一帧的 widthheightlefttop 比较完整,第 2、3、4 帧的 widthheightlefttop 都是不同的,因为被算法优化过。

    那么 blendOpdisposeOp 字段分别代表什么呢?可以看出笔者是没有注释的,这 2 个字段就是前文第 2 节中提到的 【第 2、3、4 帧如何绘制呢?如何知道复用哪些元素呢?】问题的答案。那具体如何处理的,在下一节绘制中再来解答。

    3.3 绘制每一帧

    APNG 的绘制,主要是通过 requestAnimationFrame 不断的调用 renderFrame 方法绘制每一帧,每一帧的图像、宽高、位置我们都在上一节中获取到了。 requestAnimationFrame 在正常情况下能达到 60 fps(每隔 16.7ms 左右),在上一节中提过 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为帧间隔时间
    };
    

    从上面的绘制代码中,我们可以看到 blendOpdisposeOp 2个字段决定了是否复用绘制过的帧数据。2 个字段对应的配置参数信息如下:

    • disposeOp 指定了下一帧绘制之前对缓冲区的操作
      • 0:不清空画布,直接把新的图像数据渲染到画布指定的区域
      • 1:在渲染下一帧前将当前帧的区域内的画布清空为默认背景色
      • 2:在渲染下一帧前将画布的当前帧区域内恢复为上一帧绘制后的结果
    • blendOp 指定了绘制当前帧之前对缓冲区的操作
      • 0:表示清除当前区域再绘制
      • 1:表示不清除直接绘制当前区域,图像叠加

    对应时钟 4 帧绘制流程如下:

    • 第一帧:

      • blendOp:0 绘制当前帧之前,清除当前区域再绘制
      • disposeOp:0 不清空画布,直接把新的图像数据渲染到画布指定的区域
    • 第二帧:

      • blendOp:1 绘制当前帧之前,表示不清除直接绘制当前区域,图像叠加
      • disposeOp:0 不清空画布,直接把新的图像数据渲染到画布指定的区域
    • 第三帧:

      • blendOp:1 绘制当前帧之前,表示不清除直接绘制当前区域,图像叠加
      • disposeOp:2 渲染下一帧前将画布的当前帧区域内恢复为上一帧绘制后的结果(因为第4张图覆盖的是第二张图的红色线条,所以第三张图动完要回到第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. 加载一张 1x1 像素大小的 Base64 编码图片,图像有 2 帧数据,区分就是每一帧最后一个值不同。

      // frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255]
      // frame 2: [0, 0, 0, 0]
      
    2. 将其绘制到画布中,通过 getImageData() 方法去获取该图片的像素数据,主要是获取 data[3] 的 Alpha 透明通道(值的范围:0 - 255)。在不支持 APNG 的浏览器上会降级只显示第一帧,因此 data[3] 会等于 255。在支持 APNG 的浏览器上最终会显示第 2 帧,因此 data[3] 会等于 0,则表示支持 APNG。

    5. 总结

    1. 本文介绍了 APNG 的使用、性能、踩坑、兼容性以及检测、 apng-canvas 库源码分析,主要是对笔者个人学习进行总结。

    2. 在实际使用中,由于 Safari for iOS 中 loop 会自动 +1,所以不适合那些只播放一次的动画。

    3. APNG 文件存储多帧数据会很大,所以建议使用比较小的动画场景上。如果场景合适,也可以放一张静态图在底部,待 APNG 加载完毕后替换,不过这种需要第一帧是可以对用户静态展示的。

    4. apng-canvas 解码比较耗时,如果动画是进页面就展示的,会增加页面阻塞时间。笔者尝试过放到Web Worker 中解析,可以节省耗时 100ms 左右。

    Web 帧动画解决方案 - APNG原理与实现

    6. 参考资料

    文中图片和相关信息来自以下参考资料:

    • gist.github.com/eligrey/175…
    • wiki.mozilla.org/APNG_Specif…
    • littlesvr.ca/apng/inter-…
    • github.com/davidmz/apn…
    • Web 端 APNG 播放实现原理
    • APNG 那些事

    起源地下载网 » Web 帧动画解决方案 - APNG原理与实现

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元