Add NoiseStyle and NoiseFilter support with animation binding and SVG export#3483
Add NoiseStyle and NoiseFilter support with animation binding and SVG export#3483zfw1234567 wants to merge 39 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3483 +/- ##
==========================================
+ Coverage 80.52% 80.84% +0.31%
==========================================
Files 620 622 +2
Lines 67017 68227 +1210
Branches 19882 20293 +411
==========================================
+ Hits 53968 55160 +1192
- Misses 9092 9105 +13
- Partials 3957 3962 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
5072176 to
436f7cc
Compare
…er position displacement.
…th tgfx ink bounds
…ts test for consistent font rendering
…nt tgfx and SVG rendering
…ch to align with Figma.
…fer for contrast and density banding.
…ve debug log output.
…depth limit, remove unused contentBounds params, add NoiseMode enum conversion.
…SVG and webp output.
…dowFilter SVG and webp output.
…filter-style combinations.
0fb22b5 to
275aabc
Compare
…r codecov coverage.
| void bindNoiseStyleChannels(const pagx::NoiseStyle* node) { | ||
| _result.binding.setWriter(node, "size", WriteNoiseStyleSize); | ||
| _result.binding.setWriter(node, "density", WriteNoiseStyleDensity); | ||
| _result.binding.setWriter(node, "seed", WriteNoiseStyleSeed); | ||
| _result.binding.setWriter(node, "color", WriteNoiseStyleColor); | ||
| _result.binding.setWriter(node, "firstColor", WriteNoiseStyleFirstColor); | ||
| _result.binding.setWriter(node, "secondColor", WriteNoiseStyleSecondColor); | ||
| _result.binding.setWriter(node, "opacity", WriteNoiseStyleOpacity); | ||
| } |
There was a problem hiding this comment.
[健壮性 / UB 风险] 这里无视 node->mode 无条件注册了全部 7 个 writer,但 WriteNoiseStyleColor 内部 static_cast<tgfx::MonoNoiseStyle*>(object)、WriteNoiseStyleFirstColor/SecondColor 内部 static_cast<tgfx::DuoNoiseStyle*>(object)、WriteNoiseStyleOpacity 内部 static_cast<tgfx::MultiNoiseStyle*>(object)。
这三个 tgfx 类是 NoiseStyle 的兄弟子类,互相之间没有继承关系。如果用户对 Mono 模式的节点附加了 firstColor channel(或对 Duo 模式附加 opacity channel 等),动画 apply 时会执行兄弟类间的 static_cast——这是未定义行为。
建议根据 node->mode 选择性注册 writer:
_result.binding.setWriter(node, "size", WriteNoiseStyleSize);
_result.binding.setWriter(node, "density", WriteNoiseStyleDensity);
_result.binding.setWriter(node, "seed", WriteNoiseStyleSeed);
switch (node->mode) {
case NoiseMode::Mono:
_result.binding.setWriter(node, "color", WriteNoiseStyleColor);
break;
case NoiseMode::Duo:
_result.binding.setWriter(node, "firstColor", WriteNoiseStyleFirstColor);
_result.binding.setWriter(node, "secondColor", WriteNoiseStyleSecondColor);
break;
case NoiseMode::Multi:
_result.binding.setWriter(node, "opacity", WriteNoiseStyleOpacity);
break;
}bindNoiseFilterChannels 同样问题。
| void bindNoiseFilterChannels(const pagx::NoiseFilter* node) { | ||
| _result.binding.setWriter(node, "size", WriteNoiseFilterSize); | ||
| _result.binding.setWriter(node, "density", WriteNoiseFilterDensity); | ||
| _result.binding.setWriter(node, "seed", WriteNoiseFilterSeed); | ||
| _result.binding.setWriter(node, "color", WriteNoiseFilterColor); | ||
| _result.binding.setWriter(node, "firstColor", WriteNoiseFilterFirstColor); | ||
| _result.binding.setWriter(node, "secondColor", WriteNoiseFilterSecondColor); | ||
| _result.binding.setWriter(node, "opacity", WriteNoiseFilterOpacity); | ||
| } |
There was a problem hiding this comment.
[健壮性 / UB 风险] 同 bindNoiseStyleChannels 的问题。WriteNoiseFilterColor 直接 static_cast<tgfx::MonoNoiseFilter*>,WriteNoiseFilterFirstColor/SecondColor static_cast<tgfx::DuoNoiseFilter*>,WriteNoiseFilterOpacity static_cast<tgfx::MultiNoiseFilter*>——这三者是 tgfx::NoiseFilter 的兄弟子类,对错误 mode 的 filter 触发是 UB。
建议同样按 node->mode 选择性注册 writer。
| std::string SVGWriter::writeNoiseTurbulence(const NoiseStyle* noise, | ||
| const std::string& resultName) { | ||
| auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; | ||
| _defs->openElement("feTurbulence"); | ||
| _defs->addAttribute("type", "fractalNoise"); | ||
| _defs->addAttribute("baseFrequency", FloatToString(freq)); | ||
| _defs->addAttribute("stitchTiles", "stitch"); | ||
| _defs->addAttribute("numOctaves", "3"); | ||
| _defs->addAttribute("seed", FloatToString(noise->seed)); | ||
| _defs->addAttribute("result", resultName); | ||
| _defs->closeElementSelfClosing(); | ||
| return resultName; | ||
| } | ||
|
|
||
| std::string SVGWriter::writeNoiseBand(const NoiseStyle* noise, bool isDark, | ||
| const std::string& label) { | ||
| auto turbResult = writeNoiseTurbulence(noise, "turb" + label); | ||
|
|
||
| _defs->openElement("feColorMatrix"); | ||
| _defs->addAttribute("in", turbResult); | ||
| _defs->addAttribute("type", "luminanceToAlpha"); | ||
| _defs->addAttribute("result", "luma" + label); | ||
| _defs->closeElementSelfClosing(); | ||
|
|
||
| auto d = std::clamp(noise->density, 0.0f, 1.0f); | ||
| int lower = 0; | ||
| int upper = 0; | ||
| if (isDark) { | ||
| lower = std::clamp(static_cast<int>(std::lround(-25.0f * d + 25.0f)), 0, 99); | ||
| upper = std::clamp(static_cast<int>(std::lround(24.0f * d + 25.0f)), 0, 99); | ||
| } else { | ||
| lower = std::clamp(static_cast<int>(std::lround(-24.0f * d + 74.0f)), 0, 99); | ||
| upper = std::clamp(static_cast<int>(std::lround(25.0f * d + 74.0f)), 0, 99); | ||
| } | ||
| std::string table; | ||
| table.reserve(300); | ||
| for (int i = 0; i < 100; i++) { | ||
| table += (i >= lower && i <= upper) ? "1 " : "0 "; | ||
| } | ||
| table.pop_back(); | ||
|
|
||
| _defs->openElement("feComponentTransfer"); | ||
| _defs->addAttribute("in", "luma" + label); | ||
| _defs->addAttribute("result", "band" + label); | ||
| _defs->closeElementStart(); | ||
| _defs->openElement("feFuncA"); | ||
| _defs->addAttribute("type", "discrete"); | ||
| _defs->addAttribute("tableValues", table); | ||
| _defs->closeElementSelfClosing(); | ||
| _defs->closeElement(); | ||
| return "band" + label; |
There was a problem hiding this comment.
[可维护性 / 代码重复] 这两个 NoiseStyle 重载(writeNoiseTurbulence 和 writeNoiseBand)与上方 NoiseFilter 版本(991–1041 行)函数体完全相同,只有参数类型不同(都只读 noise->size、noise->density、noise->seed)。再加上后面 writeNoiseStyle Multi 分支(1390+)与 writeNoiseFilter Multi 分支(1211+)也大段重复——总共约 200 行重复代码。
建议用模板抽取共用实现:
template <typename T>
std::string writeNoiseTurbulenceImpl(const T* noise, const std::string& resultName) {
// ... shared body using noise->size / noise->seed ...
}T 同时支持 NoiseFilter 和 NoiseStyle(依赖鸭子类型即可)。writeNoiseBand 同理。
| } | ||
|
|
||
| protected: | ||
| protected: |
There was a problem hiding this comment.
[代码规范] 这一处 protected: 前导空格被去掉,与 PR 主题(noise)无关,是 stray 改动。且与项目其他兄弟类不一致——例如 include/pagx/nodes/Group.h:91 使用 protected:(1 空格)。建议还原此行,或在本地运行 ./codeformat.sh 重新格式化。
| std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex) { | ||
| std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); | ||
|
|
||
| if (noise->mode == NoiseMode::Mono) { | ||
| auto band = writeNoiseBand(noise, true, "Dark" + styleId); | ||
| _defs->openElement("feFlood"); | ||
| _defs->addAttribute("flood-color", ColorToSVGString(noise->color)); | ||
| if (noise->color.alpha < 1.0f) { | ||
| _defs->addAttribute("flood-opacity", FloatToString(noise->color.alpha)); | ||
| } | ||
| _defs->addAttribute("result", "flood" + styleId); | ||
| _defs->closeElementSelfClosing(); | ||
|
|
||
| _defs->openElement("feComposite"); | ||
| _defs->addAttribute("in", "flood" + styleId); | ||
| _defs->addAttribute("in2", band); | ||
| _defs->addAttribute("operator", "in"); | ||
| _defs->addAttribute("result", "colored" + styleId); | ||
| _defs->closeElementSelfClosing(); | ||
|
|
||
| auto resultName = "noiseStyleOut" + styleId; | ||
| _defs->openElement("feComposite"); | ||
| _defs->addAttribute("in", "colored" + styleId); | ||
| _defs->addAttribute("in2", "SourceGraphic"); | ||
| _defs->addAttribute("operator", "in"); | ||
| _defs->addAttribute("result", resultName); | ||
| _defs->closeElementSelfClosing(); | ||
| return resultName; |
There was a problem hiding this comment.
[API 一致性 / SVG 导出] NoiseStyle 继承自 LayerStyle,其 blendMode 字段在运行时会被 LayerBuilderContext 调 tgfxStyle->setBlendMode(...) 应用,但本函数完全忽略了 noise->blendMode,最终通过 agg.aboveResults→feMergeNode 合成(feMerge 等价 SrcOver)。
如果用户给 NoiseStyle 设置 Multiply/Screen 等 blendMode,运行时(tgfx)渲染会生效,SVG 导出会静默丢失该效果——与 writeNoiseFilter 末尾通过 feBlend 显式应用 blendMode 不一致(参考本文件 1188 行附近)。
建议二选一:
- 在
writeNoiseStyle输出末尾增加一个 feBlend 节点应用 blendMode,与 NoiseFilter 行为对齐; - 或在
NoiseStyle头文件 / SVG 导出文档中明确标注"SVG 导出场景下 blendMode 不生效"。
注:DropShadowStyle 在 SVG 导出中也忽略了 blendMode,所以这不是新增 regression,但 NoiseStyle 与 NoiseFilter 不一致这一点是新引入的。
| Color color = {}; | ||
|
|
||
| /** | ||
| * The first noise color for Duo mode. The alpha component controls its opacity. | ||
| */ | ||
| Color firstColor = {}; | ||
|
|
||
| /** | ||
| * The second noise color for Duo mode. The alpha component controls its opacity. | ||
| */ | ||
| Color secondColor = {}; |
There was a problem hiding this comment.
[API 易用性] pagx::Color 默认 alpha = 1(见 pagx/types/Color.h:47),所以这三处 Color color = {} / Color firstColor = {} / Color secondColor = {} 实际值都是不透明黑 (0,0,0,1)。
这意味着用户构造 Duo 模式但忘记同时设置 firstColor 和 secondColor 时,得到的是两个不透明黑——视觉上等同于禁用了 Duo 双色效果。
对应的 tgfx 端默认值更合理:MonoNoiseStyle._color = Color::Black()、DuoNoiseStyle._firstColor = Black()、_secondColor = White()(见 third_party/tgfx/include/tgfx/layers/layerstyles/NoiseStyle.h:154/201/202)。
建议让 pagx 默认值与 tgfx 一致,至少 secondColor 默认为白色:
Color secondColor = {1.0f, 1.0f, 1.0f, 1.0f};NoiseFilter.h 同样问题。
| * A noise layer style that overlays procedural Perlin noise above the layer content. Three noise | ||
| * modes are available: Mono (single color), Duo (two complementary colors), and Multi (preserving | ||
| * original noise RGB with enhanced contrast). | ||
| */ | ||
| class NoiseStyle : public LayerStyle { |
There was a problem hiding this comment.
[API 设计 / 文档] NoiseStyle 同时暴露了 color(Mono 用)、firstColor/secondColor(Duo 用)、opacity(Multi 用)四个字段,但任意时刻只有一组生效。这种"扁平字段 + mode 切换"的设计是相对 tgfx 的子类层次(MonoNoiseStyle/DuoNoiseStyle/MultiNoiseStyle)的折衷,便于用户构造,但带来副作用:
- 用户从代码层面看不出哪些字段必须设置(Issue 请问下 ios有没有可以监听动画播放实时进度的API #5 默认值问题就是这个的副作用);
- 序列化/反序列化无法决定要不要写出未使用的字段;
- 引发
bindNoiseStyleChannels中的 UB 风险(见 LayerBuilder.cpp 行级评论)。
建议在 class doc(这段顶部注释)中明确写出:"非当前 mode 对应的字段会被忽略;切换 mode 后请重新设置对应字段。"目前的字段级注释"used in X mode"较容易被忽略。
NoiseFilter.h 同样建议。
| std::string SVGWriter::writeNoiseTurbulence(const NoiseFilter* noise, | ||
| const std::string& resultName) { | ||
| auto freq = noise->size > 0.0f ? 1.0f / noise->size : 0.25f; | ||
| _defs->openElement("feTurbulence"); | ||
| _defs->addAttribute("type", "fractalNoise"); | ||
| _defs->addAttribute("baseFrequency", FloatToString(freq)); | ||
| _defs->addAttribute("stitchTiles", "stitch"); | ||
| _defs->addAttribute("numOctaves", "3"); | ||
| _defs->addAttribute("seed", FloatToString(noise->seed)); | ||
| _defs->addAttribute("result", resultName); | ||
| _defs->closeElementSelfClosing(); | ||
| return resultName; | ||
| } |
There was a problem hiding this comment.
[视觉一致性 / 文档] freq = 1.0f / size 没有考虑 contentScale。tgfx 端 MakeNoiseShader 计算的是 freq = 1.0f / (size * scale)(见 third_party/tgfx/src/layers/filters/NoiseFilter.cpp:32)。
feTurbulence 在 SVG filter region 像素空间生成,浏览器以 viewBox 缩放渲染时不会自动缩放频率——因此非 1.0 contentScale 下,SVG 导出与 tgfx GPU 渲染的颗粒大小会有偏差。
这不是 bug,是 SVG vs GPU shader 的固有差异。建议在此处加一行注释说明这一点,避免后续维护者误以为是计算错误。
| pagx::FontConfig fontConfig; | ||
| fontConfig.addFallbackTypefaces(GetFallbackTypefaces()); | ||
|
|
||
| auto typeface = Typeface::MakeFromPath("/System/Library/Fonts/Helvetica.ttc"); |
There was a problem hiding this comment.
[测试质量 / 跨平台] 这里硬编码了 macOS 系统字体路径 /System/Library/Fonts/Helvetica.ttc,仅在 macOS 可用。Linux/Windows CI 上 typeface 为 null,会跳过 fontConfig.registerTypeface,文本节点退回 fallback——但 NoiseFilterAllElements 的截图基线包含 Text/TextBox 字形,跨 OS 跑测试时基线 hash 会无法对齐。
建议使用项目中现有的 fallback typeface 机制(如 GetFallbackTypefaces()),或参照其他测试用例使用项目自带的测试字体资源(如 test/resources/font/...)。
概述
新增 NoiseStyle 和 NoiseFilter 两种噪声节点的定义、动画绑定及 SVG 导出支持。
主要变更
新增文件
include/pagx/types/NoiseMode.h— NoiseMode 枚举,定义 Mono / Duo / Multi 三种噪声模式include/pagx/nodes/NoiseStyle.h— NoiseStyle 节点定义(LayerStyle,覆盖 SourceGraphic)include/pagx/nodes/NoiseFilter.h— NoiseFilter 节点定义(LayerFilter,覆盖全部合成结果)核心修改
include/pagx/nodes/Node.h— 添加 NoiseStyle、NoiseFilter 枚举值src/pagx/utils/StringParser.cpp— 添加 NoiseStyle / NoiseFilter 的字符串解析src/renderer/LayerBuilder.cpp:src/pagx/svg/SVGExporter.cpp— 实现三种噪声模式的 SVG 滤镜导出测试
test/src/PAGXTest.cpp— 新增测试用例:test/baseline/version.json— 新增基线版本