[docs]classTimeline:"""Keyframe animation Timeline. 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 ---------- 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 animations that it controls automatically. loop : bool, optional Whether loop playing the timeline or play once. """@warn_on_args_to_kwargs()def__init__(self,*,animations=None,playback_panel=False,loop=True,length=None):self._scene=Noneself.playback_panel=Noneself._current_timestamp=0self._speed=1.0self._last_started_time=0self._playing=Falseself._animations=[]self._loop=loopself._length=lengthself._duration=lengthiflengthisnotNoneelse0.0ifplayback_panel:defset_loop(is_loop):self._loop=is_loopdefset_speed(speed):self.speed=speedself.playback_panel=PlaybackPanel(loop=self._loop)self.playback_panel.on_play=self.playself.playback_panel.on_stop=self.stopself.playback_panel.on_pause=self.pauseself.playback_panel.on_loop_toggle=set_loopself.playback_panel.on_progress_bar_changed=self.seekself.playback_panel.on_speed_changed=set_speedifanimationsisnotNone:self.add_animation(animations)
[docs]defupdate_duration(self):"""Update and return the duration of the Timeline. Returns ------- float The duration of the Timeline. """ifself._lengthisnotNone:self._duration=self._lengthelse:self._duration=max([0.0]+[anim.update_duration()foraniminself._animations])ifself.has_playback_panel:self.playback_panel.final_time=self.durationreturnself.duration
@propertydefduration(self):"""Return the duration of the Timeline. Returns ------- float The duration of the Timeline. """returnself._duration
[docs]defplay(self):"""Play the animation"""ifnotself.playing:ifself.current_timestamp>=self.duration:self.current_timestamp=0self._last_started_time=(perf_counter()-self._current_timestamp/self.speed)self._playing=True
[docs]defpause(self):"""Pause the animation"""self._current_timestamp=self.current_timestampself._playing=False
[docs]defstop(self):"""Stop the animation"""self._current_timestamp=0self._playing=Falseself.update(force=True)
[docs]defrestart(self):"""Restart the animation"""self._current_timestamp=0self._playing=Trueself.update(force=True)
@propertydefcurrent_timestamp(self):"""Get current timestamp of the Timeline. Returns ------- float The current time of the Timeline. """ifself.playing:self._current_timestamp=(perf_counter()-self._last_started_time)*self.speedreturnself._current_timestamp@current_timestamp.setterdefcurrent_timestamp(self,timestamp):"""Set the current timestamp of the Timeline. Parameters ---------- timestamp: float The time to set as current time of the Timeline. """self.seek(timestamp)
[docs]defseek(self,timestamp):"""Set the current timestamp of the Timeline. Parameters ---------- timestamp: float The time to seek. """# assuring timestamp value is in the timeline rangeiftimestamp<0:timestamp=0eliftimestamp>self.duration:timestamp=self.durationifself.playing:self._last_started_time=perf_counter()-timestamp/self.speedelse:self._current_timestamp=timestampself.update(force=True)
[docs]defseek_percent(self,percent):"""Seek a percentage of the Timeline's final timestamp. Parameters ---------- percent: float Value from 1 to 100. """t=percent*self.duration/100self.seek(t)
@propertydefplaying(self):"""Return whether the Timeline is playing. Returns ------- bool True if the Timeline is playing. """returnself._playing@propertydefstopped(self):"""Return whether the Timeline is stopped. Returns ------- bool True if Timeline is stopped. """returnnotself.playingandnotself._current_timestamp@propertydefpaused(self):"""Return whether the Timeline is paused. Returns ------- bool True if the Timeline is paused. """returnnotself.playingandself._current_timestampisnotNone@propertydefspeed(self):"""Return the speed of the timeline's playback. Returns ------- float The speed of the timeline's playback. """returnself._speed@speed.setterdefspeed(self,speed):"""Set the speed of the timeline's playback. Parameters ---------- speed: float The speed of the timeline's playback. """current=self.current_timestampifspeed<=0:returnself._speed=speedself._last_started_time=perf_counter()self.current_timestamp=current@propertydefloop(self):"""Get loop condition of the timeline. Returns ------- bool Whether the playback is in loop mode (True) or play one mode (False). """returnself._loop@loop.setterdefloop(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@propertydefhas_playback_panel(self):"""Return whether the `Timeline` has a playback panel. Returns ------- bool: 'True' if the `Timeline` has a playback panel. otherwise, 'False' """returnself.playback_panelisnotNone
[docs]@warn_on_args_to_kwargs()defrecord(self,*,fname=None,fps=30,speed=1.0,size=(900,768),order_transparent=True,multi_samples=8,max_peels=4,show_panel=False,):"""Record the animation Parameters ---------- fname : str, optional The file name. Save a GIF file if name ends with '.gif', or mp4 video if name ends with'.mp4'. If None, this method will only return an array of frames. fps : int, optional The number of frames per second of the record. size : (int, int) ``(width, height)`` of the window. Default is (900, 768). speed : float, optional, default 1.0 The speed of the animation. order_transparent : bool, optional Default False. Use depth peeling to sort transparent objects. If True also enables anti-aliasing. multi_samples : int, optional Number of samples for anti-aliasing (Default 8). For no anti-aliasing use 0. max_peels : int, optional Maximum number of peels for depth peeling (Default 4). show_panel : bool, optional, default False Controls whether to show the playback (if True) panel of hide it (if False) Returns ------- ndarray: The recorded frames. Notes ----- It's recommended to use 50 or 30 FPS while recording to a GIF file. """ext=os.path.splitext(fname)[-1]mp4=ext==".mp4"ifmp4:try:importcv2exceptImportErroraserr:raiseImportError("OpenCV must be installed in order to ""save as MP4 video.")fromerrfourcc=cv2.VideoWriter.fourcc(*"mp4v")out=cv2.VideoWriter(fname,fourcc,fps,size)duration=self.durationstep=speed/fpsframes=[]t=0scene=self._sceneifnotscene:scene=window.Scene()scene.add(self)_hide_panel=Falseifself.has_playback_panelandnotshow_panel:self.playback_panel.hide()_hide_panel=Truerender_window=RenderWindow()render_window.SetOffScreenRendering(1)render_window.AddRenderer(scene)render_window.SetSize(*size)iforder_transparent:window.antialiasing(scene,render_window,multi_samples,max_peels,0)render_window=RenderWindow()render_window.SetOffScreenRendering(1)render_window.AddRenderer(scene)render_window.SetSize(*size)iforder_transparent:window.antialiasing(scene,render_window,multi_samples,max_peels,0)window_to_image_filter=WindowToImageFilter()print("Recording...")whilet<duration:self.seek(t)render_window.Render()window_to_image_filter.SetInput(render_window)window_to_image_filter.Update()window_to_image_filter.Modified()vtk_image=window_to_image_filter.GetOutput()h,w,_=vtk_image.GetDimensions()vtk_array=vtk_image.GetPointData().GetScalars()components=vtk_array.GetNumberOfComponents()snap=numpy_support.vtk_to_numpy(vtk_array).reshape(w,h,components)corrected_snap=np.flipud(snap)ifmp4:cv_img=cv2.cvtColor(corrected_snap,cv2.COLOR_RGB2BGR)out.write(cv_img)else:pillow_snap=Image.fromarray(corrected_snap)frames.append(pillow_snap)t+=stepprint("Saving...")iffnameisNone:returnframesifmp4:out.release()else:frames[0].save(fname,append_images=frames[1:],loop=0,duration=1000/fps,save_all=True,)if_hide_panel:self.playback_panel.show()returnframes
[docs]defadd_animation(self,animation):"""Add Animation or list of Animations. Parameters ---------- animation: Animation or list[Animation] or tuple[Animation] Animation/s to be added. """ifisinstance(animation,(list,tuple)):[self.add_animation(anim)foraniminanimation]elifisinstance(animation,Animation):animation._timeline=selfself._animations.append(animation)self.update_duration()else:raiseTypeError("Expected an Animation, a list or a tuple.")
@propertydefanimations(self)->"list[Animation]":"""Return a list of Animations. Returns ------- list: List of Animations controlled by the timeline. """returnself._animations
[docs]@warn_on_args_to_kwargs()defupdate(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_timestampifself.has_playback_panel:self.playback_panel.current_time=timeiftime>self.duration:ifself._loop:self.seek(0)else:self.seek(self.duration)# Doing this will pause both the timeline and the panel.ifself.has_playback_panel:self.playback_panel.pause()else:self.pause()ifself.playingorforce:[anim.update_animation(time=time)foraniminself._animations]
[docs]defadd_to_scene(self,scene):"""Add Timeline and all of its Animations to the scene"""self._scene=sceneifself.has_playback_panel:self.playback_panel.add_to_scene(scene)[animation.add_to_scene(scene)foranimationinself._animations]
[docs]defremove_from_scene(self,scene):"""Remove Timeline and all of its Animations to the scene"""self._scene=Noneifself.has_playback_panel:scene.rm(*tuple(self.playback_panel.actors))[animation.remove_from_scene(scene)foranimationinself._animations]