diff --git a/include/pagx/nodes/LayerFilter.h b/include/pagx/nodes/LayerFilter.h index 22dfe67a33..7530b2776e 100644 --- a/include/pagx/nodes/LayerFilter.h +++ b/include/pagx/nodes/LayerFilter.h @@ -23,7 +23,8 @@ namespace pagx { /** - * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, ColorMatrixFilter). + * Base class for layer filters (BlurFilter, DropShadowFilter, InnerShadowFilter, BlendFilter, + * ColorMatrixFilter, NoiseFilter). */ class LayerFilter : public Node { public: diff --git a/include/pagx/nodes/LayerStyle.h b/include/pagx/nodes/LayerStyle.h index 24c39a2be1..23d328fe64 100644 --- a/include/pagx/nodes/LayerStyle.h +++ b/include/pagx/nodes/LayerStyle.h @@ -24,7 +24,7 @@ namespace pagx { /** - * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle). + * Base class for layer styles (DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle, NoiseStyle). */ class LayerStyle : public Node { public: diff --git a/include/pagx/nodes/Node.h b/include/pagx/nodes/Node.h index dd1362420a..4b5fb06cee 100644 --- a/include/pagx/nodes/Node.h +++ b/include/pagx/nodes/Node.h @@ -117,6 +117,10 @@ enum class NodeType { * A background blur layer style. */ BackgroundBlurStyle, + /** + * A noise layer style. + */ + NoiseStyle, // Layer Filters /** @@ -139,6 +143,10 @@ enum class NodeType { * A color matrix filter. */ ColorMatrixFilter, + /** + * A noise filter. + */ + NoiseFilter, // Elements (geometry, painters, modifiers, containers) /** diff --git a/include/pagx/nodes/NoiseFilter.h b/include/pagx/nodes/NoiseFilter.h new file mode 100644 index 0000000000..d01db85695 --- /dev/null +++ b/include/pagx/nodes/NoiseFilter.h @@ -0,0 +1,97 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerFilter.h" +#include "pagx/types/BlendMode.h" +#include "pagx/types/Color.h" +#include "pagx/types/NoiseMode.h" + +namespace pagx { + +/** + * A noise filter that overlays procedural Perlin noise on the layer. Three noise modes are + * available: Mono (single color), Duo (two complementary colors), and Multi (preserving original + * noise RGB with enhanced contrast). Mode-specific fields for inactive modes are ignored; set the + * fields for the new mode after changing mode. + */ +class NoiseFilter : public LayerFilter { + public: + /** + * The noise mode. The default value is Mono. + */ + NoiseMode mode = NoiseMode::Mono; + + /** + * The noise grain size. Larger values produce coarser grains. Must be positive. The default value + * is 4.0. + */ + float size = 4.0f; + + /** + * The noise density in [0, 1]. Controls the proportion of visible noise pixels. The default value + * is 0.5. + */ + float density = 0.5f; + + /** + * The random seed for the noise pattern. The default value is 0. + */ + float seed = 0.0f; + + /** + * The blend mode used to composite the noise with the source image. The default value is Normal. + */ + BlendMode blendMode = BlendMode::Normal; + + /** + * The noise color for Mono mode. Ignored by Duo and Multi modes. The alpha component controls the + * noise opacity. The default value is black. + */ + Color color = {}; + + /** + * The first noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is black. + */ + Color firstColor = {}; + + /** + * The second noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is white. + */ + Color secondColor = {1.0f, 1.0f, 1.0f, 1.0f}; + + /** + * The overall noise opacity for Multi mode, in [0, 1]. Ignored by Mono and Duo modes. The default + * value is 0.15. + */ + float opacity = 0.15f; + + NodeType nodeType() const override { + return NodeType::NoiseFilter; + } + + private: + NoiseFilter() = default; + + friend class PAGXDocument; +}; + +} // namespace pagx diff --git a/include/pagx/nodes/NoiseStyle.h b/include/pagx/nodes/NoiseStyle.h new file mode 100644 index 0000000000..d483a37138 --- /dev/null +++ b/include/pagx/nodes/NoiseStyle.h @@ -0,0 +1,91 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "pagx/nodes/LayerStyle.h" +#include "pagx/types/Color.h" +#include "pagx/types/NoiseMode.h" + +namespace pagx { + +/** + * 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). Mode-specific fields for inactive modes are ignored; + * set the fields for the new mode after changing mode. + */ +class NoiseStyle : public LayerStyle { + public: + /** + * The noise mode. The default value is Mono. + */ + NoiseMode mode = NoiseMode::Mono; + + /** + * The noise grain size. Larger values produce coarser grains. Must be positive. The default value + * is 4.0. + */ + float size = 4.0f; + + /** + * The noise density in [0, 1]. Controls the proportion of visible noise pixels. The default value + * is 0.5. + */ + float density = 0.5f; + + /** + * The random seed for the noise pattern. The default value is 0. + */ + float seed = 0.0f; + + /** + * The noise color for Mono mode. Ignored by Duo and Multi modes. The alpha component controls the + * noise opacity. The default value is black. + */ + Color color = {}; + + /** + * The first noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is black. + */ + Color firstColor = {}; + + /** + * The second noise color for Duo mode. Ignored by Mono and Multi modes. The alpha component + * controls its opacity. The default value is white. + */ + Color secondColor = {1.0f, 1.0f, 1.0f, 1.0f}; + + /** + * The overall noise opacity for Multi mode, in [0, 1]. Ignored by Mono and Duo modes. The default + * value is 0.15. + */ + float opacity = 0.15f; + + NodeType nodeType() const override { + return NodeType::NoiseStyle; + } + + private: + NoiseStyle() = default; + + friend class PAGXDocument; +}; + +} // namespace pagx diff --git a/include/pagx/types/NoiseMode.h b/include/pagx/types/NoiseMode.h new file mode 100644 index 0000000000..c7e8803f17 --- /dev/null +++ b/include/pagx/types/NoiseMode.h @@ -0,0 +1,41 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making libpag available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file +// except in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace pagx { + +/** + * Noise modes for noise filters and noise layer styles. + */ +enum class NoiseMode { + /** + * Single-color noise. Noise pixels are filled with one color. + */ + Mono, + /** + * Dual-color noise. The noise source is split into two complementary regions. + */ + Duo, + /** + * Multi-color noise preserving original Perlin noise RGB with enhanced contrast. + */ + Multi +}; + +} // namespace pagx diff --git a/src/pagx/PAGXImporter.cpp b/src/pagx/PAGXImporter.cpp index cd0ec5fab4..512dcc02aa 100644 --- a/src/pagx/PAGXImporter.cpp +++ b/src/pagx/PAGXImporter.cpp @@ -52,6 +52,8 @@ #include "pagx/nodes/InnerShadowStyle.h" #include "pagx/nodes/LinearGradient.h" #include "pagx/nodes/MergePath.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" #include "pagx/nodes/Polystar.h" @@ -183,6 +185,8 @@ static DropShadowFilter* ParseDropShadowFilter(const DOMNode* node, PAGXDocument static InnerShadowFilter* ParseInnerShadowFilter(const DOMNode* node, PAGXDocument* doc); static BlendFilter* ParseBlendFilter(const DOMNode* node, PAGXDocument* doc); static ColorMatrixFilter* ParseColorMatrixFilter(const DOMNode* node, PAGXDocument* doc); +static NoiseStyle* ParseNoiseStyle(const DOMNode* node, PAGXDocument* doc); +static NoiseFilter* ParseNoiseFilter(const DOMNode* node, PAGXDocument* doc); //============================================================================== // Custom data parsing @@ -577,9 +581,9 @@ static Layer* ParseLayer(const DOMNode* node, PAGXDocument* doc) { " Expected: Layer, Group, Rectangle, Ellipse, Polystar," " Path, Text, Fill, Stroke, TrimPath, RoundCorner," " MergePath, TextModifier, TextPath, TextBox, Repeater," - " DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle," + " DropShadowStyle, InnerShadowStyle, BackgroundBlurStyle, NoiseStyle," " BlurFilter, DropShadowFilter, InnerShadowFilter," - " BlendFilter, ColorMatrixFilter."); + " BlendFilter, ColorMatrixFilter, NoiseFilter."); } return layer; @@ -623,7 +627,7 @@ static void ParseStyles(const DOMNode* node, Layer* layer, PAGXDocument* doc) { "Element '" + current->name + "' is not allowed in 'styles'." " Expected: DropShadowStyle, InnerShadowStyle," - " BackgroundBlurStyle."); + " BackgroundBlurStyle, NoiseStyle."); } } } @@ -644,7 +648,7 @@ static void ParseFilters(const DOMNode* node, Layer* layer, PAGXDocument* doc) { "Element '" + current->name + "' is not allowed in 'filters'." " Expected: BlurFilter, DropShadowFilter," - " InnerShadowFilter, BlendFilter, ColorMatrixFilter."); + " InnerShadowFilter, BlendFilter, ColorMatrixFilter, NoiseFilter."); } } } @@ -758,6 +762,9 @@ static LayerStyle* ParseLayerStyle(const DOMNode* node, PAGXDocument* doc) { if (node->name == "BackgroundBlurStyle") { return ParseBackgroundBlurStyle(node, doc); } + if (node->name == "NoiseStyle") { + return ParseNoiseStyle(node, doc); + } return nullptr; } @@ -777,6 +784,9 @@ static LayerFilter* ParseLayerFilter(const DOMNode* node, PAGXDocument* doc) { if (node->name == "ColorMatrixFilter") { return ParseColorMatrixFilter(node, doc); } + if (node->name == "NoiseFilter") { + return ParseNoiseFilter(node, doc); + } return nullptr; } @@ -2031,6 +2041,34 @@ static void ParseShadowAttributes(const DOMNode* node, PAGXDocument* doc, float& } } +static NoiseStyle* ParseNoiseStyle(const DOMNode* node, PAGXDocument* doc) { + auto style = makeNodeFromXML(node, doc); + if (!style) { + return nullptr; + } + style->blendMode = GET_ENUM(node, "blendMode", "normal", doc, BlendMode); + style->excludeChildEffects = + GetBoolAttribute(node, "excludeChildEffects", Default().excludeChildEffects, doc); + style->mode = GET_ENUM(node, "mode", "mono", doc, NoiseMode); + style->size = GetFloatAttribute(node, "size", Default().size, doc); + style->density = GetFloatAttribute(node, "density", Default().density, doc); + style->seed = GetFloatAttribute(node, "seed", Default().seed, doc); + auto colorStr = GetAttribute(node, "color"); + if (!colorStr.empty()) { + style->color = GetColorAttribute(node, "color", doc); + } + auto firstColorStr = GetAttribute(node, "firstColor"); + if (!firstColorStr.empty()) { + style->firstColor = GetColorAttribute(node, "firstColor", doc); + } + auto secondColorStr = GetAttribute(node, "secondColor"); + if (!secondColorStr.empty()) { + style->secondColor = GetColorAttribute(node, "secondColor", doc); + } + style->opacity = GetFloatAttribute(node, "opacity", Default().opacity, doc); + return style; +} + static DropShadowStyle* ParseDropShadowStyle(const DOMNode* node, PAGXDocument* doc) { auto style = makeNodeFromXML(node, doc); if (!style) { @@ -2077,6 +2115,32 @@ static BackgroundBlurStyle* ParseBackgroundBlurStyle(const DOMNode* node, PAGXDo // Layer filter parsing //============================================================================== +static NoiseFilter* ParseNoiseFilter(const DOMNode* node, PAGXDocument* doc) { + auto filter = makeNodeFromXML(node, doc); + if (!filter) { + return nullptr; + } + filter->mode = GET_ENUM(node, "mode", "mono", doc, NoiseMode); + filter->size = GetFloatAttribute(node, "size", Default().size, doc); + filter->density = GetFloatAttribute(node, "density", Default().density, doc); + filter->seed = GetFloatAttribute(node, "seed", Default().seed, doc); + filter->blendMode = GET_ENUM(node, "blendMode", "normal", doc, BlendMode); + auto colorStr = GetAttribute(node, "color"); + if (!colorStr.empty()) { + filter->color = GetColorAttribute(node, "color", doc); + } + auto firstColorStr = GetAttribute(node, "firstColor"); + if (!firstColorStr.empty()) { + filter->firstColor = GetColorAttribute(node, "firstColor", doc); + } + auto secondColorStr = GetAttribute(node, "secondColor"); + if (!secondColorStr.empty()) { + filter->secondColor = GetColorAttribute(node, "secondColor", doc); + } + filter->opacity = GetFloatAttribute(node, "opacity", Default().opacity, doc); + return filter; +} + static BlurFilter* ParseBlurFilter(const DOMNode* node, PAGXDocument* doc) { auto filter = makeNodeFromXML(node, doc); if (!filter) { diff --git a/src/pagx/svg/SVGExporter.cpp b/src/pagx/svg/SVGExporter.cpp index 86eff3a726..f05f515f11 100644 --- a/src/pagx/svg/SVGExporter.cpp +++ b/src/pagx/svg/SVGExporter.cpp @@ -49,14 +49,19 @@ #include "pagx/nodes/InnerShadowStyle.h" #include "pagx/nodes/LayerStyle.h" #include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" +#include "pagx/nodes/Polystar.h" #include "pagx/nodes/RadialGradient.h" #include "pagx/nodes/Rectangle.h" +#include "pagx/nodes/Repeater.h" #include "pagx/nodes/SolidColor.h" #include "pagx/nodes/Stroke.h" #include "pagx/nodes/Text.h" #include "pagx/nodes/TextBox.h" +#include "pagx/nodes/TextPath.h" #include "pagx/svg/SVGBlendMode.h" #include "pagx/svg/SVGFeatureProbe.h" #include "pagx/svg/SVGPathParser.h" @@ -403,12 +408,23 @@ class SVGWriter { void writeColorMatrixFilter(const ColorMatrixFilter* cm, int& colorMatrixIndex, std::string& currentSource); void writeBlendFilter(const BlendFilter* blend, int& shadowIndex, std::string& currentSource); + std::string writeNoiseTurbulence(float size, float seed, const std::string& resultName); + std::string writeNoiseBand(float size, float density, float seed, bool isDark, + const std::string& label); + std::string writeNoiseMultiCore(float size, float density, float seed, float opacity, + const std::string& id); + void writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, std::string& currentSource); + void writeNoiseResultBlend(const std::string& clippedResult, const std::string& filterId, + BlendMode blendMode, std::string& currentSource); + void writeNoiseStyleClip(const std::string& noiseResult, const std::string& styleId); // Collected per-filter state fed into the final feMerge aggregation. struct ShadowAggregate { std::vector dropShadowResults; + std::vector aboveResults; std::vector innerShadowResults; bool needSourceGraphic = false; }; + std::string writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex); void writeFilterList(const std::vector& filters, int& shadowIndex, ShadowAggregate& agg, std::string& currentSource); void writeStyleList(const std::vector& styles, int& shadowIndex, @@ -443,6 +459,10 @@ class SVGWriter { // needs-group path (inside the ) and for the bare-through path. void writeLayerBody(SVGBuilder& out, const Layer* layer, float perChildAlpha = 1.0f); + // Writes flood-color (and optional flood-opacity) on the current feFlood element, with Display + // P3 override when the color's colorSpace is DisplayP3. + void applyFloodColor(const Color& color); + // Fill / stroke attribute helpers void applyFillAttributes(SVGBuilder& out, const Fill* fill, const Rect& shapeBounds = {}, std::string* p3Style = nullptr, float alphaMultiplier = 1.0f); @@ -932,10 +952,7 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, std::string idx = std::to_string(shadowIndex++); std::string floodResult = "blendFlood" + idx; _defs->openElement("feFlood"); - _defs->addAttribute("flood-color", ColorToSVGString(blend->color)); - if (blend->color.alpha < 1.0f) { - _defs->addAttribute("flood-opacity", FloatToString(blend->color.alpha)); - } + applyFloodColor(blend->color); _defs->addAttribute("result", floodResult); _defs->closeElementSelfClosing(); @@ -971,10 +988,347 @@ void SVGWriter::writeBlendFilter(const BlendFilter* blend, int& shadowIndex, currentSource = blendOut; } +//============================================================================== +// SVGWriter – noise filter primitives +//============================================================================== + +std::string SVGWriter::writeNoiseTurbulence(float size, float seed, const std::string& resultName) { + // SVG filter primitives operate in document units and do not receive the runtime content scale + // used by GPU rasterization, so exported noise frequency intentionally derives from authoring size. + auto freq = size > 0.0f ? 1.0f / 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(seed)); + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + return resultName; +} + +std::string SVGWriter::writeNoiseBand(float size, float density, float seed, bool isDark, + const std::string& label) { + auto turbResult = writeNoiseTurbulence(size, seed, "turb" + label); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "luminanceToAlpha"); + _defs->addAttribute("result", "luma" + label); + _defs->closeElementSelfClosing(); + + auto d = std::clamp(density, 0.0f, 1.0f); + int lower = 0; + int upper = 0; + // Maps density to a percentile band [lower, upper] in the luminance histogram: + // dark (isDark=true ): d=0→[25,25], d=1→[ 0,49] + // bright (isDark=false): d=0→[74,74], d=1→[50,99] + // The band narrows at d=0 and widens toward the extremes as density increases, + // producing a continuous noise density gradient. + if (isDark) { + lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + upper = std::clamp(static_cast(std::lround(24.0f * d + 25.0f)), 0, 99); + } else { + lower = std::clamp(static_cast(std::lround(-24.0f * d + 74.0f)), 0, 99); + upper = std::clamp(static_cast(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; +} + +std::string SVGWriter::writeNoiseMultiCore(float size, float density, float seed, float opacity, + const std::string& id) { + auto turbResult = writeNoiseTurbulence(size, seed, "n" + id); + + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("result", "contrast" + id); + _defs->closeElementStart(); + _defs->openElement("feFuncR"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->openElement("feFuncG"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->openElement("feFuncB"); + _defs->addAttribute("type", "linear"); + _defs->addAttribute("slope", "2"); + _defs->addAttribute("intercept", "-0.5"); + _defs->closeElementSelfClosing(); + _defs->closeElement(); + + auto d = std::clamp(density, 0.0f, 1.0f); + int lower = std::clamp(static_cast(std::lround(-25.0f * d + 25.0f)), 0, 99); + int upper = std::clamp(static_cast(std::lround(24.0f * d + 25.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("feColorMatrix"); + _defs->addAttribute("in", turbResult); + _defs->addAttribute("type", "luminanceToAlpha"); + _defs->addAttribute("result", "luma" + id); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComponentTransfer"); + _defs->addAttribute("in", "luma" + id); + _defs->addAttribute("result", "band" + id); + _defs->closeElementStart(); + _defs->openElement("feFuncA"); + _defs->addAttribute("type", "discrete"); + _defs->addAttribute("tableValues", table); + _defs->closeElementSelfClosing(); + _defs->closeElement(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "contrast" + id); + _defs->addAttribute("in2", "band" + id); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "masked" + id); + _defs->closeElementSelfClosing(); + + _defs->openElement("feColorMatrix"); + _defs->addAttribute("in", "masked" + id); + _defs->addAttribute("type", "matrix"); + auto clampedOpacity = std::clamp(opacity, 0.0f, 1.0f); + std::string opacityValues = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 "; + opacityValues += FloatToString(clampedOpacity); + opacityValues += " 0"; + _defs->addAttribute("values", opacityValues); + _defs->addAttribute("result", "final" + id); + _defs->closeElementSelfClosing(); + return "final" + id; +} + +void SVGWriter::writeNoiseResultBlend(const std::string& clippedResult, const std::string& filterId, + BlendMode blendMode, std::string& currentSource) { + auto resultName = "noiseOut" + filterId; + _defs->openElement("feBlend"); + _defs->addAttribute("in", clippedResult); + _defs->addAttribute("in2", currentSource); + auto modeStr = BlendModeToFEBlendString(blendMode); + if (modeStr) { + _defs->addAttribute("mode", modeStr); + } + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); + currentSource = resultName; +} + +void SVGWriter::writeNoiseFilter(const NoiseFilter* noise, int& noiseIndex, + std::string& currentSource) { + if (noise->size <= 0.0f) { + return; + } + std::string filterId = "noise" + std::to_string(noiseIndex++); + + if (noise->mode == NoiseMode::Mono) { + auto band = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + filterId); + _defs->openElement("feFlood"); + applyFloodColor(noise->color); + _defs->addAttribute("result", "flood" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "flood" + filterId); + _defs->addAttribute("in2", band); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "colored" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "colored" + filterId); + _defs->addAttribute("in2", currentSource); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "clipped" + filterId); + _defs->closeElementSelfClosing(); + + writeNoiseResultBlend("clipped" + filterId, filterId, noise->blendMode, currentSource); + return; + } + + if (noise->mode == NoiseMode::Duo) { + auto dark = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + filterId); + auto bright = + writeNoiseBand(noise->size, noise->density, noise->seed, false, "Bright" + filterId); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", dark); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "out"); + _defs->addAttribute("result", "duoDark" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + applyFloodColor(noise->firstColor); + _defs->addAttribute("result", "floodDark" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodDark" + filterId); + _defs->addAttribute("in2", "duoDark" + filterId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "darkColored" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + applyFloodColor(noise->secondColor); + _defs->addAttribute("result", "floodBright" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodBright" + filterId); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "brightColored" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feBlend"); + _defs->addAttribute("in", "darkColored" + filterId); + _defs->addAttribute("in2", "brightColored" + filterId); + _defs->addAttribute("mode", "screen"); + _defs->addAttribute("result", "duo" + filterId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "duo" + filterId); + _defs->addAttribute("in2", currentSource); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "clipped" + filterId); + _defs->closeElementSelfClosing(); + + writeNoiseResultBlend("clipped" + filterId, filterId, noise->blendMode, currentSource); + return; + } + + auto multiResult = + writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, filterId); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", multiResult); + _defs->addAttribute("in2", currentSource); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "clipped" + filterId); + _defs->closeElementSelfClosing(); + + writeNoiseResultBlend("clipped" + filterId, filterId, noise->blendMode, currentSource); +} + +std::string SVGWriter::writeNoiseStyle(const NoiseStyle* noise, int& noiseStyleIndex) { + if (noise->size <= 0.0f) { + return {}; + } + std::string styleId = "noiseStyle" + std::to_string(noiseStyleIndex++); + + if (noise->mode == NoiseMode::Mono) { + auto band = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + styleId); + _defs->openElement("feFlood"); + applyFloodColor(noise->color); + _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(); + + writeNoiseStyleClip("colored" + styleId, styleId); + return "noiseStyleOut" + styleId; + } + + if (noise->mode == NoiseMode::Duo) { + auto dark = writeNoiseBand(noise->size, noise->density, noise->seed, true, "Dark" + styleId); + auto bright = + writeNoiseBand(noise->size, noise->density, noise->seed, false, "Bright" + styleId); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", dark); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "out"); + _defs->addAttribute("result", "duoDark" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + applyFloodColor(noise->firstColor); + _defs->addAttribute("result", "floodDark" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodDark" + styleId); + _defs->addAttribute("in2", "duoDark" + styleId); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "darkColored" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feFlood"); + applyFloodColor(noise->secondColor); + _defs->addAttribute("result", "floodBright" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feComposite"); + _defs->addAttribute("in", "floodBright" + styleId); + _defs->addAttribute("in2", bright); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", "brightColored" + styleId); + _defs->closeElementSelfClosing(); + + _defs->openElement("feBlend"); + _defs->addAttribute("in", "darkColored" + styleId); + _defs->addAttribute("in2", "brightColored" + styleId); + _defs->addAttribute("mode", "screen"); + _defs->addAttribute("result", "duo" + styleId); + _defs->closeElementSelfClosing(); + + writeNoiseStyleClip("duo" + styleId, styleId); + return "noiseStyleOut" + styleId; + } + + auto multiResult = + writeNoiseMultiCore(noise->size, noise->density, noise->seed, noise->opacity, styleId); + + writeNoiseStyleClip(multiResult, styleId); + return "noiseStyleOut" + styleId; +} + +void SVGWriter::writeNoiseStyleClip(const std::string& noiseResult, const std::string& styleId) { + auto resultName = "noiseStyleOut" + styleId; + _defs->openElement("feComposite"); + _defs->addAttribute("in", noiseResult); + _defs->addAttribute("in2", "SourceGraphic"); + _defs->addAttribute("operator", "in"); + _defs->addAttribute("result", resultName); + _defs->closeElementSelfClosing(); +} + void SVGWriter::writeFilterList(const std::vector& filters, int& shadowIndex, ShadowAggregate& agg, std::string& currentSource) { int colorMatrixIndex = 0; int blurIndex = 0; + int noiseIndex = 0; for (const auto* filter : filters) { switch (filter->nodeType()) { case NodeType::BlurFilter: { @@ -1015,6 +1369,9 @@ void SVGWriter::writeFilterList(const std::vector& filters, int& s case NodeType::BlendFilter: writeBlendFilter(static_cast(filter), shadowIndex, currentSource); break; + case NodeType::NoiseFilter: + writeNoiseFilter(static_cast(filter), noiseIndex, currentSource); + break; default: break; } @@ -1024,9 +1381,11 @@ void SVGWriter::writeFilterList(const std::vector& filters, int& s // LayerStyle emission mirrors the Filter pass so DropShadowStyle / InnerShadowStyle // share the feMerge aggregation logic. BackgroundBlurStyle is silently skipped because SVG has // no portable backdrop-blur primitive (the deprecated enable-background is not supported by -// modern renderers). +// modern renderers). NoiseStyle emits its SVG primitives and adds the result name to +// agg.aboveResults so it composites above the source in the final feMerge. void SVGWriter::writeStyleList(const std::vector& styles, int& shadowIndex, ShadowAggregate& agg) { + int noiseStyleIndex = 0; for (const auto* style : styles) { switch (style->nodeType()) { case NodeType::DropShadowStyle: { @@ -1047,6 +1406,35 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad agg.needSourceGraphic = true; break; } + case NodeType::NoiseStyle: { + auto noise = static_cast(style); + auto result = writeNoiseStyle(noise, noiseStyleIndex); + if (result.empty()) { + break; + } + if (noise->blendMode != BlendMode::Normal) { + auto modeStr = BlendModeToFEBlendString(noise->blendMode); + if (modeStr) { + std::string blendResult = result + "Blended"; + // NoiseStyle blends against SourceGraphic (not currentSource) because in the tgfx + // runtime, LayerStyles execute before LayerFilters and blend against the original + // layer content, not the filter chain output. + _defs->openElement("feBlend"); + _defs->addAttribute("in", result); + _defs->addAttribute("in2", "SourceGraphic"); + _defs->addAttribute("mode", modeStr); + _defs->addAttribute("result", blendResult); + _defs->closeElementSelfClosing(); + agg.aboveResults.push_back(blendResult); + } else { + agg.aboveResults.push_back(result); + } + } else { + agg.aboveResults.push_back(result); + } + agg.needSourceGraphic = true; + break; + } default: break; } @@ -1055,12 +1443,12 @@ void SVGWriter::writeStyleList(const std::vector& styles, int& shad void SVGWriter::writeShadowMerge(const ShadowAggregate& agg, const std::string& currentSource) { bool hasShadows = !agg.dropShadowResults.empty() || !agg.innerShadowResults.empty(); - if (!hasShadows) { - // No shadows but upstream filters may have reshaped the source (Blur, - // ColorMatrix, Blend). In that case the chain already terminates in - // `currentSource`, which is the default implicit output — nothing to - // merge. Only when no image-transforming filter ran does currentSource - // remain "SourceGraphic", and the would have been empty anyway. + bool hasAbove = !agg.aboveResults.empty(); + if (!hasShadows && !hasAbove) { + // No shadows and no above-layer styles (e.g. NoiseStyle). Upstream filters + // may have reshaped the source (Blur, ColorMatrix, Blend). In that case the + // chain already terminates in `currentSource`, which is the default implicit + // output — nothing to merge. return; } bool multipleShadows = (agg.dropShadowResults.size() + agg.innerShadowResults.size()) > 1; @@ -1086,6 +1474,11 @@ void SVGWriter::writeShadowMerge(const ShadowAggregate& agg, const std::string& _defs->addAttribute("in", currentSource); _defs->closeElementSelfClosing(); } + for (const auto& result : agg.aboveResults) { + _defs->openElement("feMergeNode"); + _defs->addAttribute("in", result); + _defs->closeElementSelfClosing(); + } for (const auto& result : agg.innerShadowResults) { _defs->openElement("feMergeNode"); _defs->addAttribute("in", result); @@ -1335,6 +1728,25 @@ void SVGWriter::applyP3Style(SVGBuilder& out, const std::string& p3Style) { } } +void SVGWriter::applyFloodColor(const Color& color) { + _defs->addAttribute("flood-color", ColorToSVGString(color)); + if (color.alpha < 1.0f) { + _defs->addAttribute("flood-opacity", FloatToString(color.alpha)); + } + if (color.colorSpace == ColorSpace::DisplayP3) { + std::string style; + style.reserve(120); + style += "flood-color:"; + style += ColorToSVGString(color); + style += ";flood-color:"; + style += ColorToDisplayP3String(color); + style += ";flood-opacity:"; + style += FloatToString(color.alpha); + style += ';'; + _defs->addAttribute("style", style); + } +} + void SVGWriter::applyPainters(SVGBuilder& out, const FillStrokeInfo& fs, const Rect& shapeBounds, float alpha) { std::string p3Style; diff --git a/src/pagx/utils/StringParser.cpp b/src/pagx/utils/StringParser.cpp index f0576fd733..31e81c7bbb 100644 --- a/src/pagx/utils/StringParser.cpp +++ b/src/pagx/utils/StringParser.cpp @@ -88,6 +88,8 @@ const char* NodeTypeName(NodeType type) { return "InnerShadowStyle"; case NodeType::BackgroundBlurStyle: return "BackgroundBlurStyle"; + case NodeType::NoiseStyle: + return "NoiseStyle"; case NodeType::BlurFilter: return "BlurFilter"; case NodeType::DropShadowFilter: @@ -98,6 +100,8 @@ const char* NodeTypeName(NodeType type) { return "BlendFilter"; case NodeType::ColorMatrixFilter: return "ColorMatrixFilter"; + case NodeType::NoiseFilter: + return "NoiseFilter"; case NodeType::Rectangle: return "Rectangle"; case NodeType::Ellipse: @@ -243,6 +247,9 @@ DEFINE_ENUM_CONVERSION(Arrangement, Arrangement::Start, {Arrangement::Start, "st {Arrangement::SpaceEvenly, "spaceEvenly"}, {Arrangement::SpaceAround, "spaceAround"}) +DEFINE_ENUM_CONVERSION(NoiseMode, NoiseMode::Mono, {NoiseMode::Mono, "mono"}, + {NoiseMode::Duo, "duo"}, {NoiseMode::Multi, "multi"}) + std::string ColorSpaceToString(ColorSpace space) { switch (space) { case ColorSpace::SRGB: diff --git a/src/pagx/utils/StringParser.h b/src/pagx/utils/StringParser.h index eae70c9b8e..c6035ef22d 100644 --- a/src/pagx/utils/StringParser.h +++ b/src/pagx/utils/StringParser.h @@ -40,6 +40,7 @@ #include "pagx/types/LayoutMode.h" #include "pagx/types/Matrix.h" #include "pagx/types/MipmapMode.h" +#include "pagx/types/NoiseMode.h" #include "pagx/types/Padding.h" #include "pagx/types/ScaleMode.h" #include "pagx/types/TextAnchor.h" @@ -196,6 +197,13 @@ std::string ArrangementToString(Arrangement arr); Arrangement ArrangementFromString(const std::string& str); bool IsValidArrangementString(const std::string& str); +//============================================================================== +// NoiseMode +//============================================================================== +std::string NoiseModeToString(NoiseMode mode); +NoiseMode NoiseModeFromString(const std::string& str); +bool IsValidNoiseModeString(const std::string& str); + //============================================================================== // Padding //============================================================================== diff --git a/src/renderer/LayerBuilder.cpp b/src/renderer/LayerBuilder.cpp index 061fe428c7..8850c6044f 100644 --- a/src/renderer/LayerBuilder.cpp +++ b/src/renderer/LayerBuilder.cpp @@ -45,6 +45,8 @@ #include "pagx/nodes/LinearGradient.h" #include "pagx/nodes/MergePath.h" #include "pagx/nodes/Node.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/Polystar.h" #include "pagx/nodes/RadialGradient.h" @@ -90,9 +92,11 @@ #include "tgfx/layers/filters/DropShadowFilter.h" #include "tgfx/layers/filters/InnerShadowFilter.h" #include "tgfx/layers/filters/LayerFilter.h" +#include "tgfx/layers/filters/NoiseFilter.h" #include "tgfx/layers/layerstyles/BackgroundBlurStyle.h" #include "tgfx/layers/layerstyles/DropShadowStyle.h" #include "tgfx/layers/layerstyles/InnerShadowStyle.h" +#include "tgfx/layers/layerstyles/NoiseStyle.h" #include "tgfx/layers/vectors/Ellipse.h" #include "tgfx/layers/vectors/FillStyle.h" #include "tgfx/layers/vectors/Gradient.h" @@ -1044,6 +1048,34 @@ class LayerBuilderContext { bindBackgroundBlurStyleChannels(style); return tgfxStyle; } + case NodeType::NoiseStyle: { + auto style = static_cast(node); + if (style->size <= 0.0f) { + return nullptr; + } + std::shared_ptr tgfxStyle; + switch (style->mode) { + case NoiseMode::Mono: + tgfxStyle = tgfx::NoiseStyle::MakeMono(style->size, style->density, + ToTGFX(style->color), style->seed); + break; + case NoiseMode::Duo: + tgfxStyle = + tgfx::NoiseStyle::MakeDuo(style->size, style->density, ToTGFX(style->firstColor), + ToTGFX(style->secondColor), style->seed); + break; + case NoiseMode::Multi: + tgfxStyle = tgfx::NoiseStyle::MakeMulti(style->size, style->density, style->opacity, + style->seed); + break; + } + if (tgfxStyle && node->blendMode != BlendMode::Normal) { + tgfxStyle->setBlendMode(ToTGFX(node->blendMode)); + } + _result.binding.set(style, tgfxStyle); + bindNoiseStyleChannels(style); + return tgfxStyle; + } default: return nullptr; } @@ -1189,6 +1221,90 @@ class LayerBuilderContext { _result.binding.setWriter(node, "blurY", WriteBackgroundBlurStyleBlurY); } + static void WriteNoiseStyleSize(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setSize(MixFloat(style->size(), *v, mix)); + } + + static void WriteNoiseStyleDensity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setDensity(MixFloat(style->density(), *v, mix)); + } + + static void WriteNoiseStyleSeed(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setSeed(MixFloat(style->seed(), *v, mix)); + } + + static void WriteNoiseStyleColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + auto target = ToTGFX(*v); + style->setColor(MixTGFXColor(style->color(), target, mix)); + } + + static void WriteNoiseStyleFirstColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + auto target = ToTGFX(*v); + style->setFirstColor(MixTGFXColor(style->firstColor(), target, mix)); + } + + static void WriteNoiseStyleSecondColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + auto target = ToTGFX(*v); + style->setSecondColor(MixTGFXColor(style->secondColor(), target, mix)); + } + + static void WriteNoiseStyleOpacity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* style = static_cast(object); + style->setOpacity(MixFloat(style->opacity(), *v, mix)); + } + + void bindNoiseStyleChannels(const pagx::NoiseStyle* node) { + _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; + } + } + static void WriteBlurFilterX(void* object, const KeyValue& value, float mix) { auto* v = std::get_if(&value); if (v == nullptr) { @@ -1352,6 +1468,90 @@ class LayerBuilderContext { _result.binding.setWriter(node, "color", WriteBlendFilterColor); } + static void WriteNoiseFilterSize(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setSize(MixFloat(filter->size(), *v, mix)); + } + + static void WriteNoiseFilterDensity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setDensity(MixFloat(filter->density(), *v, mix)); + } + + static void WriteNoiseFilterSeed(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setSeed(MixFloat(filter->seed(), *v, mix)); + } + + static void WriteNoiseFilterColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + auto target = ToTGFX(*v); + filter->setColor(MixTGFXColor(filter->color(), target, mix)); + } + + static void WriteNoiseFilterFirstColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + auto target = ToTGFX(*v); + filter->setFirstColor(MixTGFXColor(filter->firstColor(), target, mix)); + } + + static void WriteNoiseFilterSecondColor(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + auto target = ToTGFX(*v); + filter->setSecondColor(MixTGFXColor(filter->secondColor(), target, mix)); + } + + static void WriteNoiseFilterOpacity(void* object, const KeyValue& value, float mix) { + auto* v = std::get_if(&value); + if (v == nullptr) { + return; + } + auto* filter = static_cast(object); + filter->setOpacity(MixFloat(filter->opacity(), *v, mix)); + } + + void bindNoiseFilterChannels(const pagx::NoiseFilter* node) { + _result.binding.setWriter(node, "size", WriteNoiseFilterSize); + _result.binding.setWriter(node, "density", WriteNoiseFilterDensity); + _result.binding.setWriter(node, "seed", WriteNoiseFilterSeed); + switch (node->mode) { + case NoiseMode::Mono: + _result.binding.setWriter(node, "color", WriteNoiseFilterColor); + break; + case NoiseMode::Duo: + _result.binding.setWriter(node, "firstColor", WriteNoiseFilterFirstColor); + _result.binding.setWriter(node, "secondColor", WriteNoiseFilterSecondColor); + break; + case NoiseMode::Multi: + _result.binding.setWriter(node, "opacity", WriteNoiseFilterOpacity); + break; + } + } + std::shared_ptr convertLayerFilter(const LayerFilter* node) { if (!node) { return nullptr; @@ -1395,6 +1595,32 @@ class LayerBuilderContext { auto filter = static_cast(node); return tgfx::ColorMatrixFilter::Make(filter->matrix); } + case NodeType::NoiseFilter: { + auto filter = static_cast(node); + if (filter->size <= 0.0f) { + return nullptr; + } + auto tgfxBlendMode = ToTGFX(filter->blendMode); + std::shared_ptr tgfxFilter; + switch (filter->mode) { + case NoiseMode::Mono: + tgfxFilter = tgfx::NoiseFilter::MakeMono( + filter->size, filter->density, ToTGFX(filter->color), filter->seed, tgfxBlendMode); + break; + case NoiseMode::Duo: + tgfxFilter = tgfx::NoiseFilter::MakeDuo( + filter->size, filter->density, ToTGFX(filter->firstColor), + ToTGFX(filter->secondColor), filter->seed, tgfxBlendMode); + break; + case NoiseMode::Multi: + tgfxFilter = tgfx::NoiseFilter::MakeMulti(filter->size, filter->density, + filter->opacity, filter->seed, tgfxBlendMode); + break; + } + _result.binding.set(filter, tgfxFilter); + bindNoiseFilterChannels(filter); + return tgfxFilter; + } default: return nullptr; } diff --git a/test/baseline/version.json b/test/baseline/version.json index fb22ceedbc..5e233e945a 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -8558,6 +8558,24 @@ }, "PAGXTest": { "LayerBuilderAPIConsistency": "f40e23b6", + "NoiseFilterAllElements": "126228e0", + "NoiseFilterAnimation": { + "frame_0": "8d43445d", + "frame_1": "e25fac81", + "frame_10": "e25fac81", + "frame_11": "e25fac81", + "frame_2": "e25fac81", + "frame_3": "e25fac81", + "frame_4": "e25fac81", + "frame_5": "e25fac81", + "frame_6": "e25fac81", + "frame_7": "e25fac81", + "frame_8": "e25fac81", + "frame_9": "e25fac81" + }, + "NoiseFilterModes": "798f40c4", + "NoiseStyleBlendModeOnImage": "126228e0", + "NoiseStyleModes": "1af07597", "PrecomposedTextRender": "9b6dea6a", "html": { "blend_modes_showcase": "f94e413d", diff --git a/test/src/PAGXTest.cpp b/test/src/PAGXTest.cpp index 42da624cde..2c5fb92458 100644 --- a/test/src/PAGXTest.cpp +++ b/test/src/PAGXTest.cpp @@ -36,6 +36,7 @@ #include "pagx/PAGXExporter.h" #include "pagx/PAGXImporter.h" #include "pagx/PAGXOptimizer.h" +#include "pagx/SVGExporter.h" #include "pagx/SVGImporter.h" #include "pagx/TextLayout.h" #include "pagx/TextLayoutParams.h" @@ -56,8 +57,11 @@ #include "pagx/nodes/Group.h" #include "pagx/nodes/Image.h" #include "pagx/nodes/ImagePattern.h" +#include "pagx/nodes/InnerShadowStyle.h" #include "pagx/nodes/Layer.h" #include "pagx/nodes/LinearGradient.h" +#include "pagx/nodes/NoiseFilter.h" +#include "pagx/nodes/NoiseStyle.h" #include "pagx/nodes/Path.h" #include "pagx/nodes/PathData.h" #include "pagx/nodes/Polystar.h" @@ -96,7 +100,9 @@ #include "tgfx/layers/filters/BlendFilter.h" #include "tgfx/layers/filters/BlurFilter.h" #include "tgfx/layers/filters/DropShadowFilter.h" +#include "tgfx/layers/filters/NoiseFilter.h" #include "tgfx/layers/layerstyles/DropShadowStyle.h" +#include "tgfx/layers/layerstyles/NoiseStyle.h" #include "tgfx/layers/vectors/Gradient.h" #include "tgfx/layers/vectors/SolidColor.h" #include "tgfx/layers/vectors/Text.h" @@ -7211,4 +7217,910 @@ PAGX_TEST(PAGXTest, HitTestGlobalMatrix) { EXPECT_FLOAT_EQ(matrix.ty, 45.0f); } +static pagx::Layer* MakeTestLayer(pagx::PAGXDocument* doc, float x, float y, float fillR, + float fillG, float fillB) { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(x, y); + auto rect = doc->makeNode(); + rect->position = {50, 50}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {fillR, fillG, fillB, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; +} + +static pagx::Layer* MakeTestLayerSimple(pagx::PAGXDocument* doc, float x, float y) { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(x, y); + auto rect = doc->makeNode(); + rect->position = {0, 0}; + rect->size = {100, 100}; + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {0.8f, 0.8f, 0.8f, 1.0f}; + fill->color = solid; + layer->contents.push_back(rect); + layer->contents.push_back(fill); + return layer; +} + +static pagx::NoiseFilter* MakeMonoNoiseFilter(pagx::PAGXDocument* doc, float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->color = {0.0f, 0.0f, 0.0f, 1.0f}; + return noise; +} + +static pagx::NoiseFilter* MakeDuoNoiseFilter(pagx::PAGXDocument* doc, float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Duo; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + noise->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + return noise; +} + +static pagx::NoiseFilter* MakeMultiNoiseFilter(pagx::PAGXDocument* doc, float density) { + auto noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Multi; + noise->size = 10; + noise->density = density; + noise->seed = 42; + noise->opacity = 1.0f; + return noise; +} + +static pagx::Fill* MakeSolidFill(pagx::PAGXDocument* doc, float r, float g, float b) { + auto fill = doc->makeNode(); + auto solid = doc->makeNode(); + solid->color = {r, g, b, 1.0f}; + fill->color = solid; + return fill; +} + +static void WriteSVGFile(const std::string& svgContent, const std::string& relativePath) { + auto outPath = ProjectPath::Absolute(relativePath); + auto dirPath = std::filesystem::path(outPath).parent_path(); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + std::ofstream file(outPath, std::ios::binary); + file.write(svgContent.data(), static_cast(svgContent.size())); +} + +/** + * Test rendering with Mono, Duo, and Multi noise filters side by side. + */ +PAGX_TEST(PAGXTest, NoiseFilterModes) { + constexpr int canvasW = 400; + constexpr int canvasH = 150; + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + auto layer1 = MakeTestLayer(doc.get(), 20, 0, 0.2f, 0.5f, 0.8f); + auto mono = doc->makeNode(); + mono->mode = pagx::NoiseMode::Mono; + mono->size = 8; + mono->density = 0.5f; + mono->seed = 42; + mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer1->filters.push_back(mono); + doc->layers.push_back(layer1); + + auto layer2 = MakeTestLayer(doc.get(), 150, 0, 0.2f, 0.5f, 0.8f); + auto duo = doc->makeNode(); + duo->mode = pagx::NoiseMode::Duo; + duo->size = 8; + duo->density = 0.5f; + duo->seed = 42; + duo->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + duo->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer2->filters.push_back(duo); + doc->layers.push_back(layer2); + + auto layer3 = MakeTestLayer(doc.get(), 280, 0, 0.2f, 0.5f, 0.8f); + auto multi = doc->makeNode(); + multi->mode = pagx::NoiseMode::Multi; + multi->size = 8; + multi->density = 0.5f; + multi->seed = 42; + multi->opacity = 1.0f; + layer3->filters.push_back(multi); + doc->layers.push_back(layer3); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterModes")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("fontFamily(); + auto fontStyle = typeface->fontStyle(); + + // Row 1: Rectangle(Mono,0), Ellipse(Duo,0.25), Path(Multi,0.5) + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(20, 20); + + auto rect = doc->makeNode(); + rect->position = {60, 60}; + rect->size = {100, 100}; + layer->contents.push_back(rect); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.2f, 0.5f, 0.8f)); + layer->filters.push_back(MakeMonoNoiseFilter(doc.get(), 0.0f)); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(150, 20); + + auto ellipse = doc->makeNode(); + ellipse->position = {60, 60}; + ellipse->size = {100, 100}; + layer->contents.push_back(ellipse); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.8f, 0.3f, 0.3f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 0.25f)); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(280, 20); + + auto path = doc->makeNode(); + path->data = doc->makeNode(); + path->data->moveTo(0, 0); + path->data->lineTo(100, 0); + path->data->lineTo(50, 100); + path->data->close(); + path->position = {10, 10}; + layer->contents.push_back(path); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.3f, 0.7f, 0.3f)); + layer->filters.push_back(MakeMultiNoiseFilter(doc.get(), 0.5f)); + doc->layers.push_back(layer); + } + + // Row 2: Polystar(Mono,0.75), Text(Duo,1), Group(Multi,0.5) + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(20, 160); + + auto polystar = doc->makeNode(); + polystar->position = {60, 60}; + polystar->outerRadius = 50; + polystar->innerRadius = 25; + polystar->pointCount = 5; + layer->contents.push_back(polystar); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.7f, 0.5f, 0.9f)); + layer->filters.push_back(MakeMonoNoiseFilter(doc.get(), 0.75f)); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(150, 160); + + auto text = doc->makeNode(); + text->text = "Hi"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; + text->fontSize = 60; + layer->contents.push_back(text); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.2f, 0.6f, 0.9f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 1.0f)); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(280, 160); + + auto group = doc->makeNode(); + group->position = {10, 10}; + auto rect = doc->makeNode(); + rect->position = {40, 40}; + rect->size = {60, 60}; + group->elements.push_back(rect); + group->elements.push_back(MakeSolidFill(doc.get(), 0.9f, 0.6f, 0.1f)); + layer->contents.push_back(group); + layer->filters.push_back(MakeMultiNoiseFilter(doc.get(), 0.5f)); + doc->layers.push_back(layer); + } + + // Row 3: TextBox(Duo,0.5), Repeater(Multi,0.5) + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(20, 320); + + auto textBox = doc->makeNode(); + textBox->width = 120; + textBox->height = 100; + auto text = doc->makeNode(); + text->text = "AB CD"; + text->fontFamily = fontFamily; + text->fontStyle = fontStyle; + text->fontSize = 30; + textBox->elements.push_back(text); + textBox->elements.push_back(MakeSolidFill(doc.get(), 0.1f, 0.4f, 0.7f)); + layer->contents.push_back(textBox); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 0.5f)); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(200, 320); + + auto rect = doc->makeNode(); + rect->position = {30, 30}; + rect->size = {40, 40}; + auto repeater = doc->makeNode(); + repeater->copies = 3; + repeater->offset = 0; + repeater->position = {50, 0}; + layer->contents.push_back(rect); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.3f, 0.8f, 0.6f)); + layer->contents.push_back(repeater); + layer->filters.push_back(MakeMultiNoiseFilter(doc.get(), 0.5f)); + doc->layers.push_back(layer); + } + + // Row 4: Off-center content (contentBounds origin != 0,0) + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(440, 20); + + auto rect = doc->makeNode(); + rect->position = {80, 40}; + rect->size = {60, 60}; + layer->contents.push_back(rect); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.6f, 0.2f, 0.8f)); + layer->filters.push_back(MakeMonoNoiseFilter(doc.get(), 0.5f)); + doc->layers.push_back(layer); + } + { + auto layer = doc->makeNode(); + layer->matrix = pagx::Matrix::Translate(580, 20); + + auto ellipse = doc->makeNode(); + ellipse->position = {40, 80}; + ellipse->size = {60, 80}; + layer->contents.push_back(ellipse); + layer->contents.push_back(MakeSolidFill(doc.get(), 0.8f, 0.7f, 0.2f)); + layer->filters.push_back(MakeDuoNoiseFilter(doc.get(), 0.5f)); + doc->layers.push_back(layer); + } + + doc->applyLayout(&fontConfig); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseFilterAllElements")); + + pagx::SVGExportOptions svgOpts; + svgOpts.fontConfig = &fontConfig; + auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("makeNode(); + + auto* rect = doc->makeNode(); + rect->position = {100, 100}; + rect->size = {200, 200}; + + auto* image = doc->makeNode(); + auto imageData = + tgfx::Data::MakeFromFile(ProjectPath::Absolute("resources/apitest/imageReplacement.png")); + ASSERT_TRUE(imageData != nullptr); + image->data = pagx::Data::MakeWithCopy(imageData->bytes(), imageData->size()); + + auto* pattern = doc->makeNode(); + pattern->image = image; + pattern->matrix = {1, 0, 0, 1, 0, 0}; + + auto* fill = doc->makeNode(); + fill->color = pattern; + + auto* noise = doc->makeNode(); + noise->mode = pagx::NoiseMode::Mono; + noise->size = 8; + noise->density = 1.0f; + noise->seed = 7; + noise->color = {0.5f, 0.5f, 0.5f, 1.0f}; + noise->blendMode = pagx::BlendMode::Multiply; + + layer->contents.push_back(rect); + layer->contents.push_back(fill); + layer->styles.push_back(noise); + doc->layers.push_back(layer); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseStyleBlendModeOnImage")); + + pagx::SVGExportOptions svgOpts; + auto svg = pagx::SVGExporter::ToSVG(*doc, svgOpts); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("feTurbulence"), std::string::npos); + EXPECT_NE(svg.find("makeNode(); + mono->mode = pagx::NoiseMode::Mono; + mono->size = 8; + mono->density = 0.5f; + mono->seed = 42; + mono->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer1->styles.push_back(mono); + doc->layers.push_back(layer1); + + auto layer2 = MakeTestLayerSimple(doc.get(), 150, 40); + auto duo = doc->makeNode(); + duo->mode = pagx::NoiseMode::Duo; + duo->size = 8; + duo->density = 0.5f; + duo->seed = 42; + duo->firstColor = {1.0f, 1.0f, 0.0f, 1.0f}; + duo->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer2->styles.push_back(duo); + doc->layers.push_back(layer2); + + auto layer3 = MakeTestLayerSimple(doc.get(), 280, 40); + auto multi = doc->makeNode(); + multi->mode = pagx::NoiseMode::Multi; + multi->size = 8; + multi->density = 0.5f; + multi->seed = 42; + multi->opacity = 1.0f; + layer3->styles.push_back(multi); + doc->layers.push_back(layer3); + + doc->applyLayout(); + auto tgfxLayer = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxLayer != nullptr); + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + DisplayList displayList; + displayList.root()->addChild(tgfxLayer); + displayList.render(surface.get(), false); + + EXPECT_TRUE(Baseline::Compare(surface, "PAGXTest/NoiseStyleModes")); + + auto svg = pagx::SVGExporter::ToSVG(*doc); + EXPECT_FALSE(svg.empty()); + EXPECT_NE(svg.find("makeNode("L"); + layer->width = 100; + layer->height = 100; + doc->layers.push_back(layer); + + auto monoFilter = doc->makeNode("NF_MONO"); + monoFilter->mode = pagx::NoiseMode::Mono; + monoFilter->size = 4; + monoFilter->density = 0.2f; + monoFilter->seed = 10; + monoFilter->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer->filters.push_back(monoFilter); + + auto duoFilter = doc->makeNode("NF_DUO"); + duoFilter->mode = pagx::NoiseMode::Duo; + duoFilter->size = 6; + duoFilter->density = 0.3f; + duoFilter->seed = 20; + duoFilter->firstColor = {1.0f, 0.0f, 0.0f, 1.0f}; + duoFilter->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer->filters.push_back(duoFilter); + + auto multiFilter = doc->makeNode("NF_MULTI"); + multiFilter->mode = pagx::NoiseMode::Multi; + multiFilter->size = 8; + multiFilter->density = 0.4f; + multiFilter->seed = 30; + multiFilter->opacity = 0.5f; + layer->filters.push_back(multiFilter); + + auto anim = doc->makeNode("a"); + anim->duration = 60; + anim->frameRate = 60; + doc->animations.push_back(anim); + + // Mono filter channels + auto* monoObj = doc->makeNode(); + monoObj->target = "NF_MONO"; + anim->objects.push_back(monoObj); + auto* monoSize = doc->makeNode>(); + monoSize->name = "size"; + monoSize->keyframes.push_back({0, 20.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSize); + auto* monoDensity = doc->makeNode>(); + monoDensity->name = "density"; + monoDensity->keyframes.push_back({0, 0.8f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoDensity); + auto* monoSeed = doc->makeNode>(); + monoSeed->name = "seed"; + monoSeed->keyframes.push_back({0, 100.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSeed); + auto* monoColor = doc->makeNode>(); + monoColor->name = "color"; + monoColor->keyframes.push_back( + {0, pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoColor); + + // Duo filter channels + auto* duoObj = doc->makeNode(); + duoObj->target = "NF_DUO"; + anim->objects.push_back(duoObj); + auto* duoFirstColor = doc->makeNode>(); + duoFirstColor->name = "firstColor"; + duoFirstColor->keyframes.push_back( + {0, pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoFirstColor); + auto* duoSecondColor = doc->makeNode>(); + duoSecondColor->name = "secondColor"; + duoSecondColor->keyframes.push_back( + {0, pagx::Color{1.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoSecondColor); + + // Multi filter channels + auto* multiObj = doc->makeNode(); + multiObj->target = "NF_MULTI"; + anim->objects.push_back(multiObj); + auto* multiOpacity = doc->makeNode>(); + multiOpacity->name = "opacity"; + multiOpacity->keyframes.push_back({0, 1.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + multiObj->channels.push_back(multiOpacity); + + auto file = pagx::PAGScene::Make(doc); + ASSERT_TRUE(file != nullptr); + auto& tree = *file->mutableBinding(); + + auto tgfxMono = tree.get(monoFilter); + ASSERT_TRUE(tgfxMono != nullptr); + auto tgfxDuo = tree.get(duoFilter); + ASSERT_TRUE(tgfxDuo != nullptr); + auto tgfxMulti = tree.get(multiFilter); + ASSERT_TRUE(tgfxMulti != nullptr); + + EXPECT_FALSE(tree.apply(monoFilter, "firstColor", + pagx::KeyValue(pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(monoFilter, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(duoFilter, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(duoFilter, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(multiFilter, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(multiFilter, "secondColor", + pagx::KeyValue(pagx::Color{1.0f, 1.0f, 1.0f, 1.0f}), 1.0f)); + + auto timeline = file->getDefaultTimeline(); + ASSERT_TRUE(timeline != nullptr); + + // Apply at mix=1.0: values fully overwritten by keyframe targets. + timeline->apply(1.0f); + + // Mono: size 4→20, density 0.2→0.8, seed 10→100, color black→red + EXPECT_FLOAT_EQ(tgfxMono->size(), 20.0f); + EXPECT_FLOAT_EQ(tgfxMono->density(), 0.8f); + EXPECT_FLOAT_EQ(tgfxMono->seed(), 100.0f); + auto monoNoise = std::static_pointer_cast(tgfxMono); + EXPECT_FLOAT_EQ(monoNoise->color().red, 1.0f); + EXPECT_FLOAT_EQ(monoNoise->color().green, 0.0f); + + // Duo: firstColor red→green, secondColor blue→yellow + auto duoNoise = std::static_pointer_cast(tgfxDuo); + EXPECT_FLOAT_EQ(duoNoise->firstColor().green, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->firstColor().red, 0.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().red, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().green, 1.0f); + + // Multi: opacity 0.5→1.0 + auto multiNoise = std::static_pointer_cast(tgfxMulti); + EXPECT_FLOAT_EQ(multiNoise->opacity(), 1.0f); + + // Apply at mix=0.5: interpolate from initial toward keyframe. + // Reset initial values by re-building. + auto file2 = pagx::PAGScene::Make(doc); + auto& tree2 = *file2->mutableBinding(); + auto tgfxMono2 = tree2.get(monoFilter); + auto timeline2 = file2->getDefaultTimeline(); + timeline2->apply(0.5f); + + // size: 4 + (20-4)*0.5 = 12 + EXPECT_FLOAT_EQ(tgfxMono2->size(), 12.0f); + // density: 0.2 + (0.8-0.2)*0.5 = 0.5 + EXPECT_FLOAT_EQ(tgfxMono2->density(), 0.5f); + // seed: 10 + (100-10)*0.5 = 55 + EXPECT_FLOAT_EQ(tgfxMono2->seed(), 55.0f); +} + +/** + * Test animation channel binding for NoiseStyle (Mono/Duo/Multi modes). + */ +PAGX_TEST(PAGXTest, ChannelNoiseStyle) { + auto doc = pagx::PAGXDocument::Make(100, 100); + auto layer = doc->makeNode("L"); + layer->width = 100; + layer->height = 100; + doc->layers.push_back(layer); + + auto monoStyle = doc->makeNode("NS_MONO"); + monoStyle->mode = pagx::NoiseMode::Mono; + monoStyle->size = 4; + monoStyle->density = 0.2f; + monoStyle->seed = 10; + monoStyle->color = {0.0f, 0.0f, 0.0f, 1.0f}; + layer->styles.push_back(monoStyle); + + auto duoStyle = doc->makeNode("NS_DUO"); + duoStyle->mode = pagx::NoiseMode::Duo; + duoStyle->size = 6; + duoStyle->density = 0.3f; + duoStyle->seed = 20; + duoStyle->firstColor = {1.0f, 0.0f, 0.0f, 1.0f}; + duoStyle->secondColor = {0.0f, 0.0f, 1.0f, 1.0f}; + layer->styles.push_back(duoStyle); + + auto multiStyle = doc->makeNode("NS_MULTI"); + multiStyle->mode = pagx::NoiseMode::Multi; + multiStyle->size = 8; + multiStyle->density = 0.4f; + multiStyle->seed = 30; + multiStyle->opacity = 0.5f; + layer->styles.push_back(multiStyle); + + auto anim = doc->makeNode("a"); + anim->duration = 60; + anim->frameRate = 60; + doc->animations.push_back(anim); + + // Mono style channels + auto* monoObj = doc->makeNode(); + monoObj->target = "NS_MONO"; + anim->objects.push_back(monoObj); + auto* monoSize = doc->makeNode>(); + monoSize->name = "size"; + monoSize->keyframes.push_back({0, 16.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSize); + auto* monoDensity = doc->makeNode>(); + monoDensity->name = "density"; + monoDensity->keyframes.push_back({0, 0.9f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoDensity); + auto* monoSeed = doc->makeNode>(); + monoSeed->name = "seed"; + monoSeed->keyframes.push_back({0, 80.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoSeed); + auto* monoColor = doc->makeNode>(); + monoColor->name = "color"; + monoColor->keyframes.push_back( + {0, pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + monoObj->channels.push_back(monoColor); + + // Duo style channels + auto* duoObj = doc->makeNode(); + duoObj->target = "NS_DUO"; + anim->objects.push_back(duoObj); + auto* duoFirstColor = doc->makeNode>(); + duoFirstColor->name = "firstColor"; + duoFirstColor->keyframes.push_back( + {0, pagx::Color{0.0f, 0.0f, 1.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoFirstColor); + auto* duoSecondColor = doc->makeNode>(); + duoSecondColor->name = "secondColor"; + duoSecondColor->keyframes.push_back( + {0, pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}, pagx::KeyframeInterpolationType::Hold, {}, {}}); + duoObj->channels.push_back(duoSecondColor); + + // Multi style channels + auto* multiObj = doc->makeNode(); + multiObj->target = "NS_MULTI"; + anim->objects.push_back(multiObj); + auto* multiOpacity = doc->makeNode>(); + multiOpacity->name = "opacity"; + multiOpacity->keyframes.push_back({0, 1.0f, pagx::KeyframeInterpolationType::Hold, {}, {}}); + multiObj->channels.push_back(multiOpacity); + + auto file = pagx::PAGScene::Make(doc); + ASSERT_TRUE(file != nullptr); + auto& tree = *file->mutableBinding(); + + auto tgfxMono = tree.get(monoStyle); + ASSERT_TRUE(tgfxMono != nullptr); + auto tgfxDuo = tree.get(duoStyle); + ASSERT_TRUE(tgfxDuo != nullptr); + auto tgfxMulti = tree.get(multiStyle); + ASSERT_TRUE(tgfxMulti != nullptr); + + EXPECT_FALSE(tree.apply(monoStyle, "firstColor", + pagx::KeyValue(pagx::Color{0.0f, 1.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(monoStyle, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(duoStyle, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(duoStyle, "opacity", pagx::KeyValue(1.0f), 1.0f)); + EXPECT_FALSE( + tree.apply(multiStyle, "color", pagx::KeyValue(pagx::Color{1.0f, 0.0f, 0.0f, 1.0f}), 1.0f)); + EXPECT_FALSE(tree.apply(multiStyle, "secondColor", + pagx::KeyValue(pagx::Color{1.0f, 1.0f, 1.0f, 1.0f}), 1.0f)); + + auto timeline = file->getDefaultTimeline(); + ASSERT_TRUE(timeline != nullptr); + + // Apply at mix=1.0 + timeline->apply(1.0f); + + // Mono: size 4→16, density 0.2→0.9, seed 10→80, color black→green + EXPECT_FLOAT_EQ(tgfxMono->size(), 16.0f); + EXPECT_FLOAT_EQ(tgfxMono->density(), 0.9f); + EXPECT_FLOAT_EQ(tgfxMono->seed(), 80.0f); + auto monoNoise = std::static_pointer_cast(tgfxMono); + EXPECT_FLOAT_EQ(monoNoise->color().green, 1.0f); + EXPECT_FLOAT_EQ(monoNoise->color().red, 0.0f); + + // Duo: firstColor red→blue, secondColor blue→green + auto duoNoise = std::static_pointer_cast(tgfxDuo); + EXPECT_FLOAT_EQ(duoNoise->firstColor().blue, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->firstColor().red, 0.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().green, 1.0f); + EXPECT_FLOAT_EQ(duoNoise->secondColor().blue, 0.0f); + + // Multi: opacity 0.5→1.0 + auto multiNoise = std::static_pointer_cast(tgfxMulti); + EXPECT_FLOAT_EQ(multiNoise->opacity(), 1.0f); + + // Apply at mix=0.5: interpolate from initial values. + auto file2 = pagx::PAGScene::Make(doc); + auto& tree2 = *file2->mutableBinding(); + auto tgfxMono2 = tree2.get(monoStyle); + auto timeline2 = file2->getDefaultTimeline(); + timeline2->apply(0.5f); + + // size: 4 + (16-4)*0.5 = 10 + EXPECT_FLOAT_EQ(tgfxMono2->size(), 10.0f); + // density: 0.2 + (0.9-0.2)*0.5 = 0.55 + EXPECT_FLOAT_EQ(tgfxMono2->density(), 0.55f); + // seed: 10 + (80-10)*0.5 = 45 + EXPECT_FLOAT_EQ(tgfxMono2->seed(), 45.0f); +} + +/** + * Render NoiseFilter/NoiseStyle animation frames across all modes and combined with shadows. + * Layout: two rectangles side-by-side, left with NoiseFilter, right with NoiseStyle. + * 12 frames total: 3 Mono, 3 Duo, 3 Multi, 3 Multi+DropShadowFilter+InnerShadowStyle. + * Density animates from 0.1 to 1.0 across all 12 frames. + * The baseline key is "NoiseFilterAnimation" (without the "Export" prefix) to avoid + * re-accepting baselines after renaming the test. + */ +PAGX_TEST(PAGXTest, ExportNoiseFilterAnimation) { + constexpr int canvasW = 500; + constexpr int canvasH = 260; + constexpr int totalFrames = 12; + constexpr int framesPerSection = 3; + + auto outDir = ProjectPath::Absolute("test/out/PAGXTest/NoiseFilterAnimation"); + auto dirPath = std::filesystem::path(outDir); + if (!std::filesystem::exists(dirPath)) { + std::filesystem::create_directories(dirPath); + } + + auto surface = Surface::Make(context, canvasW, canvasH); + ASSERT_TRUE(surface != nullptr); + + for (int i = 0; i < totalFrames; i++) { + float density = 0.1f + 0.9f * (static_cast(i) / (totalFrames - 1)); + int section = i / framesPerSection; + + auto doc = pagx::PAGXDocument::Make(canvasW, canvasH); + + // Left rectangle with NoiseFilter + auto layerL = doc->makeNode("LL"); + layerL->width = 220; + layerL->height = 220; + layerL->matrix = pagx::Matrix::Translate(10, 20); + auto rectL = doc->makeNode(); + rectL->position = {110, 110}; + rectL->size = {220, 220}; + rectL->roundness = 12; + auto fillL = doc->makeNode(); + auto solidL = doc->makeNode(); + solidL->color = {0.9f, 0.9f, 0.9f, 1.0f}; + fillL->color = solidL; + layerL->contents.push_back(rectL); + layerL->contents.push_back(fillL); + + auto noiseFilter = doc->makeNode("NF"); + noiseFilter->size = 8; + noiseFilter->density = density; + noiseFilter->seed = 42; + switch (section) { + case 0: + noiseFilter->mode = pagx::NoiseMode::Mono; + noiseFilter->color = {0.0f, 0.0f, 0.0f, 1.0f}; + break; + case 1: + noiseFilter->mode = pagx::NoiseMode::Duo; + noiseFilter->firstColor = {1.0f, 0.2f, 0.0f, 1.0f}; + noiseFilter->secondColor = {0.0f, 0.2f, 1.0f, 1.0f}; + break; + default: + noiseFilter->mode = pagx::NoiseMode::Multi; + noiseFilter->opacity = 0.8f; + break; + } + layerL->filters.push_back(noiseFilter); + + if (section == 3) { + auto shadowL = doc->makeNode(); + shadowL->offsetX = 4; + shadowL->offsetY = 4; + shadowL->blurX = 6; + shadowL->blurY = 6; + shadowL->color = {0.0f, 0.0f, 0.0f, 0.6f}; + layerL->filters.push_back(shadowL); + + auto innerShadowL = doc->makeNode(); + innerShadowL->offsetX = 3; + innerShadowL->offsetY = 3; + innerShadowL->blurX = 8; + innerShadowL->blurY = 8; + innerShadowL->color = {0.0f, 0.0f, 0.0f, 0.5f}; + layerL->styles.push_back(innerShadowL); + } + + doc->layers.push_back(layerL); + + // Right rectangle with NoiseStyle + auto layerR = doc->makeNode("LR"); + layerR->width = 220; + layerR->height = 220; + layerR->matrix = pagx::Matrix::Translate(270, 20); + auto rectR = doc->makeNode(); + rectR->position = {110, 110}; + rectR->size = {220, 220}; + rectR->roundness = 12; + auto fillR = doc->makeNode(); + auto solidR = doc->makeNode(); + solidR->color = {0.9f, 0.9f, 0.9f, 1.0f}; + fillR->color = solidR; + layerR->contents.push_back(rectR); + layerR->contents.push_back(fillR); + + auto noiseStyle = doc->makeNode("NS"); + noiseStyle->size = 8; + noiseStyle->density = density; + noiseStyle->seed = 42; + switch (section) { + case 0: + noiseStyle->mode = pagx::NoiseMode::Mono; + noiseStyle->color = {0.0f, 0.0f, 0.0f, 1.0f}; + break; + case 1: + noiseStyle->mode = pagx::NoiseMode::Duo; + noiseStyle->firstColor = {1.0f, 0.2f, 0.0f, 1.0f}; + noiseStyle->secondColor = {0.0f, 0.2f, 1.0f, 1.0f}; + break; + default: + noiseStyle->mode = pagx::NoiseMode::Multi; + noiseStyle->opacity = 0.8f; + break; + } + layerR->styles.push_back(noiseStyle); + + if (section == 3) { + auto shadowR = doc->makeNode(); + shadowR->offsetX = 4; + shadowR->offsetY = 4; + shadowR->blurX = 6; + shadowR->blurY = 6; + shadowR->color = {0.0f, 0.0f, 0.0f, 0.6f}; + layerR->filters.push_back(shadowR); + + auto innerShadowR = doc->makeNode(); + innerShadowR->offsetX = 3; + innerShadowR->offsetY = 3; + innerShadowR->blurX = 8; + innerShadowR->blurY = 8; + innerShadowR->color = {0.0f, 0.0f, 0.0f, 0.5f}; + layerR->styles.push_back(innerShadowR); + } + + doc->layers.push_back(layerR); + + doc->applyLayout(); + auto tgfxRoot = pagx::LayerBuilder::Build(doc.get()); + ASSERT_TRUE(tgfxRoot != nullptr); + + tgfx::DisplayList displayList; + displayList.root()->addChild(tgfxRoot); + displayList.render(surface.get(), true); + + auto key = "PAGXTest/NoiseFilterAnimation/frame_" + std::to_string(i); + EXPECT_TRUE(Baseline::Compare(surface, key)); + } +} + } // namespace pag