|
| 1 | +#include "knobs.h" |
| 2 | + |
| 3 | +#include <cmath> |
| 4 | +#include <cstdlib> |
| 5 | +#include <imgui.h> |
| 6 | +#include <imgui_internal.h> |
| 7 | +#include <utility> |
| 8 | + |
| 9 | +namespace ImGuiKnobs { |
| 10 | + namespace detail { |
| 11 | + std::pair<ImVec2, ImVec2> bezier_arc(ImVec2 center, ImVec2 start, ImVec2 end) { |
| 12 | + auto ax = start[0] - center[0]; |
| 13 | + auto ay = start[1] - center[1]; |
| 14 | + auto bx = end[0] - center[0]; |
| 15 | + auto by = end[1] - center[1]; |
| 16 | + auto q1 = ax * ax + ay * ay; |
| 17 | + auto q2 = q1 + ax * bx + ay * by; |
| 18 | + auto k2 = (4.0f / 3.0f) * (sqrtf((2.0f * q1 * q2)) - q2) / (ax * by - ay * bx); |
| 19 | + |
| 20 | + return { |
| 21 | + {center[0] + ax - k2 * ay, center[1] + ay + k2 * ax}, |
| 22 | + {center[0] + bx + k2 * by, center[1] + by - k2 * bx}, |
| 23 | + }; |
| 24 | + } |
| 25 | + |
| 26 | + void draw_arc1(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color, int num_segments) { |
| 27 | + ImVec2 start = { |
| 28 | + center[0] + cosf(start_angle) * radius, |
| 29 | + center[1] + sinf(start_angle) * radius, |
| 30 | + }; |
| 31 | + |
| 32 | + ImVec2 end = { |
| 33 | + center[0] + cosf(end_angle) * radius, |
| 34 | + center[1] + sinf(end_angle) * radius, |
| 35 | + }; |
| 36 | + |
| 37 | + auto arc = bezier_arc(center, start, end); |
| 38 | + |
| 39 | + auto *draw_list = ImGui::GetWindowDrawList(); |
| 40 | + |
| 41 | + draw_list->AddBezierCurve(start, arc.first, arc.second, end, color, thickness, num_segments); |
| 42 | + } |
| 43 | + |
| 44 | + void draw_arc(ImVec2 center, float radius, float start_angle, float end_angle, float thickness, ImColor color, int num_segments, int bezier_count) { |
| 45 | + // Overlap and angle of ends of bezier curves needs work, only looks good when not transperant |
| 46 | + auto overlap = thickness * radius * 0.00001f * M_PI; |
| 47 | + auto delta = end_angle - start_angle; |
| 48 | + auto bez_step = 1.0f / bezier_count; |
| 49 | + auto mid_angle = start_angle + overlap; |
| 50 | + |
| 51 | + for (auto i = 0; i < bezier_count - 1; i++) { |
| 52 | + auto mid_angle2 = delta * bez_step + mid_angle; |
| 53 | + draw_arc1( |
| 54 | + center, |
| 55 | + radius, |
| 56 | + mid_angle - overlap, |
| 57 | + mid_angle2 + overlap, |
| 58 | + thickness, |
| 59 | + color, |
| 60 | + num_segments); |
| 61 | + mid_angle = mid_angle2; |
| 62 | + } |
| 63 | + |
| 64 | + draw_arc1( |
| 65 | + center, |
| 66 | + radius, |
| 67 | + mid_angle - overlap, |
| 68 | + end_angle, |
| 69 | + thickness, |
| 70 | + color, |
| 71 | + num_segments); |
| 72 | + } |
| 73 | + |
| 74 | + bool knob_control(const char *id, float *p_value, float v_min, float v_max, float v_default, float radius) { |
| 75 | + ImGui::InvisibleButton(id, {radius * 2.0f, radius * 2.0f}); |
| 76 | + |
| 77 | + auto value_changed = false; |
| 78 | + |
| 79 | + auto is_active = ImGui::IsItemActive(); |
| 80 | + auto delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left, 0.0001f); |
| 81 | + |
| 82 | + const auto &io = ImGui::GetIO(); |
| 83 | + |
| 84 | + // Maybe this should be configurable |
| 85 | + auto speed = io.KeyShift ? 2000.0f : 200.0f; |
| 86 | + |
| 87 | + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && is_active) { |
| 88 | + *p_value = v_default; |
| 89 | + value_changed = true; |
| 90 | + } else if (is_active && delta[1] != 0.0) { |
| 91 | + auto step = (v_max - v_min) / speed; |
| 92 | + *p_value -= delta[1] * step; |
| 93 | + if (*p_value < v_min) { |
| 94 | + *p_value = v_min; |
| 95 | + } |
| 96 | + if (*p_value > v_max) { |
| 97 | + *p_value = v_max; |
| 98 | + } |
| 99 | + value_changed = true; |
| 100 | + |
| 101 | + // There may be a way to do this without using this |
| 102 | + ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left); |
| 103 | + } |
| 104 | + |
| 105 | + return value_changed; |
| 106 | + } |
| 107 | + |
| 108 | + knob::knob(const char *_label, float *_p_value, float _v_min, float _v_max, float _v_default, float _radius) { |
| 109 | + auto _angle_min = M_PI * 0.75; |
| 110 | + auto _angle_max = M_PI * 2.25; |
| 111 | + auto _t = (*_p_value - _v_min) / (_v_max - _v_min); |
| 112 | + auto _angle = _angle_min + (_angle_max - _angle_min) * _t; |
| 113 | + auto _value_changed = false; |
| 114 | + auto _screen_pos = ImGui::GetCursorScreenPos(); |
| 115 | + |
| 116 | + _value_changed = knob_control(_label, _p_value, _v_min, _v_max, _v_default, _radius); |
| 117 | + |
| 118 | + label = _label; |
| 119 | + p_value = _p_value; |
| 120 | + v_min = _v_min; |
| 121 | + v_max = _v_max; |
| 122 | + v_default = _v_default; |
| 123 | + radius = _radius; |
| 124 | + screen_pos = _screen_pos; |
| 125 | + value_changed = _value_changed; |
| 126 | + center = {screen_pos[0] + radius, screen_pos[1] + radius}; |
| 127 | + is_active = ImGui::IsItemActive(); |
| 128 | + is_hovered = ImGui::IsItemHovered(); |
| 129 | + angle_min = _angle_min; |
| 130 | + angle_max = _angle_max; |
| 131 | + t = _t; |
| 132 | + angle = _angle; |
| 133 | + angle_cos = cosf(angle); |
| 134 | + angle_sin = sinf(angle); |
| 135 | + } |
| 136 | + |
| 137 | + void knob::draw_dot(float size, float radius, float angle, color_set color, bool filled, int segments) { |
| 138 | + auto dot_size = size * this->radius; |
| 139 | + auto dot_radius = radius * this->radius; |
| 140 | + |
| 141 | + ImGui::GetWindowDrawList()->AddCircleFilled( |
| 142 | + { |
| 143 | + center[0] + cosf(angle) * dot_radius, |
| 144 | + center[1] + sinf(angle) * dot_radius, |
| 145 | + }, |
| 146 | + dot_size, |
| 147 | + is_active ? color.active : (is_hovered ? color.hovered : color.base), |
| 148 | + segments); |
| 149 | + } |
| 150 | + |
| 151 | + void knob::draw_tick(float start, float end, float width, float angle, color_set color) { |
| 152 | + auto tick_start = start * radius; |
| 153 | + auto tick_end = end * radius; |
| 154 | + auto angle_cos = cosf(angle); |
| 155 | + auto angle_sin = sinf(angle); |
| 156 | + |
| 157 | + ImGui::GetWindowDrawList()->AddLine( |
| 158 | + { |
| 159 | + center[0] + angle_cos * tick_end, |
| 160 | + center[1] + angle_sin * tick_end, |
| 161 | + }, |
| 162 | + { |
| 163 | + center[0] + angle_cos * tick_start, |
| 164 | + center[1] + angle_sin * tick_start, |
| 165 | + }, |
| 166 | + is_active ? color.active : (is_hovered ? color.hovered : color.base), |
| 167 | + width * radius); |
| 168 | + } |
| 169 | + |
| 170 | + void knob::draw_circle(float size, color_set color, bool filled, int segments) { |
| 171 | + auto circle_radius = size * radius; |
| 172 | + |
| 173 | + ImGui::GetWindowDrawList()->AddCircleFilled( |
| 174 | + center, |
| 175 | + circle_radius, |
| 176 | + is_active ? color.active : (is_hovered ? color.hovered : color.base)); |
| 177 | + } |
| 178 | + |
| 179 | + void knob::draw_arc(float radius, float size, float start_angle, float end_angle, color_set color, int segments, int bezier_count) { |
| 180 | + auto track_radius = radius * this->radius; |
| 181 | + auto track_size = size * this->radius * 0.5f + 0.0001f; |
| 182 | + |
| 183 | + detail::draw_arc( |
| 184 | + center, |
| 185 | + track_radius, |
| 186 | + start_angle, |
| 187 | + end_angle, |
| 188 | + track_size, |
| 189 | + is_active ? color.active : (is_hovered ? color.hovered : color.base), |
| 190 | + segments, |
| 191 | + bezier_count); |
| 192 | + } |
| 193 | + |
| 194 | + knob knob_with_drag(const char *label, float *p_value, float v_min, float v_max, float v_default, const char *format, float size) { |
| 195 | + ImGui::PushID(label); |
| 196 | + auto width = size == 0 ? ImGui::GetTextLineHeight() * 4.0f : size * ImGui::GetIO().FontGlobalScale; |
| 197 | + ImGui::PushItemWidth(width); |
| 198 | + |
| 199 | + // Draw title |
| 200 | + auto title_size = ImGui::CalcTextSize(label, NULL, false, width); |
| 201 | + auto old_cursor_pos = ImGui::GetCursorPos(); |
| 202 | + |
| 203 | + // Center title |
| 204 | + ImGui::SetCursorPos({old_cursor_pos[0] + (width - title_size[0]) * 0.5f, old_cursor_pos[1]}); |
| 205 | + |
| 206 | + ImGui::Text("%s", label); |
| 207 | + |
| 208 | + // Reset cursor X |
| 209 | + ImGui::SetCursorPosX(old_cursor_pos[0]); |
| 210 | + |
| 211 | + knob k(label, p_value, v_min, v_max, v_default, width * 0.5f); |
| 212 | + |
| 213 | + ImGui::DragScalar("###knob_drag", ImGuiDataType_Float, p_value, (v_max - v_min) / 1000.f, &v_min, &v_max, format); |
| 214 | + |
| 215 | + ImGui::PopItemWidth(); |
| 216 | + ImGui::PopID(); |
| 217 | + return k; |
| 218 | + } |
| 219 | + |
| 220 | + color_set GetPrimaryColorSet() { |
| 221 | + auto *colors = ImGui::GetStyle().Colors; |
| 222 | + |
| 223 | + return {colors[ImGuiCol_ButtonActive], colors[ImGuiCol_ButtonHovered], colors[ImGuiCol_ButtonHovered]}; |
| 224 | + } |
| 225 | + |
| 226 | + color_set GetSecondaryColorSet() { |
| 227 | + auto *colors = ImGui::GetStyle().Colors; |
| 228 | + auto active = colors[ImGuiCol_ButtonActive] * ImVec4(0.5f, 0.5f, 0.5f, 1.f); |
| 229 | + auto hovered = colors[ImGuiCol_ButtonHovered] * ImVec4(0.5f, 0.5f, 0.5f, 1.f); |
| 230 | + return {active, hovered, hovered}; |
| 231 | + } |
| 232 | + |
| 233 | + color_set GetTrackColorSet() { |
| 234 | + auto *colors = ImGui::GetStyle().Colors; |
| 235 | + |
| 236 | + return {colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBg], colors[ImGuiCol_FrameBg]}; |
| 237 | + } |
| 238 | + }// namespace detail |
| 239 | + |
| 240 | + |
| 241 | + // Knob implementations |
| 242 | + KNOB_WIDGET(WiperKnob) { |
| 243 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 244 | + knob.draw_circle(0.7, detail::GetSecondaryColorSet(), true, 32); |
| 245 | + knob.draw_arc(0.8, 0.41, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 16, 2); |
| 246 | + |
| 247 | + if (knob.t > 0.01) { |
| 248 | + knob.draw_arc(0.8, 0.43, knob.angle_min, knob.angle, detail::GetPrimaryColorSet(), 16, 2); |
| 249 | + } |
| 250 | + return knob.value_changed; |
| 251 | + } |
| 252 | + |
| 253 | + KNOB_WIDGET(WiperOnlyKnob) { |
| 254 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 255 | + knob.draw_arc(0.8, 0.41, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 32, 2); |
| 256 | + |
| 257 | + if (knob.t > 0.01) { |
| 258 | + knob.draw_arc(0.8, 0.43, knob.angle_min, knob.angle, detail::GetPrimaryColorSet(), 16, 2); |
| 259 | + } |
| 260 | + return knob.value_changed; |
| 261 | + } |
| 262 | + |
| 263 | + KNOB_WIDGET(WiperDotKnob) { |
| 264 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 265 | + knob.draw_circle(0.6, detail::GetSecondaryColorSet(), true, 32); |
| 266 | + knob.draw_arc(0.85, 0.41, knob.angle_min, knob.angle_max, detail::GetTrackColorSet(), 16, 2); |
| 267 | + knob.draw_dot(0.1, 0.85, knob.angle, detail::GetPrimaryColorSet(), true, 12); |
| 268 | + return knob.value_changed; |
| 269 | + } |
| 270 | + |
| 271 | + |
| 272 | + KNOB_WIDGET(TickKnob) { |
| 273 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 274 | + knob.draw_circle(0.7, detail::GetSecondaryColorSet(), true, 32); |
| 275 | + knob.draw_tick(0.4, 0.7, 0.08, knob.angle, detail::GetPrimaryColorSet()); |
| 276 | + return knob.value_changed; |
| 277 | + } |
| 278 | + |
| 279 | + KNOB_WIDGET(DotKnob) { |
| 280 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 281 | + knob.draw_circle(0.85, detail::GetSecondaryColorSet(), true, 32); |
| 282 | + knob.draw_dot(0.12, 0.6, knob.angle, detail::GetPrimaryColorSet(), true, 12); |
| 283 | + return knob.value_changed; |
| 284 | + } |
| 285 | + |
| 286 | + KNOB_WIDGET(SpaceKnob) { |
| 287 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 288 | + knob.draw_circle(0.3 - knob.t * 0.1, detail::GetSecondaryColorSet(), true, 16); |
| 289 | + |
| 290 | + if (knob.t > 0.01) { |
| 291 | + knob.draw_arc(0.4, 0.15, knob.angle_min - 1.0, knob.angle - 1.0, detail::GetPrimaryColorSet(), 16, 2); |
| 292 | + knob.draw_arc(0.6, 0.15, knob.angle_min + 1.0, knob.angle + 1.0, detail::GetPrimaryColorSet(), 16, 2); |
| 293 | + knob.draw_arc(0.8, 0.15, knob.angle_min + 3.0, knob.angle + 3.0, detail::GetPrimaryColorSet(), 16, 2); |
| 294 | + } |
| 295 | + |
| 296 | + return knob.value_changed; |
| 297 | + } |
| 298 | + |
| 299 | + KNOB_WIDGET(SteppedKnob, int steps) { |
| 300 | + auto knob = detail::knob_with_drag(title, p_value, v_min, v_max, 0, format, size); |
| 301 | + for (auto n = 0.f; n < steps; n++) { |
| 302 | + auto a = n / (steps - 1); |
| 303 | + auto angle = knob.angle_min + (knob.angle_max - knob.angle_min) * a; |
| 304 | + knob.draw_tick(0.7, 0.9, 0.04, angle, detail::GetPrimaryColorSet()); |
| 305 | + } |
| 306 | + knob.draw_circle(0.6, detail::GetSecondaryColorSet(), true, 32); |
| 307 | + knob.draw_dot(0.12, 0.4, knob.angle, detail::GetPrimaryColorSet(), true, 12); |
| 308 | + return knob.value_changed; |
| 309 | + } |
| 310 | +}// namespace ImGuiKnobs |
0 commit comments