В одном из сайд-проектов с использованием 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 лицензией.
