Browse Source

Add interpolation for color stop lists, see #667

This enables animating colors and position of stops in gradient decorators. For now, this is only supported with the same number of color stops, otherwise it will fall back to discrete interpolation.
Michael Ragazzon 1 year ago
parent
commit
fa02110f81
2 changed files with 155 additions and 39 deletions
  1. 80 35
      Source/Core/ElementAnimation.cpp
  2. 75 4
      Tests/Source/UnitTests/Animation.cpp

+ 80 - 35
Source/Core/ElementAnimation.cpp

@@ -27,6 +27,7 @@
  */
 
 #include "ElementAnimation.h"
+#include "../../Include/RmlUi/Core/DecorationTypes.h"
 #include "../../Include/RmlUi/Core/Decorator.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/Filter.h"
@@ -45,6 +46,12 @@ namespace Rml {
 
 static Property InterpolateProperties(const Property& p0, const Property& p1, float alpha, Element& element, const PropertyDefinition* definition);
 
+template <typename T>
+static T Mix(const T& v0, const T& v1, float alpha)
+{
+	return v0 * (1.0f - alpha) + v1 * alpha;
+}
+
 static Colourf ColourToLinearSpace(Colourb c)
 {
 	Colourf result;
@@ -69,6 +76,14 @@ static Colourb ColourFromLinearSpace(Colourf c)
 	return result;
 }
 
+static Colourb InterpolateColour(Colourb c0, Colourb c1, float alpha)
+{
+	Colourf c0f = ColourToLinearSpace(c0);
+	Colourf c1f = ColourToLinearSpace(c1);
+	Colourf c = Mix(c0f, c1f, alpha);
+	return ColourFromLinearSpace(c);
+}
+
 // Merges all the primitives to a single DecomposedMatrix4 primitive
 static bool CombineAndDecompose(Transform& t, Element& e)
 {
@@ -177,44 +192,53 @@ static bool InterpolateEffectProperties(PropertyDictionary& properties, const Ef
 	return false;
 }
 
-static Property InterpolateProperties(const Property& p0, const Property& p1, float alpha, Element& element, const PropertyDefinition* definition)
+static NumericValue InterpolateNumericValue(NumericValue v0, NumericValue v1, float alpha, Element& element, const PropertyDefinition* definition)
 {
-	const Property& p_discrete = (alpha < 0.5f ? p0 : p1);
+	// If we have the same units, we can simply interpolate regardless of what the value represents.
+	if (v0.unit == v1.unit)
+		return NumericValue{Mix(v0.number, v1.number, alpha), v0.unit};
 
-	if (Any(p0.unit & Unit::NUMBER_LENGTH_PERCENT) && Any(p1.unit & Unit::NUMBER_LENGTH_PERCENT))
+	// When mixing lengths or relative sizes, resolve them to pixel lengths and interpolate. This only works if we have a definition.
+	if (Any(v0.unit & Unit::NUMBER_LENGTH_PERCENT) && Any(v1.unit & Unit::NUMBER_LENGTH_PERCENT) && definition)
 	{
-		if (p0.unit == p1.unit || !definition)
-		{
-			// If we have the same units, we can just interpolate regardless of what the value represents.
-			// Or if we have distinct units but no definition, all bets are off. This shouldn't occur, just interpolate values.
-			float f0 = p0.value.Get<float>();
-			float f1 = p1.value.Get<float>();
-			float f = (1.0f - alpha) * f0 + alpha * f1;
-			return Property{f, p0.unit};
-		}
-		else
-		{
-			// Otherwise, convert units to pixels.
-			float f0 = element.GetStyle()->ResolveRelativeLength(p0.GetNumericValue(), definition->GetRelativeTarget());
-			float f1 = element.GetStyle()->ResolveRelativeLength(p1.GetNumericValue(), definition->GetRelativeTarget());
-			float f = (1.0f - alpha) * f0 + alpha * f1;
-			return Property{f, Unit::PX};
-		}
+		float f0 = element.GetStyle()->ResolveRelativeLength(v0, definition->GetRelativeTarget());
+		float f1 = element.GetStyle()->ResolveRelativeLength(v1, definition->GetRelativeTarget());
+		return NumericValue{Mix(f0, f1, alpha), Unit::PX};
+	}
+
+	// As long as we don't mix lengths and percentages, we can still resolve lengths without a definition.
+	if (Any(v0.unit & Unit::LENGTH) && Any(v1.unit & Unit::LENGTH))
+	{
+		float f0 = element.ResolveLength(v0);
+		float f1 = element.ResolveLength(v0);
+		return NumericValue{Mix(f0, f1, alpha), Unit::PX};
 	}
 
-	if (Any(p0.unit & Unit::ANGLE) && Any(p1.unit & Unit::ANGLE))
+	if (Any(v0.unit & Unit::ANGLE) && Any(v1.unit & Unit::ANGLE))
 	{
-		float f0 = ComputeAngle(p0.GetNumericValue());
-		float f1 = ComputeAngle(p1.GetNumericValue());
-		float f = (1.0f - alpha) * f0 + alpha * f1;
-		return Property{f, Unit::RAD};
+		float f = Mix(ComputeAngle(v0), ComputeAngle(v1), alpha);
+		return NumericValue{f, Unit::RAD};
+	}
+
+	// Fall back to discrete interpolation for incompatible units.
+	return alpha < 0.5f ? v0 : v1;
+}
+
+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::NUMERIC) && Any(p1.unit & Unit::NUMERIC))
+	{
+		NumericValue v = InterpolateNumericValue(p0.GetNumericValue(), p1.GetNumericValue(), alpha, element, definition);
+		return Property{v.number, v.unit};
 	}
 
 	if (p0.unit == Unit::KEYWORD && p1.unit == Unit::KEYWORD)
 	{
 		// Discrete interpolation, swap at alpha = 0.5.
 		// Special case for the 'visibility' property as in the CSS specs:
-		//   Apply the visible property if present during the entire transition period, ie. alpha (0,1).
+		//   Apply the visible property if present during the entire transition period, i.e. alpha (0,1).
 		if (definition && definition->GetId() == PropertyId::Visibility)
 		{
 			if (p0.Get<int>() == (int)Style::Visibility::Visible)
@@ -228,12 +252,8 @@ static Property InterpolateProperties(const Property& p0, const Property& p1, fl
 
 	if (p0.unit == Unit::COLOUR && p1.unit == Unit::COLOUR)
 	{
-		Colourf c0 = ColourToLinearSpace(p0.value.Get<Colourb>());
-		Colourf c1 = ColourToLinearSpace(p1.value.Get<Colourb>());
-
-		Colourf c = c0 * (1.0f - alpha) + c1 * alpha;
-
-		return Property{ColourFromLinearSpace(c), Unit::COLOUR};
+		Colourb c = InterpolateColour(p0.value.Get<Colourb>(), p1.value.Get<Colourb>(), alpha);
+		return Property{c, Unit::COLOUR};
 	}
 
 	if (p0.unit == Unit::TRANSFORM && p1.unit == Unit::TRANSFORM)
@@ -362,6 +382,32 @@ static Property InterpolateProperties(const Property& p0, const Property& p1, fl
 		return Property{FiltersPtr(std::move(filter)), Unit::FILTER};
 	}
 
+	if (p0.unit == Unit::COLORSTOPLIST && p1.unit == Unit::COLORSTOPLIST)
+	{
+		RMLUI_ASSERT(p0.value.GetType() == Variant::COLORSTOPLIST && p1.value.GetType() == Variant::COLORSTOPLIST);
+		const auto& c0 = p0.value.GetReference<ColorStopList>();
+		const auto& c1 = p1.value.GetReference<ColorStopList>();
+
+		if (c0.size() != c1.size())
+			return p_discrete;
+
+		const size_t N = c0.size();
+		ColorStopList result(N);
+
+		for (size_t i = 0; i < N; i++)
+		{
+			result[i].color = InterpolateColour(c0[i].color.ToNonPremultiplied(), c1[i].color.ToNonPremultiplied(), alpha).ToPremultiplied();
+
+			// We don't provide the property definition in the following, because it doesn't actually represent how
+			// percentages are resolved for stop positions. Here, we don't trivially know how they are resolved, so if
+			// users try to mix lengths and percentages, we instead fall back to discrete interpolation. See the
+			// gradient decorators for how stop positions are resolved.
+			result[i].position = InterpolateNumericValue(c0[i].position, c1[i].position, alpha, element, nullptr);
+		}
+
+		return Property{std::move(result), Unit::COLORSTOPLIST};
+	}
+
 	// Fall back to discrete interpolation for incompatible units.
 	return p_discrete;
 }
@@ -598,9 +644,8 @@ static void PrepareFilter(AnimationKey& key)
 
 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) :
-	property_id(property_id),
-	duration(duration), num_iterations(num_iterations), alternate_direction(alternate_direction), last_update_world_time(start_world_time),
-	origin(origin)
+	property_id(property_id), duration(duration), num_iterations(num_iterations), alternate_direction(alternate_direction),
+	last_update_world_time(start_world_time), origin(origin)
 {
 	if (!current_value.definition)
 	{

+ 75 - 4
Tests/Source/UnitTests/Animation.cpp

@@ -205,20 +205,87 @@ TEST_CASE("animation.decorator")
 
 			"horizontal-gradient(horizontal #7f7f7f3f #7f7f7f3f), horizontal-gradient(horizontal #7f7f7f3f #7f7f7f3f)",
 		},
+
+		// Standard declaration of linear gradients (consider string conversion a best-effort for now)
+		{
+			"",
+			"",
+
+			"linear-gradient(transparent, transparent)",
+			"linear-gradient(white, white)",
+
+			"linear-gradient(180deg unspecified unspecified unspecified #7d7d7d3f, #7d7d7d3f)",
+		},
+		{
+			"",
+			"",
+
+			"linear-gradient(0deg, transparent, transparent)",
+			"linear-gradient(180deg, white, white)",
+
+			"linear-gradient(45deg unspecified unspecified unspecified #7d7d7d3f, #7d7d7d3f)",
+		},
+		{
+			"",
+			"",
+
+			"linear-gradient(105deg, #000000 0%, #ff0000 100%)",
+			"linear-gradient(105deg, #ffffff 20%, #00ff00 60%)",
+
+			"linear-gradient(105deg unspecified unspecified unspecified #7f7f7f 5%, #dc7f00 90%)",
+		},
+		{
+			"",
+			"",
+
+			"linear-gradient(105deg, #000000 0%, #ff0000 50%, #ff00ff 100%)",
+			"linear-gradient(105deg, #ffffff 0%, #ffffff 10%, #ffffff 100%)",
+
+			"linear-gradient(105deg unspecified unspecified unspecified #7f7f7f 0%, #ff7f7f 40%, #ff7fff 100%)",
+		},
+		{
+			"",
+			"",
+
+			"linear-gradient(to right, transparent, transparent)",
+			"linear-gradient(270deg, transparent, transparent)",
+
+			// We don't really handle mixing direction keywords and angles here, the output will not be what one might expect.
+			"linear-gradient(202.5deg to right unspecified #00000000, #00000000)",
+		},
+		{
+			"",
+			"",
+
+			"linear-gradient(to right, transparent, transparent)",
+			"linear-gradient(to left, transparent, transparent)",
+
+			// This will effectively evaluate to "to right" with angle being ignored, resulting in discrete interpolation. Not ideal.
+			"linear-gradient(180deg to right unspecified #00000000, #00000000)",
+		},
+		{
+			"",
+			"",
+
+			"linear-gradient(#000000 0%, #ffffff 100%)",
+			"repeating-linear-gradient(#000000 0%, #ffffff 100%)",
+
+			"linear-gradient(#000000 0%, #ffffff 100%)",
+		},
 	};
 
 	TestsSystemInterface* system_interface = TestsShell::GetTestsSystemInterface();
 	Context* context = TestsShell::GetContext();
 
-	for (const char* property_str : {"decorator", "mask-image"})
+	for (const String property_str : {"decorator", "mask-image"})
 	{
 		for (const Test& test : tests)
 		{
 			const double t_final = 0.1;
 
 			system_interface->SetTime(0.0);
-			String document_rml = Rml::CreateString(document_decorator_rml.c_str(), test.from_rule.c_str(), test.to_rule.c_str(), property_str,
-				test.from.c_str(), property_str, test.to.c_str());
+			String document_rml = Rml::CreateString(document_decorator_rml.c_str(), test.from_rule.c_str(), test.to_rule.c_str(),
+				property_str.c_str(), test.from.c_str(), property_str.c_str(), test.to.c_str());
 
 			ElementDocument* document = context->LoadDocumentFromMemory(document_rml, "assets/");
 			Element* element = document->GetChild(0);
@@ -228,7 +295,11 @@ TEST_CASE("animation.decorator")
 
 			system_interface->SetTime(0.25 * t_final);
 			TestsShell::RenderLoop();
-			CHECK_MESSAGE(element->GetProperty<String>(property_str) == test.expected_25p, property_str, " from: ", test.from, ", to: ", test.to);
+
+			CAPTURE(property_str);
+			CAPTURE(test.from);
+			CAPTURE(test.to);
+			CHECK(element->GetProperty<String>(property_str) == test.expected_25p);
 
 			document->Close();
 		}