Browse Source

Add animation support for filters

Michael Ragazzon 2 years ago
parent
commit
c6f550dabe

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

@@ -107,6 +107,14 @@
 }
 .boxshadow_inset { box-shadow: #f4fd 10px 5px 5px 3px inset; }
 
+@keyframes animate-filter {
+	from { filter: drop-shadow(#f00) opacity(1.0) sepia(1.0); }
+	to   { filter: drop-shadow(#000 30px 20px 5px) opacity(0.2) sepia(0.2); }
+}
+.animate {
+	animation: animate-filter 1.5s cubic-in-out infinite alternate;
+}
+
 </style>
 </head>
 <body
@@ -167,8 +175,10 @@
 
 <div class="box hue_rotate"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 
+<div class="box animate"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box saturate"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box invert"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
+<div class="box blur"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 
 <div class="box brightness"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>
 <div class="box contrast"><img sprite="icon-invader"/>Hello, do you feel the funk?</div>

+ 165 - 94
Source/Core/ElementAnimation.cpp

@@ -29,6 +29,7 @@
 #include "ElementAnimation.h"
 #include "../../Include/RmlUi/Core/Decorator.h"
 #include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/Filter.h"
 #include "../../Include/RmlUi/Core/PropertyDefinition.h"
 #include "../../Include/RmlUi/Core/PropertySpecification.h"
 #include "../../Include/RmlUi/Core/StyleSheet.h"
@@ -42,6 +43,8 @@
 
 namespace Rml {
 
+static Property InterpolateProperties(const Property& p0, const Property& p1, float alpha, Element& element, const PropertyDefinition* definition);
+
 static Colourf ColourToLinearSpace(Colourb c)
 {
 	Colourf result;
@@ -88,8 +91,96 @@ static bool CombineAndDecompose(Transform& t, Element& e)
 	return true;
 }
 
+/**
+    An abstraction for decorator and filter declarations.
+ */
+struct EffectDeclarationView {
+	EffectDeclarationView() = default;
+	EffectDeclarationView(const DecoratorDeclaration& declaration) :
+		instancer(declaration.instancer), type(&declaration.type), properties(&declaration.properties), paint_area(declaration.paint_area)
+	{}
+	EffectDeclarationView(const NamedDecorator* named_decorator) :
+		instancer(Factory::GetDecoratorInstancer(named_decorator->type)), type(&named_decorator->type), properties(&named_decorator->properties)
+	{}
+	EffectDeclarationView(const FilterDeclaration& declaration) :
+		instancer(declaration.instancer), type(&declaration.type), properties(&declaration.properties)
+	{}
+
+	EffectSpecification* instancer = nullptr;
+	const String* type = nullptr;
+	const PropertyDictionary* properties = nullptr;
+	BoxArea paint_area = BoxArea::Auto;
+
+	explicit operator bool() const { return instancer != nullptr; }
+};
+
+// Interpolate two effect declarations. One of them can be empty, in which case the empty one is replaced by default values.
+static bool InterpolateEffectProperties(PropertyDictionary& properties, const EffectDeclarationView& d0, const EffectDeclarationView& d1, float alpha,
+	Element& element)
+{
+	if (d0 && d1)
+	{
+		// Both declarations are specified, check if they are compatible for interpolation.
+		if (!d0.instancer || d0.instancer != d1.instancer || *d0.type != *d1.type ||
+			d0.properties->GetNumProperties() != d1.properties->GetNumProperties() || d0.paint_area != d1.paint_area)
+			return false;
+
+		const auto& properties0 = d0.properties->GetProperties();
+		const auto& properties1 = d1.properties->GetProperties();
+
+		for (const auto& pair0 : properties0)
+		{
+			const PropertyId id = pair0.first;
+			const Property& prop0 = pair0.second;
+
+			auto it = properties1.find(id);
+			if (it == properties1.end())
+			{
+				RMLUI_ERRORMSG("Incompatible decorator properties.");
+				return false;
+			}
+			const Property& prop1 = it->second;
+
+			Property p = InterpolateProperties(prop0, prop1, alpha, element, prop0.definition);
+			p.definition = prop0.definition;
+			properties.SetProperty(id, p);
+		}
+		return true;
+	}
+	else if ((d0 && !d1) || (!d0 && d1))
+	{
+		// One of the declarations is empty, interpolate against the default values of its type.
+		const auto& d_filled = (d0 ? d0 : d1);
+
+		const PropertySpecification& specification = d_filled.instancer->GetPropertySpecification();
+		const PropertyMap& properties_filled = d_filled.properties->GetProperties();
+
+		for (const auto& pair_filled : properties_filled)
+		{
+			const PropertyId id = pair_filled.first;
+			const PropertyDefinition* underlying_definition = specification.GetProperty(id);
+			if (!underlying_definition)
+				return false;
+
+			const Property& p_filled = pair_filled.second;
+			const Property& p_default = *underlying_definition->GetDefaultValue();
+			const Property& p_interp0 = (d0 ? p_filled : p_default);
+			const Property& p_interp1 = (d1 ? p_filled : p_default);
+
+			Property p = InterpolateProperties(p_interp0, p_interp1, alpha, element, p_filled.definition);
+			p.definition = p_filled.definition;
+			properties.SetProperty(id, p);
+		}
+		return true;
+	}
+
+	return false;
+}
+
 static Property InterpolateProperties(const Property& p0, const Property& p1, float alpha, Element& element, const PropertyDefinition* definition)
 {
+	const Property& p_discrete = (alpha < 0.5f ? p0 : p1);
+
 	if (Any(p0.unit & Unit::NUMBER_LENGTH_PERCENT) && Any(p1.unit & Unit::NUMBER_LENGTH_PERCENT))
 	{
 		if (p0.unit == p1.unit || !definition)
@@ -132,7 +223,7 @@ static Property InterpolateProperties(const Property& p0, const Property& p1, fl
 				return alpha <= 0.f ? p0 : p1;
 		}
 
-		return alpha < 0.5f ? p0 : p1;
+		return p_discrete;
 	}
 
 	if (p0.unit == Unit::COLOUR && p1.unit == Unit::COLOUR)
@@ -177,40 +268,29 @@ static Property InterpolateProperties(const Property& p0, const Property& p1, fl
 		return Property{TransformPtr(std::move(t)), Unit::TRANSFORM};
 	}
 
-	struct DecoratorDeclarationView {
-		DecoratorDeclarationView(const DecoratorDeclaration& declaration) :
-			type(declaration.type), instancer(declaration.instancer), properties(declaration.properties), paint_area(declaration.paint_area)
-		{}
-		DecoratorDeclarationView(const NamedDecorator* specification) :
-			type(specification->type), instancer(Factory::GetDecoratorInstancer(specification->type)), properties(specification->properties)
-		{}
-		const String& type;
-		DecoratorInstancer* instancer;
-		const PropertyDictionary& properties;
-		BoxArea paint_area = BoxArea::Auto;
-	};
-
 	if (p0.unit == Unit::DECORATOR && p1.unit == Unit::DECORATOR)
 	{
-		auto DiscreteInterpolation = [&]() { return alpha < 0.5f ? p0 : p1; };
+		auto GetEffectDeclarationView = [](const Vector<DecoratorDeclaration>& declarations, size_t i, Element& element) -> EffectDeclarationView {
+			if (i >= declarations.size())
+				return EffectDeclarationView();
 
-		// If we have an instancer we pass that directly to the declaration view, otherwise look for a named @decorator.
-		auto GetDecoratorDeclarationView = [&](const DecoratorDeclaration& declaration) -> DecoratorDeclarationView {
+			const DecoratorDeclaration& declaration = declarations[i];
 			if (declaration.instancer)
-				return DecoratorDeclarationView{declaration};
+				return EffectDeclarationView(declaration);
 
+			// If we don't have a decorator instancer, then this should be a named @decorator, look for one now.
 			const StyleSheet* style_sheet = element.GetStyleSheet();
 			if (!style_sheet)
-				return DecoratorDeclarationView{declaration};
+				return EffectDeclarationView();
 
 			const NamedDecorator* named_decorator = style_sheet->GetNamedDecorator(declaration.type);
 			if (!named_decorator)
 			{
 				Log::Message(Log::LT_WARNING, "Could not find a named @decorator '%s'.", declaration.type.c_str());
-				return DecoratorDeclarationView{declaration};
+				return EffectDeclarationView();
 			}
 
-			return DecoratorDeclarationView{named_decorator};
+			return EffectDeclarationView(named_decorator);
 		};
 
 		auto& ptr0 = p0.value.GetReference<DecoratorsPtr>();
@@ -218,95 +298,72 @@ static Property InterpolateProperties(const Property& p0, const Property& p1, fl
 		if (!ptr0 || !ptr1)
 		{
 			RMLUI_ERRORMSG("Invalid decorator pointer, were the decorator keys properly prepared?");
-			return DiscreteInterpolation();
+			return p_discrete;
 		}
 
-		const bool p0_smaller = (ptr0->list.size() < ptr1->list.size());
-		auto& small = (p0_smaller ? ptr0->list : ptr1->list);
-		auto& big = (p0_smaller ? ptr1->list : ptr0->list);
-
-		// Build the new, interpolated decorator.
-		UniquePtr<DecoratorDeclarationList> decorator(new DecoratorDeclarationList);
-		decorator->list.reserve(ptr0->list.size());
+		// Build the new, interpolated decorator list.
+		const bool p0_bigger = ptr0->list.size() > ptr1->list.size();
+		auto& big_list = (p0_bigger ? ptr0->list : ptr1->list);
+		auto decorator = MakeUnique<DecoratorDeclarationList>();
+		auto& list = decorator->list;
+		list.reserve(big_list.size());
 
-		// Interpolate decorators that have common types.
-		for (size_t i = 0; i < small.size(); i++)
+		for (size_t i = 0; i < big_list.size(); i++)
 		{
-			DecoratorDeclarationView d0_view{GetDecoratorDeclarationView(ptr0->list[i])};
-			DecoratorDeclarationView d1_view{GetDecoratorDeclarationView(ptr1->list[i])};
+			EffectDeclarationView d0 = GetEffectDeclarationView(ptr0->list, i, element);
+			EffectDeclarationView d1 = GetEffectDeclarationView(ptr1->list, i, element);
 
-			if (!d0_view.instancer || !d1_view.instancer)
-				return DiscreteInterpolation();
+			const EffectDeclarationView& declaration = (p0_bigger ? d0 : d1);
+			list.push_back(DecoratorDeclaration{*declaration.type, static_cast<DecoratorInstancer*>(declaration.instancer), PropertyDictionary(),
+				declaration.paint_area});
 
-			if (d0_view.instancer != d1_view.instancer || d0_view.type != d1_view.type ||
-				d0_view.properties.GetNumProperties() != d1_view.properties.GetNumProperties() || d0_view.paint_area != d1_view.paint_area)
-			{
-				// Incompatible decorators, fall back to discrete interpolation.
-				return DiscreteInterpolation();
-			}
-
-			decorator->list.push_back(DecoratorDeclaration{d0_view.type, d0_view.instancer, PropertyDictionary(), d0_view.paint_area});
-			PropertyDictionary& props = decorator->list.back().properties;
-
-			const auto& props0 = d0_view.properties.GetProperties();
-			const auto& props1 = d1_view.properties.GetProperties();
-
-			for (const auto& pair0 : props0)
-			{
-				const PropertyId id = pair0.first;
-				const Property& prop0 = pair0.second;
+			if (!InterpolateEffectProperties(list.back().properties, d0, d1, alpha, element))
+				return p_discrete;
+		}
 
-				auto it = props1.find(id);
-				if (it == props1.end())
-				{
-					RMLUI_ERRORMSG("Incompatible decorator properties.");
-					return DiscreteInterpolation();
-				}
-				const Property& prop1 = it->second;
+		return Property{DecoratorsPtr(std::move(decorator)), Unit::DECORATOR};
+	}
 
-				Property p = InterpolateProperties(prop0, prop1, alpha, element, prop0.definition);
-				p.definition = prop0.definition;
-				props.SetProperty(id, p);
-			}
-		}
+	if (p0.unit == Unit::FILTER && p1.unit == Unit::FILTER)
+	{
+		auto GetEffectDeclarationView = [](const Vector<FilterDeclaration>& declarations, size_t i) -> EffectDeclarationView {
+			if (i >= declarations.size())
+				return EffectDeclarationView();
+			return EffectDeclarationView(declarations[i]);
+		};
 
-		// Append any trailing decorators from the largest list and interpolate against the default values of its type.
-		for (size_t i = small.size(); i < big.size(); i++)
+		auto& ptr0 = p0.value.GetReference<FiltersPtr>();
+		auto& ptr1 = p1.value.GetReference<FiltersPtr>();
+		if (!ptr0 || !ptr1)
 		{
-			DecoratorDeclarationView dbig_view{GetDecoratorDeclarationView(big[i])};
+			RMLUI_ERRORMSG("Invalid filter pointer, were the filter keys properly prepared?");
+			return p_discrete;
+		}
 
-			if (!dbig_view.instancer)
-				return DiscreteInterpolation();
+		// Build the new, interpolated filter list.
+		const bool p0_bigger = ptr0->list.size() > ptr1->list.size();
+		auto& big_list = (p0_bigger ? ptr0->list : ptr1->list);
+		auto filter = MakeUnique<FilterDeclarationList>();
+		auto& list = filter->list;
+		list.reserve(big_list.size());
 
-			decorator->list.push_back(DecoratorDeclaration{dbig_view.type, dbig_view.instancer, PropertyDictionary(), dbig_view.paint_area});
-			DecoratorDeclaration& d_new = decorator->list.back();
+		for (size_t i = 0; i < big_list.size(); i++)
+		{
+			EffectDeclarationView d0 = GetEffectDeclarationView(ptr0->list, i);
+			EffectDeclarationView d1 = GetEffectDeclarationView(ptr1->list, i);
 
-			const PropertySpecification& specification = d_new.instancer->GetPropertySpecification();
+			const EffectDeclarationView& declaration = (p0_bigger ? d0 : d1);
+			list.push_back(FilterDeclaration{*declaration.type, static_cast<FilterInstancer*>(declaration.instancer), PropertyDictionary()});
 
-			const PropertyMap& props_big = dbig_view.properties.GetProperties();
-			for (const auto& pair_big : props_big)
-			{
-				const PropertyId id = pair_big.first;
-				const PropertyDefinition* underlying_definition = specification.GetProperty(id);
-				if (!underlying_definition)
-					return DiscreteInterpolation();
-
-				const Property& p_big = pair_big.second;
-				const Property& p_small = *underlying_definition->GetDefaultValue();
-				const Property& p_interp0 = (p0_smaller ? p_small : p_big);
-				const Property& p_interp1 = (p0_smaller ? p_big : p_small);
-
-				Property p = InterpolateProperties(p_interp0, p_interp1, alpha, element, p_big.definition);
-				p.definition = p_big.definition;
-				d_new.properties.SetProperty(id, p);
-			}
+			if (!InterpolateEffectProperties(list.back().properties, d0, d1, alpha, element))
+				return p_discrete;
 		}
 
-		return Property{DecoratorsPtr(std::move(decorator)), Unit::DECORATOR};
+		return Property{FiltersPtr(std::move(filter)), Unit::FILTER};
 	}
 
 	// Fall back to discrete interpolation for incompatible units.
-	return alpha < 0.5f ? p0 : p1;
+	return p_discrete;
 }
 
 enum class PrepareTransformResult { Unchanged = 0, ChangedT0 = 1, ChangedT1 = 2, ChangedT0andT1 = 3, Invalid = 4 };
@@ -530,6 +587,14 @@ static void PrepareDecorator(AnimationKey& key)
 	if (!property.value.GetReference<DecoratorsPtr>())
 		property.value = MakeShared<DecoratorDeclarationList>();
 }
+static void PrepareFilter(AnimationKey& key)
+{
+	Property& property = key.property;
+	RMLUI_ASSERT(property.value.GetType() == Variant::FILTERSPTR);
+
+	if (!property.value.GetReference<FiltersPtr>())
+		property.value = MakeShared<FilterDeclarationList>();
+}
 
 ElementAnimation::ElementAnimation(PropertyId property_id, ElementAnimationOrigin origin, const Property& current_value, Element& element,
 	double start_world_time, float duration, int num_iterations, bool alternate_direction) :
@@ -547,7 +612,8 @@ ElementAnimation::ElementAnimation(PropertyId property_id, ElementAnimationOrigi
 
 bool ElementAnimation::InternalAddKey(float time, const Property& in_property, Element& element, Tween tween)
 {
-	const Units valid_units = (Unit::NUMBER_LENGTH_PERCENT | Unit::ANGLE | Unit::COLOUR | Unit::TRANSFORM | Unit::KEYWORD | Unit::DECORATOR);
+	const Units valid_units =
+		(Unit::NUMBER_LENGTH_PERCENT | Unit::ANGLE | Unit::COLOUR | Unit::TRANSFORM | Unit::KEYWORD | Unit::DECORATOR | Unit::FILTER);
 
 	if (!Any(in_property.unit & valid_units))
 	{
@@ -556,16 +622,21 @@ bool ElementAnimation::InternalAddKey(float time, const Property& in_property, E
 	}
 
 	keys.emplace_back(time, in_property, tween);
+	Property& property = keys.back().property;
 	bool result = true;
 
-	if (keys.back().property.unit == Unit::TRANSFORM)
+	if (property.unit == Unit::TRANSFORM)
 	{
 		result = PrepareTransforms(keys, element, (int)keys.size() - 1);
 	}
-	else if (keys.back().property.unit == Unit::DECORATOR)
+	else if (property.unit == Unit::DECORATOR)
 	{
 		PrepareDecorator(keys.back());
 	}
+	else if (property.unit == Unit::FILTER)
+	{
+		PrepareFilter(keys.back());
+	}
 
 	if (!result)
 	{

+ 122 - 0
Tests/Source/UnitTests/Animation.cpp

@@ -238,3 +238,125 @@ TEST_CASE("animation.decorator")
 
 	TestsShell::ShutdownShell();
 }
+
+static const String document_filter_rml = R"(
+<rml>
+<head>
+	<title>Test</title>
+	<link type="text/rcss" href="/assets/rml.rcss"/>
+	<style>
+		body {
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+		}
+		@keyframes mix {
+			from { filter: %s; }
+			to   { filter: %s; }
+		}
+		div {
+			background: #333;
+			height: 64px;
+			width: 64px;
+			decorator: image(high_scores_alien_1.tga);
+			animation: mix 0.1s;
+		}
+	</style>
+</head>
+
+<body>
+	<div/>
+</body>
+</rml>
+)";
+
+TEST_CASE("animation.filter")
+{
+	struct Test {
+		String from;
+		String to;
+		String expected_25p; // expected interpolated value at 25% progression
+	};
+
+	Vector<Test> tests{
+		{
+			"blur( 0px)",
+			"blur(40px)",
+			"blur(10px)",
+		},
+		{
+			"blur(10px)",
+			"blur(25dp)", // assumes dp-ratio == 2
+			"blur(20px)",
+		},
+		{
+			"blur(40px)",
+			"none",
+			"blur(30px)",
+		},
+		{
+			"none",
+			"blur(40px)",
+			"blur(10px)",
+		},
+		{
+			"drop-shadow(#000 30px 20px 0px)",
+			"drop-shadow(#f00 30px 20px 4px)", // colors interpolated in linear space
+			"drop-shadow(rgba(127,0,0,255) 30px 20px 1px)",
+		},
+		{
+			"opacity(0) brightness(2)",
+			"none",
+			"opacity(0.25) brightness(1.75)",
+		},
+		{
+			"opacity(0) brightness(0)",
+			"opacity(0.5)",
+			"opacity(0.125) brightness(0.25)",
+		},
+		{
+			"opacity(0.5)",
+			"opacity(0) brightness(0)",
+			"opacity(0.375) brightness(0.75)",
+		},
+		{
+			"opacity(0) brightness(0)",
+			"brightness(1) opacity(0.5)", // discrete interpolation due to non-matching types
+			"opacity(0) brightness(0)",
+		},
+		{
+			"none", // Test initial values of various filters.
+			"brightness(2.00) contrast(2.00) grayscale(1.00) hue-rotate(4rad) invert(1.00) opacity(0.00) sepia(1.00) saturate(2.00)",
+			"brightness(1.25) contrast(1.25) grayscale(0.25) hue-rotate(1rad) invert(0.25) opacity(0.75) sepia(0.25) saturate(1.25)",
+		},
+	};
+
+	TestsSystemInterface* system_interface = TestsShell::GetTestsSystemInterface();
+	Context* context = TestsShell::GetContext();
+	context->SetDensityIndependentPixelRatio(2.0f);
+
+	for (const Test& test : tests)
+	{
+		const double t_final = 0.1;
+
+		system_interface->SetTime(0.0);
+		String document_rml = Rml::CreateString(document_filter_rml.size() + 512, document_filter_rml.c_str(), test.from.c_str(), test.to.c_str());
+
+		ElementDocument* document = context->LoadDocumentFromMemory(document_rml, "assets/");
+		Element* element = document->GetChild(0);
+
+		document->Show();
+
+		system_interface->SetTime(0.25 * t_final);
+		TestsShell::RenderLoop();
+
+		CHECK_MESSAGE(element->GetProperty<String>("filter") == test.expected_25p, "from: ", test.from, ", to: ", test.to);
+
+		document->Close();
+	}
+
+	system_interface->SetTime(0.0);
+
+	TestsShell::ShutdownShell();
+}