I twist, I twist, I want to draw imgui loader …

In one of the side projects using imgui I needed a “spinner” (loader, spinner, loading animation). Out of the box, this ui-framework does not provide such widgets, so I decided to make my own: the code is simple, there is almost no mathematics. Showed ocornut-y, he liked it too, now the basic widget is in line for integration into imgui. I looked for interesting spinners on different sites for web interfaces – dozens of types for every taste and color, there is also 3d, but everything is basically either pre-renders in the form (gif) or vector animations, which require a separate framework like cairo for rendering, and algorithms or there is almost no description of how it works. All spinners are made in the style of “what I see is what I sing”, some math sine / cosines for coordinates, and testing until it looks like a solution from a UI designer. Yes, yes, I understand that when spaceships plow the expanses of the Bolshoi Theater DALL E 2 draws a “Madonna’s smile”, write something on the pluses, and even UI …


It all started with a simple spinner that draws a tail chasing the start. I don’t remember where I saw it, but the “twirl” is entertaining with logic for “three kopecks”. _CalcCircleAutoSegmentCount() selects the optimal number of segments for the current rendering radius so that the circle seems smooth, a_min/a_max are the initial and final angles of the arch, the final angle is selected so that it always falls short of 3 segments before the start. We add some colors, then we get the effect like on animation.

The code
const size_t num_segments = _CalcCircleAutoSegmentCount(radius);
float start = ImAbs(ImSin(ImGui::GetTime() * 1.8f) * (num_segments - 5));

const float a_min = IM_PI * 2.0f * (start) / num_segments;
const float a_max = IM_PI * 2.0f * (num_segments - 3) / num_segments;

for (size_t i = 0; i < num_segments; i++) {
    const float a = a_min + (i / num_segments) * (a_max - a_min);
    PathLineTo(ImVec2(centre.x + ImCos(a + ImGui::GetTime() * speed) * radius,
               centre.y + ImSin(a + ImGui::GetTime() * speed) * radius));
}

If you block the beginning and end of the arch at given angles, you get quite ordinary spinners, you want with a backing, you want without. In order not to write complex code, it is drawn in two passes, first the backing, then the spinner body itself, to start the arch we use the current time.

The code
const size_t num_segments = _CalcCircleAutoSegmentCount(radius);
float start = ImGui::GetTime() * speed;
const float bg_angle_offset = IM_PI * 2.f / num_segments;
for (size_t i = 0; i <= num_segments; i++) {
    const float a = start + (i * bg_angle_offset);
    PathLineTo(ImVec2(centre.x + ImCos(a) * radius, centre.y + ImSin(a) * radius));
}
PathStroke(bg, false, thickness);


const float angle_offset = angle / num_segments;
for (size_t i = 0; i < num_segments; i++) {
    const float a = start + (i * angle_offset);
    PathLineTo(ImVec2(centre.x + ImCos(a) * radius, centre.y + ImSin(a) * radius));
}

If you change the solid drawing to dots, then without changing the main part of the logic, you get a different spinner. You can overlay a solid line on top, then progress will be more clearly displayed.

The code
float start = ImGui::GetTime() * speed;
const float bg_angle_offset = IM_PI * 2.f / dots;
dots = min(dots, 32);

for (size_t i = 0; i <= dots; i++) {
    float a = start + (i * bg_angle_offset);
    a = ImFmod(a, 2 * IM_PI);
    AddCircleFilled(ImVec2(centre.x + ImCos(-a) * radius, centre.y + ImSin(-a) * radius), thickness / 2, color, 8);
}

window->DrawList->PathClear();
const float d_ang = (mdots / dots) * 2 * IM_PI;
const float angle_offset = (d_ang / dots);
for (size_t i = 0; i < dots; i++) {
    const float a = start + (i * angle_offset);
    PathLineTo(ImVec2(centre.x + ImCos(a) * radius, centre.y + ImSin(a) * radius));
}

A beautiful effect is obtained if the arch is not completely drawn over the dots, but with dots, the size of which depends on the distance to the center of the arch. We shift the angle of the beginning of the arch against the movement of points and adjust its speed, visually it seems that the points jump from one to another.

The code
float def_nextdot = 0;
float &ref_nextdot = nextdot ? *nextdot : def_nextdot;

auto thcorrect = [&thickness, &ref_nextdot, &mdots, &minth] (int i) {
    const float nth = minth < 0.f ? thickness / 2.f : minth;
    return ImMax(nth, ImSin(((i - ref_nextdot) / mdots) * IM_PI) * thickness);
};

for (size_t i = 0; i <= dots; i++) {
    float a = start + (i * bg_angle_offset);
    a = ImFmod(a, 2 * IM_PI);
    float th = minth < 0 ? thickness / 2.f : minth;

    if (ref_nextdot + mdots < dots) {
        if (i > ref_nextdot && i < ref_nextdot + mdots)
            th = thcorrect(i);
    } else {
        if ((i > ref_nextdot && i < dots) || (i < ((int)(ref_nextdot + mdots)) % dots))
            th = thcorrect(i);
    }

    AddCircleFilled(ImVec2(centre.x + ImCos(-a) * radius, centre.y + ImSin(-a) * radius), th, color, 8);
}

Based on this logic with a discrete display of a point, you can come up with a few more spinners, for example, with changing transparency, the size of a point or the distance between them, or even draw not a point, but a line. And in order for the points to move more discretely, it is necessary to cut off the fractional part of the angle of the arch segment.

The code
float start = (float)ImGui::GetTime() * speed;
float astart = ImFmod(start, IM_PI / dots);
start -= astart;  // дискретизация движения точки
const float bg_angle_offset = IM_PI / dots;
dots = ImMin<size_t>(dots, 32);

for (size_t i = 0; i <= dots; i++) {
  float a = start + (i * bg_angle_offset);
  ImColor c = color;
  c.Value.w = ImMax(0.1f, i / (float)dots);
  AddCircleFilled(ImVec2(centre.x + ImCos(a) * radius, centre.y + ImSin(a) * radius), thickness, c, 8);
}

You can place the points in a row and play around with the sine of time by tying it to the offset along the X\Y axis, transparency or the size of the point. All this will give different effects, with almost the same logic. And by replacing the points on the line, you can generally get a different kind of spinner.

The code
// Y
float a = start + (IM_PI - i * offset);
float sina = ImSin(a * heightSpeed);
float y = centre.y + sina * thickness * heightKoeff;
if (y > centre.y)
  y = centre.y;
AddCircleFilled(ImVec2(pos.x + style.FramePadding.x  + i * (thickness * nextItemKoeff), y), thickness, color, 8);

// Fade
float a = start + (IM_PI - i * offset);
ImColor c = color;
c.Value.w = ImMax(0.1f, ImSin(a * heightSpeed));
AddCircleFilled(ImVec2(pos.x + style.FramePadding.x  + i * (thickness * nextItemKoeff), centre.y), thickness, c, 8);

// Radius
const float a = start + (IM_PI - i * offset);
const float th = thickness * ImSin(a * heightSpeed);
ImColor fade_color = color;
fade_color.Value.w = 0.1f;
AddCircleFilled(ImVec2(pos.x + style.FramePadding.x  + i * (thickness * nextItemKoeff), centre.y), thickness, fade_color, 8);
AddCircleFilled(ImVec2(pos.x + style.FramePadding.x  + i * (thickness * nextItemKoeff), centre.y), th, color, 8);

// Moving
 const float a = start + (i * IM_PI / dots);
float th = thickness;
offset =  ImFmod(start + i * (size.x / dots), size.x);
if (offset < thickness)
  th = offset;
if (offset > size.x - thickness)
  th = size.x - offset;

AddCircleFilled(ImVec2(pos.x + style.FramePadding.x + offset, centre.y), th, color, 8);

If you draw the matte unevenly, gradually increasing the width of the line, you get almost yin-yang. You can play with the radius of the halves, reverse or direct movement.

The code
сonst float angle_offset = angle / num_segments;
const float th = thickness / num_segments;
for (size_t i = 0; i < num_segments; i++) {
  const float a = startI + (i * angle_offset);
  const float a1 = startI + ((i + 1) * angle_offset);
  window->DrawList->AddLine(ImVec2(centre.x + ImCos(a) * radius, centre.y + ImSin(a) * radius),
                            ImVec2(centre.x + ImCos(a1) * radius, centre.y + ImSin(a1) * radius),
                            colorI,
                            th * i);
}

If you let thin arches around the substrate, we also get an interesting effect.

The code
for (size_t i = 0; i <= num_segments; i++) {
  const float a = start + (i * bg_angle_offset);
  PathLineTo(ImVec2(centre.x + ImCos(a) * radius1, centre.y + ImSin(a) * radius1));
}
PathStroke(bg, false, thickness);

const float angle_offset = angle / num_segments;
for (size_t arc_num = 0; arc_num < arcs; ++arc_num) {
    window->DrawList->PathClear();
    float arc_start = 2 * IM_PI / arcs;
    for (size_t i = 0; i < num_segments; i++) {
      const float a = arc_start * arc_num + start + (i * angle_offset);
      PathLineTo(ImVec2(centre.x + ImCos(a) * radius2, centre.y + ImSin(a) * radius2));
    }
    PathStroke(color, false, thickness);
}

And you can draw dots instead of arches, a minimum of logic changes, but the spinner looks different. The last variant with three arches that rotate at different speeds, I will not give its code – it is very banal. But from this effect, you can make another one, when the arches change their length, rotating in one direction or in opposite directions. It looks better in dynamics than in text.

The code
for (size_t i = 0; i <= 2 * num_segments; i++) { // белая арка растет быстрее красной
  const float a = start + (i * angle_offset);
  if (i * angle_offset > 2 * bofsset)
    break;
  PathLineTo(ImVec2(centre.x + ImCos(a) * radius1, centre.y + ImSin(a) * radius1));
}

for (size_t i = 0; i < num_segments / 2; i++) { // красная арка растет до половины
  const float a = start + (i * angle_offset);
  if (i * angle_offset > bofsset)
    break;
  PathLineTo(ImVec2(centre.x + ImCos(a) * radius2, centre.y + ImSin(a) * radius2));
}

Finally, I laid out the remaining views, it may be interesting, perhaps, the first one: we consider the sine of time in the range of 0 – 720 degrees, while the angle is within one arch, we change its transparency, or draw it opaque. We have gone full circle, now we do the same, but we draw all the arches opaque, and in the sector where the sine of time is now, we gradually increase the transparency.

The code
for (size_t arc_num = 0; arc_num < arcs; ++arc_num)
{
  for (size_t i = 0; i <= num_segments + 1; i++) { // подложк 
    const float a = arc_angle * arc_num + (i * angle_offset) - IM_PI / 2.f - IM_PI / 4.f;
    PathLineTo(ImVec2(centre.x + ImCos(a) * radius, centre.y + ImSin(a) * radius));
  }
  const float a = arc_angle * arc_num;
  ImColor c = color;
  if (start < IM_PI * 2.f) { // первый круг проходим на заполнение
    c.Value.w = 0.f;
    if (start > a && start < (a + arc_angle)) { // заполняем, пока угол в этой секции
      c.Value.w = 1.f - (start - a) / arc_angle;
    } else if (start < a) { // угол больше этой секции
      c.Value.w = 1.f;
    }
    c.Value.w = ImMax(0.05f, 1.f - c.Value.w);
  } else { // второй круг проходим на угасание
    const float startk = start - IM_PI * 2.f;
    c.Value.w = 0.f;
    if (startk > a && startk < (a + arc_angle)) { // угасаем пока угол в этой секции
      c.Value.w = 1.f - (startk - a) / arc_angle;
    } else if (startk < a) {
      c.Value.w = 1.f; // полностью угасли
    }
    c.Value.w = ImMax(0.05f, c.Value.w);
  }
  PathStroke(c, false, thickness);
}

Alexandrescu’s declarative constructor

Back when I was just learning to (w)code, around 2000-01, I came across an Alexandrescu article about a declarative constructor in a magazine (MSDN magazine, I don’t remember exactly). The essence is this – we implement a special type of constructor that takes an arbitrary number of parameters of certain types and processes them according to the type, and not the position in the arguments. Then it looked wild and incomprehensible and I didn’t see much use for this technique, and it was implemented through the black magic of gcc and macros, but it didn’t start in the studio. Now, in C++14, this is done in a few lines of code.
As a result, we get this kind of expression:

ImSpinner::Spinner<e_st_angle>("SpinnerAng", 
                                Radius{16.f}, 
                                Thickness{2.f}, 
                                Color{255, 255, 255}, 
                                BgColor{255, 255, 255, 128}, 
                                Speed{8 * velocity},
                                Angle{IM_PI});

and if you change the order of the arguments in the function, then the result does not change

ImSpinner::Spinner<e_st_angle>("SpinnerAng", 
                                Angle{IM_PI}, 
                                Speed{8 * velocity}, 
                                BgColor{255, 255, 255, 128}, 
                                Color{255, 255, 255}, 
                                Thickness{2.f},
                                Radius{16.f});

As I got about a dozen functions, I thought that the declarative ctor is quite viable in this case. There are also enough cons, take at least the need to be used strong typesbut the article was not about that.

Thank you for reading.

Z.Y. I do not pretend to any technical significance of the article and code, sometimes “small sticky garbage” is written in a couple of evenings, posted on github (https://github.com/dalerank/imspinner) under the MIT license

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *