Source code for fury.animation.animation

"""Animation module."""

from collections import defaultdict
from time import perf_counter
from warnings import warn

import numpy as np
from scipy.spatial import transform

from fury import utils

# from fury.actor import line
from fury.animation.interpolator import (  # noqa F401
    linear_interpolator,
    slerp,
    spline_interpolator,
    step_interpolator,
)

# from fury.lib import Actor, Camera, Transform


[docs] class Animation: """ Create and manage keyframe animations for Fury actors. Animation is responsible for handling keyframe animations for a single actor or a group of actors. It provides control over multiple attributes and properties of Fury actors including position, rotation, scale, color, and opacity. The class also supports custom data interpolation. By default, linear interpolation is used to interpolate data between keyframes. Parameters ---------- actors : Actor or list of Actor, optional Actor(s) to be animated. length : float or int, optional The fixed length/duration of the animation in seconds. If None, the animation will derive its duration from the keyframes. loop : bool, optional Whether to loop the animation (True) or play once (False). motion_path_res : int, optional The number of line segments used to visualize the animation's motion path (position visualization). Higher values create smoother paths. """
[docs] def __init__(self, *, actors=None, length=None, loop=True, motion_path_res=None): """Initialize the Animation.""" 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 = [] # Adding actors to the animation if actors is not None: self.add_actor(actors)
[docs] def update_duration(self): """ Update and return the duration of the Animation. Update the animation duration based on the length parameter or the maximum timestamp of all keyframes and child animations. Returns ------- float The duration of the animation in seconds. """ 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. """ if self._timeline: return self._timeline.current_timestamp elif self.parent_animation: return self.parent_animation.current_timestamp return self._current_timestamp
[docs] def update_motion_path(self): """ Update motion path visualization actor. Creates or updates the motion path visualization actor that represents the animation path. The resolution of the path is determined by motion_path_res. """ 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)
[docs] def get_keyframes(self, *, attrib=None): """ Get keyframes for a specific or all attributes. Parameters ---------- attrib : str, optional The name of the attribute. If None, all keyframes for all set attributes will be returned. Returns ------- dict Dictionary of keyframes for the specified attribute(s). """ 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", {})
[docs] 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 True. **kwargs : dict, optional Additional parameters for the keyframe. This can include: - `in_cp` and `out_cp` for cubic Bézier interpolation (float). - `in_tangent` and `out_tangent` for cubic spline interpolation, ndarray, shape (1, M). """ 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(time=0) self.update_motion_path()
[docs] 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: >>> import numpy as np >>> 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) # doctest: +SKIP """ for t, keyframe in keyframes.items(): if isinstance(keyframe, dict): self.set_keyframe(attrib, t, **keyframe) else: self.set_keyframe(attrib, t, keyframe)
[docs] def is_inside_scene_at(self, timestamp): """ Check if the Animation is set to be inside the scene at a specific timestamp. Parameters ---------- timestamp : float The timestamp to check scene presence at. 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
[docs] def add_to_scene_at(self, timestamp): """ Set timestamp for adding Animation to scene event. Parameters ---------- timestamp : float Timestamp of the event when the animation should be added to the scene. """ 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)
[docs] def remove_from_scene_at(self, timestamp): """ Set timestamp for removing Animation from scene event. Parameters ---------- timestamp : float Timestamp of the event when the animation should be removed from the scene. """ 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): """ Handle adding/removing actors from scene at a specific timestamp. Parameters ---------- timestamp : float The timestamp to handle the scene event at. """ 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
[docs] 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]) **kwargs : dict, optional Additional parameters for the interpolator. This can include: - spline_degree : int - 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) # doctest: +SKIP >>> pos_fun = lambda t: np.array([np.sin(t), np.cos(t), 0]) # doctest: +SKIP >>> Animation.set_interpolator('position', pos_fun) # doctest: +SKIP """ 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 in case 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()
[docs] 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"))
[docs] 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. **kwargs : dict, optional Additional parameters for the interpolator. This can include: - degree : int - The degree of the spline interpolation in case of setting the `spline_interpolator`. """ self.set_interpolator( "position", interpolator, is_evaluator=is_evaluator, **kwargs )
[docs] def set_scale_interpolator(self, interpolator, *, is_evaluator=False): """ Set the scale interpolator. Parameters ---------- interpolator : callable The 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) # doctest: +SKIP """ self.set_interpolator("scale", interpolator, is_evaluator=is_evaluator)
[docs] 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) # doctest: +SKIP """ self.set_interpolator("rotation", interpolator, is_evaluator=is_evaluator)
[docs] 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) # doctest: +SKIP """ self.set_interpolator("color", interpolator, is_evaluator=is_evaluator)
[docs] 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) # doctest: +SKIP """ self.set_interpolator("opacity", interpolator, is_evaluator=is_evaluator)
[docs] 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. Returns ------- ndarray or float or bool The interpolated value of the attribute at the given timestamp. """ value = ( self._data.get(attrib, {}).get("interpolator", {}).get("func")(timestamp) ) return value
[docs] def get_current_value(self, attrib): """ Return the value of an attribute at current time. Parameters ---------- attrib : str The attribute name. Returns ------- ndarray or float or bool The interpolated value of the attribute at the current timestamp. """ return ( self._data.get(attrib) .get("interpolator") .get("func")(self._timeline.current_timestamp) )
[docs] 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. **kwargs : dict, optional Additional parameters for the keyframe. This can include: - `in_cp` and `out_cp` for cubic Bézier interpolation (float). - `in_tangent` and `out_tangent` for cubic spline interpolation, ndarray, shape (1, M). Notes ----- `in_cp` and `out_cp` only needed when using the cubic bezier interpolation method. """ self.set_keyframe("position", timestamp, position, **kwargs)
[docs] def set_position_keyframes(self, keyframes): """ Set a dict of position keyframes at once. Parameters ---------- keyframes : dict A dict with timestamps as keys and positions as values. Should be in the following form: {timestamp_1: position_1, timestamp_2: position_2}. Examples -------- >>> pos_keyframes = {1, (0, 0, 0), 3, (50, 6, 6)} >>> Animation.set_position_keyframes(pos_keyframes) # doctest: +SKIP """ self.set_keyframes("position", keyframes)
[docs] 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). **kwargs : dict, optional Additional parameters for the keyframe. 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!", stacklevel=2, )
[docs] 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. **kwargs : dict, optional Additional parameters for the keyframe. """ quat = transform.Rotation.from_rotvec(vector).as_quat() self.set_keyframe("rotation", timestamp, quat, **kwargs)
[docs] 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. **kwargs : dict, optional Additional parameters for the keyframe. """ self.set_keyframe("scale", timestamp, scalar, **kwargs)
[docs] def set_scale_keyframes(self, keyframes): """ Set a dict of scale keyframes at once. Parameters ---------- keyframes : dict A dict with timestamps as keys and scales as values. Should be in the following form: {timestamp_1: scale_1, timestamp_2: scale_2}. Examples -------- >>> scale_keyframes = {1, (1, 1, 1), 3, (2, 2, 3)} >>> Animation.set_scale_keyframes(scale_keyframes) # doctest: +SKIP """ self.set_keyframes("scale", keyframes)
[docs] 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. **kwargs : dict, optional Additional parameters for the keyframe. """ self.set_keyframe("color", timestamp, color, **kwargs)
[docs] def set_color_keyframes(self, keyframes): """ Set a dict of color keyframes at once. Parameters ---------- keyframes : dict A dict with timestamps as keys and color as values. Should be in the following form: {timestamp_1: color_1, timestamp_2: color_2}. Examples -------- >>> import numpy as np >>> color_keyframes = {1, (1, 0, 1), 3, (0, 0, 1)} >>> Animation.set_color_keyframes(color_keyframes) # doctest: +SKIP """ self.set_keyframes("color", keyframes)
[docs] 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. **kwargs : dict Additional parameters for the keyframe. """ self.set_keyframe("opacity", timestamp, opacity, **kwargs)
[docs] def set_opacity_keyframes(self, keyframes): """ Set a dict of opacity keyframes at once. Parameters ---------- keyframes : dict A dict with timestamps as keys and opacities as values. Should be in the following form: {timestamp_1: opacity_1, timestamp_2: opacity_2}. Notes ----- Opacity values should be between 0 and 1. Examples -------- >>> opacity = {1, (1, 1, 1), 3, (2, 2, 3)} >>> Animation.set_scale_keyframes(opacity) # doctest: +SKIP """ self.set_keyframes("opacity", keyframes)
[docs] def get_position(self, t): """ Return the interpolated position. Parameters ---------- t : float The time to interpolate position at. Returns ------- ndarray, shape (1, 3) The interpolated position. """ return self.get_value("position", t)
[docs] 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, shape (1, 3) or shape (1, 4) The interpolated rotation as Euler degrees by default or as quaternion if as_quat is True. """ 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()
[docs] def get_scale(self, t): """ Return the interpolated scale. Parameters ---------- t : float The time to interpolate scale at. Returns ------- ndarray, shape (1, 3) The interpolated scale. """ return self.get_value("scale", t)
[docs] def get_color(self, t): """ Return the interpolated color. Parameters ---------- t : float The time to interpolate color value at. Returns ------- ndarray, shape (1, 3) The interpolated color. """ return self.get_value("color", t)
[docs] def get_opacity(self, t): """ Return the opacity value. Parameters ---------- t : float The time to interpolate opacity at. Returns ------- ndarray or float The interpolated opacity. """ return self.get_value("opacity", t)
[docs] 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")
[docs] 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()
[docs] 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
[docs] def add_static_actor(self, actor): """ Add static actor(s) which will not be controlled/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
[docs] def remove_animations(self): """Remove all child Animations from the Animation.""" self._animations.clear()
[docs] 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)
[docs] 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
[docs] def add_update_callback(self, callback, prop=None): """ Add a function to be called each time animation is updated. Add a callback function that will be invoked whenever the animation is updated. The 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 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)
[docs] 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 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: 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=time) for animation in self._animations] if self._scene and not has_handler: self._scene.reset_clipping_range()
[docs] def add_to_scene(self, scene): """ Add this Animation, its actors and sub Animations to the scene. Parameters ---------- scene : fury.window.Scene The scene to add the animation, actors, and sub-animations to. """ [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) self._scene = scene self._added_to_scene = True self._start_time = perf_counter() self.update_animation(time=0)
[docs] def remove_from_scene(self, scene): """ Remove Animation, its actors and sub Animations from the scene. Parameters ---------- scene : fury.window.Scene The scene from which to remove the animation, actors, and sub-animations. """ [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
[docs] class CameraAnimation(Animation): """ Camera keyframe animation class. This is used for animating a single camera using a set of keyframes. Parameters ---------- camera : Camera, optional Camera to be animated. If None, active camera will be animated. length : float or int, optional The fixed length/duration of the animation in seconds. If None, the animation will derive its duration from the keyframes. loop : bool, optional Whether to loop the animation (True) or play once (False). motion_path_res : int, optional The number of line segments used to visualize the animation's motion path (position visualization). Higher values create smoother paths. Attributes ---------- camera : Camera Camera being animated by the CameraAnimation. duration : float The duration of the animation in seconds. loop : bool Whether the animation is in loop mode (True) or play one mode (False). """
[docs] def __init__(self, *, camera=None, length=None, loop=True, motion_path_res=None): """Initialize CameraAnimation.""" super(CameraAnimation, self).__init__( length=length, loop=loop, motion_path_res=motion_path_res ) self._camera = camera
[docs] def set_focal(self, timestamp, position, **kwargs): """ Set camera's focal position keyframe. Parameters ---------- timestamp : float The time to set the keyframe at. position : ndarray, shape (1, 3) The camera focal position. **kwargs : dict, optional Additional keyword arguments for the keyframe. The following parameters are supported: - in_cp: ndarray - The in control point for cubic Bézier interpolation. - out_cp: ndarray - The out control point for cubic Bézier interpolation. - in_tangent: ndarray - The in tangent at that position for cubic spline curve. - out_tangent: ndarray - The out tangent at that position for cubic spline curve. """ self.set_keyframe("focal", timestamp, position, **kwargs)
[docs] def set_view_up(self, timestamp, direction, **kwargs): """ Set the camera view-up direction keyframe. Parameters ---------- timestamp : float The time to set the keyframe at. direction : ndarray, shape (1, 3) The camera view-up direction vector. **kwargs : dict, optional Additional keyword arguments for the keyframe. The following parameters are supported: - in_cp: ndarray - The in control point for cubic Bézier interpolation. - out_cp: ndarray - The out control point for cubic Bézier interpolation. - in_tangent: ndarray - The in tangent at that position for cubic spline curve. - out_tangent: ndarray - The out tangent at that position for cubic spline curve. """ self.set_keyframe("view_up", timestamp, direction, **kwargs)
[docs] def set_focal_keyframes(self, keyframes): """ Set multiple camera focal position keyframes at once. Parameters ---------- keyframes : dict A dict with timestamps as keys and camera focal positions as values. Should be in the following form: {timestamp_1: focal_1, timestamp_2: focal_1, ...}. Examples -------- >>> focal_pos = {0, (1, 1, 1), 3, (20, 0, 0)} >>> CameraAnimation.set_focal_keyframes(focal_pos) # doctest: +SKIP """ self.set_keyframes("focal", keyframes)
[docs] def set_view_up_keyframes(self, keyframes): """ Set multiple camera view up direction keyframes. Parameters ---------- keyframes : dict A dict with timestamps as keys and camera view up vectors as values. Should be in the following form: {timestamp_1: view_up_1, timestamp_2: view_up_2, ...}. Examples -------- >>> view_ups = {0: np.array([1, 0, 0]), 3: np.array([0, 1, 0])} >>> CameraAnimation.set_view_up_keyframes(view_ups) # doctest: +SKIP """ self.set_keyframes("view_up", keyframes)
[docs] def get_focal(self, t): """ Return the interpolated camera's focal position. Parameters ---------- t : float The time to interpolate at. Returns ------- ndarray, shape (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)
[docs] 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, shape (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)
[docs] 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)
[docs] 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)
[docs] def update_animation(self, *, time=None): """ Update the camera animation. Parameters ---------- time : float or int, optional 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(tile=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()