December 5, 2023

Animated SVG loader

Today, in the era of SPA, almost no web project is complete without all kinds of loading indicators. For any Frontend developer, rendering a loader is an everyday task. Usually, this does not cause difficulties, there are many options for implementing such loaders. There are also a lot of various ready-made solutions and generators for every taste and color. However, from time to time, you have to deal with non-standard options.

Not so long ago, I just needed to implement one of these non-standard options. The designer asked me to make a branded loader in the shape of an animated company logo. The logo itself is quite complex, it contains letters of the Latin alphabet and geometric shapes. According to the designer's idea, the letters should animate their spelling, and the geometry should move along the contour of the letters, change its size. In addition, one of the requirements for the loader was that it had to be completely vector.

Well, the challenge was accepted, the loader was done. For copyright reasons, I will not publish the final branded version here. And the format of the article is not very suitable for such a voluminous work.

However, it inspired me to make a small improvised demo version of such an implementation.

So, let's have a simple orange square as the logo. There will also be a yellow dot on the loader, which will draw the contours of the square, and then make a turnover around the square along an elliptical trajectory.

Let's start the implementation.

Stage 1. Preparation of the vector basis

To begin with, let's take any vector graphics editor and create a canvas of the desired size in it. For more detail, I'll take a 1000x1000 canvas.

We will also create the necessary vector elements on the canvas. In our case, it is a square and a circle (which will symbolize a point).

It is important that the square be created as a Path, not a Rectangle. In this particular example, we could do with the second one, but for more complex geometry we need a Path, which we will need further.

So we have two vector elements, a square:

<path
  d="M700,700.5 L700,еь300 L300,300 L300,700.5 L700,700.5"
  stroke="#A94005"
  stroke-width="40"
  stroke-linecap="square"
  fill="none"
>

and the circle:

<circle fill="#F9C141" cx="670" cy="670" r="50">

In addition to the main elements, we also need the trajectory of our point. According to the idea, part of its path will follow the contour of a square, the rest is an elliptical orbit. Let's draw this trajectory as a curve.

The Path of the trajectory looks like this

<path
  d="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5 C926.01092,451.207538 961.074865,338.373425 805.191837,361.99766 C675.217992,381.695332 220.510369,567.838892 124.783651,640.760981 C-62.8581216,783.701533 61.7032261,922.899504 451.788931,852.02978 C553.891878,833.479991 694.785354,782.970064 874.469359,700.5"
>

Here you can see that the first part of the trajectory completely coincides with the Path of the square. That is, part of the path our point will move along the contour of the square.

Stage 2. SVG Preparation

The main elements are ready. Now we need the SVG itself, which we will display in the browser. To do this, export the received elements from the editor

<svg width="200px" height="200px" class="loader" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <path
    d="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5"
    stroke="#A94005"
    stroke-width="40"
    stroke-linecap="square"
    fill="none"
  ></path>
  
  <circle fill="#F9C141" cx="670" cy="670" r="50"></circle>
</svg>

The size of the resulting SVG on the page can be arbitrary. In our case, let it be 200x200.

Stage 3. Animation

Now add, in fact, the animation itself. Let's start with a square. Here we will use the classic approach using stroke-dasharray / strokedashoffset.

Some basic theory

stroke-dasharray represents the stroke of an element as a dotted line. In order to calculate this parameter in the way we need, we need to know the length of the contour of our element, this is the key value for this attribute. You can determine the length of the contour both empirically (manually selected) and by means of mathematics, for example

In our case, the length of the contour of the square will be 4 * 400 = 1600

Since the contour of our figure must be drawn with one solid line, then the stroke length will be equal to the contour length, i.e.

<path ... stroke-dasharray="1600">

The second attribute of strokedashoffset is to add the offset of the first stroke. Since initially the square should not be drawn yet, we will make an offset for the entire length of the contour

<path ... stroke-dashoffset="1600">

Next, it will animate the strokedashoffset attribute from 1600 to 0, simulating the drawing of the contour. This will be done with the help of the <animante> tag

<path
    d="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5"
    stroke="#A94005"
    stroke-width="40"
    stroke-linecap="square"
    fill="none"
    stroke-dasharray="1600"
    stroke-dashoffset="1600"
  >
    <animate
       fill="freeze"
       dur="4s"
       repeatCount="indefinite"
       attributeName="stroke-dashoffset"
       values="1600; 1200; 1200; 800; 800; 400; 400; 0; 0; 0"
       keyTimes="0; 0.115; 0.125; 0.24; 0.25; 0.365; 0.375; 0.49; 0.5; 1"
    ></animate>
  </path>

I would also like to draw your attention to the fact that there are two key attributes values and keyTimes. They work in pairs and set the non-linearity of the animation itself. With the first attribute, we set the values that the animation should reach by a certain point in time, the second is the moments of time themselves, where 0 is the beginning of the animation, 1 is its end

In our version, we will make small stops in each corner of the square, and in the end, there will be no animation for half the time (stroke-dasharray will already reach 0 by this time), because after drawing the contour of the square, there will be an additional animated movement of the point.

Now let's move on to the point itself. Its movement can be divided into two parts. The first part is a repetition of the contour of the square in the same timing, the second part is an elliptical revolution around the square. We will move using <animateMotion>

<circle fill="#F9C141" cx="0" cy="0" r="50">
    <animateMotion
       path="M700,700.5 L700,300 L300,300 L300,700.5 L700,700.5 C926.01092,451.207538 961.074865,338.373425 805.191837,361.99766 C675.217992,381.695332 220.510369,567.838892 124.783651,640.760981 C-62.8581216,783.701533 61.7032261,922.899504 451.788931,852.02978 C553.891878,833.479991 694.785354,782.970064 874.469359,700.5"
       dur="4s"
       calcMode="linear"
       repeatCount="indefinite"
       keyPoints="0; 0.103; 0.103; 0.206; 0.206; 0.309; 0.309; 0.412; 0.412; 1; 1"
       keyTimes="0; 0.115; 0.125; 0.24; 0.25; 0.365; 0.375; 0.49; 0.6; 0.7; 1"
    ></animateMotion>
  </circle>

<animateMotion> takes the path path as an attribute (the one we drew in the editor), otherwise it is very similar to <animate>. Another feature here is the use of keyPoints. It, like values, works in conjunction with keyTimes, the only difference is that the values are the distance that the element will travel to a given point in time, where 0 is the starting point of the path, 1 is the end point of the path. In our case, the length of the contour of the square is 0.412 of the entire route, this part of the path the point must pass in half the time (more precisely, in 0.49 of the time, i.e.e. by the time the square is fully rendered).

Since our animation is not additive, I set the initial coordinates of the point to 0

cx="0"
cy="0"

Also, I recommend always explicitly specifying calcMode="linear", since some browsers, such as Safari and Mozilla, may have arbitrary default values, which sometimes leads to non-working animations in them.

It remains to put everything together and enjoy the result

My telegram channels:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

Русская версия: https://blog.frontend-almanac.ru/gdJLkngE5fF