Browse Source

Adding support for animation @keyframes

Michael 7 years ago
parent
commit
1620032f5a

+ 21 - 8
Include/Rocket/Core/Element.h

@@ -292,16 +292,15 @@ public:
 	const Vector2f Project(const Vector2f& point) noexcept;
 	const Vector2f Project(const Vector2f& point) noexcept;
 
 
 	/// Start an animation of the given property on this element.
 	/// Start an animation of the given property on this element.
-	/// If an animation of the same property name exists, the target value and duration will be added as a new animation key, 
-	/// adding to its total duration. Then, num_iterations, alternate_direction and delay will be ignored.
+	/// If an animation of the same property name exists, it will be replaced.
 	/// If start_value is null, the current property value on this element is used.
 	/// If start_value is null, the current property value on this element is used.
-	/// @return True if a new animation or key was added.
+	/// @return True if a new animation was added.
 	bool Animate(const String& property_name, const Property& target_value, float duration, Tween tween = Tween{}, int num_iterations = 1, bool alternate_direction = true, float delay = 0.0f, const Property* start_value = nullptr);
 	bool Animate(const String& property_name, const Property& target_value, float duration, Tween tween = Tween{}, int num_iterations = 1, bool alternate_direction = true, float delay = 0.0f, const Property* start_value = nullptr);
 
 
-	/// Start a transition of the given property on this element.
-	/// If an animation exists for the property, the call will be ignored. If a transition exists for this property, it will be replaced.
-	/// @return True if the transition was added.
-	bool StartTransition(const Transition& transition, const Property& start_value, const Property& target_value);
+	/// Add a key to an animation, extending its duration.
+	/// If no animation exists for the given property name, the call will be ignored.
+	/// @return True if a new animation key was added.
+	bool AddAnimationKey(const String& property_name, const Property& target_value, float duration, Tween tween = Tween{});
 	
 	
 	/// Iterates over the properties defined on this element.
 	/// Iterates over the properties defined on this element.
 	/// @param[inout] index Index of the property to fetch. This is incremented to the next valid index after the fetch. Indices are not necessarily incremental.
 	/// @param[inout] index Index of the property to fetch. This is incremented to the next valid index after the fetch. Indices are not necessarily incremental.
@@ -686,7 +685,20 @@ private:
 	void DirtyTransformState(bool perspective_changed, bool transform_changed, bool parent_pv_changed);
 	void DirtyTransformState(bool perspective_changed, bool transform_changed, bool parent_pv_changed);
 	void UpdateTransformState();
 	void UpdateTransformState();
 
 
-	void UpdateAnimations();
+	// Start an animation, replacing any existing animations of the same property name. If start_value is null, the element's current value is used.
+	ElementAnimationList::iterator StartAnimation(const String & property_name, const Property * start_value, int num_iterations, bool alternate_direction, float delay);
+
+	// Add a key to an animation, extending its duration. If target_value is null, the element's current value is used.
+	bool AddAnimationKeyTime(const String & property_name, const Property * target_value, float time, Tween tween);
+
+	/// Start a transition of the given property on this element.
+	/// If an animation exists for the property, the call will be ignored. If a transition exists for this property, it will be replaced.
+	/// @return True if the transition was added.
+	bool StartTransition(const Transition& transition, const Property& start_value, const Property& target_value);
+
+	void DirtyAnimation();
+	void UpdateAnimation();
+	void AdvanceAnimations();
 
 
 	// Original tag this element came from.
 	// Original tag this element came from.
 	String tag;
 	String tag;
@@ -772,6 +784,7 @@ private:
 	bool transform_state_parent_transform_dirty;
 	bool transform_state_parent_transform_dirty;
 
 
 	ElementAnimationList animations;
 	ElementAnimationList animations;
+	bool dirty_animation;
 
 
 	friend class Context;
 	friend class Context;
 	friend class ElementStyle;
 	friend class ElementStyle;

+ 1 - 0
Include/Rocket/Core/Property.h

@@ -76,6 +76,7 @@ public:
 
 
 		TRANSFORM = 1 << 17,			// transform; fetch as < TransformRef >
 		TRANSFORM = 1 << 17,			// transform; fetch as < TransformRef >
 		TRANSITION = 1 << 18,           // transition; fetch as < TransitionList >
 		TRANSITION = 1 << 18,           // transition; fetch as < TransitionList >
+		ANIMATION = 1 << 19,            // animation; fetch as < AnimationList >
 
 
 		LENGTH = PX | DP | PPI_UNIT | EM | REM,
 		LENGTH = PX | DP | PPI_UNIT | EM | REM,
 		LENGTH_PERCENT = LENGTH | PERCENT,
 		LENGTH_PERCENT = LENGTH | PERCENT,

+ 16 - 0
Include/Rocket/Core/StyleSheet.h

@@ -40,6 +40,16 @@ class Element;
 class ElementDefinition;
 class ElementDefinition;
 class StyleSheetNode;
 class StyleSheetNode;
 
 
+struct KeyframeBlock {
+	float normalized_time;  // [0, 1]
+	PropertyDictionary properties;
+};
+struct Keyframes {
+	std::vector<String> property_names;
+	std::vector<KeyframeBlock> blocks;
+};
+typedef std::unordered_map<String, Keyframes> KeyframesMap;
+
 /**
 /**
 	StyleSheet maintains a single stylesheet definition. A stylesheet can be combined with another stylesheet to create
 	StyleSheet maintains a single stylesheet definition. A stylesheet can be combined with another stylesheet to create
 	a new, merged stylesheet.
 	a new, merged stylesheet.
@@ -64,6 +74,9 @@ public:
 	/// Builds the node index for a combined style sheet.
 	/// Builds the node index for a combined style sheet.
 	void BuildNodeIndex();
 	void BuildNodeIndex();
 
 
+	/// Returns the Keyframes of the given name, or null if it does not exist.
+	Keyframes* GetKeyframes(const String& name);
+
 	/// Returns the compiled element definition for a given element hierarchy. A reference count will be added for the
 	/// Returns the compiled element definition for a given element hierarchy. A reference count will be added for the
 	/// caller, so another should not be added. The definition should be released by removing the reference count.
 	/// caller, so another should not be added. The definition should be released by removing the reference count.
 	ElementDefinition* GetElementDefinition(const Element* element) const;
 	ElementDefinition* GetElementDefinition(const Element* element) const;
@@ -83,6 +96,9 @@ private:
 	// precedence in the event of a conflict.
 	// precedence in the event of a conflict.
 	int specificity_offset;
 	int specificity_offset;
 
 
+	// Name of every @keyframes mapped to their keys
+	KeyframesMap keyframes;
+
 	// Map of only nodes with actual style information.
 	// Map of only nodes with actual style information.
 	NodeIndex styled_node_index;
 	NodeIndex styled_node_index;
 	// Map of every node, even empty, un-styled, nodes.
 	// Map of every node, even empty, un-styled, nodes.

+ 15 - 0
Include/Rocket/Core/Transition.h

@@ -53,10 +53,25 @@ struct TransitionList {
 };
 };
 
 
 
 
+struct Animation {
+	float duration = 0.0f;
+	Tween tween;
+	float delay = 0.0f;
+	bool alternate = false;
+	bool paused = false;
+	int num_iterations = 1;
+	String name;
+};
+
+typedef std::vector<Animation> AnimationList;
+
+
 inline bool operator==(const Transition& a, const Transition& b) { return a.name == b.name && a.tween == b.tween && a.duration == b.duration && a.delay == b.delay && a.reverse_adjustment_factor == b.reverse_adjustment_factor; }
 inline bool operator==(const Transition& a, const Transition& b) { return a.name == b.name && a.tween == b.tween && a.duration == b.duration && a.delay == b.delay && a.reverse_adjustment_factor == b.reverse_adjustment_factor; }
 inline bool operator!=(const Transition& a, const Transition& b) { return !(a == b); }
 inline bool operator!=(const Transition& a, const Transition& b) { return !(a == b); }
 inline bool operator==(const TransitionList& a, const TransitionList& b) { return a.none == b.none && a.all == b.all && a.transitions == b.transitions; }
 inline bool operator==(const TransitionList& a, const TransitionList& b) { return a.none == b.none && a.all == b.all && a.transitions == b.transitions; }
 inline bool operator!=(const TransitionList& a, const TransitionList& b) { return !(a == b); }
 inline bool operator!=(const TransitionList& a, const TransitionList& b) { return !(a == b); }
+inline bool operator==(const Animation& a, const Animation& b) { return a.duration == b.duration && a.tween == b.tween && a.delay == b.delay && a.alternate == b.alternate && a.paused == b.paused && a.num_iterations == b.num_iterations && a.name == b.name; }
+inline bool operator!=(const Animation& a, const Animation& b) { return !(a == b); }
 
 
 
 
 }
 }

+ 1 - 1
Include/Rocket/Core/Tween.h

@@ -38,7 +38,7 @@ public:
 	enum Direction { In = 1, Out = 2, InOut = 3 };
 	enum Direction { In = 1, Out = 2, InOut = 3 };
 	typedef float(*CallbackFnc)(float);
 	typedef float(*CallbackFnc)(float);
 
 
-	Tween(Type type = Linear, Direction direction = In) {
+	Tween(Type type = Linear, Direction direction = Out) {
 		if (direction & In) type_in = type;
 		if (direction & In) type_in = type;
 		if (direction & Out) type_out = type;
 		if (direction & Out) type_out = type;
 	}
 	}

+ 1 - 0
Include/Rocket/Core/TypeConverter.inl

@@ -113,6 +113,7 @@ PASS_THROUGH(Colourb);
 PASS_THROUGH(String);
 PASS_THROUGH(String);
 PASS_THROUGH(TransformRef);
 PASS_THROUGH(TransformRef);
 PASS_THROUGH(TransitionList);
 PASS_THROUGH(TransitionList);
+PASS_THROUGH(AnimationList);
 
 
 // Pointer types need to be typedef'd
 // Pointer types need to be typedef'd
 class ScriptInterface;
 class ScriptInterface;

+ 2 - 32
Include/Rocket/Core/Variant.h

@@ -77,6 +77,7 @@ public:
 		SCRIPTINTERFACE = 'p',
 		SCRIPTINTERFACE = 'p',
 		TRANSFORMREF = 't',
 		TRANSFORMREF = 't',
 		TRANSITIONLIST = 'T',
 		TRANSITIONLIST = 'T',
+		ANIMATIONLIST = 'A',
 		VOIDPTR = '*',			
 		VOIDPTR = '*',			
 	};
 	};
 
 
@@ -118,53 +119,22 @@ public:
 
 
 private:
 private:
 
 
-	/// Sets a byte value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const byte value);
 	void Set(const byte value);
-	/// Sets a signed char value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const char value);
 	void Set(const char value);
-	/// Sets a float value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const float value);
 	void Set(const float value);
-	/// Sets a signed int value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const int value);
 	void Set(const int value);
-	/// Sets a word value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const word value);
 	void Set(const word value);
-	/// Sets a constant C string value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const char* value);
 	void Set(const char* value);
-	/// Sets a generic void* value on this variant.
-	/// @param[in] value New value to set.
 	void Set(void* value);
 	void Set(void* value);
-	/// Sets an EMP string value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const String& value);
 	void Set(const String& value);
-	/// Sets a Vector2f value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const Vector2f& value);
 	void Set(const Vector2f& value);
-	/// Sets a Vector3f value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const Vector3f& value);
 	void Set(const Vector3f& value);
-	/// Sets a Vector4f value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const Vector4f& value);
 	void Set(const Vector4f& value);
-	/// Sets a TransformRef value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const TransformRef& value);
 	void Set(const TransformRef& value);
-	/// Sets a TransitionList value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const TransitionList& value);
 	void Set(const TransitionList& value);
-	/// Sets a Colourf value on this variant.
-	/// @param[in] value New value to set.
+	void Set(const AnimationList& value);
 	void Set(const Colourf& value);
 	void Set(const Colourf& value);
-	/// Sets a Colourb value on this variant.
-	/// @param[in] value New value to set.
 	void Set(const Colourb& value);
 	void Set(const Colourb& value);
-	/// Sets a script object value on this variant.
-	/// @param[in] value New value to set.
 	void Set(ScriptInterface* value);
 	void Set(ScriptInterface* value);
 	
 	
 #ifdef ROCKET_ARCH_64
 #ifdef ROCKET_ARCH_64

+ 34 - 31
Include/Rocket/Core/Variant.inl

@@ -52,69 +52,72 @@ bool Variant::GetInto(T& value) const
 {	
 {	
 	switch (type)
 	switch (type)
 	{
 	{
-		case BYTE:
-			return TypeConverter< byte, T >::Convert(*(byte*)data, value);
+	case BYTE:
+		return TypeConverter< byte, T >::Convert(*(byte*)data, value);
 		break;
 		break;
 
 
-		case CHAR:
-			return TypeConverter< char, T >::Convert(*(char*)data, value);
+	case CHAR:
+		return TypeConverter< char, T >::Convert(*(char*)data, value);
 		break;
 		break;
 
 
-		case FLOAT:
-			return TypeConverter< float, T >::Convert(*(float*)data, value);
+	case FLOAT:
+		return TypeConverter< float, T >::Convert(*(float*)data, value);
 		break;
 		break;
 
 
-		case INT:
-			return TypeConverter< int, T >::Convert(*(int*)data, value);
+	case INT:
+		return TypeConverter< int, T >::Convert(*(int*)data, value);
 		break;
 		break;
 
 
-		case STRING:
-			return TypeConverter< String, T >::Convert(*(String*)data, value);
+	case STRING:
+		return TypeConverter< String, T >::Convert(*(String*)data, value);
 		break;
 		break;
 
 
-		case WORD:
-			return TypeConverter< word, T >::Convert(*(word*)data, value);
+	case WORD:
+		return TypeConverter< word, T >::Convert(*(word*)data, value);
 		break;
 		break;
 
 
-		case VECTOR2:
-			return TypeConverter< Vector2f, T >::Convert(*(Vector2f*)data, value);
+	case VECTOR2:
+		return TypeConverter< Vector2f, T >::Convert(*(Vector2f*)data, value);
 		break;
 		break;
 
 
-		case VECTOR3:
-			return TypeConverter< Vector3f, T >::Convert(*(Vector3f*)data, value);
+	case VECTOR3:
+		return TypeConverter< Vector3f, T >::Convert(*(Vector3f*)data, value);
 		break;
 		break;
 
 
-		case VECTOR4:
-			return TypeConverter< Vector4f, T >::Convert(*(Vector4f*)data, value);
+	case VECTOR4:
+		return TypeConverter< Vector4f, T >::Convert(*(Vector4f*)data, value);
 		break;
 		break;
 
 
-		case TRANSFORMREF:
-			return TypeConverter< TransformRef, T >::Convert(*(TransformRef*)data, value);
+	case TRANSFORMREF:
+		return TypeConverter< TransformRef, T >::Convert(*(TransformRef*)data, value);
 		break;
 		break;
 
 
-		case TRANSITIONLIST:
-			return TypeConverter< TransitionList, T >::Convert(*(TransitionList*)data, value);
+	case TRANSITIONLIST:
+		return TypeConverter< TransitionList, T >::Convert(*(TransitionList*)data, value);
 		break;
 		break;
 
 
-		case COLOURF:
-			return TypeConverter< Colourf, T >::Convert(*(Colourf*)data, value);
+	case ANIMATIONLIST:
+		return TypeConverter< AnimationList, T >::Convert(*(AnimationList*)data, value);
 		break;
 		break;
 
 
-		case COLOURB:
-			return TypeConverter< Colourb, T >::Convert(*(Colourb*)data, value);
+	case COLOURF:
+		return TypeConverter< Colourf, T >::Convert(*(Colourf*)data, value);
 		break;
 		break;
 
 
-		case SCRIPTINTERFACE:
-			return TypeConverter< ScriptInterface*, T >::Convert(*(ScriptInterface**)data, value);			
+	case COLOURB:
+		return TypeConverter< Colourb, T >::Convert(*(Colourb*)data, value);
 		break;
 		break;
 
 
-		case VOIDPTR:
-			return TypeConverter< void*, T >::Convert(*(void**)data, value);
+	case SCRIPTINTERFACE:
+		return TypeConverter< ScriptInterface*, T >::Convert(*(ScriptInterface**)data, value);
 		break;
 		break;
 
 
-		case NONE:
+	case VOIDPTR:
+		return TypeConverter< void*, T >::Convert(*(void**)data, value);
 		break;
 		break;
 
 
+	case NONE:
+		break;
 	}
 	}
 
 
 	return false;
 	return false;

+ 49 - 0
Samples/basic/animation/data/animation.rml

@@ -106,6 +106,54 @@
 			margin-left: -50px;
 			margin-left: -50px;
 			font-size: 22px;
 			font-size: 22px;
 		}
 		}
+		
+		/* -- KEYFRAMES TESTS */
+		@keyframes hello-world 
+		{
+			25% {
+				transform: rotate(180deg);
+			}
+			40% {
+				transform: rotate(180deg) translateX(200px);
+			}
+			60% {
+				transform: rotate(180deg) translateX(-200px);
+			}
+			75% {
+				transform: rotate(180deg);
+			}
+			to {
+				transform: rotate(360deg);
+			}
+		}
+		#keyframes_test
+		{
+			transform: rotate(0);
+			animation: 5s bounce-out infinite hello-world;
+			transition: background-color 0.5s exponential-in-out;
+		}
+		@keyframes some-missing-keys
+		{
+			0%, 30% {
+				background-color: #d99;
+			}
+			50% {
+				background-color: #9d9;
+			}
+			to { 
+				background-color: #f9f;
+				width: 100%;
+			}
+		}
+		#keyframes_missing_keys
+		{
+			position: relative;
+			margin: 0; padding: 0;
+			top: 0; left: 0;
+			width: 25px; height: 25px;
+			animation: 2s cubic-in-out infinite alternate some-missing-keys;
+		}
+		
 	</style>
 	</style>
 </head>
 </head>
 
 
@@ -136,6 +184,7 @@
 	<div style="font-size: 1.5em; text-align: left;">Transition tests</div>
 	<div style="font-size: 1.5em; text-align: left;">Transition tests</div>
 	<div class="container"><div class="plain" id="transition_test">Transition test (hover)</div></div>
 	<div class="container"><div class="plain" id="transition_test">Transition test (hover)</div></div>
 	<div class="container"><div class="plain" id="transition_class" onclick="add_class">Transition class (click)</div></div>
 	<div class="container"><div class="plain" id="transition_class" onclick="add_class">Transition class (click)</div></div>
+	<div class="container"><div class="plain" id="keyframes_test">Keyframes test</div><div class="plain" id="keyframes_missing_keys"/></div>
 </div>
 </div>
 </body>
 </body>
 </rml>
 </rml>

+ 13 - 9
Samples/basic/animation/src/main.cpp

@@ -68,20 +68,20 @@ public:
 				el->Animate("transform", *p, 1.8f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
 				el->Animate("transform", *p, 1.8f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
 
 
 				auto pp = Transform::MakeProperty({ Transforms::Scale2D{3.f} });
 				auto pp = Transform::MakeProperty({ Transforms::Scale2D{3.f} });
-				el->Animate("transform", pp, 1.3f, Tween{ Tween::Elastic, Tween::InOut }, -1, true);
+				el->AddAnimationKey("transform", pp, 1.3f, Tween{ Tween::Elastic, Tween::InOut });
 			}
 			}
 			{
 			{
 				auto el = document->GetElementById("high_scores");
 				auto el = document->GetElementById("high_scores");
 				el->Animate("margin-left", Property(0.f, Property::PX), 0.3f, Tween{ Tween::Sine, Tween::In }, 10, true, 1.f);
 				el->Animate("margin-left", Property(0.f, Property::PX), 0.3f, Tween{ Tween::Sine, Tween::In }, 10, true, 1.f);
-				el->Animate("margin-left", Property(100.f, Property::PX), 3.0f, Tween{ Tween::Circular, Tween::Out });
+				el->AddAnimationKey("margin-left", Property(100.f, Property::PX), 3.0f, Tween{ Tween::Circular, Tween::Out });
 			}
 			}
 			{
 			{
 				auto el = document->GetElementById("options");
 				auto el = document->GetElementById("options");
 				el->Animate("image-color", Property(Colourb(128, 255, 255, 255), Property::COLOUR), 0.3f, Tween{}, -1, false);
 				el->Animate("image-color", Property(Colourb(128, 255, 255, 255), Property::COLOUR), 0.3f, Tween{}, -1, false);
-				el->Animate("image-color", Property(Colourb(128, 128, 255, 255), Property::COLOUR), 0.3f);
-				el->Animate("image-color", Property(Colourb(0, 128, 128, 255), Property::COLOUR), 0.3f);
-				el->Animate("image-color", Property(Colourb(64, 128, 255, 0), Property::COLOUR), 0.9f);
-				el->Animate("image-color", Property(Colourb(255, 255, 255, 255), Property::COLOUR), 0.3f);
+				el->AddAnimationKey("image-color", Property(Colourb(128, 128, 255, 255), Property::COLOUR), 0.3f);
+				el->AddAnimationKey("image-color", Property(Colourb(0, 128, 128, 255), Property::COLOUR), 0.3f);
+				el->AddAnimationKey("image-color", Property(Colourb(64, 128, 255, 0), Property::COLOUR), 0.9f);
+				el->AddAnimationKey("image-color", Property(Colourb(255, 255, 255, 255), Property::COLOUR), 0.3f);
 			}
 			}
 			{
 			{
 				auto el = document->GetElementById("help");
 				auto el = document->GetElementById("help");
@@ -173,7 +173,9 @@ void GameLoop()
 	static float t_prev = 0.0f;
 	static float t_prev = 0.0f;
 	float t = Shell::GetElapsedTime();
 	float t = Shell::GetElapsedTime();
 	float dt = t - t_prev;
 	float dt = t - t_prev;
-	t_prev = t;
+	static int count_frames = 0;
+	count_frames += 1;
+	//t_prev = t;
 	//if(dt > 1.0f)
 	//if(dt > 1.0f)
 	if(nudge)
 	if(nudge)
 	{
 	{
@@ -188,10 +190,12 @@ void GameLoop()
 		nudge = 0;
 		nudge = 0;
 	}
 	}
 
 
-	if (window)
+	if (window && dt > 0.2f)
 	{
 	{
+		t_prev = t;
 		auto el = window->GetDocument()->GetElementById("fps");
 		auto el = window->GetDocument()->GetElementById("fps");
-		float fps = 1.0f / dt;
+		float fps = float(count_frames) / dt;
+		count_frames = 0;
 		el->SetInnerRML(Rocket::Core::String{ 20, "FPS: %f", fps });
 		el->SetInnerRML(Rocket::Core::String{ 20, "FPS: %f", fps });
 	}
 	}
 	//static int f_prev = 0.0f;
 	//static int f_prev = 0.0f;

+ 226 - 126
Source/Core/Element.cpp

@@ -78,7 +78,8 @@ public:
 };
 };
 
 
 /// Constructs a new libRocket element.
 /// Constructs a new libRocket element.
-Element::Element(const String& _tag) : relative_offset_base(0, 0), relative_offset_position(0, 0), absolute_offset(0, 0), scroll_offset(0, 0), boxes(1), content_offset(0, 0), content_box(0, 0), transform_state(), transform_state_perspective_dirty(true), transform_state_transform_dirty(true), transform_state_parent_transform_dirty(true)
+Element::Element(const String& _tag) : relative_offset_base(0, 0), relative_offset_position(0, 0), absolute_offset(0, 0), scroll_offset(0, 0), boxes(1), content_offset(0, 0), content_box(0, 0), 
+transform_state(), transform_state_perspective_dirty(true), transform_state_transform_dirty(true), transform_state_parent_transform_dirty(true), dirty_animation(false)
 {
 {
 	tag = _tag.ToLower();
 	tag = _tag.ToLower();
 	parent = NULL;
 	parent = NULL;
@@ -161,55 +162,22 @@ void Element::Update()
 	for (size_t i = 0; i < active_children.size(); i++)
 	for (size_t i = 0; i < active_children.size(); i++)
 		active_children[i]->Update();
 		active_children[i]->Update();
 
 
-	// Update animations, if necessary.
-	UpdateAnimations();
-
 	// Force a definition reload, if necessary.
 	// Force a definition reload, if necessary.
 	style->GetDefinition();
 	style->GetDefinition();
+	scroll->Update();
+
+	// Update and advance animations, if necessary.
+	UpdateAnimation();
+	AdvanceAnimations();
 
 
 	// Update the transform state, if necessary.
 	// Update the transform state, if necessary.
 	UpdateTransformState();
 	UpdateTransformState();
 
 
-	scroll->Update();
 	OnUpdate();
 	OnUpdate();
 }
 }
 
 
 
 
 
 
-void Element::UpdateAnimations()
-{
-	if (!animations.empty())
-	{
-		float time = Clock::GetElapsedTime();
-
-		for(auto& animation : animations)
-		{
-			Property property = animation.UpdateAndGetProperty(time, *this);
-			if(property.unit != Property::UNKNOWN)
-				SetProperty(animation.GetPropertyName(), property);
-		}
-
-		auto it_completed = std::remove_if(animations.begin(), animations.end(), [](const ElementAnimation& animation) { return animation.IsComplete(); });
-
-		std::vector<Dictionary> dictionary_list;
-		dictionary_list.reserve(animations.end() - it_completed);
-
-		for (auto it = it_completed; it != animations.end(); ++it)
-		{
-			auto& dictionary = dictionary_list.emplace_back();
-			dictionary.Set(TRANSITION, it->IsTransition());
-			dictionary.Set("property", it->GetPropertyName());
-		}
-
-		// Need to erase elements before submitting event, so that we can add new animations of same property in the handler
-		animations.erase(it_completed, animations.end());
-
-		for (auto& dictionary : dictionary_list)
-			DispatchEvent(ANIMATIONEND, dictionary);
-	}
-}
-
-
 void Element::Render()
 void Element::Render()
 {
 {
 	// Rebuild our stacking context if necessary.
 	// Rebuild our stacking context if necessary.
@@ -924,93 +892,6 @@ const Vector2f Element::Project(const Vector2f& point) noexcept
 	}
 	}
 }
 }
 
 
-bool Element::Animate(const String & property_name, const Property & target_value, float duration, Tween tween, int num_iterations, bool alternate_direction, float delay, const Property* start_value)
-{
-	ElementAnimation* animation = nullptr;
-
-	for (auto& existing_animation : animations)
-	{
-		if (existing_animation.GetPropertyName() == property_name)
-		{
-			animation = &existing_animation;
-			break;
-		}
-	}
-
-	float target_time = duration;
-
-	if (!animation)
-	{
-		float start_time = Clock::GetElapsedTime() + delay;
-		if(!start_value)
-			start_value = GetProperty(property_name);
-		if (!start_value || !start_value->definition) 
-			return false;
-
-		animations.push_back(
-			ElementAnimation{ property_name, *start_value, start_time, duration, num_iterations, alternate_direction, false }
-		);
-		animation = &animations.back();
-	}
-	else
-	{
-		target_time += animation->GetDuration();
-		animation->SetDuration(target_time);
-	}
-
-	bool result = animation->AddKey(target_time, target_value, *this, tween);
-
-	return result;
-}
-
-bool Element::StartTransition(const Transition & transition, const Property& start_value, const Property & target_value)
-{
-	ElementAnimation* animation = nullptr;
-
-	for (auto& existing_animation : animations)
-	{
-		if (existing_animation.GetPropertyName() == transition.name)
-		{
-			animation = &existing_animation;
-			break;
-		}
-	}
-
-	if (animation && !animation->IsTransition())
-		return false;
-
-	float duration = transition.duration;
-
-	if (!animation)
-	{
-		float start_time = Clock::GetElapsedTime() + transition.delay;
-
-		animations.push_back(
-			ElementAnimation{ transition.name, start_value, start_time, duration, 1, false, true }
-		);
-		animation = &animations.back();
-	}
-	else
-	{
-		float start_time = Clock::GetElapsedTime() + transition.delay;
-		
-		// Compress the duration based on the progress of the current animation
-		float f = animation->GetInterpolationFactor();
-		f = 1.0f - (1.0f - f)*transition.reverse_adjustment_factor;
-		duration = duration * f;
-
-		*animation = ElementAnimation{ transition.name, start_value, start_time, duration, 1, false, true };
-	}
-
-	bool result = animation->AddKey(duration, target_value, *this, transition.tween);
-
-	if (result)
-	{
-		SetProperty(transition.name, start_value);
-	}
-
-	return result;
-}
 
 
 // Iterates over the properties defined on this element.
 // Iterates over the properties defined on this element.
 bool Element::IterateProperties(int& index, PseudoClassList& pseudo_classes, String& name, const Property*& property) const
 bool Element::IterateProperties(int& index, PseudoClassList& pseudo_classes, String& name, const Property*& property) const
@@ -2051,6 +1932,12 @@ void Element::OnPropertyChange(const PropertyNameList& changed_properties)
 	{
 	{
 		DirtyTransformState(false, true, false);
 		DirtyTransformState(false, true, false);
 	}
 	}
+
+	// Check for `animation' changes
+	if (all_dirty || changed_properties.find(ANIMATION) != changed_properties.end())
+	{
+		DirtyAnimation();
+	}
 }
 }
 
 
 // Called when a child node has been added somewhere in the hierarchy
 // Called when a child node has been added somewhere in the hierarchy
@@ -2398,6 +2285,219 @@ void Element::DirtyStructure()
 	}
 	}
 }
 }
 
 
+
+bool Element::Animate(const String & property_name, const Property & target_value, float duration, Tween tween, int num_iterations, bool alternate_direction, float delay, const Property* start_value)
+{
+	bool result = false;
+
+	if (auto it_animation = StartAnimation(property_name, start_value, num_iterations, alternate_direction, delay); it_animation != animations.end())
+	{
+		result = it_animation->AddKey(duration, target_value, *this, tween, true);
+		if (result)
+			SetProperty(property_name, *it_animation->GetStartValue());
+		else
+			animations.erase(it_animation);
+	}
+
+	return result;
+}
+
+
+bool Element::AddAnimationKey(const String & property_name, const Property & target_value, float duration, Tween tween)
+{
+	ElementAnimation* animation = nullptr;
+
+	for (auto& existing_animation : animations) {
+		if (existing_animation.GetPropertyName() == property_name) {
+			animation = &existing_animation;
+			break;
+		}
+	}
+	if (!animation)
+		return false;
+
+	bool result = animation->AddKey(animation->GetDuration() + duration, target_value, *this, tween, true);
+
+	return result;
+}
+
+
+ElementAnimationList::iterator Element::StartAnimation(const String & property_name, const Property* start_value, int num_iterations, bool alternate_direction, float delay)
+{
+	auto it = std::find_if(animations.begin(), animations.end(), [&](const ElementAnimation& el) { return el.GetPropertyName() == property_name; });
+
+	if (it == animations.end())
+	{
+		animations.emplace_back();
+		it = animations.end() - 1;
+	}
+
+	if (!start_value)
+		start_value = GetProperty(property_name);
+
+	if (start_value && start_value->definition)
+	{
+		float start_time = Clock::GetElapsedTime() + delay;
+		*it = ElementAnimation{ property_name, *start_value, start_time, 0.0f, num_iterations, alternate_direction, false };
+	}
+	else
+	{
+		animations.erase(it);
+		it = animations.end();
+	}
+
+	return it;
+}
+
+
+bool Element::AddAnimationKeyTime(const String & property_name, const Property* target_value, float time, Tween tween)
+{
+	if (!target_value)
+		target_value = GetProperty(property_name);
+	if (!target_value)
+		return false;
+
+	ElementAnimation* animation = nullptr;
+
+	for (auto& existing_animation : animations) {
+		if (existing_animation.GetPropertyName() == property_name) {
+			animation = &existing_animation;
+			break;
+		}
+	}
+	if (!animation)
+		return false;
+
+	bool result = animation->AddKey(time, *target_value, *this, tween, true);
+
+	return result;
+}
+
+bool Element::StartTransition(const Transition & transition, const Property& start_value, const Property & target_value)
+{
+	auto it = std::find_if(animations.begin(), animations.end(), [&](const ElementAnimation& el) { return el.GetPropertyName() == transition.name; });
+
+	if (it != animations.end() && !it->IsTransition())
+		return false;
+
+	float duration = transition.duration;
+	float start_time = Clock::GetElapsedTime() + transition.delay;
+
+	if (it == animations.end())
+	{
+		// Add transition as new animation
+		animations.push_back(
+			ElementAnimation{ transition.name, start_value, start_time, 0.0f, 1, false, true }
+		);
+		it = (animations.end() - 1);
+	}
+	else
+	{
+		// Compress the duration based on the progress of the current animation
+		float f = it->GetInterpolationFactor();
+		f = 1.0f - (1.0f - f)*transition.reverse_adjustment_factor;
+		duration = duration * f;
+		// Replace old transition
+		*it = ElementAnimation{ transition.name, start_value, start_time, 0.0f, 1, false, true };
+	}
+
+	bool result = it->AddKey(duration, target_value, *this, transition.tween, true);
+
+	if (result)
+		SetProperty(transition.name, start_value);
+	else
+		animations.erase(it);
+
+	return result;
+}
+
+
+void Element::DirtyAnimation()
+{
+	dirty_animation = true;
+}
+
+void Element::UpdateAnimation()
+{
+	if (dirty_animation)
+	{
+		const Property* property = style->GetLocalProperty(ANIMATION);
+		StyleSheet* stylesheet = nullptr;
+
+		if (property && (stylesheet = GetStyleSheet()))
+		{
+			auto animation_list = property->Get<AnimationList>();
+
+			for (auto& animation : animation_list)
+			{
+				Keyframes* keyframes_ptr = stylesheet->GetKeyframes(animation.name);
+				if (keyframes_ptr && keyframes_ptr->blocks.size() >= 1 && !animation.paused)
+				{
+					auto& property_names = keyframes_ptr->property_names;
+					auto& blocks = keyframes_ptr->blocks;
+
+					bool has_from_key = (blocks[0].normalized_time == 0);
+					bool has_to_key = (blocks.back().normalized_time == 1);
+
+					// If the first key defines initial conditions for a given property, use those values, else, use this element's current values.
+					for (auto& property : property_names)
+						StartAnimation(property, (has_from_key ? blocks[0].properties.GetProperty(property) : nullptr), animation.num_iterations, animation.alternate, animation.delay);
+
+					// Need to skip the first and last keys if they set the initial and end conditions, respectively.
+					for (int i = (has_from_key ? 1 : 0); i < (int)blocks.size() + (has_to_key ? -1 : 0); i++)
+					{
+						// Add properties of current key to animation
+						float time = blocks[i].normalized_time * animation.duration;
+						for (auto& property : blocks[i].properties.GetProperties())
+							AddAnimationKeyTime(property.first, &property.second, time, animation.tween);
+					}
+
+					// If the last key defines end conditions for a given property, use those values, else, use this element's current values.
+					float time = animation.duration;
+					for (auto& property : property_names)
+						AddAnimationKeyTime(property, (has_to_key ? blocks.back().properties.GetProperty(property) : nullptr), time, animation.tween);
+				}
+			}
+		}
+
+		dirty_animation = false;
+	}
+}
+
+void Element::AdvanceAnimations()
+{
+	if (!animations.empty())
+	{
+		float time = Clock::GetElapsedTime();
+
+		for (auto& animation : animations)
+		{
+			Property property = animation.UpdateAndGetProperty(time, *this);
+			if (property.unit != Property::UNKNOWN)
+				SetProperty(animation.GetPropertyName(), property);
+		}
+
+		auto it_completed = std::remove_if(animations.begin(), animations.end(), [](const ElementAnimation& animation) { return animation.IsComplete(); });
+
+		std::vector<Dictionary> dictionary_list;
+		dictionary_list.reserve(animations.end() - it_completed);
+
+		for (auto it = it_completed; it != animations.end(); ++it)
+		{
+			if(!it->IsTransition())
+				dictionary_list.emplace_back().Set("property", it->GetPropertyName());
+		}
+
+		// Need to erase elements before submitting event, as iterators might be invalidated when calling external code.
+		animations.erase(it_completed, animations.end());
+
+		for (auto& dictionary : dictionary_list)
+			DispatchEvent(ANIMATIONEND, dictionary);
+	}
+}
+
+
+
 void Element::DirtyTransformState(bool perspective_changed, bool transform_changed, bool parent_transform_changed)
 void Element::DirtyTransformState(bool perspective_changed, bool transform_changed, bool parent_transform_changed)
 {
 {
 	for (size_t i = 0; i < children.size(); ++i)
 	for (size_t i = 0; i < children.size(); ++i)

+ 8 - 5
Source/Core/ElementAnimation.cpp

@@ -367,18 +367,19 @@ ElementAnimation::ElementAnimation(const String& property_name, const Property&
 }
 }
 
 
 
 
-bool ElementAnimation::AddKey(float time, const Property & property, Element& element, Tween tween)
+bool ElementAnimation::AddKey(float target_time, const Property & in_property, Element& element, Tween tween, bool extend_duration)
 {
 {
 	int valid_properties = (Property::NUMBER_LENGTH_PERCENT | Property::ANGLE | Property::COLOUR | Property::TRANSFORM);
 	int valid_properties = (Property::NUMBER_LENGTH_PERCENT | Property::ANGLE | Property::COLOUR | Property::TRANSFORM);
 
 
-	if (!(property.unit & valid_properties))
+	if (!(in_property.unit & valid_properties))
 	{
 	{
-		Log::Message(Log::LT_WARNING, "Property '%s' is not a valid target for interpolation.", property.ToString().CString());
+		Log::Message(Log::LT_WARNING, "Property '%s' is not a valid target for interpolation.", in_property.ToString().CString());
 		return false;
 		return false;
 	}
 	}
 
 
 	bool result = true;
 	bool result = true;
-	keys.push_back({ time, property, tween });
+	keys.push_back({ target_time, in_property, tween });
+	auto& property = keys.back().property;
 
 
 	if (property.unit == Property::TRANSFORM)
 	if (property.unit == Property::TRANSFORM)
 	{
 	{
@@ -401,7 +402,9 @@ bool ElementAnimation::AddKey(float time, const Property & property, Element& el
 			result = PrepareTransforms(keys, element, (int)keys.size() - 1);
 			result = PrepareTransforms(keys, element, (int)keys.size() - 1);
 	}
 	}
 
 
-	if (!result)
+	if(result)
+		if(extend_duration) duration = target_time;
+	else
 		keys.pop_back();
 		keys.pop_back();
 
 
 	return result;
 	return result;

+ 5 - 2
Source/Core/ElementAnimation.h

@@ -43,6 +43,7 @@ struct AnimationKey {
 };
 };
 
 
 
 
+
 class ElementAnimation
 class ElementAnimation
 {
 {
 private:
 private:
@@ -64,10 +65,10 @@ private:
 
 
 	float GetInterpolationFactorAndKeys(int* out_key0, int* out_key1) const;
 	float GetInterpolationFactorAndKeys(int* out_key0, int* out_key1) const;
 public:
 public:
-
+	ElementAnimation() {}
 	ElementAnimation(const String& property_name, const Property& current_value, float start_world_time, float duration, int num_iterations, bool alternate_direction, bool is_transition);
 	ElementAnimation(const String& property_name, const Property& current_value, float start_world_time, float duration, int num_iterations, bool alternate_direction, bool is_transition);
 
 
-	bool AddKey(float time, const Property& property, Element& element, Tween tween);
+	bool AddKey(float target_time, const Property & property, Element & element, Tween tween, bool extend_duration);
 
 
 	Property UpdateAndGetProperty(float time, Element& element);
 	Property UpdateAndGetProperty(float time, Element& element);
 
 
@@ -75,8 +76,10 @@ public:
 	float GetDuration() const { return duration; }
 	float GetDuration() const { return duration; }
 	void SetDuration(float duration) { this->duration = duration; }
 	void SetDuration(float duration) { this->duration = duration; }
 	bool IsComplete() const { return animation_complete; }
 	bool IsComplete() const { return animation_complete; }
+	void SetComplete(bool complete) { animation_complete = complete; }
 	bool IsTransition() const { return is_transition; }
 	bool IsTransition() const { return is_transition; }
 	float GetInterpolationFactor() const { return GetInterpolationFactorAndKeys(nullptr, nullptr); }
 	float GetInterpolationFactor() const { return GetInterpolationFactorAndKeys(nullptr, nullptr); }
+	const Property* GetStartValue() const { return (keys.empty() ? nullptr : &keys[0].property); }
 };
 };
 
 
 
 

+ 0 - 1
Source/Core/ElementStyle.cpp

@@ -168,7 +168,6 @@ void ElementStyle::TransitionPropertyChanges(Element* element, PropertyNameList&
 				return transition_added;
 				return transition_added;
 			};
 			};
 
 
-
 			if (transition_list.all)
 			if (transition_list.all)
 			{
 			{
 				Transition transition = transition_list.transitions[0];
 				Transition transition = transition_list.transitions[0];

+ 7 - 0
Source/Core/PropertyParserTransform.cpp

@@ -46,6 +46,13 @@ PropertyParserTransform::~PropertyParserTransform()
 // Called to parse a RCSS transform declaration.
 // Called to parse a RCSS transform declaration.
 bool PropertyParserTransform::ParseValue(Property& property, const String& value, const ParameterMap& parameters) const
 bool PropertyParserTransform::ParseValue(Property& property, const String& value, const ParameterMap& parameters) const
 {
 {
+	if(value == NONE)
+	{
+		property.value = Variant(TransformRef(new Transform));
+		property.unit = Property::TRANSFORM;
+		return true;
+	}
+
 	auto transform = std::make_unique<Transform>();
 	auto transform = std::make_unique<Transform>();
 
 
 	char const* next = value.CString();
 	char const* next = value.CString();

+ 156 - 18
Source/Core/PropertyParserTransition.cpp

@@ -36,17 +36,27 @@ namespace Rocket {
 namespace Core {
 namespace Core {
 
 
 
 
-struct TransitionSpec {
-	enum Type { KEYWORD_NONE, KEYWORD_ALL, TWEEN } type;
+struct Keyword {
+	enum Type { NONE, TWEEN, ALL, ALTERNATE, INFINITE, PAUSED } type;
 	Tween tween;
 	Tween tween;
-	TransitionSpec(Tween tween) : type(TWEEN), tween(tween) {}
-	TransitionSpec(Type type) : type(type) {}
+	Keyword(Tween tween) : type(TWEEN), tween(tween) {}
+	Keyword(Type type) : type(type) {}
+
+	bool ValidTransition() const {
+		return type == NONE || type == TWEEN || type == ALL;
+	}
+	bool ValidAnimation() const {
+		return type == NONE || type == TWEEN || type == ALTERNATE || type == INFINITE || type == PAUSED;
+	}
 };
 };
 
 
 
 
-static const std::unordered_map<String, TransitionSpec> transition_spec = {
-		{"none", {TransitionSpec::KEYWORD_NONE} },
-		{"all", {TransitionSpec::KEYWORD_ALL}},
+static const std::unordered_map<String, Keyword> keywords = {
+		{"none", {Keyword::NONE} },
+		{"all", {Keyword::ALL}},
+		{"alternate", {Keyword::ALTERNATE}},
+		{"infinite", {Keyword::INFINITE}},
+		{"paused", {Keyword::PAUSED}},
 
 
 		{"back-in", {Tween{Tween::Back, Tween::In}}},
 		{"back-in", {Tween{Tween::Back, Tween::In}}},
 		{"back-out", {Tween{Tween::Back, Tween::Out}}},
 		{"back-out", {Tween{Tween::Back, Tween::Out}}},
@@ -101,23 +111,128 @@ PropertyParserTransition::PropertyParserTransition()
 }
 }
 
 
 
 
-bool PropertyParserTransition::ParseValue(Property & property, const String & value, const ParameterMap & parameters) const
+static bool ParseAnimation(Property & property, const StringList& animation_values)
 {
 {
-	StringList list_of_properties;
+	AnimationList animation_list;
+
+	for (const String& single_animation_value : animation_values)
 	{
 	{
-		auto lowercase_value = value.ToLower();
-		StringUtilities::ExpandString(list_of_properties, lowercase_value, ',');
+		Animation animation;
+
+		StringList arguments;
+		StringUtilities::ExpandString(arguments, single_animation_value, ' ');
+
+		bool duration_found = false;
+		bool delay_found = false;
+		bool num_iterations_found = false;
+
+		for (auto& argument : arguments)
+		{
+			if (argument.Empty())
+				continue;
+
+			// See if we have a <keyword> or <tween> specifier as defined in keywords
+			if (auto it = keywords.find(argument); it != keywords.end() && it->second.ValidAnimation())
+			{
+				switch (it->second.type)
+				{
+				case Keyword::NONE:
+				{
+					if (animation_list.size() > 0) // The none keyword can not be part of multiple definitions
+						return false;
+					property = Property{ AnimationList{}, Property::ANIMATION };
+					return true;
+				}
+				break;
+				case Keyword::TWEEN:
+					animation.tween = it->second.tween;
+					break;
+				case Keyword::ALTERNATE:
+					animation.alternate = true;
+					break;
+				case Keyword::INFINITE:
+					if (num_iterations_found)
+						return false;
+					animation.num_iterations = -1;
+					num_iterations_found = true;
+					break;
+				case Keyword::PAUSED:
+					animation.paused = true;
+					break;
+				}
+			}
+			else
+			{
+				// Either <duration>, <delay>, <num_iterations> or a <keyframes-name>
+				float number = 0.0f;
+				int count = 0;
+
+				if (sscanf(argument.CString(), "%fs%n", &number, &count) == 1)
+				{
+					// Found a number, if there was an 's' unit, count will be positive
+					if (count > 0)
+					{
+						// Duration or delay was assigned
+						if (!duration_found)
+						{
+							duration_found = true;
+							animation.duration = number;
+						}
+						else if (!delay_found)
+						{
+							delay_found = true;
+							animation.delay = number;
+						}
+						else
+							return false;
+					}
+					else
+					{
+						// No 's' unit means num_iterations was found
+						if (!num_iterations_found)
+						{
+							animation.num_iterations = Math::RoundToInteger(number);
+							num_iterations_found = true;
+						}
+						else
+							return false;
+					}
+				}
+				else
+				{
+					// Must be an animation name
+					animation.name = argument;
+				}
+			}
+		}
+
+		// Validate the parsed transition
+		if (animation.name.Empty() || animation.duration <= 0.0f || (animation.num_iterations < -1 || animation.num_iterations == 0))
+		{
+			return false;
+		}
+
+		animation_list.push_back(std::move(animation));
 	}
 	}
 
 
+	property = Property{ animation_list, Property::ANIMATION };
+
+	return true;
+}
+
+
+static bool ParseTransition(Property & property, const StringList& transition_values)
+{
 	TransitionList transition_list{ false, false, {} };
 	TransitionList transition_list{ false, false, {} };
 
 
-	for (const String& single_property : list_of_properties)
+	for (const String& single_transition_value : transition_values)
 	{
 	{
+
 		Transition transition;
 		Transition transition;
 		StringList target_property_names;
 		StringList target_property_names;
 
 
 		StringList arguments;
 		StringList arguments;
-		StringUtilities::ExpandString(arguments, single_property, ' ');
+		StringUtilities::ExpandString(arguments, single_transition_value, ' ');
 
 
 
 
 		bool duration_found = false;
 		bool duration_found = false;
@@ -129,24 +244,24 @@ bool PropertyParserTransition::ParseValue(Property & property, const String & va
 			if (argument.Empty())
 			if (argument.Empty())
 				continue;
 				continue;
 
 
-			// See if we have a <keyword> or <tween> specifier as defined in transition_spec
-			if (auto it = transition_spec.find(argument); it != transition_spec.end())
+			// See if we have a <keyword> or <tween> specifier as defined in keywords
+			if (auto it = keywords.find(argument); it != keywords.end() && it->second.ValidTransition())
 			{
 			{
-				if (it->second.type == TransitionSpec::KEYWORD_NONE)
+				if (it->second.type == Keyword::NONE)
 				{
 				{
 					if (transition_list.transitions.size() > 0) // The none keyword can not be part of multiple definitions
 					if (transition_list.transitions.size() > 0) // The none keyword can not be part of multiple definitions
 						return false;
 						return false;
 					property = Property{ TransitionList{true, false, {}}, Property::TRANSITION };
 					property = Property{ TransitionList{true, false, {}}, Property::TRANSITION };
 					return true;
 					return true;
 				}
 				}
-				else if (it->second.type == TransitionSpec::KEYWORD_ALL)
+				else if (it->second.type == Keyword::ALL)
 				{
 				{
 					if (transition_list.transitions.size() > 0) // The all keyword can not be part of multiple definitions
 					if (transition_list.transitions.size() > 0) // The all keyword can not be part of multiple definitions
 						return false;
 						return false;
 					transition_list.all = true;
 					transition_list.all = true;
 					target_property_names.push_back("all");
 					target_property_names.push_back("all");
 				}
 				}
-				else if (it->second.type == TransitionSpec::TWEEN)
+				else if (it->second.type == Keyword::TWEEN)
 				{
 				{
 					transition.tween = it->second.tween;
 					transition.tween = it->second.tween;
 				}
 				}
@@ -230,6 +345,29 @@ bool PropertyParserTransition::ParseValue(Property & property, const String & va
 	return true;
 	return true;
 }
 }
 
 
+
+bool PropertyParserTransition::ParseValue(Property & property, const String & value, const ParameterMap & parameters) const
+{
+	StringList list_of_values;
+	{
+		auto lowercase_value = value.ToLower();
+		StringUtilities::ExpandString(list_of_values, lowercase_value, ',');
+	}
+
+	bool result = false;
+
+	if (parameters.find(TRANSITION) != parameters.end())
+	{
+		result = ParseTransition(property, list_of_values);
+	}
+	else if (parameters.find(ANIMATION) != parameters.end())
+	{
+		result = ParseAnimation(property, list_of_values);
+	}
+
+	return result;
+}
+
 void PropertyParserTransition::Release()
 void PropertyParserTransition::Release()
 {
 {
 	delete this;
 	delete this;

+ 3 - 0
Source/Core/StringCache.cpp

@@ -104,7 +104,10 @@ const String TRANSFORM_ORIGIN = "transform-origin";
 const String TRANSFORM_ORIGIN_X = "transform-origin-x";
 const String TRANSFORM_ORIGIN_X = "transform-origin-x";
 const String TRANSFORM_ORIGIN_Y = "transform-origin-y";
 const String TRANSFORM_ORIGIN_Y = "transform-origin-y";
 const String TRANSFORM_ORIGIN_Z = "transform-origin-z";
 const String TRANSFORM_ORIGIN_Z = "transform-origin-z";
+const String NONE = "none";
 const String TRANSITION = "transition";
 const String TRANSITION = "transition";
+const String ANIMATION = "animation";
+const String KEYFRAMES = "keyframes";
 const String SCROLL_DEFAULT_STEP_SIZE = "scroll-default-step-size";
 const String SCROLL_DEFAULT_STEP_SIZE = "scroll-default-step-size";
 const String OPACITY = "opacity";
 const String OPACITY = "opacity";
 const String POINTER_EVENTS = "pointer-events";
 const String POINTER_EVENTS = "pointer-events";

+ 3 - 0
Source/Core/StringCache.h

@@ -108,8 +108,11 @@ extern const String TRANSFORM_ORIGIN;
 extern const String TRANSFORM_ORIGIN_X;
 extern const String TRANSFORM_ORIGIN_X;
 extern const String TRANSFORM_ORIGIN_Y;
 extern const String TRANSFORM_ORIGIN_Y;
 extern const String TRANSFORM_ORIGIN_Z;
 extern const String TRANSFORM_ORIGIN_Z;
+extern const String NONE;
 
 
 extern const String TRANSITION;
 extern const String TRANSITION;
+extern const String ANIMATION;
+extern const String KEYFRAMES;
 
 
 extern const String SCROLL_DEFAULT_STEP_SIZE;
 extern const String SCROLL_DEFAULT_STEP_SIZE;
 extern const String OPACITY;
 extern const String OPACITY;

+ 15 - 1
Source/Core/StyleSheet.cpp

@@ -66,7 +66,7 @@ StyleSheet::~StyleSheet()
 bool StyleSheet::LoadStyleSheet(Stream* stream)
 bool StyleSheet::LoadStyleSheet(Stream* stream)
 {
 {
 	StyleSheetParser parser;
 	StyleSheetParser parser;
-	specificity_offset = parser.Parse(root, stream);
+	specificity_offset = parser.Parse(root, keyframes, stream);
 	return specificity_offset >= 0;
 	return specificity_offset >= 0;
 }
 }
 
 
@@ -81,6 +81,12 @@ StyleSheet* StyleSheet::CombineStyleSheet(const StyleSheet* other_sheet) const
 		return NULL;
 		return NULL;
 	}
 	}
 
 
+	// Any matching @keyframe names are overridden as per CSS rules
+	for (auto& other_keyframes : other_sheet->keyframes)
+	{
+		new_sheet->keyframes[other_keyframes.first] = other_keyframes.second;
+	}
+
 	new_sheet->specificity_offset = specificity_offset + other_sheet->specificity_offset;
 	new_sheet->specificity_offset = specificity_offset + other_sheet->specificity_offset;
 	return new_sheet;
 	return new_sheet;
 }
 }
@@ -97,6 +103,14 @@ void StyleSheet::BuildNodeIndex()
 	}
 	}
 }
 }
 
 
+// Returns the Keyframes of the given name, or null if it does not exist.
+Keyframes * StyleSheet::GetKeyframes(const String & name) {
+	auto it = keyframes.find(name);
+	if (it != keyframes.end())
+		return &(it->second);
+	return nullptr;
+}
+
 // Returns the compiled element definition for a given element hierarchy.
 // Returns the compiled element definition for a given element hierarchy.
 ElementDefinition* StyleSheet::GetElementDefinition(const Element* element) const
 ElementDefinition* StyleSheet::GetElementDefinition(const Element* element) const
 {
 {

+ 198 - 16
Source/Core/StyleSheetParser.cpp

@@ -49,38 +49,220 @@ StyleSheetParser::~StyleSheetParser()
 {
 {
 }
 }
 
 
-int StyleSheetParser::Parse(StyleSheetNode* node, Stream* _stream)
+static bool IsValidIdentifier(const String& str)
+{
+	if (str.Empty())
+		return false;
+
+	for (int i = 0; i < str.Length(); i++)
+	{
+		char c = str[i];
+		bool valid = (
+			(c >= 'a' && c <= 'z')
+			|| (c >= 'A' && c <= 'Z')
+			|| (c >= '0' && c <= '9')
+			|| (c == '-')
+			|| (c == '_')
+			);
+		if (!valid)
+			return false;
+	}
+
+	return true;
+}
+
+
+static void PostprocessKeyframes(KeyframesMap& keyframes_map)
+{
+	for (auto& keyframes_pair : keyframes_map)
+	{
+		Keyframes& keyframes = keyframes_pair.second;
+		auto& blocks = keyframes.blocks;
+		auto& property_names = keyframes.property_names;
+
+		// Sort keyframes on selector value.
+		std::sort(blocks.begin(), blocks.end(), [](const KeyframeBlock& a, const KeyframeBlock& b) { return a.normalized_time < b.normalized_time; });
+
+		// Add all property names specified by any block
+		if(blocks.size() > 0) property_names.reserve(blocks.size() * blocks[0].properties.GetNumProperties());
+		for(auto& block : blocks)
+		{
+			for (auto& property : block.properties.GetProperties())
+				property_names.push_back(property.first);
+		}
+		// Remove duplicate property names
+		std::sort(property_names.begin(), property_names.end());
+		property_names.erase(std::unique(property_names.begin(), property_names.end()), property_names.end());
+		property_names.shrink_to_fit();
+	}
+
+}
+
+
+bool StyleSheetParser::ParseKeyframeBlock(KeyframesMap& keyframes_map, const String& identifier, const String& rules, const PropertyDictionary& properties)
+{
+	if (!IsValidIdentifier(identifier))
+	{
+		Log::Message(Log::LT_WARNING, "Invalid keyframes identifier '%s' at %s:%d", identifier.CString(), stream_file_name.CString(), line_number);
+		return false;
+	}
+	if (properties.GetNumProperties() == 0)
+		return true;
+
+	StringList rule_list;
+	StringUtilities::ExpandString(rule_list, rules);
+
+	std::vector<float> rule_values;
+	rule_values.reserve(rule_list.size());
+
+	for (auto rule : rule_list)
+	{
+		float value = 0.0f;
+		int count = 0;
+		rule = rule.ToLower();
+		if (rule == "from")
+			rule_values.push_back(0.0f);
+		else if (rule == "to")
+			rule_values.push_back(1.0f);
+		else if(sscanf(rule.CString(), "%f%%%n", &value, &count) == 1)
+			if(count > 0 && value >= 0.0f && value <= 100.0f)
+				rule_values.push_back(0.01f * value);
+	}
+
+	if (rule_values.empty())
+	{
+		Log::Message(Log::LT_WARNING, "Invalid keyframes rule(s) '%s' at %s:%d", rules.CString(), stream_file_name.CString(), line_number);
+		return false;
+	}
+
+	Keyframes& keyframes = keyframes_map[identifier];
+
+	for(float selector : rule_values)
+	{
+		auto it = std::find_if(keyframes.blocks.begin(), keyframes.blocks.end(), [selector](const KeyframeBlock& keyframe_block) { return Math::AbsoluteValue(keyframe_block.normalized_time - selector) < 0.0001f; });
+		if (it == keyframes.blocks.end())
+		{
+			keyframes.blocks.push_back(KeyframeBlock{ selector });
+			it = (keyframes.blocks.end() - 1);
+		}
+		else
+		{
+			// In case of duplicate keyframes, we only use the latest definition as per CSS rules
+			it->properties = PropertyDictionary();
+		}
+
+		it->properties.Import(properties);
+	}
+
+	return true;
+}
+
+int StyleSheetParser::Parse(StyleSheetNode* node, KeyframesMap& keyframes, Stream* _stream)
 {
 {
 	int rule_count = 0;
 	int rule_count = 0;
 	line_number = 0;
 	line_number = 0;
 	stream = _stream;
 	stream = _stream;
 	stream_file_name = stream->GetSourceURL().GetURL().Replace("|", ":");
 	stream_file_name = stream->GetSourceURL().GetURL().Replace("|", ":");
 
 
+	enum class State { Global, KeyframesIdentifier, KeyframesRules, Invalid };
+	State state = State::Global;
+	String keyframes_identifier;
+
 	// Look for more styles while data is available
 	// Look for more styles while data is available
 	while (FillBuffer())
 	while (FillBuffer())
 	{
 	{
-		String style_names;
+		String pre_token_str;
 		
 		
-		while (FindToken(style_names, "{", true))
+		while (char token = FindToken(pre_token_str, "{@}", true))
 		{
 		{
-			// Read the attributes
-			PropertyDictionary properties;
-			if (!ReadProperties(properties))
+			switch (state)
 			{
 			{
-				continue;
+			case State::Global:
+			{
+				if (token == '{')
+				{
+					// Read the attributes
+					PropertyDictionary properties;
+					if (!ReadProperties(properties))
+						continue;
+
+					StringList style_name_list;
+					StringUtilities::ExpandString(style_name_list, pre_token_str);
+
+					// Add style nodes to the root of the tree
+					for (size_t i = 0; i < style_name_list.size(); i++)
+						ImportProperties(node, style_name_list[i], properties, rule_count);
+
+					rule_count++;
+				}
+				else if (token == '@')
+				{
+					state = State::KeyframesIdentifier;
+				}
+				else
+				{
+					Log::Message(Log::LT_WARNING, "Invalid character '%c' found while parsing stylesheet at %s:%d. Trying to proceed.", token, stream_file_name.CString(), line_number);
+				}
 			}
 			}
+			break;
+			case State::KeyframesIdentifier:
+			{
+				if (token == '{')
+				{
+					keyframes_identifier.Clear();
+					if (pre_token_str.Substring(0, KEYFRAMES.Length()) == KEYFRAMES)
+					{
+						keyframes_identifier = StringUtilities::StripWhitespace(pre_token_str.Substring(KEYFRAMES.Length()));
+					}
+					state = State::KeyframesRules;
+				}
+				else
+				{
+					Log::Message(Log::LT_WARNING, "Invalid character '%c' found while parsing keyframes identifier in stylesheet at %s:%d", token, stream_file_name.CString(), line_number);
+					state = State::Invalid;
+				}
+			}
+			break;
+			case State::KeyframesRules:
+			{
+				if (token == '{')
+				{
+					state = State::KeyframesRules;
 
 
-			StringList style_name_list;
-			StringUtilities::ExpandString(style_name_list, style_names);
+					PropertyDictionary properties;
+					if (!ReadProperties(properties))
+						continue;
 
 
-			// Add style nodes to the root of the tree
-			for (size_t i = 0; i < style_name_list.size(); i++)
-				ImportProperties(node, style_name_list[i], properties, rule_count);
+					if (!ParseKeyframeBlock(keyframes, keyframes_identifier, pre_token_str, properties))
+						continue;
+				}
+				else if (token == '}')
+				{
+					state = State::Global;
+				}
+				else
+				{
+					Log::Message(Log::LT_WARNING, "Invalid character '%c' found while parsing keyframes in stylesheet at %s:%d", token, stream_file_name.CString(), line_number);
+					state = State::Invalid;
+				}
+			}
+			break;
+			default:
+				ROCKET_ERROR;
+				state = State::Invalid;
+				break;
+			}
 
 
-			rule_count++;
+			if (state == State::Invalid)
+				break;
 		}
 		}
+
+		if (state == State::Invalid)
+			break;
 	}	
 	}	
 
 
+	PostprocessKeyframes(keyframes);
+
 	return rule_count;
 	return rule_count;
 }
 }
 
 
@@ -269,7 +451,7 @@ bool StyleSheetParser::ImportProperties(StyleSheetNode* node, const String& name
 	return true;
 	return true;
 }
 }
 
 
-bool StyleSheetParser::FindToken(String& buffer, const char* tokens, bool remove_token)
+char StyleSheetParser::FindToken(String& buffer, const char* tokens, bool remove_token)
 {
 {
 	buffer.Clear();
 	buffer.Clear();
 	char character;
 	char character;
@@ -279,7 +461,7 @@ bool StyleSheetParser::FindToken(String& buffer, const char* tokens, bool remove
 		{
 		{
 			if (remove_token)
 			if (remove_token)
 				parse_buffer_pos++;
 				parse_buffer_pos++;
-			return true;
+			return character;
 		}
 		}
 		else
 		else
 		{
 		{
@@ -288,7 +470,7 @@ bool StyleSheetParser::FindToken(String& buffer, const char* tokens, bool remove
 		}
 		}
 	}
 	}
 
 
-	return false;
+	return 0;
 }
 }
 
 
 // Attempts to find the next character in the active stream.
 // Attempts to find the next character in the active stream.

+ 5 - 2
Source/Core/StyleSheetParser.h

@@ -51,7 +51,7 @@ public:
 	/// @param node The root node the stream will be parsed into
 	/// @param node The root node the stream will be parsed into
 	/// @param stream The stream to read
 	/// @param stream The stream to read
 	/// @return The number of parsed rules, or -1 if an error occured.
 	/// @return The number of parsed rules, or -1 if an error occured.
-	int Parse(StyleSheetNode* node, Stream* stream);	
+	int Parse(StyleSheetNode* node, KeyframesMap& keyframes, Stream* stream);	
 
 
 	/// Parses the given string into the property dictionary
 	/// Parses the given string into the property dictionary
 	/// @param parsed_properties The properties dictionary the properties will be read into
 	/// @param parsed_properties The properties dictionary the properties will be read into
@@ -83,12 +83,15 @@ private:
 	// @param rule_specificity The specifity of the rule
 	// @param rule_specificity The specifity of the rule
 	bool ImportProperties(StyleSheetNode* node, const String& names, const PropertyDictionary& properties, int rule_specificity);
 	bool ImportProperties(StyleSheetNode* node, const String& names, const PropertyDictionary& properties, int rule_specificity);
 
 
+	// Attempts to parse a @keyframes block
+	bool ParseKeyframeBlock(KeyframesMap & keyframes_map, const String & identifier, const String & rules, const PropertyDictionary & properties);
+
 	// Attempts to find one of the given character tokens in the active stream
 	// Attempts to find one of the given character tokens in the active stream
 	// If it's found, buffer is filled with all content up until the token
 	// If it's found, buffer is filled with all content up until the token
 	// @param buffer The buffer that receives the content
 	// @param buffer The buffer that receives the content
 	// @param characters The character tokens to find
 	// @param characters The character tokens to find
 	// @param remove_token If the token that caused the find to stop should be removed from the stream
 	// @param remove_token If the token that caused the find to stop should be removed from the stream
-	bool FindToken(String& buffer, const char* tokens, bool remove_token);
+	char FindToken(String& buffer, const char* tokens, bool remove_token);
 
 
 	// Attempts to find the next character in the active stream.
 	// Attempts to find the next character in the active stream.
 	// If it's found, buffer is filled with the character
 	// If it's found, buffer is filled with the character

+ 3 - 2
Source/Core/StyleSheetSpecification.cpp

@@ -276,13 +276,14 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PERSPECTIVE_ORIGIN_X, "50%", false, false).AddParser("keyword", "left, center, right").AddParser("length_percent");
 	RegisterProperty(PERSPECTIVE_ORIGIN_X, "50%", false, false).AddParser("keyword", "left, center, right").AddParser("length_percent");
 	RegisterProperty(PERSPECTIVE_ORIGIN_Y, "50%", false, false).AddParser("keyword", "top, center, bottom").AddParser("length_percent");
 	RegisterProperty(PERSPECTIVE_ORIGIN_Y, "50%", false, false).AddParser("keyword", "top, center, bottom").AddParser("length_percent");
 	RegisterShorthand(PERSPECTIVE_ORIGIN, "perspective-origin-x, perspective-origin-y");
 	RegisterShorthand(PERSPECTIVE_ORIGIN, "perspective-origin-x, perspective-origin-y");
-	RegisterProperty(TRANSFORM, "none", false, false).AddParser("keyword", "none").AddParser(TRANSFORM);
+	RegisterProperty(TRANSFORM, "none", false, false).AddParser(TRANSFORM);
 	RegisterProperty(TRANSFORM_ORIGIN_X, "50%", false, false).AddParser("keyword", "left, center, right").AddParser("length_percent");
 	RegisterProperty(TRANSFORM_ORIGIN_X, "50%", false, false).AddParser("keyword", "left, center, right").AddParser("length_percent");
 	RegisterProperty(TRANSFORM_ORIGIN_Y, "50%", false, false).AddParser("keyword", "top, center, bottom").AddParser("length_percent");
 	RegisterProperty(TRANSFORM_ORIGIN_Y, "50%", false, false).AddParser("keyword", "top, center, bottom").AddParser("length_percent");
 	RegisterProperty(TRANSFORM_ORIGIN_Z, "0", false, false).AddParser("length");
 	RegisterProperty(TRANSFORM_ORIGIN_Z, "0", false, false).AddParser("length");
 	RegisterShorthand(TRANSFORM_ORIGIN, "transform-origin-x, transform-origin-y, transform-origin-z");
 	RegisterShorthand(TRANSFORM_ORIGIN, "transform-origin-x, transform-origin-y, transform-origin-z");
 
 
-	RegisterProperty(TRANSITION, "none", false, false).AddParser("transition");
+	RegisterProperty(TRANSITION, "none", false, false).AddParser("transition", TRANSITION);
+	RegisterProperty(ANIMATION, "none", false, false).AddParser("transition", ANIMATION);
 }
 }
 
 
 }
 }

+ 36 - 19
Source/Core/Variant.cpp

@@ -39,6 +39,7 @@ Variant::Variant() : type(NONE)
 	ROCKET_STATIC_ASSERT(sizeof(Colourf) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_Colourf);
 	ROCKET_STATIC_ASSERT(sizeof(Colourf) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_Colourf);
 	ROCKET_STATIC_ASSERT(sizeof(String) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_String);
 	ROCKET_STATIC_ASSERT(sizeof(String) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_String);
 	ROCKET_STATIC_ASSERT(sizeof(TransitionList) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_TRANSITION_LIST);
 	ROCKET_STATIC_ASSERT(sizeof(TransitionList) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_TRANSITION_LIST);
+	ROCKET_STATIC_ASSERT(sizeof(AnimationList) <= LOCAL_DATA_SIZE, LOCAL_DATA_TOO_SMALL_FOR_ANIMATION_LIST);
 }
 }
 
 
 Variant::Variant( const Variant& copy ) : type(NONE)
 Variant::Variant( const Variant& copy ) : type(NONE)
@@ -77,6 +78,13 @@ void Variant::Clear()
 			transition_list->~TransitionList();
 			transition_list->~TransitionList();
 		}
 		}
 		break;
 		break;
+		case ANIMATIONLIST:
+		{
+			// Clean up the transition list.
+			AnimationList* animation_list = (AnimationList*)data;
+			animation_list->~AnimationList();
+		}
+		break;
 		default:
 		default:
 		break;
 		break;
 	}
 	}
@@ -95,31 +103,26 @@ void Variant::Set(const Variant& copy)
 {
 {
 	switch (copy.type)
 	switch (copy.type)
 	{
 	{
-		case STRING:
-		{
-			// Create the string
-			Set(*(String*)copy.data);
-		}
+	case STRING:
+		Set(*(String*)copy.data);
 		break;
 		break;
 
 
-		case TRANSFORMREF:
-		{
-			// Create the transform
-			Set(*(TransformRef*)copy.data);
-		}
+	case TRANSFORMREF:
+		Set(*(TransformRef*)copy.data);
 		break;
 		break;
 
 
-		case TRANSITIONLIST:
-		{
-			// Create the transition list
-			Set(*(TransitionList*)copy.data);
-		}
+	case TRANSITIONLIST:
+		Set(*(TransitionList*)copy.data);
 		break;
 		break;
 
 
-		default:
-			Clear();
-			memcpy(data, copy.data, LOCAL_DATA_SIZE);
-		break;	
+	case ANIMATIONLIST:
+		Set(*(AnimationList*)copy.data);
+		break;
+
+	default:
+		Clear();
+		memcpy(data, copy.data, LOCAL_DATA_SIZE);
+		break;
 	}
 	}
 	type = copy.type;
 	type = copy.type;
 }
 }
@@ -221,6 +224,18 @@ void Variant::Set(const TransitionList& value)
 		new(data) TransitionList(value);
 		new(data) TransitionList(value);
 	}
 	}
 }
 }
+void Variant::Set(const AnimationList& value)
+{
+	if (type == ANIMATIONLIST)
+	{
+		*(AnimationList*)data = value;
+	}
+	else
+	{
+		type = ANIMATIONLIST;
+		new(data) AnimationList(value);
+	}
+}
 
 
 void Variant::Set(const Colourf& value)
 void Variant::Set(const Colourf& value)
 {
 {
@@ -277,6 +292,8 @@ bool Variant::operator==(const Variant & other) const
 		return DEFAULT_VARIANT_COMPARE(TransformRef);
 		return DEFAULT_VARIANT_COMPARE(TransformRef);
 	case TRANSITIONLIST:
 	case TRANSITIONLIST:
 		return DEFAULT_VARIANT_COMPARE(TransitionList);
 		return DEFAULT_VARIANT_COMPARE(TransitionList);
+	case ANIMATIONLIST:
+		return DEFAULT_VARIANT_COMPARE(AnimationList);
 	case COLOURF:
 	case COLOURF:
 		return DEFAULT_VARIANT_COMPARE(Colourf);
 		return DEFAULT_VARIANT_COMPARE(Colourf);
 	case COLOURB:
 	case COLOURB: