一、pag介绍
pag官网定义:完整的动效工作流解决方案;目标是降低或消除动效相关的研发成本,能够一键将设计师在 AE(Adobe After Effects)中制作的动效内容导出成素材文件,并快速上线应用于几乎所有的主流平台。
1.1 特性:
- ⾼效⽂件格式:⼆进制流存储、极限压缩策略。 采用可扩展的二进制文件格式,可单文件集成图片音频等资源,实现快速交付。导出相同的 AE 动效内容,在文件解码速度和压缩率上均大幅领先于同类型方案。
- BMP 预合成:支持ae全特性导出,充分发挥设计师的创造力。 在纯矢量导出方式上支持更多 AE 特性的同时,还引入了BMP预合成结合矢量的混合导出能力,实现支持所有 AE 特性的同时又能保持动效运行时的可编辑性
| 纯BMP导出(原理和afx类似) | BMP预合成 |
|---|---|
- 完善的桌面工具:ae导出插件、物料预览工具。 提供从「导出插件」到「桌面预览」等一系列完善的桌面效率工具,让设计师可以所见即所得地生产素材,研发无需介入还原效果,极大降低了设计与研发的对接成本;安装ae插件、导出pag物料教程、安装pagPreview。
- 运⾏时编辑:占位替换、拼装组合、图层套壳。 运行时,可在保留动效效果前提下,动态修改替换文本或替换占位图内容,甚至对任意子图层进行增删改及移动,轻松实现照片和视频模板等素材的批量化生产。
- 全新绘图引擎:TGFX、包体优化策略
1.2 动效方案对比:
1.2.1 整体对比
| 方案 | pag | lottie | svga | afx |
|---|---|---|---|---|
| 诞⽣场景 | 视频编辑 | UI动画 | 直播礼物 | 动画彩蛋 |
| ⽂件格式 | ⾼压缩⼆进制编码,⽂件最⼩,解码最快 | JSON格式⽂本,⽂件较⼤,解码较慢 | PB序列化格式,解码快,⽂件较⼤ | Mp4 视频,资源体积和pag差不多 |
| 渲染架构 | 跨平台⼀致的C++架构,可任意⼦线程渲染 | 依赖平台相关渲染接⼝,各端能⼒不⼀致 | 依赖平台相关渲染接⼝ | 依赖平台相关渲染接⼝,各端能⼒不⼀致 |
| AE特性⽀持 | 混合导出能⼒⽀持所有AE特性导出 | 仅⽀持有限的⽮量特性导出 | 仅⽀持有限的AE特性,复杂图形⽀持不佳 | 视频序列帧,支持所有AE特性,视觉展示细腻 |
| 运⾏时编辑 | 完整的⽂本和占位图编辑以及图层编辑 | 有限的⽂本和占位图编辑 | 有限的⽂本和占位图编辑 | 不支持 |
| 是否依赖动效sdk | 是 | 是 | 是 | 否 |
1.2.2 物料体积
悬浮球物料举例:svga>afx>pag,相同画质情况,pag体积小于afx
| svga | afx | pag |
|---|---|---|
| 2.svga | 悬浮器afx.mp4 | 1.pag |
彩蛋物料举例:彩蛋使用较多特效插件和AE特性,pag纯BMP(视频序列帧)预合成导出,物料体积较大,和afx体积接近,但也比afx略小。
| svga | afx | pag |
|---|---|---|
| 不支持大部分AE特性和特效插件 | 754k.mp4 | 665k.pag |
1.2.3 色彩差异
左边afx色彩较淡,pag色彩完还原设计稿色彩。
二、接入方案
- 主模板添加异步依赖配置
- 阿拉丁卡异步引入
// PAG依赖加载
getPAG() {
if (this.PAG) {
return Promise.resolve(this.PAG);
}
return new Promise((resolve, reject) => {
(window as any).require(['dep/libpag'], ({PAGInit}) => {
PAGInit({
// 指定lipag.wasm资源地址
locateFile: () => 'https://search-operate.cdn.bcebos.com/test/ea89c95c674842599c9ad28056016bc6.wasm'https://search-operate.cdn.bcebos.com/test/ea89c95c674842599c9ad28056016bc6.wasm locateFile: () => 'https://search-operate.cdn.bcebos.com/test/ea89c95c674842599c9ad28056016bc6.wasm'
}).then(async PAG => {
this.PAG = PAG;
resolve(PAG);
});
});
});
}
// pag物料加载
getPagFile() {
if (this.pagFile) {
return Promise.resolve(this.pagFile);
}
return new Promise(async (resolve, reject) => {
const PAG = await this.getPAG();
// 加载pag物料
const arrayBuffer = await customFetch(this.data.get('src')).then(res => {
return res.arrayBuffer();
});
// pag物料解码
const pagFile = await PAG.PAGFile.load(arrayBuffer);
const ratio = pagFile.height() / pagFile.width();
const canvasId = this.data.get('pagId');
// pag sdk默认会设置canvas宽高
document.getElementById(canvasId).width = pagFile.width();
document.getElementById(canvasId).height = pagFile.height();
// 前端自定义等比缩放canvas至父容器宽度
this.nextTick(() => {
document.getElementById(canvasId).style.width = '100%';
document.getElementById(canvasId).style.height = 100 * ratio + '%';
});
this.pagFile = pagFile;
resolve(pagFile);
});
}
// 创建pagView
getPagView() {
if (this.pagView) {
return Promise.resolve(this.pagView);
}
return new Promise(async (resolve, reject) => {
const PAG = await this.getPAG();
const pagFile = await this.getPagFile();
// pagView初始化
const pagView = await PAG.PAGView.init(pagFile, `#${this.data.get('pagId')}`);
pagView.setRepeatCount(this.data.get('pagId'));
// 监听动画开始
pagView.addListener('onAnimationStart', () => {
this.fire('start');
});
// 监听动画结束
pagView.addListener('onAnimationEnd', () => {
this.fire('end');
});
resolve(pagView);
this.pagView = pagView;
this.fire('loadSuccess');
});
}
async play() {
const pagView = await this.getPagView();
pagView.play();
}
async pause() {
const pagView = await this.getPagView();
pagView.pause();
}
async stop() {
const pagView = await this.getPagView();
pagView.stop();
}
// 创建pagImage
getPagImage(url) {
return new Promise(async (resolve, reject) => {
const PAG = await this.getPAG();
const image = new Image();
image.setAttribute('crossOrigin', 'Anonymous');
image.onload = async () => {
resolve(PAG.PAGImage.fromSource(image));
};
image.onerror = (error) => {
reject(error);
};
image.src = url;
});
}
// 替换图片
async setImage(index, imgUrl) {
const pagImage = await this.getPagImage(imgUrl);
const pagFile = await this.getPagFile();
pagFile.replaceImage(index, pagImage);
}
// 替换文本,详细文本参数:https://pag.art/apis/web/classes/types.TextDocument.html#applyFillhttps://pag.art/apis/web/classes/types.TextDocument.html#applyFill
async setText(index, text: {
text: string;
fontSize: number;
fontStyle: string;
fontFamily: string;
fillColor: string;
backgroundColor: string;
backgroundAlpha: number;
}) {
const pagFile = await this.getPagFile();
const PAG = await this.getPAG();
// 拿到当前原始文本数据,在原始文本数据上修改
let textDocument = pagFile.getTextData(index);
textDocument = Object.assign(textDocument, text);
pagFile.replaceText(index, textDocument);
}
- pagView api:pag.art/apis/web/cl…
- demo示例:yq01-ecom-nova07115436f.yq01.baidu.com:8081/activity/pa…
三、性能、动画稳定性
3.1 依赖资源(目前都是异步)
| 资源 | 大小 | 耗时 | 备注 |
|---|---|---|---|
| libpag.js | 70.8k | | libpag sdk 代理层,暴露主要的api |
| libpag.wasm | 3.1MB | 395ms | libpag sdk 核心层,引入 libpag.js 后调用 PAGInit() 接口进行实例化时候,加载libpag.wasm,默认加载同级目录的libpag.wasm,可以使用 PAGInit() 上的钩子 locateFile 去指定 libpag.wasm 的路径。 |
| 动画物料 | —— | | |
备注:
- 可确定的依赖里面,libpag.wasm文件是最大的,wasm 文件是可以 GZIP 的,公共 CDN 默认启用了 wasm 文件的 GZIP,如果是自己的静态资源服务需要手动配置。
- 目前bos服务不支持gzip压缩(尝试手动gzip压缩后是3MB -> 900k),百度云部门反馈预计4月底支持(3.22询问测试中);
官方pag wasm文件(支持gzip):cdn.jsdelivr.net/npm/libpag@…,860k,网络请求耗时:
| 无异常:270ms | |
|---|---|
| 链接cdn服务超长:1.79s | |
| 服务响应超长:694ms | |
| 协议通信期间发生了协议错误:12.04s |
以下是一些可能导致 ERR_QUIC_PROTOCOL_ERROR 错误的原因:
- 网络问题:可能是由于网络连接不稳定或网络延迟导致的错误。在某些情况下,QUIC 协议可能对网络环境敏感。
- 服务器配置问题:可能是由于服务器端的配置或设置问题导致的。服务器可能没有正确地支持 QUIC 协议,或者与客户端的通信存在问题。
- 防火墙或代理问题:防火墙或代理可能会干扰 QUIC 协议的通信,导致错误发生。
- 浏览器问题:可能是由于浏览器本身的问题或 bug 导致的。尝试更新浏览器版本或使用其他浏览器可能会解决问题。
3.2 动画起播时间(卡片attach -> 开始播放):
| | afx 方案 | pag | page-lite |
|---|---|---|---|
| FCP指标说明:developer.chrome.com/docs/lighth… | 5.3 szhengmengmei.bcc-bdbl.baidu.com_2024-05-16_20-31-01.report.html | 5.3 szhengmengmei.bcc-bdbl.baidu.com_2024-05-16_20-31-23.report.html | 5.3s |
| TBT指标说明:developer.chrome.com/docs/lighth… | 100ms | 510ms | 220ms |
| 首次进入播放动画时间(app清理所有数据首次进入)起始时间:AFX/PAG逻辑开始执行结束时间:动画开始播放 | IOS平均:2.4s安卓平均:2.8s | IOS平均:3.8s安卓平均:4.7s | IOS平均:1.7s安卓平均:2.4s,偶现清除数据冷启后无法正常播放,目前看跟手百安卓端框架的支持度有关: github.com/Tencent/lib… |
| 首次进入播放动画时间(已经进入过该页面重新进) | IOS平均:1.7s安卓平均:1.8s | IOS平均:1.7s安卓平均:2.2s | IOS平均:0.86s安卓平均:1.13s |
| 播放第二个动画(afx/pag素材体积相似) | IOS平均:3.1s安卓平均:1.9s | IOS平均:1.1s安卓平均:1.5s | - |
| | | | |
|---|---|---|---|
| 物料\动画方案 | svga | afx | pag(bos平台cdn不支持gzip)预计支持gzip后能优化240ms |
| 悬浮球svga:32kafx:174kpag:7k | 1. 468 'svga首帧渲染时长' |
-
753 'svga首帧渲染时长'
-
449 'svga首帧渲染时长'
-
572 'svga首帧渲染时长'
-
596 'svga首帧渲染时长'
-
637 'svga首帧渲染时长'
-
739 'svga首帧渲染时长'
-
519 'svga首帧渲染时长'
-
795 'svga首帧渲染时长'
-
481 'svga首帧渲染时长' | 1. 357 'afx首帧渲染时长'
-
445 'afx首帧渲染时长'
-
533 'afx首帧渲染时长'
-
424 'afx首帧渲染时长'
-
583 'afx首帧渲染时长'
-
464 'afx首帧渲染时长'
-
379 'afx首帧渲染时长'
-
483 'afx首帧渲染时长'
-
457 'afx首帧渲染时长'
-
529 'afx首帧渲染时长' | 1. 1437 'pag首帧渲染时长'
-
1783 'pag首帧渲染时长'
-
1501 'pag首帧渲染时长'
-
2051 'pag首帧渲染时长'
-
1702 'pag首帧渲染时长'
-
1826 'pag首帧渲染时长'
-
2071 'pag首帧渲染时长'
-
1929 'pag首帧渲染时长'
-
1905 'pag首帧渲染时长'
-
1777 'pag首帧渲染时长' | | 彩蛋afx:754kpag:665k | | 1. 600 'afx首帧渲染时长'
-
742 'afx首帧渲染时长'
-
444 'afx首帧渲染时长'
-
453 'afx首帧渲染时长'
-
426 'afx首帧渲染时长'
-
396 'afx首帧渲染时长'
-
530 'afx首帧渲染时长'
-
539 'afx首帧渲染时长'
-
603 'afx首帧渲染时长'
-
773 'afx首帧渲染时长' | 1. 1974 'pag首帧渲染时长'
-
1742 'pag首帧渲染时长'
-
1847 'pag首帧渲染时长'
-
1588 'pag首帧渲染时长'
-
1753 'pag首帧渲染时长'
-
1667 'pag首帧渲染时长'
-
1775 'pag首帧渲染时长'
-
1799 'pag首帧渲染时长'
-
1537 'pag首帧渲染时长'
-
1431 'pag首帧渲染时长'1918:153(js)+76(pag)+800 (wasm)+other |
3.3 动画稳定性
cpu占用较afx相比更高,wasm编解码以及处理canvas播放逻辑可能导致js线程的占用;动画稳定性较好,中低端机支持较为良好,无导致崩溃;
| afx | pag |
|---|---|
| 5.4% | 11.8% |
四、调研总结
pag首次播放速度较慢,在能接受起播速度较慢情况下,可以使用;
- 动画简单、尺寸较小且有编辑场景,使用svga;
- 动画效果丰富且无编辑场景,使用afx;
- 动画效果丰富、尺寸较大,且有编辑场景,使用pag;
参考文档
pag官网
afx介绍