Animated Diagram Layers

Creating meaningful documentation is hard. Written paragraphs that are never read are useless. Text without a clear storyline is hard to understand, hard to remember, and often ambiguous. Diagrams are a great tool for communication because they are a direct connection to the perception of the readers.

But sometimes we can do even more than create a static diagram. Sometimes a video or an animation is even better to describe a solution or an idea.

The creation of such content can be intimidating. In this post, I will show you how I made a simple animation to visualize the extraction of a framework animation out of a microservices’ architecture.

This is visualized by tilting the original diagram, lifting the framework out of the architecture, and then showing the usage of the framework by the indication of the border color of the hexagons in the last step.

I created an easy-to-read Ruby script, that uses mostly ImageMagick tools and FFmpeg. All the code is available on GitHub.

If you want to find out more about how I draw these diagrams, check out this page.

Why do animations work so well?

Everybody is watching videos online. Be it for entertainment or education. It is a pretty common medium for communication nowadays.

And communication is the key point here. A software architect’s central task is to communicate ideas, decisions, concepts and plans well. They have to be able to talk to non-technical stakeholders as well as developers and engineers.

There is scientific proof that communication via pictorial material is more efficient than communicating via text (written or spoken). In an article published in the “Proceedings of the National Academy of Sciences” in 1998 the scientists wrote:

A striking characteristic of human memory is that pictures are remembered better than words.
From an evolutionary perspective, the ability to remember various aspects of one’s visual environment must be vital for survival,
so it is not surprising that memory for pictorial material is particularly well-developed.

https://www.pnas.org/content/95/5/2703

So, sometimes it is worth going the extra mile to drive a point home and make your audience understand within seconds what you are trying to tell them. Here is how I made this.

The General Idea

The animation is split into multiple steps, as shown in the following chart.

Steps of the animation.

Tools from ImageMagick are used to transform a single input image to create frames. ffmpeg merges the frames of all steps into a video or animated GIF.

Video rendering process

Depending on the desired frame rate of the output animation, the number of frames is calculated based on the desired duration of the animation steps.

Creating the diagrams

As a basis for the animation, I created 3 separate diagrams that represent layers using diagrams.net.

If you want to find out more about how I draw these diagrams, check out this page. Press the buttons to open the diagrams in diagrams.net.

System overview (without framework)

Layer 0

Shows the system without the framework.

Open in diagrams.net
Framework

Layer 1

Shows the extracted framework.

Open in diagrams.net
System overview (using framework)

Layer 2

Shows the system with the used framework. Indicated by the black border.

Open in diagrams.net

Each layer has to have the same size and alignment so that stacking them in an animation is easier. The best way to achieve this is to duplicate “Layer 0” and make the needed changes.

A transparent square shape is used as the background to have full control of the exported dimensions and margins. When exporting diagrams from diagrams.net, the size of the resulting image is the minimal bounding box of all elements of the diagram.

The size of the exported layers is the key influencer of the image quality. Sadly, the process that is shown here only works with the PNG format. JPEG doesn’t support transparent background, which is needed for some animation steps. SVG is not supported well by the used tools. So make sure that you export your layers with the appropriate size and DPI settings.

Frame Creation

The following sections describe how individual frames are created. 

Please keep in mind, that each command is just creating a single frame.

The script that I wrote to generate all frames uses a loop of the size depending on the frame rate. Please refer to this section of this post to find out more about the script itself.

Step 1

For tilting the image, I used a script from Fred’s ImageMagick Scripts. It is called 3Drotate and can do exactly what we need.

The partly rendered animation of this step looks like this:

The Command

$ 3Drotate \
  tilt=35.0 \
  bgcolor='white -alpha remove -alpha off' \
  '/usr/src/app/layers/0_system.drawio.png' \
  '/usr/src/app/tmp/frames/01-0_system.drawio-tilt-029.png'

The preceding command uses these parameters:

ParameterExplanation
tilt=35.0the calculated tilt angle of the current iteration
bgcolor='white -alpha remove -alpha off'set white background
'/usr/src/app/layers/0_system.drawio.png'input filename
'/usr/src/app/tmp/frames/01-0_system.drawio-tilt-029.png'output filename 

Step 1.5

This tiny in-between step is not directly used in the resulting animation but used as an input for the next steps. We need to generate a tilted frame of the framework diagram. We can use 3Drotate again.

The created image looks like this:

Tilted framework

The Command

$ 3Drotate \
  tilt=70.0 \
  bgcolor='none' \
  '/usr/src/app/layers/1_framework.drawio.png' \
  '/usr/src/app/tmp/frames/000-1_framework.drawio-tilt-000.png'

The preceding command uses these parameters:

ParameterExplanation
tilt=70.0the tilt angle
bgcolor='none'we need a transparent background (see next 2 steps)
'/usr/src/app/layers/1_framework.drawio.png'input filename 
'/usr/src/app/tmp/frames/000-1_framework.drawio-tilt-000.png'output filename 

Step 2

The second animation step is to visualize the extraction of the framework by lifting it out of the tilted overview layer from step 1. To achieve that, I used convert again.

The partly rendered animation of this step looks like this:

The Command

$ convert \
  '/usr/src/app/tmp/frames/01-0_system.drawio-tilt-059.png' \
  -fill white -colorize 66% \
  -page +0-175.0% \
  '/usr/src/app/tmp/frames/000-1_framework.drawio-tilt-000.png' \
  -flatten \
  '/usr/src/app/tmp/frames/02-000-1_framework.drawio-tilt-000-lift-020.png'

The preceding command uses these parameters:

ParameterExplanation
'/usr/src/app/tmp/frames/01-0_system.drawio-tilt-059.png'input filename 
'-fill white -colorize 66%'(calculated) makes the background fade to white
'-page +0-175.0%'(calculated) adds a layer on top of the background
'/usr/src/app/tmp/frames/000-1_framework.drawio-tilt-000.png'new layer filename 
'-flatten'flattens the layers
'/usr/src/app/tmp/frames/02-000-1_framework.drawio-tilt-000-lift-020.png'output filename 

Step 3

Step 3 is similar to step 2, but the loop is reversed, and the images are created without the fading background image.

The partly rendered animation of this step looks like this:

The Command

$ convert \ 
  -background white \
  -page +0-150.0% \
  '/usr/src/app/tmp/frames/000-1_framework.drawio-tilt-000.png' \
  -flatten \
  '/usr/src/app/tmp/frames/03-000-1_framework.drawio-tilt-000-lift-012.png'

The preceding command uses these parameters:

ParameterExplanation
'-background white'solid white background
'-page +0-150.0%'(calculated) adds a layer on top of the background
'/usr/src/app/tmp/frames/000-1_framework.drawio-tilt-000.png'input filename 
'-flatten'flattens the layers
'/usr/src/app/tmp/frames/03-000-1_framework.drawio-tilt-000-lift-012.png'output filename 

Step 4

This step tilts the framework back to the original orientation. The framework diagram is used as input but the loop is reversed.

The partly rendered animation of this step looks like this:

The Command

$ 3Drotate \
  tilt=14.0 \
  bgcolor='white -alpha remove -alpha off' \
  '/usr/src/app/layers/1_framework.drawio.png' \
  '/usr/src/app/tmp/frames/04-1_framework.drawio-tilt-024.png'

The preceding command uses these parameters:

ParameterExplanation
tilt=14.0(calculated) the tilt angle of the current iteration
bgcolor='white -alpha remove -alpha off'solid white background
'/usr/src/app/layers/1_framework.drawio.png'input filename 
'/usr/src/app/tmp/frames/04-1_framework.drawio-tilt-024.png'output filename

Step 5

The final step blends the final image over the framework.

The partly rendered animation of this step looks like this:

The Command

convert \
  '/usr/src/app/layers/1_framework.drawio.png' \
  -background white -alpha remove -alpha off \
  \( \
      '/usr/src/app/layers/2_framework_system.drawio.png' \
      -alpha set \
      -channel A \
      -evaluate multiply 0.5 \
      +channel \
  \) \
  -compose over -composite \
  '/usr/src/app/tmp/frames/05-2_framework_system.drawio-fade-015.png'

The preceding command uses these parameters:

ParameterExplanation
'/usr/src/app/layers/1_framework.drawio.png'input filename 
'-background white -alpha remove -alpha off'solid white background
'/usr/src/app/layers/2_framework_system.drawio.png'overlay image filename 
'-alpha set'add and reset the alpha channel
'-channel A'select the alpha channel
'-evaluate multiply 0.5'(calculated) multiply alpha channel by value
'+channel'reset alpha channel
'-compose over -composite'composite images
'/usr/src/app/tmp/frames/05-2_framework_system.drawio-fade-015.png'output filename

Video Rendering

Using the frames from the previous steps, we can now render a video. I implemented two methods to render the video. The first method creates an mp4 the other method creates a gif.

Both methods use the ffmpeg command-line tool and use a file called render_sequence.ffmpeg.txt to specify the used frames, their order, and the time between them. See this snippet for an example:

file '/usr/src/app/tmp/frames/05-2_framework_system.drawio-fade-000.png'
duration 0.03333333333333333
file '/usr/src/app/tmp/frames/05-2_framework_system.drawio-fade-001.png'
duration 0.03333333333333333
...

MP4

Rendering a video in the mp4 format is pretty much straightforward.

ffmpeg \
  -y \
  -hide_banner \
  -loglevel error \
  -f concat \
  -safe 0 \
  -r 30 \
  -i /usr/src/app/tmp/render_sequence.ffmpeg.txt \
  -vf scale=1920:-1 \
  /usr/src/app/out/animation.mp4
ParameterExplanation
'-y'overwrite output file
'-hide_banner'hide ffmpeg banner
'-loglevel error'only show errors
'-f concat'concatenate input files
'-safe 0'disable safe mode (suppress warnings, we have full control over the input files anyway)
'-r 30'(calculated) set frame rate to 30
'-i /usr/src/app/tmp/render_sequence.ffmpeg.txt'input filename
'-vf scale=1920:-1'scale the video to width 1920, “-1” keeps the aspect ratio and calculates the height
'/usr/src/app/out/animation.mp4'output filename

GIF

Rendering a video in the gif format is pretty much similar to the mp4 video. But for the gif format, we need to use a complex filter.

ffmpeg \
  -y \
  -hide_banner \
  -loglevel error \
  -f concat \
  -safe 0 \
  -r 30 \
  -i /usr/src/app/tmp/render_sequence.ffmpeg.txt \
  -filter_complex '[0:v] fps=15,scale=w=720:h=-1,split [a][b];[a] palettegen=stats_mode=single [p];[b][p] paletteuse=new=1' \
  /usr/src/app/tmp/animation_tmp.gif
ParameterExplanation
'-y'overwrite output file
'-hide_banner'hide ffmpeg banner
'-loglevel error'only show errors
'-f concat'concatenate input files
'-safe 0'disable safe mode (suppress warnings, we have full control over the input files anyway)
'-r 30'set frame rate to 30 (calculated)
'-i /usr/src/app/tmp/render_sequence.ffmpeg.txt'input filename
-filter_complex '[0:v] fps=15,scale=w=720:h=-1,split [a][b];[a] palettegen=stats_mode=single [p];[b][p] paletteuse=new=1'see below
'/usr/src/app/tmp/animation_tmp.gif'output filename

The complex filter is where most of the magic happens. The basic idea is to create a custom color palette for each frame and use it to render the frame in the gif. While this creates a larger file, I found that this produces way better colors.

An alternative would be to generate a single color palette for all frames, but this is more complicated in the sense that you need to know how to generate this palette. You need to basically find a frame that has all the colors you need, which is difficult to automate.

Read up more details here.

ParameterExplanation
[0:v]select the first video stream
'fps=15'(calculated) set the frame rate to 15
'scale=w=720:h=-1'scale the video to width 720, “-1” keeps the aspect ratio and calculates the height
'split [a][b]'split the video into two streams
'[a]'select the first stream
'[b]'select the second stream
'[a] palettegen=stats_mode=single' [p]generate a palette for the first stream
[b][p] 'paletteuse=new=1'use the palette for the second stream

The last step toward a proper gif is to optimize it using gifsicle. This reduces the file size a little.

gifsicle \
  --colors 256 \
  -O3 \
  /usr/src/app/tmp/animation_tmp.gif \
  -o /usr/src/app/out/animation.gif
ParameterExplanation
'--colors 256'set the number of colors to 256
'-O3'optimize the GIF, level 3
'/usr/src/app/tmp/animation_tmp.gif'input filename
'-o /usr/src/app/out/animation.gif'output filename

Code

The whole animation is backed by a Ruby script that you can find in this GitHub repository. Take a look at the file in src/main.rb. Read it from the bottom up.

It deliberately is kept simple and within one file. My goal was to automate and document the way the animation is created.

There is one thing to mention: The method when_things_changed implements a caching mechanism to prevent the script from rendering frames that are not changed. To achieve this, this method creates two temporary files per frame. The first simply stores the raw command that is executed, and the other stores a hex-digest of the input file.

Running it

Clone the repository and run the following command:

$ cd src
$ docker-compose run app

The output will look something like this:

$ docker-compose run app
[✓] Operation: tilt | Nr. Frames:  60 | File: 0_system.drawio.png
[✓] Operation: tilt | Nr. Frames:   1 | File: 1_framework.drawio.png
[✓] Operation: lift | Nr. Frames:  30 | File: 000-1_framework.drawio-tilt-000.png
[✓] Operation: lift | Nr. Frames:  30 | File: 000-1_framework.drawio-tilt-000.png
[✓] Operation: tilt | Nr. Frames:  30 | File: 1_framework.drawio.png
[✓] Operation: fade | Nr. Frames:  30 | File: 2_framework_system.drawio.png
[✓] Rendering: out/animation.mp4 | Nr. Frames: 250 -> Duration: ~8s
[✓] Rendering: out/1_overview.mp4 | Nr. Frames:  60 -> Duration: ~2s
[✓] Rendering: out/2_framework_lift.mp4 | Nr. Frames:  30 -> Duration: ~1s
[✓] Rendering: out/3_framework_lift_reverse.mp4 | Nr. Frames:  30 -> Duration: ~1s
[✓] Rendering: out/4_framework_tilt_reverse.mp4 | Nr. Frames:  30 -> Duration: ~1s
[✓] Rendering: out/5_framework_system_fade_over.mp4 | Nr. Frames:  30 -> Duration: ~1s

Enjoy

You are welcome to ask questions, report bugs, fork, modify, or send pull requests on GitHub.

I would love to hear from you.

Written by

Simon Lasselsberger

Founder Lasssim, Software Architecture and Development Expert

Written by

Simon Lasselsberger

Founder Lasssim, Software Architecture and Development Expert

Over the past ten years, I’ve worked with small startups as well as global companies like adidas, helped them implement their business ideas by creating technical plans and by guiding their development teams through projects. The proudest I am about the success we had with Runtastic. They got acquired by adidas in 2015 for €220 million.

How can I help you?

SIMON LASSELSBERGER
Software Architecture and Development Expert