Skip to content

Tutorial 5: Planar Map Animation

Given a global point layer and countries layer like this:

You can create a nice planar map animation effect like this:

In planar mode, we do not pan and zoom the map from feature to feature. Rather, the map zoom stays constant and the variables for

  • current_hover_frame
  • hover_frames
  • hover_feature_id

are updated as we iterate over the features of your animation layer. In this example project I duplicated the animation point layer twice. The first (lower) copy is used to 'drive' the animation, whilst the second (upper) layer shows only the feature currently being hovered over, with animation effects applied to that feature.

I set up the animation workbench like this:

For the above animated GIF, I compressed it using imagemagick like this:

convert globe.gif -coalesce -resize 700x525 -fuzz 2% +dither -layers Optimize +map globe_small.gif

This is a handy technique if you want to generate small file size animations.

Expressions Used

Firstly for debugging, we use the following copyright label in View ⇒ Decorations ⇒ Copyright Label. You can use the checkbox in the Copyright configuration dialog to toggle this on and off. This will help you while debugging / tweaking your animations. When you are ready to render your final product, simply turn it off before rendering.

[%
' \nRotation:' ||  to_string( 0-((1440 * (@current_hover_frame/@hover_frames)) % 360)) ||
'\nFeature Variables:' ||
' \n------------------------' ||
' \nPrevious Feature ' || to_string(coalesce(attribute(@previous_feature, 'name'), '-'))  ||
' \nPrevious Feature ID ' || to_string(coalesce(@previous_feature_id, '-'))  ||
' \n' ||
' \nNext Feature ' || to_string(coalesce(attribute(@next_feature, 'name'), '-'))  ||
' \nNext Feature ID ' || to_string(coalesce(@next_feature_id, '-'))  ||
' \n' ||
' \nHover Feature ' || to_string(coalesce(attribute(@hover_feature, 'name'), '-'))  ||
' \nHover Feature ID ' || to_string(coalesce(@hover_feature_id, '-'))  ||
' \n' ||
' \nFrom Feature ' || to_string(coalesce(attribute(@from_feature, 'name'), '-'))  ||
' \nFrom Feature ID ' || to_string(coalesce(@from_feature_id, '-'))  ||
' \n' ||
' \nTo Feature ' || to_string(coalesce(attribute(@to_feature, 'name'), '-'))  ||
' \nTo Feature ID ' || to_string(coalesce(@to_feature_id, '-'))  ||
' \n' ||
' \nTotal Hover Frames ' || to_string(coalesce(@hover_frames, 0))  ||
' \nCurrent Hover Frame ' || to_string(coalesce(@current_hover_frame, 0))  ||
' \nTotal Travel Frames ' || to_string(coalesce(@travel_frames, 0))  ||
' \nCurrent Travel Frame ' || to_string(coalesce(@current_travel_frame, 0))  ||
' \nTotal Frame Count ' || to_string(coalesce(@total_frame_count, 0))  ||
' \nFrame Number ' || to_string(coalesce(@frame_number, 0))  ||
' \nFrame Rate ' || to_string(coalesce(@frame_rate, 0))  ||
' \nwith Current Animation Action: ' || @current_animation_action ||
' \nTo Direction ' ||  coalesce(format_number(degrees(azimuth( geometry(@hover_feature), geometry(@previous_feature) ) ) ), 0) || 
' \nFrom Direction ' ||  coalesce(format_number(degrees( azimuth( geometry(@hover_feature), geometry(@next_feature) ) ) ), 0)%]

Symbol Rotation

For the points I made a red marker using a quarter circle that spins around the points like this:

The first line of the listing from the previous section gives you a hint about how we can vary the rotation of a symbol depending on how far through the animation sequence we are. With the addition of an if clause, we can apply this rotation only to features that are being hovered over during the planar animation.

if (
  @id = @hover_feature_id, 
  0-((1440 * (@current_hover_frame/@hover_frames)) % 360), 
  0)

This if clause has the effect of excluding calculation for any feature that is not the current hover feature.

This will spin around 4 times during the hover cycle. This is because four rotations are 4 x 360 = 1440. We calculate the percentage of completion for the current hover frame (@current_hover_frame/@hover_frames) and then multiply our rotation product by the current completion percentage. Lastly we calculate the modulus of this (% 360) to compute how far along we are in the current rotation. More advanced users could substitute 1440 with a project variable so that it is easy to change the number of desired rotations in a single place.

Symbol Size

The rotating symbol layer and the other symbol layers in our animation layer are similarly hidden if the feature being rendered is not the hover_feature_id using an expression like this:

if ( @id = @hover_feature_id,  10, 0)

This has the effect of setting the symbol size to 0 if it is not the feature we are focussing on.

Other Planar Experiments

With the basic concepts of working with planar animations covered above, you can do other interesting things.

Generate a line

In this example, we can generate a line using the Geometry Generator function in QGIS. The line will start from the previous point, extend through the current point and terminate and the next point.

if ( 
  $id = @hover_feature_id,
   make_line(
    geometry(@previous_feature),
    geometry(@hover_feature),
    geometry(@next_feature)
  ),
  $geometry)

We wrap it in an if clause again so that the line is not rendered if the current feature being rendered is not the same as the current animation feature.

There may be some edge cases where there is no previous or next feature. This example does not try to deal with these cases but you could easily add some logic that checks if each of the three components making up the line is null or not.

Generate a curve

We can extend the above example by creating a curve rather than a line, for a more natural looking connection between the hover feature and its previous and following feature.

if ( 
  $id = @hover_feature_id,
   smooth(
    make_line(
      geometry(@previous_feature),
      geometry(@hover_feature),
      geometry(@next_feature)
    ),
    iterations:=1,
    offset:=0.2,
    min_length:=-1,
    max_angle:=180),
  $geometry)

If you increase the number of iterations, you can achieve a more and more smoothed out line, at the expense of processing time.

if ( 
  $id = @hover_feature_id,
   smooth(
    make_line(
      geometry(@previous_feature),
      geometry(@hover_feature),
      geometry(@next_feature)
    ),
    iterations:=5,
    offset:=0.2,
    min_length:=-1,
    max_angle:=180),
  $geometry)

Subtring the Line

As a much more advanced example, you can extract a substring of the smoothed line that connects the previous, current and next features. Don't get put off by the with_variable elements - they just allow us to re-use calculations in our expression.

First, let's start with extracting the first half of the smoothed line:

if ( 
    $id = @hover_feature_id,
    with_variable(
        'smoothed_line',
        smooth(
          make_line(
              geometry(@previous_feature),
                geometry(@hover_feature),
                geometry(@next_feature)
          ),
      iterations:=5,
      offset:=0.2,
      min_length:=-1,
      max_angle:=180),
          with_variable(
              'line_length',
          length(@smoothed_line),
          line_substring(@smoothed_line, 0, @line_length / 2 ))),
  $geometry)

Animating the substring

If we follow the same approach as above, but vary the start and length of the line clip, we can create some cool line animation effects.

if ( 
    $id = @hover_feature_id,
    with_variable(
        'smoothed_line',
        smooth(
          make_line(
              geometry(@previous_feature),
                geometry(@hover_feature),
                geometry(@next_feature)
          ),
      iterations:=5,
      offset:=0.2,
      min_length:=-1,
      max_angle:=180),
          with_variable(
              'line_length',
          length(@smoothed_line),
          line_substring(
                    @smoothed_line, 
                    @line_length * (@current_hover_frame/@hover_frames), 
                    @line_length ))),

  $geometry)

Final Render

There are still a few details that would need to be taken care of to reach a final solution - in particular taking care of datelines and the like. But here is a little example of what we managed to make thus far (without any debugging text).