Skip to content

Commit

Permalink
Add interpolation for color stop lists, see #667
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mikke89 committed Sep 8, 2024
1 parent 9d36a83 commit fa02110
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 39 deletions.
115 changes: 80 additions & 35 deletions Source/Core/ElementAnimation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
{
Expand Down
79 changes: 75 additions & 4 deletions Tests/Source/UnitTests/Animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
}
Expand Down

0 comments on commit fa02110

Please sign in to comment.