8
8
# text.set_position()
9
9
# quiver.set_offsets(), quiver.set_UVC()
10
10
# FancyArrow.set_xy()
11
-
11
+ import os . path
12
12
import numpy as np
13
13
import matplotlib .pyplot as plt
14
14
from matplotlib import animation
15
+ from numpy .lib .arraysetops import isin
15
16
from spatialmath import base
16
17
from collections .abc import Iterable , Generator , Iterator
18
+ import time
17
19
20
+ # global variable holds reference to FuncAnimation object, this is essential
21
+ # for animatiion to work
22
+ _ani = None
18
23
19
24
class Animate :
20
25
"""
@@ -131,9 +136,9 @@ def trplot(self, end, start=None, **kwargs):
131
136
self .start = start
132
137
133
138
# draw axes at the origin
134
- base .trplot (self .start , axes = self , block = None , ** kwargs )
139
+ base .trplot (self .start , axes = self , ** kwargs )
135
140
136
- def run (self , movie = None , axes = None , repeat = False , interval = 50 , nframes = 100 , pause = 0 , ** kwargs ):
141
+ def run (self , movie = None , axes = None , repeat = False , interval = 50 , nframes = 100 , wait = False , ** kwargs ):
137
142
"""
138
143
Run the animation
139
144
@@ -147,6 +152,8 @@ def run(self, movie=None, axes=None, repeat=False, interval=50, nframes=100, pau
147
152
:type interval: int
148
153
:param movie: name of file to write MP4 movie into
149
154
:type movie: str
155
+ :param wait: wait until animation is complete, default False
156
+ :type wait: bool
150
157
151
158
Animates a 3D coordinate frame moving from the world frame to a frame
152
159
represented by the SO(3) or SE(3) matrix to the current axes.
@@ -159,51 +166,63 @@ def run(self, movie=None, axes=None, repeat=False, interval=50, nframes=100, pau
159
166
"""
160
167
161
168
def update (frame , animation ):
162
- if self .trajectory is not None :
163
- # passed a trajectory as an iterator or generator, get next
164
- try :
165
- T = next (self .trajectory )
166
- except StopIteration :
167
- animation .done = True
168
- return
169
- else :
169
+
170
+ # frame is the result of calling next() on a iterator or generator
171
+ # seemingly the animation framework isn't checking StopException
172
+ # so there is no way to know when this is no longer called.
173
+ # we implement a rather hacky heartbeat style timeout
174
+
175
+ if isinstance (frame , float ):
170
176
# passed a single transform, interpolate it
171
- T = base .trinterp (start = self .start , end = self .end , s = frame / nframes )
177
+ T = base .trinterp (start = self .start , end = self .end , s = frame )
178
+ else :
179
+ # assume it is an SO(3) or SE(3)
180
+ T = frame
181
+
172
182
# ensure result is SE(3)
173
183
if T .shape == (3 ,3 ):
174
184
T = base .r2t (T )
175
185
176
186
# update the scene
177
187
animation ._draw (T )
178
188
179
- # are we done yet
180
- if frame == nframes - 1 :
181
- animation .done = True
189
+ self .count += 1 # say we're still running
190
+
182
191
return animation .artists ()
183
192
193
+ global _ani
194
+
184
195
# blit leaves a trail and first frame
185
196
if movie is not None :
186
197
repeat = False
187
198
188
- self .done = False
199
+ self .count = 1
189
200
if self .trajectory is not None :
190
201
if not isinstance (self .trajectory , Iterator ):
191
202
# make it iterable, eg. if a list or tuple
192
203
self .trajectory = iter (self .trajectory )
193
- frames = None
204
+ frames = self . trajectory
194
205
else :
195
- frames = range ( 0 , nframes )
206
+ frames = iter ( np . linspace ( 0 , 1 , nframes ) )
196
207
197
- # ani = animation.FuncAnimation(fig=plt.gcf(), func=update, frames=range(0, nframes), fargs=(self,), blit=False, interval=interval, repeat=repeat)
198
- ani = animation .FuncAnimation (fig = plt .gcf (), func = update , frames = frames , fargs = (self ,), blit = False , interval = interval , repeat = repeat )
199
- if movie is None :
200
- while repeat or not self .done :
201
- plt .pause (0.1 )
202
- else :
208
+ _ani = animation .FuncAnimation (fig = plt .gcf (), func = update , frames = frames , fargs = (self ,), blit = False , interval = interval , repeat = repeat )
209
+
210
+ if movie is not None :
203
211
# Set up formatting for the movie files
204
- print ('creating movie' , movie )
205
- FFwriter = animation .FFMpegWriter (fps = 10 , extra_args = ['-vcodec' , 'libx264' ])
206
- ani .save (movie , writer = FFwriter )
212
+ if os .path .exists (movie ):
213
+ print ('overwriting movie' , movie )
214
+ else :
215
+ print ('creating movie' , movie )
216
+ FFwriter = animation .FFMpegWriter (fps = 1000 / interval , extra_args = ['-vcodec' , 'libx264' ])
217
+ _ani .save (movie , writer = FFwriter )
218
+
219
+ if wait :
220
+ # wait for the animation to finish. Dig into the timer for this
221
+ # animation and wait for its callback to be deregistered.
222
+ while True :
223
+ plt .pause (0.25 )
224
+ if len (_ani .event_source .callbacks ) == 0 :
225
+ break
207
226
208
227
def __repr__ (self ):
209
228
"""
@@ -550,7 +569,10 @@ def update(frame, a):
550
569
plt .pause (1 )
551
570
else :
552
571
# Set up formatting for the movie files
553
- print ('creating movie' , movie )
572
+ if os .path .exists (movie ):
573
+ print ('overwriting movie' , movie )
574
+ else :
575
+ print ('creating movie' , movie )
554
576
FFwriter = animation .FFMpegWriter (fps = 10 , extra_args = ['-vcodec' , 'libx264' ])
555
577
ani .save (movie , writer = FFwriter )
556
578
0 commit comments