* feat(ui): 新增 APNG/GIF 动图解码与动画帧时钟 AnimatedImage 按 APNG 规范解析 acTL/fcTL/fdAT 并校验序号、CRC 与帧边界,逐帧重建 PNG 交给 WIC 解码后按 dispose_op/blend_op 合成;GIF 走 WIC 多帧解码。延时规则与浏览器一致,超限或损坏的动画退回静态首帧。 DeviceResources::GetImageFrame 按动画时钟取帧并汇报下一次换帧时刻,动图帧按 64 MB 预算缓存;Scene 与 Window 据此按需重绘,可整体关闭动画。DrawNineSlice 按 CSS border-image 语义区分源切片与目标边宽、支持 fill,并用夹取画刷消除接缝渗色。 * feat(server): 新增 canvas 基础皮肤并要求 v2 皮肤基于它 canvas 是全贴图基底:透明面板、无描边、无圆角、无阴影,外观来自九宫格面板图与挂件。它只能作为外部皮肤的 base,不进内置皮肤列表;v1 皮肤用它直接报错。 schema_version = 2 而 base 不是 canvas 的皮肤按 v1 加载、忽略全部 v2 字段,并在扫描结果里记一条问题。v2 字段补齐校验:图片限 .png/.apng/.gif/.webp,fill 必须是布尔,偏移与 z_index 有范围,挂件最多 32 层。 * feat(server): D2D 候选窗补齐 v2 九宫格、挂件分层与动图 canvas 皮肤在 D2D 候选窗里按 CSS border-image 语义绘制九宫格面板:切片以源图像素计、边宽以 DIP 计、fill = false 不画中心;有面板图时卡片的底色、描边、圆角与阴影让给贴图。content_insets_dip 从面板外缘量起且不小于边框宽度,line_gap_dip 只作用于预编辑行与候选之间。 挂件拆成卡片前后两层:z_index < 1 在面板之下,其余在面板之上,同层按 z_index 与声明顺序稳定排序;图片在框内等比居中。动图按场景给出的下一次换帧时刻排一次性定时器续帧,只在候选窗显示时运行,隐藏、换皮肤时停;系统关闭动画效果时只显示首帧,v1 皮肤保持静态。 设置页原生预览认得 canvas 基底并绘制面板图。 * feat(examples): 新增基于 canvas 的 v2 全贴图皮肤样例 canvas-demo 用 2 倍九宫格面板、面板下层的叶子挂件和面板上层循环播放的 APNG 星光演示 v2 皮肤,素材均为程序生成的几何图形,随目录以 MIT 授权。样例测试同步校验 schema 2 与 canvas 互为前提、v2 图片扩展名与数值范围。 * docs(examples): 说明 canvas 基底、v2 九宫格与挂件分层及动图规则 schema 说明去掉「仅 WebView2」,写明 v2 必须以 canvas 为 base、canvas 只能当 base;九宫格切片按源图像素、边宽按 DIP,content_insets_dip 不小于边宽,line_gap_dip 只管预编辑行与候选之间;挂件以 z_index 1 为界分在面板上下两层;动图格式、帧时长、上限与系统减少动画时的行为。server/docs/ui-backends.md 与 skins/README.md 同步更新。 * docs(server): 候选窗与皮肤目录的注释不再引用已删除的 WebView2 页面
127 lines
4.2 KiB
C++
127 lines
4.2 KiB
C++
#pragma once
|
|
|
|
#include <wincodec.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace msimeui
|
|
{
|
|
// Frame timing of an animation, kept apart from the pixels so a GPU cache can keep it once the
|
|
// decoded frames are uploaded.
|
|
struct AnimationTimeline
|
|
{
|
|
// How long each frame stays on screen, in milliseconds, already normalized by
|
|
// NormalizeFrameDelayMs.
|
|
std::vector<uint32_t> frameDelaysMs;
|
|
// How many times the whole animation plays; 0 repeats forever. After the last play the last
|
|
// frame stays on screen.
|
|
uint32_t playCount = 0;
|
|
|
|
struct Sample
|
|
{
|
|
size_t frameIndex = 0;
|
|
// Elapsed time (ms since the animation started) at which the frame on screen changes
|
|
// next. Empty when nothing changes any more: a single frame, or a finished animation that
|
|
// now holds its last frame.
|
|
std::optional<uint64_t> nextChangeMs;
|
|
};
|
|
|
|
uint64_t LoopDurationMs() const;
|
|
Sample SampleAt(uint64_t elapsedMs) const;
|
|
};
|
|
|
|
// Browsers show frames that ask for 10 ms or less for 100 ms instead; so does this library, so
|
|
// an animation runs at the speed its author previewed it at.
|
|
uint32_t NormalizeFrameDelayMs(uint32_t delayMs);
|
|
|
|
// Decoding limits; an animation beyond them shows its still first frame. At namespace scope so
|
|
// it is complete where AnimatedImage uses it as a default argument.
|
|
struct AnimatedImageLimits
|
|
{
|
|
uint32_t maxFrames = 1024;
|
|
// All frames together, decoded (width * height * 4 * frames).
|
|
uint64_t maxDecodedBytes = 64ull * 1024 * 1024;
|
|
uint64_t maxFileBytes = 64ull * 1024 * 1024;
|
|
uint32_t maxDimension = 16384;
|
|
};
|
|
|
|
// A decoded still or animated image: every frame composited onto a full-size, premultiplied
|
|
// BGRA canvas, ready to upload as it is.
|
|
//
|
|
// Animated PNG is decoded per the APNG specification (acTL / fcTL / fdAT): each frame is rebuilt
|
|
// as a standalone PNG and decoded with WIC, then composited honoring dispose_op and blend_op.
|
|
// Animated GIF goes through WIC's multi-frame GIF decoder with the same compositing rules. Any
|
|
// other container that WIC reports as multi-frame (animated WebP when the installed codec exposes
|
|
// its frames) is played frame by frame at 100 ms. Malformed or oversized animations fall back to
|
|
// the still first frame; they never fail harder than the still image would.
|
|
class AnimatedImage
|
|
{
|
|
public:
|
|
enum class Format
|
|
{
|
|
Still,
|
|
Apng,
|
|
Gif,
|
|
MultiFrame,
|
|
};
|
|
|
|
using Limits = AnimatedImageLimits;
|
|
|
|
// Returns nothing when not even the still first frame can be decoded.
|
|
static std::optional<AnimatedImage> DecodeFile(IWICImagingFactory *factory, const std::wstring &filePath,
|
|
const Limits &limits = Limits());
|
|
static std::optional<AnimatedImage> DecodeMemory(IWICImagingFactory *factory, const uint8_t *data, size_t size,
|
|
const Limits &limits = Limits());
|
|
// Cheap check without decoding pixels: true when the file looks like an animation (an APNG
|
|
// acTL chunk, or a WIC container with more than one frame). Still images can skip DecodeFile.
|
|
static bool MayBeAnimated(IWICImagingFactory *factory, const std::wstring &filePath);
|
|
|
|
uint32_t Width() const
|
|
{
|
|
return width_;
|
|
}
|
|
uint32_t Height() const
|
|
{
|
|
return height_;
|
|
}
|
|
size_t FrameCount() const
|
|
{
|
|
return frames_.size();
|
|
}
|
|
bool IsAnimated() const
|
|
{
|
|
return frames_.size() > 1;
|
|
}
|
|
Format GetFormat() const
|
|
{
|
|
return format_;
|
|
}
|
|
const AnimationTimeline &Timeline() const
|
|
{
|
|
return timeline_;
|
|
}
|
|
// Premultiplied BGRA, Width() * 4 bytes per row, the whole canvas.
|
|
const std::vector<uint8_t> &FramePixels(size_t index) const
|
|
{
|
|
return frames_[index];
|
|
}
|
|
uint64_t DecodedBytes() const
|
|
{
|
|
return static_cast<uint64_t>(width_) * height_ * 4 * frames_.size();
|
|
}
|
|
|
|
private:
|
|
friend struct AnimatedImageDecoder;
|
|
|
|
uint32_t width_ = 0;
|
|
uint32_t height_ = 0;
|
|
Format format_ = Format::Still;
|
|
AnimationTimeline timeline_;
|
|
std::vector<std::vector<uint8_t>> frames_;
|
|
};
|
|
} // namespace msimeui
|