diff --git a/docs/tutorials/05_animation/viz_keyframe_bezier_interpolator.py b/docs/tutorials/05_animation/viz_bezier_interpolator.py similarity index 81% rename from docs/tutorials/05_animation/viz_keyframe_bezier_interpolator.py rename to docs/tutorials/05_animation/viz_bezier_interpolator.py index 74cd6f2e5..fdb700d4c 100644 --- a/docs/tutorials/05_animation/viz_keyframe_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, 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')), - 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_keyframe_camera_animation.py b/docs/tutorials/05_animation/viz_camera.py similarity index 65% rename from docs/tutorials/05_animation/viz_keyframe_camera_animation.py rename to docs/tutorials/05_animation/viz_camera.py index 3df8e30e3..2a43f3561 100644 --- a/docs/tutorials/05_animation/viz_keyframe_camera_animation.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_timeline(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_timeline(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_keyframe_color_interpolators.py b/docs/tutorials/05_animation/viz_color_interpolators.py similarity index 56% rename from docs/tutorials/05_animation/viz_keyframe_color_interpolators.py rename to docs/tutorials/05_animation/viz_color_interpolators.py index 8e07a8ab4..cf66a14b0 100644 --- a/docs/tutorials/05_animation/viz_keyframe_color_interpolators.py +++ b/docs/tutorials/05_animation/viz_color_interpolators.py @@ -1,17 +1,20 @@ """ -================== -Keyframe animation -================== +============================ +Keyframe Color Interpolators +============================ -Color animation explained +Color animation explained in this tutorial and how to use different color +space interpolators. """ 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() @@ -39,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_timeline([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_keyframe_custom_interpolator.py b/docs/tutorials/05_animation/viz_custom_interpolator.py similarity index 79% rename from docs/tutorials/05_animation/viz_keyframe_custom_interpolator.py rename to docs/tutorials/05_animation/viz_custom_interpolator.py index da54a6c4c..3bba22471 100644 --- a/docs/tutorials/05_animation/viz_keyframe_custom_interpolator.py +++ b/docs/tutorials/05_animation/viz_custom_interpolator.py @@ -1,15 +1,15 @@ """ -===================== -Keyframe animation -===================== +============================ +Making a custom interpolator +============================ Keyframe animation using custom interpolator. """ 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,20 +119,20 @@ def interpolate(t): # Cubic spline keyframes data same as the one you get from glTF file. # =================================================================== -# 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.]]] +# 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.]], + [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. -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_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)) diff --git a/docs/tutorials/05_animation/viz_interpolators.py b/docs/tutorials/05_animation/viz_interpolators.py new file mode 100644 index 000000000..4ebd2c806 --- /dev/null +++ b/docs/tutorials/05_animation/viz_interpolators.py @@ -0,0 +1,137 @@ +""" +===================== +Keyframe animation +===================== + +Minimal tutorial of making keyframe-based animation in FURY. + +""" + +############################################################################### +# What is an ``Animation`` +# ======================== +# +# ``Animation`` is responsible for animating FURY actors using a set of +# keyframes by interpolating values between timestamps of these keyframes. + +import numpy as np +from fury import actor, window +from fury.animation import Animation +from fury.animation.interpolator import cubic_spline_interpolator + +keyframes = { + 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])} +} + +############################################################################### +# 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 an ``Animation`` +# ========================= +# +# First step is creating the Animation. +animation = Animation() + +############################################################################### +# Adding the sphere actor to the timeline +# This could've been done during initialization. +animation.add_actor(arrow) + +############################################################################### +# Setting position keyframes +# ========================== +# +# Adding some position keyframes +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 +# ======================================================= +# +# For all properties except **rotation**, linear interpolator is used by +# default. In order to change the default interpolator and set another +# 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**. +animation.set_position_interpolator(cubic_spline_interpolator) + +############################################################################### +# Adding some rotation keyframes. +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. +# 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. +scene.camera().SetPosition(0, 0, 90) + +############################################################################### +# Adding main animation to the ``ShowManager``. +showm.add_animation(animation) + +############################################################################### +# Start the ``ShowManager`` to start playing the animation +interactive = False + +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..0ac7e6de6 --- /dev/null +++ b/docs/tutorials/05_animation/viz_introduction.py @@ -0,0 +1,118 @@ +""" +=============================== +Keyframe animation introduction +=============================== + +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 +# ========================== +# +# 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. +# +# For example, a Keyframe might define that the position of a FURY actor is +# (0, 0, 0) at time equals 1 second. +# +# 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 (1, 1, 0), +# +# Then the correct position of the actor for all the times between 3 and 10 +# will be interpolated. +# +# 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 import Animation + + +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 an ``Animation``. See ``viz_animation.py`` tutorial +animation = Animation() + +############################################################################### +# We also create the FURY sphere actor that will be animated. +sphere = actor.sphere(np.zeros([1, 3]), np.ones([1, 3])) + +############################################################################### +# Then lets add the sphere actor to the ``Animation`` +animation.add_actor(sphere) + +############################################################################### +# Then, we set our position keyframes at different timestamps +# 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. + +animation.set_position(0.0, [-1, -1, 0]) +animation.set_position(3.0, [1, 1, 0]) +animation.set_position(6.0, [-1, -1, 0]) + +############################################################################### +# The ``Animation`` must be added to the ``ShowManager`` as follows: +showm.add_animation(animation) +scene.camera().SetPosition(0, 0, 10) + +############################################################################### +# 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. + + +############################################################################### +# No need to add the sphere actor to scene, since it's now a part of the +# ``Animation``. + +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_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 f60a091f5..c8ecfa22d 100644 --- a/docs/tutorials/05_animation/viz_robot_arm_animation.py +++ b/docs/tutorials/05_animation/viz_robot_arm_animation.py @@ -1,14 +1,14 @@ """ -===================== -Keyframe animation -===================== +=================== +Arm Robot Animation +=================== Tutorial on making a robot arm animation in FURY. """ import numpy as np from fury import actor, window -from fury.animation.timeline import Timeline +from fury.animation import Animation, Timeline from fury.utils import set_actor_origin scene = window.Scene() @@ -18,6 +18,7 @@ order_transparent=True) showm.initialize() + ############################################################################### # Creating robot arm components @@ -36,29 +37,32 @@ 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])) ############################################################################### -# 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(playback_panel=True) + +############################################################################### +# 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 Timelines in hierarchical order -tl_main.add_child_timeline(tl_child) -tl_child.add_child_timeline(tl_grand_child) +# Adding other Animations in hierarchical order +main_arm_animation.add_child_animation(child_arm_animation) +child_arm_animation.add_child_animation(drill_animation) ############################################################################### # Creating Arm joints time dependent animation functions. 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): @@ -72,42 +76,38 @@ 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. scene.camera().SetPosition(0, 0, 90) ############################################################################### -# Adding timelines to the main Timeline. -scene.add(tl_main, 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): - tl_main.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_keyframe_spline_interpolator.py b/docs/tutorials/05_animation/viz_spline_interpolator.py similarity index 60% rename from docs/tutorials/05_animation/viz_keyframe_spline_interpolator.py rename to docs/tutorials/05_animation/viz_spline_interpolator.py index 5cc03b1cf..05b492ad3 100644 --- a/docs/tutorials/05_animation/viz_keyframe_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. @@ -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() @@ -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]), } ############################################################################### @@ -38,63 +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) ############################################################################### -# Adding everything to a main ``Timeline`` to control the two timelines. +# Wrapping animations up! # ============================================================================= # -############################################################################### -# Creating a timeline with a playback panel -main_timeline = Timeline(playback_panel=True, motion_path_res=100) +# Adding everything to a ``Timeline`` to control the two timelines. ############################################################################### -# Add visualization dots actor to the timeline as a static actor. -main_timeline.add_static_actor(pos_dots) +# First we create a timeline with a playback panel: +timeline = Timeline(playback_panel=True) ############################################################################### -# Adding timelines to the main timeline (so that it controls their playback) -main_timeline.add([spline_tl, linear_tl]) +# Add visualization dots actor to the scene. +scene.add(pos_dots) ############################################################################### -# Adding the main timeline to the scene. -scene.add(main_timeline) - +# Adding the animations to the timeline (so that it controls their playback). +timeline.add_animation([linear_anim, spline_anim]) ############################################################################### -# 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() +# Adding the timeline to the show manager. +showm.add_animation(timeline) ############################################################################### -# Adding the callback function that updates the animation -showm.add_timer_callback(True, 10, timer_callback) +# Now that these two animations are added to timeline, if the timeline +# is played, paused, ..., all these changes will reflect on the animations. + interactive = False diff --git a/docs/tutorials/05_animation/viz_timeline.py b/docs/tutorials/05_animation/viz_timeline.py new file mode 100644 index 000000000..40cf881de --- /dev/null +++ b/docs/tutorials/05_animation/viz_timeline.py @@ -0,0 +1,96 @@ +""" +============================== +Timeline and setting keyframes +============================== + +In his tutorial, you will learn how to use Fury ``Timeline`` for playing the +animations. + +""" + +############################################################################### +# What is ``Timeline``? +# =================== +# +# ``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`` +# ======================= +# +# FURY ``Timeline`` has the option to attaches a very useful panel for +# controlling the animation by setting ``playback_panel=True``. + +############################################################################### +# Creating a ``Timeline`` with a PlaybackPanel. +timeline = Timeline(playback_panel=True) + +############################################################################### +# Creating a Fury Animation an usual +anim = Animation() +sphere = actor.sphere(np.zeros([1, 3]), np.ones([1, 3])) +anim.add_actor(sphere) +# Now that the actor is addd to the ``Animation``, setting keyframes to the +# Animation will animate the actor accordingly. + + +############################################################################### +# Setting Keyframes +# ================= +# +# There are multiple ways to set keyframes: +# +# 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: +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 +# ``animation.set__keyframes(keyframes)``. +keyframes = { + 1.0: np.array([0, 0, 0]), + 3.0: np.array([-2, 0, 0]) +} + +anim.set_position_keyframes(keyframes) + +############################################################################### +# That's it! Now we are done setting keyframes. + +############################################################################### +# 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) + +scene.camera().SetPosition(0, 0, -10) + +interactive = False + +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 65% rename from docs/tutorials/05_animation/viz_animation_using_time_equations.py rename to docs/tutorials/05_animation/viz_using_time_equations.py index 3e63aa6ff..2f492775e 100644 --- a/docs/tutorials/05_animation/viz_animation_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 diff --git a/fury/animation/__init__.py b/fury/animation/__init__.py index e69de29bb..33129d5b4 100644 --- a/fury/animation/__init__.py +++ b/fury/animation/__init__.py @@ -0,0 +1,2 @@ +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 new file mode 100644 index 000000000..fc409fc8d --- /dev/null +++ b/fury/animation/animation.py @@ -0,0 +1,1433 @@ +import numpy as np +from warnings import warn +from time import perf_counter +from collections import defaultdict +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 * +from fury.shaders import shader_to_actor, import_fury_shader, \ + add_shader_callback + + +class Animation: + """Keyframe animation class. + + 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. + Linear interpolation is used by default to interpolate data between the + main keyframes. + + Attributes + ---------- + 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 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, actors=None, length=None, loop=True, + motion_path_res=None, use_shaders=False): + + super().__init__() + self._data = defaultdict(dict) + self._animations = [] + self._actors = [] + self._static_actors = [] + self._timeline = None + self._parent_animation = None + self._scene = None + self._start_time = 0 + 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 + 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) + + def update_duration(self): + """Update and return the duration of the Animation. + + Returns + ------- + float + The duration of the animation. + """ + 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 + def duration(self): + """Return the duration of the animation. + + Returns + ------- + float + The duration of the animation. + """ + 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 + 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'): + 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] + elif len(self._actors) >= 1: + colors = sum([i.vcolors[0] / 255 for i in self._actors]) / \ + len(self._actors) + else: + colors = [1, 1, 1] + + if len(lines) > 0: + lines = np.array([lines]) + if isinstance(colors, list): + colors = np.array([colors]) + + mpa = 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): + """Get animation data. + + Returns + ------- + dict: + The animation data containing keyframes and interpolators. + """ + return self._data + + def _get_attribute_data(self, attrib): + """Get animation data for a specific attribute. + + Parameters + ---------- + attrib: str + The attribute name to get data for. + + Returns + ------- + dict: + The animation data for a specific attribute. + """ + data = self._get_data() + + 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 get_keyframes(self, attrib=None): + """Get a keyframe for a specific or all attributes. + + Parameters + ---------- + attrib: str, optional, default: None + The name of the attribute. + If None, all keyframes for all set attributes will be returned. + """ + + 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, 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. + 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) + 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, **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(0) + self.update_motion_path() + + def set_keyframes(self, attrib, keyframes): + """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. + + 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]}} + >>> pos_keyframes = {1: np.array([1, 2, 3]), 3: np.array([5, 5, 5])} + >>> Animation.set_keyframes('position', pos_keyframes) + """ + for t, keyframe in keyframes.items(): + if isinstance(keyframe, dict): + self.set_keyframe(attrib, t, **keyframe) + else: + 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 + 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: + parent_in_scene = parent._added_to_scene + + if self.is_interpolatable('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. + + 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 Animation 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: + self._scene.add(*self._actors) + self._added_to_scene = True + elif not should_be_in_scene and self._added_to_scene: + self._scene.rm(*self._actors) + self._added_to_scene = False + + def set_interpolator(self, attrib, interpolator, is_evaluator=False, + **kwargs): + """Set keyframes interpolator for a certain property + + Parameters + ---------- + attrib: str + The name of the property. + interpolator: callable + The generator function of the interpolator to be used to + interpolate/evaluate keyframes. + 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 + -------- + >>> Animation.set_interpolator('position', linear_interpolator) + + >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) + >>> Animation.set_interpolator('position', pos_fun) + """ + + attrib_data = self._get_attribute_data(attrib) + 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_duration() + self.update_motion_path() + + def is_interpolatable(self, attrib): + """Check whether a property is interpolatable. + + Parameters + ---------- + attrib: str + The name of the property. + + Returns + ------- + bool + 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. + + """ + data = self._data + return bool(data.get(attrib, {}).get('interpolator', {}).get('func')) + + def set_position_interpolator(self, interpolator, is_evaluator=False, + **kwargs): + """Set the position interpolator. + + Parameters + ---------- + interpolator: callable + 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 + -------- + >>> 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. + + Parameters + ---------- + interpolator: callable + 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 + -------- + >>> 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 rotation interpolator . + + Parameters + ---------- + 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, + is_evaluator=is_evaluator) + + def set_color_interpolator(self, interpolator, is_evaluator=False): + """Set the color interpolator. + + Parameters + ---------- + 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) + + def set_opacity_interpolator(self, interpolator, is_evaluator=False): + """Set the opacity interpolator. + + Parameters + ---------- + 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, + 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 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])} + >>> Animation.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: + 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])} + >>> Animation.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])} + >>> Animation.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])} + >>> Animation.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 add(self, item): + """Add an item to the Animation. + This item can be an Actor, Animation, list of Actors, or a list of + Animations. + + Parameters + ---------- + item: Animation, vtkActor, list[Animation], or list[vtkActor] + Actor/s to be animated by the Animation. + """ + 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 animated") + + 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) + self.update_duration() + + def add_actor(self, actor, static=False): + """Add an actor or list of actors to the Animation. + + Parameters + ---------- + actor: vtkActor or list(vtkActor) + Actor/s to be animated by the Animation. + static: bool + Indicated whether the actor should be animated and controlled by + 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: + self.add_actor(a, static=static) + elif static: + if actor not in self.static_actors: + self._static_actors.append(actor) + else: + if actor not in self._actors: + actor.vcolors = utils.colors_from_actor(actor) + self._actors.append(actor) + + @property + 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. + + Parameters + ---------- + + timeline: 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. + + Returns + ------- + list: + List of actors controlled by the Animation. + """ + return self._actors + + @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 Animation. All static actors will be + added to the scene when the Animation 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._actors.remove(actor) + + def remove_actors(self): + """Remove all actors from the Animation""" + self._actors.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, 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. + + + Parameters + ---------- + 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) + + def update_animation(self, time=None): + """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 + ---------- + time: float or int, optional, default: None + The time to update animation at. If None, the animation will play + without adding it to a Timeline. + """ + has_handler = True + if time is None: + time = perf_counter() - self._start_time + has_handler = False + + # 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 > self.duration: + 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: + self._transform.Identity() + + self._current_timestamp = time + + # actors properties + if in_scene and not self._use_shaders: + if self.is_interpolatable('position'): + position = self.get_position(time) + self._transform.Translate(*position) + + if self.is_interpolatable('opacity'): + 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(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(time) + self._transform.Scale(*scale) + + if self.is_interpolatable('color'): + color = self.get_color(time) + 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 != [] and self.is_interpolatable(attrib): + 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] + + 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] + [scene.add(static_act) for static_act in self._static_actors] + [scene.add(animation) for animation in self._animations] + + 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() + 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 + + +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. + + 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): + """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 + 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, as_quat=True) + 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() diff --git a/fury/animation/tests/test_animation.py b/fury/animation/tests/test_animation.py new file mode 100644 index 000000000..c27a41d6d --- /dev/null +++ b/fury/animation/tests/test_animation.py @@ -0,0 +1,118 @@ +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, CameraAnimation +from fury.lib import Camera + + +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()) + + +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])) diff --git a/fury/animation/tests/test_timeline.py b/fury/animation/tests/test_timeline.py index 7059480e3..6452cd525 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, Animation from fury.ui import PlaybackPanel @@ -15,38 +11,20 @@ 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)) 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) + 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, @@ -68,38 +46,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(force=True) - - 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) + anim = Animation(length=12) + tl_2.add_animation(anim) + assert anim in tl_2.animations + + anim.set_position(12, [1, 2, 1]) + assert tl_2.duration == length + + tl_2 = Timeline(anim, length=11) + assert tl_2.duration == 11 + + tl = Timeline(playback_panel=True) + assert tl.has_playback_panel is True + + tl.loop = True + assert tl.loop is True diff --git a/fury/animation/timeline.py b/fury/animation/timeline.py index f77c6a0e6..a591b3be8 100644 --- a/fury/animation/timeline.py +++ b/fury/animation/timeline.py @@ -1,78 +1,46 @@ -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 time import perf_counter from fury.ui.elements import PlaybackPanel -from fury.lib import Actor, Transform +from fury.animation.animation import Animation -class Timeline(Container): - """Keyframe animation timeline class. +class Timeline: + """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 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 : str - a formatted string to print out what the animal says + 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 - 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 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__() - self._data = defaultdict(dict) - self._camera_data = defaultdict(dict) 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._speed = 1.0 self._last_started_time = 0 self._playing = False - self._length = length - self._final_timestamp = 0 - self._needs_update = False - self._reverse_playing = False + self._animations = [] 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._length = length + self._duration = length if length is not None else 0.0 - # 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 @@ -84,1295 +52,63 @@ 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) + if animations is not None: + self.add_animation(animations) - def update_final_timestamp(self): - """Calculate and return the final timestamp of all keyframes. + def update_duration(self): + """Update and return the duration of the Timeline. Returns ------- float - final timestamp that can be reached inside the Timeline. + The duration of 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])) + if self._length is not None: + self._duration = self._length else: - self._final_timestamp = self._length + self._duration = max([0.0] + [anim.update_duration() for anim + in self._animations]) 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) + self.playback_panel.final_time = self.duration + return self.duration @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. + def duration(self): + """Return the duration of the Timeline. 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. - + float + The duration of the Timeline. """ - 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() + return self._duration 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 + 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(force=True) def restart(self): """Restart the animation""" - self._last_timestamp = 0 + self._current_timestamp = 0 self._playing = True - self.update_animation(force=True) + self.update(force=True) @property def current_timestamp(self): @@ -1385,9 +121,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 = (perf_counter() - + self._last_started_time) * self.speed + return self._current_timestamp @current_timestamp.setter def current_timestamp(self, timestamp): @@ -1401,18 +137,6 @@ def current_timestamp(self, timestamp): """ self.seek(timestamp) - @property - def final_timestamp(self): - """Get the final timestamp of the Timeline. - - Returns - ------- - float - The final time of the Timeline. - - """ - return self._final_timestamp - def seek(self, timestamp): """Set the current timestamp of the Timeline. @@ -1425,15 +149,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 + perf_counter() - timestamp / self.speed else: - self._last_timestamp = timestamp - self.update_animation(force=True) + self._current_timestamp = timestamp + self.update(force=True) def seek_percent(self, percent): """Seek a percentage of the Timeline's final timestamp. @@ -1444,7 +167,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 @@ -1454,22 +177,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. @@ -1477,10 +188,10 @@ def stopped(self): Returns ------- bool - Timeline is stopped if True. + True if Timeline is stopped. """ - return not self.playing and not self._last_timestamp + return not self.playing and not self._current_timestamp @property def paused(self): @@ -1489,15 +200,15 @@ def paused(self): Returns ------- bool - Timeline is paused if True. + True if the Timeline is paused. """ - 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): - """Return the speed of the timeline. + """Return the speed of the timeline's playback. Returns ------- @@ -1508,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 ---------- @@ -1520,9 +231,33 @@ 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 + 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. @@ -1533,13 +268,71 @@ 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_animation(self, animation): + """Add Animation or list of Animations. + + Parameters + ---------- + animation: Animation or list[Animation] or tuple[Animation] + Animation/s to be added. + """ + 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.") + + @property + def animations(self) -> 'list[Animation]': + """Return a list of Animations. + + Returns + ------- + list: + List of Animations controlled by the timeline. + """ + return self._animations + + def update(self, force=False): + """Update the timeline. + + Update the Timeline and all the animations that it controls. As well as + the playback of the Timeline (if exists). + + Parameters + ---------- + 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. + + """ + time = self.current_timestamp + if self.has_playback_panel: + self.playback_panel.current_time = time + if time > 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: + [anim.update_animation(time) for anim in self._animations] + + def add_to_scene(self, scene): + """Add Timeline and all of its Animations to the 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""" + if self.has_playback_panel: + scene.rm(*tuple(self.playback_panel.actors)) + [animation.remove_from_scene(scene) for animation in self._animations] 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 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 diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index 23fa99e72..edd0a6c59 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 import Timeline, Animation 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,45 @@ def timer_callback(_obj, _event): assert_greater(ideal_fps, 0) assert_greater(ideal_fps, actual_fps) + +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) + animation = Animation(cube) + timeline.add_animation(animation) + showm.add_animation(timeline) + + npt.assert_equal(len(showm._timelines), 1) + 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 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 animation.static_actors] + assert_true(cube not in actors) + 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() # test_record() diff --git a/fury/ui/elements.py b/fury/ui/elements.py index d26b14e22..a2951bd26 100644 --- a/fury/ui/elements.py +++ b/fury/ui/elements.py @@ -3841,7 +3841,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. diff --git a/fury/window.py b/fury/window.py index 842ef89ad..a1814c651 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 import Timeline, Animation 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,90 @@ 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._animations = [] + self._animation_callback = None def initialize(self): """Initialize interaction.""" self.iren.Initialize() + @property + def timelines(self): + """Return a list of Timelines that were added to the ShowManager. + + Returns + ------- + list[Timeline]: + List of Timelines. + """ + return self._timelines + + @property + def animations(self): + """Return a list of Animations that were added to the ShowManager. + + Returns + ------- + list[Animation]: + List of Animations. + """ + return self._animations + + 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 + ---------- + animation : Animation or Timeline + The Animation or Timeline to be added to the ShowManager. + """ + 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() for tl in self._timelines] + [anim.update_animation() for anim in self._animations] + self.render() + self._animation_callback = self.add_timer_callback(True, 10, + animation_cbk) + + def remove_animation(self, animation): + """Remove an Animation or a Timeline from the ShowManager. + + Animation will be removed from the Scene as well as from the + ShowManager. + + Parameters + ---------- + animation : Animation or Timeline + The Timeline to be removed. + """ + + 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.""" self.window.Render()