当前位置: 首页 > backend >正文

2508C++,skia动画

gif动画原理

先了解一下gif动画的原理:
gif动画由一系列静态图像(或叫帧)组成.这些图像按特定的顺序排列,每一帧都代表动画中的一个瞬间,帧图像是支持透明的.

每两帧之间有指定的时间间隔(一般小于60毫秒),gif播放器每渲染一帧静态图像后,即等待此时间间隔,依此逻辑不断循环渲染每一帧,这样就是一个动画了(基于人眼的视觉暂留现象)

大部分gif动画文件是基于一个压缩算法生成的:如果前一帧中包含的一部分像素后一帧中包含的像素相同,则后一帧不必存储这些像素,以此减少文件体积.

也即,这类gif动画的第一帧是一个完整的图像,后面每一帧存储的像素都是这一帧与前一帧不同像素数据,没有相同像素数据.

这类gif动画要求播放器渲染每一帧时都是在前一帧的基础上渲染的(叠加在前一帧上面).

在窗口中播放gif动画

在窗口中播放动画的原理:每渲染一帧动画重画一次窗口.

因为gif动画帧与帧之间等待时间一般都比较短(此例动画帧间隔时间为50毫秒).所以得修改窗口的基础代码:
全局变量设置surfaceMemory,并在创建窗口成功后,即初化它指向的内存空间.

每次执行绘画方法后,不再释放surfaceMemory指向的内存空间,以避免每次重画都要重新申请内存,造不必要的CPU消耗.

改变窗口大小时,再重置surfaceMemory指向的内存空间.

全局变量设置窗口句柄,HWND hwnd,这样在渲染每一帧请求重画窗口.
具体见全部示例代码.来看一下播放gif动画的示例代码:

    //#include <thread>
SkBitmap* frameBitmap;
void animateGif()
{std::wstring imgPath = L"D:\\project\\SkiaInAction\\动画Gif\\demo.gif";auto pathStr = wideStrToStr(imgPath);std::unique_ptr<SkFILEStream> stream = SkFILEStream::Make(pathStr.data());std::unique_ptr<SkCodec> codec = SkCodec::MakeFromStream(std::move(stream));frameBitmap = new SkBitmap();auto t = std::thread([](std::unique_ptr<SkCodec> codec) {auto imgInfo = codec->getInfo().makeColorType(kN32_SkColorType);frameBitmap->allocN32Pixels(imgInfo.width(), imgInfo.height());int frameCount = codec->getFrameCount();std::vector<SkCodec::FrameInfo> frameInfo = codec->getFrameInfo();SkCodec::Options option;option.fFrameIndex = 0;option.fPriorFrame = -1;while (true){auto start = std::chrono::system_clock::now();codec->getPixels(imgInfo, frameBitmap->getPixels(), imgInfo.minRowBytes(), &option);InvalidateRect(hwnd, nullptr, false);auto end = std::chrono::system_clock::now();auto tSpan = end - start;auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(tSpan);auto msCount = frameInfo[option.fFrameIndex].fDuration - ms.count();auto duration = std::chrono::milliseconds(msCount);std::this_thread::sleep_for(duration);if (option.fFrameIndex == frameCount - 1){option.fPriorFrame = -1;option.fFrameIndex = 0;}else{option.fPriorFrame = option.fPriorFrame + 1;option.fFrameIndex = option.fFrameIndex + 1;}}}, std::move(codec));t.detach();
}

这段代码有以下几点注意:
1,animateGif方法并不是在重画窗口时执行的,而是在创建窗口成功后执行的.

2,frameBitmap是一个SkBitmap*类型的全局变量.用来存储一帧像素数据.

3,创建了一个新的线程以解码gif图像中的每一帧数据,这样做主要是为了不让解码工作影响应用的主线程.

4,每时每刻都在解码(包括线程等待std::this_thread::sleep_for),如果不在一个独立的线程放置该工作,主线程就会卡死.

5,codec解码器的类型是std::unique_ptr<SkCodec>(不能复制),所以不能在线程的匿名函数中抓它,必须把它移动(std::move)到匿名函数内才可以.

6,通过线程对象解附方法按后台线程设置线程,让其自行运行(线程对象join方法会阻塞主线程),生产环境下需自行增加处理异常,释放线程资源保护性代码.

刚开始执行线程方法时,执行了一系列准备工作:

得到ImageInfo信息.

解码器(codec)的getInfo方法得到的ImageInfo对象是gif图像默认定义的,它有可能并不适合用来解码帧数据SkBitmap对象.

因此基于它的基础信息(长,宽等),创建了一个新的ImageInfo对象,该对象的颜色类型为:kN32_SkColorType.
初化frameBitmap,全局变量的只能存储一帧数据内存空间.
得到gif文件中的帧数量:codec->getFrameCount()
得到帧信息:std::vector<SkCodec::FrameInfo>frameInfo=codec->getFrameInfo();
SkCodec::FrameInfo包含了很多与帧有关的信息,其中最重要的就是帧的等待时间(单位:毫秒).

初化SkCodec::Options

SkCodec::Options对象中fFrameIndex表示当前正在播放第几帧(默认为第0帧),fPriorFrame表示上一帧是第几帧.

准备好这些工作之后,开始正式解码gif图像.
循环播放``gif,所以解码工作是在一个不会停止的循环中的.
在一些低端电脑上,解码工作较长,所以记录了该时间消耗.
该工作使用std::chrono::system_clock完成,得到的时间间隔单位为毫秒.

解码器codecgetPixels方法负责把选项中指定的帧解码到frameBitmap指向的内存空间中.
frameBitmap->getPixels()得到的是frameBitmap持有的像素数据的地址.
InvalidateRect窗口接口提供的方法,它负责向窗口发送重画消息.

执行此方法后,窗口将收到WM_PAINT消息.
根据frameInfo里记录的帧信息,让线程等待一段时间再解码下一帧.

注意这里在帧等待时间(fDuration)上减去了解码消耗的时间,这样做可保证,程序即使在一些低端设备上也能流畅播放.

最后更新选项里的当前帧信息和上一帧信息.
判断是否解码到了最后一帧,如果是,则按第0帧设置.如果不是,则按下一帧设置,接着解码下一帧.

整个循环中,最关键的信息就是:在不断的改变frameBitmap指向的内存空间的数据,而且每改变一次(解码一帧),即请求一次重画窗口.

重画方法(绘画方法)的关键代码为:

SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);
auto canvas = SkCanvas::MakeRasterDirect(info, surfaceMemory, 4 * w);
if (frameBitmap) {auto x = (w - frameBitmap->width()) / 2;auto y = (h - frameBitmap->height()) / 2;canvas->writePixels(*frameBitmap, x, y);
}

这段代码很简单,其主要意图是在窗口正中间绘画frameBitmap.因为每次重画frameBitmap里的像素数据都是一帧新的图像,所以gif就在窗口中播放起来了.
程序运行结果如下图所示:
程序中使用的gif图像源自:github.com/ImageOptim/...

注意

gif动画虽然兼容很好,但效果不好.
其最多只能处理256色,不适合真彩色图片.gif虽然支持透明效果,但其透明效果在高分屏上表现很差,图像颗粒感很强,有锯齿.

gif外,还有很多其他格式的文件支持动画,比如webp,apng,svga,lottie等.

用本节示例代码所展示的方式解码,播放大部分非向量格式的动画文件.

但像svga,lottie此类向量格式动画文件,就需要写其他代码来渲染了.

有时并不能根据一个文件的扩展名来判断该文件的格式.

Skia解码器SkCodecgetEncodedFormat方法可取文件的真实格式,如下代码所示:

    //#include "include/codec/SkEncodedImageFormat.h"
std::unique_ptr<SkFILEStream> stream = SkFILEStream::Make(pathStr.data());
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromStream(std::move(stream));
auto imgFormat = codec->getEncodedFormat();
if(imgFormat == SkEncodedImageFormat::kGIF){//......
}

在本文示例代码中,通过一个独立的线程来解码gif动画文件中的每一帧图像(codec->getPixels),每解码一帧图像重画一次窗口(InvalidateRect),重画窗口时,会在窗口正中间渲染解码得到的图像,重画完成之后,等待一段时间(frameInfo[option. fFrameIndex].fDuration)再解码下一帧图像(option.fFrameIndex+=1).

实际上Skia提供了一个类型:modules\skresources\src\SkAnimCodecPlayer.h来帮助播放动画,大家也可用该类型的代码实现来播放gif动画.

http://www.xdnf.cn/news/20000.html

相关文章:

  • 【iOS】对象复制与属性关键字
  • 同步安卓手机的照片到NAS的方案(完美)
  • 人工智能学习:鸢尾花数据获取
  • qwen-code 功能分析报告
  • 软件安装教程(四):在 Windows 上安装与配置 MATLAB(超详细)
  • 【2025企业建站推荐指南】深度解析十大顶尖网站建设公司:从品牌设计到技术落地的全维度解决方案
  • 01_配置版本
  • BERT家族进化史:从BERT到LLaMA,每一次飞跃都源于对“学习”的更深理解
  • 【面试题】生成式搜索能否保证top-1的准确性?
  • MySQL中CASE语法规则的详细解析及扩展示例
  • Spring Cloud Alibaba快速入门01
  • 去中心化投票系统开发教程
  • Java 双亲委派机制解析和破坏双亲委派的方式
  • sealos部署k8s
  • 华为校招实习留学生机试全攻略:真题目录+算法分类+在线OJ+备考策略
  • 如何将两个网段互相打通
  • Java场景题面试合集
  • 「数据获取」中国科技统计年鉴(1991-2024)Excel
  • 江协科技STM32学习笔记补充之004
  • ETL VS ELT企业应该怎么选择数据集成方式
  • 前缀和和差分思路理解以及典题题解
  • Java面试宝典:Redis的设计、实现
  • Flash Attention vs Paged Attention:大语言模型注意力计算的内存管理革命
  • 【国内电子数据取证厂商龙信科技】IOS 逆向脱壳
  • Milvus快速入门以及用 Java 操作 Milvus
  • PAT 1093 Count PAT‘s
  • [技术革命]Harmonizer:仅20MB模型如何实现8K图像_视频的完美和谐化?
  • 三高项目-缓存设计
  • k8s证书理论知识之/etc/kubernetes/pki/ 和/var/lib/kubelet/pki/的区别
  • 将 PDF 转换为 TIFF 图片:简单有效的 Java 教程