Кручу, верчу, imgui loader нарисовать хочу…

от автора

В одном из сайд-проектов с использованием imgui понадобилась «вертячка» (loader, spinner, анимация загрузки). Из коробки этот ui-фреймворк таких виджетов не предоставляет, поэтому решил сделать свой: код простой, математики почти нет. Показал ocornut-y, ему тоже понравилось, теперь базовый виджет на очереди интеграции в imgui. Поискал интересные спинеры на разных сайтах для веб-интерфейсов — десятки видов на любой вкус и цвет, есть и 3д, но все в основном или пререндеры в виде (gif) или векторные анимации, которые для отрисовки требует отдельного фреймворка вроде cairo, а алгоритмов или описания как это работает, почти нет. Все спинеры сделаны в стиле «что вижу, то и пою», немного математики синусы/косинусы для координат, и тестировать пока не будет похоже на решение от UI дизайнера. Да-да, я понимаю, что когда космические корабли бороздят просторы большого театра DALL·E 2 рисует «улыбку мадонны», писать что-то на плюсах, да еще и UI…


Началось все с простого спинера, который рисует гоняющийся за началом хвост. Уже не помню где я его увидел, но «вертячка» занимательная с логикой на «три копейки». _CalcCircleAutoSegmentCount() подбирает оптимальное число сегментов, для текущего радиуса отрисовки, чтобы окружность казалось плавной, a_min/a_max начальный и конечный углы арки, конечный угол подбираем так, чтобы он всегда недотягивал 3 сегмента до начала. Добавляем немного красок, тогда получается эффект как на анимации.

Код
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)); } 

Если заблокировать начало и конец арки на заданных углах, получаются вполне обычные спинеры, хочешь с подложкой, хочешь без. Чтобы не писать сложный код, рисуется в два прохода, сначала подложку, потом само тело спинера, для начала арки используем текущее время.

Код
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)); }

Если поменять сплошную отрисовку на точки, то без изменения основой части логики, получится уже другой спинер. Поверх можно наложить сплошную линию, тогда прогресс будет более явно отображен.

Код
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)); }

Красивый эффект получается, если арку поверх точек отрисовать не полностью, а точками, размер которых зависит от расстояния до центра арки. Смещаем угол начала арки против движения точек и отрегулируем её скорость, визуально кажется что точки перескакивают с одной на другую.

Код
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); }

На основе этой логики с дискретным отображением точки, можно придумать еще несколько спинеров, например с изменяющейся прозрачностью, размером точки или расстоянием между ними, или вообще рисовать не точку а линию. А чтобы точки более дискретно смещались надо отсекать дробную часть угла сегмента арки.

Код
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); }

Можно разместить точки в ряд и поиграться с синусом времени, завязав его на смещение по оси X\Y, прозрачность или размер точки. Все это будет давать разные эффекты, при практически одинаковой логике. А подменив точки на линии, можно вообще получить другой вид спинера.

Код
// 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);

Если отрисовать подложку неравномерно, постепенно увеличивая ширину линии, то получится почти инь-янь. Можно поиграться с радиусом половинок, реверсивным или прямым движением.

Код
с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); }

Если пустить тонкие арки вокруг подложки, тоже получим интересный эффект.

Код
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); }

А можно рисовать вместо арок точки, минимум изменений логики, но спинер выглядит уже по другому. Последним затесался вариант с тремя арками, которые вращаются с разной скорость, его код приводить не буду — он очень банальный. Но из этого эффекта, можно сделать другой, когда арки изменяют свою длину, вращаясь в одну или в противоположные стороны. В динамике выглядит лучше, чем в тексте.

Код
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)); }

Напоследок выложил оставшиеся виды, интересен может быть, разве что, первый: синус от времени считаем в диапазоне 0 — 720 градусов, пока угол находится в пределах одной арки меняем её прозрачность, или рисуем непрозрачной. Прошли полный круг, теперь делаем тоже самое, но все арки рисуем непрозрачные, а в том секторе, где сейчас находится синус от времени, плавно увеличиваем прозрачность.

Код
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); }

Декларативный конструктор Александреску

Еще когда я только учился (ш)кодить, году эдак в 2000-01, наткнулся на статью Александреску про декларативный конструктор в журнале (MSDN magazine вроде, точно не помню). Суть такая — реализуем специальный тип конструктора, который принимает произвольное число параметров определенных типов и обрабатывает их в соответсвии с типом, а не положением в аргументах. Тогда это выглядело дико и непонятно и особого применения этой технике я не увидел, да и реализовано было через черную магию gcc и макросы, а в студии не завелось. Сейчас, на с++14, это делается в несколько строк кода.
В итоге получаем вот такого вида выражение:

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});

и если поменять порядок аргументов в функции, то результат не меняется

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});

Как набралось с десяток функций, подумал что declarative ctor вполне жизнеспособен в этом случае. Минусов тоже достаточно, взять хотя бы необходимость использоваться strong types, но статья была не об этом.

Благодарю, что дочитали.

З.Ы. не претендую на какую-то техническую значимость статьи и кода, иногда «мелкая залипательная фигня» пишется за пару вечеров, выложил на github (https://github.com/dalerank/imspinner) под MIT лицензией.


ссылка на оригинал статьи https://habr.com/ru/post/692300/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *