فهرست منبع

Add support for 'mask-image' property

Michael Ragazzon 2 سال پیش
والد
کامیت
0471ce737e

+ 70 - 4
Backends/RmlUi_Renderer_GL3.cpp

@@ -241,6 +241,19 @@ void main() {
 	finalColor = _color_matrix * texColor;
 }
 )";
+static const char* shader_frag_blend_mask = RMLUI_SHADER_HEADER R"(
+uniform sampler2D _tex;
+uniform sampler2D _texMask;
+
+in vec2 fragTexCoord;
+out vec4 finalColor;
+
+void main() {
+	vec4 texColor = texture(_tex, fragTexCoord);
+	float maskAlpha = texture(_texMask, fragTexCoord).a;
+	finalColor = texColor * maskAlpha;
+}
+)";
 
 #define RMLUI_SHADER_BLUR_HEADER \
 	RMLUI_SHADER_HEADER "\n#define BLUR_SIZE " RMLUI_STRINGIFY(BLUR_SIZE) "\n#define BLUR_NUM_WEIGHTS " RMLUI_STRINGIFY(BLUR_NUM_WEIGHTS)
@@ -301,6 +314,7 @@ enum class ProgramId {
 	Creation,
 	Passthrough,
 	ColorMatrix,
+	BlendMask,
 	Blur,
 	DropShadow,
 	Count,
@@ -318,6 +332,7 @@ enum class FragShaderId {
 	Creation,
 	Passthrough,
 	ColorMatrix,
+	BlendMask,
 	Blur,
 	DropShadow,
 	Count,
@@ -331,6 +346,7 @@ enum class UniformId {
 	TexelOffset,
 	TexCoordMin,
 	TexCoordMax,
+	TexMask,
 	Weights,
 	Func,
 	P,
@@ -346,8 +362,8 @@ enum class UniformId {
 namespace Gfx {
 
 static const char* const program_uniform_names[(size_t)UniformId::Count] = {"_translate", "_transform", "_tex", "_color", "_color_matrix",
-	"_texelOffset", "_texCoordMin", "_texCoordMax", "_weights[0]", "_func", "_p", "_v", "_stop_colors[0]", "_stop_positions[0]", "_num_stops",
-	"_value", "_dimensions"};
+	"_texelOffset", "_texCoordMin", "_texCoordMax", "_texMask", "_weights[0]", "_func", "_p", "_v", "_stop_colors[0]", "_stop_positions[0]",
+	"_num_stops", "_value", "_dimensions"};
 
 enum class VertexAttribute { Position, Color0, TexCoord0, Count };
 static const char* const vertex_attribute_names[(size_t)VertexAttribute::Count] = {"inPosition", "inColor0", "inTexCoord0"};
@@ -382,6 +398,7 @@ static const FragShaderDefinition frag_shader_definitions[] = {
 	{FragShaderId::Creation,    "creation",     shader_frag_creation},
 	{FragShaderId::Passthrough, "passthrough",  shader_frag_passthrough},
 	{FragShaderId::ColorMatrix, "color_matrix", shader_frag_color_matrix},
+	{FragShaderId::BlendMask,   "blend_mask",   shader_frag_blend_mask},
 	{FragShaderId::Blur,        "blur",         shader_frag_blur},
 	{FragShaderId::DropShadow,  "drop_shadow",  shader_frag_drop_shadow},
 };
@@ -392,6 +409,7 @@ static const ProgramDefinition program_definitions[] = {
 	{ProgramId::Creation,    "creation",     VertShaderId::Main,        FragShaderId::Creation},
 	{ProgramId::Passthrough, "passthrough",  VertShaderId::Passthrough, FragShaderId::Passthrough},
 	{ProgramId::ColorMatrix, "color_matrix", VertShaderId::Passthrough, FragShaderId::ColorMatrix},
+	{ProgramId::BlendMask,   "blend_mask",   VertShaderId::Passthrough, FragShaderId::BlendMask},
 	{ProgramId::Blur,        "blur",         VertShaderId::Blur,        FragShaderId::Blur},
 	{ProgramId::DropShadow,  "drop_shadow",  VertShaderId::Passthrough, FragShaderId::DropShadow},
 };
@@ -736,6 +754,9 @@ static bool CreateShaders(ProgramData& data)
 			return ReportError("program", def.name_str);
 	}
 
+	glUseProgram(data.programs[ProgramId::BlendMask]);
+	glUniform1i(data.uniforms.Get(ProgramId::BlendMask, UniformId::TexMask), 1);
+
 	glUseProgram(0);
 
 	return true;
@@ -1478,7 +1499,7 @@ void RenderInterface_GL3::SetTransform(const Rml::Matrix4f* new_transform)
 	program_transform_dirty.set();
 }
 
-enum class FilterType { Invalid = 0, Passthrough, Blur, DropShadow, ColorMatrix };
+enum class FilterType { Invalid = 0, Passthrough, Blur, DropShadow, ColorMatrix, MaskImage };
 struct CompiledFilter {
 	FilterType type;
 
@@ -1856,6 +1877,28 @@ void RenderInterface_GL3::RenderFilters(const Rml::FilterHandleList& filter_hand
 			glEnable(GL_BLEND);
 		}
 		break;
+		case FilterType::MaskImage:
+		{
+			UseProgram(ProgramId::BlendMask);
+			glDisable(GL_BLEND);
+
+			const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary();
+			const Gfx::FramebufferData& blend_mask = render_layers.GetBlendMask();
+			const Gfx::FramebufferData& destination = render_layers.GetPostprocessSecondary();
+
+			Gfx::BindTexture(source);
+			glActiveTexture(GL_TEXTURE1);
+			Gfx::BindTexture(blend_mask);
+			glActiveTexture(GL_TEXTURE0);
+
+			glBindFramebuffer(GL_FRAMEBUFFER, destination.framebuffer);
+
+			DrawFullscreenQuad();
+
+			render_layers.SwapPostprocessPrimarySecondary();
+			glEnable(GL_BLEND);
+		}
+		break;
 		case FilterType::Invalid:
 		{
 			Rml::Log::Message(Rml::Log::LT_WARNING, "Unhandled render filter %d.", (int)type);
@@ -1960,6 +2003,29 @@ Rml::TextureHandle RenderInterface_GL3::SaveLayerAsTexture(Rml::Vector2i dimensi
 	return render_texture;
 }
 
+Rml::CompiledFilterHandle RenderInterface_GL3::SaveLayerAsMaskImage()
+{
+	BlitTopLayerToPostprocessPrimary();
+
+	const Gfx::FramebufferData& source = render_layers.GetPostprocessPrimary();
+	const Gfx::FramebufferData& destination = render_layers.GetBlendMask();
+
+	glBindFramebuffer(GL_FRAMEBUFFER, destination.framebuffer);
+	BindTexture(source);
+	UseProgram(ProgramId::Passthrough);
+	glDisable(GL_BLEND);
+
+	DrawFullscreenQuad();
+
+	glEnable(GL_BLEND);
+	glBindFramebuffer(GL_FRAMEBUFFER, render_layers.GetTopLayer().framebuffer);
+	Gfx::CheckGLError("SaveLayerAsMaskImage");
+
+	CompiledFilter filter = {};
+	filter.type = FilterType::MaskImage;
+	return reinterpret_cast<Rml::CompiledFilterHandle>(new CompiledFilter(std::move(filter)));
+}
+
 void RenderInterface_GL3::UseProgram(ProgramId program_id)
 {
 	RMLUI_ASSERT(program_data);
@@ -1994,7 +2060,7 @@ void RenderInterface_GL3::SubmitTransformUniform(Rml::Vector2f translation)
 
 RenderInterface_GL3::RenderLayerStack::RenderLayerStack()
 {
-	fb_postprocess.resize(3);
+	fb_postprocess.resize(4);
 }
 
 RenderInterface_GL3::RenderLayerStack::~RenderLayerStack()

+ 3 - 0
Backends/RmlUi_Renderer_GL3.h

@@ -86,6 +86,8 @@ public:
 
 	Rml::TextureHandle SaveLayerAsTexture(Rml::Vector2i dimensions) override;
 
+	Rml::CompiledFilterHandle SaveLayerAsMaskImage() override;
+
 	Rml::CompiledFilterHandle CompileFilter(const Rml::String& name, const Rml::Dictionary& parameters) override;
 	void ReleaseCompiledFilter(Rml::CompiledFilterHandle filter) override;
 
@@ -158,6 +160,7 @@ private:
 		const Gfx::FramebufferData& GetPostprocessPrimary() { return EnsureFramebufferPostprocess(0); }
 		const Gfx::FramebufferData& GetPostprocessSecondary() { return EnsureFramebufferPostprocess(1); }
 		const Gfx::FramebufferData& GetPostprocessTertiary() { return EnsureFramebufferPostprocess(2); }
+		const Gfx::FramebufferData& GetBlendMask() { return EnsureFramebufferPostprocess(3); }
 
 		void SwapPostprocessPrimarySecondary();
 

+ 4 - 1
Include/RmlUi/Core/ComputedValues.h

@@ -159,7 +159,7 @@ namespace Style {
 
 			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None), overscroll_behavior(OverscrollBehavior::Auto),
 
-			has_filter(false), has_backdrop_filter(false), has_box_shadow(false)
+			has_mask_image(false), has_filter(false), has_backdrop_filter(false), has_box_shadow(false)
 		{}
 
 		LengthPercentage::Type min_width_type : 1, max_width_type : 1;
@@ -177,6 +177,7 @@ namespace Style {
 		TabIndex tab_index : 1;
 		OverscrollBehavior overscroll_behavior : 1;
 
+		bool has_mask_image : 1;
 		bool has_filter : 1;
 		bool has_backdrop_filter : 1;
 		bool has_box_shadow : 1;
@@ -304,6 +305,7 @@ namespace Style {
 		LengthPercentage  column_gap()                 const { return LengthPercentage(rare.column_gap_type, rare.column_gap); }
 		OverscrollBehavior overscroll_behavior()       const { return rare.overscroll_behavior; }
 		float             scrollbar_margin()           const { return rare.scrollbar_margin; }
+		bool              has_mask_image()             const { return rare.has_mask_image; }
 		bool              has_filter()                 const { return rare.has_filter; }
 		bool              has_backdrop_filter()        const { return rare.has_backdrop_filter; }
 		bool              has_box_shadow()             const { return rare.has_box_shadow; }
@@ -387,6 +389,7 @@ namespace Style {
 		void image_color               (Colourb value)           { rare.image_color                = value; }
 		void overscroll_behavior       (OverscrollBehavior value){ rare.overscroll_behavior        = value; }
 		void scrollbar_margin          (float value)             { rare.scrollbar_margin           = value; }
+		void has_mask_image            (bool value)              { rare.has_mask_image             = value; }
 		void has_filter                (bool value)              { rare.has_filter                 = value; }
 		void has_backdrop_filter       (bool value)              { rare.has_backdrop_filter        = value; }
 		void has_box_shadow            (bool value)              { rare.has_box_shadow             = value; }

+ 2 - 0
Include/RmlUi/Core/ID.h

@@ -155,7 +155,9 @@ enum class PropertyId : uint8_t {
 	Focus,
 
 	Decorator,
+	MaskImage,
 	FontEffect,
+
 	Filter,
 	BackdropFilter,
 	BoxShadow,

+ 7 - 1
Include/RmlUi/Core/RenderInterface.h

@@ -130,7 +130,7 @@ public:
 	/// @return True if the texture generation succeeded and the handle is valid, false if not.
 	virtual bool GenerateTexture(TextureHandle& texture_handle, const byte* source, const Vector2i& source_dimensions);
 	/// Called by RmlUi when a loaded texture is no longer required.
-	/// @param texture The texture handle to release.
+	/// @param[in] texture The texture handle to release.
 	virtual void ReleaseTexture(TextureHandle texture);
 
 	/// Called by RmlUi when it wants the renderer to use a new transform matrix.
@@ -152,9 +152,14 @@ public:
 	/// @return The handle to the new texture.
 	virtual TextureHandle SaveLayerAsTexture(Vector2i dimensions);
 
+	/// Called by RmlUi when it wants to store the current layer as a mask image, to be applied later as a filter.
+	/// @return The handle to a new filter representng the stored mask image.
+	virtual CompiledFilterHandle SaveLayerAsMaskImage();
+
 	/// Called by RmlUi when it wants to compile a new filter.
 	/// @param[in] name The name of the filter.
 	/// @param[in] parameters The list of name-value parameters specified for the filter.
+	/// @return The handle representing the compiled filter.
 	virtual CompiledFilterHandle CompileFilter(const String& name, const Dictionary& parameters);
 	/// Called by RmlUi when it no longer needs a previously compiled filter.
 	/// @param[in] filter The handle to a previously compiled filter.
@@ -163,6 +168,7 @@ public:
 	/// Called by RmlUi when it wants to compile a new shader.
 	/// @param[in] name The name of the shader.
 	/// @param[in] parameters The list of name-value parameters specified for the filter.
+	/// @return The handle representing the compiled shader.
 	virtual CompiledShaderHandle CompileShader(const String& name, const Dictionary& parameters);
 	/// Called by RmlUi when it wants to render geometry using the given shader.
 	/// @param[in] shader The handle to a previously compiled shader.

+ 5 - 0
Samples/basic/effect/data/effect.rml

@@ -63,6 +63,10 @@
 
 .transform, .filter.transform_all > .box { transform: rotate3d(0.2, 0.4, 0.1, 15deg); }
 
+.mask {
+	decorator: horizontal-gradient(#f00 #ff0);
+	mask-image: image(icon-invader scale-none 15px 50%), horizontal-gradient(#0000 #000f);
+}
 .shader { decorator: shader("creation"); }
 .gradient { decorator: linear-gradient(110deg, #fff3, #fff 10%, #c33 250dp, #3c3, #33c, #000 90%, #0003) border-box; }
 
@@ -159,6 +163,7 @@
 <div class="box"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box big shader"><div class="label">"Creation" (Danilo Guanabara)</div></div>
 <div class="box big gradient"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+<div class="box mask"><div class="placeholder"/>Hello, do you feel the funk?</div>
 
 <div class="box hue_rotate"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 

+ 5 - 4
Source/Core/Element.cpp

@@ -1731,16 +1731,17 @@ void Element::OnPropertyChange(const PropertyIdSet& changed_properties)
 		changed_properties.Contains(PropertyId::BorderBottomRightRadius) || //
 		changed_properties.Contains(PropertyId::BorderBottomLeftRadius)     //
 	);
-	const bool filter_changed = (changed_properties.Contains(PropertyId::Filter) || changed_properties.Contains(PropertyId::BackdropFilter));
+	const bool filter_or_mask_changed = (changed_properties.Contains(PropertyId::Filter) || changed_properties.Contains(PropertyId::BackdropFilter) ||
+		changed_properties.Contains(PropertyId::MaskImage));
 
 	// Update the z-index and stacking context.
-	if (changed_properties.Contains(PropertyId::ZIndex) || filter_changed)
+	if (changed_properties.Contains(PropertyId::ZIndex) || filter_or_mask_changed)
 	{
 		const Style::ZIndex z_index_property = meta->computed_values.z_index();
 
 		const float new_z_index = (z_index_property.type == Style::ZIndex::Auto ? 0.f : z_index_property.value);
 		const bool enable_local_stacking_context = (z_index_property.type != Style::ZIndex::Auto || local_stacking_context_forced ||
-			meta->computed_values.has_filter() || meta->computed_values.has_backdrop_filter());
+			meta->computed_values.has_filter() || meta->computed_values.has_backdrop_filter() || meta->computed_values.has_mask_image());
 
 		if (z_index != new_z_index || local_stacking_context != enable_local_stacking_context)
 		{
@@ -1788,7 +1789,7 @@ void Element::OnPropertyChange(const PropertyIdSet& changed_properties)
 	}
 
 	// Dirty the decoration if it's changed.
-	if (border_radius_changed || filter_changed || changed_properties.Contains(PropertyId::Decorator))
+	if (border_radius_changed || filter_or_mask_changed || changed_properties.Contains(PropertyId::Decorator))
 	{
 		meta->decoration.DirtyDecorators();
 	}

+ 73 - 42
Source/Core/ElementDecoration.cpp

@@ -60,49 +60,55 @@ void ElementDecoration::InstanceDecorators()
 
 	const ComputedValues& computed = element->GetComputedValues();
 
-	if (computed.has_decorator())
+	if (computed.has_decorator() || computed.has_mask_image())
 	{
-		const Property* property = element->GetLocalProperty(PropertyId::Decorator);
-		if (!property || property->unit != Unit::DECORATOR)
-			return;
-
-		DecoratorsPtr decorators_ptr = property->Get<DecoratorsPtr>();
-		if (!decorators_ptr)
-			return;
-
 		const StyleSheet* style_sheet = element->GetStyleSheet();
 		if (!style_sheet)
 			return;
 
-		PropertySource document_source("", 0, "");
-		const PropertySource* source = property->source.get();
-
-		if (!source)
+		for (const auto id : {PropertyId::Decorator, PropertyId::MaskImage})
 		{
-			if (ElementDocument* document = element->GetOwnerDocument())
+			const Property* property = element->GetLocalProperty(id);
+			if (!property || property->unit != Unit::DECORATOR)
+				continue;
+
+			DecoratorsPtr decorators_ptr = property->Get<DecoratorsPtr>();
+			if (!decorators_ptr)
+				continue;
+
+			PropertySource document_source("", 0, "");
+			const PropertySource* source = property->source.get();
+
+			if (!source)
 			{
-				document_source.path = document->GetSourceURL();
-				source = &document_source;
+				if (ElementDocument* document = element->GetOwnerDocument())
+				{
+					document_source.path = document->GetSourceURL();
+					source = &document_source;
+				}
 			}
-		}
 
-		const DecoratorPtrList& decorator_list = style_sheet->InstanceDecorators(*decorators_ptr, source);
-		RMLUI_ASSERT(decorator_list.empty() || decorator_list.size() == decorators_ptr->list.size());
+			const DecoratorPtrList& decorator_list = style_sheet->InstanceDecorators(*decorators_ptr, source);
+			RMLUI_ASSERT(decorator_list.empty() || decorator_list.size() == decorators_ptr->list.size());
 
-		for (size_t i = 0; i < decorator_list.size() && i < decorators_ptr->list.size(); i++)
-		{
-			const SharedPtr<const Decorator>& decorator = decorator_list[i];
-			if (decorator)
+			DecoratorEntryList& decorators_target = (id == PropertyId::Decorator ? decorators : mask_images);
+			decorators_target.reserve(decorators_ptr->list.size());
+
+			for (size_t i = 0; i < decorator_list.size() && i < decorators_ptr->list.size(); i++)
 			{
-				DecoratorEntry entry;
-				entry.decorator_data = 0;
-				entry.decorator = decorator;
-				entry.paint_area = decorators_ptr->list[i].paint_area;
-				if (entry.paint_area == BoxArea::Auto)
-					entry.paint_area = BoxArea::Padding;
-
-				RMLUI_ASSERT(entry.paint_area >= BoxArea::Border && entry.paint_area <= BoxArea::Content);
-				decorators.push_back(std::move(entry));
+				const SharedPtr<const Decorator>& decorator = decorator_list[i];
+				if (decorator)
+				{
+					DecoratorEntry entry;
+					entry.decorator_data = 0;
+					entry.decorator = decorator;
+					entry.paint_area = decorators_ptr->list[i].paint_area;
+					if (entry.paint_area == BoxArea::Auto)
+						entry.paint_area = (id == PropertyId::Decorator ? BoxArea::Padding : BoxArea::Border);
+
+					RMLUI_ASSERT(entry.paint_area >= BoxArea::Border && entry.paint_area <= BoxArea::Content);
+					decorators_target.push_back(std::move(entry));
+				}
 			}
 		}
 	}
@@ -146,12 +152,15 @@ void ElementDecoration::ReloadDecoratorsData()
 	{
 		decorators_data_dirty = false;
 
-		for (DecoratorEntry& decorator : decorators)
+		for (DecoratorEntryList* list : {&decorators, &mask_images})
 		{
-			if (decorator.decorator_data)
-				decorator.decorator->ReleaseElementData(decorator.decorator_data);
+			for (DecoratorEntry& decorator : *list)
+			{
+				if (decorator.decorator_data)
+					decorator.decorator->ReleaseElementData(decorator.decorator_data);
 
-			decorator.decorator_data = decorator.decorator->GenerateElementData(element, decorator.paint_area);
+				decorator.decorator_data = decorator.decorator->GenerateElementData(element, decorator.paint_area);
+			}
 		}
 
 		for (FilterEntryList* list : {&filters, &backdrop_filters})
@@ -169,12 +178,15 @@ void ElementDecoration::ReloadDecoratorsData()
 
 void ElementDecoration::ReleaseDecorators()
 {
-	for (DecoratorEntry& decorator : decorators)
+	for (DecoratorEntryList* list : {&decorators, &mask_images})
 	{
-		if (decorator.decorator_data)
-			decorator.decorator->ReleaseElementData(decorator.decorator_data);
+		for (DecoratorEntry& decorator : *list)
+		{
+			if (decorator.decorator_data)
+				decorator.decorator->ReleaseElementData(decorator.decorator_data);
+		}
+		list->clear();
 	}
-	decorators.clear();
 
 	for (FilterEntryList* list : {&filters, &backdrop_filters})
 	{
@@ -206,7 +218,7 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 		}
 	}
 
-	if (filters.empty() && backdrop_filters.empty())
+	if (filters.empty() && backdrop_filters.empty() && mask_images.empty())
 		return;
 
 	RenderInterface* render_interface = ::Rml::GetRenderInterface();
@@ -262,7 +274,7 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 		}
 	}
 
-	if (!filters.empty())
+	if (!filters.empty() || !mask_images.empty())
 	{
 		if (render_stage == RenderStage::Enter)
 		{
@@ -272,15 +284,34 @@ void ElementDecoration::RenderDecorators(RenderStage render_stage)
 		{
 			ApplyClippingRegion(PropertyId::Filter);
 
+			CompiledFilterHandle mask_image_handle = {};
 			FilterHandleList filter_handles;
+
 			for (auto& filter : filters)
 			{
 				if (filter.handle)
 					filter_handles.push_back(filter.handle);
 			}
 
+			if (!mask_images.empty())
+			{
+				render_interface->PushLayer(LayerFill::Clear);
+
+				for (int i = (int)mask_images.size() - 1; i >= 0; i--)
+				{
+					DecoratorEntry& mask_image = mask_images[i];
+					mask_image.decorator->RenderElement(element, mask_image.decorator_data);
+				}
+				mask_image_handle = render_interface->SaveLayerAsMaskImage();
+				if (mask_image_handle)
+					filter_handles.push_back(mask_image_handle);
+				render_interface->PopLayer(BlendMode::Discard, {});
+			}
+
 			render_interface->PopLayer(BlendMode::Blend, filter_handles);
 
+			if (mask_image_handle)
+				render_interface->ReleaseCompiledFilter(mask_image_handle);
 			render_manager.SetScissorRegion(initial_scissor_region);
 		}
 	}

+ 1 - 0
Source/Core/ElementDecoration.h

@@ -84,6 +84,7 @@ private:
 
 	// The list of decorators and filters used by this element from all style rules.
 	DecoratorEntryList decorators;
+	DecoratorEntryList mask_images;
 	FilterEntryList filters;
 	FilterEntryList backdrop_filters;
 

+ 3 - 0
Source/Core/ElementStyle.cpp

@@ -849,6 +849,9 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 		case PropertyId::Decorator:
 			values.has_decorator(p->unit == Unit::DECORATOR);
 			break;
+		case PropertyId::MaskImage:
+			values.has_mask_image(p->unit == Unit::DECORATOR);
+			break;
 		case PropertyId::FontEffect:
 			values.has_font_effect(p->unit == Unit::FONTEFFECT);
 			break;

+ 5 - 0
Source/Core/RenderInterface.cpp

@@ -78,6 +78,11 @@ TextureHandle RenderInterface::SaveLayerAsTexture(Vector2i /*dimensions*/)
 	return TextureHandle{};
 }
 
+CompiledFilterHandle RenderInterface::SaveLayerAsMaskImage()
+{
+	return CompiledFilterHandle{};
+}
+
 CompiledFilterHandle RenderInterface::CompileFilter(const String& /*name*/, const Dictionary& /*parameters*/)
 {
 	return CompiledFilterHandle{};

+ 1 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -414,6 +414,7 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 
 	// Decorators and effects
 	RegisterProperty(PropertyId::Decorator, "decorator", "", false, false).AddParser("decorator");
+	RegisterProperty(PropertyId::MaskImage, "mask-image", "", false, false).AddParser("decorator");
 	RegisterProperty(PropertyId::FontEffect, "font-effect", "", true, false).AddParser("font_effect");
 		
 	RegisterProperty(PropertyId::Filter, "filter", "", false, false).AddParser("filter", "filter");