Animation
Animation is a powerful tool to debug, test and demonstrate simulations.
It is possible to show a number of shapes (lines, rectangles, circles, etc), texts as well (images) in a window. These objects can be dynamically updated. Monitors may be animated by showing the current value against the time. Furthermore the components in a queue may be shown in a highly customizable way. As text animation may be dynamically updated, it is even possible to show the current state, (monitor) statistics, etc. in the animation windows.
Salabim’s animation engine also allows some user input.
It is important to realize that animation calls can be still given when animation is actually off. In that case, there is hardly any impact on the performance.
Salabim animations can be
synchronized with the simulation clock and run in real time (synchronized)
advanced per simulation event (non synchronized)
In synchronized mode, one time unit in the simulation can correspond to any period in real time, e.g.
1 time unit in simulation time –> 1 second real time (speed = 1) (default)
1 time unit in simulation time –> 4 seconds real time (speed = 0.25)
4 time units in simulation time –> 1 second real time (speed = 4)
The most common way to start an animation is by calling
`` env.animate(True)`` or with a call to animation_parameters(animate=True)
.
Animations can be started and stopped during execution (i.e. run).
The animation uses a coordinate system that -by default- is in screen pixels. The lower left corner is (0,0).
But, the user can change both the coordinate of the lower left corner (translation) as well as set the x-coordinate
of the lower right hand corner (scaling). Note that x- and y-scaling are always the same.
Furthermore, it is possible to specify the colour of the background with animation_parameters(background_color=)
or background_color
.
Prior to version 2.3.0 there was actually just one animation object class: Animate. This interface is described later as the new animation classes are easier to use and even offer some additional functionality.
New style animation classes can be used to put texts, rectangles, polygon, lines, series of points, circles or images on the screen. All types can be connected to an optional text.
Here is a sample program to show all the new style animation classes
env = sim.Environment()
env.animate(True)
env.modelname("Demo animation classes")
env.background_color("90%gray")
sim.AnimatePolygon(spec=(100, 100, 300, 100, 200, 190), text="This is\na polygon")
sim.AnimateLine(spec=(100, 200, 300, 300), text="This is a line")
sim.AnimateRectangle(spec=(100, 10, 300, 30), text="This is a rectangle")
sim.AnimateCircle(radius=60, x=100, y=400, text="This is a cicle")
sim.AnimateCircle(radius=60, radius1=30, x=300, y=400, text="This is an ellipse")
sim.AnimatePoints(spec=(100, 500, 150, 550, 180, 570, 250, 500, 300, 500), text="These are points")
sim.AnimateText(text="This is a one-line text", x=100, y=600)
sim.AnimateText(
text="""\
Multi line text
-----------------
Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute
irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla
pariatur.
Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
""",
x=500,
y=100,
)
sim.AnimateImage("flowers.jpg", x=500, y=400, width=500, text="Flowers", fontsize=150)
env.run(env.inf)
Resulting in:
Animation of the components of a queue is accomplished with AnimateQueue()
or Queue.animate()
.
It is possible to use the standard shape of components, which is a rectangle with the sequence number or define
your own shape(s). The queue can be build up in west, east, north or south directions. Or it
can follow a trajectory (see the chapter on Trajectory).
It is possible to limit the number of component shown.
Monitors can be visualized dynamically with AnimateMonitor()
.
These features are demonstrated in Demo queue animation.py
import salabim as sim
sim.yieldless(False)
'''
This is a demonstration of several ways to show queues dynamically and the corresponding statistics
The model simply generates components that enter a queue and leave after a certain time.
Note that the actual model code (in the process description of X does not contain any reference
to the animation!
'''
class X(sim.Component):
def setup(self, i):
self.i = i
def animation_objects(self, id):
'''
the way the component is determined by the id, specified in AnimateQueue
'text' means just the name
any other value represents the colour
'''
if id == 'text':
ao0 = sim.AnimateText(text=self.name(), textcolor='fg', text_anchor='nw')
return 0, 16, ao0
else:
ao0 = sim.AnimateRectangle((-20, 0, 20, 20),
text=self.name(), fillcolor=id, textcolor='white', arg=self)
return 45, 0, ao0
def process(self):
while True:
yield self.hold(sim.Uniform(0, 20)())
self.enter(q)
yield self.hold(sim.Uniform(0, 20)())
self.leave()
env = sim.Environment(trace=False)
env.background_color('20%gray')
q = sim.Queue('queue')
qa0 = sim.AnimateQueue(q, x=100, y=50, title='queue, normal', direction='e', id='blue')
qa1 = sim.AnimateQueue(q, x=100, y=250, title='queue, maximum 6 components', direction='e', max_length=6, id='red')
qa2 = sim.AnimateQueue(q, x=100, y=150, title='queue, reversed', direction='e', reverse=True, id='green')
qa3 = sim.AnimateQueue(q, x=100, y=440, title='queue, text only', direction='s', id='text')
sim.AnimateMonitor(q.length, x=10, y=450, width=480, height=100, horizontal_scale=5, vertical_scale=5)
sim.AnimateMonitor(q.length_of_stay, x=10, y=570, width=480, height=100, horizontal_scale=5, vertical_scale=5)
sim.AnimateText(text=lambda: q.length.print_histogram(as_str=True), x=500, y=700,
text_anchor='nw', font='narrow', fontsize=10)
sim.AnimateText(text=lambda: q.print_info(as_str=True), x=500, y=340,
text_anchor='nw', font='narrow', fontsize=10)
[X(i=i) for i in range(15)]
env.animate(True)
env.modelname('Demo queue animation')
env.run()
Here is the animation:
Advanced
The various classes have a lot of parameters, like color, line width, font, etc.
These parameters can be given just as a scalar, like:
sim.AnimateText(text='Hello world', x=200, y=300, textcolor='red')
But each of these parameters may also be a:
function with zero arguments
function with zero arguments with defaults
function with one argument being the time t
function with two arguments being ‘arg’ and the time t
a method with instance ‘arg’ and the time t
The second form is very useful to refer to the ‘parent’ component, like in
class X(sim.Component:
def setup(self):
self.message='Hello'
sim.AnimateText(text=lambda self=self: self.message)
The function or method is called at each animation frame update (maximum of 30 frames per second).
This makes it for instance possible to show dynamically the mean of monitor m, like in
sim.AnimateRectangle(spec=(10, 10, 200, 30), text=lambda: str(m.mean())
It is also possible and indeed very useful to later change attributes
an = sim.AnimateImage(image="im1.jpg", x=100, y=100)
...
an.image = "im2.jpg"
...
an.x = lambda t: t * 10
Class AnimateLine
This class makes it possible to draw a straight line or a series of connected straight lines. The line width, line color may be specified.
When a text is given, it will be placed according to the text_anchor parameter.
Here are some example of usage of AnimateLine:
Class AnimatePoints
This class is essentially the same as AnimateLine, with the main difference that only the begin and end point of the line segments will be drawn.
Here are some example of usage of AnimatePoints:
Class AnimatePolygon
This class is essentially the same as AnimateLine, with the main difference that the polygon will always be closed (end point=begin point) and that the shape can be filled.
Here are some example of usage of AnimatePolygon:
Class AnimateRectangle
This class makes can be used to show a rectangle.
Here are some example of usage of AnimateRectangle:
Class AnimateCircle
This class can be used to draw circles and circle segments (and ellipses).
In case of circle segments, the arcs can be drawn as well.
The circle can be filled or not.
Here are some example of usage of AnimateCircle:
Class AnimateImage
This class can be use to show an image, which can either specified as a filename, a URL or a PIL image. It is possible to scale, rotate, flip and position the image.
Salabim supports in .jpg, .png, .gif, .webp, .bmp, .ico and .tiff file formats. It respects the alpha channel (transparency) information.
Here are some example of usage of AnimateImage:
It is also possible to specify a filename within a zip file, by specifying image as <zipfile>|<filename> like cars.png|bmw.png.
It is possible to use AnimateImage to show animated GIFs or animates .webp files. Including repeat and pingpong.
Class AnimateText
This class can be used to display a text. It is possible to specify font and fontsize.
The text_anchor parameter controls where to place the text relative to the given x and y.
Here are some example of usage of AnimateText:
Class Animate
This class can be used to show:
line (if line0 is specified)
rectangle (if rectangle0 is specified)
polygon (if polygon0 is specified)
circle (if circle0 is specified)
text (if text is specified)
image (if image is specified)
Note that only one type is allowed per instance of Animate.
Nearly all attributes of an Animate object are interpolated between time t0 and t1. If t0 is not specified, now() is assumed. If t1 is not specified inf is assumed, which means that the attribute will be the ‘0’ attribute.
E.g.:
Animate(x0=100,y0=100,rectangle0==(-10,-10,10,10))
will show a square around (100,100) for ever
Animate(x0=100,y0=100,x1=200,y1=0,rectangle0=(-10,-10,10,10))
will still show the same square around (100,100) as t1 is not specified
Animate(t1=env.now()+10,x0=100,y0=100,x1=200,y1=0,rectangle0=(-10,-10,10,10))
will show a square moving from (100,100) to (200,0) in 10 units of time.
It also possible to let the rectangle change shape over time:
Animate(t1=env.now(),x0=100,y0=100,x1=200,y1=0,rectangle0=(-10,-10,10,10),rectangle1=(-20,-20,20,20))
will show a moving and growing rectangle.
By default, the animation object will not change anymore after t1, but will remain visible. Alternatively, if keep=False is specified, the object will disappear at time t1.
Also, colors, fontsizes, angles can be changed in a linear way over time.
E.g.:
Animate(t1=env.now()+10,text='Test',textcolor0='red',textcolor1='blue',angle0=0,angle1=360)
will show a rotating text changing from red to blue in 10 units of time.
The animation object can be updated with the update method. Here, once again, all the attributes can be specified to change over time. Note that the defaults for the ‘0’ values are the actual values at t=now().
Thus,
an=Animate(t0=0,t1=10,x0=0,x1=100,y0=0,circle0=(10,),circle1=(20,))
will show a horizontally moving, growing circle.
Now, at time t=5, we issue
an.update(t1=10,y1=50,circle1=(10,))
Then x0 will be set 50 (halfway 0 an 100) and cicle0 to (15,) (halfway 10 and 20).
Thus the circle will shrink to its original size and move vertically from (50,0) to (50,50).
This concept is very useful for moving objects whose position and orientation are controlled by the simulation.
Here we explain how an attribute changes during time. We use x as an example. Normally, x=x0 at t=t0 and x=x1 at t>=t1. between t=t0 and t=t1, x is linearly interpolated. An application can however override the x method. The prefered way is to subclass the Animate class
import salabim as sim
sim.yieldless(False)
class AnimateMovingText(sim.Animate):
def __init__(self):
sim.Animate.__init__(self, text="", x0=100, x1=1000, t1=env.now() + 10)
def y(self, t):
return int(t) * 50 + 20
def text(self, t):
return f"{t:0.1f}"
env = sim.Environment()
env.animate(True)
AnimateMovingText()
env.run(12)
This code will show the current simulation time moving from left to right, uniformly accelerated. And the text will be shown a bit higher up, every second.
Here’s the animation:
The following methods may be overridden:
method circle image line polygon rectangle text
------------- ------ ----- ---- ------- --------- ----
anchor -
angle - - - - - -
circle -
fillcolor - - -
fontsize -
image -
layer - - - - - -
line -
linecolor - - - -
linewidth - - - -
max_lines -
offsetx - - - - - -
offsety - - - - - -
polygon -
rectangle -
text -
text_anchor -
textcolor -
visible - - - - - -
width -
x - - - - - -
xy_anchor - - - - - -
y - - - - - -
------------- ------ ----- ---- ------- --------- ----
Using layers
Each Animatexxx class has a parameter layer
, which defaults to 0.
The user has control over the placement of objects by specifying a layer number. lower layer numbers come on top of higher layer numbers.
Thus,
sim.AnimateRectangle(spec=(0, 0, 200, 200), fillcolor="blue" ,text="layer=1", layer=-1)
sim.AnimateRectangle(spec=(150, 150, 350, 350), fillcolor="red", text="layer=0")
sim.AnimateRectangle(spec=(300, 300, 500, 500), fillcolor="green" ,text="layer=1", layer=1)
will show as:
Using colours
When a colour has to be specified in one of the animation methods, salabim offers a choice of specification:
#rrggbb rr, gg, bb in hex, alpha=255
#rrggbbaa rr, gg, bb, aa in hex, alpha=aa
(r, g, b) r, g, b in 0-255, alpha=255
(r, g, b, a) r, g, b in 0-255, alpha=a
“fg” current foreground color
“bg” current background color
colorname alpha=255
(colorname, a) alpha=a
The colornames are defined as follows:
This output can be generated with the following program
env = sim.Environment()
env.show_time(False)
env.animate(True)
env.background_color("90%gray")
for i, name in enumerate(sorted(env.colornames())):
x = 7 + (i % 6) * 170
y = 720 - (i // 6) * 28
env.AnimateRectangle(
(0, 0, 160, 21),
x=x,
y=y,
fillcolor=name,
text=(name, "<null string>")[name == ""],
textcolor=("black", "white")[env.is_dark(name)],
font="calibribold",
fontsize=17,
linecolor="black",
)
env.run(env.inf)
Advanced animation with animated images
Here’s a script that demonstrates animated images during animation.
import salabim as sim
sim.yieldless(False)
env = sim.Environment()
class X(sim.Component):
def animation_objects1(self):
an1 = env.AnimateImage("https://salabim.org/bird.gif", animation_repeat=True, width=50, offsety=-15)
an2 = env.AnimateText(text=f"{self.sequence_number()}", offsetx=15, offsety=-25, fontsize=13)
return 50, 50, an1, an2
def process(self):
self.enter(env.q)
yield self.hold(env.Uniform(5))
self.leave(env.q)
env.speed(3)
env.background_color(("#eeffcc"))
env.width(1000, True)
env.height(700)
env.animate(True)
env.AnimateImage("https://salabim.org/bird.gif", animation_repeat=True, width=150, x=lambda t: 1000 - 30 * t, y=150)
env.AnimateImage("https://salabim.org/bird.gif", animation_repeat=True, width=300, x=lambda t: 1000 - 60 * t, y=220)
env.AnimateImage(
"https://salabim.org/bird.gif",
animation_repeat=True,
width=100,
animation_speed=1.5,
x=lambda t: 0 + 100 * (t - 25),
y=350,
animation_start=25,
flip_horizontal=True,
)
env.AnimateImage("https://salabim.org/bird.gif", animation_repeat=True, width=240, animation_speed=0.5, x=lambda t: 1000 - 50 * t, y=380)
env.AnimateImage("https://salabim.org/bird.gif", animation_repeat=True, width=250, animation_speed=1.3, x=lambda t: 1000 - 40 * t, y=480)
env.q = env.Queue("queue")
env.q.animate(x=700, y=50, direction="w")
env.ComponentGenerator(X, iat=env.Exponential(1.5))
env.run()
Note that this model uses animated gifs for the animation objects in the queue. And it uses the flip_vertical feature to mirror an image.
Here is the animation:
Running animations on PyDroid3
In order to run animations on PyDroid3 platforms, it required that the main program imports tkinter
import tkinter
Note that it can’t harm to include this import on non PyDroid3 platforms, apart from Pythonista, where tkinter is not available. In order to make a platform independent animation, you could use
if not sim.Pythonista:
import tkinter
or
try:
import tkinter
except ImportError:
pass
Avoiding crashes in tkinter
When animating a large number of objects, it is possible that tkinter crashes because there are too many tkinter bitmaps aka canvas objects, sometimes by issuing a ‘Fail to allocate bitmap’, sometimes without any message. Salabim limits the number of bitmap automatically by combining animation objects in one aggregated bitmap if the number of bitmaps exceeds a given maximum. Unfortunately it is not possible to detect this ‘Fail to allocate bitmap’, so it may take some experimentation to find a workable maximum (maybe going as low as 1000).
By default, salabim sets the maximum number of bitmaps to 4000, but may be changed with the Environment.maximum_number_of_bitmaps() method, or the maximum_number_of_bitmaps parameter of Environment.animation_parameters(). Choosing a too low maximum (particularly 0), may result in a performance degradation. The bitmap aggregation process is transparent to the user.
Note that does this not apply to the Pythonista implementation, where bitmaps are always aggregated.
Video production and snapshots
An animation can be recorded as an .mp4 (not under PyDroid3) or .avi video by specifying video=filename
in the call to animation_parameters, or issue video(filename)
.
The effect is that 30 times per second (scaled animation time) a frame is written. In this case, the animation does not
run synchronized with the wall clock any more. Depending on the complexity of the animation, the simulation might run
faster of slower than real time. In contrast to an ordinary animation, frames are never skipped.
The video has to be closed explicity with
env.video(False)
or
env.video_close()
or it is possible to use a context manager to automatically close a video file, like:
with env.video('myvideo.mp4'):
...
env.run(10)
This will automatically close the file myvideo.mp4 upon leaving the with block.
It is also possible to create an animated gif or animated png files by specifying a .gif or .png file. In that case, repeat and pingpong are additional options. Note that animated gif/png are considerable bigger than ordinary video files. So, try and limit the length to 10 seconds. Animated png, gif and webp files may be written with a transparent background (alpha < 255).
Video production supports also the creation of a series of individual frames, in .jpg, .gif, .png, .webp, .tiff or .bmp format. In this case, the video name has to contain an asterisk (*) which will be expanded at runtime to a 6 digit zero padded frame number, e.g.
env.video('test*.jpg')
will write individual autonumbered frames named
test000000.jpg
test000001.jpg
test000002.jpg
...
Prior to creating the frames, all files matching the specification will be removed, in order to get only the required frames, most likely for post processing with ffmpeg or similar.
Note that individual frame video production and animated gif/png/webp production are available on all platforms, including Pythonista.
Salabim also supports taking a snapshot of an animated screen with Environment.snapshot()
.
Video creation on machines that do not support tkinter
On some servers, tkinter is not available. In that case it is still possible to create videos.
That can be done by setting the blind_animation=True
in the call to sim.Environment
.
Note that this can also be used to (slightly) increase the performance of video production.
Audio support
On Windows platforms, it is possible to add an audio track to a video.
With Environment.audio()
an audio track (usually an mp3 file) will be added.
The audio may stopped by issueing audio("")
.
If another audio is started, the current audio, if any, will be stopped.
Adding audio to a video requires that ffmpeg is installed and in the search path. Refer to www.ffmpeg.org for downloads and instructions.
In order to develop lip synced videos, it is possible to play audios parallel to a simulation, provided the animation speed is equal to the audio_speed (1 by default). Audio playback is supported on Pythonista and Windows platforms only.