Skip to content

Add NoiseStyle and NoiseFilter support with animation binding and SVG export#3483

Open
zfw1234567 wants to merge 39 commits into
mainfrom
feature/fengweizou_noise_svg_export
Open

Add NoiseStyle and NoiseFilter support with animation binding and SVG export#3483
zfw1234567 wants to merge 39 commits into
mainfrom
feature/fengweizou_noise_svg_export

Conversation

@zfw1234567

@zfw1234567 zfw1234567 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

概述

新增 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:
    • 实现 pagx NoiseStyle / NoiseFilter → tgfx 渲染层的映射
    • 新增 14 个 Writer 函数及 bindNoiseFilterChannels / bindNoiseStyleChannels,支持动画 channel 驱动
  • src/pagx/svg/SVGExporter.cpp — 实现三种噪声模式的 SVG 滤镜导出

测试

  • test/src/PAGXTest.cpp — 新增测试用例:
    • NoiseFilterModes / NoiseStyleModes — 三种模式渲染截图 + SVG 导出
    • ChannelNoiseFilter / ChannelNoiseStyle — 动画 channel 绑定验证
    • ExportNoiseFilterAnimation — 12 帧多模式逐帧截图(Mono/Duo/Multi/Multi+Shadows)
  • test/baseline/version.json — 新增基线版本

@codecov-commenter

codecov-commenter commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.91243% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.84%. Comparing base (adc9ec2) to head (ae79c84).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/renderer/LayerBuilder.cpp 78.72% 15 Missing and 15 partials ⚠️
src/pagx/svg/SVGExporter.cpp 96.24% 6 Missing and 9 partials ⚠️
test/src/PAGXTest.cpp 97.78% 9 Missing and 5 partials ⚠️
src/pagx/utils/StringParser.cpp 33.33% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zfw1234567 zfw1234567 force-pushed the feature/fengweizou_noise_svg_export branch from 5072176 to 436f7cc Compare June 8, 2026 08:50
zfw1234567 added 23 commits June 8, 2026 20:30
…depth limit, remove unused contentBounds params, add NoiseMode enum conversion.
@zfw1234567 zfw1234567 force-pushed the feature/fengweizou_noise_svg_export branch from 0fb22b5 to 275aabc Compare June 8, 2026 12:31
@zfw1234567 zfw1234567 changed the title Add NoiseStyle and NoiseFilter support with SVG export Add NoiseStyle and NoiseFilter support with animation binding and SVG export Jun 11, 2026
Comment on lines +1287 to +1295
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);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[健壮性 / 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 同样问题。

Comment on lines +1526 to +1534
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);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[健壮性 / 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。

Comment on lines +1044 to +1094
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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[可维护性 / 代码重复] 这两个 NoiseStyle 重载(writeNoiseTurbulencewriteNoiseBand)与上方 NoiseFilter 版本(991–1041 行)函数体完全相同,只有参数类型不同(都只读 noise->sizenoise->densitynoise->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 同时支持 NoiseFilterNoiseStyle(依赖鸭子类型即可)。writeNoiseBand 同理。

}

protected:
protected:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[代码规范] 这一处 protected: 前导空格被去掉,与 PR 主题(noise)无关,是 stray 改动。且与项目其他兄弟类不一致——例如 include/pagx/nodes/Group.h:91 使用 protected:(1 空格)。建议还原此行,或在本地运行 ./codeformat.sh 重新格式化。

Comment on lines +1302 to +1329
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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[API 一致性 / SVG 导出] NoiseStyle 继承自 LayerStyle,其 blendMode 字段在运行时会被 LayerBuilderContexttgfxStyle->setBlendMode(...) 应用,但本函数完全忽略了 noise->blendMode,最终通过 agg.aboveResultsfeMergeNode 合成(feMerge 等价 SrcOver)。

如果用户给 NoiseStyle 设置 Multiply/Screen 等 blendMode,运行时(tgfx)渲染会生效,SVG 导出会静默丢失该效果——与 writeNoiseFilter 末尾通过 feBlend 显式应用 blendMode 不一致(参考本文件 1188 行附近)。

建议二选一:

  1. writeNoiseStyle 输出末尾增加一个 feBlend 节点应用 blendMode,与 NoiseFilter 行为对齐;
  2. 或在 NoiseStyle 头文件 / SVG 导出文档中明确标注"SVG 导出场景下 blendMode 不生效"。

注:DropShadowStyle 在 SVG 导出中也忽略了 blendMode,所以这不是新增 regression,但 NoiseStyle 与 NoiseFilter 不一致这一点是新引入的。

Comment on lines +59 to +69
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 = {};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[API 易用性] pagx::Color 默认 alpha = 1(见 pagx/types/Color.h:47),所以这三处 Color color = {} / Color firstColor = {} / Color secondColor = {} 实际值都是不透明黑 (0,0,0,1)

这意味着用户构造 Duo 模式但忘记同时设置 firstColorsecondColor 时,得到的是两个不透明黑——视觉上等同于禁用了 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 同样问题。

Comment on lines +28 to +32
* 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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[API 设计 / 文档] NoiseStyle 同时暴露了 color(Mono 用)、firstColor/secondColor(Duo 用)、opacity(Multi 用)四个字段,但任意时刻只有一组生效。这种"扁平字段 + mode 切换"的设计是相对 tgfx 的子类层次(MonoNoiseStyle/DuoNoiseStyle/MultiNoiseStyle)的折衷,便于用户构造,但带来副作用:

  1. 用户从代码层面看不出哪些字段必须设置(Issue 请问下 ios有没有可以监听动画播放实时进度的API #5 默认值问题就是这个的副作用);
  2. 序列化/反序列化无法决定要不要写出未使用的字段;
  3. 引发 bindNoiseStyleChannels 中的 UB 风险(见 LayerBuilder.cpp 行级评论)。

建议在 class doc(这段顶部注释)中明确写出:"非当前 mode 对应的字段会被忽略;切换 mode 后请重新设置对应字段。"目前的字段级注释"used in X mode"较容易被忽略。

NoiseFilter.h 同样建议。

Comment on lines +991 to +1003
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;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[视觉一致性 / 文档] 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 的固有差异。建议在此处加一行注释说明这一点,避免后续维护者误以为是计算错误。

Comment thread test/src/PAGXTest.cpp
pagx::FontConfig fontConfig;
fontConfig.addFallbackTypefaces(GetFallbackTypefaces());

auto typeface = Typeface::MakeFromPath("/System/Library/Fonts/Helvetica.ttc");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[测试质量 / 跨平台] 这里硬编码了 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/...)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants