3D animation

3D animation provides a way to visualize simulations in 3D.

Salabim supports the ‘classical’ 2D animation along with a 3D animation. The 3D animation is particularly useful for showing the functionality of a system to domain experts, where objects are moving up and down, like stacking systems, multi storey facilities and complex road infrastructure. But also just to make a visualy more attractive presentation (video).

3D animations always run in parallel with the tkinter (2D) window, to control the simulation (pausing, exiting, etc). Also custom buttons may be installed in that window. And of course, a 2D animation as well.

As with 2D animations, animation calls can be still given when animation is actually off. In that case, there is hardly any impact on the performance.

The 3D animation uses its own coordinate system.

All 3D animation objects are so call new style animation classes.

The following 3D classes are available as of now:

  • Animate3dBox
  • Animate3dBar
  • Animate3dCylinder
  • Animate3dRectangle
  • Animate3dLine
  • Animate3dObj

On top of that, animation of the components of a queue in 3D is accomplished with Animate3dQueue(). It is possible to use the standard shape of components, which is a box of size 1 in all directions. The queue can be build up in +x, -x, +y, -y, +z, -z direction. It is possible to limit the number of component shown.


The various classes have a lot of parameters, like color, x, x_ref, 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 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 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())

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.


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.


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().


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:

# Demo animate 1
import salabim as sim

class AnimateMovingText(sim.Animate):
    def __init__(self):
        sim.Animate.__init__(self, text="", x0=100, x1=1000, y0=100, t1=env.now() + 10)

    def x(self, t):
        return sim.interpolate(sim.interpolate(t, self.t0, self.t1, 0, 1) ** 2, 0, 1, self.x0, self.x1)

    def y(self, t):
        return int(t) * 50

    def text(self, t):
        return "{:0.1f}".format(t)

env = sim.Environment()



env.run(till=sim.inf)  # otherwise the simulation will end at t=0, because there are no events left

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. It is not necessary to use t0, t1, x0, x1, but is a convenient way of setting attributes.

The following methods may be overridden:

method circle image line polygon rectangle text

Dashboard animation

Here we present an example model where the simulation code is completely separated from the animation code. This makes communication and debugging and switching off animation much easier.

The example below generates 15 persons starting at time 0, 1, … . These persons enter a queue called q and stay there 15 time units.

The animation dashboard shows the first 10 persons in the queue q, along with the length of that q.

# Demo animate 2.py
import salabim as sim

class AnimateWaitSquare(sim.Animate):
    def __init__(self, i):
        self.i = i
            self, rectangle0=(-12, -10, 12, 10), x0=300 - 30 * i, y0=100, fillcolor0="red", linewidth0=0

    def visible(self, t):
        return q[self.i] is not None

class AnimateWaitText(sim.Animate):
    def __init__(self, i):
        self.i = i
        sim.Animate.__init__(self, text="", x0=300 - 30 * i, y0=100, textcolor0="white")

    def text(self, t):
        component_i = q[self.i]

        if component_i is None:
            return ""
            return component_i.name()

def do_animation():
    for i in range(10):
    show_length = sim.Animate(text="", x0=330, y0=100, textcolor0="black", anchor="w")
    show_length.text = lambda t: "Length= " + str(len(q))

class Person(sim.Component):
    def process(self):
        yield self.hold(15)

env = sim.Environment(trace=True)

q = sim.Queue("q")
for i in range(15):
    Person(name="{:02d}".format(i), at=i)



All animation initialization is in do_animation, where first 10 rectangle and text Animate objects are created. These are classes that are inherited from sim.Animate.

The AnimateWaitSquare defines a red rectangle at a specific position in the sim.Animate.__init__() call. Note that normally these squares should be displayed. But, here we have overridden the visible method. If there is no i-th component in the q, the square will be made invisible. Otherwise, it is visible.

The AnimateWaitText is more or less defined in a similar way. It defines a text in white at a specific position. Only the text method is overridden and will return the name of the i-th component in the queue, if any. Otherwise the null string will be returned.

The length of the queue q could be defined also by subclassing sim.Animate, but here we just make a direct instance of Animate with the null string as the text to be displayed. And then we immediately override the text method with a lambda function. Note that in this case, self is not available!

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:

# Show colornames

import salabim as sim

env = sim.Environment()
names = sorted(sim.colornames().keys())
env.modelname("show colornames")
x = 10
y = env.height() - 110
sx = 165
sy = 21

for name in names:
    sim.Animate(rectangle0=(x, y, x + sx, y + sy), fillcolor0=name)
        text=(name, "<null string>")[name == ""],
        x0=x + sx / 2,
        y0=y + sy / 2,
        textcolor0=("black", "white")[env.is_dark(name)],
    x += sx + 4
    if x + sx > 1024:
        y -= sy + 4
        x = 10


Ruuning 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


    import tkinter
except ImportError:

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


or it is possible to use a context manager to automatically close a video file, like:

with env.video('myvideo.mp4'):

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 pngs 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, .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.


will write individual autonumbered frames named


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 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.