From 5b63f7fe1324c87b8430c693dcfc4692f81340e9 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 5 Sep 2022 15:35:16 +0200 Subject: [PATCH 01/62] Updated tutorials --- ...rpolator.py => viz_bezier_interpolator.py} | 0 ...rame_camera_animation.py => viz_camera.py} | 0 ...polators.py => viz_color_interpolators.py} | 0 ...rpolator.py => viz_custom_interpolator.py} | 0 .../05_animation/viz_interpolators.py | 153 ++++++++++++++++++ .../05_animation/viz_introduction.py | 92 +++++++++++ .../viz_keyframe_animation_introduction.py | 75 --------- .../05_animation/viz_robot_arm_animation.py | 14 +- ...rpolator.py => viz_spline_interpolator.py} | 2 +- docs/tutorials/05_animation/viz_timeline.py | 101 ++++++++++++ ...uations.py => viz_using_time_equations.py} | 0 11 files changed, 348 insertions(+), 89 deletions(-) rename docs/tutorials/05_animation/{viz_keyframe_bezier_interpolator.py => viz_bezier_interpolator.py} (100%) rename docs/tutorials/05_animation/{viz_keyframe_camera_animation.py => viz_camera.py} (100%) rename docs/tutorials/05_animation/{viz_keyframe_color_interpolators.py => viz_color_interpolators.py} (100%) rename docs/tutorials/05_animation/{viz_keyframe_custom_interpolator.py => viz_custom_interpolator.py} (100%) create mode 100644 docs/tutorials/05_animation/viz_interpolators.py create mode 100644 docs/tutorials/05_animation/viz_introduction.py delete mode 100644 docs/tutorials/05_animation/viz_keyframe_animation_introduction.py rename docs/tutorials/05_animation/{viz_keyframe_spline_interpolator.py => viz_spline_interpolator.py} (99%) create mode 100644 docs/tutorials/05_animation/viz_timeline.py rename docs/tutorials/05_animation/{viz_animation_using_time_equations.py => viz_using_time_equations.py} (100%) diff --git a/docs/tutorials/05_animation/viz_keyframe_bezier_interpolator.py b/docs/tutorials/05_animation/viz_bezier_interpolator.py similarity index 100% rename from docs/tutorials/05_animation/viz_keyframe_bezier_interpolator.py rename to docs/tutorials/05_animation/viz_bezier_interpolator.py diff --git a/docs/tutorials/05_animation/viz_keyframe_camera_animation.py b/docs/tutorials/05_animation/viz_camera.py similarity index 100% rename from docs/tutorials/05_animation/viz_keyframe_camera_animation.py rename to docs/tutorials/05_animation/viz_camera.py diff --git a/docs/tutorials/05_animation/viz_keyframe_color_interpolators.py b/docs/tutorials/05_animation/viz_color_interpolators.py similarity index 100% rename from docs/tutorials/05_animation/viz_keyframe_color_interpolators.py rename to docs/tutorials/05_animation/viz_color_interpolators.py diff --git a/docs/tutorials/05_animation/viz_keyframe_custom_interpolator.py b/docs/tutorials/05_animation/viz_custom_interpolator.py similarity index 100% rename from docs/tutorials/05_animation/viz_keyframe_custom_interpolator.py rename to docs/tutorials/05_animation/viz_custom_interpolator.py diff --git a/docs/tutorials/05_animation/viz_interpolators.py b/docs/tutorials/05_animation/viz_interpolators.py new file mode 100644 index 000000000..bce960111 --- /dev/null +++ b/docs/tutorials/05_animation/viz_interpolators.py @@ -0,0 +1,153 @@ +""" +===================== +Keyframe animation +===================== + +Minimal tutorial of making keyframe-based animation in FURY. + +""" + +############################################################################### +# What is a ``Timeline`` +# ====================== +# +# ``Timeline`` is responsible for animating FURY actors using a set of +# keyframes by interpolating values between timestamps of these keyframes. +# ``Timeline`` has playback methods such as ``play``, ``pause``, ``stop``, ... + +import numpy as np +from fury import actor, window +from fury.animation.timeline import Timeline +from fury.animation.interpolator import cubic_spline_interpolator, slerp + +############################################################################### +# What are keyframes +# ================== +# +# A keyframe consists of a timestamp and some data. +# These data can be anything such as temperature, position, or scale. +# How to define Keyframes that FURY can work with? +# A simple dictionary object with timestamps as keys and data as values. + +keyframes = { + 1: {'value': np.array([0, 0, 0])}, + 2: {'value': np.array([-4, 1, 0])}, + 5: {'value': np.array([0, 0, 12])}, + 6: {'value': np.array([25, 0, 12])} +} + +############################################################################### +# Why keyframes data are also a dictionary ``{'value': np.array([0, 0, 0])})``? +# -> Since some keyframes data can only be defined by a set of data i.e. a +# single position keyframe could consist of a position, in control point, and +# out control point or any other data that helps to define this keyframe. + + +############################################################################### +# What are the interpolators +# ========================== +# +# The keyframes interpolators are functions that takes a set of keyframes and +# returns a function that calculates an interpolated value between these +# keyframes. +# Below there is an example on how to use interpolators manually to interpolate +# the above defined ``keyframes``. + +interpolation_function = cubic_spline_interpolator(keyframes) + +############################################################################### +# Now, if we feed any time to this function it would return the cubic +# interpolated position at that time. + +position = interpolation_function(1.44434) + +############################################################################### +# ``position`` would contain an interpolated position at time equals 1.44434 + +############################################################################### +# Creating the environment +# ======================== +# +# In order to make any animations in FURY, a `ShowManager` is needed to handle +# updating the animation and rendering the scene. + +scene = window.Scene() + +showm = window.ShowManager(scene, + size=(900, 768), reset_camera=False, + order_transparent=True) +showm.initialize() + +arrow = actor.arrow(np.array([[0, 0, 0]]), (0, 0, 0), (1, 0, 1), scales=6) + +############################################################################### +# Creating the ``Timeline`` +# ======================== +# +# First step is creating the Timeline. A playback panel should be +timeline = Timeline(playback_panel=True) + +############################################################################### +# Adding the sphere actor to the timeline +# This could've been done during initialization. +timeline.add_actor(arrow) + +############################################################################### +# Setting position keyframes +# ========================== +# +# Adding some position keyframes +timeline.set_position(0, np.array([0, 0, 0])) +timeline.set_position(2, np.array([10, 10, 10])) +timeline.set_position(5, np.array([-10, -3, -6])) +timeline.set_position(9, np.array([10, 6, 20])) + +############################################################################### +# Changing the default interpolator for a single property +# ======================================================= +# +# For all properties except **rotation**, linear interpolator is used by +# default. In order to change the default interpolator and set another +# interpolator, call ``timeline.set__interpolator(new_interpolator)`` +# FURY already has some interpolators located at: +# ``fury.animation.interpolator``. +# +# Below we set the interpolator for position keyframes to be +# **cubic spline interpolator**. +timeline.set_position_interpolator(cubic_spline_interpolator) + +############################################################################### +# Adding some rotation keyframes. +timeline.set_rotation(0, np.array([160, 50, 0])) +timeline.set_rotation(8, np.array([60, 160, 0])) + +############################################################################### +# For Rotation keyframes, Slerp is used as the default interpolator. + +############################################################################### +# Setting camera position to see the animation better. +scene.camera().SetPosition(0, 0, 90) + +############################################################################### +# Adding main Timeline to the ``Scene``. +scene.add(timeline) + + +############################################################################### +# making a function to update the animation and render the scene. +def timer_callback(_obj, _event): + timeline.update_animation() + showm.render() + + +############################################################################### +# Adding the callback function that updates the animation. +showm.add_timer_callback(True, 10, timer_callback) + +interactive = True + +if interactive: + showm.start() + +window.record(scene, out_path='viz_keyframe_interpolator.png', + size=(900, 768)) diff --git a/docs/tutorials/05_animation/viz_introduction.py b/docs/tutorials/05_animation/viz_introduction.py new file mode 100644 index 000000000..416b2de58 --- /dev/null +++ b/docs/tutorials/05_animation/viz_introduction.py @@ -0,0 +1,92 @@ +""" +=============================== +Keyframe animation introduction +=============================== + +This tutorial explains keyframe animation in FURY. + +""" + +############################################################################### +# What is Keyframe Animation? +# =========================== +# +# A keyframe animation is the transition in a property setting in between two +# keyframes. That change could be anything. +# +# A Keyframe is simply a marker of time which stores the value of a property. +# +# For example, a Keyframe might define that the position of a FURY actor is +# at (0, 0, 0) at time equals 1 second. +# +# The purpose of a Keyframe is to allow for interpolated animation, meaning, +# for example, that the user could then add another key at time equals 3 +# seconds, specifying the actor's position is at (1, 1, 0), +# +# Then the correct position of the actor for all the times between 3 and 10 +# will be interpolated. +# +# Almost any parameter that you can set for FURY actors can be animated +# using keyframes. +# +# For this tutorial, we are going to use the FURY animation module to translate +# FURY sphere actor. + +import numpy as np +from fury import actor, window +from fury.animation.timeline import Timeline + + +scene = window.Scene() + +showm = window.ShowManager(scene, size=(900, 768)) +showm.initialize() + + +############################################################################### +# Translating a sphere +# ==================== +# +# This is a quick demo showing how to translate a sphere from (0, 0, 0) to +# (1, 1, 1). +# First, we create a ``Timeline`` +timeline = Timeline(playback_panel=True) + +############################################################################### +# Our FURY sphere actor +sphere = actor.sphere(np.zeros([1, 3]), np.ones([1, 3])) + +############################################################################### +# We add the sphere actor to the ``Timeline`` +timeline.add_actor(sphere) + +############################################################################### +# Then, we set our position keyframes at different timestamps +timeline.set_position(1, np.array([0, 0, 0])) +timeline.set_position(3, np.array([1, 1, 0])) + +############################################################################### +# The ``Timeline`` must be added to the ``Scene`` +scene.add(timeline) + +############################################################################### +# No need to add the sphere actor, since it's now a part of the ``Timeline`` + + +############################################################################### +# Now we have to update the animation using a timer callback function + +def timer_callback(_obj, _event): + timeline.update_animation() + showm.render() + + +showm.add_timer_callback(True, 1, timer_callback) + +interactive = True + +if interactive: + showm.start() + +window.record(scene, out_path='viz_keyframe_animation_introduction.png', + size=(900, 768)) diff --git a/docs/tutorials/05_animation/viz_keyframe_animation_introduction.py b/docs/tutorials/05_animation/viz_keyframe_animation_introduction.py deleted file mode 100644 index 145a6fe7f..000000000 --- a/docs/tutorials/05_animation/viz_keyframe_animation_introduction.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -===================== -Keyframe animation -===================== - -Minimal tutorial of making keyframe-based animation in FURY. - -""" - -import numpy as np -from fury import actor, window -from fury.animation.timeline import Timeline -from fury.animation.interpolator import cubic_spline_interpolator - -scene = window.Scene() - -showm = window.ShowManager(scene, size=(900, 768), reset_camera=False, - order_transparent=True) -showm.initialize() - -arrow = actor.arrow(np.array([[0, 0, 0]]), (0, 0, 0), (1, 0, 1), scales=6) - -############################################################################### -# Creating a timeline to animate the actor -timeline = Timeline(playback_panel=True) - -############################################################################### -# Adding the sphere actor to the timeline -# This could've been done during initialization. -timeline.add_actor(arrow) - -############################################################################### -# Adding some position keyframes -timeline.set_position(0, np.array([0, 0, 0])) -timeline.set_position(2, np.array([10, 10, 10])) -timeline.set_position(5, np.array([-10, 16, 0])) -timeline.set_position(9, np.array([10, 0, 20])) - -############################################################################### -# change the position interpolator to Cubic spline interpolator. -timeline.set_position_interpolator(cubic_spline_interpolator) - -############################################################################### -# Adding some rotation keyframes. -timeline.set_rotation(0, np.array([160, 50, 20])) -timeline.set_rotation(4, np.array([60, 160, 0])) -timeline.set_rotation(8, np.array([0, -180, 90])) - -############################################################################### -# Main timeline to control all the timelines. -scene.camera().SetPosition(0, 0, 90) - -############################################################################### -# Adding timelines to the main Timeline. -scene.add(timeline) - - -############################################################################### -# making a function to update the animation and render the scene. -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - - -############################################################################### -# Adding the callback function that updates the animation. -showm.add_timer_callback(True, 10, timer_callback) - -interactive = False - -if interactive: - showm.start() - -window.record(scene, out_path='viz_keyframe_animation_introduction.png', - size=(900, 768)) diff --git a/docs/tutorials/05_animation/viz_robot_arm_animation.py b/docs/tutorials/05_animation/viz_robot_arm_animation.py index 20dd485a7..199a91f48 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -74,20 +74,8 @@ def set_actor_center(actor, center): ############################################################################### # Creating Arm joints time dependent animation functions. -def main_pos(t): - return np.array([np.sin(t), np.cos(t) * np.sin(t), 0]) * 30 - - -def child_pos(t): - return np.array([np.cos(t * 20), np.cos(t * 20), np.cos(t * 20)]) * 3 - - -def grand_child_pos(t): - return np.array([np.sin(t * 20), 0, 0]) * 3 - - def rot_main_arm(t): - return np.array([np.sin(t/2) * 180, np.cos(t/2) * 180, 0]) + return np.array([np.sin(t / 2) * 180, np.cos(t / 2) * 180, 0]) def rot_sub_arm(t): diff --git a/docs/tutorials/05_animation/viz_keyframe_spline_interpolator.py b/docs/tutorials/05_animation/viz_spline_interpolator.py similarity index 99% rename from docs/tutorials/05_animation/viz_keyframe_spline_interpolator.py rename to docs/tutorials/05_animation/viz_spline_interpolator.py index 5cc03b1cf..430decbfd 100644 --- a/docs/tutorials/05_animation/viz_keyframe_spline_interpolator.py +++ b/docs/tutorials/05_animation/viz_spline_interpolator.py @@ -96,7 +96,7 @@ def timer_callback(_obj, _event): # Adding the callback function that updates the animation showm.add_timer_callback(True, 10, timer_callback) -interactive = False +interactive = 1 if interactive: showm.start() diff --git a/docs/tutorials/05_animation/viz_timeline.py b/docs/tutorials/05_animation/viz_timeline.py new file mode 100644 index 000000000..e066c9d6d --- /dev/null +++ b/docs/tutorials/05_animation/viz_timeline.py @@ -0,0 +1,101 @@ +""" +============================== +Timeline and setting keyframes +============================== + +In his tutorial, you will learn how to use timeline to make simple keyframe +animations on FURY actors. + +""" + +############################################################################### +# What is ``Timeline``? +# =================== +# ``Timeline`` is responsible for handling keyframe animation of FURY actors +# using a set of keyframes by interpolating values between timestamps of these +# keyframes. +# +# ``Timeline`` has playback methods such as ``play``, ``pause``, ``stop``, ... +# which can be used to control the animation. +# + +############################################################################### +# Creating a ``Timeline`` +# ======================= +# +# FURY ``Timeline`` has the option to attaches a very useful panel for +# controlling the animation by setting ``playback_panel=True``. + +import numpy as np +from fury import actor, window +from fury.animation.timeline import Timeline + +timeline = Timeline(playback_panel=True) + +############################################################################### +# Adding Actors to ``Timeline`` +# ============================ +# +# FURY actors must be added to the ``Timeline`` in order to be animated. + +sphere = actor.sphere(np.zeros([1, 3]), np.ones([1, 3])) +timeline.add_actor(sphere) + +############################################################################### +# Main use of ``Timeline`` is to handle playback (play, stop, ...) of the +# keyframe animation. But it can be used to animate FURY actors as well. +# Now that the actor is addd to the timeline, setting keyframes to the timeline +# will animate the actor accordingly. + +############################################################################### +# Setting Keyframes +# ================= +# +# There are multiple ways to set keyframes: +# +# 1- To set a single keyframe, you may use ``timeline.set_(t, k)``, +# where is the name of the property to be set. I.e. setting position +# to (1, 2, 3) at time 0.0 would be as following: +timeline.set_position(0, np.array([1, 2, 3])) + +############################################################################### +# Supported properties are: **position, rotation, scale, color, and opacity**. +# +# 2- To set multiple keyframes at once, you may use +# ``timeline.set__keyframes(keyframes)``. +keyframes = { + 1: np.array([0, 0, 0]), + 3: np.array([-2, 0, 0]) +} + +timeline.set_position_keyframes(keyframes) + +############################################################################### +# That's it! Now we are done setting keyframes. + +scene = window.Scene() + +showm = window.ShowManager(scene, size=(900, 768)) +showm.initialize() + +scene.add(timeline) + + +############################################################################### +# Now we have to update the ``Timeline`` animation then render the scene +# using a timer callback function. + +def timer_callback(_obj, _event): + timeline.update_animation() + showm.render() + + +showm.add_timer_callback(True, 1, timer_callback) + +interactive = True + +if interactive: + showm.start() + +window.record(scene, out_path='viz_keyframe_animation_timeline.png', + size=(900, 768)) diff --git a/docs/tutorials/05_animation/viz_animation_using_time_equations.py b/docs/tutorials/05_animation/viz_using_time_equations.py similarity index 100% rename from docs/tutorials/05_animation/viz_animation_using_time_equations.py rename to docs/tutorials/05_animation/viz_using_time_equations.py From 68e01dc52cadb7cd6d660146774febda0e44a6bb Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 5 Sep 2022 15:42:12 +0200 Subject: [PATCH 02/62] Updated titles --- .../05_animation/viz_color_interpolators.py | 9 +++++---- .../05_animation/viz_custom_interpolator.py | 6 +++--- .../05_animation/viz_robot_arm_animation.py | 6 +++--- .../05_animation/viz_spline_interpolator.py | 12 +++++++----- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/tutorials/05_animation/viz_color_interpolators.py b/docs/tutorials/05_animation/viz_color_interpolators.py index 8e07a8ab4..d9139f21f 100644 --- a/docs/tutorials/05_animation/viz_color_interpolators.py +++ b/docs/tutorials/05_animation/viz_color_interpolators.py @@ -1,9 +1,10 @@ """ -================== -Keyframe animation -================== +============================ +Keyframe Color Interpolators +============================ -Color animation explained +Color animation explained in this tutorial and how to use different color +space interpolators. """ diff --git a/docs/tutorials/05_animation/viz_custom_interpolator.py b/docs/tutorials/05_animation/viz_custom_interpolator.py index da54a6c4c..79a123d76 100644 --- a/docs/tutorials/05_animation/viz_custom_interpolator.py +++ b/docs/tutorials/05_animation/viz_custom_interpolator.py @@ -1,7 +1,7 @@ """ -===================== -Keyframe animation -===================== +============================ +Making a custom interpolator +============================ Keyframe animation using custom interpolator. diff --git a/docs/tutorials/05_animation/viz_robot_arm_animation.py b/docs/tutorials/05_animation/viz_robot_arm_animation.py index 199a91f48..671701468 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -1,7 +1,7 @@ """ -===================== -Keyframe animation -===================== +=================== +Arm Robot Animation +=================== Tutorial on making a robot arm animation in FURY. diff --git a/docs/tutorials/05_animation/viz_spline_interpolator.py b/docs/tutorials/05_animation/viz_spline_interpolator.py index 430decbfd..59ede430f 100644 --- a/docs/tutorials/05_animation/viz_spline_interpolator.py +++ b/docs/tutorials/05_animation/viz_spline_interpolator.py @@ -1,7 +1,7 @@ """ -===================== -Keyframe animation -===================== +============================= +Keyframes Spline Interpolator +============================= Tutorial on making keyframe-based animation in FURY using Spline interpolators. @@ -60,11 +60,13 @@ spline_tl.set_position_interpolator(spline_interpolator, degree=5) ############################################################################### -# Adding everything to a main ``Timeline`` to control the two timelines. +# Wrapping Timelines up! # ============================================================================= # +# Adding everything to a main ``Timeline`` to control the two timelines. + ############################################################################### -# Creating a timeline with a playback panel +# First we create a timeline with a playback panel: main_timeline = Timeline(playback_panel=True, motion_path_res=100) ############################################################################### From c4d7ca5eff5ae5de5fd03e1f4bda258b101dd845 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 12 Sep 2022 01:57:28 +0200 Subject: [PATCH 03/62] Time as float representation --- .../05_animation/viz_bezier_interpolator.py | 4 ++-- .../05_animation/viz_custom_interpolator.py | 10 ++++----- .../05_animation/viz_interpolators.py | 22 +++++++++---------- .../05_animation/viz_introduction.py | 2 +- .../05_animation/viz_robot_arm_animation.py | 2 +- .../05_animation/viz_spline_interpolator.py | 14 ++++++------ docs/tutorials/05_animation/viz_timeline.py | 6 ++--- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/tutorials/05_animation/viz_bezier_interpolator.py b/docs/tutorials/05_animation/viz_bezier_interpolator.py index 706063a21..92a44433e 100644 --- a/docs/tutorials/05_animation/viz_bezier_interpolator.py +++ b/docs/tutorials/05_animation/viz_bezier_interpolator.py @@ -83,9 +83,9 @@ # Note: If a control point is not provided or set `None`, this control point # will be the same as the position itself. -timeline.set_position(0, np.array(keyframe_1.get('value')), +timeline.set_position(0.0, np.array(keyframe_1.get('value')), out_cp=np.array(keyframe_1.get('out_cp'))) -timeline.set_position(5, np.array(keyframe_2.get('value')), +timeline.set_position(5.0, np.array(keyframe_2.get('value')), in_cp=np.array(keyframe_2.get('in_cp'))) ############################################################################### diff --git a/docs/tutorials/05_animation/viz_custom_interpolator.py b/docs/tutorials/05_animation/viz_custom_interpolator.py index 79a123d76..0ecd6d790 100644 --- a/docs/tutorials/05_animation/viz_custom_interpolator.py +++ b/docs/tutorials/05_animation/viz_custom_interpolator.py @@ -120,11 +120,11 @@ def interpolate(t): # =================================================================== # t in tangent position out tangent -translation = [[0, [0., 0., 0.], [3.3051798, 6.640117, 0.], [1., 0., 0.]], - [1, [0., 0., 0.], [3.3051798, 8., 0.], [-1., 0., 0.]], - [2, [-1., 0., 0.], [3.3051798, 6., 0.], [1., 0., 0.]], - [3, [0., 0., 0.], [3.3051798, 8., 0.], [-1., 0., 0.]], - [4, [0, -1., 0.], [3.3051798, 6., 0.], [0., 0., 0.]]] +translation = [[0.0, [0., 0., 0.], [3.3051798, 6.640117, 0.], [1., 0., 0.]], + [1.0, [0., 0., 0.], [3.3051798, 8., 0.], [-1., 0., 0.]], + [2.0, [-1., 0., 0.], [3.3051798, 6., 0.], [1., 0., 0.]], + [3.0, [0., 0., 0.], [3.3051798, 8., 0.], [-1., 0., 0.]], + [4.0, [0, -1., 0.], [3.3051798, 6., 0.], [0., 0., 0.]]] ############################################################################### # Initializing a ``Timeline`` and adding sphere actor to it. diff --git a/docs/tutorials/05_animation/viz_interpolators.py b/docs/tutorials/05_animation/viz_interpolators.py index bce960111..41c383e83 100644 --- a/docs/tutorials/05_animation/viz_interpolators.py +++ b/docs/tutorials/05_animation/viz_interpolators.py @@ -30,10 +30,10 @@ # A simple dictionary object with timestamps as keys and data as values. keyframes = { - 1: {'value': np.array([0, 0, 0])}, - 2: {'value': np.array([-4, 1, 0])}, - 5: {'value': np.array([0, 0, 12])}, - 6: {'value': np.array([25, 0, 12])} + 1.0: {'value': np.array([0, 0, 0])}, + 2.0: {'value': np.array([-4, 1, 0])}, + 5.0: {'value': np.array([0, 0, 12])}, + 6.0: {'value': np.array([25, 0, 12])} } ############################################################################### @@ -97,10 +97,10 @@ # ========================== # # Adding some position keyframes -timeline.set_position(0, np.array([0, 0, 0])) -timeline.set_position(2, np.array([10, 10, 10])) -timeline.set_position(5, np.array([-10, -3, -6])) -timeline.set_position(9, np.array([10, 6, 20])) +timeline.set_position(0.0, np.array([0, 0, 0])) +timeline.set_position(2.0, np.array([10, 10, 10])) +timeline.set_position(5.0, np.array([-10, -3, -6])) +timeline.set_position(9.0, np.array([10, 6, 20])) ############################################################################### # Changing the default interpolator for a single property @@ -118,8 +118,8 @@ ############################################################################### # Adding some rotation keyframes. -timeline.set_rotation(0, np.array([160, 50, 0])) -timeline.set_rotation(8, np.array([60, 160, 0])) +timeline.set_rotation(0.0, np.array([160, 50, 0])) +timeline.set_rotation(8.0, np.array([60, 160, 0])) ############################################################################### # For Rotation keyframes, Slerp is used as the default interpolator. @@ -144,7 +144,7 @@ def timer_callback(_obj, _event): # Adding the callback function that updates the animation. showm.add_timer_callback(True, 10, timer_callback) -interactive = True +interactive = False if interactive: showm.start() diff --git a/docs/tutorials/05_animation/viz_introduction.py b/docs/tutorials/05_animation/viz_introduction.py index 416b2de58..8a1b5e4e8 100644 --- a/docs/tutorials/05_animation/viz_introduction.py +++ b/docs/tutorials/05_animation/viz_introduction.py @@ -83,7 +83,7 @@ def timer_callback(_obj, _event): showm.add_timer_callback(True, 1, timer_callback) -interactive = True +interactive = False if interactive: showm.start() diff --git a/docs/tutorials/05_animation/viz_robot_arm_animation.py b/docs/tutorials/05_animation/viz_robot_arm_animation.py index 671701468..1fc83726a 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -116,7 +116,7 @@ def timer_callback(_obj, _event): # Adding the callback function that updates the animation. showm.add_timer_callback(True, 10, timer_callback) -interactive = 1 +interactive = False if interactive: showm.start() diff --git a/docs/tutorials/05_animation/viz_spline_interpolator.py b/docs/tutorials/05_animation/viz_spline_interpolator.py index 59ede430f..10a05f064 100644 --- a/docs/tutorials/05_animation/viz_spline_interpolator.py +++ b/docs/tutorials/05_animation/viz_spline_interpolator.py @@ -23,12 +23,12 @@ # Position keyframes as a dict object containing timestamps as keys and # positions as values. position_keyframes = { - 0: np.array([0, 0, 0]), - 2: np.array([10, 3, 5]), - 4: np.array([20, 14, 13]), - 6: np.array([-20, 20, 0]), - 8: np.array([17, -10, 15]), - 10: np.array([0, -6, 0]), + 0.0: np.array([0, 0, 0]), + 2.0: np.array([10, 3, 5]), + 4.0: np.array([20, 14, 13]), + 6.0: np.array([-20, 20, 0]), + 8.0: np.array([17, -10, 15]), + 10.0: np.array([0, -6, 0]), } ############################################################################### @@ -98,7 +98,7 @@ def timer_callback(_obj, _event): # Adding the callback function that updates the animation showm.add_timer_callback(True, 10, timer_callback) -interactive = 1 +interactive = False if interactive: showm.start() diff --git a/docs/tutorials/05_animation/viz_timeline.py b/docs/tutorials/05_animation/viz_timeline.py index e066c9d6d..559be6e4f 100644 --- a/docs/tutorials/05_animation/viz_timeline.py +++ b/docs/tutorials/05_animation/viz_timeline.py @@ -56,7 +56,7 @@ # 1- To set a single keyframe, you may use ``timeline.set_(t, k)``, # where is the name of the property to be set. I.e. setting position # to (1, 2, 3) at time 0.0 would be as following: -timeline.set_position(0, np.array([1, 2, 3])) +timeline.set_position(0.0, np.array([1, 2, 3])) ############################################################################### # Supported properties are: **position, rotation, scale, color, and opacity**. @@ -64,8 +64,8 @@ # 2- To set multiple keyframes at once, you may use # ``timeline.set__keyframes(keyframes)``. keyframes = { - 1: np.array([0, 0, 0]), - 3: np.array([-2, 0, 0]) + 1.0: np.array([0, 0, 0]), + 3.0: np.array([-2, 0, 0]) } timeline.set_position_keyframes(keyframes) From 88d9187c3da3977bf5121c83d95e5a30a80f7641 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 12 Sep 2022 16:14:00 +0200 Subject: [PATCH 04/62] Added slerp docs --- docs/tutorials/05_animation/viz_interpolators.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/05_animation/viz_interpolators.py b/docs/tutorials/05_animation/viz_interpolators.py index 41c383e83..292e8f58c 100644 --- a/docs/tutorials/05_animation/viz_interpolators.py +++ b/docs/tutorials/05_animation/viz_interpolators.py @@ -18,7 +18,7 @@ import numpy as np from fury import actor, window from fury.animation.timeline import Timeline -from fury.animation.interpolator import cubic_spline_interpolator, slerp +from fury.animation.interpolator import cubic_spline_interpolator ############################################################################### # What are keyframes @@ -123,6 +123,10 @@ ############################################################################### # For Rotation keyframes, Slerp is used as the default interpolator. +# What is Slerp? +# Slerp (spherical linear interpolation) of quaternions results in a constant +# speed rotation in keyframe animation. +# Reed more about Slerp: https://en.wikipedia.org/wiki/Slerp ############################################################################### # Setting camera position to see the animation better. From 2b40bf354ad321f09178c191397458a76e0b83f5 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 19 Sep 2022 08:51:25 +0200 Subject: [PATCH 05/62] Added add_timeline method --- fury/window.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/fury/window.py b/fury/window.py index 842ef89ad..11e4213f6 100644 --- a/fury/window.py +++ b/fury/window.py @@ -9,6 +9,7 @@ from scipy import ndimage from fury import __version__ as fury_version +from fury.animation.timeline import Timeline from fury.decorators import is_osx from fury.interactor import CustomInteractorStyle from fury.io import load_image, save_image @@ -365,7 +366,6 @@ def __init__(self, scene=None, title='FURY', size=(300, 300), self.timers = [] self._fps = 0 self._last_render_time = 0 - if self.reset_camera: self.scene.ResetCamera() @@ -399,11 +399,37 @@ def __init__(self, scene=None, title='FURY', size=(300, 300), self.style.SetInteractor(self.iren) self.iren.SetInteractorStyle(self.style) self.iren.SetRenderWindow(self.window) + self._timelines = [] + self._timeline_cbk = None def initialize(self): """Initialize interaction.""" self.iren.Initialize() + def add_timeline(self, timeline: Timeline): + """Add Timeline to the ShowManager. + Adding the Timeline to the ShowManager ensures that it gets added to + the scene, gets updated and rendered without any extra code. + + Parameters + ---------- + timeline : Timeline + The Timeline to be added to the ShowManager. + """ + + self.scene.add(timeline) + if timeline in self._timelines: + return + self._timelines.append(timeline) + + if self._timeline_cbk is not None: + return + + def animation_cbk(_obj, _event): + [tl.update_animation() for tl in self._timelines] + self.render() + self._timeline_cbk = self.add_timer_callback(True, 10, animation_cbk) + def render(self): """Render only once.""" self.window.Render() From ddb3bdb949590ad379c0907749771230e0db4411 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 19 Sep 2022 09:24:35 +0200 Subject: [PATCH 06/62] added unit test for `add_timeline` --- fury/tests/test_window.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index 23fa99e72..b13754df6 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -5,8 +5,10 @@ import pytest import itertools from fury import actor, window, io +from fury.animation.timeline import Timeline from fury.lib import ImageData, Texture, numpy_support -from fury.testing import captured_output, assert_less_equal, assert_greater +from fury.testing import captured_output, assert_less_equal, assert_greater, \ + assert_true from fury.decorators import skip_osx, skip_win, skip_linux from fury import shaders from fury.utils import remove_observer_from_actor @@ -605,6 +607,25 @@ def timer_callback(_obj, _event): assert_greater(ideal_fps, 0) assert_greater(ideal_fps, actual_fps) + +def test_add_timeline_to_show_manager(): + showm = window.ShowManager() + showm.initialize() + + cube = actor.cube(np.array([[2, 2, 3]])) + + timeline = Timeline(playback_panel=True) + timeline.add(cube) + showm.add_timeline(timeline) + + npt.assert_equal(len(showm._timelines), 1) + assert_true(showm._timeline_cbk is not None) + + actors = showm.scene.GetActors() + assert_true(cube in actors) + + + # test_opengl_state_add_remove_and_check() # test_opengl_state_simple() # test_record() From 671a4416941958054ba1342c56293b9310ac55f1 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 19 Sep 2022 09:25:14 +0200 Subject: [PATCH 07/62] Fixed pep issue --- fury/tests/test_window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index b13754df6..40ba9285b 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -625,7 +625,6 @@ def test_add_timeline_to_show_manager(): assert_true(cube in actors) - # test_opengl_state_add_remove_and_check() # test_opengl_state_simple() # test_record() From 143b9d28779a5b0220dcacbde8163a0b1d0b0bf2 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 09:20:30 +0200 Subject: [PATCH 08/62] @filipinascimento's review --- fury/tests/test_window.py | 2 +- fury/window.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index 40ba9285b..cc935ff07 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -619,7 +619,7 @@ def test_add_timeline_to_show_manager(): showm.add_timeline(timeline) npt.assert_equal(len(showm._timelines), 1) - assert_true(showm._timeline_cbk is not None) + assert_true(showm._timeline_callback is not None) actors = showm.scene.GetActors() assert_true(cube in actors) diff --git a/fury/window.py b/fury/window.py index 11e4213f6..9212d6f23 100644 --- a/fury/window.py +++ b/fury/window.py @@ -400,15 +400,15 @@ def __init__(self, scene=None, title='FURY', size=(300, 300), self.iren.SetInteractorStyle(self.style) self.iren.SetRenderWindow(self.window) self._timelines = [] - self._timeline_cbk = None + self._timeline_callback = None def initialize(self): """Initialize interaction.""" self.iren.Initialize() def add_timeline(self, timeline: Timeline): - """Add Timeline to the ShowManager. - Adding the Timeline to the ShowManager ensures that it gets added to + """Add a Timeline to the ShowManager. + Adding a Timeline to the ShowManager ensures that it gets added to the scene, gets updated and rendered without any extra code. Parameters @@ -422,13 +422,13 @@ def add_timeline(self, timeline: Timeline): return self._timelines.append(timeline) - if self._timeline_cbk is not None: + if self._timeline_callback is not None: return def animation_cbk(_obj, _event): [tl.update_animation() for tl in self._timelines] self.render() - self._timeline_cbk = self.add_timer_callback(True, 10, animation_cbk) + self._timeline_callback = self.add_timer_callback(True, 10, animation_cbk) def render(self): """Render only once.""" From 5bef71b4e040bbd73b9be4207afdb20ad723d892 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:04:38 +0200 Subject: [PATCH 09/62] pep8 --- fury/ui/elements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fury/ui/elements.py b/fury/ui/elements.py index 6c45060a4..2aa8f4f10 100644 --- a/fury/ui/elements.py +++ b/fury/ui/elements.py @@ -3604,7 +3604,8 @@ def _setup(self): self.time_text = TextBlock2D(position=(820, 10)) self.speed_text = TextBlock2D(text='1', position=(0, 0), font_size=21, color=(0.2, 0.2, 0.2), bold=True, - justification='center', vertical_justification='middle') + justification='center', + vertical_justification='middle') self.panel = Panel2D(size=(190, 30), color=(1, 1, 1), align="right", has_border=True, border_color=(0, 0.3, 0), From 7d3adeaa8868ca3ebc1b3c4ec6f20ed3bb4dd12e Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:05:09 +0200 Subject: [PATCH 10/62] _get_actors to return list of actors not UI element --- fury/ui/elements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fury/ui/elements.py b/fury/ui/elements.py index 2aa8f4f10..5b3c15665 100644 --- a/fury/ui/elements.py +++ b/fury/ui/elements.py @@ -3837,7 +3837,8 @@ def speed(self, speed): def _get_actors(self): """Get the actors composing this UI component.""" - return self.panel.actors, self._progress_bar.actors, self.time_text + return self.panel.actors + self._progress_bar.actors + self.time_text.\ + actors def _add_to_scene(self, _scene): """Add all subcomponents or VTK props that compose this UI component. From de4b9524c43618e78c37c79b2266dcf120dc7605 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:05:42 +0200 Subject: [PATCH 11/62] Modified how PlaybackPanel objects are added to scene --- fury/animation/timeline.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index cdc856644..a35f7afd4 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -83,7 +83,7 @@ def set_speed(speed): self.playback_panel.on_loop_toggle = set_loop self.playback_panel.on_progress_bar_changed = self.seek self.playback_panel.on_speed_changed = set_speed - self.add_actor(self.playback_panel, static=True) + self.add(self.playback_panel) if actors is not None: self.add_actor(actors) @@ -1095,7 +1095,7 @@ def add(self, item): item: Timeline, vtkActor, list(Timeline), or list(vtkActor) Actor/s to be animated by the timeline. """ - if isinstance(item, list): + if isinstance(item, (list, tuple)): for a in item: self.add(a) return @@ -1103,6 +1103,10 @@ def add(self, item): self.add_actor(item) elif isinstance(item, Timeline): self.add_timeline(item) + elif isinstance(item, PlaybackPanel): + actors = item.actors + print(actors) + self.add_actor(actors, static=True) else: raise ValueError(f"Object of type {type(item)} can't be added to " f"the timeline.") @@ -1135,7 +1139,7 @@ def add_actor(self, actor, static=False): the timeline or just a static actor that gets added to the scene along with the Timeline. """ - if isinstance(actor, list): + if isinstance(actor, (list, tuple)): for a in actor: self.add_actor(a, static=static) elif static: @@ -1505,3 +1509,15 @@ def add_to_scene(self, ren): self._scene = ren self._added_to_scene = True self.update_animation(force=True) + + def remove_from_scene(self, ren): + """Remove Timeline and all actors and sub Timelines from the scene""" + super(Timeline, self).add_to_scene(ren) + [ren.rm(act) for act in self.actors] + print(self._static_actors) + [ren.rm(static_act) for static_act in self._static_actors] + for tl in self.timelines: + tl.remove_from_scene(ren) + if self._motion_path_actor: + ren.rm(self._motion_path_actor) + self._added_to_scene = False From 4b6a75f3ac2fc862fb1a83f7974ed086a022965f Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:06:14 +0200 Subject: [PATCH 12/62] Implemented remove_timeline from ShowManagger --- fury/window.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/fury/window.py b/fury/window.py index 9212d6f23..19949bccf 100644 --- a/fury/window.py +++ b/fury/window.py @@ -406,6 +406,10 @@ def initialize(self): """Initialize interaction.""" self.iren.Initialize() + @property + def timelines(self): + return self._timelines + def add_timeline(self, timeline: Timeline): """Add a Timeline to the ShowManager. Adding a Timeline to the ShowManager ensures that it gets added to @@ -428,7 +432,23 @@ def add_timeline(self, timeline: Timeline): def animation_cbk(_obj, _event): [tl.update_animation() for tl in self._timelines] self.render() - self._timeline_callback = self.add_timer_callback(True, 10, animation_cbk) + self._timeline_callback = self.add_timer_callback(True, 10, + animation_cbk) + + def remove_timeline(self, timeline: Timeline): + """Remove a Timeline from the ShowManager. + Timeline will be removed from the Scene as well as from the ShowManager + + Parameters + ---------- + timeline : Timeline + The Timeline to be removed. + """ + if timeline in self.timelines: + timeline.remove_from_scene(self.scene) + self._timelines.remove(timeline) + if not len(self.timelines): + self.iren.DestroyTimer(self._timeline_callback) def render(self): """Render only once.""" From ffa85d87c3135277234f260c1966e0545bb28c02 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:13:57 +0200 Subject: [PATCH 13/62] removed additional prints --- fury/animation/timeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index a35f7afd4..79e48ab04 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1105,7 +1105,6 @@ def add(self, item): self.add_timeline(item) elif isinstance(item, PlaybackPanel): actors = item.actors - print(actors) self.add_actor(actors, static=True) else: raise ValueError(f"Object of type {type(item)} can't be added to " @@ -1514,7 +1513,6 @@ def remove_from_scene(self, ren): """Remove Timeline and all actors and sub Timelines from the scene""" super(Timeline, self).add_to_scene(ren) [ren.rm(act) for act in self.actors] - print(self._static_actors) [ren.rm(static_act) for static_act in self._static_actors] for tl in self.timelines: tl.remove_from_scene(ren) From 70a78d07e746d5de47dd3f1837523304f7369533 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:30:58 +0200 Subject: [PATCH 14/62] Renamed `ren` to `scene` --- fury/animation/timeline.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 79e48ab04..b513b49c0 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1498,24 +1498,28 @@ def has_playback_panel(self): """ return self.playback_panel is not None - def add_to_scene(self, ren): + def add_to_scene(self, scene): """Add Timeline and all actors and sub Timelines to the scene""" - super(Timeline, self).add_to_scene(ren) - [ren.add(static_act) for static_act in self._static_actors] - [ren.add(timeline) for timeline in self.timelines] + super(Timeline, self).add_to_scene(scene) + [scene.add(static_act) for static_act in self._static_actors] + [scene.add(timeline) for timeline in self.timelines] + if self.has_playback_panel: + scene.add(self.playback_panel) if self._motion_path_actor: - ren.add(self._motion_path_actor) - self._scene = ren + scene.add(self._motion_path_actor) + self._scene = scene self._added_to_scene = True self.update_animation(force=True) - def remove_from_scene(self, ren): + def remove_from_scene(self, scene): """Remove Timeline and all actors and sub Timelines from the scene""" - super(Timeline, self).add_to_scene(ren) - [ren.rm(act) for act in self.actors] - [ren.rm(static_act) for static_act in self._static_actors] + super(Timeline, self).add_to_scene(scene) + [scene.rm(act) for act in self.actors] + [scene.rm(static_act) for static_act in self._static_actors] + if self.has_playback_panel: + self.playback_panel.add_to_scene(scene) for tl in self.timelines: - tl.remove_from_scene(ren) + tl.remove_from_scene(scene) if self._motion_path_actor: - ren.rm(self._motion_path_actor) + scene.rm(self._motion_path_actor) self._added_to_scene = False From 93a2a2cf32763d5a130db4fbdc890c0ef714abf6 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 20 Sep 2022 10:40:30 +0200 Subject: [PATCH 15/62] Set timer_callback to None again and added tests --- fury/tests/test_window.py | 10 ++++++++++ fury/window.py | 1 + 2 files changed, 11 insertions(+) diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index cc935ff07..553066ce8 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -623,6 +623,16 @@ def test_add_timeline_to_show_manager(): actors = showm.scene.GetActors() assert_true(cube in actors) + actors_2d = showm.scene.GetActors2D() + + [assert_true(act in actors_2d) for act in timeline.static_actors] + + showm.remove_timeline(timeline) + actors = showm.scene.GetActors() + [assert_true(act not in actors) for act in timeline.static_actors] + assert_true(cube not in actors) + assert_true(showm._timeline_callback is None) + assert_true(showm.timelines == []) # test_opengl_state_add_remove_and_check() diff --git a/fury/window.py b/fury/window.py index 19949bccf..822ead025 100644 --- a/fury/window.py +++ b/fury/window.py @@ -449,6 +449,7 @@ def remove_timeline(self, timeline: Timeline): self._timelines.remove(timeline) if not len(self.timelines): self.iren.DestroyTimer(self._timeline_callback) + self._timeline_callback = None def render(self): """Render only once.""" From 50b31a7f5daf080e75abb7d4c4f3d3fa46dfe1ab Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 23 Sep 2022 13:20:44 +0200 Subject: [PATCH 16/62] @skoudoro review (pep) --- fury/window.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fury/window.py b/fury/window.py index 822ead025..2afd6933d 100644 --- a/fury/window.py +++ b/fury/window.py @@ -412,6 +412,7 @@ def timelines(self): def add_timeline(self, timeline: Timeline): """Add a Timeline to the ShowManager. + Adding a Timeline to the ShowManager ensures that it gets added to the scene, gets updated and rendered without any extra code. @@ -421,9 +422,9 @@ def add_timeline(self, timeline: Timeline): The Timeline to be added to the ShowManager. """ - self.scene.add(timeline) if timeline in self._timelines: return + self.scene.add(timeline) self._timelines.append(timeline) if self._timeline_callback is not None: @@ -437,6 +438,7 @@ def animation_cbk(_obj, _event): def remove_timeline(self, timeline: Timeline): """Remove a Timeline from the ShowManager. + Timeline will be removed from the Scene as well as from the ShowManager Parameters From c722947930d8e976b3b1109a741ba74d961f2eb5 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 23 Sep 2022 13:21:23 +0200 Subject: [PATCH 17/62] @skoudoro review (issue) and updated tests --- fury/animation/timeline.py | 3 --- fury/tests/test_window.py | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index b513b49c0..b04037e6e 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1513,11 +1513,8 @@ def add_to_scene(self, scene): def remove_from_scene(self, scene): """Remove Timeline and all actors and sub Timelines from the scene""" - super(Timeline, self).add_to_scene(scene) [scene.rm(act) for act in self.actors] [scene.rm(static_act) for static_act in self._static_actors] - if self.has_playback_panel: - self.playback_panel.add_to_scene(scene) for tl in self.timelines: tl.remove_from_scene(scene) if self._motion_path_actor: diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index 553066ce8..7e9da1a24 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -626,13 +626,16 @@ def test_add_timeline_to_show_manager(): actors_2d = showm.scene.GetActors2D() [assert_true(act in actors_2d) for act in timeline.static_actors] - showm.remove_timeline(timeline) + actors = showm.scene.GetActors() + actors_2d = showm.scene.GetActors2D() + [assert_true(act not in actors) for act in timeline.static_actors] assert_true(cube not in actors) assert_true(showm._timeline_callback is None) assert_true(showm.timelines == []) + assert_true(list(actors_2d) == []) # test_opengl_state_add_remove_and_check() From c219c40bbc44fa1379ead5545de30cde67c56be3 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sun, 25 Sep 2022 16:12:26 +0200 Subject: [PATCH 18/62] Seperating Timeline into Timeline and Animation --- fury/animation/__init__.py | 2 + fury/animation/animation.py | 1318 ++++++++++++++++++++++++++++++++ fury/animation/timeline.py | 1404 ++--------------------------------- 3 files changed, 1397 insertions(+), 1327 deletions(-) create mode 100644 fury/animation/animation.py diff --git a/fury/animation/__init__.py b/fury/animation/__init__.py index e69de29bb..f4df5c808 100644 --- a/fury/animation/__init__.py +++ b/fury/animation/__init__.py @@ -0,0 +1,2 @@ +from fury.animation.animation import Animation +from fury.animation.timeline import Timeline diff --git a/fury/animation/animation.py b/fury/animation/animation.py new file mode 100644 index 000000000..84c663114 --- /dev/null +++ b/fury/animation/animation.py @@ -0,0 +1,1318 @@ +import time +import warnings +from collections import defaultdict +from fury import utils, actor +from fury.actor import Container +from fury.animation.interpolator import spline_interpolator, \ + step_interpolator, linear_interpolator, slerp +import numpy as np +from scipy.spatial import transform +from fury.ui.elements import PlaybackPanel +from fury.lib import Actor, Transform + + +class Animation(Container): + """Keyframe animation timeline class. + + This timeline is responsible for keyframe animations for a single or a + group of models. + It's used to handle multiple attributes and properties of Fury actors such + as transformations, color, and scale. + It also accepts custom data and interpolates them, such as temperature. + Linear interpolation is used by default to interpolate data between the + main keyframes. + + Attributes + ---------- + actors : str + a formatted string to print out what the animal says + playback_panel : bool, optional + If True, the timeline will have a playback panel set, which can be used + to control the playback of the timeline. + length : float or int, default: None, optional + the fixed length of the timeline. If set to None, the timeline will get + its length from the keyframes. + loop : bool, optional + the number of legs the animal has (default 4) + motion_path_res : int, default: None + the number of line segments used to visualizer the timeline's motion + path. + """ + + def __init__(self, actors=None, length=None, loop=False, + motion_path_res=None): + + super().__init__() + self._data = defaultdict(dict) + self._camera_data = defaultdict(dict) + self._animations = [] + self._static_actors = [] + self._timeline = None + self._parent_animation = None + self._camera = None + self._scene = None + self._duration = 0 + self._loop = loop + self._length = length + self._max_timestamp = 0 + self._added_to_scene = True + self._is_camera_animated = False + self._motion_path_res = motion_path_res + self._motion_path_actor = None + self._transform = Transform() + + if actors is not None: + self.add_actor(actors) + + def update_duration(self): + """Update and return the duration of the Animation. + + Returns + ------- + float + The duration of the animation. + """ + curr_d = self._max_timestamp if self._length is None else self._length + self._duration = max( + curr_d, max([0] + [anim.update_duration() for anim in + self.child_animations]) + ) + return self.duration + + @property + def duration(self): + """Return the duration of the animation. + + Returns + ------- + float + The duration of the animation. + """ + return self._duration + + def update_motion_path(self): + """Update motion path visualization actor""" + res = self._motion_path_res + tl = self + while isinstance(tl._parent_animation, Animation): + tl = tl._parent_animation + res = tl._motion_path_res + if not res: + return + lines = [] + colors = [] + if self.is_interpolatable('position'): + ts = np.linspace(0, self._max_timestamp, res) + [lines.append(self.get_position(t).tolist()) for t in ts] + if self.is_interpolatable('color'): + [colors.append(self.get_color(t)) for t in ts] + elif len(self.items) >= 1: + colors = sum([i.vcolors[0] / 255 for i in self.items]) / \ + len(self.items) + else: + colors = [1, 1, 1] + if len(lines) > 0: + lines = np.array([lines]) + if colors is []: + colors = np.array([colors]) + + mpa = actor.line(lines, colors=colors, opacity=0.6) + if self._scene: + # remove old motion path actor + if self._motion_path_actor is not None: + self._scene.rm(self._motion_path_actor) + self._scene.add(mpa) + self._motion_path_actor = mpa + + def _get_data(self, is_camera=False): + if is_camera: + self._is_camera_animated = True + return self._camera_data + else: + return self._data + + def _get_attribute_data(self, attrib, is_camera=False): + data = self._get_data(is_camera=is_camera) + + if attrib not in data: + data[attrib] = { + 'keyframes': defaultdict(dict), + 'interpolator': { + 'base': linear_interpolator if attrib != 'rotation' else + slerp, + 'func': None, + 'args': defaultdict() + }, + 'callbacks': [], + } + return data.get(attrib) + + def set_keyframe(self, attrib, timestamp, value, is_camera=False, + update_interpolator=True, **kwargs): + """Set a keyframe for a certain attribute. + + Parameters + ---------- + attrib: str + The name of the attribute. + timestamp: float + Timestamp of the keyframe. + value: ndarray or float or bool + Value of the keyframe at the given timestamp. + is_camera: bool, optional + Indicated whether setting a camera property or general property. + update_interpolator: bool, optional + Interpolator will be reinitialized if Ture + + Other Parameters + ---------------- + in_cp: ndarray, shape (1, M), optional + The in control point in case of using cubic Bézier interpolator. + out_cp: ndarray, shape (1, M), optional + The out control point in case of using cubic Bézier interpolator. + in_tangent: ndarray, shape (1, M), optional + The in tangent at that position for the cubic spline curve. + out_tangent: ndarray, shape (1, M), optional + The out tangent at that position for the cubic spline curve. + """ + + attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) + keyframes = attrib_data.get('keyframes') + + keyframes[timestamp] = { + 'value': np.array(value).astype(float), + **{par: np.array(val).astype(float) for par, val in kwargs.items() + if val is not None} + } + + if update_interpolator: + interp = attrib_data.get('interpolator') + interp_base = interp.get( + 'base', linear_interpolator if attrib != 'rotation' else slerp) + args = interp.get('args', {}) + self.set_interpolator(attrib, interp_base, + is_camera=is_camera, **args) + + if timestamp > self._max_timestamp: + self._max_timestamp = timestamp + if self._timeline is not None: + self._timeline.update_duration() + else: + self.update_duration() + self.update_animation() + self.update_motion_path() + + def set_keyframes(self, attrib, keyframes, is_camera=False): + """Set multiple keyframes for a certain attribute. + + Parameters + ---------- + attrib: str + The name of the attribute. + keyframes: dict + A dict object containing keyframes to be set. + is_camera: bool + Indicated whether setting a camera property or general property. + + Notes + --------- + Keyframes can be on any of the following forms: + >>> key_frames_simple = {1: [1, 2, 1], 2: [3, 4, 5]} + >>> key_frames_bezier = {1: {'value': [1, 2, 1]}, + >>> 2: {'value': [3, 4, 5], 'in_cp': [1, 2, 3]}} + + Examples + --------- + >>> pos_keyframes = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} + >>> Timeline.set_keyframes('position', pos_keyframes) + """ + for t, keyframe in keyframes.items(): + if isinstance(keyframe, dict): + self.set_keyframe(attrib, t, **keyframe, is_camera=is_camera) + else: + self.set_keyframe(attrib, t, keyframe, is_camera=is_camera) + + def set_camera_keyframe(self, attrib, timestamp, value, **kwargs): + """Set a keyframe for a camera property + + Parameters + ---------- + attrib: str + The name of the attribute. + timestamp: float + Timestamp of the keyframe. + value: value: ndarray or float or bool + Value of the keyframe at the given timestamp. + **kwargs: dict, optional + Additional keyword arguments passed to `set_keyframe`. + """ + self.set_keyframe(attrib, timestamp, value, is_camera=True, **kwargs) + + def is_inside_scene_at(self, timestamp): + parent = self._parent_animation + parent_in_scene = True + if parent is not None: + parent_in_scene = parent._added_to_scene + + if self.is_interpolatable('in_scene'): + return parent_in_scene and self.get_value('in_scene', timestamp) + + return parent_in_scene + + def add_to_scene_at(self, timestamp): + """Set timestamp for adding Timeline to scene event. + + Parameters + ---------- + timestamp: float + Timestamp of the event. + """ + if not self.is_interpolatable('in_scene'): + self.set_keyframe('in_scene', timestamp, True) + self.set_interpolator('in_scene', step_interpolator) + else: + self.set_keyframe('in_scene', timestamp, True) + + def remove_from_scene_at(self, timestamp): + """Set timestamp for removing Timeline to scene event. + + Parameters + ---------- + timestamp: float + Timestamp of the event. + """ + if not self.is_interpolatable('in_scene'): + self.set_keyframe('in_scene', timestamp, False) + self.set_interpolator('in_scene', step_interpolator) + else: + self.set_keyframe('in_scene', timestamp, False) + + def handle_scene_event(self, timestamp): + should_be_in_scene = self.is_inside_scene_at(timestamp) + if self._scene is not None: + if should_be_in_scene and not self._added_to_scene: + super(Animation, self).add_to_scene(self._scene) + self._added_to_scene = True + elif not should_be_in_scene and self._added_to_scene: + super(Animation, self).remove_from_scene(self._scene) + self._added_to_scene = False + + def set_camera_keyframes(self, attrib, keyframes): + """Set multiple keyframes for a certain camera property + + Parameters + ---------- + attrib: str + The name of the property. + keyframes: dict + A dict object containing keyframes to be set. + + Notes + --------- + Cubic Bézier curve control points are not supported yet in this setter. + + Examples + --------- + >>> cam_pos = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} + >>> Timeline.set_camera_keyframes('position', cam_pos) + """ + self.set_keyframes(attrib, keyframes, is_camera=True) + + def set_interpolator(self, attrib, interpolator, is_camera=False, + is_evaluator=False, **kwargs): + """Set keyframes interpolator for a certain property + + Parameters + ---------- + attrib: str + The name of the property. + interpolator: function + The generator function of the interpolator to be used to + interpolate/evaluate keyframes. + is_camera: bool, optional + Indicated whether dealing with a camera property or general + property. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes such as: + >>> def get_position(t): + >>> return np.array([np.sin(t), np.cos(t) * 5, 5]) + + Other Parameters + ---------------- + spline_degree: int, optional + The degree of the spline in case of setting a spline interpolator. + + Notes + ----- + If an evaluator is used to set the values of actor's properties such as + position, scale, color, rotation, or opacity, it has to return a value + with the same shape as the evaluated property, i.e.: for scale, it + has to return an array with shape 1x3, and for opacity, it has to + return a 1x1, an int, or a float value. + + Examples + --------- + >>> Timeline.set_interpolator('position', linear_interpolator) + + >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) + >>> Timeline.set_interpolator('position', pos_fun) + """ + + attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) + keyframes = attrib_data.get('keyframes', {}) + interp_data = attrib_data.get('interpolator', {}) + if is_evaluator: + interp_data['base'] = None + interp_data['func'] = interpolator + else: + interp_data['base'] = interpolator + interp_data['args'] = kwargs + # Maintain interpolator base incase new keyframes are added. + if len(keyframes) == 0: + return + new_interp = interpolator(keyframes, **kwargs) + interp_data['func'] = new_interp + + # update motion path + self.update_motion_path() + + def is_interpolatable(self, attrib, is_camera=False): + """Checks whether a property is interpolatable. + + Parameters + ---------- + attrib: str + The name of the property. + is_camera: bool + Indicated whether checking a camera property or general property. + + Returns + ------- + bool + True if the property is interpolatable by the Timeline. + + Notes + ------- + True means that it's safe to use `Interpolator.interpolate(t)` for the + specified property. And False means the opposite. + + """ + data = self._camera_data if is_camera else self._data + return bool(data.get(attrib, {}).get('interpolator', {}).get('func')) + + def set_camera_interpolator(self, attrib, interpolator, + is_evaluator=False): + """Set the interpolator for a specific camera property. + + Parameters + ---------- + attrib: str + The name of the camera property. + The already handled properties are position, focal, and view_up. + + interpolator: function + The generator function of the interpolator that handles the + camera property interpolation between keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + + Examples + --------- + >>> Timeline.set_camera_interpolator('focal', linear_interpolator) + """ + self.set_interpolator(attrib, interpolator, is_camera=True, + is_evaluator=is_evaluator) + + def set_position_interpolator(self, interpolator, is_evaluator=False, + **kwargs): + """Set the position interpolator for all actors inside the + timeline. + + Parameters + ---------- + interpolator: function + The generator function of the interpolator that would handle the + position keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + + Other Parameters + ---------------- + degree: int + The degree of the spline interpolation in case of setting + the `spline_interpolator`. + + Examples + --------- + >>> Timeline.set_position_interpolator(spline_interpolator, degree=5) + """ + self.set_interpolator('position', interpolator, + is_evaluator=is_evaluator, **kwargs) + + def set_scale_interpolator(self, interpolator, is_evaluator=False): + """Set the scale interpolator for all the actors inside the + timeline. + + Parameters + ---------- + interpolator: function + TThe generator function of the interpolator that would handle + the scale keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + + Examples + --------- + >>> Timeline.set_scale_interpolator(step_interpolator) + """ + self.set_interpolator('scale', interpolator, is_evaluator=is_evaluator) + + def set_rotation_interpolator(self, interpolator, is_evaluator=False): + """Set the scale interpolator for all the actors inside the + timeline. + + Parameters + ---------- + interpolator: function + The generator function of the interpolator that would handle the + rotation (orientation) keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + Examples + --------- + >>> Timeline.set_rotation_interpolator(slerp) + """ + self.set_interpolator('rotation', interpolator, + is_evaluator=is_evaluator) + + def set_color_interpolator(self, interpolator, is_evaluator=False): + """Set the color interpolator for all the actors inside the + timeline. + + Parameters + ---------- + interpolator: function + The generator function of the interpolator that would handle + the color keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + Examples + --------- + >>> Timeline.set_color_interpolator(lab_color_interpolator) + """ + self.set_interpolator('color', interpolator, is_evaluator=is_evaluator) + + def set_opacity_interpolator(self, interpolator, is_evaluator=False): + """Set the opacity interpolator for all the actors inside the + timeline. + + Parameters + ---------- + interpolator: function + The generator function of the interpolator that would handle + the opacity keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + Examples + --------- + >>> Timeline.set_opacity_interpolator(step_interpolator) + """ + self.set_interpolator('opacity', interpolator, + is_evaluator=is_evaluator) + + def set_camera_position_interpolator(self, interpolator, + is_evaluator=False): + """Set the camera position interpolator. + + Parameters + ---------- + interpolator: function + The generator function of the interpolator that would handle the + interpolation of the camera position keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + """ + self.set_camera_interpolator("position", interpolator, + is_evaluator=is_evaluator) + + def set_camera_focal_interpolator(self, interpolator, is_evaluator=False): + """Set the camera focal position interpolator. + + Parameters + ---------- + interpolator: function + The generator function of the interpolator that would handle the + interpolation of the camera focal position keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + """ + self.set_camera_interpolator("focal", interpolator, + is_evaluator=is_evaluator) + + def get_value(self, attrib, timestamp): + """Return the value of an attribute at any given timestamp. + + Parameters + ---------- + attrib: str + The attribute name. + timestamp: float + The timestamp to interpolate at. + """ + value = self._data.get(attrib, {}).get('interpolator', {}). \ + get('func')(timestamp) + return value + + def get_current_value(self, attrib): + """Return the value of an attribute at current time. + + Parameters + ---------- + attrib: str + The attribute name. + """ + return self._data.get(attrib).get('interpolator'). \ + get('func')(self._timeline.current_timestamp) + + def get_camera_value(self, attrib, timestamp): + """Return the value of an attribute interpolated at any given + timestamp. + + Parameters + ---------- + attrib: str + The attribute name. + timestamp: float + The timestamp to interpolate at. + + """ + return self._camera_data.get(attrib).get('interpolator'). \ + get('func')(timestamp) + + def set_position(self, timestamp, position, **kwargs): + """Set a position keyframe at a specific timestamp. + + Parameters + ---------- + timestamp: float + Timestamp of the keyframe + position: ndarray, shape (1, 3) + Position value + + Other Parameters + ---------------- + in_cp: float + The control point in case of using `cubic Bézier interpolator` when + time exceeds this timestamp. + out_cp: float + The control point in case of using `cubic Bézier interpolator` when + time precedes this timestamp. + in_tangent: ndarray, shape (1, M), optional + The in tangent at that position for the cubic spline curve. + out_tangent: ndarray, shape (1, M), optional + The out tangent at that position for the cubic spline curve. + + Notes + ----- + `in_cp` and `out_cp` only needed when using the cubic bezier + interpolation method. + """ + self.set_keyframe('position', timestamp, position, **kwargs) + + def set_position_keyframes(self, keyframes): + """Set a dict of position keyframes at once. + Should be in the following form: + {timestamp_1: position_1, timestamp_2: position_2} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and positions as values. + + Examples + -------- + >>> pos_keyframes = {1, np.array([0, 0, 0]), 3, np.array([50, 6, 6])} + >>> Timeline.set_position_keyframes(pos_keyframes) + """ + self.set_keyframes('position', keyframes) + + def set_rotation(self, timestamp, rotation, **kwargs): + """Set a rotation keyframe at a specific timestamp. + + Parameters + ---------- + timestamp: float + Timestamp of the keyframe + rotation: ndarray, shape(1, 3) or shape(1, 4) + Rotation data in euler degrees with shape(1, 3) or in quaternions + with shape(1, 4). + + Notes + ----- + Euler rotations are executed by rotating first around Z then around X, + and finally around Y. + """ + no_components = len(np.array(rotation).flatten()) + if no_components == 4: + self.set_keyframe('rotation', timestamp, rotation, **kwargs) + elif no_components == 3: + # user is expected to set rotation order by default as setting + # orientation of a `vtkActor` ordered as z->x->y. + rotation = np.asarray(rotation, dtype=float) + rotation = transform.Rotation.from_euler('zxy', + rotation[[2, 0, 1]], + degrees=True).as_quat() + self.set_keyframe('rotation', timestamp, rotation, **kwargs) + else: + warnings.warn(f'Keyframe with {no_components} components is not a ' + f'valid rotation data. Skipped!') + + def set_rotation_as_vector(self, timestamp, vector, **kwargs): + """Set a rotation keyframe at a specific timestamp. + + Parameters + ---------- + timestamp: float + Timestamp of the keyframe + vector: ndarray, shape(1, 3) + Directional vector that describes the rotation. + """ + quat = transform.Rotation.from_rotvec(vector).as_quat() + self.set_keyframe('rotation', timestamp, quat, **kwargs) + + def set_scale(self, timestamp, scalar, **kwargs): + """Set a scale keyframe at a specific timestamp. + + Parameters + ---------- + timestamp: float + Timestamp of the keyframe + scalar: ndarray, shape(1, 3) + Scale keyframe value associated with the timestamp. + """ + self.set_keyframe('scale', timestamp, scalar, **kwargs) + + def set_scale_keyframes(self, keyframes): + """Set a dict of scale keyframes at once. + Should be in the following form: + {timestamp_1: scale_1, timestamp_2: scale_2} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and scales as values. + + Examples + -------- + >>> scale_keyframes = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} + >>> Timeline.set_scale_keyframes(scale_keyframes) + """ + self.set_keyframes('scale', keyframes) + + def set_color(self, timestamp, color, **kwargs): + """Set color keyframe at a specific timestamp. + + Parameters + ---------- + timestamp: float + Timestamp of the keyframe + color: ndarray, shape(1, 3) + Color keyframe value associated with the timestamp. + """ + self.set_keyframe('color', timestamp, color, **kwargs) + + def set_color_keyframes(self, keyframes): + """Set a dict of color keyframes at once. + Should be in the following form: + {timestamp_1: color_1, timestamp_2: color_2} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and color as values. + + Examples + -------- + >>> color_keyframes = {1, np.array([1, 0, 1]), 3, np.array([0, 0, 1])} + >>> Timeline.set_color_keyframes(color_keyframes) + """ + self.set_keyframes('color', keyframes) + + def set_opacity(self, timestamp, opacity, **kwargs): + """Set opacity keyframe at a specific timestamp. + + Parameters + ---------- + timestamp: float + Timestamp of the keyframe + opacity: ndarray, shape(1, 3) + Opacity keyframe value associated with the timestamp. + """ + self.set_keyframe('opacity', timestamp, opacity, **kwargs) + + def set_opacity_keyframes(self, keyframes): + """Set a dict of opacity keyframes at once. + Should be in the following form: + {timestamp_1: opacity_1, timestamp_2: opacity_2} + + Parameters + ---------- + keyframes: dict(float: ndarray, shape(1, 1) or float or int) + A dict with timestamps as keys and opacities as values. + + Notes + ----- + Opacity values should be between 0 and 1. + + Examples + -------- + >>> opacity = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} + >>> Timeline.set_scale_keyframes(opacity) + """ + self.set_keyframes('opacity', keyframes) + + def get_position(self, t): + """Return the interpolated position. + + Parameters + ---------- + t: float + The time to interpolate position at. + + Returns + ------- + ndarray(1, 3): + The interpolated position. + """ + return self.get_value('position', t) + + def get_rotation(self, t, as_quat=False): + """Return the interpolated rotation. + + Parameters + ---------- + t: float + the time to interpolate rotation at. + as_quat: bool + Returned rotation will be as quaternion if True. + + Returns + ------- + ndarray(1, 3): + The interpolated rotation as Euler degrees by default. + """ + rot = self.get_value('rotation', t) + if len(rot) == 4: + if as_quat: + return rot + r = transform.Rotation.from_quat(rot) + degrees = r.as_euler('zxy', degrees=True)[[1, 2, 0]] + return degrees + elif not as_quat: + return rot + return transform.Rotation.from_euler('zxy', + rot[[2, 0, 1]], + degrees=True).as_quat() + + def get_scale(self, t): + """Return the interpolated scale. + + Parameters + ---------- + t: float + The time to interpolate scale at. + + Returns + ------- + ndarray(1, 3): + The interpolated scale. + """ + return self.get_value('scale', t) + + def get_color(self, t): + """Return the interpolated color. + + Parameters + ---------- + t: float + The time to interpolate color value at. + + Returns + ------- + ndarray(1, 3): + The interpolated color. + """ + return self.get_value('color', t) + + def get_opacity(self, t): + """Return the opacity value. + + Parameters + ---------- + t: float + The time to interpolate opacity at. + + Returns + ------- + ndarray(1, 1): + The interpolated opacity. + """ + return self.get_value('opacity', t) + + def set_camera_position(self, timestamp, position, **kwargs): + """Set the camera position keyframe. + + Parameters + ---------- + timestamp: float + The time to interpolate opacity at. + position: ndarray, shape(1, 3) + The camera position + """ + self.set_camera_keyframe('position', timestamp, position, **kwargs) + + def set_camera_focal(self, timestamp, position, **kwargs): + """Set camera's focal position keyframe. + + Parameters + ---------- + timestamp: float + The time to interpolate opacity at. + position: ndarray, shape(1, 3) + The camera position + """ + self.set_camera_keyframe('focal', timestamp, position, **kwargs) + + def set_camera_view_up(self, timestamp, direction, **kwargs): + """Set the camera view-up direction keyframe. + + Parameters + ---------- + timestamp: float + The time to interpolate at. + direction: ndarray, shape(1, 3) + The camera view-up direction + """ + self.set_camera_keyframe('view_up', timestamp, direction, **kwargs) + + def set_camera_rotation(self, timestamp, rotation, **kwargs): + """Set the camera rotation keyframe. + + Parameters + ---------- + timestamp: float + The time to interpolate at. + rotation: ndarray, shape(1, 3) or shape(1, 4) + Rotation data in euler degrees with shape(1, 3) or in quaternions + with shape(1, 4). + + Notes + ----- + Euler rotations are executed by rotating first around Z then around X, + and finally around Y. + """ + self.set_rotation(timestamp, rotation, is_camera=True, **kwargs) + + def set_camera_position_keyframes(self, keyframes): + """Set a dict of camera position keyframes at once. + Should be in the following form: + {timestamp_1: position_1, timestamp_2: position_2} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and opacities as values. + + Examples + -------- + >>> pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} + >>> Timeline.set_camera_position_keyframes(pos) + """ + self.set_camera_keyframes('position', keyframes) + + def set_camera_focal_keyframes(self, keyframes): + """Set multiple camera focal position keyframes at once. + Should be in the following form: + {timestamp_1: focal_1, timestamp_2: focal_1, ...} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and camera focal positions as + values. + + Examples + -------- + >>> focal_pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} + >>> Timeline.set_camera_focal_keyframes(focal_pos) + """ + self.set_camera_keyframes('focal', keyframes) + + def set_camera_view_up_keyframes(self, keyframes): + """Set multiple camera view up direction keyframes. + Should be in the following form: + {timestamp_1: view_up_1, timestamp_2: view_up_2, ...} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and camera view up vectors as + values. + + Examples + -------- + >>> view_ups = {0, np.array([1, 0, 0]), 3, np.array([0, 1, 0])} + >>> Timeline.set_camera_view_up_keyframes(view_ups) + """ + self.set_camera_keyframes('view_up', keyframes) + + def get_camera_position(self, t): + """Return the interpolated camera position. + + Parameters + ---------- + t: float + The time to interpolate camera position value at. + + Returns + ------- + ndarray(1, 3): + The interpolated camera position. + + Notes + ----- + The returned position does not necessarily reflect the current camera + position, but te expected one. + """ + return self.get_camera_value('position', t) + + def get_camera_focal(self, t): + """Return the interpolated camera's focal position. + + Parameters + ---------- + t: float + The time to interpolate at. + + Returns + ------- + ndarray(1, 3): + The interpolated camera's focal position. + + Notes + ----- + The returned focal position does not necessarily reflect the current + camera's focal position, but the expected one. + """ + return self.get_camera_value('focal', t) + + def get_camera_view_up(self, t): + """Return the interpolated camera's view-up directional vector. + + Parameters + ---------- + t: float + The time to interpolate at. + + Returns + ------- + ndarray(1, 3): + The interpolated camera view-up directional vector. + + Notes + ----- + The returned focal position does not necessarily reflect the actual + camera view up directional vector, but the expected one. + """ + return self.get_camera_value('view_up', t) + + def get_camera_rotation(self, t): + """Return the interpolated rotation for the camera expressed + in euler angles. + + Parameters + ---------- + t: float + The time to interpolate at. + + Returns + ------- + ndarray(1, 3): + The interpolated camera's rotation. + + Notes + ----- + The returned focal position does not necessarily reflect the actual + camera view up directional vector, but the expected one. + """ + return self.get_camera_value('rotation', t) + + def add(self, item): + """Add an item to the Timeline. + This item can be an actor, Timeline, list of actors, or a list of + Timelines. + + Parameters + ---------- + item: Timeline, vtkActor, list(Timeline), or list(vtkActor) + Actor/s to be animated by the timeline. + """ + if isinstance(item, list): + for a in item: + self.add(a) + return + elif isinstance(item, Actor): + self.add_actor(item) + elif isinstance(item, Animation): + self.add_child_animation(item) + else: + raise ValueError(f"Object of type {type(item)} can't be added to " + f"the timeline.") + + def add_child_animation(self, animation): + """Add child Animation or list of Animations. + + Parameters + ---------- + animation: Animation or list(Animation) + Animation/s to be added. + """ + if isinstance(animation, list): + for a in animation: + self.add_child_animation(a) + return + animation._parent_animation = self + animation.update_motion_path() + self._animations.append(animation) + + def add_actor(self, actor, static=False): + """Add an actor or list of actors to the Timeline. + + Parameters + ---------- + actor: vtkActor or list(vtkActor) + Actor/s to be animated by the timeline. + static: bool + Indicated whether the actor should be animated and controlled by + the timeline or just a static actor that gets added to the scene + along with the Timeline. + """ + if isinstance(actor, list): + for a in actor: + self.add_actor(a, static=static) + elif static: + self._static_actors.append(actor) + else: + actor.vcolors = utils.colors_from_actor(actor) + super(Animation, self).add(actor) + + @property + def timeline(self) -> 'Timeline': + return self._timeline + + @timeline.setter + def timeline(self, timeline): + """Assign the Timeline responsible for handling the animation.""" + self._timeline = timeline + if self._animations: + for animation in self._animations: + animation.timeline = timeline + + @property + def actors(self): + """Return a list of actors. + + Returns + ------- + list: + List of actors controlled by the Timeline. + """ + return self.items + + @property + def child_animations(self) -> 'list[Animation]': + """Return a list of child Animations. + + Returns + ------- + list: + List of child Animations of this Animation. + """ + return self._animations + + def add_static_actor(self, actor): + """Add an actor or list of actors as static actor/s which will not be + controlled nor animated by the Timeline. All static actors will be + added to the scene when the Timeline is added to the scene. + + Parameters + ---------- + actor: vtkActor or list(vtkActor) + Static actor/s. + """ + self.add_actor(actor, static=True) + + @property + def static_actors(self): + """Return a list of static actors. + + Returns + ------- + list: + List of static actors. + """ + return self._static_actors + + def remove_animations(self): + """Remove all child Animations from the Animation""" + self._animations.clear() + + def remove_actor(self, actor): + """Remove an actor from the Animation. + + Parameters + ---------- + actor: vtkActor + Actor to be removed from the Animation. + """ + self._items.remove(actor) + + def remove_actors(self): + """Remove all actors from the Animation""" + self.clear() + + def add_update_callback(self, property_name, cbk_func): + """Add a function to be called each time animation is updated + This function must accept only one argument which is the current value + of the named property. + + + Parameters + ---------- + property_name: str + The name of the property. + cbk_func: function + The function to be called whenever the animation is updated. + """ + attrib = self._get_attribute_data(property_name) + attrib.get('callbacks', []).append(cbk_func) + + def update_animation(self, t=0.0): + """Update the timeline animations + + Parameters + ---------- + t: float or int, optional, default: 0.0 + Time to update animation at. + """ + + # handling in/out of scene events + in_scene = self.is_inside_scene_at(t) + self.handle_scene_event(t) + + if self._loop and t: + t = t % self.duration + if isinstance(self._parent_animation, Animation): + self._transform.DeepCopy(self._parent_animation._transform) + else: + self._transform.Identity() + + if self._camera is not None: + if self.is_interpolatable('rotation', is_camera=True): + pos = self._camera.GetPosition() + translation = np.identity(4) + translation[:3, 3] = pos + # camera axis is reverted + rot = -self.get_camera_rotation(t) + rot = transform.Rotation.from_quat(rot).as_matrix() + rot = np.array([[*rot[0], 0], + [*rot[1], 0], + [*rot[2], 0], + [0, 0, 0, 1]]) + rot = translation @ rot @ np.linalg.inv(translation) + self._camera.SetModelTransformMatrix(rot.flatten()) + + if self.is_interpolatable('position', is_camera=True): + cam_pos = self.get_camera_position(t) + self._camera.SetPosition(cam_pos) + + if self.is_interpolatable('focal', is_camera=True): + cam_foc = self.get_camera_focal(t) + self._camera.SetFocalPoint(cam_foc) + + if self.is_interpolatable('view_up', is_camera=True): + cam_up = self.get_camera_view_up(t) + self._camera.SetViewUp(cam_up) + elif not self.is_interpolatable('view_up', is_camera=True): + # to preserve up-view as default after user interaction + self._camera.SetViewUp(0, 1, 0) + + elif self._is_camera_animated and self._scene: + self._camera = self._scene.camera() + self.update_animation(t) + return + + # actors properties + if in_scene: + if self.is_interpolatable('position'): + position = self.get_position(t) + self._transform.Translate(*position) + + if self.is_interpolatable('opacity'): + opacity = self.get_opacity(t) + [act.GetProperty().SetOpacity(opacity) for + act in self.actors] + + if self.is_interpolatable('rotation'): + x, y, z = self.get_rotation(t) + # Rotate in the same order as VTK defaults. + self._transform.RotateZ(z) + self._transform.RotateX(x) + self._transform.RotateY(y) + + if self.is_interpolatable('scale'): + scale = self.get_scale(t) + self._transform.Scale(*scale) + + if self.is_interpolatable('color'): + color = self.get_color(t) + for act in self.actors: + act.vcolors[:] = color * 255 + utils.update_actor(act) + + # update actors' transformation matrix + [act.SetUserTransform(self._transform) for act in self.actors] + + for attrib in self._data: + callbacks = self._data.get(attrib, {}).get('callbacks', []) + if callbacks is not [] and self.is_interpolatable(attrib): + value = self.get_value(attrib, t) + [cbk(value) for cbk in callbacks] + + # Also update all child Timelines. + [anim.update_animation(t) for anim in self._animations] + + def add_to_scene(self, ren): + """Add Timeline and all actors and sub Timelines to the scene""" + super(Animation, self).add_to_scene(ren) + [ren.add(static_act) for static_act in self._static_actors] + [ren.add(animation) for animation in self._animations] + + b = np.zeros(6) + + + if self._motion_path_actor: + ren.add(self._motion_path_actor) + self._scene = ren + self._added_to_scene = True + self.update_animation(0) + diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index f77c6a0e6..80bb9d0f4 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1,17 +1,9 @@ import time -import warnings -from collections import defaultdict -from fury import utils, actor -from fury.actor import Container -from fury.animation.interpolator import spline_interpolator, \ - step_interpolator, linear_interpolator, slerp -import numpy as np -from scipy.spatial import transform from fury.ui.elements import PlaybackPanel -from fury.lib import Actor, Transform +from fury.animation.animation import Animation -class Timeline(Container): +class Timeline(Animation): """Keyframe animation timeline class. This timeline is responsible for keyframe animations for a single or a @@ -33,42 +25,24 @@ class Timeline(Container): the fixed length of the timeline. If set to None, the timeline will get its length from the keyframes. loop : bool, optional - the number of legs the animal has (default 4) - motion_path_res : int, default: None - the number of line segments used to visualizer the timeline's motion - path. + Whether loop playing the animation or not """ def __init__(self, actors=None, playback_panel=False, length=None, loop=False, motion_path_res=None): - super().__init__() - self._data = defaultdict(dict) - self._camera_data = defaultdict(dict) + super().__init__(actors=actors, length=length, loop=loop, + motion_path_res=motion_path_res) self.playback_panel = None - self._last_timestamp = 0 self._current_timestamp = 0 self._speed = 1 - self._timelines = [] - self._static_actors = [] - self._camera = None - self._scene = None self._last_started_time = 0 self._playing = False self._length = length - self._final_timestamp = 0 - self._needs_update = False - self._reverse_playing = False - self._loop = loop - self._added_to_scene = True - self._add_to_scene_time = 0 - self._remove_from_scene_time = None - self._is_camera_animated = False - self._motion_path_res = motion_path_res - self._motion_path_actor = None - self._parent_timeline = None - self._transform = Transform() - + self._reverse_playing = True + self._animations = [] + self._timeline = None + self._parent_animation = None # Handle actors while constructing the timeline. if playback_panel: def set_loop(loop): @@ -89,1290 +63,31 @@ def set_speed(speed): if actors is not None: self.add_actor(actors) - def update_final_timestamp(self): - """Calculate and return the final timestamp of all keyframes. - - Returns - ------- - float - final timestamp that can be reached inside the Timeline. - """ - if self._length is None: - self._final_timestamp = max(self.final_timestamp, - max([0] + [tl.update_final_timestamp() - for tl in self.timelines])) - else: - self._final_timestamp = self._length - if self.has_playback_panel: - self.playback_panel.final_time = self._final_timestamp - return self._final_timestamp - - def update_motion_path(self): - """Update motion path visualization actor""" - res = self._motion_path_res - tl = self - while not res and isinstance(tl._parent_timeline, Timeline): - tl = tl._parent_timeline - res = tl._motion_path_res - if not res: - return - lines = [] - colors = [] - if self.is_interpolatable('position'): - ts = np.linspace(0, self.final_timestamp, res) - [lines.append(self.get_position(t).tolist()) for t in ts] - if self.is_interpolatable('color'): - [colors.append(self.get_color(t)) for t in ts] - elif len(self.items) >= 1: - colors = sum([i.vcolors[0] / 255 for i in self.items]) / \ - len(self.items) - else: - colors = [1, 1, 1] - if len(lines) > 0: - lines = np.array([lines]) - if colors is []: - colors = np.array([colors]) - - mpa = actor.line(lines, colors=colors, opacity=0.6) - if self._scene: - # remove old motion path actor - if self._motion_path_actor is not None: - self._scene.rm(self._motion_path_actor) - self._scene.add(mpa) - self._motion_path_actor = mpa - - def set_timestamp(self, timestamp): - """Set the current timestamp of the animation. - - Parameters - ---------- - timestamp: float - Current timestamp to be set. - """ - if self.playing: - self._last_started_time = \ - time.perf_counter() - timestamp / self.speed - else: - self._last_timestamp = timestamp - - def _get_data(self, is_camera=False): - if is_camera: - self._is_camera_animated = True - return self._camera_data - else: - return self._data - - def _get_attribute_data(self, attrib, is_camera=False): - data = self._get_data(is_camera=is_camera) - - if attrib not in data: - data[attrib] = { - 'keyframes': defaultdict(dict), - 'interpolator': { - 'base': linear_interpolator if attrib != 'rotation' else - slerp, - 'func': None, - 'args': defaultdict() - }, - 'callbacks': [], - } - return data.get(attrib) - - def set_keyframe(self, attrib, timestamp, value, is_camera=False, - update_interpolator=True, **kwargs): - """Set a keyframe for a certain attribute. - - Parameters - ---------- - attrib: str - The name of the attribute. - timestamp: float - Timestamp of the keyframe. - value: ndarray or float or bool - Value of the keyframe at the given timestamp. - is_camera: bool, optional - Indicated whether setting a camera property or general property. - update_interpolator: bool, optional - Interpolator will be reinitialized if Ture - - Other Parameters - ---------------- - in_cp: ndarray, shape (1, M), optional - The in control point in case of using cubic Bézier interpolator. - out_cp: ndarray, shape (1, M), optional - The out control point in case of using cubic Bézier interpolator. - in_tangent: ndarray, shape (1, M), optional - The in tangent at that position for the cubic spline curve. - out_tangent: ndarray, shape (1, M), optional - The out tangent at that position for the cubic spline curve. - """ - - attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) - keyframes = attrib_data.get('keyframes') - - keyframes[timestamp] = { - 'value': np.array(value).astype(float), - **{par: np.array(val).astype(float) for par, val in kwargs.items() - if val is not None} - } - - if update_interpolator: - interp = attrib_data.get('interpolator') - interp_base = interp.get( - 'base', linear_interpolator if attrib != 'rotation' else slerp) - args = interp.get('args', {}) - self.set_interpolator(attrib, interp_base, - is_camera=is_camera, **args) - - if timestamp > self.final_timestamp: - self._final_timestamp = timestamp - if self.has_playback_panel: - final_t = self.update_final_timestamp() - self.playback_panel.final_time = final_t - - if timestamp > 0: - self.update_animation(force=True) - - # update motion path - self.update_motion_path() - - def set_keyframes(self, attrib, keyframes, is_camera=False): - """Set multiple keyframes for a certain attribute. - - Parameters - ---------- - attrib: str - The name of the attribute. - keyframes: dict - A dict object containing keyframes to be set. - is_camera: bool - Indicated whether setting a camera property or general property. - - Notes - --------- - Keyframes can be on any of the following forms: - >>> key_frames_simple = {1: [1, 2, 1], 2: [3, 4, 5]} - >>> key_frames_bezier = {1: {'value': [1, 2, 1]}, - >>> 2: {'value': [3, 4, 5], 'in_cp': [1, 2, 3]}} - - Examples - --------- - >>> pos_keyframes = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} - >>> Timeline.set_keyframes('position', pos_keyframes) - """ - for t, keyframe in keyframes.items(): - if isinstance(keyframe, dict): - self.set_keyframe(attrib, t, **keyframe, is_camera=is_camera) - else: - self.set_keyframe(attrib, t, keyframe, is_camera=is_camera) - - def set_camera_keyframe(self, attrib, timestamp, value, **kwargs): - """Set a keyframe for a camera property - - Parameters - ---------- - attrib: str - The name of the attribute. - timestamp: float - Timestamp of the keyframe. - value: value: ndarray or float or bool - Value of the keyframe at the given timestamp. - **kwargs: dict, optional - Additional keyword arguments passed to `set_keyframe`. - """ - self.set_keyframe(attrib, timestamp, value, is_camera=True, **kwargs) - - def is_inside_scene_at(self, timestamp): - parent = self.parent_timeline - parent_in_scene = True - if parent is not None: - parent_in_scene = parent._added_to_scene - - if self.is_interpolatable('in_scene'): - return parent_in_scene and self.get_value('in_scene', timestamp) - - return parent_in_scene - - def add_to_scene_at(self, timestamp): - """Set timestamp for adding Timeline to scene event. - - Parameters - ---------- - timestamp: float - Timestamp of the event. - """ - if not self.is_interpolatable('in_scene'): - self.set_keyframe('in_scene', timestamp, True) - self.set_interpolator('in_scene', step_interpolator) - else: - self.set_keyframe('in_scene', timestamp, True) - - def remove_from_scene_at(self, timestamp): - """Set timestamp for removing Timeline to scene event. - - Parameters - ---------- - timestamp: float - Timestamp of the event. - """ - if not self.is_interpolatable('in_scene'): - self.set_keyframe('in_scene', timestamp, False) - self.set_interpolator('in_scene', step_interpolator) - else: - self.set_keyframe('in_scene', timestamp, False) - - def handle_scene_event(self, timestamp): - should_be_in_scene = self.is_inside_scene_at(timestamp) - if self._scene is not None: - if should_be_in_scene and not self._added_to_scene: - super(Timeline, self).add_to_scene(self._scene) - self._added_to_scene = True - elif not should_be_in_scene and self._added_to_scene: - super(Timeline, self).remove_from_scene(self._scene) - self._added_to_scene = False - - def set_camera_keyframes(self, attrib, keyframes): - """Set multiple keyframes for a certain camera property - - Parameters - ---------- - attrib: str - The name of the property. - keyframes: dict - A dict object containing keyframes to be set. - - Notes - --------- - Cubic Bézier curve control points are not supported yet in this setter. - - Examples - --------- - >>> cam_pos = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} - >>> Timeline.set_camera_keyframes('position', cam_pos) - """ - self.set_keyframes(attrib, keyframes, is_camera=True) - - def set_interpolator(self, attrib, interpolator, is_camera=False, - is_evaluator=False, **kwargs): - """Set keyframes interpolator for a certain property - - Parameters - ---------- - attrib: str - The name of the property. - interpolator: function - The generator function of the interpolator to be used to - interpolate/evaluate keyframes. - is_camera: bool, optional - Indicated whether dealing with a camera property or general - property. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes such as: - >>> def get_position(t): - >>> return np.array([np.sin(t), np.cos(t) * 5, 5]) - - Other Parameters - ---------------- - spline_degree: int, optional - The degree of the spline in case of setting a spline interpolator. - - Notes - ----- - If an evaluator is used to set the values of actor's properties such as - position, scale, color, rotation, or opacity, it has to return a value - with the same shape as the evaluated property, i.e.: for scale, it - has to return an array with shape 1x3, and for opacity, it has to - return a 1x1, an int, or a float value. - - Examples - --------- - >>> Timeline.set_interpolator('position', linear_interpolator) - - >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) - >>> Timeline.set_interpolator('position', pos_fun) - """ - - attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) - keyframes = attrib_data.get('keyframes', {}) - interp_data = attrib_data.get('interpolator', {}) - if is_evaluator: - interp_data['base'] = None - interp_data['func'] = interpolator - else: - interp_data['base'] = interpolator - interp_data['args'] = kwargs - # Maintain interpolator base incase new keyframes are added. - if len(keyframes) == 0: - return - new_interp = interpolator(keyframes, **kwargs) - interp_data['func'] = new_interp - - # update motion path - self.update_motion_path() - - def is_interpolatable(self, attrib, is_camera=False): - """Checks whether a property is interpolatable. - - Parameters - ---------- - attrib: str - The name of the property. - is_camera: bool - Indicated whether checking a camera property or general property. - - Returns - ------- - bool - True if the property is interpolatable by the Timeline. - - Notes - ------- - True means that it's safe to use `Interpolator.interpolate(t)` for the - specified property. And False means the opposite. - - """ - data = self._camera_data if is_camera else self._data - return bool(data.get(attrib, {}).get('interpolator', {}).get('func')) - - def set_camera_interpolator(self, attrib, interpolator, - is_evaluator=False): - """Set the interpolator for a specific camera property. - - Parameters - ---------- - attrib: str - The name of the camera property. - The already handled properties are position, focal, and view_up. - - interpolator: function - The generator function of the interpolator that handles the - camera property interpolation between keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - - Examples - --------- - >>> Timeline.set_camera_interpolator('focal', linear_interpolator) - """ - self.set_interpolator(attrib, interpolator, is_camera=True, - is_evaluator=is_evaluator) - - def set_position_interpolator(self, interpolator, is_evaluator=False, - **kwargs): - """Set the position interpolator for all actors inside the - timeline. - - Parameters - ---------- - interpolator: function - The generator function of the interpolator that would handle the - position keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - - Other Parameters - ---------------- - degree: int - The degree of the spline interpolation in case of setting - the `spline_interpolator`. - - Examples - --------- - >>> Timeline.set_position_interpolator(spline_interpolator, degree=5) - """ - self.set_interpolator('position', interpolator, - is_evaluator=is_evaluator, **kwargs) - - def set_scale_interpolator(self, interpolator, is_evaluator=False): - """Set the scale interpolator for all the actors inside the - timeline. - - Parameters - ---------- - interpolator: function - TThe generator function of the interpolator that would handle - the scale keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - - Examples - --------- - >>> Timeline.set_scale_interpolator(step_interpolator) - """ - self.set_interpolator('scale', interpolator, is_evaluator=is_evaluator) - - def set_rotation_interpolator(self, interpolator, is_evaluator=False): - """Set the scale interpolator for all the actors inside the - timeline. - - Parameters - ---------- - interpolator: function - The generator function of the interpolator that would handle the - rotation (orientation) keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - Examples - --------- - >>> Timeline.set_rotation_interpolator(slerp) - """ - self.set_interpolator('rotation', interpolator, - is_evaluator=is_evaluator) - - def set_color_interpolator(self, interpolator, is_evaluator=False): - """Set the color interpolator for all the actors inside the - timeline. - - Parameters - ---------- - interpolator: function - The generator function of the interpolator that would handle - the color keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - Examples - --------- - >>> Timeline.set_color_interpolator(lab_color_interpolator) - """ - self.set_interpolator('color', interpolator, is_evaluator=is_evaluator) - - def set_opacity_interpolator(self, interpolator, is_evaluator=False): - """Set the opacity interpolator for all the actors inside the - timeline. - - Parameters - ---------- - interpolator: function - The generator function of the interpolator that would handle - the opacity keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - Examples - --------- - >>> Timeline.set_opacity_interpolator(step_interpolator) - """ - self.set_interpolator('opacity', interpolator, - is_evaluator=is_evaluator) - - def set_camera_position_interpolator(self, interpolator, - is_evaluator=False): - """Set the camera position interpolator. - - Parameters - ---------- - interpolator: function - The generator function of the interpolator that would handle the - interpolation of the camera position keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - """ - self.set_camera_interpolator("position", interpolator, - is_evaluator=is_evaluator) - - def set_camera_focal_interpolator(self, interpolator, is_evaluator=False): - """Set the camera focal position interpolator. - - Parameters - ---------- - interpolator: function - The generator function of the interpolator that would handle the - interpolation of the camera focal position keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - """ - self.set_camera_interpolator("focal", interpolator, - is_evaluator=is_evaluator) - - def get_value(self, attrib, timestamp): - """Return the value of an attribute at any given timestamp. - - Parameters - ---------- - attrib: str - The attribute name. - timestamp: float - The timestamp to interpolate at. - """ - value = self._data.get(attrib, {}).get('interpolator', {}). \ - get('func')(timestamp) - return value - - def get_current_value(self, attrib): - """Return the value of an attribute at current time. - - Parameters - ---------- - attrib: str - The attribute name. - """ - return self._data.get(attrib).get('interpolator'). \ - get('func')(self.current_timestamp) - - def get_camera_value(self, attrib, timestamp): - """Return the value of an attribute interpolated at any given - timestamp. - - Parameters - ---------- - attrib: str - The attribute name. - timestamp: float - The timestamp to interpolate at. - - """ - return self._camera_data.get(attrib).get('interpolator'). \ - get('func')(timestamp) - - def set_position(self, timestamp, position, **kwargs): - """Set a position keyframe at a specific timestamp. - - Parameters - ---------- - timestamp: float - Timestamp of the keyframe - position: ndarray, shape (1, 3) - Position value - - Other Parameters - ---------------- - in_cp: float - The control point in case of using `cubic Bézier interpolator` when - time exceeds this timestamp. - out_cp: float - The control point in case of using `cubic Bézier interpolator` when - time precedes this timestamp. - in_tangent: ndarray, shape (1, M), optional - The in tangent at that position for the cubic spline curve. - out_tangent: ndarray, shape (1, M), optional - The out tangent at that position for the cubic spline curve. - - Notes - ----- - `in_cp` and `out_cp` only needed when using the cubic bezier - interpolation method. - """ - self.set_keyframe('position', timestamp, position, **kwargs) - - def set_position_keyframes(self, keyframes): - """Set a dict of position keyframes at once. - Should be in the following form: - {timestamp_1: position_1, timestamp_2: position_2} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and positions as values. - - Examples - -------- - >>> pos_keyframes = {1, np.array([0, 0, 0]), 3, np.array([50, 6, 6])} - >>> Timeline.set_position_keyframes(pos_keyframes) - """ - self.set_keyframes('position', keyframes) - - def set_rotation(self, timestamp, rotation, **kwargs): - """Set a rotation keyframe at a specific timestamp. - - Parameters - ---------- - timestamp: float - Timestamp of the keyframe - rotation: ndarray, shape(1, 3) or shape(1, 4) - Rotation data in euler degrees with shape(1, 3) or in quaternions - with shape(1, 4). - - Notes - ----- - Euler rotations are executed by rotating first around Z then around X, - and finally around Y. - """ - no_components = len(np.array(rotation).flatten()) - if no_components == 4: - self.set_keyframe('rotation', timestamp, rotation, **kwargs) - elif no_components == 3: - # user is expected to set rotation order by default as setting - # orientation of a `vtkActor` ordered as z->x->y. - rotation = np.asarray(rotation, dtype=float) - rotation = transform.Rotation.from_euler('zxy', - rotation[[2, 0, 1]], - degrees=True).as_quat() - self.set_keyframe('rotation', timestamp, rotation, **kwargs) - else: - warnings.warn(f'Keyframe with {no_components} components is not a ' - f'valid rotation data. Skipped!') - - def set_rotation_as_vector(self, timestamp, vector, **kwargs): - """Set a rotation keyframe at a specific timestamp. - - Parameters - ---------- - timestamp: float - Timestamp of the keyframe - vector: ndarray, shape(1, 3) - Directional vector that describes the rotation. - """ - quat = transform.Rotation.from_rotvec(vector).as_quat() - self.set_keyframe('rotation', timestamp, quat, **kwargs) - - def set_scale(self, timestamp, scalar, **kwargs): - """Set a scale keyframe at a specific timestamp. - - Parameters - ---------- - timestamp: float - Timestamp of the keyframe - scalar: ndarray, shape(1, 3) - Scale keyframe value associated with the timestamp. - """ - self.set_keyframe('scale', timestamp, scalar, **kwargs) - - def set_scale_keyframes(self, keyframes): - """Set a dict of scale keyframes at once. - Should be in the following form: - {timestamp_1: scale_1, timestamp_2: scale_2} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and scales as values. - - Examples - -------- - >>> scale_keyframes = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} - >>> Timeline.set_scale_keyframes(scale_keyframes) - """ - self.set_keyframes('scale', keyframes) - - def set_color(self, timestamp, color, **kwargs): - """Set color keyframe at a specific timestamp. - - Parameters - ---------- - timestamp: float - Timestamp of the keyframe - color: ndarray, shape(1, 3) - Color keyframe value associated with the timestamp. - """ - self.set_keyframe('color', timestamp, color, **kwargs) - - def set_color_keyframes(self, keyframes): - """Set a dict of color keyframes at once. - Should be in the following form: - {timestamp_1: color_1, timestamp_2: color_2} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and color as values. - - Examples - -------- - >>> color_keyframes = {1, np.array([1, 0, 1]), 3, np.array([0, 0, 1])} - >>> Timeline.set_color_keyframes(color_keyframes) - """ - self.set_keyframes('color', keyframes) - - def set_opacity(self, timestamp, opacity, **kwargs): - """Set opacity keyframe at a specific timestamp. - - Parameters - ---------- - timestamp: float - Timestamp of the keyframe - opacity: ndarray, shape(1, 3) - Opacity keyframe value associated with the timestamp. - """ - self.set_keyframe('opacity', timestamp, opacity, **kwargs) - - def set_opacity_keyframes(self, keyframes): - """Set a dict of opacity keyframes at once. - Should be in the following form: - {timestamp_1: opacity_1, timestamp_2: opacity_2} - - Parameters - ---------- - keyframes: dict(float: ndarray, shape(1, 1) or float or int) - A dict with timestamps as keys and opacities as values. - - Notes - ----- - Opacity values should be between 0 and 1. - - Examples - -------- - >>> opacity = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} - >>> Timeline.set_scale_keyframes(opacity) - """ - self.set_keyframes('opacity', keyframes) - - def get_position(self, t): - """Return the interpolated position. - - Parameters - ---------- - t: float - The time to interpolate position at. - - Returns - ------- - ndarray(1, 3): - The interpolated position. - """ - return self.get_value('position', t) - - def get_rotation(self, t, as_quat=False): - """Return the interpolated rotation. - - Parameters - ---------- - t: float - the time to interpolate rotation at. - as_quat: bool - Returned rotation will be as quaternion if True. - - Returns - ------- - ndarray(1, 3): - The interpolated rotation as Euler degrees by default. - """ - rot = self.get_value('rotation', t) - if len(rot) == 4: - if as_quat: - return rot - r = transform.Rotation.from_quat(rot) - degrees = r.as_euler('zxy', degrees=True)[[1, 2, 0]] - return degrees - elif not as_quat: - return rot - return transform.Rotation.from_euler('zxy', - rot[[2, 0, 1]], - degrees=True).as_quat() - - def get_scale(self, t): - """Return the interpolated scale. - - Parameters - ---------- - t: float - The time to interpolate scale at. - - Returns - ------- - ndarray(1, 3): - The interpolated scale. - """ - return self.get_value('scale', t) - - def get_color(self, t): - """Return the interpolated color. - - Parameters - ---------- - t: float - The time to interpolate color value at. - - Returns - ------- - ndarray(1, 3): - The interpolated color. - """ - return self.get_value('color', t) - - def get_opacity(self, t): - """Return the opacity value. - - Parameters - ---------- - t: float - The time to interpolate opacity at. - - Returns - ------- - ndarray(1, 1): - The interpolated opacity. - """ - return self.get_value('opacity', t) - - def set_camera_position(self, timestamp, position, **kwargs): - """Set the camera position keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate opacity at. - position: ndarray, shape(1, 3) - The camera position - """ - self.set_camera_keyframe('position', timestamp, position, **kwargs) - - def set_camera_focal(self, timestamp, position, **kwargs): - """Set camera's focal position keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate opacity at. - position: ndarray, shape(1, 3) - The camera position - """ - self.set_camera_keyframe('focal', timestamp, position, **kwargs) - - def set_camera_view_up(self, timestamp, direction, **kwargs): - """Set the camera view-up direction keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate at. - direction: ndarray, shape(1, 3) - The camera view-up direction - """ - self.set_camera_keyframe('view_up', timestamp, direction, **kwargs) - - def set_camera_rotation(self, timestamp, rotation, **kwargs): - """Set the camera rotation keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate at. - rotation: ndarray, shape(1, 3) or shape(1, 4) - Rotation data in euler degrees with shape(1, 3) or in quaternions - with shape(1, 4). - - Notes - ----- - Euler rotations are executed by rotating first around Z then around X, - and finally around Y. - """ - self.set_rotation(timestamp, rotation, is_camera=True, **kwargs) - - def set_camera_position_keyframes(self, keyframes): - """Set a dict of camera position keyframes at once. - Should be in the following form: - {timestamp_1: position_1, timestamp_2: position_2} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and opacities as values. - - Examples - -------- - >>> pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} - >>> Timeline.set_camera_position_keyframes(pos) - """ - self.set_camera_keyframes('position', keyframes) - - def set_camera_focal_keyframes(self, keyframes): - """Set multiple camera focal position keyframes at once. - Should be in the following form: - {timestamp_1: focal_1, timestamp_2: focal_1, ...} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and camera focal positions as - values. - - Examples - -------- - >>> focal_pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} - >>> Timeline.set_camera_focal_keyframes(focal_pos) - """ - self.set_camera_keyframes('focal', keyframes) - - def set_camera_view_up_keyframes(self, keyframes): - """Set multiple camera view up direction keyframes. - Should be in the following form: - {timestamp_1: view_up_1, timestamp_2: view_up_2, ...} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and camera view up vectors as - values. - - Examples - -------- - >>> view_ups = {0, np.array([1, 0, 0]), 3, np.array([0, 1, 0])} - >>> Timeline.set_camera_view_up_keyframes(view_ups) - """ - self.set_camera_keyframes('view_up', keyframes) - - def get_camera_position(self, t): - """Return the interpolated camera position. - - Parameters - ---------- - t: float - The time to interpolate camera position value at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera position. - - Notes - ----- - The returned position does not necessarily reflect the current camera - position, but te expected one. - """ - return self.get_camera_value('position', t) - - def get_camera_focal(self, t): - """Return the interpolated camera's focal position. - - Parameters - ---------- - t: float - The time to interpolate at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera's focal position. - - Notes - ----- - The returned focal position does not necessarily reflect the current - camera's focal position, but the expected one. - """ - return self.get_camera_value('focal', t) - - def get_camera_view_up(self, t): - """Return the interpolated camera's view-up directional vector. - - Parameters - ---------- - t: float - The time to interpolate at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera view-up directional vector. - - Notes - ----- - The returned focal position does not necessarily reflect the actual - camera view up directional vector, but the expected one. - """ - return self.get_camera_value('view_up', t) - - def get_camera_rotation(self, t): - """Return the interpolated rotation for the camera expressed - in euler angles. - - Parameters - ---------- - t: float - The time to interpolate at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera's rotation. - - Notes - ----- - The returned focal position does not necessarily reflect the actual - camera view up directional vector, but the expected one. - """ - return self.get_camera_value('rotation', t) - - def add(self, item): - """Add an item to the Timeline. - This item can be an actor, Timeline, list of actors, or a list of - Timelines. - - Parameters - ---------- - item: Timeline, vtkActor, list(Timeline), or list(vtkActor) - Actor/s to be animated by the timeline. - """ - if isinstance(item, list): - for a in item: - self.add(a) - return - elif isinstance(item, Actor): - self.add_actor(item) - elif isinstance(item, Timeline): - self.add_child_timeline(item) - else: - raise ValueError(f"Object of type {type(item)} can't be added to " - f"the timeline.") - - def add_child_timeline(self, timeline): - """Add child Timeline or list of Timelines. - - Parameters - ---------- - timeline: Timeline or list(Timeline) - Actor/s to be animated by the timeline. - """ - if isinstance(timeline, list): - for a in timeline: - self.add_child_timeline(a) - return - timeline._parent_timeline = self - timeline.update_motion_path() - self._timelines.append(timeline) - - def add_actor(self, actor, static=False): - """Add an actor or list of actors to the Timeline. - - Parameters - ---------- - actor: vtkActor or list(vtkActor) - Actor/s to be animated by the timeline. - static: bool - Indicated whether the actor should be animated and controlled by - the timeline or just a static actor that gets added to the scene - along with the Timeline. - """ - if isinstance(actor, list): - for a in actor: - self.add_actor(a, static=static) - elif static: - self._static_actors.append(actor) - else: - actor.vcolors = utils.colors_from_actor(actor) - super(Timeline, self).add(actor) - - @property - def parent_timeline(self) -> 'Timeline': - return self._parent_timeline - - @property - def actors(self): - """Return a list of actors. - - Returns - ------- - list: - List of actors controlled by the Timeline. - """ - return self.items - - @property - def timelines(self): - """Return a list of child Timelines. - - Returns - ------- - list: - List of child Timelines of this Timeline. - """ - return self._timelines - - def add_static_actor(self, actor): - """Add an actor or list of actors as static actor/s which will not be - controlled nor animated by the Timeline. All static actors will be - added to the scene when the Timeline is added to the scene. - - Parameters - ---------- - actor: vtkActor or list(vtkActor) - Static actor/s. - """ - self.add_actor(actor, static=True) - - @property - def static_actors(self): - """Return a list of static actors. - - Returns - ------- - list: - List of static actors. - """ - return self._static_actors - - def remove_timelines(self): - """Remove all child Timelines from the Timeline""" - self._timelines.clear() - - def remove_actor(self, actor): - """Remove an actor from the Timeline. - - Parameters - ---------- - actor: vtkActor - Actor to be removed from the timeline. - """ - self._items.remove(actor) - - def remove_actors(self): - """Remove all actors from the Timeline""" - self.clear() - - def add_update_callback(self, property_name, cbk_func): - """Add a function to be called each time animation is updated - This function must accept only one argument which is the current value - of the named property. - - - Parameters - ---------- - property_name: str - The name of the property. - cbk_func: function - The function to be called whenever the animation is updated. - """ - attrib = self._get_attribute_data(property_name) - attrib.get('callbacks', []).append(cbk_func) - - def update_animation(self, t=None, force=False): - """Update the timeline animations - - Parameters - ---------- - t: float or int, optional, default: None - Time to update animation at, if `None`, current time of the - `Timeline` will be used. - force: bool, optional, default: False - If 'True', the animation will be updating even if the `Timeline` is - paused or stopped. - - """ - if t is None: - t = self.current_timestamp - if t > self._final_timestamp: - if self._loop: - self.seek(0) - else: - self.seek(self.final_timestamp) - # Doing this will pause both the timeline and the panel. - self.playback_panel.pause() - if self.has_playback_panel and (self.playing or force): - self.update_final_timestamp() - self.playback_panel.current_time = t - - # handling in/out of scene events - in_scene = self.is_inside_scene_at(t) - self.handle_scene_event(t) - - if self.playing or force: - if isinstance(self._parent_timeline, Timeline): - self._transform.DeepCopy(self._parent_timeline._transform) - else: - self._transform.Identity() - - if self._camera is not None: - if self.is_interpolatable('rotation', is_camera=True): - pos = self._camera.GetPosition() - translation = np.identity(4) - translation[:3, 3] = pos - # camera axis is reverted - rot = -self.get_camera_rotation(t) - rot = transform.Rotation.from_quat(rot).as_matrix() - rot = np.array([[*rot[0], 0], - [*rot[1], 0], - [*rot[2], 0], - [0, 0, 0, 1]]) - rot = translation @ rot @ np.linalg.inv(translation) - self._camera.SetModelTransformMatrix(rot.flatten()) - - if self.is_interpolatable('position', is_camera=True): - cam_pos = self.get_camera_position(t) - self._camera.SetPosition(cam_pos) - - if self.is_interpolatable('focal', is_camera=True): - cam_foc = self.get_camera_focal(t) - self._camera.SetFocalPoint(cam_foc) - - if self.is_interpolatable('view_up', is_camera=True): - cam_up = self.get_camera_view_up(t) - self._camera.SetViewUp(cam_up) - elif not self.is_interpolatable('view_up', is_camera=True): - # to preserve up-view as default after user interaction - self._camera.SetViewUp(0, 1, 0) - - elif self._is_camera_animated and self._scene: - self._camera = self._scene.camera() - self.update_animation(force=True) - return - - # actors properties - if in_scene: - if self.is_interpolatable('position'): - position = self.get_position(t) - self._transform.Translate(*position) - - if self.is_interpolatable('opacity'): - opacity = self.get_opacity(t) - [act.GetProperty().SetOpacity(opacity) for - act in self.actors] - - if self.is_interpolatable('rotation'): - x, y, z = self.get_rotation(t) - # Rotate in the same order as VTK defaults. - self._transform.RotateZ(z) - self._transform.RotateX(x) - self._transform.RotateY(y) - - if self.is_interpolatable('scale'): - scale = self.get_scale(t) - self._transform.Scale(*scale) - - if self.is_interpolatable('color'): - color = self.get_color(t) - for act in self.actors: - act.vcolors[:] = color * 255 - utils.update_actor(act) - - # update actors' transformation matrix - [act.SetUserTransform(self._transform) for act in self.actors] - - for attrib in self._data: - callbacks = self._data.get(attrib, {}).get('callbacks', []) - if callbacks is not [] and self.is_interpolatable(attrib): - value = self.get_current_value(attrib) - [cbk(value) for cbk in callbacks] - - # Also update all child Timelines. - [tl.update_animation(t, force=True) - for tl in self.timelines] - # update clipping range - if self.parent_timeline is None and self._scene: - self._scene.reset_clipping_range() - def play(self): """Play the animation""" if not self.playing: - if self.current_timestamp >= self.final_timestamp: + if self.current_timestamp >= self.duration: self.current_timestamp = 0 - self.update_final_timestamp() self._last_started_time = \ - time.perf_counter() - self._last_timestamp / self.speed + time.perf_counter() - self._current_timestamp / self.speed self._playing = True def pause(self): """Pause the animation""" - self._last_timestamp = self.current_timestamp + self._current_timestamp = self.current_timestamp self._playing = False def stop(self): """Stop the animation""" - self._last_timestamp = 0 + self._current_timestamp = 0 self._playing = False - self.update_animation(force=True) + self.update_animation(0) def restart(self): """Restart the animation""" - self._last_timestamp = 0 + self._current_timestamp = 0 self._playing = True - self.update_animation(force=True) + self.update_animation(0) @property def current_timestamp(self): @@ -1385,9 +100,9 @@ def current_timestamp(self): """ if self.playing: - self._last_timestamp = (time.perf_counter() - - self._last_started_time) * self.speed - return self._last_timestamp + self._current_timestamp = (time.perf_counter() - + self._last_started_time) * self.speed + return self._current_timestamp @current_timestamp.setter def current_timestamp(self, timestamp): @@ -1401,17 +116,16 @@ def current_timestamp(self, timestamp): """ self.seek(timestamp) - @property - def final_timestamp(self): - """Get the final timestamp of the Timeline. + def get_current_timestamp(self): + """Get last calculated current timestamp of the Timeline. Returns ------- float - The final time of the Timeline. + The last calculated current time of the Timeline. """ - return self._final_timestamp + return self._current_timestamp def seek(self, timestamp): """Set the current timestamp of the Timeline. @@ -1425,15 +139,14 @@ def seek(self, timestamp): # assuring timestamp value is in the timeline range if timestamp < 0: timestamp = 0 - elif timestamp > self.final_timestamp: - timestamp = self.final_timestamp - + elif timestamp > self.duration: + timestamp = self.duration if self.playing: self._last_started_time = \ time.perf_counter() - timestamp / self.speed else: - self._last_timestamp = timestamp - self.update_animation(force=True) + self._current_timestamp = timestamp + self.update_animation(timestamp) def seek_percent(self, percent): """Seek a percentage of the Timeline's final timestamp. @@ -1444,7 +157,7 @@ def seek_percent(self, percent): Value from 1 to 100. """ - t = percent * self._final_timestamp / 100 + t = percent * self.duration / 100 self.seek(t) @property @@ -1480,7 +193,7 @@ def stopped(self): Timeline is stopped if True. """ - return not self.playing and not self._last_timestamp + return not self.playing and not self._current_timestamp @property def paused(self): @@ -1493,7 +206,7 @@ def paused(self): """ - return not self.playing and self._last_timestamp is not None + return not self.playing and self._current_timestamp is not None @property def speed(self): @@ -1533,13 +246,50 @@ def has_playback_panel(self): """ return self.playback_panel is not None - def add_to_scene(self, ren): - """Add Timeline and all actors and sub Timelines to the scene""" - super(Timeline, self).add_to_scene(ren) - [ren.add(static_act) for static_act in self._static_actors] - [ren.add(timeline) for timeline in self.timelines] - if self._motion_path_actor: - ren.add(self._motion_path_actor) - self._scene = ren - self._added_to_scene = True - self.update_animation(force=True) + def add_child_animation(self, animation): + """Add child Animation or list of Animations. + + Parameters + ---------- + animation: Animation or list(Animation) + Animation/s to be added. + """ + super(Timeline, self).add_child_animation(animation) + if isinstance(animation, Animation): + animation._timeline = self + self.update_duration() + + def update_duration(self): + """Update and return the duration of the Timeline. + + Returns + ------- + float + The duration of the Timeline. + """ + super(Timeline, self).update_duration() + if self.has_playback_panel: + self.playback_panel.final_time = self.duration + return self.duration + + def update_animation(self, t=None): + force = True + if t is None: + t = self.current_timestamp + force = False + if self.has_playback_panel: + self.playback_panel.current_time = t + if t > self.duration: + if self._loop: + self.seek(0) + else: + self.seek(self.duration) + # Doing this will pause both the timeline and the panel. + if self.has_playback_panel: + self.playback_panel.pause() + else: + self.pause() + if self.playing or force: + super(Timeline, self).update_animation(t) + if self._scene: + self._scene.reset_clipping_range() From a97754a2fb16120fb8f77abfc4df6ca670093002 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sun, 25 Sep 2022 16:12:39 +0200 Subject: [PATCH 19/62] Modifing tests --- fury/animation/tests/test_timeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fury/animation/tests/test_timeline.py b/fury/animation/tests/test_timeline.py index 7059480e3..91978f578 100644 --- a/fury/animation/tests/test_timeline.py +++ b/fury/animation/tests/test_timeline.py @@ -40,13 +40,13 @@ def test_timeline(): for t in [-10, 0, 2.2, 7, 100]: tl.seek(t) - ft.assert_less_equal(tl.current_timestamp, tl.final_timestamp) + ft.assert_less_equal(tl.current_timestamp, tl.duration) ft.assert_greater_equal(tl.current_timestamp, 0) ft.assert_greater_equal(tl.current_timestamp, tl.playback_panel.current_time) - if 0 <= t <= tl.final_timestamp: + if 0 <= t <= tl.duration: npt.assert_almost_equal(tl.current_timestamp, t) # check if seeking a certain time affects the time slider's value. npt.assert_almost_equal(tl.current_timestamp, @@ -93,7 +93,7 @@ def test_timeline(): tl.add_actor(cube) # using force since the animation is not playing - tl.update_animation(force=True) + tl.update_animation(0) if not shaders: transform = cube.GetUserTransform() From 49d0d736d0a5af45358aac4e3d549e540d056946 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 09:03:17 +0200 Subject: [PATCH 20/62] Fixed some docs --- fury/animation/animation.py | 160 ++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 73 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 84c663114..eed450c81 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -12,10 +12,10 @@ class Animation(Container): - """Keyframe animation timeline class. + """Keyframe animation class. - This timeline is responsible for keyframe animations for a single or a - group of models. + Animation is responsible for keyframe animations for a single or a + group of actors. It's used to handle multiple attributes and properties of Fury actors such as transformations, color, and scale. It also accepts custom data and interpolates them, such as temperature. @@ -24,19 +24,16 @@ class Animation(Container): Attributes ---------- - actors : str - a formatted string to print out what the animal says - playback_panel : bool, optional - If True, the timeline will have a playback panel set, which can be used - to control the playback of the timeline. + actors : Actor or list[Actor], optional, default: None + Actor/s to be animated. length : float or int, default: None, optional - the fixed length of the timeline. If set to None, the timeline will get - its length from the keyframes. + the fixed length of the animation. If set to None, the animation will + get its duration from the keyframes being set. loop : bool, optional - the number of legs the animal has (default 4) + Whether to loop the animation (True) of play once (False). motion_path_res : int, default: None - the number of line segments used to visualizer the timeline's motion - path. + the number of line segments used to visualizer the animation's motion + path (visualizing position). """ def __init__(self, actors=None, length=None, loop=False, @@ -61,6 +58,7 @@ def __init__(self, actors=None, length=None, loop=False, self._motion_path_actor = None self._transform = Transform() + # Adding actors to the animation if actors is not None: self.add_actor(actors) @@ -72,11 +70,12 @@ def update_duration(self): float The duration of the animation. """ - curr_d = self._max_timestamp if self._length is None else self._length - self._duration = max( - curr_d, max([0] + [anim.update_duration() for anim in - self.child_animations]) - ) + if self._length is not None: + self._duration = self._length + else: + self._duration = max( + self._max_timestamp, max([0] + [anim.update_duration() for anim + in self.child_animations])) return self.duration @property @@ -95,10 +94,13 @@ def update_motion_path(self): res = self._motion_path_res tl = self while isinstance(tl._parent_animation, Animation): + if res: + break tl = tl._parent_animation res = tl._motion_path_res if not res: return + lines = [] colors = [] if self.is_interpolatable('position'): @@ -147,6 +149,25 @@ def _get_attribute_data(self, attrib, is_camera=False): } return data.get(attrib) + def get_keyframes(self, attrib=None, is_camera=False): + """Set a keyframe for a certain attribute. + + Parameters + ---------- + attrib: str, optional, default: None + The name of the attribute. + If None, all keyframes for all set attributes will be returned. + is_camera: bool, optional + Indicated whether setting a camera property or general property. + """ + + data = self._get_data(is_camera=is_camera) + if attrib is None: + attribs = data.keys() + return {attrib: data.get(attrib, {}).get('keyframes', {}) for + attrib in attribs} + return data.get(attrib, {}).get('keyframes', {}) + def set_keyframe(self, attrib, timestamp, value, is_camera=False, update_interpolator=True, **kwargs): """Set a keyframe for a certain attribute. @@ -224,7 +245,7 @@ def set_keyframes(self, attrib, keyframes, is_camera=False): Examples --------- >>> pos_keyframes = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} - >>> Timeline.set_keyframes('position', pos_keyframes) + >>> Animation.set_keyframes('position', pos_keyframes) """ for t, keyframe in keyframes.items(): if isinstance(keyframe, dict): @@ -260,7 +281,7 @@ def is_inside_scene_at(self, timestamp): return parent_in_scene def add_to_scene_at(self, timestamp): - """Set timestamp for adding Timeline to scene event. + """Set timestamp for adding Animation to scene event. Parameters ---------- @@ -274,7 +295,7 @@ def add_to_scene_at(self, timestamp): self.set_keyframe('in_scene', timestamp, True) def remove_from_scene_at(self, timestamp): - """Set timestamp for removing Timeline to scene event. + """Set timestamp for removing Animation to scene event. Parameters ---------- @@ -287,7 +308,7 @@ def remove_from_scene_at(self, timestamp): else: self.set_keyframe('in_scene', timestamp, False) - def handle_scene_event(self, timestamp): + def _handle_scene_event(self, timestamp): should_be_in_scene = self.is_inside_scene_at(timestamp) if self._scene is not None: if should_be_in_scene and not self._added_to_scene: @@ -314,7 +335,7 @@ def set_camera_keyframes(self, attrib, keyframes): Examples --------- >>> cam_pos = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} - >>> Timeline.set_camera_keyframes('position', cam_pos) + >>> Animation.set_camera_keyframes('position', cam_pos) """ self.set_keyframes(attrib, keyframes, is_camera=True) @@ -353,10 +374,10 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, Examples --------- - >>> Timeline.set_interpolator('position', linear_interpolator) + >>> Animation.set_interpolator('position', linear_interpolator) >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) - >>> Timeline.set_interpolator('position', pos_fun) + >>> Animation.set_interpolator('position', pos_fun) """ attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) @@ -390,7 +411,7 @@ def is_interpolatable(self, attrib, is_camera=False): Returns ------- bool - True if the property is interpolatable by the Timeline. + True if the property is interpolatable by the Animation. Notes ------- @@ -420,15 +441,14 @@ def set_camera_interpolator(self, attrib, interpolator, Examples --------- - >>> Timeline.set_camera_interpolator('focal', linear_interpolator) + >>> Animation.set_camera_interpolator('focal', linear_interpolator) """ self.set_interpolator(attrib, interpolator, is_camera=True, is_evaluator=is_evaluator) def set_position_interpolator(self, interpolator, is_evaluator=False, **kwargs): - """Set the position interpolator for all actors inside the - timeline. + """Set the position interpolator. Parameters ---------- @@ -447,14 +467,13 @@ def set_position_interpolator(self, interpolator, is_evaluator=False, Examples --------- - >>> Timeline.set_position_interpolator(spline_interpolator, degree=5) + >>> Animation.set_position_interpolator(spline_interpolator, degree=5) """ self.set_interpolator('position', interpolator, is_evaluator=is_evaluator, **kwargs) def set_scale_interpolator(self, interpolator, is_evaluator=False): - """Set the scale interpolator for all the actors inside the - timeline. + """Set the scale interpolator. Parameters ---------- @@ -467,13 +486,12 @@ def set_scale_interpolator(self, interpolator, is_evaluator=False): Examples --------- - >>> Timeline.set_scale_interpolator(step_interpolator) + >>> Animation.set_scale_interpolator(step_interpolator) """ self.set_interpolator('scale', interpolator, is_evaluator=is_evaluator) def set_rotation_interpolator(self, interpolator, is_evaluator=False): - """Set the scale interpolator for all the actors inside the - timeline. + """Set the rotation interpolator . Parameters ---------- @@ -485,14 +503,13 @@ def set_rotation_interpolator(self, interpolator, is_evaluator=False): function that does not depend on keyframes. Examples --------- - >>> Timeline.set_rotation_interpolator(slerp) + >>> Animation.set_rotation_interpolator(slerp) """ self.set_interpolator('rotation', interpolator, is_evaluator=is_evaluator) def set_color_interpolator(self, interpolator, is_evaluator=False): - """Set the color interpolator for all the actors inside the - timeline. + """Set the color interpolator. Parameters ---------- @@ -504,13 +521,12 @@ def set_color_interpolator(self, interpolator, is_evaluator=False): function that does not depend on keyframes. Examples --------- - >>> Timeline.set_color_interpolator(lab_color_interpolator) + >>> Animation.set_color_interpolator(lab_color_interpolator) """ self.set_interpolator('color', interpolator, is_evaluator=is_evaluator) def set_opacity_interpolator(self, interpolator, is_evaluator=False): - """Set the opacity interpolator for all the actors inside the - timeline. + """Set the opacity interpolator. Parameters ---------- @@ -522,7 +538,7 @@ def set_opacity_interpolator(self, interpolator, is_evaluator=False): function that does not depend on keyframes. Examples --------- - >>> Timeline.set_opacity_interpolator(step_interpolator) + >>> Animation.set_opacity_interpolator(step_interpolator) """ self.set_interpolator('opacity', interpolator, is_evaluator=is_evaluator) @@ -641,7 +657,7 @@ def set_position_keyframes(self, keyframes): Examples -------- >>> pos_keyframes = {1, np.array([0, 0, 0]), 3, np.array([50, 6, 6])} - >>> Timeline.set_position_keyframes(pos_keyframes) + >>> Animation.set_position_keyframes(pos_keyframes) """ self.set_keyframes('position', keyframes) @@ -714,7 +730,7 @@ def set_scale_keyframes(self, keyframes): Examples -------- >>> scale_keyframes = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} - >>> Timeline.set_scale_keyframes(scale_keyframes) + >>> Animation.set_scale_keyframes(scale_keyframes) """ self.set_keyframes('scale', keyframes) @@ -743,7 +759,7 @@ def set_color_keyframes(self, keyframes): Examples -------- >>> color_keyframes = {1, np.array([1, 0, 1]), 3, np.array([0, 0, 1])} - >>> Timeline.set_color_keyframes(color_keyframes) + >>> Animation.set_color_keyframes(color_keyframes) """ self.set_keyframes('color', keyframes) @@ -776,7 +792,7 @@ def set_opacity_keyframes(self, keyframes): Examples -------- >>> opacity = {1, np.array([1, 1, 1]), 3, np.array([2, 2, 3])} - >>> Timeline.set_scale_keyframes(opacity) + >>> Animation.set_scale_keyframes(opacity) """ self.set_keyframes('opacity', keyframes) @@ -935,7 +951,7 @@ def set_camera_position_keyframes(self, keyframes): Examples -------- >>> pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} - >>> Timeline.set_camera_position_keyframes(pos) + >>> Animation.set_camera_position_keyframes(pos) """ self.set_camera_keyframes('position', keyframes) @@ -953,7 +969,7 @@ def set_camera_focal_keyframes(self, keyframes): Examples -------- >>> focal_pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} - >>> Timeline.set_camera_focal_keyframes(focal_pos) + >>> Animation.set_camera_focal_keyframes(focal_pos) """ self.set_camera_keyframes('focal', keyframes) @@ -971,7 +987,7 @@ def set_camera_view_up_keyframes(self, keyframes): Examples -------- >>> view_ups = {0, np.array([1, 0, 0]), 3, np.array([0, 1, 0])} - >>> Timeline.set_camera_view_up_keyframes(view_ups) + >>> Animation.set_camera_view_up_keyframes(view_ups) """ self.set_camera_keyframes('view_up', keyframes) @@ -1057,14 +1073,14 @@ def get_camera_rotation(self, t): return self.get_camera_value('rotation', t) def add(self, item): - """Add an item to the Timeline. - This item can be an actor, Timeline, list of actors, or a list of - Timelines. + """Add an item to the Animation. + This item can be an Actor, Animation, list of Actors, or a list of + Animations. Parameters ---------- - item: Timeline, vtkActor, list(Timeline), or list(vtkActor) - Actor/s to be animated by the timeline. + item: Animation, vtkActor, list[Animation], or list[vtkActor] + Actor/s to be animated by the Animation. """ if isinstance(item, list): for a in item: @@ -1075,15 +1091,14 @@ def add(self, item): elif isinstance(item, Animation): self.add_child_animation(item) else: - raise ValueError(f"Object of type {type(item)} can't be added to " - f"the timeline.") + raise ValueError(f"Object of type {type(item)} can't be animated") def add_child_animation(self, animation): """Add child Animation or list of Animations. Parameters ---------- - animation: Animation or list(Animation) + animation: Animation or list[Animation] Animation/s to be added. """ if isinstance(animation, list): @@ -1095,16 +1110,16 @@ def add_child_animation(self, animation): self._animations.append(animation) def add_actor(self, actor, static=False): - """Add an actor or list of actors to the Timeline. + """Add an actor or list of actors to the Animation. Parameters ---------- actor: vtkActor or list(vtkActor) - Actor/s to be animated by the timeline. + Actor/s to be animated by the Animation. static: bool Indicated whether the actor should be animated and controlled by - the timeline or just a static actor that gets added to the scene - along with the Timeline. + the animation or just a static actor that gets added to the scene + along with the Animation. """ if isinstance(actor, list): for a in actor: @@ -1134,7 +1149,7 @@ def actors(self): Returns ------- list: - List of actors controlled by the Timeline. + List of actors controlled by the Animation. """ return self.items @@ -1151,8 +1166,8 @@ def child_animations(self) -> 'list[Animation]': def add_static_actor(self, actor): """Add an actor or list of actors as static actor/s which will not be - controlled nor animated by the Timeline. All static actors will be - added to the scene when the Timeline is added to the scene. + controlled nor animated by the Animation. All static actors will be + added to the scene when the Animation is added to the scene. Parameters ---------- @@ -1207,7 +1222,10 @@ def add_update_callback(self, property_name, cbk_func): attrib.get('callbacks', []).append(cbk_func) def update_animation(self, t=0.0): - """Update the timeline animations + """Update the animation. + + Update the animation at a certain time. This will make sure all + attributes are calculated and set to the actors at that given time. Parameters ---------- @@ -1217,7 +1235,7 @@ def update_animation(self, t=0.0): # handling in/out of scene events in_scene = self.is_inside_scene_at(t) - self.handle_scene_event(t) + self._handle_scene_event(t) if self._loop and t: t = t % self.duration @@ -1298,21 +1316,17 @@ def update_animation(self, t=0.0): value = self.get_value(attrib, t) [cbk(value) for cbk in callbacks] - # Also update all child Timelines. - [anim.update_animation(t) for anim in self._animations] + # Also update all child Animations. + [animation.update_animation(t) for animation in self._animations] def add_to_scene(self, ren): - """Add Timeline and all actors and sub Timelines to the scene""" + """Add this Animation, its actors and sub Animations to the scene""" super(Animation, self).add_to_scene(ren) [ren.add(static_act) for static_act in self._static_actors] [ren.add(animation) for animation in self._animations] - b = np.zeros(6) - - if self._motion_path_actor: ren.add(self._motion_path_actor) self._scene = ren self._added_to_scene = True self.update_animation(0) - From 03c14d2293d3e5f35a5502c85b27791e5018bf4a Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 12:05:23 +0200 Subject: [PATCH 21/62] Added more docs --- fury/animation/timeline.py | 73 ++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 80bb9d0f4..b40c58530 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -4,20 +4,17 @@ class Timeline(Animation): - """Keyframe animation timeline class. + """Keyframe animation Timeline. - This timeline is responsible for keyframe animations for a single or a - group of models. - It's used to handle multiple attributes and properties of Fury actors such - as transformations, color, and scale. - It also accepts custom data and interpolates them, such as temperature. - Linear interpolation is used by default to interpolate data between the - main keyframes. + Timeline is responsible for handling the playback of keyframes animations. + It can also act like an Animation with playback options which makes it easy + to control the playback, speed, state of the animation with/without a GUI + playback panel. Attributes ---------- - actors : str - a formatted string to print out what the animal says + actors : Actor or list[Actor], optional, default: None + Actor/s to be animated directly by the Timeline (main Animation). playback_panel : bool, optional If True, the timeline will have a playback panel set, which can be used to control the playback of the timeline. @@ -26,9 +23,12 @@ class Timeline(Animation): its length from the keyframes. loop : bool, optional Whether loop playing the animation or not + motion_path_res : int, default: None + the number of line segments used to visualizer the animation's motion + path (visualizing position). """ - def __init__(self, actors=None, playback_panel=False, length=None, + def __init__(self, actors=None, playback_panel=True, length=None, loop=False, motion_path_res=None): super().__init__(actors=actors, length=length, loop=loop, @@ -39,14 +39,13 @@ def __init__(self, actors=None, playback_panel=False, length=None, self._last_started_time = 0 self._playing = False self._length = length - self._reverse_playing = True self._animations = [] self._timeline = None self._parent_animation = None # Handle actors while constructing the timeline. if playback_panel: - def set_loop(loop): - self._loop = loop + def set_loop(is_loop): + self._loop = is_loop def set_speed(speed): self.speed = speed @@ -116,17 +115,6 @@ def current_timestamp(self, timestamp): """ self.seek(timestamp) - def get_current_timestamp(self): - """Get last calculated current timestamp of the Timeline. - - Returns - ------- - float - The last calculated current time of the Timeline. - - """ - return self._current_timestamp - def seek(self, timestamp): """Set the current timestamp of the Timeline. @@ -167,22 +155,10 @@ def playing(self): Returns ------- bool - Timeline is playing if True. + True if the Timeline is playing. """ return self._playing - @playing.setter - def playing(self, playing): - """Set the playing state of the Timeline. - - Parameters - ---------- - playing: bool - The playing state to be set. - - """ - self._playing = playing - @property def stopped(self): """Return whether the Timeline is stopped. @@ -190,7 +166,7 @@ def stopped(self): Returns ------- bool - Timeline is stopped if True. + True if Timeline is stopped. """ return not self.playing and not self._current_timestamp @@ -202,7 +178,7 @@ def paused(self): Returns ------- bool - Timeline is paused if True. + True if the Timeline is paused. """ @@ -257,7 +233,6 @@ def add_child_animation(self, animation): super(Timeline, self).add_child_animation(animation) if isinstance(animation, Animation): animation._timeline = self - self.update_duration() def update_duration(self): """Update and return the duration of the Timeline. @@ -273,6 +248,19 @@ def update_duration(self): return self.duration def update_animation(self, t=None): + """Update the animation. + + Update the animation and the playback of the Timeline. As well as + updating all animations handled by the Timeline. + + Parameters + ---------- + t: float or int, optional, default: None + Time to update animation at. + IF None, The time is determined by the Timeline itseld and can be + controlled using the playback panel graphically or by the Timeline + methods such as ``Timeline.seek(t)``. + """ force = True if t is None: t = self.current_timestamp @@ -291,5 +279,4 @@ def update_animation(self, t=None): self.pause() if self.playing or force: super(Timeline, self).update_animation(t) - if self._scene: - self._scene.reset_clipping_range() + From e158720ceced7f0ee395f160b212dcaff514abcb Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 12:06:28 +0200 Subject: [PATCH 22/62] Duration should be initialized --- fury/animation/animation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index eed450c81..754ea969c 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -48,9 +48,10 @@ def __init__(self, actors=None, length=None, loop=False, self._parent_animation = None self._camera = None self._scene = None - self._duration = 0 - self._loop = loop + self._added_to_scene_time = 0 self._length = length + self._duration = length if length else 0 + self._loop = loop self._max_timestamp = 0 self._added_to_scene = True self._is_camera_animated = False @@ -1108,6 +1109,7 @@ def add_child_animation(self, animation): animation._parent_animation = self animation.update_motion_path() self._animations.append(animation) + self.update_duration() def add_actor(self, actor, static=False): """Add an actor or list of actors to the Animation. From 2105548f8b315d35da5a35f272ce2f972149d2de Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 12:07:36 +0200 Subject: [PATCH 23/62] Animation playable on its own --- fury/animation/animation.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 754ea969c..27a21cef1 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1231,16 +1231,21 @@ def update_animation(self, t=0.0): Parameters ---------- - t: float or int, optional, default: 0.0 + t: float or int, optional, default: None Time to update animation at. """ + t = t if t is not None else \ + time.perf_counter() - self._added_to_scene_time # handling in/out of scene events in_scene = self.is_inside_scene_at(t) self._handle_scene_event(t) - if self._loop and t: - t = t % self.duration + if self.duration: + if self._loop and t > 0: + t = t % self.duration + elif t > self.duration: + t = self.duration if isinstance(self._parent_animation, Animation): self._transform.DeepCopy(self._parent_animation._transform) else: @@ -1321,6 +1326,9 @@ def update_animation(self, t=0.0): # Also update all child Animations. [animation.update_animation(t) for animation in self._animations] + if self._scene and self._parent_animation is None: + self._scene.reset_clipping_range() + def add_to_scene(self, ren): """Add this Animation, its actors and sub Animations to the scene""" super(Animation, self).add_to_scene(ren) @@ -1331,4 +1339,5 @@ def add_to_scene(self, ren): ren.add(self._motion_path_actor) self._scene = ren self._added_to_scene = True + self._added_to_scene_time = time.perf_counter() self.update_animation(0) From fb9df0d5fe6b80d2e826d52ae78b0bcefe54b61f Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 12:07:56 +0200 Subject: [PATCH 24/62] parent animation impl and doc --- fury/animation/animation.py | 47 ++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 27a21cef1..250c0a937 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1133,17 +1133,58 @@ def add_actor(self, actor, static=False): super(Animation, self).add(actor) @property - def timeline(self) -> 'Timeline': + def timeline(self): + """Return the Timeline handling the current animation. + + Returns + ------- + Timeline: + The Timeline handling the current animation, None, if there is no + associated Timeline. + + """ return self._timeline @timeline.setter def timeline(self, timeline): - """Assign the Timeline responsible for handling the animation.""" + """Assign the Timeline responsible for handling the Animation. + + Parameters + ---------- + + Timeline: + The Timeline handling the current animation, None, if there is no + associated Timeline. + + """ self._timeline = timeline if self._animations: for animation in self._animations: animation.timeline = timeline + @property + def parent_animation(self): + """Return the hierarchical parent Animation for current Animation. + + Returns + ------- + Animation: + The parent Animation. + + """ + return self._parent_animation + + @parent_animation.setter + def parent_animation(self, parent_animation): + """Assign a parent Animation for the current Animation. + + Parameters + ---------- + parent_animation: Animation + The parent Animation instance. + """ + self._parent_animation = parent_animation + @property def actors(self): """Return a list of actors. @@ -1223,7 +1264,7 @@ def add_update_callback(self, property_name, cbk_func): attrib = self._get_attribute_data(property_name) attrib.get('callbacks', []).append(cbk_func) - def update_animation(self, t=0.0): + def update_animation(self, t=None): """Update the animation. Update the animation at a certain time. This will make sure all From af350a155fe01ea83c6a85f24744ad6f04a6de15 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 12:37:33 +0200 Subject: [PATCH 25/62] updated robot arm tutorial --- .../05_animation/viz_robot_arm_animation.py | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/tutorials/05_animation/viz_robot_arm_animation.py b/docs/tutorials/05_animation/viz_robot_arm_animation.py index f60a091f5..187d1b0ab 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -8,6 +8,7 @@ """ import numpy as np from fury import actor, window +from fury.animation import Animation from fury.animation.timeline import Timeline from fury.utils import set_actor_origin @@ -43,15 +44,23 @@ set_actor_origin(sub_arm, np.array([-4, 0, 0])) ############################################################################### -# Creating a timeline to animate the actor -tl_main = Timeline([main_arm, joint_1], playback_panel=True, length=2 * np.pi) -tl_child = Timeline([sub_arm, joint_2]) -tl_grand_child = Timeline(end) +# Creating a timeline +timeline = Timeline() ############################################################################### -# Adding Timelines in hierarchical order -tl_main.add_child_timeline(tl_child) -tl_child.add_child_timeline(tl_grand_child) +# Creating animations +main_arm_animation = Animation([main_arm, joint_1], length=2 * np.pi) +child_arm_animation = Animation([sub_arm, joint_2]) +drill_animation = Animation(end) + +############################################################################### +# Adding the main animation to the Timeline. +timeline.add_child_animation(main_arm_animation) + +############################################################################### +# Adding other Animations in hierarchical order +main_arm_animation.add_child_animation(child_arm_animation) +child_arm_animation.add_child_animation(drill_animation) ############################################################################### @@ -72,22 +81,22 @@ def rot_drill(t): ############################################################################### # Setting timelines (joints) relative position # 1- Placing the main arm on the cube static base. -tl_main.set_position(0, np.array([0, 1.3, 0])) +main_arm_animation.set_position(0, np.array([0, 1.3, 0])) ############################################################################### # 2- Translating the timeline containing the sub arm to the end of the first # arm. -tl_child.set_position(0, np.array([12, 0, 0])) +child_arm_animation.set_position(0, np.array([12, 0, 0])) ############################################################################### # 3- Translating the timeline containing the drill to the end of the sub arm. -tl_grand_child.set_position(0, np.array([8, 0, 0])) +drill_animation.set_position(0, np.array([8, 0, 0])) ############################################################################### # Setting rotation time-based evaluators -tl_main.set_rotation_interpolator(rot_main_arm, is_evaluator=True) -tl_child.set_rotation_interpolator(rot_sub_arm, is_evaluator=True) -tl_grand_child.set_rotation_interpolator(rot_drill, is_evaluator=True) +main_arm_animation.set_rotation_interpolator(rot_main_arm, is_evaluator=True) +child_arm_animation.set_rotation_interpolator(rot_sub_arm, is_evaluator=True) +drill_animation.set_rotation_interpolator(rot_drill, is_evaluator=True) ############################################################################### # Setting camera position to observe the robot arm. @@ -95,13 +104,13 @@ def rot_drill(t): ############################################################################### # Adding timelines to the main Timeline. -scene.add(tl_main, base) +scene.add(timeline, base) ############################################################################### # making a function to update the animation and render the scene. def timer_callback(_obj, _event): - tl_main.update_animation() + timeline.update_animation() showm.render() From c4a0c6d4d26f0585d7f1ba197f9bee1bad039f21 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 20:59:13 +0200 Subject: [PATCH 26/62] pep fixing --- fury/animation/timeline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index b40c58530..e1ab6a5b9 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -257,7 +257,7 @@ def update_animation(self, t=None): ---------- t: float or int, optional, default: None Time to update animation at. - IF None, The time is determined by the Timeline itseld and can be + IF None, The time is determined by the Timeline itself and can be controlled using the playback panel graphically or by the Timeline methods such as ``Timeline.seek(t)``. """ @@ -279,4 +279,3 @@ def update_animation(self, t=None): self.pause() if self.playing or force: super(Timeline, self).update_animation(t) - From 83e716cc49e58d656756606fc7444551d2503761 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 21:01:53 +0200 Subject: [PATCH 27/62] Tutorials adapt to new method --- docs/tutorials/05_animation/viz_keyframe_camera_animation.py | 4 ++-- .../05_animation/viz_keyframe_color_interpolators.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/05_animation/viz_keyframe_camera_animation.py b/docs/tutorials/05_animation/viz_keyframe_camera_animation.py index 3df8e30e3..54dafb04f 100644 --- a/docs/tutorials/05_animation/viz_keyframe_camera_animation.py +++ b/docs/tutorials/05_animation/viz_keyframe_camera_animation.py @@ -77,7 +77,7 @@ ############################################################################### # ``text_timeline`` contains the text actor is added to the main Timeline. -main_timeline.add_child_timeline(text_timeline) +main_timeline.add_child_animation(text_timeline) ############################################################################### # Creating and animating 50 Spheres @@ -113,7 +113,7 @@ ########################################################################### # Finally, the ``Timeline`` is added to the ``main_timeline``. - main_timeline.add_child_timeline(timeline) + main_timeline.add_child_animation(timeline) ############################################################################### # Animating the camera diff --git a/docs/tutorials/05_animation/viz_keyframe_color_interpolators.py b/docs/tutorials/05_animation/viz_keyframe_color_interpolators.py index 8e07a8ab4..91a7b9d2d 100644 --- a/docs/tutorials/05_animation/viz_keyframe_color_interpolators.py +++ b/docs/tutorials/05_animation/viz_keyframe_color_interpolators.py @@ -57,7 +57,7 @@ ############################################################################### # Adding timelines to the main Timeline. -main_timeline.add_child_timeline([timeline_linear_color, +main_timeline.add_child_animation([timeline_linear_color, timeline_LAB_color, timeline_HSV_color, timeline_XYZ_color, From f25550173aff2d911cd883a918b3f76fecf7bf24 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 21:09:59 +0200 Subject: [PATCH 28/62] Update duration in case of hard setting the length --- fury/animation/animation.py | 1 + fury/animation/timeline.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 250c0a937..1d734eb7c 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -397,6 +397,7 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, interp_data['func'] = new_interp # update motion path + self.update_duration() self.update_motion_path() def is_interpolatable(self, attrib, is_camera=False): diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index e1ab6a5b9..6f8eb994b 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -38,7 +38,6 @@ def __init__(self, actors=None, playback_panel=True, length=None, self._speed = 1 self._last_started_time = 0 self._playing = False - self._length = length self._animations = [] self._timeline = None self._parent_animation = None From 7d5615b469b4ee3fbdb15775002fd19b4740ddca Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 26 Sep 2022 21:20:22 +0200 Subject: [PATCH 29/62] Set back playback_panel to False untill tutorials are edited --- docs/tutorials/05_animation/viz_robot_arm_animation.py | 2 +- fury/animation/timeline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/05_animation/viz_robot_arm_animation.py b/docs/tutorials/05_animation/viz_robot_arm_animation.py index 187d1b0ab..a3563406b 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -45,7 +45,7 @@ ############################################################################### # Creating a timeline -timeline = Timeline() +timeline = Timeline(playback_panel=True) ############################################################################### # Creating animations diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 6f8eb994b..9cd238a12 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -28,7 +28,7 @@ class Timeline(Animation): path (visualizing position). """ - def __init__(self, actors=None, playback_panel=True, length=None, + def __init__(self, actors=None, playback_panel=False, length=None, loop=False, motion_path_res=None): super().__init__(actors=actors, length=length, loop=loop, From 0d602f4f78c6afa9bc2d1c75b0f720f7d362f436 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 27 Sep 2022 14:39:44 +0200 Subject: [PATCH 30/62] @skoudoro review - pep and doc issues --- fury/animation/animation.py | 70 ++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 1d734eb7c..ad54b9e32 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1,14 +1,13 @@ import time import warnings +import numpy as np from collections import defaultdict +from scipy.spatial import transform from fury import utils, actor from fury.actor import Container +from fury.lib import Actor, Transform from fury.animation.interpolator import spline_interpolator, \ step_interpolator, linear_interpolator, slerp -import numpy as np -from scipy.spatial import transform -from fury.ui.elements import PlaybackPanel -from fury.lib import Actor, Transform class Animation(Container): @@ -233,18 +232,15 @@ def set_keyframes(self, attrib, keyframes, is_camera=False): The name of the attribute. keyframes: dict A dict object containing keyframes to be set. - is_camera: bool + is_camera: bool, optional Indicated whether setting a camera property or general property. Notes - --------- + ----- Keyframes can be on any of the following forms: >>> key_frames_simple = {1: [1, 2, 1], 2: [3, 4, 5]} >>> key_frames_bezier = {1: {'value': [1, 2, 1]}, >>> 2: {'value': [3, 4, 5], 'in_cp': [1, 2, 3]}} - - Examples - --------- >>> pos_keyframes = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} >>> Animation.set_keyframes('position', pos_keyframes) """ @@ -271,6 +267,20 @@ def set_camera_keyframe(self, attrib, timestamp, value, **kwargs): self.set_keyframe(attrib, timestamp, value, is_camera=True, **kwargs) def is_inside_scene_at(self, timestamp): + """Check if the Animation is set to be inside the scene at a specific + timestamp. + + Returns + ------- + bool + True if the Animation is set to be inside the scene at the given + timestamp. + Notes + ----- + If the parent Animation is set to be out of the scene at that time, all + of their child animations will be out of the scene as well. + + """ parent = self._parent_animation parent_in_scene = True if parent is not None: @@ -330,11 +340,11 @@ def set_camera_keyframes(self, attrib, keyframes): A dict object containing keyframes to be set. Notes - --------- + ----- Cubic Bézier curve control points are not supported yet in this setter. Examples - --------- + -------- >>> cam_pos = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} >>> Animation.set_camera_keyframes('position', cam_pos) """ @@ -348,7 +358,7 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, ---------- attrib: str The name of the property. - interpolator: function + interpolator: callable The generator function of the interpolator to be used to interpolate/evaluate keyframes. is_camera: bool, optional @@ -401,13 +411,13 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, self.update_motion_path() def is_interpolatable(self, attrib, is_camera=False): - """Checks whether a property is interpolatable. + """Check whether a property is interpolatable. Parameters ---------- attrib: str The name of the property. - is_camera: bool + is_camera: bool, optional Indicated whether checking a camera property or general property. Returns @@ -416,7 +426,7 @@ def is_interpolatable(self, attrib, is_camera=False): True if the property is interpolatable by the Animation. Notes - ------- + ----- True means that it's safe to use `Interpolator.interpolate(t)` for the specified property. And False means the opposite. @@ -434,7 +444,7 @@ def set_camera_interpolator(self, attrib, interpolator, The name of the camera property. The already handled properties are position, focal, and view_up. - interpolator: function + interpolator: callable The generator function of the interpolator that handles the camera property interpolation between keyframes. is_evaluator: bool, optional @@ -454,7 +464,7 @@ def set_position_interpolator(self, interpolator, is_evaluator=False, Parameters ---------- - interpolator: function + interpolator: callable The generator function of the interpolator that would handle the position keyframes. is_evaluator: bool, optional @@ -468,7 +478,7 @@ def set_position_interpolator(self, interpolator, is_evaluator=False, the `spline_interpolator`. Examples - --------- + -------- >>> Animation.set_position_interpolator(spline_interpolator, degree=5) """ self.set_interpolator('position', interpolator, @@ -479,7 +489,7 @@ def set_scale_interpolator(self, interpolator, is_evaluator=False): Parameters ---------- - interpolator: function + interpolator: callable TThe generator function of the interpolator that would handle the scale keyframes. is_evaluator: bool, optional @@ -487,7 +497,7 @@ def set_scale_interpolator(self, interpolator, is_evaluator=False): function that does not depend on keyframes. Examples - --------- + -------- >>> Animation.set_scale_interpolator(step_interpolator) """ self.set_interpolator('scale', interpolator, is_evaluator=is_evaluator) @@ -497,14 +507,15 @@ def set_rotation_interpolator(self, interpolator, is_evaluator=False): Parameters ---------- - interpolator: function + interpolator: callable The generator function of the interpolator that would handle the rotation (orientation) keyframes. is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. + Examples - --------- + -------- >>> Animation.set_rotation_interpolator(slerp) """ self.set_interpolator('rotation', interpolator, @@ -515,14 +526,15 @@ def set_color_interpolator(self, interpolator, is_evaluator=False): Parameters ---------- - interpolator: function + interpolator: callable The generator function of the interpolator that would handle the color keyframes. is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. + Examples - --------- + -------- >>> Animation.set_color_interpolator(lab_color_interpolator) """ self.set_interpolator('color', interpolator, is_evaluator=is_evaluator) @@ -532,14 +544,14 @@ def set_opacity_interpolator(self, interpolator, is_evaluator=False): Parameters ---------- - interpolator: function + interpolator: callable The generator function of the interpolator that would handle the opacity keyframes. is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. Examples - --------- + -------- >>> Animation.set_opacity_interpolator(step_interpolator) """ self.set_interpolator('opacity', interpolator, @@ -551,7 +563,7 @@ def set_camera_position_interpolator(self, interpolator, Parameters ---------- - interpolator: function + interpolator: callable The generator function of the interpolator that would handle the interpolation of the camera position keyframes. is_evaluator: bool, optional @@ -566,7 +578,7 @@ def set_camera_focal_interpolator(self, interpolator, is_evaluator=False): Parameters ---------- - interpolator: function + interpolator: callable The generator function of the interpolator that would handle the interpolation of the camera focal position keyframes. is_evaluator: bool, optional @@ -1153,7 +1165,7 @@ def timeline(self, timeline): Parameters ---------- - Timeline: + timeline: Timeline The Timeline handling the current animation, None, if there is no associated Timeline. From 373268e52afdcc9d1c45e7e5b4e2925c8df2507b Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 27 Sep 2022 14:46:31 +0200 Subject: [PATCH 31/62] Impl `get_camera_data` method --- fury/animation/animation.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index ad54b9e32..5ab809112 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -127,13 +127,48 @@ def update_motion_path(self): self._motion_path_actor = mpa def _get_data(self, is_camera=False): + """Get animation data. + + Parameters + ---------- + is_camera: bool, optional, default : False + Specifies whether return general data or camera-related data. + Returns + ------- + dict: + The animation data containing keyframes and interpolators. + """ if is_camera: self._is_camera_animated = True return self._camera_data else: return self._data + def _get_camera_data(self): + """Get camera animation data. + + Returns + ------- + dict: + The camera animation data containing keyframes and interpolators. + """ + return self._get_data(is_camera=True) + def _get_attribute_data(self, attrib, is_camera=False): + """Get animation data for a specific attribute. + + Parameters + ---------- + attrib: str + The attribute name to get data for. + is_camera: bool, optional, default : False + Specifies whether return general data or camera-related data. + + Returns + ------- + dict: + The animation data for a specific attribute. + """ data = self._get_data(is_camera=is_camera) if attrib not in data: From 965daa1027227ea790fc892466022e381e617e67 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 27 Sep 2022 14:47:46 +0200 Subject: [PATCH 32/62] get_keyframes doc typo --- fury/animation/animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 5ab809112..529add0c3 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -185,7 +185,7 @@ def _get_attribute_data(self, attrib, is_camera=False): return data.get(attrib) def get_keyframes(self, attrib=None, is_camera=False): - """Set a keyframe for a certain attribute. + """Get a keyframe for a specific or all attributes. Parameters ---------- From bf1ab654f9b3b253cd16c43e58b67181e777e01f Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 27 Sep 2022 14:49:17 +0200 Subject: [PATCH 33/62] Too long dash fixed --- fury/animation/animation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 529add0c3..d1a0b007c 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -419,7 +419,7 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, return a 1x1, an int, or a float value. Examples - --------- + -------- >>> Animation.set_interpolator('position', linear_interpolator) >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) @@ -487,7 +487,7 @@ def set_camera_interpolator(self, attrib, interpolator, function that does not depend on keyframes. Examples - --------- + -------- >>> Animation.set_camera_interpolator('focal', linear_interpolator) """ self.set_interpolator(attrib, interpolator, is_camera=True, From fc67551daf26b221b558e0d53f6f44b016bb2222 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 27 Sep 2022 14:59:52 +0200 Subject: [PATCH 34/62] Fixed one letter param --- fury/animation/animation.py | 60 ++++++++++++++++++------------------- fury/animation/timeline.py | 24 +++++++-------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index d1a0b007c..f97c5884c 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1,6 +1,6 @@ -import time -import warnings import numpy as np +from warnings import warn +from time import perf_counter from collections import defaultdict from scipy.spatial import transform from fury import utils, actor @@ -195,11 +195,11 @@ def get_keyframes(self, attrib=None, is_camera=False): is_camera: bool, optional Indicated whether setting a camera property or general property. """ - + data = self._get_data(is_camera=is_camera) if attrib is None: attribs = data.keys() - return {attrib: data.get(attrib, {}).get('keyframes', {}) for + return {attrib: data.get(attrib, {}).get('keyframes', {}) for attrib in attribs} return data.get(attrib, {}).get('keyframes', {}) @@ -738,8 +738,8 @@ def set_rotation(self, timestamp, rotation, **kwargs): degrees=True).as_quat() self.set_keyframe('rotation', timestamp, rotation, **kwargs) else: - warnings.warn(f'Keyframe with {no_components} components is not a ' - f'valid rotation data. Skipped!') + warn(f'Keyframe with {no_components} components is not a ' + f'valid rotation data. Skipped!') def set_rotation_as_vector(self, timestamp, vector, **kwargs): """Set a rotation keyframe at a specific timestamp. @@ -1312,7 +1312,7 @@ def add_update_callback(self, property_name, cbk_func): attrib = self._get_attribute_data(property_name) attrib.get('callbacks', []).append(cbk_func) - def update_animation(self, t=None): + def update_animation(self, time=None): """Update the animation. Update the animation at a certain time. This will make sure all @@ -1320,21 +1320,21 @@ def update_animation(self, t=None): Parameters ---------- - t: float or int, optional, default: None - Time to update animation at. + time: float or int, optional, default: None + The time to update animation at. """ - t = t if t is not None else \ - time.perf_counter() - self._added_to_scene_time + time = time if time is not None else \ + perf_counter() - self._added_to_scene_time # handling in/out of scene events - in_scene = self.is_inside_scene_at(t) - self._handle_scene_event(t) + in_scene = self.is_inside_scene_at(time) + self._handle_scene_event(time) if self.duration: - if self._loop and t > 0: - t = t % self.duration - elif t > self.duration: - t = self.duration + if self._loop and time > 0: + time = time % self.duration + elif time > self.duration: + time = self.duration if isinstance(self._parent_animation, Animation): self._transform.DeepCopy(self._parent_animation._transform) else: @@ -1346,7 +1346,7 @@ def update_animation(self, t=None): translation = np.identity(4) translation[:3, 3] = pos # camera axis is reverted - rot = -self.get_camera_rotation(t) + rot = -self.get_camera_rotation(time) rot = transform.Rotation.from_quat(rot).as_matrix() rot = np.array([[*rot[0], 0], [*rot[1], 0], @@ -1356,15 +1356,15 @@ def update_animation(self, t=None): self._camera.SetModelTransformMatrix(rot.flatten()) if self.is_interpolatable('position', is_camera=True): - cam_pos = self.get_camera_position(t) + cam_pos = self.get_camera_position(time) self._camera.SetPosition(cam_pos) if self.is_interpolatable('focal', is_camera=True): - cam_foc = self.get_camera_focal(t) + cam_foc = self.get_camera_focal(time) self._camera.SetFocalPoint(cam_foc) if self.is_interpolatable('view_up', is_camera=True): - cam_up = self.get_camera_view_up(t) + cam_up = self.get_camera_view_up(time) self._camera.SetViewUp(cam_up) elif not self.is_interpolatable('view_up', is_camera=True): # to preserve up-view as default after user interaction @@ -1372,33 +1372,33 @@ def update_animation(self, t=None): elif self._is_camera_animated and self._scene: self._camera = self._scene.camera() - self.update_animation(t) + self.update_animation(time) return # actors properties if in_scene: if self.is_interpolatable('position'): - position = self.get_position(t) + position = self.get_position(time) self._transform.Translate(*position) if self.is_interpolatable('opacity'): - opacity = self.get_opacity(t) + opacity = self.get_opacity(time) [act.GetProperty().SetOpacity(opacity) for act in self.actors] if self.is_interpolatable('rotation'): - x, y, z = self.get_rotation(t) + x, y, z = self.get_rotation(time) # Rotate in the same order as VTK defaults. self._transform.RotateZ(z) self._transform.RotateX(x) self._transform.RotateY(y) if self.is_interpolatable('scale'): - scale = self.get_scale(t) + scale = self.get_scale(time) self._transform.Scale(*scale) if self.is_interpolatable('color'): - color = self.get_color(t) + color = self.get_color(time) for act in self.actors: act.vcolors[:] = color * 255 utils.update_actor(act) @@ -1409,11 +1409,11 @@ def update_animation(self, t=None): for attrib in self._data: callbacks = self._data.get(attrib, {}).get('callbacks', []) if callbacks is not [] and self.is_interpolatable(attrib): - value = self.get_value(attrib, t) + value = self.get_value(attrib, time) [cbk(value) for cbk in callbacks] # Also update all child Animations. - [animation.update_animation(t) for animation in self._animations] + [animation.update_animation(time) for animation in self._animations] if self._scene and self._parent_animation is None: self._scene.reset_clipping_range() @@ -1428,5 +1428,5 @@ def add_to_scene(self, ren): ren.add(self._motion_path_actor) self._scene = ren self._added_to_scene = True - self._added_to_scene_time = time.perf_counter() + self._added_to_scene_time = perf_counter() self.update_animation(0) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 9cd238a12..1e438a0f3 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1,4 +1,4 @@ -import time +from time import perf_counter from fury.ui.elements import PlaybackPanel from fury.animation.animation import Animation @@ -67,7 +67,7 @@ def play(self): if self.current_timestamp >= self.duration: self.current_timestamp = 0 self._last_started_time = \ - time.perf_counter() - self._current_timestamp / self.speed + perf_counter() - self._current_timestamp / self.speed self._playing = True def pause(self): @@ -98,7 +98,7 @@ def current_timestamp(self): """ if self.playing: - self._current_timestamp = (time.perf_counter() - + self._current_timestamp = (perf_counter() - self._last_started_time) * self.speed return self._current_timestamp @@ -130,7 +130,7 @@ def seek(self, timestamp): timestamp = self.duration if self.playing: self._last_started_time = \ - time.perf_counter() - timestamp / self.speed + perf_counter() - timestamp / self.speed else: self._current_timestamp = timestamp self.update_animation(timestamp) @@ -208,7 +208,7 @@ def speed(self, speed): if speed <= 0: return self._speed = speed - self._last_started_time = time.perf_counter() + self._last_started_time = perf_counter() self.current_timestamp = current @property @@ -246,7 +246,7 @@ def update_duration(self): self.playback_panel.final_time = self.duration return self.duration - def update_animation(self, t=None): + def update_animation(self, time=None): """Update the animation. Update the animation and the playback of the Timeline. As well as @@ -254,19 +254,19 @@ def update_animation(self, t=None): Parameters ---------- - t: float or int, optional, default: None + time: float or int, optional, default: None Time to update animation at. IF None, The time is determined by the Timeline itself and can be controlled using the playback panel graphically or by the Timeline methods such as ``Timeline.seek(t)``. """ force = True - if t is None: - t = self.current_timestamp + if time is None: + time = self.current_timestamp force = False if self.has_playback_panel: - self.playback_panel.current_time = t - if t > self.duration: + self.playback_panel.current_time = time + if time > self.duration: if self._loop: self.seek(0) else: @@ -277,4 +277,4 @@ def update_animation(self, t=None): else: self.pause() if self.playing or force: - super(Timeline, self).update_animation(t) + super(Timeline, self).update_animation(time) From 097cb3f66d278fb8281ac0f5099ea548e961bc76 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 27 Sep 2022 18:07:34 +0200 Subject: [PATCH 35/62] fixed glTF animation test --- fury/gltf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fury/gltf.py b/fury/gltf.py index ddb5e1df7..72b7c82c3 100644 --- a/fury/gltf.py +++ b/fury/gltf.py @@ -585,7 +585,7 @@ def main_timeline(self): main_timeline = Timeline(playback_panel=True) timelines = self.get_animation_timelines() for timeline in timelines: - main_timeline.add_child_timeline(timeline) + main_timeline.add_child_animation(timeline) return main_timeline From cc806e4a4936f374636ec4090491bb68d8351e9a Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 28 Sep 2022 11:21:39 +0200 Subject: [PATCH 36/62] Optimized motion path and some docs --- fury/animation/animation.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index f97c5884c..7b1dc3fc0 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -3,7 +3,8 @@ from time import perf_counter from collections import defaultdict from scipy.spatial import transform -from fury import utils, actor +from fury.actor import line +from fury import utils from fury.actor import Container from fury.lib import Actor, Transform from fury.animation.interpolator import spline_interpolator, \ @@ -104,7 +105,7 @@ def update_motion_path(self): lines = [] colors = [] if self.is_interpolatable('position'): - ts = np.linspace(0, self._max_timestamp, res) + ts = np.linspace(0, self.duration, res) [lines.append(self.get_position(t).tolist()) for t in ts] if self.is_interpolatable('color'): [colors.append(self.get_color(t)) for t in ts] @@ -113,12 +114,14 @@ def update_motion_path(self): len(self.items) else: colors = [1, 1, 1] + print(lines) + if len(lines) > 0: lines = np.array([lines]) - if colors is []: + if isinstance(colors, list): colors = np.array([colors]) - mpa = actor.line(lines, colors=colors, opacity=0.6) + mpa = line(lines, colors=colors, opacity=0.6) if self._scene: # remove old motion path actor if self._motion_path_actor is not None: @@ -1296,7 +1299,7 @@ def remove_actors(self): """Remove all actors from the Animation""" self.clear() - def add_update_callback(self, property_name, cbk_func): + def add_update_callback(self, prop, callback): """Add a function to be called each time animation is updated This function must accept only one argument which is the current value of the named property. @@ -1304,13 +1307,13 @@ def add_update_callback(self, property_name, cbk_func): Parameters ---------- - property_name: str + prop: str The name of the property. - cbk_func: function + callback: function The function to be called whenever the animation is updated. """ - attrib = self._get_attribute_data(property_name) - attrib.get('callbacks', []).append(cbk_func) + attrib = self._get_attribute_data(prop) + attrib.get('callbacks', []).append(callback) def update_animation(self, time=None): """Update the animation. @@ -1408,7 +1411,7 @@ def update_animation(self, time=None): for attrib in self._data: callbacks = self._data.get(attrib, {}).get('callbacks', []) - if callbacks is not [] and self.is_interpolatable(attrib): + if callbacks != [] and self.is_interpolatable(attrib): value = self.get_value(attrib, time) [cbk(value) for cbk in callbacks] From 8b35e9ad123cdadcd30b3fd286b65aa769c324dd Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 28 Sep 2022 11:26:19 +0200 Subject: [PATCH 37/62] assert actor is not already added to animation --- fury/animation/animation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 7b1dc3fc0..51ea154e0 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1178,10 +1178,12 @@ def add_actor(self, actor, static=False): for a in actor: self.add_actor(a, static=static) elif static: - self._static_actors.append(actor) + if actor not in self.static_actors: + self._static_actors.append(actor) else: - actor.vcolors = utils.colors_from_actor(actor) - super(Animation, self).add(actor) + if actor not in self.actors: + actor.vcolors = utils.colors_from_actor(actor) + super(Animation, self).add(actor) @property def timeline(self): From 2682f5b541723b4b9b5df5639983315fec9c1c09 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 28 Sep 2022 12:28:39 +0200 Subject: [PATCH 38/62] updated tests --- fury/animation/tests/test_animation.py | 84 ++++++++++++++++++++++++++ fury/animation/tests/test_timeline.py | 76 +++++++---------------- 2 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 fury/animation/tests/test_animation.py diff --git a/fury/animation/tests/test_animation.py b/fury/animation/tests/test_animation.py new file mode 100644 index 000000000..e5358dca3 --- /dev/null +++ b/fury/animation/tests/test_animation.py @@ -0,0 +1,84 @@ +import numpy as np +import numpy.testing as npt +from fury import actor +from fury.animation.interpolator import linear_interpolator, \ + step_interpolator, cubic_spline_interpolator, cubic_bezier_interpolator, \ + spline_interpolator +from fury.animation import Animation + + +def assert_not_equal(x, y): + npt.assert_equal(np.any(np.not_equal(x, y)), True) + + +def test_animation(): + shaders = False + anim = Animation() + + cube_actor = actor.cube(np.array([[0, 0, 0]])) + anim.add(cube_actor) + + assert cube_actor in anim.actors + assert cube_actor not in anim.static_actors + + anim.add_static_actor(cube_actor) + assert cube_actor in anim.static_actors + + anim = Animation(cube_actor) + assert cube_actor in anim.actors + + anim_main = Animation() + anim_main.add_child_animation(anim) + assert anim in anim_main.child_animations + + anim = Animation(cube_actor) + anim.set_position(0, np.array([1, 1, 1])) + # overriding a keyframe + anim.set_position(0, np.array([0, 0, 0])) + anim.set_position(3, np.array([2, 2, 2])) + anim.set_position(5, np.array([3, 15, 2])) + anim.set_position(7, np.array([4, 2, 20])) + + anim.set_opacity(0, 0) + anim.set_opacity(7, 1) + + anim.set_rotation(0, np.array([90, 0, 0])) + anim.set_rotation(7, np.array([0, 180, 0])) + + anim.set_scale(0, np.array([1, 1, 1])) + anim.set_scale(7, np.array([5, 5, 5])) + + anim.set_color(0, np.array([1, 0, 1])) + + npt.assert_almost_equal(anim.get_position(0), np.array([0, 0, 0])) + npt.assert_almost_equal(anim.get_position(7), np.array([4, 2, 20])) + + anim.set_position_interpolator(linear_interpolator) + anim.set_position_interpolator(cubic_bezier_interpolator) + anim.set_position_interpolator(step_interpolator) + anim.set_position_interpolator(cubic_spline_interpolator) + anim.set_position_interpolator(spline_interpolator, degree=2) + anim.set_rotation_interpolator(step_interpolator) + anim.set_scale_interpolator(linear_interpolator) + anim.set_opacity_interpolator(step_interpolator) + anim.set_color_interpolator(linear_interpolator) + + npt.assert_almost_equal(anim.get_position(0), np.array([0, 0, 0])) + npt.assert_almost_equal(anim.get_position(7), np.array([4, 2, 20])) + + npt.assert_almost_equal(anim.get_color(7), np.array([1, 0, 1])) + anim.set_color(25, np.array([0.2, 0.2, 0.5])) + assert_not_equal(anim.get_color(7), np.array([1, 0, 1])) + assert_not_equal(anim.get_color(25), np.array([0.2, 0.2, 0.5])) + + cube = actor.cube(np.array([[0, 0, 0]])) + anim.add_actor(cube) + anim.update_animation(0) + if not shaders: + transform = cube.GetUserTransform() + npt.assert_almost_equal(anim.get_position(0), + transform.GetPosition()) + npt.assert_almost_equal(anim.get_scale(0), + transform.GetScale()) + npt.assert_almost_equal(anim.get_rotation(0), + transform.GetOrientation()) diff --git a/fury/animation/tests/test_timeline.py b/fury/animation/tests/test_timeline.py index 91978f578..8335d5d1d 100644 --- a/fury/animation/tests/test_timeline.py +++ b/fury/animation/tests/test_timeline.py @@ -1,12 +1,8 @@ import time import numpy as np import numpy.testing as npt -from fury import actor -from fury.animation.interpolator import linear_interpolator, \ - step_interpolator, cubic_spline_interpolator, cubic_bezier_interpolator, \ - spline_interpolator -from fury.animation.timeline import Timeline import fury.testing as ft +from fury.animation import Timeline from fury.ui import PlaybackPanel @@ -15,36 +11,24 @@ def assert_not_equal(x, y): def test_timeline(): - shaders = False tl = Timeline(playback_panel=True) tl.set_position(0, np.array([1, 1, 1])) # overriding a keyframe tl.set_position(0, np.array([0, 0, 0])) - tl.set_position(3, np.array([2, 2, 2])) - tl.set_position(5, np.array([3, 15, 2])) - tl.set_position(7, np.array([4, 2, 20])) - - tl.set_opacity(0, 0) - tl.set_opacity(7, 1) - - tl.set_rotation(0, np.array([90, 0, 0])) tl.set_rotation(7, np.array([0, 180, 0])) - tl.set_scale(0, np.array([1, 1, 1])) - tl.set_scale(7, np.array([5, 5, 5])) - - tl.set_color(0, np.array([1, 0, 1])) - # test playback panel ft.assert_true(isinstance(tl.playback_panel, PlaybackPanel)) + tl.update_animation() + for t in [-10, 0, 2.2, 7, 100]: tl.seek(t) ft.assert_less_equal(tl.current_timestamp, tl.duration) ft.assert_greater_equal(tl.current_timestamp, 0) ft.assert_greater_equal(tl.current_timestamp, - tl.playback_panel.current_time) + tl.playback_panel.current_time) if 0 <= t <= tl.duration: npt.assert_almost_equal(tl.current_timestamp, t) @@ -68,38 +52,20 @@ def test_timeline(): ft.assert_true(tl.stopped) npt.assert_almost_equal(tl.current_timestamp, 0) - npt.assert_almost_equal(tl.get_position(0), np.array([0, 0, 0])) - npt.assert_almost_equal(tl.get_position(7), np.array([4, 2, 20])) - - tl.set_position_interpolator(linear_interpolator) - tl.set_position_interpolator(cubic_bezier_interpolator) - tl.set_position_interpolator(step_interpolator) - tl.set_position_interpolator(cubic_spline_interpolator) - tl.set_position_interpolator(spline_interpolator, degree=2) - tl.set_rotation_interpolator(step_interpolator) - tl.set_scale_interpolator(linear_interpolator) - tl.set_opacity_interpolator(step_interpolator) - tl.set_color_interpolator(linear_interpolator) - - npt.assert_almost_equal(tl.get_position(0), np.array([0, 0, 0])) - npt.assert_almost_equal(tl.get_position(7), np.array([4, 2, 20])) - - npt.assert_almost_equal(tl.get_color(7), np.array([1, 0, 1])) - tl.set_color(25, np.array([0.2, 0.2, 0.5])) - assert_not_equal(tl.get_color(7), np.array([1, 0, 1])) - assert_not_equal(tl.get_color(25), np.array([0.2, 0.2, 0.5])) - - cube = actor.cube(np.array([[0, 0, 0]])) - tl.add_actor(cube) - - # using force since the animation is not playing - tl.update_animation(0) - - if not shaders: - transform = cube.GetUserTransform() - npt.assert_almost_equal(tl.get_position(tl.current_timestamp), - transform.GetPosition()) - npt.assert_almost_equal(tl.get_scale(tl.current_timestamp), - transform.GetScale()) - npt.assert_almost_equal(tl.get_rotation(tl.current_timestamp), - transform.GetOrientation()) + length = 8 + tl_2 = Timeline(length=length) + tl.add_child_animation(tl_2) + assert tl_2 in tl.child_animations + + tl_2.set_position(12, [1, 2, 1]) + assert tl_2.duration == length + + tl_2 = Timeline() + tl_2.set_position(12, [0, 0, 1]) + assert tl_2.duration == 12 + + tl = Timeline(length=2, loop=True) + tl.play() + time.sleep(3) + print(tl.current_timestamp) + assert tl.current_timestamp >= 3 From 426936bd49d54864413e51ec45fd1fce29dd1b4e Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 28 Sep 2022 12:42:17 +0200 Subject: [PATCH 39/62] sleep time is not accurate and fails the tests on some OSs --- fury/animation/tests/test_timeline.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/fury/animation/tests/test_timeline.py b/fury/animation/tests/test_timeline.py index 8335d5d1d..93db8ccb5 100644 --- a/fury/animation/tests/test_timeline.py +++ b/fury/animation/tests/test_timeline.py @@ -64,8 +64,6 @@ def test_timeline(): tl_2.set_position(12, [0, 0, 1]) assert tl_2.duration == 12 - tl = Timeline(length=2, loop=True) - tl.play() - time.sleep(3) - print(tl.current_timestamp) - assert tl.current_timestamp >= 3 + tl = Timeline(length=12) + tl.set_position(1, np.array([1, 1, 1])) + assert tl.duration == 12 From 5c23ecea156654c92be95eea76c3b82514f11ee2 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 30 Sep 2022 15:18:16 +0200 Subject: [PATCH 40/62] Added loop property --- fury/animation/animation.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 51ea154e0..cb308cbaa 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -29,14 +29,14 @@ class Animation(Container): length : float or int, default: None, optional the fixed length of the animation. If set to None, the animation will get its duration from the keyframes being set. - loop : bool, optional + loop : bool, optional, default: True Whether to loop the animation (True) of play once (False). motion_path_res : int, default: None the number of line segments used to visualizer the animation's motion path (visualizing position). """ - def __init__(self, actors=None, length=None, loop=False, + def __init__(self, actors=None, length=None, loop=True, motion_path_res=None): super().__init__() @@ -1301,6 +1301,29 @@ def remove_actors(self): """Remove all actors from the Animation""" self.clear() + @property + def loop(self): + """Get loop condition of the current animation. + + Returns + ------- + bool + Whether the animation in loop mode (True) or play one mode (False). + """ + return self._loop + + @loop.setter + def loop(self, loop): + """Set the animation to loop or play once. + + Parameters + ---------- + loop: bool + The loop condition to be set. (True) to loop the animation, and + (False) to play only once. + """ + self._loop = loop + def add_update_callback(self, prop, callback): """Add a function to be called each time animation is updated This function must accept only one argument which is the current value From 117c2704fdbfd005ae01e9adeb49531ccdad8de8 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sat, 1 Oct 2022 20:23:34 +0200 Subject: [PATCH 41/62] making Timeline independent --- fury/animation/animation.py | 1 - fury/animation/timeline.py | 151 ++++++++++++++++++++++++------------ 2 files changed, 101 insertions(+), 51 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index cb308cbaa..fbd5134a0 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -114,7 +114,6 @@ def update_motion_path(self): len(self.items) else: colors = [1, 1, 1] - print(lines) if len(lines) > 0: lines = np.array([lines]) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 1e438a0f3..e8383ff94 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -3,45 +3,44 @@ from fury.animation.animation import Animation -class Timeline(Animation): +class Timeline: """Keyframe animation Timeline. Timeline is responsible for handling the playback of keyframes animations. - It can also act like an Animation with playback options which makes it easy + It has multiple playback options which makes it easy to control the playback, speed, state of the animation with/without a GUI playback panel. Attributes ---------- - actors : Actor or list[Actor], optional, default: None + animations : Animation or list[Animation], optional, default: None Actor/s to be animated directly by the Timeline (main Animation). playback_panel : bool, optional If True, the timeline will have a playback panel set, which can be used to control the playback of the timeline. length : float or int, default: None, optional the fixed length of the timeline. If set to None, the timeline will get - its length from the keyframes. + its length from the animations that it controls automatically. loop : bool, optional - Whether loop playing the animation or not - motion_path_res : int, default: None - the number of line segments used to visualizer the animation's motion - path (visualizing position). + Whether loop playing the timeline or play once. """ - def __init__(self, actors=None, playback_panel=False, length=None, - loop=False, motion_path_res=None): + def __init__(self, animations=None, playback_panel=False, loop=True, + length=None): - super().__init__(actors=actors, length=length, loop=loop, - motion_path_res=motion_path_res) self.playback_panel = None self._current_timestamp = 0 - self._speed = 1 + self._speed = 1.0 self._last_started_time = 0 self._playing = False self._animations = [] - self._timeline = None - self._parent_animation = None + self._loop = loop + self._length = length + self._duration = length if length is not None else 0.0 # Handle actors while constructing the timeline. + + if animations is not None: + self.add_animation(animations) if playback_panel: def set_loop(is_loop): self._loop = is_loop @@ -56,10 +55,34 @@ def set_speed(speed): self.playback_panel.on_loop_toggle = set_loop self.playback_panel.on_progress_bar_changed = self.seek self.playback_panel.on_speed_changed = set_speed - self.add_actor(self.playback_panel, static=True) - if actors is not None: - self.add_actor(actors) + def update_duration(self): + """Update and return the duration of the Timeline. + + Returns + ------- + float + The duration of the Timeline. + """ + if self._length is not None: + self._duration = self._length + else: + self._duration = max([0.0] + [anim.update_duration() for anim + in self._animations]) + if self.has_playback_panel: + self.playback_panel.final_time = self.duration + return self.duration + + @property + def duration(self): + """Return the duration of the Timeline. + + Returns + ------- + float + The duration of the Timeline. + """ + return self._duration def play(self): """Play the animation""" @@ -79,13 +102,13 @@ def stop(self): """Stop the animation""" self._current_timestamp = 0 self._playing = False - self.update_animation(0) + self.update_timeline(force=True) def restart(self): """Restart the animation""" self._current_timestamp = 0 self._playing = True - self.update_animation(0) + self.update_timeline(force=True) @property def current_timestamp(self): @@ -133,7 +156,7 @@ def seek(self, timestamp): perf_counter() - timestamp / self.speed else: self._current_timestamp = timestamp - self.update_animation(timestamp) + self.update_timeline(force=True) def seek_percent(self, percent): """Seek a percentage of the Timeline's final timestamp. @@ -185,7 +208,7 @@ def paused(self): @property def speed(self): - """Return the speed of the timeline. + """Return the speed of the timeline's playback. Returns ------- @@ -196,7 +219,7 @@ def speed(self): @speed.setter def speed(self, speed): - """Set the speed of the timeline. + """Set the speed of the timeline's playback. Parameters ---------- @@ -211,6 +234,30 @@ def speed(self, speed): self._last_started_time = perf_counter() self.current_timestamp = current + @property + def loop(self): + """Get loop condition of the timeline. + + Returns + ------- + bool + Whether the playback is in loop mode (True) or play one mode + (False). + """ + return self._loop + + @loop.setter + def loop(self, loop): + """Set the timeline's playback to loop or play once. + + Parameters + ---------- + loop: bool + The loop condition to be set. (True) to loop the playback, and + (False) to play only once. + """ + self._loop = loop + @property def has_playback_panel(self): """Return whether the `Timeline` has a playback panel. @@ -221,49 +268,48 @@ def has_playback_panel(self): """ return self.playback_panel is not None - def add_child_animation(self, animation): - """Add child Animation or list of Animations. + def add_animation(self, animation): + """Add Animation or list of Animations. Parameters ---------- - animation: Animation or list(Animation) + animation: Animation or list[Animation] or tuple[Animation] Animation/s to be added. """ - super(Timeline, self).add_child_animation(animation) - if isinstance(animation, Animation): + if isinstance(animation, (list, tuple)): + [self.add_animation(anim) for anim in animation] + elif isinstance(animation, Animation): animation._timeline = self + self._animations.append(animation) + self.update_duration() + else: + raise TypeError(f"Expected an Animation, a list or a tuple.") - def update_duration(self): - """Update and return the duration of the Timeline. + @property + def animations(self) -> 'list[Animation]': + """Return a list of Animations. Returns ------- - float - The duration of the Timeline. + list: + List of Animations controlled by the timeline. """ - super(Timeline, self).update_duration() - if self.has_playback_panel: - self.playback_panel.final_time = self.duration - return self.duration + return self._animations - def update_animation(self, time=None): - """Update the animation. + def update_timeline(self, force=False): + """Update the timeline. - Update the animation and the playback of the Timeline. As well as - updating all animations handled by the Timeline. + Update the Timeline and all the animations that it controls. As well as + the playback of the Timeline (if exists). Parameters ---------- - time: float or int, optional, default: None - Time to update animation at. - IF None, The time is determined by the Timeline itself and can be - controlled using the playback panel graphically or by the Timeline - methods such as ``Timeline.seek(t)``. + force: bool, optional, default: False + If True, the timeline will update even when the timeline is paused + or stopped and hence, more resources will be used. + """ - force = True - if time is None: - time = self.current_timestamp - force = False + time = self.current_timestamp if self.has_playback_panel: self.playback_panel.current_time = time if time > self.duration: @@ -277,4 +323,9 @@ def update_animation(self, time=None): else: self.pause() if self.playing or force: - super(Timeline, self).update_animation(time) + [anim.update_animation(time) for anim in self._animations] + + def add_to_scene(self, ren): + """Add this Timeline and all of its Animations to the scene""" + self.playback_panel.add_to_scene(ren) + [ren.add(animation) for animation in self._animations] From 97514573ec25af9aac6f17d249667757671c9d4c Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sat, 1 Oct 2022 20:23:59 +0200 Subject: [PATCH 42/62] updated tests --- fury/animation/tests/test_timeline.py | 28 ++++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/fury/animation/tests/test_timeline.py b/fury/animation/tests/test_timeline.py index 93db8ccb5..6452cd525 100644 --- a/fury/animation/tests/test_timeline.py +++ b/fury/animation/tests/test_timeline.py @@ -2,7 +2,7 @@ import numpy as np import numpy.testing as npt import fury.testing as ft -from fury.animation import Timeline +from fury.animation import Timeline, Animation from fury.ui import PlaybackPanel @@ -12,16 +12,10 @@ def assert_not_equal(x, y): def test_timeline(): tl = Timeline(playback_panel=True) - tl.set_position(0, np.array([1, 1, 1])) - # overriding a keyframe - tl.set_position(0, np.array([0, 0, 0])) - tl.set_rotation(7, np.array([0, 180, 0])) # test playback panel ft.assert_true(isinstance(tl.playback_panel, PlaybackPanel)) - tl.update_animation() - for t in [-10, 0, 2.2, 7, 100]: tl.seek(t) ft.assert_less_equal(tl.current_timestamp, tl.duration) @@ -54,16 +48,18 @@ def test_timeline(): length = 8 tl_2 = Timeline(length=length) - tl.add_child_animation(tl_2) - assert tl_2 in tl.child_animations + anim = Animation(length=12) + tl_2.add_animation(anim) + assert anim in tl_2.animations - tl_2.set_position(12, [1, 2, 1]) + anim.set_position(12, [1, 2, 1]) assert tl_2.duration == length - tl_2 = Timeline() - tl_2.set_position(12, [0, 0, 1]) - assert tl_2.duration == 12 + tl_2 = Timeline(anim, length=11) + assert tl_2.duration == 11 + + tl = Timeline(playback_panel=True) + assert tl.has_playback_panel is True - tl = Timeline(length=12) - tl.set_position(1, np.array([1, 1, 1])) - assert tl.duration == 12 + tl.loop = True + assert tl.loop is True From a9f97f4328686751c06b9886c6ab325d1f99fe1e Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sun, 2 Oct 2022 15:31:22 +0200 Subject: [PATCH 43/62] Implemented `remove_from_scene` for Timeline and Animation --- fury/animation/animation.py | 24 +++++++++++++++++------- fury/animation/timeline.py | 13 +++++++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index fbd5134a0..603d60f16 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1445,15 +1445,25 @@ def update_animation(self, time=None): if self._scene and self._parent_animation is None: self._scene.reset_clipping_range() - def add_to_scene(self, ren): - """Add this Animation, its actors and sub Animations to the scene""" - super(Animation, self).add_to_scene(ren) - [ren.add(static_act) for static_act in self._static_actors] - [ren.add(animation) for animation in self._animations] + def add_to_scene(self, scene): + """Add Animation, its actors and sub Animations to the scene""" + super(Animation, self).add_to_scene(scene) + [scene.add(static_act) for static_act in self._static_actors] + [scene.add(animation) for animation in self._animations] if self._motion_path_actor: - ren.add(self._motion_path_actor) - self._scene = ren + scene.add(self._motion_path_actor) + self._scene = scene self._added_to_scene = True self._added_to_scene_time = perf_counter() self.update_animation(0) + + def remove_from_scene(self, scene): + """Remove Animation, its actors and sub Animations from the scene""" + [scene.rm(act) for act in self.actors] + [scene.rm(static_act) for static_act in self._static_actors] + for anim in self.child_animations: + anim.remove_from_scene(scene) + if self._motion_path_actor: + scene.rm(self._motion_path_actor) + self._added_to_scene = False diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index e8383ff94..37bed0aab 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -325,7 +325,12 @@ def update_timeline(self, force=False): if self.playing or force: [anim.update_animation(time) for anim in self._animations] - def add_to_scene(self, ren): - """Add this Timeline and all of its Animations to the scene""" - self.playback_panel.add_to_scene(ren) - [ren.add(animation) for animation in self._animations] + def add_to_scene(self, scene): + """Add Timeline and all of its Animations to the scene""" + self.playback_panel.add_to_scene(scene) + [animation.add_to_scene(scene) for animation in self._animations] + + def remove_from_scene(self, scene): + """Remove Timeline and all of its Animations to the scene""" + scene.rm(*tuple(self.playback_panel.actors)) + [animation.remove_from_scene(scene) for animation in self._animations] From 79f30d1afa91242e800848f11458446175a2a558 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sun, 2 Oct 2022 16:27:48 +0200 Subject: [PATCH 44/62] add_animation to showManager --- fury/window.py | 86 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/fury/window.py b/fury/window.py index 2afd6933d..119aabc4d 100644 --- a/fury/window.py +++ b/fury/window.py @@ -9,7 +9,7 @@ from scipy import ndimage from fury import __version__ as fury_version -from fury.animation.timeline import Timeline +from fury.animation import Timeline, Animation from fury.decorators import is_osx from fury.interactor import CustomInteractorStyle from fury.io import load_image, save_image @@ -400,7 +400,8 @@ def __init__(self, scene=None, title='FURY', size=(300, 300), self.iren.SetInteractorStyle(self.style) self.iren.SetRenderWindow(self.window) self._timelines = [] - self._timeline_callback = None + self._animations = [] + self._animation_callback = None def initialize(self): """Initialize interaction.""" @@ -408,50 +409,79 @@ def initialize(self): @property def timelines(self): + """Return a list of Timelines that were added to the ShowManager. + + Returns + ------- + list[Timeline]: + List of Timelines. + """ return self._timelines - def add_timeline(self, timeline: Timeline): - """Add a Timeline to the ShowManager. + @property + def animations(self): + """Return a list of Animations that were added to the ShowManager. + + Returns + ------- + list[Animation]: + List of Animations. + """ + return self._animations - Adding a Timeline to the ShowManager ensures that it gets added to - the scene, gets updated and rendered without any extra code. + def add_animation(self, animation): + """Add an Animation or a Timeline to the ShowManager. + + Adding an Animation or a Timeline to the ShowManager ensures that it + gets added to the scene, gets updated and rendered without any extra + code. Parameters ---------- - timeline : Timeline - The Timeline to be added to the ShowManager. + animation : Animation or Timeline + The Animation or Timeline to be added to the ShowManager. """ - - if timeline in self._timelines: - return - self.scene.add(timeline) - self._timelines.append(timeline) - - if self._timeline_callback is not None: + animation.add_to_scene(self.scene) + if isinstance(animation, Animation): + if animation in self._animations: + return + self._animations.append(animation) + elif isinstance(animation, Timeline): + if animation in self._timelines: + return + self._timelines.append(animation) + + if self._animation_callback is not None: return def animation_cbk(_obj, _event): - [tl.update_animation() for tl in self._timelines] + [tl.update_timeline() for tl in self._timelines] + [anim.update_animation() for anim in self._animations] self.render() - self._timeline_callback = self.add_timer_callback(True, 10, - animation_cbk) + self._animation_callback = self.add_timer_callback(True, 10, + animation_cbk) - def remove_timeline(self, timeline: Timeline): - """Remove a Timeline from the ShowManager. + def remove_animation(self, animation): + """Remove an Animation or a Timeline from the ShowManager. - Timeline will be removed from the Scene as well as from the ShowManager + Animation will be removed from the Scene as well as from the + ShowManager. Parameters ---------- - timeline : Timeline + animation : Animation or Timeline The Timeline to be removed. """ - if timeline in self.timelines: - timeline.remove_from_scene(self.scene) - self._timelines.remove(timeline) - if not len(self.timelines): - self.iren.DestroyTimer(self._timeline_callback) - self._timeline_callback = None + + if animation in self.timelines or animation in self.animations: + animation.remove_from_scene(self.scene) + if isinstance(animation, Animation): + self._animations.remove(animation) + elif isinstance(animation, Timeline): + self._timelines.remove(animation) + if not (len(self.timelines) or len(self.animations)): + self.iren.DestroyTimer(self._animation_callback) + self._animation_callback = None def render(self): """Render only once.""" From 2cf48cbcaa536f0cf7515259216aea4f91f7cf6d Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sun, 2 Oct 2022 16:27:57 +0200 Subject: [PATCH 45/62] updated tests --- fury/tests/test_window.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index 7e9da1a24..edd0a6c59 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -5,7 +5,7 @@ import pytest import itertools from fury import actor, window, io -from fury.animation.timeline import Timeline +from fury.animation import Timeline, Animation from fury.lib import ImageData, Texture, numpy_support from fury.testing import captured_output, assert_less_equal, assert_greater, \ assert_true @@ -608,35 +608,43 @@ def timer_callback(_obj, _event): assert_greater(ideal_fps, actual_fps) -def test_add_timeline_to_show_manager(): +def test_add_animation_to_show_manager(): showm = window.ShowManager() showm.initialize() cube = actor.cube(np.array([[2, 2, 3]])) timeline = Timeline(playback_panel=True) - timeline.add(cube) - showm.add_timeline(timeline) + animation = Animation(cube) + timeline.add_animation(animation) + showm.add_animation(timeline) npt.assert_equal(len(showm._timelines), 1) - assert_true(showm._timeline_callback is not None) + assert_true(showm._animation_callback is not None) actors = showm.scene.GetActors() assert_true(cube in actors) actors_2d = showm.scene.GetActors2D() - [assert_true(act in actors_2d) for act in timeline.static_actors] - showm.remove_timeline(timeline) + [assert_true(act in actors_2d) for act in animation.static_actors] + showm.remove_animation(timeline) actors = showm.scene.GetActors() actors_2d = showm.scene.GetActors2D() - [assert_true(act not in actors) for act in timeline.static_actors] + [assert_true(act not in actors) for act in animation.static_actors] assert_true(cube not in actors) - assert_true(showm._timeline_callback is None) + assert_true(showm._animation_callback is None) assert_true(showm.timelines == []) assert_true(list(actors_2d) == []) + showm.add_animation(animation) + assert_true(cube in showm.scene.GetActors()) + + showm.remove_animation(animation) + assert_true(cube not in showm.scene.GetActors()) + assert_true(showm.animations == []) + assert_true(list(showm.scene.GetActors()) == []) # test_opengl_state_add_remove_and_check() # test_opengl_state_simple() From 09ad0470657191a3581523dced1ba2aa0ee4abf0 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 3 Oct 2022 11:06:24 +0200 Subject: [PATCH 46/62] PlaybackPanel might not exist --- fury/animation/timeline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 37bed0aab..3b371737c 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -327,10 +327,12 @@ def update_timeline(self, force=False): def add_to_scene(self, scene): """Add Timeline and all of its Animations to the scene""" - self.playback_panel.add_to_scene(scene) + if self.has_playback_panel: + self.playback_panel.add_to_scene(scene) [animation.add_to_scene(scene) for animation in self._animations] def remove_from_scene(self, scene): """Remove Timeline and all of its Animations to the scene""" - scene.rm(*tuple(self.playback_panel.actors)) + if self.has_playback_panel: + scene.rm(*tuple(self.playback_panel.actors)) [animation.remove_from_scene(scene) for animation in self._animations] From a2f8030d86d594df39bb12b80259ce5af4fcfb80 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Mon, 3 Oct 2022 11:38:12 +0200 Subject: [PATCH 47/62] Adding animations after adding creating the PlaybackPanel --- fury/animation/timeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index e8383ff94..4e1180f5d 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -37,10 +37,7 @@ def __init__(self, animations=None, playback_panel=False, loop=True, self._loop = loop self._length = length self._duration = length if length is not None else 0.0 - # Handle actors while constructing the timeline. - if animations is not None: - self.add_animation(animations) if playback_panel: def set_loop(is_loop): self._loop = is_loop @@ -56,6 +53,9 @@ def set_speed(speed): self.playback_panel.on_progress_bar_changed = self.seek self.playback_panel.on_speed_changed = set_speed + if animations is not None: + self.add_animation(animations) + def update_duration(self): """Update and return the duration of the Timeline. From c45dc5b143d9c5ceec162f32beabc5b04b8da6f8 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 4 Oct 2022 12:36:52 +0200 Subject: [PATCH 48/62] some renaming and loop issue fixed --- fury/animation/animation.py | 18 +++++++++++------- fury/animation/timeline.py | 3 ++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index fbd5134a0..f5b43fecf 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -48,7 +48,7 @@ def __init__(self, actors=None, length=None, loop=True, self._parent_animation = None self._camera = None self._scene = None - self._added_to_scene_time = 0 + self._start_time = 0 self._length = length self._duration = length if length else 0 self._loop = loop @@ -77,6 +77,7 @@ def update_duration(self): self._duration = max( self._max_timestamp, max([0] + [anim.update_duration() for anim in self.child_animations])) + return self.duration @property @@ -1350,16 +1351,19 @@ def update_animation(self, time=None): time: float or int, optional, default: None The time to update animation at. """ - time = time if time is not None else \ - perf_counter() - self._added_to_scene_time + need_reset_clipping = False + if time is None: + time = perf_counter() - self._start_time + need_reset_clipping = True # handling in/out of scene events in_scene = self.is_inside_scene_at(time) self._handle_scene_event(time) if self.duration: - if self._loop and time > 0: - time = time % self.duration + if self._loop and time > self.duration: + time = 0 + self._start_time = perf_counter() elif time > self.duration: time = self.duration if isinstance(self._parent_animation, Animation): @@ -1442,7 +1446,7 @@ def update_animation(self, time=None): # Also update all child Animations. [animation.update_animation(time) for animation in self._animations] - if self._scene and self._parent_animation is None: + if self._scene and need_reset_clipping: self._scene.reset_clipping_range() def add_to_scene(self, ren): @@ -1455,5 +1459,5 @@ def add_to_scene(self, ren): ren.add(self._motion_path_actor) self._scene = ren self._added_to_scene = True - self._added_to_scene_time = perf_counter() + self._start_time = perf_counter() self.update_animation(0) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index 4e1180f5d..e73f74a35 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -327,5 +327,6 @@ def update_timeline(self, force=False): def add_to_scene(self, ren): """Add this Timeline and all of its Animations to the scene""" - self.playback_panel.add_to_scene(ren) + if self.has_playback_panel: + self.playback_panel.add_to_scene(ren) [ren.add(animation) for animation in self._animations] From 37ab99c4f02e87ef06bc8839e6c5357bd6d87e93 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Tue, 4 Oct 2022 13:08:07 +0200 Subject: [PATCH 49/62] Abandoned the `Container` as a superclass --- fury/animation/animation.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index f5b43fecf..dd252e813 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -11,7 +11,7 @@ step_interpolator, linear_interpolator, slerp -class Animation(Container): +class Animation: """Keyframe animation class. Animation is responsible for keyframe animations for a single or a @@ -43,6 +43,7 @@ def __init__(self, actors=None, length=None, loop=True, self._data = defaultdict(dict) self._camera_data = defaultdict(dict) self._animations = [] + self._actors = [] self._static_actors = [] self._timeline = None self._parent_animation = None @@ -110,9 +111,9 @@ def update_motion_path(self): [lines.append(self.get_position(t).tolist()) for t in ts] if self.is_interpolatable('color'): [colors.append(self.get_color(t)) for t in ts] - elif len(self.items) >= 1: - colors = sum([i.vcolors[0] / 255 for i in self.items]) / \ - len(self.items) + elif len(self._actors) >= 1: + colors = sum([i.vcolors[0] / 255 for i in self._actors]) / \ + len(self._actors) else: colors = [1, 1, 1] @@ -325,9 +326,11 @@ def is_inside_scene_at(self, timestamp): parent_in_scene = parent._added_to_scene if self.is_interpolatable('in_scene'): - return parent_in_scene and self.get_value('in_scene', timestamp) - - return parent_in_scene + in_scene = parent_in_scene and \ + self.get_value('in_scene', timestamp) + else: + in_scene = parent_in_scene + return in_scene def add_to_scene_at(self, timestamp): """Set timestamp for adding Animation to scene event. @@ -361,10 +364,10 @@ def _handle_scene_event(self, timestamp): should_be_in_scene = self.is_inside_scene_at(timestamp) if self._scene is not None: if should_be_in_scene and not self._added_to_scene: - super(Animation, self).add_to_scene(self._scene) + self._scene.add(*self._actors) self._added_to_scene = True elif not should_be_in_scene and self._added_to_scene: - super(Animation, self).remove_from_scene(self._scene) + self._scene.rm(*self._actors) self._added_to_scene = False def set_camera_keyframes(self, attrib, keyframes): @@ -1181,9 +1184,9 @@ def add_actor(self, actor, static=False): if actor not in self.static_actors: self._static_actors.append(actor) else: - if actor not in self.actors: + if actor not in self._actors: actor.vcolors = utils.colors_from_actor(actor) - super(Animation, self).add(actor) + self._actors.append(actor) @property def timeline(self): @@ -1247,7 +1250,7 @@ def actors(self): list: List of actors controlled by the Animation. """ - return self.items + return self._actors @property def child_animations(self) -> 'list[Animation]': @@ -1295,11 +1298,11 @@ def remove_actor(self, actor): actor: vtkActor Actor to be removed from the Animation. """ - self._items.remove(actor) + self._actors.remove(actor) def remove_actors(self): """Remove all actors from the Animation""" - self.clear() + self._actors.clear() @property def loop(self): @@ -1451,7 +1454,7 @@ def update_animation(self, time=None): def add_to_scene(self, ren): """Add this Animation, its actors and sub Animations to the scene""" - super(Animation, self).add_to_scene(ren) + [ren.add(actor) for actor in self._actors] [ren.add(static_act) for static_act in self._static_actors] [ren.add(animation) for animation in self._animations] From 87d21690f77ea29c8d9b76bc66402908bed557dd Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 5 Oct 2022 16:53:39 +0200 Subject: [PATCH 50/62] Separated camera animation from normal animation --- fury/animation/animation.py | 574 ++++++++++++------------------------ 1 file changed, 193 insertions(+), 381 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index dd252e813..0146291cd 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -55,7 +55,6 @@ def __init__(self, actors=None, length=None, loop=True, self._loop = loop self._max_timestamp = 0 self._added_to_scene = True - self._is_camera_animated = False self._motion_path_res = motion_path_res self._motion_path_actor = None self._transform = Transform() @@ -130,50 +129,30 @@ def update_motion_path(self): self._scene.add(mpa) self._motion_path_actor = mpa - def _get_data(self, is_camera=False): + def _get_data(self): """Get animation data. - Parameters - ---------- - is_camera: bool, optional, default : False - Specifies whether return general data or camera-related data. Returns ------- dict: The animation data containing keyframes and interpolators. """ - if is_camera: - self._is_camera_animated = True - return self._camera_data - else: - return self._data - - def _get_camera_data(self): - """Get camera animation data. + return self._data - Returns - ------- - dict: - The camera animation data containing keyframes and interpolators. - """ - return self._get_data(is_camera=True) - - def _get_attribute_data(self, attrib, is_camera=False): + def _get_attribute_data(self, attrib): """Get animation data for a specific attribute. Parameters ---------- attrib: str The attribute name to get data for. - is_camera: bool, optional, default : False - Specifies whether return general data or camera-related data. Returns ------- dict: The animation data for a specific attribute. """ - data = self._get_data(is_camera=is_camera) + data = self._get_data() if attrib not in data: data[attrib] = { @@ -188,7 +167,7 @@ def _get_attribute_data(self, attrib, is_camera=False): } return data.get(attrib) - def get_keyframes(self, attrib=None, is_camera=False): + def get_keyframes(self, attrib=None): """Get a keyframe for a specific or all attributes. Parameters @@ -196,19 +175,17 @@ def get_keyframes(self, attrib=None, is_camera=False): attrib: str, optional, default: None The name of the attribute. If None, all keyframes for all set attributes will be returned. - is_camera: bool, optional - Indicated whether setting a camera property or general property. """ - data = self._get_data(is_camera=is_camera) + data = self._get_data() if attrib is None: attribs = data.keys() return {attrib: data.get(attrib, {}).get('keyframes', {}) for attrib in attribs} return data.get(attrib, {}).get('keyframes', {}) - def set_keyframe(self, attrib, timestamp, value, is_camera=False, - update_interpolator=True, **kwargs): + def set_keyframe(self, attrib, timestamp, value, update_interpolator=True, + **kwargs): """Set a keyframe for a certain attribute. Parameters @@ -219,8 +196,6 @@ def set_keyframe(self, attrib, timestamp, value, is_camera=False, Timestamp of the keyframe. value: ndarray or float or bool Value of the keyframe at the given timestamp. - is_camera: bool, optional - Indicated whether setting a camera property or general property. update_interpolator: bool, optional Interpolator will be reinitialized if Ture @@ -236,7 +211,7 @@ def set_keyframe(self, attrib, timestamp, value, is_camera=False, The out tangent at that position for the cubic spline curve. """ - attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) + attrib_data = self._get_attribute_data(attrib) keyframes = attrib_data.get('keyframes') keyframes[timestamp] = { @@ -250,8 +225,7 @@ def set_keyframe(self, attrib, timestamp, value, is_camera=False, interp_base = interp.get( 'base', linear_interpolator if attrib != 'rotation' else slerp) args = interp.get('args', {}) - self.set_interpolator(attrib, interp_base, - is_camera=is_camera, **args) + self.set_interpolator(attrib, interp_base, **args) if timestamp > self._max_timestamp: self._max_timestamp = timestamp @@ -262,7 +236,7 @@ def set_keyframe(self, attrib, timestamp, value, is_camera=False, self.update_animation() self.update_motion_path() - def set_keyframes(self, attrib, keyframes, is_camera=False): + def set_keyframes(self, attrib, keyframes): """Set multiple keyframes for a certain attribute. Parameters @@ -271,8 +245,6 @@ def set_keyframes(self, attrib, keyframes, is_camera=False): The name of the attribute. keyframes: dict A dict object containing keyframes to be set. - is_camera: bool, optional - Indicated whether setting a camera property or general property. Notes ----- @@ -285,25 +257,9 @@ def set_keyframes(self, attrib, keyframes, is_camera=False): """ for t, keyframe in keyframes.items(): if isinstance(keyframe, dict): - self.set_keyframe(attrib, t, **keyframe, is_camera=is_camera) + self.set_keyframe(attrib, t, **keyframe) else: - self.set_keyframe(attrib, t, keyframe, is_camera=is_camera) - - def set_camera_keyframe(self, attrib, timestamp, value, **kwargs): - """Set a keyframe for a camera property - - Parameters - ---------- - attrib: str - The name of the attribute. - timestamp: float - Timestamp of the keyframe. - value: value: ndarray or float or bool - Value of the keyframe at the given timestamp. - **kwargs: dict, optional - Additional keyword arguments passed to `set_keyframe`. - """ - self.set_keyframe(attrib, timestamp, value, is_camera=True, **kwargs) + self.set_keyframe(attrib, t, keyframe) def is_inside_scene_at(self, timestamp): """Check if the Animation is set to be inside the scene at a specific @@ -370,29 +326,8 @@ def _handle_scene_event(self, timestamp): self._scene.rm(*self._actors) self._added_to_scene = False - def set_camera_keyframes(self, attrib, keyframes): - """Set multiple keyframes for a certain camera property - - Parameters - ---------- - attrib: str - The name of the property. - keyframes: dict - A dict object containing keyframes to be set. - - Notes - ----- - Cubic Bézier curve control points are not supported yet in this setter. - - Examples - -------- - >>> cam_pos = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} - >>> Animation.set_camera_keyframes('position', cam_pos) - """ - self.set_keyframes(attrib, keyframes, is_camera=True) - - def set_interpolator(self, attrib, interpolator, is_camera=False, - is_evaluator=False, **kwargs): + def set_interpolator(self, attrib, interpolator, is_evaluator=False, + **kwargs): """Set keyframes interpolator for a certain property Parameters @@ -402,9 +337,6 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, interpolator: callable The generator function of the interpolator to be used to interpolate/evaluate keyframes. - is_camera: bool, optional - Indicated whether dealing with a camera property or general - property. is_evaluator: bool, optional Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes such as: @@ -432,7 +364,7 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, >>> Animation.set_interpolator('position', pos_fun) """ - attrib_data = self._get_attribute_data(attrib, is_camera=is_camera) + attrib_data = self._get_attribute_data(attrib) keyframes = attrib_data.get('keyframes', {}) interp_data = attrib_data.get('interpolator', {}) if is_evaluator: @@ -451,15 +383,13 @@ def set_interpolator(self, attrib, interpolator, is_camera=False, self.update_duration() self.update_motion_path() - def is_interpolatable(self, attrib, is_camera=False): + def is_interpolatable(self, attrib): """Check whether a property is interpolatable. Parameters ---------- attrib: str The name of the property. - is_camera: bool, optional - Indicated whether checking a camera property or general property. Returns ------- @@ -472,33 +402,9 @@ def is_interpolatable(self, attrib, is_camera=False): specified property. And False means the opposite. """ - data = self._camera_data if is_camera else self._data + data = self._data return bool(data.get(attrib, {}).get('interpolator', {}).get('func')) - def set_camera_interpolator(self, attrib, interpolator, - is_evaluator=False): - """Set the interpolator for a specific camera property. - - Parameters - ---------- - attrib: str - The name of the camera property. - The already handled properties are position, focal, and view_up. - - interpolator: callable - The generator function of the interpolator that handles the - camera property interpolation between keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - - Examples - -------- - >>> Animation.set_camera_interpolator('focal', linear_interpolator) - """ - self.set_interpolator(attrib, interpolator, is_camera=True, - is_evaluator=is_evaluator) - def set_position_interpolator(self, interpolator, is_evaluator=False, **kwargs): """Set the position interpolator. @@ -598,37 +504,6 @@ def set_opacity_interpolator(self, interpolator, is_evaluator=False): self.set_interpolator('opacity', interpolator, is_evaluator=is_evaluator) - def set_camera_position_interpolator(self, interpolator, - is_evaluator=False): - """Set the camera position interpolator. - - Parameters - ---------- - interpolator: callable - The generator function of the interpolator that would handle the - interpolation of the camera position keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - """ - self.set_camera_interpolator("position", interpolator, - is_evaluator=is_evaluator) - - def set_camera_focal_interpolator(self, interpolator, is_evaluator=False): - """Set the camera focal position interpolator. - - Parameters - ---------- - interpolator: callable - The generator function of the interpolator that would handle the - interpolation of the camera focal position keyframes. - is_evaluator: bool, optional - Specifies whether the `interpolator` is time-only based evaluation - function that does not depend on keyframes. - """ - self.set_camera_interpolator("focal", interpolator, - is_evaluator=is_evaluator) - def get_value(self, attrib, timestamp): """Return the value of an attribute at any given timestamp. @@ -654,21 +529,6 @@ def get_current_value(self, attrib): return self._data.get(attrib).get('interpolator'). \ get('func')(self._timeline.current_timestamp) - def get_camera_value(self, attrib, timestamp): - """Return the value of an attribute interpolated at any given - timestamp. - - Parameters - ---------- - attrib: str - The attribute name. - timestamp: float - The timestamp to interpolate at. - - """ - return self._camera_data.get(attrib).get('interpolator'). \ - get('func')(timestamp) - def set_position(self, timestamp, position, **kwargs): """Set a position keyframe at a specific timestamp. @@ -939,194 +799,6 @@ def get_opacity(self, t): """ return self.get_value('opacity', t) - def set_camera_position(self, timestamp, position, **kwargs): - """Set the camera position keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate opacity at. - position: ndarray, shape(1, 3) - The camera position - """ - self.set_camera_keyframe('position', timestamp, position, **kwargs) - - def set_camera_focal(self, timestamp, position, **kwargs): - """Set camera's focal position keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate opacity at. - position: ndarray, shape(1, 3) - The camera position - """ - self.set_camera_keyframe('focal', timestamp, position, **kwargs) - - def set_camera_view_up(self, timestamp, direction, **kwargs): - """Set the camera view-up direction keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate at. - direction: ndarray, shape(1, 3) - The camera view-up direction - """ - self.set_camera_keyframe('view_up', timestamp, direction, **kwargs) - - def set_camera_rotation(self, timestamp, rotation, **kwargs): - """Set the camera rotation keyframe. - - Parameters - ---------- - timestamp: float - The time to interpolate at. - rotation: ndarray, shape(1, 3) or shape(1, 4) - Rotation data in euler degrees with shape(1, 3) or in quaternions - with shape(1, 4). - - Notes - ----- - Euler rotations are executed by rotating first around Z then around X, - and finally around Y. - """ - self.set_rotation(timestamp, rotation, is_camera=True, **kwargs) - - def set_camera_position_keyframes(self, keyframes): - """Set a dict of camera position keyframes at once. - Should be in the following form: - {timestamp_1: position_1, timestamp_2: position_2} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and opacities as values. - - Examples - -------- - >>> pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} - >>> Animation.set_camera_position_keyframes(pos) - """ - self.set_camera_keyframes('position', keyframes) - - def set_camera_focal_keyframes(self, keyframes): - """Set multiple camera focal position keyframes at once. - Should be in the following form: - {timestamp_1: focal_1, timestamp_2: focal_1, ...} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and camera focal positions as - values. - - Examples - -------- - >>> focal_pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} - >>> Animation.set_camera_focal_keyframes(focal_pos) - """ - self.set_camera_keyframes('focal', keyframes) - - def set_camera_view_up_keyframes(self, keyframes): - """Set multiple camera view up direction keyframes. - Should be in the following form: - {timestamp_1: view_up_1, timestamp_2: view_up_2, ...} - - Parameters - ---------- - keyframes: dict - A dict with timestamps as keys and camera view up vectors as - values. - - Examples - -------- - >>> view_ups = {0, np.array([1, 0, 0]), 3, np.array([0, 1, 0])} - >>> Animation.set_camera_view_up_keyframes(view_ups) - """ - self.set_camera_keyframes('view_up', keyframes) - - def get_camera_position(self, t): - """Return the interpolated camera position. - - Parameters - ---------- - t: float - The time to interpolate camera position value at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera position. - - Notes - ----- - The returned position does not necessarily reflect the current camera - position, but te expected one. - """ - return self.get_camera_value('position', t) - - def get_camera_focal(self, t): - """Return the interpolated camera's focal position. - - Parameters - ---------- - t: float - The time to interpolate at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera's focal position. - - Notes - ----- - The returned focal position does not necessarily reflect the current - camera's focal position, but the expected one. - """ - return self.get_camera_value('focal', t) - - def get_camera_view_up(self, t): - """Return the interpolated camera's view-up directional vector. - - Parameters - ---------- - t: float - The time to interpolate at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera view-up directional vector. - - Notes - ----- - The returned focal position does not necessarily reflect the actual - camera view up directional vector, but the expected one. - """ - return self.get_camera_value('view_up', t) - - def get_camera_rotation(self, t): - """Return the interpolated rotation for the camera expressed - in euler angles. - - Parameters - ---------- - t: float - The time to interpolate at. - - Returns - ------- - ndarray(1, 3): - The interpolated camera's rotation. - - Notes - ----- - The returned focal position does not necessarily reflect the actual - camera view up directional vector, but the expected one. - """ - return self.get_camera_value('rotation', t) - def add(self, item): """Add an item to the Animation. This item can be an Actor, Animation, list of Actors, or a list of @@ -1374,41 +1046,6 @@ def update_animation(self, time=None): else: self._transform.Identity() - if self._camera is not None: - if self.is_interpolatable('rotation', is_camera=True): - pos = self._camera.GetPosition() - translation = np.identity(4) - translation[:3, 3] = pos - # camera axis is reverted - rot = -self.get_camera_rotation(time) - rot = transform.Rotation.from_quat(rot).as_matrix() - rot = np.array([[*rot[0], 0], - [*rot[1], 0], - [*rot[2], 0], - [0, 0, 0, 1]]) - rot = translation @ rot @ np.linalg.inv(translation) - self._camera.SetModelTransformMatrix(rot.flatten()) - - if self.is_interpolatable('position', is_camera=True): - cam_pos = self.get_camera_position(time) - self._camera.SetPosition(cam_pos) - - if self.is_interpolatable('focal', is_camera=True): - cam_foc = self.get_camera_focal(time) - self._camera.SetFocalPoint(cam_foc) - - if self.is_interpolatable('view_up', is_camera=True): - cam_up = self.get_camera_view_up(time) - self._camera.SetViewUp(cam_up) - elif not self.is_interpolatable('view_up', is_camera=True): - # to preserve up-view as default after user interaction - self._camera.SetViewUp(0, 1, 0) - - elif self._is_camera_animated and self._scene: - self._camera = self._scene.camera() - self.update_animation(time) - return - # actors properties if in_scene: if self.is_interpolatable('position'): @@ -1464,3 +1101,178 @@ def add_to_scene(self, ren): self._added_to_scene = True self._start_time = perf_counter() self.update_animation(0) + + +class CameraAnimation(Animation): + def __init__(self, camera=None, length=None, loop=True, + motion_path_res=None): + super(CameraAnimation, self).__init__(length=length, loop=loop, + motion_path_res=motion_path_res) + self._camera = camera + + def set_focal(self, timestamp, position, **kwargs): + """Set camera's focal position keyframe. + + Parameters + ---------- + timestamp: float + The time to interpolate opacity at. + position: ndarray, shape(1, 3) + The camera position + """ + self.set_keyframe('focal', timestamp, position, **kwargs) + + def set_view_up(self, timestamp, direction, **kwargs): + """Set the camera view-up direction keyframe. + + Parameters + ---------- + timestamp: float + The time to interpolate at. + direction: ndarray, shape(1, 3) + The camera view-up direction + """ + self.set_keyframe('view_up', timestamp, direction, **kwargs) + + def set_focal_keyframes(self, keyframes): + """Set multiple camera focal position keyframes at once. + Should be in the following form: + {timestamp_1: focal_1, timestamp_2: focal_1, ...} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and camera focal positions as + values. + + Examples + -------- + >>> focal_pos = {0, np.array([1, 1, 1]), 3, np.array([20, 0, 0])} + >>> CameraAnimation.set_focal_keyframes(focal_pos) + """ + self.set_keyframes('focal', keyframes) + + def set_view_up_keyframes(self, keyframes): + """Set multiple camera view up direction keyframes. + Should be in the following form: + {timestamp_1: view_up_1, timestamp_2: view_up_2, ...} + + Parameters + ---------- + keyframes: dict + A dict with timestamps as keys and camera view up vectors as + values. + + Examples + -------- + >>> view_ups = {0, np.array([1, 0, 0]), 3, np.array([0, 1, 0])} + >>> CameraAnimation.set_view_up_keyframes(view_ups) + """ + self.set_keyframes('view_up', keyframes) + + def get_focal(self, t): + """Return the interpolated camera's focal position. + + Parameters + ---------- + t: float + The time to interpolate at. + + Returns + ------- + ndarray(1, 3): + The interpolated camera's focal position. + + Notes + ----- + The returned focal position does not necessarily reflect the current + camera's focal position, but the expected one. + """ + return self.get_value('focal', t) + + def get_view_up(self, t): + """Return the interpolated camera's view-up directional vector. + + Parameters + ---------- + t: float + The time to interpolate at. + + Returns + ------- + ndarray(1, 3): + The interpolated camera view-up directional vector. + + Notes + ----- + The returned focal position does not necessarily reflect the actual + camera view up directional vector, but the expected one. + """ + return self.get_value('view_up', t) + + def set_focal_interpolator(self, interpolator, is_evaluator=False): + """Set the camera focal position interpolator. + + Parameters + ---------- + interpolator: callable + The generator function of the interpolator that would handle the + interpolation of the camera focal position keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + """ + self.set_interpolator("focal", interpolator, is_evaluator=is_evaluator) + + def set_view_up_interpolator(self, interpolator, is_evaluator=False): + """Set the camera up-view vector animation interpolator. + + Parameters + ---------- + interpolator: callable + The generator function of the interpolator that would handle the + interpolation of the camera view-up keyframes. + is_evaluator: bool, optional + Specifies whether the `interpolator` is time-only based evaluation + function that does not depend on keyframes. + """ + self.set_interpolator("view_up", interpolator, is_evaluator=is_evaluator) + + def update_animation(self, time=None): + if self._camera is None: + if self._scene: + self._camera = self._scene.camera() + self.update_animation(time) + return + else: + if self.is_interpolatable('rotation'): + pos = self._camera.GetPosition() + translation = np.identity(4) + translation[:3, 3] = pos + # camera axis is reverted + rot = -self.get_rotation(time) + rot = transform.Rotation.from_quat(rot).as_matrix() + rot = np.array([[*rot[0], 0], + [*rot[1], 0], + [*rot[2], 0], + [0, 0, 0, 1]]) + rot = translation @ rot @ np.linalg.inv(translation) + self._camera.SetModelTransformMatrix(rot.flatten()) + + if self.is_interpolatable('position'): + cam_pos = self.get_position(time) + self._camera.SetPosition(cam_pos) + + if self.is_interpolatable('focal'): + cam_foc = self.get_focal(time) + self._camera.SetFocalPoint(cam_foc) + + if self.is_interpolatable('view_up'): + cam_up = self.get_view_up(time) + self._camera.SetViewUp(cam_up) + elif not self.is_interpolatable('view_up'): + # to preserve up-view as default after user interaction + self._camera.SetViewUp(0, 1, 0) + if self._scene: + self._scene.reset_clipping_range() + From 0a73f7ebde84da7f5b613a0fcd23114bef615ee2 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 5 Oct 2022 17:14:51 +0200 Subject: [PATCH 51/62] Updated docs --- fury/animation/animation.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 0146291cd..2e2380526 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -41,13 +41,11 @@ def __init__(self, actors=None, length=None, loop=True, super().__init__() self._data = defaultdict(dict) - self._camera_data = defaultdict(dict) self._animations = [] self._actors = [] self._static_actors = [] self._timeline = None self._parent_animation = None - self._camera = None self._scene = None self._start_time = 0 self._length = length @@ -1024,7 +1022,8 @@ def update_animation(self, time=None): Parameters ---------- time: float or int, optional, default: None - The time to update animation at. + The time to update animation at. If None, the animation will play + without adding it to a Timeline. """ need_reset_clipping = False if time is None: @@ -1239,11 +1238,19 @@ def set_view_up_interpolator(self, interpolator, is_evaluator=False): self.set_interpolator("view_up", interpolator, is_evaluator=is_evaluator) def update_animation(self, time=None): + """Update the camera animation. + + Parameters + ---------- + time: float or int, optional, default: None + The time to update the camera animation at. If None, the animation + will play. + """ if self._camera is None: if self._scene: self._camera = self._scene.camera() self.update_animation(time) - return + return else: if self.is_interpolatable('rotation'): pos = self._camera.GetPosition() @@ -1275,4 +1282,3 @@ def update_animation(self, time=None): self._camera.SetViewUp(0, 1, 0) if self._scene: self._scene.reset_clipping_range() - From 1d0c673e0654a40a0a5a6f555699806c4bb2dbbe Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 5 Oct 2022 18:43:03 +0200 Subject: [PATCH 52/62] Added 'camera' property and some docs --- fury/animation/__init__.py | 2 +- fury/animation/animation.py | 47 ++++++++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/fury/animation/__init__.py b/fury/animation/__init__.py index f4df5c808..33129d5b4 100644 --- a/fury/animation/__init__.py +++ b/fury/animation/__init__.py @@ -1,2 +1,2 @@ -from fury.animation.animation import Animation +from fury.animation.animation import Animation, CameraAnimation from fury.animation.timeline import Timeline diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 2e2380526..0a4371d30 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -5,8 +5,7 @@ from scipy.spatial import transform from fury.actor import line from fury import utils -from fury.actor import Container -from fury.lib import Actor, Transform +from fury.lib import Actor, Transform, Camera from fury.animation.interpolator import spline_interpolator, \ step_interpolator, linear_interpolator, slerp @@ -231,7 +230,7 @@ def set_keyframe(self, attrib, timestamp, value, update_interpolator=True, self._timeline.update_duration() else: self.update_duration() - self.update_animation() + self.update_animation(0) self.update_motion_path() def set_keyframes(self, attrib, keyframes): @@ -1103,12 +1102,54 @@ def add_to_scene(self, ren): class CameraAnimation(Animation): + """Camera keyframe animation class. + + This is used for animating a single camera using a set of keyframes. + + Attributes + ---------- + camera : Camera, optional, default: None + Camera to be animated. If None, active camera will be animated. + length : float or int, default: None, optional + the fixed length of the animation. If set to None, the animation will + get its duration from the keyframes being set. + loop : bool, optional, default: True + Whether to loop the animation (True) of play once (False). + motion_path_res : int, default: None + the number of line segments used to visualizer the animation's motion + path (visualizing position). + """ def __init__(self, camera=None, length=None, loop=True, motion_path_res=None): super(CameraAnimation, self).__init__(length=length, loop=loop, motion_path_res=motion_path_res) self._camera = camera + @property + def camera(self) -> Camera: + """Return the camera assigned to this animation. + + Returns + ------- + Camera: + The camera that is being animated by this CameraAnimation. + + """ + return self._camera + + @camera.setter + def camera(self, camera: Camera): + """Set a camera to be animated. + + Parameters + ---------- + + camera: Camera + The camera to be animated + + """ + self._camera = camera + def set_focal(self, timestamp, position, **kwargs): """Set camera's focal position keyframe. From 1c32b2dd614e6d58eeabf586ba15ada4772d52f3 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 5 Oct 2022 19:00:19 +0200 Subject: [PATCH 53/62] Added tests for CameraAnimation --- fury/animation/animation.py | 2 +- fury/animation/tests/test_animation.py | 36 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index 0a4371d30..c5e0bbf7b 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1298,7 +1298,7 @@ def update_animation(self, time=None): translation = np.identity(4) translation[:3, 3] = pos # camera axis is reverted - rot = -self.get_rotation(time) + rot = -self.get_rotation(time, as_quat=True) rot = transform.Rotation.from_quat(rot).as_matrix() rot = np.array([[*rot[0], 0], [*rot[1], 0], diff --git a/fury/animation/tests/test_animation.py b/fury/animation/tests/test_animation.py index e5358dca3..c27a41d6d 100644 --- a/fury/animation/tests/test_animation.py +++ b/fury/animation/tests/test_animation.py @@ -4,7 +4,8 @@ from fury.animation.interpolator import linear_interpolator, \ step_interpolator, cubic_spline_interpolator, cubic_bezier_interpolator, \ spline_interpolator -from fury.animation import Animation +from fury.animation import Animation, CameraAnimation +from fury.lib import Camera def assert_not_equal(x, y): @@ -82,3 +83,36 @@ def test_animation(): transform.GetScale()) npt.assert_almost_equal(anim.get_rotation(0), transform.GetOrientation()) + + +def test_camera_animation(): + cam = Camera() + anim = CameraAnimation(cam) + + assert anim.camera is cam + + anim.set_position(0, [1, 2, 3]) + anim.set_position(3, [3, 2, 1]) + + anim.set_focal(0, [10, 20, 30]) + anim.set_focal(3, [30, 20, 10]) + + anim.set_rotation(0, np.array([180, 0, 0])) + + anim.update_animation(0) + npt.assert_almost_equal(cam.GetPosition(), np.array([1, 2, 3])) + npt.assert_almost_equal(cam.GetFocalPoint(), np.array([10, 20, 30])) + anim.update_animation(3) + npt.assert_almost_equal(cam.GetPosition(), np.array([3, 2, 1])) + npt.assert_almost_equal(cam.GetFocalPoint(), np.array([30, 20, 10])) + anim.update_animation(1.5) + npt.assert_almost_equal(cam.GetPosition(), np.array([2, 2, 2])) + npt.assert_almost_equal(cam.GetFocalPoint(), np.array([20, 20, 20])) + rot = np.zeros(16) + matrix = cam.GetModelTransformMatrix() + matrix.DeepCopy(rot.ravel(), matrix) + expected = np.array([[1, 0, 0, 0], + [0, -1, 0, 4], + [0, 0, -1, 2], + [0, 0, 0, 1]]) + npt.assert_almost_equal(expected, rot.reshape([4, 4])) From 0617d872f3b1f329206828134e7da119aeeeaf29 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Wed, 5 Oct 2022 19:02:01 +0200 Subject: [PATCH 54/62] renaming `update_timeline` to just `update` --- fury/animation/timeline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index e73f74a35..36fa7d380 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -102,13 +102,13 @@ def stop(self): """Stop the animation""" self._current_timestamp = 0 self._playing = False - self.update_timeline(force=True) + self.update(force=True) def restart(self): """Restart the animation""" self._current_timestamp = 0 self._playing = True - self.update_timeline(force=True) + self.update(force=True) @property def current_timestamp(self): @@ -156,7 +156,7 @@ def seek(self, timestamp): perf_counter() - timestamp / self.speed else: self._current_timestamp = timestamp - self.update_timeline(force=True) + self.update(force=True) def seek_percent(self, percent): """Seek a percentage of the Timeline's final timestamp. @@ -296,7 +296,7 @@ def animations(self) -> 'list[Animation]': """ return self._animations - def update_timeline(self, force=False): + def update(self, force=False): """Update the timeline. Update the Timeline and all the animations that it controls. As well as From 8197396df6edfcd058f42ac41498cc93c3ba7576 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 7 Oct 2022 15:22:40 +0200 Subject: [PATCH 55/62] `update_timeline` renamed --- fury/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fury/window.py b/fury/window.py index 119aabc4d..a1814c651 100644 --- a/fury/window.py +++ b/fury/window.py @@ -455,7 +455,7 @@ def add_animation(self, animation): return def animation_cbk(_obj, _event): - [tl.update_timeline() for tl in self._timelines] + [tl.update() for tl in self._timelines] [anim.update_animation() for anim in self._animations] self.render() self._animation_callback = self.add_timer_callback(True, 10, From cee57234fee297cc43c3f176650b933031437e43 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 7 Oct 2022 18:33:28 +0200 Subject: [PATCH 56/62] Animation loop adjustment --- fury/animation/animation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index c5e0bbf7b..efbe18380 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -1024,10 +1024,10 @@ def update_animation(self, time=None): The time to update animation at. If None, the animation will play without adding it to a Timeline. """ - need_reset_clipping = False + has_handler = True if time is None: time = perf_counter() - self._start_time - need_reset_clipping = True + has_handler = False # handling in/out of scene events in_scene = self.is_inside_scene_at(time) @@ -1035,8 +1035,7 @@ def update_animation(self, time=None): if self.duration: if self._loop and time > self.duration: - time = 0 - self._start_time = perf_counter() + time = time % self.duration elif time > self.duration: time = self.duration if isinstance(self._parent_animation, Animation): @@ -1084,7 +1083,7 @@ def update_animation(self, time=None): # Also update all child Animations. [animation.update_animation(time) for animation in self._animations] - if self._scene and need_reset_clipping: + if self._scene and not has_handler: self._scene.reset_clipping_range() def add_to_scene(self, ren): From 0fb1880b01f4b33359e979e6f2126cdc64b3d3f6 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 7 Oct 2022 21:23:16 +0200 Subject: [PATCH 57/62] Added the ability to get current timestamp of Animation No matter if it was set automatically of by a Timeline --- fury/animation/animation.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index efbe18380..a88a2ecc1 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -50,6 +50,7 @@ def __init__(self, actors=None, length=None, loop=True, self._length = length self._duration = length if length else 0 self._loop = loop + self._current_timestamp = 0 self._max_timestamp = 0 self._added_to_scene = True self._motion_path_res = motion_path_res @@ -88,6 +89,17 @@ def duration(self): """ return self._duration + @property + def current_timestamp(self): + """Return the current time of the animation. + + Returns + ------- + float + The current time of the animation. + """ + return self._current_timestamp + def update_motion_path(self): """Update motion path visualization actor""" res = self._motion_path_res @@ -1043,6 +1055,8 @@ def update_animation(self, time=None): else: self._transform.Identity() + self._current_timestamp = time + # actors properties if in_scene: if self.is_interpolatable('position'): @@ -1275,7 +1289,8 @@ def set_view_up_interpolator(self, interpolator, is_evaluator=False): Specifies whether the `interpolator` is time-only based evaluation function that does not depend on keyframes. """ - self.set_interpolator("view_up", interpolator, is_evaluator=is_evaluator) + self.set_interpolator("view_up", interpolator, + is_evaluator=is_evaluator) def update_animation(self, time=None): """Update the camera animation. From 331e75552c2eaa58727fe7c2d5f581e5b6ae3bd9 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 7 Oct 2022 21:33:20 +0200 Subject: [PATCH 58/62] Added support for general callbacks --- fury/animation/animation.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/fury/animation/animation.py b/fury/animation/animation.py index a88a2ecc1..7b1eaa443 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -56,7 +56,7 @@ def __init__(self, actors=None, length=None, loop=True, self._motion_path_res = motion_path_res self._motion_path_actor = None self._transform = Transform() - + self._general_callbacks = [] # Adding actors to the animation if actors is not None: self.add_actor(actors) @@ -1008,7 +1008,7 @@ def loop(self, loop): """ self._loop = loop - def add_update_callback(self, prop, callback): + def add_update_callback(self, callback, prop=None): """Add a function to be called each time animation is updated This function must accept only one argument which is the current value of the named property. @@ -1016,11 +1016,19 @@ def add_update_callback(self, prop, callback): Parameters ---------- - prop: str - The name of the property. - callback: function + callback: callable The function to be called whenever the animation is updated. + prop: str, optional, default: None + The name of the property. + + Notes + ----- + If no attribute name was provided, current time of the animation will + be provided instead of current value for the callback. """ + if prop is None: + self._general_callbacks.append(callback) + return attrib = self._get_attribute_data(prop) attrib.get('callbacks', []).append(callback) @@ -1094,6 +1102,9 @@ def update_animation(self, time=None): value = self.get_value(attrib, time) [cbk(value) for cbk in callbacks] + # Executing general callbacks that's not related to any attribute + [callback(time) for callback in self._general_callbacks] + # Also update all child Animations. [animation.update_animation(time) for animation in self._animations] From d65059e69dc563e3a4a37c39bcf8e73a4631b6db Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 7 Oct 2022 22:02:47 +0200 Subject: [PATCH 59/62] updated animation tutorials --- .../05_animation/viz_bezier_interpolator.py | 70 ++++++-------- docs/tutorials/05_animation/viz_camera.py | 93 ++++++++----------- .../05_animation/viz_color_interpolators.py | 79 ++++++++-------- .../05_animation/viz_custom_interpolator.py | 35 +++---- .../05_animation/viz_interpolators.py | 60 ++++-------- .../05_animation/viz_introduction.py | 75 +++++++++------ .../05_animation/viz_robot_arm_animation.py | 27 ++---- .../05_animation/viz_spline_interpolator.py | 53 +++++------ docs/tutorials/05_animation/viz_timeline.py | 77 +++++++-------- .../05_animation/viz_using_time_equations.py | 35 +++---- 10 files changed, 267 insertions(+), 337 deletions(-) diff --git a/docs/tutorials/05_animation/viz_bezier_interpolator.py b/docs/tutorials/05_animation/viz_bezier_interpolator.py index 9b8364361..fdb700d4c 100644 --- a/docs/tutorials/05_animation/viz_bezier_interpolator.py +++ b/docs/tutorials/05_animation/viz_bezier_interpolator.py @@ -1,14 +1,14 @@ """ -===================== -Keyframe animation -===================== +=================== +Bezier Interpolator +=================== Keyframe animation using cubic Bezier interpolator. """ import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Animation, Timeline from fury.animation.interpolator import cubic_bezier_interpolator ############################################################################### @@ -64,10 +64,10 @@ colors=np.array([0, 1, 0])) ############################################################################### -# Initializing a ``Timeline`` and adding sphere actor to it. -timeline = Timeline(playback_panel=True) +# Initializing an ``Animation`` and adding sphere actor to it. +animation = Animation() sphere = actor.sphere(np.array([[0, 0, 0]]), (1, 0, 1)) -timeline.add_actor(sphere) +animation.add_actor(sphere) ############################################################################### # Setting Cubic Bezier keyframes @@ -82,31 +82,22 @@ # Note: If a control point is not provided or set `None`, this control point # will be the same as the position itself. -timeline.set_position(0.0, np.array(keyframe_1.get('value')), - out_cp=np.array(keyframe_1.get('out_cp'))) -timeline.set_position(5.0, np.array(keyframe_2.get('value')), - in_cp=np.array(keyframe_2.get('in_cp'))) +animation.set_position(0.0, np.array(keyframe_1.get('value')), + out_cp=np.array(keyframe_1.get('out_cp'))) +animation.set_position(5.0, np.array(keyframe_2.get('value')), + in_cp=np.array(keyframe_2.get('in_cp'))) ############################################################################### -# changing position interpolation into cubic bezier interpolation -timeline.set_position_interpolator(cubic_bezier_interpolator) +# Changing position interpolation into cubic bezier interpolation +animation.set_position_interpolator(cubic_bezier_interpolator) ############################################################################### -# adding the timeline and the static actors to the scene. +# Adding the visualization actors to the scene. scene.add(pts_actor, cps_actor, cline_actor) -scene.add(timeline) - - -############################################################################### -# making a function to update the animation -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - ############################################################################### -# Adding the callback function that updates the animation -showm.add_timer_callback(True, 10, timer_callback) +# Adding the animation to the ``ShowManager`` +showm.add_animation(animation) interactive = False @@ -138,18 +129,20 @@ def timer_callback(_obj, _event): } ############################################################################### -# Initializing the timeline -timeline = Timeline(playback_panel=True) +# Creat the sphere actor. sphere = actor.sphere(np.array([[0, 0, 0]]), (1, 0, 1)) -timeline.add_actor(sphere) + +############################################################################### +# Creat an ``Animation`` and adding the sphere actor to it. +animation = Animation(sphere) ############################################################################### # Setting Cubic Bezier keyframes -timeline.set_position_keyframes(keyframes) +animation.set_position_keyframes(keyframes) ############################################################################### # changing position interpolation into cubic bezier interpolation -timeline.set_position_interpolator(cubic_bezier_interpolator) +animation.set_position_interpolator(cubic_bezier_interpolator) ########################################################################### # visualizing the points and control points (only for demonstration) @@ -174,21 +167,16 @@ def timer_callback(_obj, _event): scene.add(vis_cps, cline_actor) ############################################################################### -# adding actors to the scene -scene.add(timeline) - +# Initializing the timeline to be able to control the playback of the +# animation. +timeline = Timeline(animation, playback_panel=True) ############################################################################### -# making a function to update the animation -def timer_callback(_obj, _event): - timeline.update_animation() - show_manager.render() - +# We only need to add the ``Timeline`` to the ``ShowManager`` +show_manager.add_animation(timeline) ############################################################################### -# Adding the callback function that updates the animation -show_manager.add_timer_callback(True, 10, timer_callback) - +# Start the animation if interactive: show_manager.start() diff --git a/docs/tutorials/05_animation/viz_camera.py b/docs/tutorials/05_animation/viz_camera.py index 54dafb04f..2a43f3561 100644 --- a/docs/tutorials/05_animation/viz_camera.py +++ b/docs/tutorials/05_animation/viz_camera.py @@ -8,7 +8,7 @@ import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Timeline, Animation, CameraAnimation from fury.animation.interpolator import cubic_spline_interpolator ############################################################################### @@ -29,20 +29,21 @@ # Creating the main ``Timeline`` and adding static actors to it # ============================================================= # -# Here we create a ``Timeline``. which we will call ``main_timeline`` so that -# we can use it as a controller for the other 50 Timelines. -# So, Instead of updating and adding 50 timelines to the ``scene``, we only -# need to update the main ``Timeline``. Also, a playback panel can be assigned -# to this main Timeline. -# But, why we need 50 ``Timelines``, you may ask. -# -> A single ``Timeline`` can handle each property once at a time. So we need -# 50 ``Timelines`` to translate and scale our 50 spheres. +# Here we create a ``Timeline``. so that we can use it as a controller for the +# 50 animations we will create. +# So, Instead of updating and adding 50 Animations to the ``ShowManager``, +# we only need to update the main ``Timeline``. Also, a playback panel can be +# assigned to this main Timeline. +# +# But, why we need 50 ``Animations``, you may ask. +# -> A single ``Animation`` can handle each property once at a time. So we need +# 50 ``Animations`` to translate and scale our 50 spheres. ############################################################################### # ``playback_panel=True`` assigns a playback panel that can control the -# playback of this ``main_timeline`` and all of its children ``Timelines`` +# playback of its ``Animations`` -main_timeline = Timeline(playback_panel=True) +timeline = Timeline(playback_panel=True) ############################################################################### # Creating two actors for visualization, and to detect camera's animations. @@ -51,13 +52,6 @@ plan = actor.box(np.array([[0, 0, 0]]), colors=np.array([[1, 1, 1]]), scales=np.array([[20, 0.2, 20]])) -############################################################################### -# adding static actors to the timeline. -# Note: adding actors as static actors just ensures that they get added to the -# scene along with the Timeline and will not be controlled nor animated by the -# timeline. -main_timeline.add_static_actor([arrow, plan]) - ############################################################################### # Creating "FURY" text # ==================== @@ -66,18 +60,18 @@ scale=(2, 2, 2)) ############################################################################### -# Creating a ``Timeline`` to animate the opacity of ``fury_text`` -text_timeline = Timeline(fury_text) +# Creating an ``Animation`` to animate the opacity of ``fury_text`` +text_anim = Animation(fury_text, loop=False) ############################################################################### # opacity is set to 0 at time 28 and set to one at time 31. # Linear interpolator is always used by default. -text_timeline.set_opacity(29, 0) -text_timeline.set_opacity(35, 1) +text_anim.set_opacity(29, 0) +text_anim.set_opacity(35, 1) ############################################################################### -# ``text_timeline`` contains the text actor is added to the main Timeline. -main_timeline.add_child_animation(text_timeline) +# ``text_anim`` contains the text actor is added to the Timeline. +timeline.add_animation(text_anim) ############################################################################### # Creating and animating 50 Spheres @@ -95,7 +89,7 @@ ########################################################################### # create a timeline to animate this actor (single actor or list of actors) # Actors can be added later using `Timeline.add_actor(actor)` - timeline = Timeline(actors) + animation = Animation(actors) # We generate random position and scale values from time=0 to time=49 each # two seconds. @@ -103,29 +97,33 @@ ####################################################################### # Position and scale are set to a random value at the timestamps # mentioned above. - timeline.set_position(t, - np.random.random(3) * 30 - np.array([15, 0, 15])) - timeline.set_scale(t, np.repeat(np.random.random(1), 3)) + animation.set_position(t, + np.random.random(3) * 30 - np.array( + [15, 0, 15])) + animation.set_scale(t, np.repeat(np.random.random(1), 3)) ########################################################################### # change the position interpolator to cubic spline interpolator. - timeline.set_position_interpolator(cubic_spline_interpolator) + animation.set_position_interpolator(cubic_spline_interpolator) ########################################################################### - # Finally, the ``Timeline`` is added to the ``main_timeline``. - main_timeline.add_child_animation(timeline) + # Finally, the ``Animation`` is added to the ``Timeline``. + timeline.add_animation(animation) ############################################################################### # Animating the camera # ==================== # # Since, only one camera is needed, camera animations are preferably done using -# the main `Timeline`. Three properties can control the camera's animation: +# a seperate ``Animation``. +# Three properties can control the camera's animation: # Position, focal position (referred to by `focal`), and up-view. +camera_anim = CameraAnimation(loop=False) +timeline.add_animation(camera_anim) + ############################################################################### # Multiple keyframes can be set at once as follows. - # camera focal positions camera_positions = { # time: camera position @@ -151,35 +149,24 @@ ############################################################################### # ``set_camera_focal`` can only set one keyframeB , but # ``set_camera_focal_keyframes`` can set a dictionary of keyframes. -main_timeline.set_camera_focal_keyframes(camera_focal_positions) -main_timeline.set_camera_position_keyframes(camera_positions) +camera_anim.set_focal_keyframes(camera_focal_positions) +camera_anim.set_position_keyframes(camera_positions) ############################################################################### # Change camera position and focal interpolators -main_timeline.set_camera_position_interpolator(cubic_spline_interpolator) -main_timeline.set_camera_focal_interpolator(cubic_spline_interpolator) +camera_anim.set_position_interpolator(cubic_spline_interpolator) +camera_anim.set_focal_interpolator(cubic_spline_interpolator) ############################################################################### -# Only the main Timeline is added to the scene. -scene.add(main_timeline) - +# Adding non-animatable actors to the scene. +scene.add(arrow, plan) ############################################################################### -# making a function to update the animation -def timer_callback(_obj, _event): - ########################################################################### - # Only the main timeline is needed to be updated, and it would update all - # children ``Timelines``. - main_timeline.update_animation() - - ########################################################################### - # The scene is rendered after the animations are updated. - showm.render() - +# Adding the timeline to the ShowManager. +showm.add_animation(timeline) ############################################################################### -# Adding the callback function that updates the animation -showm.add_timer_callback(True, 10, timer_callback) +# The ShowManager must go on! interactive = False diff --git a/docs/tutorials/05_animation/viz_color_interpolators.py b/docs/tutorials/05_animation/viz_color_interpolators.py index 84775897d..cf66a14b0 100644 --- a/docs/tutorials/05_animation/viz_color_interpolators.py +++ b/docs/tutorials/05_animation/viz_color_interpolators.py @@ -10,9 +10,11 @@ import numpy as np from fury import actor, window +from fury.animation import Animation from fury.animation.timeline import Timeline from fury.animation.interpolator import step_interpolator, \ lab_color_interpolator, hsv_color_interpolator, xyz_color_interpolator +from fury.colormap import distinguishable_colormap scene = window.Scene() @@ -40,61 +42,62 @@ step_text = actor.vector_text("Step", (5.7, -1, 0)) scene.add(step_text, lab_text, linear_text, hsv_text, xyz_text) -############################################################################### -# Main timeline to control all the timelines (one for each color interpolation -# method) -main_timeline = Timeline(playback_panel=True) - ############################################################################### # Creating a timeline to animate the actor. # Also cube actor is provided for each timeline to handle as follows: # ``Timeline(actor)``, ``Timeline(list_of_actors)``, or actors can be added # later using ``Timeline.add()`` or ``timeline.add_actor()`` -timeline_linear_color = Timeline(actor.cube(cubes_pos[0])) -timeline_LAB_color = Timeline(actor.cube(cubes_pos[1])) -timeline_HSV_color = Timeline(actor.cube(cubes_pos[2])) -timeline_XYZ_color = Timeline(actor.cube(cubes_pos[3])) -timeline_step_color = Timeline(actor.cube(cubes_pos[4])) +anim_linear_color = Animation(actor.cube(cubes_pos[0])) +anim_LAB_color = Animation(actor.cube(cubes_pos[1])) +anim_HSV_color = Animation(actor.cube(cubes_pos[2])) +anim_XYZ_color = Animation(actor.cube(cubes_pos[3])) +anim_step_color = Animation(actor.cube(cubes_pos[4])) ############################################################################### -# Adding timelines to the main Timeline. -main_timeline.add_child_animation([timeline_linear_color, - timeline_LAB_color, - timeline_HSV_color, - timeline_XYZ_color, - timeline_step_color]) +# Creating a timeline to control all the animations (one for each color +# interpolation method) -############################################################################### -# Adding color keyframes to the linearly (for now) interpolated timelines -for t in range(0, 20, 5): - x = np.random.random(3) - timeline_linear_color.set_color(t, np.array(x)) - timeline_LAB_color.set_color(t, np.array(x)) - timeline_HSV_color.set_color(t, np.array(x)) - timeline_XYZ_color.set_color(t, np.array(x)) - timeline_step_color.set_color(t, np.array(x)) +timeline = Timeline(playback_panel=True) ############################################################################### -# Changing the default scale interpolator to be a step interpolator -# The default is linear interpolator for color keyframes -timeline_HSV_color.set_color_interpolator(hsv_color_interpolator) -timeline_LAB_color.set_color_interpolator(lab_color_interpolator) -timeline_step_color.set_color_interpolator(step_interpolator) -timeline_XYZ_color.set_color_interpolator(xyz_color_interpolator) +# Adding animations to a Timeline. +timeline.add_animation([anim_linear_color, + anim_LAB_color, + anim_HSV_color, + anim_XYZ_color, + anim_step_color]) ############################################################################### -# Adding the main timeline to the scene -scene.add(main_timeline) +# Setting color keyframes +# ======================= +# +# Setting the same color keyframes to all the animations +############################################################################### +# First, we generate some distinguishable colors +colors = distinguishable_colormap(nb_colors=4) ############################################################################### -# making a function to update the animation and render the scene -def timer_callback(_obj, _event): - main_timeline.update_animation() - showm.render() +# Then, we set them as keyframes for the animations +for t in range(0, 20, 5): + col = colors.pop() + anim_linear_color.set_color(t, col) + anim_LAB_color.set_color(t, col) + anim_HSV_color.set_color(t, col) + anim_XYZ_color.set_color(t, col) + anim_step_color.set_color(t, col) +############################################################################### +# Changing the default scale interpolator to be a step interpolator +# The default is linear interpolator for color keyframes +anim_HSV_color.set_color_interpolator(hsv_color_interpolator) +anim_LAB_color.set_color_interpolator(lab_color_interpolator) +anim_step_color.set_color_interpolator(step_interpolator) +anim_XYZ_color.set_color_interpolator(xyz_color_interpolator) -showm.add_timer_callback(True, 10, timer_callback) +############################################################################### +# Adding the main timeline to the show manager +showm.add_animation(timeline) interactive = False diff --git a/docs/tutorials/05_animation/viz_custom_interpolator.py b/docs/tutorials/05_animation/viz_custom_interpolator.py index 0ecd6d790..3bba22471 100644 --- a/docs/tutorials/05_animation/viz_custom_interpolator.py +++ b/docs/tutorials/05_animation/viz_custom_interpolator.py @@ -8,8 +8,8 @@ """ import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline -from fury.animation import helpers +from fury.animation import helpers, Animation + ############################################################################### # Implementing a custom interpolator @@ -90,8 +90,8 @@ def interpolate(t): # `keyframes.get(t0)`. This keyframe data contains `value` and any # other data set as a custom argument using keyframe setters. # for example: - # >>> timeline = Timeline() - # >>> timeline.set_position(0, np.array([1, 1, 1]), + # >>> animation = Animation() + # >>> animation.set_position(0, np.array([1, 1, 1]), # >>> custom_field=np.array([2, 3, 1])) # In this case `keyframes.get(0)` would return: # {'value': array(1, 1, 1), 'custom_field': array(2, 3, 1)} @@ -119,7 +119,7 @@ def interpolate(t): # Cubic spline keyframes data same as the one you get from glTF file. # =================================================================== -# t in tangent position out tangent +# t in tangent position out tangent translation = [[0.0, [0., 0., 0.], [3.3051798, 6.640117, 0.], [1., 0., 0.]], [1.0, [0., 0., 0.], [3.3051798, 8., 0.], [-1., 0., 0.]], [2.0, [-1., 0., 0.], [3.3051798, 6., 0.], [1., 0., 0.]], @@ -127,12 +127,12 @@ def interpolate(t): [4.0, [0, -1., 0.], [3.3051798, 6., 0.], [0., 0., 0.]]] ############################################################################### -# Initializing a ``Timeline`` and adding sphere actor to it. -timeline = Timeline(playback_panel=True, motion_path_res=100) +# Initializing an ``Animation`` and adding sphere actor to it. +animation = Animation(motion_path_res=100) sphere = actor.sphere(np.array([[0, 0, 0]]), (1, 0, 1), radii=0.1) -timeline.add_actor(sphere) +animation.add_actor(sphere) ############################################################################### # Setting position keyframes @@ -141,27 +141,16 @@ def interpolate(t): t, in_tan, pos, out_tan = keyframe_data # Since we used the name 'in_tangent' and 'out_tangent' in the interpolator # We must use the same name as an argument to set it in the keyframe data. - timeline.set_position(t, pos, in_tangent=in_tan, out_tangent=out_tan) + animation.set_position(t, pos, in_tangent=in_tan, out_tangent=out_tan) ############################################################################### # Set the new interpolator to interpolate position keyframes -timeline.set_position_interpolator(tan_cubic_spline_interpolator) - -############################################################################### -# adding the timeline and the static actors to the scene. -scene.add(timeline) - +animation.set_position_interpolator(tan_cubic_spline_interpolator) ############################################################################### -# making a function to update the animation -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - +# adding the animation to the show manager. +showm.add_animation(animation) -############################################################################### -# Adding the callback function that updates the animation -showm.add_timer_callback(True, 10, timer_callback) interactive = False diff --git a/docs/tutorials/05_animation/viz_interpolators.py b/docs/tutorials/05_animation/viz_interpolators.py index 292e8f58c..4ebd2c806 100644 --- a/docs/tutorials/05_animation/viz_interpolators.py +++ b/docs/tutorials/05_animation/viz_interpolators.py @@ -8,27 +8,17 @@ """ ############################################################################### -# What is a ``Timeline`` -# ====================== +# What is an ``Animation`` +# ======================== # -# ``Timeline`` is responsible for animating FURY actors using a set of +# ``Animation`` is responsible for animating FURY actors using a set of # keyframes by interpolating values between timestamps of these keyframes. -# ``Timeline`` has playback methods such as ``play``, ``pause``, ``stop``, ... import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Animation from fury.animation.interpolator import cubic_spline_interpolator -############################################################################### -# What are keyframes -# ================== -# -# A keyframe consists of a timestamp and some data. -# These data can be anything such as temperature, position, or scale. -# How to define Keyframes that FURY can work with? -# A simple dictionary object with timestamps as keys and data as values. - keyframes = { 1.0: {'value': np.array([0, 0, 0])}, 2.0: {'value': np.array([-4, 1, 0])}, @@ -81,26 +71,26 @@ arrow = actor.arrow(np.array([[0, 0, 0]]), (0, 0, 0), (1, 0, 1), scales=6) ############################################################################### -# Creating the ``Timeline`` -# ======================== +# Creating an ``Animation`` +# ========================= # -# First step is creating the Timeline. A playback panel should be -timeline = Timeline(playback_panel=True) +# First step is creating the Animation. +animation = Animation() ############################################################################### # Adding the sphere actor to the timeline # This could've been done during initialization. -timeline.add_actor(arrow) +animation.add_actor(arrow) ############################################################################### # Setting position keyframes # ========================== # # Adding some position keyframes -timeline.set_position(0.0, np.array([0, 0, 0])) -timeline.set_position(2.0, np.array([10, 10, 10])) -timeline.set_position(5.0, np.array([-10, -3, -6])) -timeline.set_position(9.0, np.array([10, 6, 20])) +animation.set_position(0.0, np.array([0, 0, 0])) +animation.set_position(2.0, np.array([10, 10, 10])) +animation.set_position(5.0, np.array([-10, -3, -6])) +animation.set_position(9.0, np.array([10, 6, 20])) ############################################################################### # Changing the default interpolator for a single property @@ -108,18 +98,18 @@ # # For all properties except **rotation**, linear interpolator is used by # default. In order to change the default interpolator and set another -# interpolator, call ``timeline.set__interpolator(new_interpolator)`` +# interpolator, call ``animation.set__interpolator(interpolator)`` # FURY already has some interpolators located at: # ``fury.animation.interpolator``. # # Below we set the interpolator for position keyframes to be # **cubic spline interpolator**. -timeline.set_position_interpolator(cubic_spline_interpolator) +animation.set_position_interpolator(cubic_spline_interpolator) ############################################################################### # Adding some rotation keyframes. -timeline.set_rotation(0.0, np.array([160, 50, 0])) -timeline.set_rotation(8.0, np.array([60, 160, 0])) +animation.set_rotation(0.0, np.array([160, 50, 0])) +animation.set_rotation(8.0, np.array([60, 160, 0])) ############################################################################### # For Rotation keyframes, Slerp is used as the default interpolator. @@ -133,21 +123,11 @@ scene.camera().SetPosition(0, 0, 90) ############################################################################### -# Adding main Timeline to the ``Scene``. -scene.add(timeline) - +# Adding main animation to the ``ShowManager``. +showm.add_animation(animation) ############################################################################### -# making a function to update the animation and render the scene. -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - - -############################################################################### -# Adding the callback function that updates the animation. -showm.add_timer_callback(True, 10, timer_callback) - +# Start the ``ShowManager`` to start playing the animation interactive = False if interactive: diff --git a/docs/tutorials/05_animation/viz_introduction.py b/docs/tutorials/05_animation/viz_introduction.py index 8a1b5e4e8..27fac5709 100644 --- a/docs/tutorials/05_animation/viz_introduction.py +++ b/docs/tutorials/05_animation/viz_introduction.py @@ -6,35 +6,53 @@ This tutorial explains keyframe animation in FURY. """ +############################################################################### +# Animations in FURY +# ================== +# +# FURY provides an easy-to-use animation system that enables users creating +# complex animations based on keyframes. +# The user only need to provide the attributes of actors at certain +# times (keyframes), and the system will take care of animating everything +# through interpolating between those keyframes. + + +############################################################################### +# What exactly is a keyframe +# ========================== +# +# A Keyframe is simply a marker of time which stores the value of a property. +# +# A keyframe consists of a timestamp and some data. These data can be anything +# such as temperature, position, or scale. ############################################################################### -# What is Keyframe Animation? -# =========================== +# What is Keyframe Animation +# ========================== # # A keyframe animation is the transition in a property setting in between two # keyframes. That change could be anything. # -# A Keyframe is simply a marker of time which stores the value of a property. +# Almost any parameter that you can set for FURY actors can be animated +# using keyframes. # # For example, a Keyframe might define that the position of a FURY actor is -# at (0, 0, 0) at time equals 1 second. +# (0, 0, 0) at time equals 1 second. # -# The purpose of a Keyframe is to allow for interpolated animation, meaning, +# The goal of a Keyframe is to allow for interpolated animation, meaning, # for example, that the user could then add another key at time equals 3 -# seconds, specifying the actor's position is at (1, 1, 0), +# seconds, specifying the actor's position is (1, 1, 0), # # Then the correct position of the actor for all the times between 3 and 10 # will be interpolated. # -# Almost any parameter that you can set for FURY actors can be animated -# using keyframes. -# # For this tutorial, we are going to use the FURY animation module to translate # FURY sphere actor. + import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Animation scene = window.Scene() @@ -49,39 +67,40 @@ # # This is a quick demo showing how to translate a sphere from (0, 0, 0) to # (1, 1, 1). -# First, we create a ``Timeline`` -timeline = Timeline(playback_panel=True) +# First, we create an ``Animation``. See ``viz_animation.py`` tutorial +animation = Animation() ############################################################################### -# Our FURY sphere actor +# We also create the FURY sphere actor that will be animated. sphere = actor.sphere(np.zeros([1, 3]), np.ones([1, 3])) ############################################################################### -# We add the sphere actor to the ``Timeline`` -timeline.add_actor(sphere) +# Then lets add the sphere actor to the ``Animation`` +animation.add_actor(sphere) ############################################################################### # Then, we set our position keyframes at different timestamps -timeline.set_position(1, np.array([0, 0, 0])) -timeline.set_position(3, np.array([1, 1, 0])) +# Here we want the sphere's position at the beginning to be [0, 0, 0]. And then +# at time equals 3 seconds to be at [1, 1, 0] then finally at the end +# (time equals 6) to return to the initial position which is [0, 0, 0] again. -############################################################################### -# The ``Timeline`` must be added to the ``Scene`` -scene.add(timeline) +animation.set_position(0.0, [-1, -1, 0]) +animation.set_position(3.0, [1, 1, 0]) +animation.set_position(6.0, [-1, -1, 0]) ############################################################################### -# No need to add the sphere actor, since it's now a part of the ``Timeline`` - +# The ``Animation`` must be added to the ``ShowManager`` as follows: +showm.add_animation(animation) +scene.camera().SetPosition(0, 0, 10) ############################################################################### -# Now we have to update the animation using a timer callback function +# Animation can be added to the scene instead of the ``ShowManager`` but, the +# animation will need to be updated and then render the scene manually. -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - -showm.add_timer_callback(True, 1, timer_callback) +############################################################################### +# No need to add the sphere actor to scene, since it's now a part of the +# ``Animation``. interactive = False diff --git a/docs/tutorials/05_animation/viz_robot_arm_animation.py b/docs/tutorials/05_animation/viz_robot_arm_animation.py index bc813b022..c8ecfa22d 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -8,8 +8,7 @@ """ import numpy as np from fury import actor, window -from fury.animation import Animation -from fury.animation.timeline import Timeline +from fury.animation import Animation, Timeline from fury.utils import set_actor_origin scene = window.Scene() @@ -19,6 +18,7 @@ order_transparent=True) showm.initialize() + ############################################################################### # Creating robot arm components @@ -37,10 +37,8 @@ np.array([[1, 0, 0]]), heights=2.2, resolution=6) ############################################################################### -# Setting the origin or rotation of both shafts to the beginning. -# Length of main arm is 12 so the beginning would be at -6 in x direction. +# Setting the center of both shafts to the beginning. set_actor_origin(main_arm, np.array([-6, 0, 0])) -# Length of the sub arm is 8 so the beginning would be at -4 in x direction. set_actor_origin(sub_arm, np.array([-4, 0, 0])) ############################################################################### @@ -53,9 +51,6 @@ child_arm_animation = Animation([sub_arm, joint_2]) drill_animation = Animation(end) -############################################################################### -# Adding the main animation to the Timeline. -timeline.add_child_animation(main_arm_animation) ############################################################################### # Adding other Animations in hierarchical order @@ -103,20 +98,16 @@ def rot_drill(t): scene.camera().SetPosition(0, 0, 90) ############################################################################### -# Adding timelines to the main Timeline. -scene.add(timeline, base) - +# Adding the base actor to the scene +scene.add(base) ############################################################################### -# making a function to update the animation and render the scene. -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - +# Adding the main parent animation to the Timeline. +timeline.add_animation(main_arm_animation) ############################################################################### -# Adding the callback function that updates the animation. -showm.add_timer_callback(True, 10, timer_callback) +# Now we add the timeline to the ShowManager +showm.add_animation(main_arm_animation) interactive = False diff --git a/docs/tutorials/05_animation/viz_spline_interpolator.py b/docs/tutorials/05_animation/viz_spline_interpolator.py index 10a05f064..05b492ad3 100644 --- a/docs/tutorials/05_animation/viz_spline_interpolator.py +++ b/docs/tutorials/05_animation/viz_spline_interpolator.py @@ -9,7 +9,7 @@ import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Animation, Timeline from fury.animation.interpolator import spline_interpolator scene = window.Scene() @@ -38,65 +38,56 @@ ############################################################################### # creating two timelines (one uses linear and the other uses' spline # interpolator), each timeline controls a sphere actor + sphere_linear = actor.sphere(np.array([[0, 0, 0]]), (1, 0.5, 0.2), 0.5) -linear_tl = Timeline() -linear_tl.add(sphere_linear) -linear_tl.set_position_keyframes(position_keyframes) +linear_anim = Animation() +linear_anim.add_actor(sphere_linear) + +linear_anim.set_position_keyframes(position_keyframes) ############################################################################### -# Note: linear_interpolator is used by default. So, no need to set it for the -# first (linear position) timeline. +# Note: linear_interpolator is used by default. So, no need to set it for this +# first animation that we need to linearly interpolate positional animation. ############################################################################### # creating a second timeline that translates another larger sphere actor using # spline interpolator. sphere_spline = actor.sphere(np.array([[0, 0, 0]]), (0.3, 0.9, 0.6), 1) -spline_tl = Timeline(sphere_spline) -spline_tl.set_position_keyframes(position_keyframes) +spline_anim = Animation(sphere_spline) +spline_anim.set_position_keyframes(position_keyframes) ############################################################################### # Setting 5th degree spline interpolator for position keyframes. -spline_tl.set_position_interpolator(spline_interpolator, degree=5) +spline_anim.set_position_interpolator(spline_interpolator, degree=5) ############################################################################### -# Wrapping Timelines up! +# Wrapping animations up! # ============================================================================= # -# Adding everything to a main ``Timeline`` to control the two timelines. +# Adding everything to a ``Timeline`` to control the two timelines. ############################################################################### # First we create a timeline with a playback panel: -main_timeline = Timeline(playback_panel=True, motion_path_res=100) +timeline = Timeline(playback_panel=True) ############################################################################### -# Add visualization dots actor to the timeline as a static actor. -main_timeline.add_static_actor(pos_dots) +# Add visualization dots actor to the scene. +scene.add(pos_dots) ############################################################################### -# Adding timelines to the main timeline (so that it controls their playback) -main_timeline.add([spline_tl, linear_tl]) +# Adding the animations to the timeline (so that it controls their playback). +timeline.add_animation([linear_anim, spline_anim]) ############################################################################### -# Adding the main timeline to the scene. -scene.add(main_timeline) - +# Adding the timeline to the show manager. +showm.add_animation(timeline) -############################################################################### -# Now that these two timelines are added to main_timeline, if main_timeline -# is played, paused, ..., all these changes will reflect on the children -# timelines. ############################################################################### -# making a function to update the animation and render the scene -def timer_callback(_obj, _event): - main_timeline.update_animation() - showm.render() - +# Now that these two animations are added to timeline, if the timeline +# is played, paused, ..., all these changes will reflect on the animations. -############################################################################### -# Adding the callback function that updates the animation -showm.add_timer_callback(True, 10, timer_callback) interactive = False diff --git a/docs/tutorials/05_animation/viz_timeline.py b/docs/tutorials/05_animation/viz_timeline.py index 559be6e4f..40cf881de 100644 --- a/docs/tutorials/05_animation/viz_timeline.py +++ b/docs/tutorials/05_animation/viz_timeline.py @@ -3,21 +3,31 @@ Timeline and setting keyframes ============================== -In his tutorial, you will learn how to use timeline to make simple keyframe -animations on FURY actors. +In his tutorial, you will learn how to use Fury ``Timeline`` for playing the +animations. """ ############################################################################### # What is ``Timeline``? # =================== -# ``Timeline`` is responsible for handling keyframe animation of FURY actors -# using a set of keyframes by interpolating values between timestamps of these -# keyframes. +# +# ``Timeline`` is responsible for handling the playback of Fury Animations. # # ``Timeline`` has playback methods such as ``play``, ``pause``, ``stop``, ... # which can be used to control the animation. -# + + +import numpy as np +from fury import actor, window +from fury.animation import Animation, Timeline + +############################################################################### +# We create our ``Scene`` and ``ShowManager`` as usual. +scene = window.Scene() + +showm = window.ShowManager(scene, size=(900, 768)) +showm.initialize() ############################################################################### # Creating a ``Timeline`` @@ -26,26 +36,18 @@ # FURY ``Timeline`` has the option to attaches a very useful panel for # controlling the animation by setting ``playback_panel=True``. -import numpy as np -from fury import actor, window -from fury.animation.timeline import Timeline - +############################################################################### +# Creating a ``Timeline`` with a PlaybackPanel. timeline = Timeline(playback_panel=True) ############################################################################### -# Adding Actors to ``Timeline`` -# ============================ -# -# FURY actors must be added to the ``Timeline`` in order to be animated. - +# Creating a Fury Animation an usual +anim = Animation() sphere = actor.sphere(np.zeros([1, 3]), np.ones([1, 3])) -timeline.add_actor(sphere) +anim.add_actor(sphere) +# Now that the actor is addd to the ``Animation``, setting keyframes to the +# Animation will animate the actor accordingly. -############################################################################### -# Main use of ``Timeline`` is to handle playback (play, stop, ...) of the -# keyframe animation. But it can be used to animate FURY actors as well. -# Now that the actor is addd to the timeline, setting keyframes to the timeline -# will animate the actor accordingly. ############################################################################### # Setting Keyframes @@ -53,46 +55,39 @@ # # There are multiple ways to set keyframes: # -# 1- To set a single keyframe, you may use ``timeline.set_(t, k)``, +# 1- To set a single keyframe, you may use ``animation.set_(t, k)``, # where is the name of the property to be set. I.e. setting position # to (1, 2, 3) at time 0.0 would be as following: -timeline.set_position(0.0, np.array([1, 2, 3])) +anim.set_position(0.0, np.array([1, 2, 3])) ############################################################################### # Supported properties are: **position, rotation, scale, color, and opacity**. # # 2- To set multiple keyframes at once, you may use -# ``timeline.set__keyframes(keyframes)``. +# ``animation.set__keyframes(keyframes)``. keyframes = { 1.0: np.array([0, 0, 0]), 3.0: np.array([-2, 0, 0]) } -timeline.set_position_keyframes(keyframes) +anim.set_position_keyframes(keyframes) ############################################################################### # That's it! Now we are done setting keyframes. -scene = window.Scene() - -showm = window.ShowManager(scene, size=(900, 768)) -showm.initialize() - -scene.add(timeline) - - ############################################################################### -# Now we have to update the ``Timeline`` animation then render the scene -# using a timer callback function. - -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() +# In order to control this animation by the timeline we created earlier, this +# animation must be added to the timeline. +timeline.add_animation(anim) +############################################################################### +# Now we add only the ``Timeline`` to the ``ShowManager`` the same way we add +# ``Animation`` to the ``ShowManager``. +showm.add_animation(timeline) -showm.add_timer_callback(True, 1, timer_callback) +scene.camera().SetPosition(0, 0, -10) -interactive = True +interactive = False if interactive: showm.start() diff --git a/docs/tutorials/05_animation/viz_using_time_equations.py b/docs/tutorials/05_animation/viz_using_time_equations.py index 3e63aa6ff..2f492775e 100644 --- a/docs/tutorials/05_animation/viz_using_time_equations.py +++ b/docs/tutorials/05_animation/viz_using_time_equations.py @@ -6,11 +6,9 @@ Tutorial on making keyframe-based animation in FURY using custom functions. """ -from cmath import sin, cos - import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Animation scene = window.Scene() @@ -22,13 +20,13 @@ cube = actor.cube(np.array([[0, 0, 0]]), (0, 0, 0), (1, 0, 1), scales=6) ############################################################################### -# Creating a timeline to animate the actor -timeline = Timeline(playback_panel=True, length=2 * np.pi, loop=True) +# Creating an ``Animation`` to animate the actor and show its motion path. +anim = Animation(length=2 * np.pi, loop=True, motion_path_res=200) ############################################################################### # Adding the sphere actor to the timeline # This could've been done during initialization. -timeline.add_actor(cube) +anim.add_actor(cube) ############################################################################### @@ -55,30 +53,19 @@ def scale_eval(t): # Setting evaluator functions is the same as setting interpolators, but with # one extra argument: `is_evaluator=True` since these functions does not need # keyframes as input. -timeline.set_position_interpolator(pos_eval, is_evaluator=True) -timeline.set_rotation_interpolator(rotation_eval, is_evaluator=True) -timeline.set_color_interpolator(color_eval, is_evaluator=True) -timeline.set_interpolator('scale', scale_eval, is_evaluator=True) +anim.set_position_interpolator(pos_eval, is_evaluator=True) +anim.set_rotation_interpolator(rotation_eval, is_evaluator=True) +anim.set_color_interpolator(color_eval, is_evaluator=True) +anim.set_interpolator('scale', scale_eval, is_evaluator=True) ############################################################################### -# Main timeline to control all the timelines. +# changing camera position to observe the animation better. scene.camera().SetPosition(0, 0, 90) ############################################################################### -# Adding timelines to the main Timeline. -scene.add(timeline) - - -############################################################################### -# making a function to update the animation and render the scene. -def timer_callback(_obj, _event): - timeline.update_animation() - showm.render() - +# Adding the animation to the show manager. +showm.add_animation(anim) -############################################################################### -# Adding the callback function that updates the animation. -showm.add_timer_callback(True, 10, timer_callback) interactive = False From 5b04d88da6f525c4ef76b825bb19b993faf7c00d Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Fri, 7 Oct 2022 22:03:11 +0200 Subject: [PATCH 60/62] Added hierarchical animation tutorial --- .../viz_hierarchical_animation.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/tutorials/05_animation/viz_hierarchical_animation.py diff --git a/docs/tutorials/05_animation/viz_hierarchical_animation.py b/docs/tutorials/05_animation/viz_hierarchical_animation.py new file mode 100644 index 000000000..6f62b6f0d --- /dev/null +++ b/docs/tutorials/05_animation/viz_hierarchical_animation.py @@ -0,0 +1,132 @@ +""" +=============================== +Keyframe hierarchical Animation +=============================== + +Creating hierarchical keyframes animation in fury + +""" +import numpy as np +from fury import actor, window +from fury.animation import Animation + +scene = window.Scene() + +showm = window.ShowManager(scene, + size=(900, 768), reset_camera=False, + order_transparent=True) +showm.initialize() + +############################################################################### +# Creating the road +road = actor.box(np.array([[0, 0, 0]]), colors=np.array([[1, 1, 1]]), + scales=np.array([[22, 0.1, 5]])) + +############################################################################### +# Constructing the car geometry + +body_actor = actor.box(np.array([[0, 0.5, 0], [-0.2, 1, 0]]), + scales=((4, 1, 2), (2.5, 1.5, 1.8)), + colors=(0.6, 0.3, 0.1)) + +############################################################################### +# Adding the the car's body to an Animation to be able to animate it later. +car_anim = Animation(body_actor) + +############################################################################### +# Creating the wheels of the car +wheel_center = np.array([[0, 0, 0]]) + +wheel_direction = np.array([[0, 0, 1]]) +wheel_positions = [ + [1.2, 0, 1.1], + [-1.2, 0, 1.1], + [1.2, 0, -1.1], + [-1.2, 0, -1.1], + +] + +wheels = [actor.cylinder(wheel_center, wheel_direction, (0.1, 0.7, 0.3), + radius=1.7, heights=0.3, resolution=10, capped=True) + for _ in range(4)] + +############################################################################### +# Animating each wheel and setting its position to the right position using a +# single keyframe that will not change. + +wheels_animations = [Animation(wheel) for wheel in wheels] + +for wheel_anim in wheels_animations: + wheel_anim.set_position(0.0, wheel_positions.pop()) + wheel_anim.set_rotation(0.0, [0, 0, 1, 1]) + wheel_anim.set_rotation(1.0, [0, 0, 1, -1]) + +############################################################################### +# Creating a radar on top of the car + +############################################################################### +# First we create the shaft holding and rotating the radar +radar_shaft = actor.cylinder(np.array([[0, 0, 0]]), np.array([[0, 1, 0]]), + (0, 1, 0), heights=1) + +############################################################################### +# In order to animate the shaft actor we have to add it to an Animation +radar_shaft_anim = Animation(radar_shaft) + +############################################################################### +# Setting a single position keyframe will make sure the actor will be placed at +# that position +radar_shaft_anim.set_position(0.0, [0, 2, 0]) + +############################################################################### +# Rotating the shaft around Y axis +radar_shaft_anim.set_rotation(0.0, [0, -250, 0]) +radar_shaft_anim.set_rotation(1.0, [0, 250, 0]) +radar_shaft_anim.set_rotation(2.0, [0, -250, 0]) + +############################################################################### +# Now we create the radar itself +radar = actor.cone(np.array([[0, 0, 0]]), directions=(0, 0, 0), + colors=(0.2, 0.2, 0.9)) + +############################################################################### +# Then add it to an animation in order to rotate it +radar_animation = Animation(radar) + +############################################################################### +# Set position and rotation as done above with the shaft. +radar_animation.set_position(0, [-.4, 0.5, 0]) +radar_animation.set_rotation(0.0, [0, 0, 0]) +radar_animation.set_rotation(1.0, [180, 0, 0]) +radar_animation.set_rotation(2.0, [0, 0, 0]) + +############################################################################### +# Now, we want the radar to rotate when the shaft rotates in hierarchical way. +# To do that we must add the radar animation as a child animation of the shaft +# animation as below: +radar_shaft_anim.add_child_animation(radar_animation) + +############################################################################### +# After that we want everything to animate related to the car. +# The wheels should always be attached to the car no matter where it moves. +# we do that by adding them as child animations of the car's body animation +car_anim.add_child_animation([wheels_animations, radar_shaft_anim]) + +############################################################################### +# Moving the car +car_anim.set_position(0.0, [-10, 0.5, 0]) +car_anim.set_position(6.0, [10, 0.5, 0]) + +############################################################################### +# Adding the car Animation to the show manager +showm.add_animation(car_anim) +scene.add(road) +scene.camera().SetPosition(0, 20, 30) + +interactive = False + +if interactive: + showm.start() + +window.record(scene, out_path='viz_keyframe_hierarchical_animation.png', + size=(900, 768)) From 890648014fd218a222935fa2ae005ca66f36229d Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sat, 8 Oct 2022 15:43:13 +0200 Subject: [PATCH 61/62] @filipinascimento's addition --- docs/tutorials/05_animation/viz_introduction.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/05_animation/viz_introduction.py b/docs/tutorials/05_animation/viz_introduction.py index 27fac5709..0ac7e6de6 100644 --- a/docs/tutorials/05_animation/viz_introduction.py +++ b/docs/tutorials/05_animation/viz_introduction.py @@ -30,8 +30,15 @@ # What is Keyframe Animation # ========================== # -# A keyframe animation is the transition in a property setting in between two -# keyframes. That change could be anything. +# Keyframe animations is a technique to simplify the process of animating a +# scene. +# Instead of providing the actor attributes for each frame, only a small amount +# of keyframes are needed to create a smooth animation. Each keyframe encodes +# the state of an actor at a certain timestamp. For instance a keyframe might +# define that an actor should be positioned at the origin (0,0,0) at the start +# of the animation. Another keyframe may define that the actor should move to +# position (1,0,0) after 10 seconds. The system will take care of interpolating +# the position of that actor between these two keyframes. # # Almost any parameter that you can set for FURY actors can be animated # using keyframes. From ae80376e62fca26bf383dc69059884620810d7c5 Mon Sep 17 00:00:00 2001 From: Mohamed Agour Date: Sat, 8 Oct 2022 20:52:23 +0200 Subject: [PATCH 62/62] Added initial shader support for animation --- fury/animation/animation.py | 80 +++++++++++++++- fury/shaders/animation_dec.vert | 155 +++++++++++++++++++++++++++++++ fury/shaders/animation_impl.vert | 22 +++++ 3 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 fury/shaders/animation_dec.vert create mode 100644 fury/shaders/animation_impl.vert diff --git a/fury/animation/animation.py b/fury/animation/animation.py index e37594f80..fc409fc8d 100644 --- a/fury/animation/animation.py +++ b/fury/animation/animation.py @@ -5,9 +5,12 @@ from scipy.spatial import transform from fury.actor import line from fury import utils +from fury.animation.helpers import get_timestamps_from_keyframes, \ + get_next_timestamp, get_previous_timestamp from fury.lib import Actor, Transform, Camera -from fury.animation.interpolator import spline_interpolator, \ - step_interpolator, linear_interpolator, slerp +from fury.animation.interpolator import * +from fury.shaders import shader_to_actor, import_fury_shader, \ + add_shader_callback class Animation: @@ -36,7 +39,7 @@ class Animation: """ def __init__(self, actors=None, length=None, loop=True, - motion_path_res=None): + motion_path_res=None, use_shaders=False): super().__init__() self._data = defaultdict(dict) @@ -57,6 +60,8 @@ def __init__(self, actors=None, length=None, loop=True, self._motion_path_actor = None self._transform = Transform() self._general_callbacks = [] + self._use_shaders = use_shaders + # Adding actors to the animation if actors is not None: self.add_actor(actors) @@ -1066,7 +1071,7 @@ def update_animation(self, time=None): self._current_timestamp = time # actors properties - if in_scene: + if in_scene and not self._use_shaders: if self.is_interpolatable('position'): position = self.get_position(time) self._transform.Translate(*position) @@ -1111,6 +1116,70 @@ def update_animation(self, time=None): if self._scene and not has_handler: self._scene.reset_clipping_range() + def _apply_shaders(self, actor): + shader_to_actor(actor, "vertex", + impl_code=import_fury_shader('animation_impl.vert')) + shader_to_actor(actor, "vertex", impl_code="", + block="color", replace_all=True, keep_default=False) + + dec_animation_vert = import_fury_shader('animation_dec.vert') + shader_to_actor(actor, "vertex", decl_code=dec_animation_vert, + block="prim_id") + attribs = ['position', 'scale', 'color', 'opacity', 'rotation'] + attribs = [att for att in attribs if self.is_interpolatable(att)] + timestamps = {attrib: get_timestamps_from_keyframes(self._data.get( + attrib, {}).get('keyframes', {})) for attrib in attribs} + keyframes = {attrib: self._data.get(attrib, {}).get('keyframes', {}) + for attrib in attribs} + + def shader_callback(_caller, _event, calldata=None): + interp_ids = { + step_interpolator: 0, + linear_interpolator: 1, + cubic_bezier_interpolator: 2, + hsv_color_interpolator: 3, + xyz_color_interpolator: 4, + slerp: 5, + # spline_interpolator: 1, + # cubic_spline_interpolator: 1, + # lab_color_interpolator: 1, + # tan_cubic_spline_interpolator: 1, + } + program = calldata + if program is not None: + t = self._current_timestamp + program.SetUniformf('time', t) + for attrib in attribs: + if self.is_interpolatable(attrib): + # todo: allow not to send an attrib + kfs = [] + t0 = get_previous_timestamp(timestamps[attrib], t) + k0 = keyframes.get(attrib, {}).get(t0, None) + kfs.append((t0, k0)) + if len(keyframes.get(attrib, {})) > 1: + t1 = get_next_timestamp(timestamps[attrib], t) + k1 = keyframes.get(attrib, {}).get(t1, None) + kfs.append((t1, k1)) + program.SetUniformi(f'{attrib}_k.count', len(kfs)) + interp = self._data.get(attrib).get('interpolator'). \ + get('base') + program.SetUniformi(f'{attrib}_k.method', interp_ids. + get(interp, 1)) + for i, (ts, k) in enumerate(kfs): + program.SetUniformf(f'{attrib}_k.keyframes[{i}].t', + ts) + for field in k: + val = k[field] + if attrib == 'opacity': + val = np.repeat(val, 3) + program.SetUniform3f( + f'{attrib}_k.keyframes[{i}].{field}', + val) + else: + program.SetUniformi(f'{attrib}_k.interpolatable', 0) + + add_shader_callback(actor, shader_callback) + def add_to_scene(self, scene): """Add this Animation, its actors and sub Animations to the scene""" [scene.add(actor) for actor in self._actors] @@ -1119,6 +1188,9 @@ def add_to_scene(self, scene): if self._motion_path_actor: scene.add(self._motion_path_actor) + + if self._use_shaders: + [self._apply_shaders(actor) for actor in self.actors] self._scene = scene self._added_to_scene = True self._start_time = perf_counter() diff --git a/fury/shaders/animation_dec.vert b/fury/shaders/animation_dec.vert new file mode 100644 index 000000000..276f87ae8 --- /dev/null +++ b/fury/shaders/animation_dec.vert @@ -0,0 +1,155 @@ + + +in vec4 scalarColor; +uniform float time; + +out float t; +out vec4 vertexColorVSOutput; + + +mat3 xyz_to_rgb_mat = mat3(3.24048134, -1.53715152, -0.49853633, + -0.96925495, 1.87599, 0.04155593, + 0.05564664, -0.20404134, 1.05731107); + +// Interpolation methods +const int STEP = 0; +const int LINEAR = 1; +const int BEZIER = 2; +const int HSV = 3; +const int XYZ = 4; +const int Slerp = 5; + +struct Keyframe { + float t; + vec3 value; + vec3 inCp; + vec3 outCp; +}; + +struct Keyframes { + Keyframe[6] keyframes; + int method; + int count; +}; + + +uniform Keyframes position_k; +uniform Keyframes scale_k; +uniform Keyframes color_k; +uniform Keyframes opacity_k; + + +Keyframe get_next_keyframe(Keyframes keyframes, float t, bool first) { + int start = 0; + if (!first) start++; + for (int i = start; i < keyframes.count; i++) + if (keyframes.keyframes[i].t > t) return keyframes.keyframes[i]; + return keyframes.keyframes[keyframes.count - 1]; +} + +Keyframe get_previous_keyframe(Keyframes keyframes, float t, bool last) { + int start = keyframes.count - 1; + if (!last) start--; + for (int i = start; i >= 0; i--) + if (keyframes.keyframes[i].t <= t) return keyframes.keyframes[i]; + return keyframes.keyframes[keyframes.count - 1]; +} + +bool has_one_keyframe(Keyframes k) { + if (k.count == 1) + return true; + return false; +} + +bool is_interpolatable(Keyframes k) { + return bool(k.count); +} + +float get_time_tau_clamped(float t, float t0, float t1){ + return clamp((t - t0) / (t1 - t0), 0, 1); +} + +float get_time_tau(float t, float t0, float t1){ + return (t - t0) / (t1 - t0); +} + +vec3 lerp(Keyframes k, float t) { + if (has_one_keyframe(k)) return k.keyframes[0].value; + Keyframe k0 = k.keyframes[0]; + Keyframe k1 = k.keyframes[1]; + float dt = get_time_tau_clamped(t, k0.t, k1.t); + return mix(k0.value, k1.value, dt); +} + +vec3 cubic_bezier(Keyframes k, float t) { + if (has_one_keyframe(k)) return k.keyframes[0].value; + Keyframe k0 = get_previous_keyframe(k, t, false); + Keyframe k1 = get_next_keyframe(k, t, false); + float dt = get_time_tau_clamped(t, k0.t, k1.t); + vec3 E = mix(k0.value, k0.outCp, dt); + vec3 F = mix(k0.outCp, k1.inCp, dt); + vec3 G = mix(k1.inCp, k1.value, dt); + + vec3 H = mix(E, F, dt); + vec3 I = mix(F, G, dt); + + vec3 P = mix(H, I, dt); + + return P; +} + +vec3 hsv2rgb(vec3 c) { + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +float clip(float x) { + if (x > 1) + return 1; + else if (x < 0) + return 0; + else return x; +} +vec3 xyz2rgb(vec3 c) { + c = c * xyz_to_rgb_mat; + float po = 1 / 2.4; + if (c.x > 0.0031308) c.x = 1.055 * pow(c.x, po) - 0.055; + else c.y *= 12.92; + if (c.y > 0.0031308) c.y = 1.055 * pow(c.y, po) - 0.055; + else c.y *= 12.92; + if (c.z > 0.0031308) c.z = 1.055 * pow(c.z, po) - 0.055; + else c.z *= 12.92; + + c.x = clip(c.x); + c.y = clip(c.y); + c.z = clip(c.z); + + return c; +} + +vec3 lab2xyz(vec3 col) { + float l = col.x; + float a = col.y; + float b = col.z; + col.y = (l + 16.) / 116.; + col.x = (a / 500.) + col.y; + col.z = col.y - (b / 200.); + return col; +} + +vec3 interp(Keyframes k, float t) { + if (k.method == LINEAR) return lerp(k, t); + else if (k.method == BEZIER) return cubic_bezier(k, t); + else if (k.method == HSV) return hsv2rgb(lerp(k, t)); + else if (k.method == XYZ) return xyz2rgb(lab2xyz(lerp(k, t))); + else if (k.method == STEP) return k.keyframes[0].value; +} + +mat4 transformation(vec3 position, vec3 scale) { + return mat4( + vec4(scale.x, 0.0, 0.0, 0.0), + vec4(0.0, scale.y, 0.0, 0.0), + vec4(0.0, 0.0, scale.z, 0.0), + vec4(position, 1.0)); +} \ No newline at end of file diff --git a/fury/shaders/animation_impl.vert b/fury/shaders/animation_impl.vert new file mode 100644 index 000000000..b5bfd1b41 --- /dev/null +++ b/fury/shaders/animation_impl.vert @@ -0,0 +1,22 @@ + +// vertexVCVSOutput = MCVCMatrix * vertexMC; + +vec3 f_pos = vec3(0., 0., 0.); +if (is_interpolatable(position_k)) + f_pos = interp(position_k, time); + +vec3 f_scale = vec3(1., 1., 1.); +if (is_interpolatable(scale_k)) + f_scale = interp(scale_k, time); + +if (is_interpolatable(color_k)) + vertexColorVSOutput = vec4(interp(color_k, time), 1); +else + vertexColorVSOutput = scalarColor; + +if (is_interpolatable(opacity_k)) + vertexColorVSOutput.a = interp(opacity_k, time).x; +else + vertexColorVSOutput = scalarColor; + +gl_Position = MCDCMatrix * transformation(f_pos, f_scale) * vertexMC ; \ No newline at end of file