From 235677b5597cd0a150fd21595b3f6bf127d702d7 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 13 Aug 2026 07:43:03 +0300 Subject: [PATCH 1/3] added screen overlay --- examples/example_hud/gui/main_window.cpp | 29 ++++++++++ examples/example_hud/gui/main_window.hpp | 13 +++++ include/omath/hud/screen_overlay.hpp | 73 ++++++++++++++++++++++++ source/hud/screen_overlay.cpp | 68 ++++++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 include/omath/hud/screen_overlay.hpp create mode 100644 source/hud/screen_overlay.cpp diff --git a/examples/example_hud/gui/main_window.cpp b/examples/example_hud/gui/main_window.cpp index 86b17a5b..8345fcb3 100644 --- a/examples/example_hud/gui/main_window.cpp +++ b/examples/example_hud/gui/main_window.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace imgui_desktop::gui { @@ -225,6 +226,28 @@ namespace imgui_desktop::gui ImGui::Combo("Figure##proj", &m_proj_figure, "Circle\0Square\0"); } + if (ImGui::CollapsingHeader("Crosshair")) + { + ImGui::Checkbox("Show##cross", &m_show_crosshair); + ImGui::ColorEdit4("Color##cross", reinterpret_cast(&m_crosshair_color), + ImGuiColorEditFlags_NoInputs); + ImGui::ColorEdit4("Outline##cross", reinterpret_cast(&m_crosshair_outline), + ImGuiColorEditFlags_NoInputs); + ImGui::SliderFloat("Gap##cross", &m_crosshair_gap, 0.f, 30.f); + ImGui::SliderFloat("Length##cross", &m_crosshair_length, 1.f, 40.f); + ImGui::SliderFloat("Thick##cross", &m_crosshair_thickness, 0.5f, 5.f); + ImGui::SliderFloat("Dot radius##cross", &m_crosshair_dot_radius, 0.f, 8.f); + } + + if (ImGui::CollapsingHeader("FOV Circle")) + { + ImGui::Checkbox("Show##fov", &m_show_fov); + ImGui::ColorEdit4("Color##fov", reinterpret_cast(&m_fov_color), ImGuiColorEditFlags_NoInputs); + ImGui::ColorEdit4("Fill##fov", reinterpret_cast(&m_fov_fill), ImGuiColorEditFlags_NoInputs); + ImGui::SliderFloat("Radius##fov", &m_fov_radius, 10.f, 300.f); + ImGui::SliderFloat("Thick##fov", &m_fov_thickness, 0.5f, 5.f); + } + if (ImGui::CollapsingHeader("Snap Line")) { ImGui::Checkbox("Show##snap", &m_show_snap); @@ -393,6 +416,12 @@ namespace imgui_desktop::gui m_proj_line_width, static_cast(m_proj_figure)}), when(m_show_snap, SnapLine{{vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width})); + + omath::hud::ScreenOverlay({vp->Size.x, vp->Size.y}, std::make_shared()) + .contents(when(m_show_crosshair, + Crosshair{m_crosshair_color, m_crosshair_gap, m_crosshair_length, m_crosshair_thickness, + m_crosshair_outline, m_crosshair_dot_radius}), + when(m_show_fov, FovCircle{m_fov_color, m_fov_radius, m_fov_fill, m_fov_thickness})); } void MainWindow::present() diff --git a/examples/example_hud/gui/main_window.hpp b/examples/example_hud/gui/main_window.hpp index 5eedeb5d..80879263 100644 --- a/examples/example_hud/gui/main_window.hpp +++ b/examples/example_hud/gui/main_window.hpp @@ -108,6 +108,19 @@ namespace imgui_desktop::gui float m_snap_width = 1.5f; bool m_show_snap = true; + // Crosshair + omath::Color m_crosshair_color = omath::Color::from_rgba(0, 255, 0, 255); + omath::Color m_crosshair_outline = omath::Color::from_rgba(0, 0, 0, 255); + float m_crosshair_gap = 4.f, m_crosshair_length = 8.f, m_crosshair_thickness = 1.f; + float m_crosshair_dot_radius = 0.f; + bool m_show_crosshair = true; + + // FOV circle + omath::Color m_fov_color = omath::Color::from_rgba(255, 255, 0, 200); + omath::Color m_fov_fill = omath::Color::from_rgba(255, 255, 0, 25); + float m_fov_radius = 120.f, m_fov_thickness = 1.5f; + bool m_show_fov = false; + // Projectile aim omath::Color m_proj_color = omath::Color::from_rgba(255, 50, 50, 255); float m_proj_size = 10.f; diff --git a/include/omath/hud/screen_overlay.hpp b/include/omath/hud/screen_overlay.hpp new file mode 100644 index 00000000..315e7fc6 --- /dev/null +++ b/include/omath/hud/screen_overlay.hpp @@ -0,0 +1,73 @@ +// +// Created by orange on 13.08.2026. +// +#pragma once +#include "entity_overlay_widgets.hpp" +#include "hud_renderer_interface.hpp" +#include "omath/linear_algebra/vector2.hpp" +#include "omath/utility/color.hpp" +#include +#include + +namespace omath::hud::widget +{ + /// Crosshair drawn in the center of the screen. `gap` is the empty space between the + /// center and each line, `length` is the length of a single line. + struct Crosshair + { + Color color; + float gap = 4.f; + float length = 8.f; + float thickness = 1.f; + Color outline{0.f, 0.f, 0.f, 0.f}; + float dot_radius = 0.f; + }; + + /// Field-of-view boundary circle drawn around the center of the screen. + struct FovCircle + { + Color color; + float radius; + Color fill{0.f, 0.f, 0.f, 0.f}; + float thickness = 1.f; + int segments = 0; + }; +} // namespace omath::hud::widget + +namespace omath::hud +{ + /// Overlay for widgets positioned relative to the screen instead of an entity. + class ScreenOverlay final + { + public: + ScreenOverlay(const Vector2& screen_size, const std::shared_ptr& renderer); + + ScreenOverlay& add_crosshair(const widget::Crosshair& crosshair); + ScreenOverlay& add_fov_circle(const widget::FovCircle& fov_circle); + + // ── Declarative interface ───────────────────────────────────────── + /// Pass any combination of screen widget:: descriptor structs (and std::optional + /// from when()) to render them all in declaration order. + template + ScreenOverlay& contents(Widgets&&... widgets) + { + (dispatch(std::forward(widgets)), ...); + return *this; + } + + private: + // optional dispatch — enables when() conditional widgets + template + void dispatch(const std::optional& w) + { + if (w) + dispatch(*w); + } + + void dispatch(const widget::Crosshair& crosshair); + void dispatch(const widget::FovCircle& fov_circle); + + Vector2 m_center; + std::shared_ptr m_renderer; + }; +} // namespace omath::hud diff --git a/source/hud/screen_overlay.cpp b/source/hud/screen_overlay.cpp new file mode 100644 index 00000000..4d12180c --- /dev/null +++ b/source/hud/screen_overlay.cpp @@ -0,0 +1,68 @@ +// +// Created by orange on 13.08.2026. +// +#include "omath/hud/screen_overlay.hpp" + +namespace omath::hud +{ + ScreenOverlay::ScreenOverlay(const Vector2& screen_size, + const std::shared_ptr& renderer) + : m_center(screen_size / 2.f), m_renderer(renderer) + { + } + + ScreenOverlay& ScreenOverlay::add_crosshair(const widget::Crosshair& crosshair) + { + // Arms are filled rectangles rather than lines: line rendering is nudged by half a + // pixel in ImGui-like backends, which would shift them off the center dot. + const auto draw_arm = [&](const Vector2& min, const Vector2& max) + { + if (crosshair.outline.value().w > 0.f) + m_renderer->add_filled_rectangle(min - Vector2{1.f, 1.f}, max + Vector2{1.f, 1.f}, + crosshair.outline); + + m_renderer->add_filled_rectangle(min, max, crosshair.color); + }; + + const auto inner = crosshair.gap; + const auto outer = crosshair.gap + crosshair.length; + const auto half_thickness = crosshair.thickness / 2.f; + + draw_arm({m_center.x - half_thickness, m_center.y - outer}, {m_center.x + half_thickness, m_center.y - inner}); + draw_arm({m_center.x - half_thickness, m_center.y + inner}, {m_center.x + half_thickness, m_center.y + outer}); + draw_arm({m_center.x - outer, m_center.y - half_thickness}, {m_center.x - inner, m_center.y + half_thickness}); + draw_arm({m_center.x + inner, m_center.y - half_thickness}, {m_center.x + outer, m_center.y + half_thickness}); + + if (crosshair.dot_radius > 0.f) + { + if (crosshair.outline.value().w > 0.f) + m_renderer->add_filled_circle(m_center, crosshair.dot_radius + 1.f, crosshair.outline); + + m_renderer->add_filled_circle(m_center, crosshair.dot_radius, crosshair.color); + } + + return *this; + } + + ScreenOverlay& ScreenOverlay::add_fov_circle(const widget::FovCircle& fov_circle) + { + if (fov_circle.fill.value().w > 0.f) + m_renderer->add_filled_circle(m_center, fov_circle.radius, fov_circle.fill, fov_circle.segments); + + m_renderer->add_circle(m_center, fov_circle.radius, fov_circle.color, fov_circle.thickness, + fov_circle.segments); + + return *this; + } + + // ── widget dispatch ─────────────────────────────────────────────────────── + void ScreenOverlay::dispatch(const widget::Crosshair& crosshair) + { + add_crosshair(crosshair); + } + + void ScreenOverlay::dispatch(const widget::FovCircle& fov_circle) + { + add_fov_circle(fov_circle); + } +} // namespace omath::hud From 61bf2235dc9d5f9c8fa3d785ba8db2f76b19e412 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 13 Aug 2026 08:10:44 +0300 Subject: [PATCH 2/3] added more widgets --- examples/example_hud/gui/main_window.cpp | 48 ++++++++- examples/example_hud/gui/main_window.hpp | 13 +++ include/omath/hud/entity_overlay.hpp | 3 +- include/omath/hud/screen_overlay.hpp | 64 ++++++++++++ include/omath/hud/widget_render.hpp | 17 ++++ source/hud/entity_overlay.cpp | 66 ++----------- source/hud/screen_overlay.cpp | 118 ++++++++++++++++++++++- source/hud/widget_render.cpp | 64 ++++++++++++ 8 files changed, 326 insertions(+), 67 deletions(-) create mode 100644 include/omath/hud/widget_render.hpp create mode 100644 source/hud/widget_render.cpp diff --git a/examples/example_hud/gui/main_window.cpp b/examples/example_hud/gui/main_window.cpp index 8345fcb3..f9e49596 100644 --- a/examples/example_hud/gui/main_window.cpp +++ b/examples/example_hud/gui/main_window.cpp @@ -248,6 +248,30 @@ namespace imgui_desktop::gui ImGui::SliderFloat("Thick##fov", &m_fov_thickness, 0.5f, 5.f); } + if (ImGui::CollapsingHeader("Threat Arrow")) + { + ImGui::Checkbox("Show##threat", &m_show_threat); + ImGui::ColorEdit4("Color##threat", reinterpret_cast(&m_threat_color), ImGuiColorEditFlags_NoInputs); + ImGui::SliderFloat("Angle##threat", &m_threat_angle_deg, 0.f, 360.f); + ImGui::SliderFloat("Radius##threat", &m_threat_radius, 20.f, 250.f); + ImGui::SliderFloat("Size##threat", &m_threat_size, 4.f, 30.f); + } + + if (ImGui::CollapsingHeader("Hit Marker")) + { + ImGui::Checkbox("Show##hit", &m_show_hit); + ImGui::ColorEdit4("Color##hit", reinterpret_cast(&m_hit_color), ImGuiColorEditFlags_NoInputs); + ImGui::SliderFloat("Size##hit", &m_hit_size, 2.f, 30.f); + ImGui::SliderFloat("Gap##hit", &m_hit_gap, 0.f, 15.f); + ImGui::SliderFloat("Thick##hit", &m_hit_thickness, 0.5f, 5.f); + ImGui::SliderFloat("Alpha##hit", &m_hit_alpha, 0.f, 1.f); + } + + if (ImGui::CollapsingHeader("Corners", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("Show##corners", &m_show_corners); + } + if (ImGui::CollapsingHeader("Snap Line")) { ImGui::Checkbox("Show##snap", &m_show_snap); @@ -417,11 +441,27 @@ namespace imgui_desktop::gui static_cast(m_proj_figure)}), when(m_show_snap, SnapLine{{vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width})); + const auto threat_angle = omath::angles::degrees_to_radians(m_threat_angle_deg); + omath::hud::ScreenOverlay({vp->Size.x, vp->Size.y}, std::make_shared()) - .contents(when(m_show_crosshair, - Crosshair{m_crosshair_color, m_crosshair_gap, m_crosshair_length, m_crosshair_thickness, - m_crosshair_outline, m_crosshair_dot_radius}), - when(m_show_fov, FovCircle{m_fov_color, m_fov_radius, m_fov_fill, m_fov_thickness})); + .contents( + when(m_show_crosshair, + Crosshair{m_crosshair_color, m_crosshair_gap, m_crosshair_length, m_crosshair_thickness, + m_crosshair_outline, m_crosshair_dot_radius}), + when(m_show_fov, FovCircle{m_fov_color, m_fov_radius, m_fov_fill, m_fov_thickness}), + when(m_show_threat, ThreatArrow{threat_angle, m_threat_color, m_threat_radius, m_threat_size}), + when(m_show_hit, HitMarker{m_hit_color, m_hit_size, m_hit_gap, m_hit_thickness, + omath::Color{0.f, 0.f, 0.f, 1.f}, m_hit_alpha}), + when(m_show_corners, Corner{Anchor::TOP_LEFT, + {Label{omath::Color::from_rgba(255, 255, 255, 255), 0.f, + Outlined::On, "omath::hud"}, + Label{omath::Color::from_rgba(150, 220, 255, 255), 0.f, + Outlined::On, "ScreenOverlay demo"}}}), + when(m_show_corners, + Corner{Anchor::BOTTOM_RIGHT, + {Label{omath::Color::from_rgba(0, 255, 120, 255), 0.f, Outlined::On, "FPS: 240"}, + Label{omath::Color::from_rgba(200, 200, 200, 255), 0.f, Outlined::On, + "Ping: 24ms"}}})); } void MainWindow::present() diff --git a/examples/example_hud/gui/main_window.hpp b/examples/example_hud/gui/main_window.hpp index 80879263..37f43091 100644 --- a/examples/example_hud/gui/main_window.hpp +++ b/examples/example_hud/gui/main_window.hpp @@ -121,6 +121,19 @@ namespace imgui_desktop::gui float m_fov_radius = 120.f, m_fov_thickness = 1.5f; bool m_show_fov = false; + // Threat arrow + omath::Color m_threat_color = omath::Color::from_rgba(255, 40, 40, 255); + float m_threat_angle_deg = 45.f, m_threat_radius = 100.f, m_threat_size = 14.f; + bool m_show_threat = true; + + // Hit marker + omath::Color m_hit_color = omath::Color::from_rgba(255, 255, 255, 255); + float m_hit_size = 8.f, m_hit_gap = 3.f, m_hit_thickness = 2.f, m_hit_alpha = 1.f; + bool m_show_hit = true; + + // Corners + bool m_show_corners = true; + // Projectile aim omath::Color m_proj_color = omath::Color::from_rgba(255, 50, 50, 255); float m_proj_size = 10.f; diff --git a/include/omath/hud/entity_overlay.hpp b/include/omath/hud/entity_overlay.hpp index 2be16154..ecd5921f 100644 --- a/include/omath/hud/entity_overlay.hpp +++ b/include/omath/hud/entity_overlay.hpp @@ -8,6 +8,7 @@ #include "omath/3d_primitives/aabb.hpp" #include "omath/linear_algebra/vector2.hpp" #include "omath/utility/color.hpp" +#include "widget_render.hpp" #include #include #include @@ -262,8 +263,6 @@ namespace omath::hud void dispatch(const widget::AimDot& aim_dot); void dispatch(const widget::ProjectileAim& proj_widget); void draw_progress_ring(const Vector2& center, const widget::ProgressRing& ring); - void draw_label(const Vector2& position, const widget::Paint& paint, widget::Outlined outlined, - const std::string_view& text, const std::optional& glow); void draw_glow_polyline(const std::span>& points, const widget::Glow& glow, float thickness) const; void draw_glow_line(const Vector2& from, const Vector2& to, const widget::Glow& glow, diff --git a/include/omath/hud/screen_overlay.hpp b/include/omath/hud/screen_overlay.hpp index 315e7fc6..e9fc262a 100644 --- a/include/omath/hud/screen_overlay.hpp +++ b/include/omath/hud/screen_overlay.hpp @@ -6,8 +6,10 @@ #include "hud_renderer_interface.hpp" #include "omath/linear_algebra/vector2.hpp" #include "omath/utility/color.hpp" +#include #include #include +#include namespace omath::hud::widget { @@ -32,6 +34,61 @@ namespace omath::hud::widget float thickness = 1.f; int segments = 0; }; + + /// Arrow pointing from the screen center toward an off-screen threat. `angle` uses the + /// same convention as HudRendererInterface::add_arc: radians, 0 = right (+X). + struct ThreatArrow + { + float angle; + Color color; + float radius = 100.f; + float size = 12.f; + Color outline{0.f, 0.f, 0.f, 0.f}; + }; + + /// Transient hit-confirm marker: four diagonal ticks around the screen center. + /// `alpha` is a caller-driven fade multiplier (e.g. time-since-hit / fade_duration) — + /// the widget owns no clock of its own. + struct HitMarker + { + Color color; + float size = 8.f; + float gap = 3.f; + float thickness = 2.f; + Color outline{0.f, 0.f, 0.f, 0.f}; + float alpha = 1.f; + }; + + // ── Corner anchors ────────────────────────────────────────────────────── + enum class Anchor + { + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT, + }; + + using CornerWidget = std::variant; + + /// Vertical stack of widgets anchored to a screen corner or edge. Items are laid out + /// in declaration order, growing away from the anchored edge (downward from TOP_*, + /// upward from BOTTOM_*). Horizontal alignment follows the anchor: LEFT/RIGHT align + /// to that edge, CENTER centers each item's width. `Label::offset` adds extra distance + /// from the anchored edge for that one item, same role it plays in EntityOverlay's + /// side containers. + struct Corner + { + Anchor anchor; + std::initializer_list children; + float padding = 10.f; + + Corner(const Anchor a, const std::initializer_list c, const float p = 10.f) + : anchor(a), children(c), padding(p) + { + } + }; } // namespace omath::hud::widget namespace omath::hud @@ -44,6 +101,9 @@ namespace omath::hud ScreenOverlay& add_crosshair(const widget::Crosshair& crosshair); ScreenOverlay& add_fov_circle(const widget::FovCircle& fov_circle); + ScreenOverlay& add_threat_arrow(const widget::ThreatArrow& threat_arrow); + ScreenOverlay& add_hit_marker(const widget::HitMarker& hit_marker); + ScreenOverlay& add_corner(const widget::Corner& corner); // ── Declarative interface ───────────────────────────────────────── /// Pass any combination of screen widget:: descriptor structs (and std::optional @@ -66,7 +126,11 @@ namespace omath::hud void dispatch(const widget::Crosshair& crosshair); void dispatch(const widget::FovCircle& fov_circle); + void dispatch(const widget::ThreatArrow& threat_arrow); + void dispatch(const widget::HitMarker& hit_marker); + void dispatch(const widget::Corner& corner); + Vector2 m_screen_size; Vector2 m_center; std::shared_ptr m_renderer; }; diff --git a/include/omath/hud/widget_render.hpp b/include/omath/hud/widget_render.hpp new file mode 100644 index 00000000..a375ef79 --- /dev/null +++ b/include/omath/hud/widget_render.hpp @@ -0,0 +1,17 @@ +// +// Created by orange on 13.08.2026. +// +#pragma once +#include "entity_overlay_widgets.hpp" +#include "hud_renderer_interface.hpp" +#include "omath/linear_algebra/vector2.hpp" +#include +#include + +namespace omath::hud +{ + /// Shared text-drawing routine used by EntityOverlay and ScreenOverlay: renders an + /// optional glow halo, an optional 8-direction outline, then the label itself. + void draw_label(HudRendererInterface& renderer, const Vector2& position, const widget::Paint& paint, + widget::Outlined outlined, const std::string_view& text, const std::optional& glow); +} // namespace omath::hud diff --git a/source/hud/entity_overlay.cpp b/source/hud/entity_overlay.cpp index 3835dd27..1c4dd764 100644 --- a/source/hud/entity_overlay.cpp +++ b/source/hud/entity_overlay.cpp @@ -123,7 +123,7 @@ namespace omath::hud const widget::Outlined outlined, const std::string_view& text, const std::optional& glow) { - draw_label(m_text_cursor_right + Vector2{offset, 0.f}, color, outlined, text, glow); + draw_label(*m_renderer, m_text_cursor_right + Vector2{offset, 0.f}, color, outlined, text, glow); m_text_cursor_right.y += m_renderer->calc_text_size(text.data()).y; @@ -135,7 +135,7 @@ namespace omath::hud { m_text_cursor_top.y -= m_renderer->calc_text_size(text.data()).y; - draw_label(m_text_cursor_top + Vector2{0.f, -offset}, color, outlined, text, glow); + draw_label(*m_renderer, m_text_cursor_top + Vector2{0.f, -offset}, color, outlined, text, glow); return *this; } @@ -395,60 +395,6 @@ namespace omath::hud return *this; } - void EntityOverlay::draw_label(const Vector2& position, const widget::Paint& paint, - const widget::Outlined outlined, const std::string_view& text, - const std::optional& glow) - { - if (glow) - { - const int radius = static_cast(std::ceil(std::max(glow->radius, 0.f))); - for (int layer = radius; layer > 0; --layer) - { - const float alpha = glow->color.value().w * std::max(glow->intensity, 0.f) - * (1.f - static_cast(layer - 1) / static_cast(radius + 1)); - const auto value = glow->color.value(); - const Color color{value.x, value.y, value.z, alpha / static_cast(radius)}; - for (int x = -layer; x <= layer; ++x) - { - m_renderer->add_text(position + Vector2{static_cast(x), static_cast(-layer)}, - color, text); - m_renderer->add_text(position + Vector2{static_cast(x), static_cast(layer)}, - color, text); - } - for (int y = -layer + 1; y < layer; ++y) - { - m_renderer->add_text(position + Vector2{static_cast(-layer), static_cast(y)}, - color, text); - m_renderer->add_text(position + Vector2{static_cast(layer), static_cast(y)}, - color, text); - } - } - } - - if (outlined == widget::Outlined::On) - { - static constexpr std::array outline_offsets = { - Vector2{-1, -1}, Vector2{-1, 0}, Vector2{-1, 1}, Vector2{0, -1}, - Vector2{0, 1}, Vector2{1, -1}, Vector2{1, 0}, Vector2{1, 1}}; - - for (const auto& outline_offset : outline_offsets) - m_renderer->add_text(position + outline_offset, Color{0.f, 0.f, 0.f, 1.f}, text); - } - - std::visit( - widget::Overloaded{ - [&](const Color& color) - { - m_renderer->add_text(position, color, text); - }, - [&](const Gradient& gradient) - { - m_renderer->add_gradient_text(position, gradient, text); - }, - }, - paint); - } - void EntityOverlay::draw_glow_polyline(const std::span>& points, const widget::Glow& glow, const float thickness) const { @@ -613,7 +559,7 @@ namespace omath::hud { const auto text_size = m_renderer->calc_text_size(text); - draw_label(m_text_cursor_bottom + Vector2{0.f, offset}, color, outlined, text, glow); + draw_label(*m_renderer, m_text_cursor_bottom + Vector2{0.f, offset}, color, outlined, text, glow); m_text_cursor_bottom.y += text_size.y; @@ -627,7 +573,7 @@ namespace omath::hud const auto text_size = m_renderer->calc_text_size(text); const auto pos = m_text_cursor_left + Vector2{-(offset + text_size.x), 0.f}; - draw_label(pos, color, outlined, text, glow); + draw_label(*m_renderer, pos, color, outlined, text, glow); m_text_cursor_left.y += text_size.y; @@ -644,7 +590,7 @@ namespace omath::hud m_canvas.bottom_left_corner.x + (m_canvas.bottom_right_corner.x - m_canvas.bottom_left_corner.x) / 2.f; const auto pos = Vector2{box_center_x - text_size.x / 2.f, m_text_cursor_bottom.y + offset}; - draw_label(pos, color, outlined, text, glow); + draw_label(*m_renderer, pos, color, outlined, text, glow); m_text_cursor_bottom.y += text_size.y; @@ -662,7 +608,7 @@ namespace omath::hud m_text_cursor_top.y -= text_size.y; const auto pos = Vector2{box_center_x - text_size.x / 2.f, m_text_cursor_top.y - offset}; - draw_label(pos, color, outlined, text, glow); + draw_label(*m_renderer, pos, color, outlined, text, glow); return *this; } diff --git a/source/hud/screen_overlay.cpp b/source/hud/screen_overlay.cpp index 4d12180c..8b08cfdf 100644 --- a/source/hud/screen_overlay.cpp +++ b/source/hud/screen_overlay.cpp @@ -2,12 +2,16 @@ // Created by orange on 13.08.2026. // #include "omath/hud/screen_overlay.hpp" +#include "omath/hud/widget_render.hpp" +#include +#include +#include namespace omath::hud { ScreenOverlay::ScreenOverlay(const Vector2& screen_size, const std::shared_ptr& renderer) - : m_center(screen_size / 2.f), m_renderer(renderer) + : m_screen_size(screen_size), m_center(screen_size / 2.f), m_renderer(renderer) { } @@ -55,6 +59,103 @@ namespace omath::hud return *this; } + ScreenOverlay& ScreenOverlay::add_threat_arrow(const widget::ThreatArrow& threat_arrow) + { + const Vector2 dir{std::cos(threat_arrow.angle), std::sin(threat_arrow.angle)}; + const Vector2 perp{-dir.y, dir.x}; + + const auto tip = m_center + dir * threat_arrow.radius; + const auto base = m_center + dir * (threat_arrow.radius - threat_arrow.size); + const auto wing = perp * (threat_arrow.size * 0.6f); + + const std::array, 3> tri = {tip, base + wing, base - wing}; + + if (threat_arrow.outline.value().w > 0.f) + m_renderer->add_polyline({tri.data(), tri.size()}, threat_arrow.outline, 2.f); + + m_renderer->add_filled_polyline({tri.data(), tri.size()}, threat_arrow.color); + + return *this; + } + + ScreenOverlay& ScreenOverlay::add_hit_marker(const widget::HitMarker& hit_marker) + { + const auto fade = std::clamp(hit_marker.alpha, 0.f, 1.f); + const auto value = hit_marker.color.value(); + const Color color{value.x, value.y, value.z, value.w * fade}; + const auto outline_value = hit_marker.outline.value(); + const Color outline{outline_value.x, outline_value.y, outline_value.z, outline_value.w * fade}; + + static constexpr float k_diag = 0.70710678f; + static constexpr std::array, 4> directions = { + Vector2{k_diag, k_diag}, Vector2{k_diag, -k_diag}, Vector2{-k_diag, k_diag}, + Vector2{-k_diag, -k_diag}}; + + for (const auto& dir : directions) + { + const auto from = m_center + dir * hit_marker.gap; + const auto to = m_center + dir * (hit_marker.gap + hit_marker.size); + + if (outline_value.w > 0.f) + m_renderer->add_line(from, to, outline, hit_marker.thickness + 2.f); + + m_renderer->add_line(from, to, color, hit_marker.thickness); + } + + return *this; + } + + ScreenOverlay& ScreenOverlay::add_corner(const widget::Corner& corner) + { + using widget::Anchor; + const bool is_top = corner.anchor == Anchor::TOP_LEFT || corner.anchor == Anchor::TOP_CENTER + || corner.anchor == Anchor::TOP_RIGHT; + const bool is_left = corner.anchor == Anchor::TOP_LEFT || corner.anchor == Anchor::BOTTOM_LEFT; + const bool is_right = corner.anchor == Anchor::TOP_RIGHT || corner.anchor == Anchor::BOTTOM_RIGHT; + + float cursor_y = is_top ? corner.padding : m_screen_size.y - corner.padding; + + const auto resolve_x = [&](const float width) -> float + { + if (is_left) + return corner.padding; + if (is_right) + return m_screen_size.x - corner.padding - width; + return (m_screen_size.x - width) / 2.f; + }; + + for (const auto& child : corner.children) + std::visit( + widget::Overloaded{ + [](const widget::None&) + { + }, + [&](const widget::Label& w) + { + const auto size = m_renderer->calc_text_size(w.text); + if (is_top) + { + draw_label(*m_renderer, {resolve_x(size.x), cursor_y + w.offset}, w.color, + w.outlined, w.text, w.glow); + cursor_y += size.y; + } + else + { + cursor_y -= size.y; + draw_label(*m_renderer, {resolve_x(size.x), cursor_y - w.offset}, w.color, + w.outlined, w.text, w.glow); + } + }, + [&](const widget::SpaceVertical& w) + { + cursor_y += is_top ? w.size : -w.size; + }, + }, + child); + + return *this; + } + // ── widget dispatch ─────────────────────────────────────────────────────── void ScreenOverlay::dispatch(const widget::Crosshair& crosshair) { @@ -65,4 +166,19 @@ namespace omath::hud { add_fov_circle(fov_circle); } + + void ScreenOverlay::dispatch(const widget::ThreatArrow& threat_arrow) + { + add_threat_arrow(threat_arrow); + } + + void ScreenOverlay::dispatch(const widget::HitMarker& hit_marker) + { + add_hit_marker(hit_marker); + } + + void ScreenOverlay::dispatch(const widget::Corner& corner) + { + add_corner(corner); + } } // namespace omath::hud diff --git a/source/hud/widget_render.cpp b/source/hud/widget_render.cpp new file mode 100644 index 00000000..0075b729 --- /dev/null +++ b/source/hud/widget_render.cpp @@ -0,0 +1,64 @@ +// +// Created by orange on 13.08.2026. +// +#include "omath/hud/widget_render.hpp" +#include +#include +#include + +namespace omath::hud +{ + void draw_label(HudRendererInterface& renderer, const Vector2& position, const widget::Paint& paint, + const widget::Outlined outlined, const std::string_view& text, + const std::optional& glow) + { + if (glow) + { + const int radius = static_cast(std::ceil(std::max(glow->radius, 0.f))); + for (int layer = radius; layer > 0; --layer) + { + const float alpha = glow->color.value().w * std::max(glow->intensity, 0.f) + * (1.f - static_cast(layer - 1) / static_cast(radius + 1)); + const auto value = glow->color.value(); + const Color color{value.x, value.y, value.z, alpha / static_cast(radius)}; + for (int x = -layer; x <= layer; ++x) + { + renderer.add_text(position + Vector2{static_cast(x), static_cast(-layer)}, + color, text); + renderer.add_text(position + Vector2{static_cast(x), static_cast(layer)}, + color, text); + } + for (int y = -layer + 1; y < layer; ++y) + { + renderer.add_text(position + Vector2{static_cast(-layer), static_cast(y)}, + color, text); + renderer.add_text(position + Vector2{static_cast(layer), static_cast(y)}, + color, text); + } + } + } + + if (outlined == widget::Outlined::On) + { + static constexpr std::array outline_offsets = { + Vector2{-1, -1}, Vector2{-1, 0}, Vector2{-1, 1}, Vector2{0, -1}, + Vector2{0, 1}, Vector2{1, -1}, Vector2{1, 0}, Vector2{1, 1}}; + + for (const auto& outline_offset : outline_offsets) + renderer.add_text(position + outline_offset, Color{0.f, 0.f, 0.f, 1.f}, text); + } + + std::visit( + widget::Overloaded{ + [&](const Color& color) + { + renderer.add_text(position, color, text); + }, + [&](const Gradient& gradient) + { + renderer.add_gradient_text(position, gradient, text); + }, + }, + paint); + } +} // namespace omath::hud From 54d91213f7707e36f296e75e36d3c947353badcc Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 13 Aug 2026 08:52:53 +0300 Subject: [PATCH 3/3] improved stuff --- examples/example_hud/gui/main_window.cpp | 61 ++++- examples/example_hud/gui/main_window.hpp | 14 ++ include/omath/hud/entity_overlay.hpp | 34 +-- include/omath/hud/entity_overlay_widgets.hpp | 2 +- include/omath/hud/screen_overlay.hpp | 50 ++++ include/omath/hud/widget_render.hpp | 40 ++++ source/hud/entity_overlay.cpp | 231 +++---------------- source/hud/screen_overlay.cpp | 69 ++++++ source/hud/widget_render.cpp | 173 ++++++++++++++ 9 files changed, 445 insertions(+), 229 deletions(-) diff --git a/examples/example_hud/gui/main_window.cpp b/examples/example_hud/gui/main_window.cpp index f9e49596..b2028714 100644 --- a/examples/example_hud/gui/main_window.cpp +++ b/examples/example_hud/gui/main_window.cpp @@ -272,6 +272,34 @@ namespace imgui_desktop::gui ImGui::Checkbox("Show##corners", &m_show_corners); } + if (ImGui::CollapsingHeader("Center Bars", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("Show##cbar", &m_show_center_bars); + ImGui::ColorEdit4("Color##cbar", reinterpret_cast(&m_center_bar_color), + ImGuiColorEditFlags_NoInputs); + ImGui::ColorEdit4("Outline##cbar", reinterpret_cast(&m_center_bar_outline), + ImGuiColorEditFlags_NoInputs); + ImGui::ColorEdit4("BG##cbar", reinterpret_cast(&m_center_bar_bg), ImGuiColorEditFlags_NoInputs); + ImGui::Checkbox("Gradient fill##cbar", &m_center_bar_gradient); + ImGui::Checkbox("Dashed##cbar", &m_center_bar_dashed); + if (m_center_bar_dashed) + { + ImGui::SliderFloat("Dash len##cbar", &m_center_bar_dash_len, 2.f, 30.f); + ImGui::SliderFloat("Gap len##cbar", &m_center_bar_gap_len, 1.f, 20.f); + } + ImGui::SliderFloat("Length##cbar", &m_center_bar_length, 20.f, 250.f); + ImGui::SliderFloat("Thick##cbar", &m_center_bar_thickness, 2.f, 30.f); + ImGui::SliderFloat("Offset##cbar", &m_center_bar_offset, 5.f, 60.f); + ImGui::SliderFloat("Left ratio##cbar", &m_center_bar_left_ratio, 0.f, 1.f); + ImGui::SliderFloat("Right ratio##cbar", &m_center_bar_right_ratio, 0.f, 1.f); + draw_glow_controls("Center bar glow", m_center_bar_glow); + if (m_center_bar_glow.enabled) + { + ImGui::InputInt("Blur layers##cbar", &m_center_bar_glow_layers); + ImGui::InputFloat("Rounding##cbar", &m_center_bar_glow_rounding); + } + } + if (ImGui::CollapsingHeader("Snap Line")) { ImGui::Checkbox("Show##snap", &m_show_snap); @@ -288,8 +316,6 @@ namespace imgui_desktop::gui using namespace omath::hud::widget; using omath::hud::when; const auto* vp = ImGui::GetMainViewport(); - const DashedBar dbar{m_bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, - m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset}; const float entity_height = m_entity_bottom_y - m_entity_top_y; const float entity_half_width = entity_height / m_entity_aspect; const auto joint = [&](const float x, const float y) @@ -353,8 +379,15 @@ namespace imgui_desktop::gui const auto bar_glow = make_glow(m_bar_glow); const auto label_glow = make_glow(m_label_glow); const auto canvas_glow = make_glow(m_canvas_glow); + const auto center_bar_glow = make_glow(m_center_bar_glow); + const std::optional center_bar_edge_glow = + center_bar_glow ? std::optional{CanvasGlow{*center_bar_glow, m_center_bar_glow_layers, + m_center_bar_glow_rounding}} + : std::nullopt; const BarPaint bar_color = m_gradient_bars ? BarPaint{BarGradient{red, green}} : BarPaint{m_bar_color}; const Bar bar{bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, m_bar_value, m_bar_offset, bar_glow}; + const DashedBar dbar{bar_color, m_bar_outline_color, m_bar_bg_color, m_bar_width, + m_bar_value, m_bar_dash_len, m_bar_dash_gap, m_bar_offset}; const Paint box_fill = m_gradient_box_fill ? Paint{omath::hud::Gradient{box_gradient_top, box_gradient_top, box_gradient_bottom, box_gradient_bottom}} @@ -442,6 +475,10 @@ namespace imgui_desktop::gui when(m_show_snap, SnapLine{{vp->Size.x / 2.f, vp->Size.y}, m_snap_color, m_snap_width})); const auto threat_angle = omath::angles::degrees_to_radians(m_threat_angle_deg); + const BarPaint center_bar_paint = + m_center_bar_gradient + ? BarPaint{BarGradient{omath::Color::from_rgba(255, 60, 60, 255), m_center_bar_color}} + : BarPaint{m_center_bar_color}; omath::hud::ScreenOverlay({vp->Size.x, vp->Size.y}, std::make_shared()) .contents( @@ -461,7 +498,25 @@ namespace imgui_desktop::gui Corner{Anchor::BOTTOM_RIGHT, {Label{omath::Color::from_rgba(0, 255, 120, 255), 0.f, Outlined::On, "FPS: 240"}, Label{omath::Color::from_rgba(200, 200, 200, 255), 0.f, Outlined::On, - "Ping: 24ms"}}})); + "Ping: 24ms"}}}), + when(m_show_center_bars && !m_center_bar_dashed, + CenterBar{CenterBar::Side::LEFT, center_bar_paint, m_center_bar_outline, m_center_bar_bg, + m_center_bar_length, m_center_bar_thickness, m_center_bar_left_ratio, + m_center_bar_offset, center_bar_edge_glow}), + when(m_show_center_bars && !m_center_bar_dashed, + CenterBar{CenterBar::Side::RIGHT, center_bar_paint, m_center_bar_outline, m_center_bar_bg, + m_center_bar_length, m_center_bar_thickness, m_center_bar_right_ratio, + m_center_bar_offset, center_bar_edge_glow}), + when(m_show_center_bars && m_center_bar_dashed, + DashedCenterBar{CenterBar::Side::LEFT, center_bar_paint, m_center_bar_outline, + m_center_bar_bg, m_center_bar_length, m_center_bar_thickness, + m_center_bar_left_ratio, m_center_bar_dash_len, m_center_bar_gap_len, + m_center_bar_offset}), + when(m_show_center_bars && m_center_bar_dashed, + DashedCenterBar{CenterBar::Side::RIGHT, center_bar_paint, m_center_bar_outline, + m_center_bar_bg, m_center_bar_length, m_center_bar_thickness, + m_center_bar_right_ratio, m_center_bar_dash_len, m_center_bar_gap_len, + m_center_bar_offset})); } void MainWindow::present() diff --git a/examples/example_hud/gui/main_window.hpp b/examples/example_hud/gui/main_window.hpp index 37f43091..67b1e9d6 100644 --- a/examples/example_hud/gui/main_window.hpp +++ b/examples/example_hud/gui/main_window.hpp @@ -134,6 +134,20 @@ namespace imgui_desktop::gui // Corners bool m_show_corners = true; + // Center bars + omath::Color m_center_bar_color = omath::Color::from_rgba(0, 220, 255, 255); + omath::Color m_center_bar_outline = omath::Color::from_rgba(0, 0, 0, 255); + omath::Color m_center_bar_bg = omath::Color::from_rgba(0, 0, 0, 130); + float m_center_bar_length = 100.f, m_center_bar_thickness = 8.f, m_center_bar_offset = 24.f; + float m_center_bar_left_ratio = 0.7f, m_center_bar_right_ratio = 0.4f; + bool m_center_bar_gradient = true; + bool m_show_center_bars = true; + GlowSettings m_center_bar_glow{{0.f, 0.9f, 1.f, 0.8f}}; + int m_center_bar_glow_layers = 24; + float m_center_bar_glow_rounding = 6.f; + bool m_center_bar_dashed = false; + float m_center_bar_dash_len = 8.f, m_center_bar_gap_len = 4.f; + // Projectile aim omath::Color m_proj_color = omath::Color::from_rgba(255, 50, 50, 255); float m_proj_size = 10.f; diff --git a/include/omath/hud/entity_overlay.hpp b/include/omath/hud/entity_overlay.hpp index ecd5921f..8a8fea00 100644 --- a/include/omath/hud/entity_overlay.hpp +++ b/include/omath/hud/entity_overlay.hpp @@ -58,19 +58,21 @@ namespace omath::hud float height, float ratio, float offset = 5.f, const std::optional& glow = std::nullopt); - EntityOverlay& add_right_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color, - float width, float ratio, float dash_len, float gap_len, - float offset = 5.f); + EntityOverlay& add_right_dashed_bar(const widget::BarPaint& color, const Color& outline_color, + const Color& bg_color, float width, float ratio, float dash_len, + float gap_len, float offset = 5.f); - EntityOverlay& add_left_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color, - float width, float ratio, float dash_len, float gap_len, float offset = 5.f); + EntityOverlay& add_left_dashed_bar(const widget::BarPaint& color, const Color& outline_color, + const Color& bg_color, float width, float ratio, float dash_len, + float gap_len, float offset = 5.f); - EntityOverlay& add_top_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color, - float height, float ratio, float dash_len, float gap_len, float offset = 5.f); + EntityOverlay& add_top_dashed_bar(const widget::BarPaint& color, const Color& outline_color, + const Color& bg_color, float height, float ratio, float dash_len, + float gap_len, float offset = 5.f); - EntityOverlay& add_bottom_dashed_bar(const Color& color, const Color& outline_color, const Color& bg_color, - float height, float ratio, float dash_len, float gap_len, - float offset = 5.f); + EntityOverlay& add_bottom_dashed_bar(const widget::BarPaint& color, const Color& outline_color, + const Color& bg_color, float height, float ratio, float dash_len, + float gap_len, float offset = 5.f); // ── Labels ─────────────────────────────────────────────────────── EntityOverlay& add_right_label(const widget::Paint& color, float offset, widget::Outlined outlined, @@ -263,22 +265,10 @@ namespace omath::hud void dispatch(const widget::AimDot& aim_dot); void dispatch(const widget::ProjectileAim& proj_widget); void draw_progress_ring(const Vector2& center, const widget::ProgressRing& ring); - void draw_glow_polyline(const std::span>& points, const widget::Glow& glow, - float thickness) const; void draw_glow_line(const Vector2& from, const Vector2& to, const widget::Glow& glow, float thickness) const; - void draw_glow_rectangle(const Vector2& min, const Vector2& max, - const std::optional& glow) const; - void draw_canvas_glow(const widget::CanvasGlow& canvas_glow) const; - void draw_filled_rectangle(const Vector2& min, const Vector2& max, - const widget::Paint& paint) const; - [[nodiscard]] - static widget::Paint resolve_bar_paint(const widget::BarPaint& paint, float ratio, bool vertical); void draw_dashed_line(const Vector2& from, const Vector2& to, const Color& color, float dash_len, float gap_len, float thickness) const; - void draw_dashed_fill(const Vector2& origin, const Vector2& step_dir, - const Vector2& perp_dir, float full_len, float filled_len, const Color& fill_color, - const Color& split_color, float dash_len, float gap_len) const; CanvasBox m_canvas; Vector2 m_text_cursor_right; diff --git a/include/omath/hud/entity_overlay_widgets.hpp b/include/omath/hud/entity_overlay_widgets.hpp index 3cca92d6..c3a37987 100644 --- a/include/omath/hud/entity_overlay_widgets.hpp +++ b/include/omath/hud/entity_overlay_widgets.hpp @@ -193,7 +193,7 @@ namespace omath::hud::widget /// A dashed bar. Same field semantics as Bar plus dash parameters. struct DashedBar { - Color color; + BarPaint color; Color outline; Color bg; float size; diff --git a/include/omath/hud/screen_overlay.hpp b/include/omath/hud/screen_overlay.hpp index e9fc262a..000fb7fb 100644 --- a/include/omath/hud/screen_overlay.hpp +++ b/include/omath/hud/screen_overlay.hpp @@ -59,6 +59,52 @@ namespace omath::hud::widget float alpha = 1.f; }; + /// Vertical bar flanking the screen center — pair a LEFT and a RIGHT instance to build a + /// ` ... ` layout around a Crosshair (or any other center widget; CenterBar is + /// not coupled to Crosshair). Supports the same paint vocabulary as EntityOverlay's + /// bars: solid/gradient/BarGradient fill, outline, background, and glow. `length` is the + /// bar's vertical extent, centered on the screen's vertical middle; `thickness` is its + /// horizontal extent. Fill grows bottom-to-top as `ratio` increases, matching + /// EntityOverlay's RightSide/LeftSide bars. `glow` wraps just the filled portion (so it + /// grows with the bar) and uses the same layered, rounded, smoothstep-falloff bloom as + /// EntityOverlay's CanvasGlow rather than a plain expanding-stroke glow. + struct CenterBar + { + enum class Side + { + LEFT, + RIGHT, + }; + + Side side; + BarPaint color; + Color outline{0.f, 0.f, 0.f, 0.f}; + Color bg{0.f, 0.f, 0.f, 0.f}; + float length; + float thickness = 6.f; + float ratio; + float offset = 20.f; + std::optional glow = std::nullopt; + }; + + /// Dashed counterpart to CenterBar, matching EntityOverlay's Bar/DashedBar split. Same + /// positioning, bottom-to-top fill direction, and BarPaint (solid/gradient/BarGradient) + /// fill support as CenterBar; unlike CenterBar it has no glow. `outline` is also the + /// color drawn in the gaps between dashes. + struct DashedCenterBar + { + CenterBar::Side side; + BarPaint color; + Color outline{0.f, 0.f, 0.f, 0.f}; + Color bg{0.f, 0.f, 0.f, 0.f}; + float length; + float thickness = 6.f; + float ratio; + float dash_len = 8.f; + float gap_len = 5.f; + float offset = 20.f; + }; + // ── Corner anchors ────────────────────────────────────────────────────── enum class Anchor { @@ -103,6 +149,8 @@ namespace omath::hud ScreenOverlay& add_fov_circle(const widget::FovCircle& fov_circle); ScreenOverlay& add_threat_arrow(const widget::ThreatArrow& threat_arrow); ScreenOverlay& add_hit_marker(const widget::HitMarker& hit_marker); + ScreenOverlay& add_center_bar(const widget::CenterBar& center_bar); + ScreenOverlay& add_dashed_center_bar(const widget::DashedCenterBar& dashed_center_bar); ScreenOverlay& add_corner(const widget::Corner& corner); // ── Declarative interface ───────────────────────────────────────── @@ -128,6 +176,8 @@ namespace omath::hud void dispatch(const widget::FovCircle& fov_circle); void dispatch(const widget::ThreatArrow& threat_arrow); void dispatch(const widget::HitMarker& hit_marker); + void dispatch(const widget::CenterBar& center_bar); + void dispatch(const widget::DashedCenterBar& dashed_center_bar); void dispatch(const widget::Corner& corner); Vector2 m_screen_size; diff --git a/include/omath/hud/widget_render.hpp b/include/omath/hud/widget_render.hpp index a375ef79..2af38cc3 100644 --- a/include/omath/hud/widget_render.hpp +++ b/include/omath/hud/widget_render.hpp @@ -6,6 +6,7 @@ #include "hud_renderer_interface.hpp" #include "omath/linear_algebra/vector2.hpp" #include +#include #include namespace omath::hud @@ -14,4 +15,43 @@ namespace omath::hud /// optional glow halo, an optional 8-direction outline, then the label itself. void draw_label(HudRendererInterface& renderer, const Vector2& position, const widget::Paint& paint, widget::Outlined outlined, const std::string_view& text, const std::optional& glow); + + /// Draws a soft multi-layer glow halo tracing a closed polyline. + void draw_glow_polyline(HudRendererInterface& renderer, std::span> points, + const widget::Glow& glow, float thickness); + + /// Draws a glow halo around the four edges of an axis-aligned rectangle. No-op when + /// `glow` is unset or has a non-positive radius. + void draw_glow_rectangle(HudRendererInterface& renderer, const Vector2& min, const Vector2& max, + const std::optional& glow); + + /// Fills a rectangle with a solid color or a corner gradient, depending on which + /// alternative `paint` currently holds. + void draw_filled_rectangle(HudRendererInterface& renderer, const Vector2& min, const Vector2& max, + const widget::Paint& paint); + + /// Resolves a bar's BarPaint into a drawable Paint. Color/Gradient pass through + /// unchanged; BarGradient blends toward `full_color` by `ratio` and builds a two-stop + /// Gradient oriented along the bar's fill axis (`vertical` selects top-to-bottom vs + /// left-to-right). + [[nodiscard]] + widget::Paint resolve_bar_paint(const widget::BarPaint& paint, float ratio, bool vertical); + + /// Draws a soft ambient glow bloom expanding outward from an axis-aligned rectangle's + /// edges: layered rounded-rect strokes with smoothstep falloff, clipped per edge so the + /// glow never bleeds into the rectangle's interior. Same algorithm as EntityOverlay's + /// CanvasGlow, generalized to any rectangle. No-op when radius is non-positive. + void draw_edge_glow(HudRendererInterface& renderer, const Vector2& min, const Vector2& max, + const widget::CanvasGlow& canvas_glow); + + /// Punches a dash-and-gap pattern into a bar: draws `gap_color` rectangles at each gap + /// interval from `origin` along `step_dir` for the full `full_len`, `perp_dir` giving + /// the bar's width. Dashes are centered within `full_len` with any remainder split into + /// a leading/trailing gap. Meant to be called after the bar's background and fill have + /// already been drawn as continuous rectangles (so gaps punch through both, and the fill + /// can be any Paint — solid, gradient, or BarGradient — rather than a flat per-dash + /// color). + void draw_dash_gaps(HudRendererInterface& renderer, const Vector2& origin, const Vector2& step_dir, + const Vector2& perp_dir, float full_len, const Color& gap_color, float dash_len, + float gap_len); } // namespace omath::hud diff --git a/source/hud/entity_overlay.cpp b/source/hud/entity_overlay.cpp index 1c4dd764..67aa61df 100644 --- a/source/hud/entity_overlay.cpp +++ b/source/hud/entity_overlay.cpp @@ -2,7 +2,6 @@ // Created by orange on 13.03.2026. // #include "omath/hud/entity_overlay.hpp" -#include namespace omath::hud { @@ -90,8 +89,8 @@ namespace omath::hud const auto fill_min = bar_start - Vector2{0.f, max_bar_height * ratio}; m_renderer->add_filled_rectangle(bar_min, bar_max, bg_color); - draw_glow_rectangle(fill_min, bar_max, glow); - draw_filled_rectangle(fill_min, bar_max, resolve_bar_paint(color, ratio, true)); + draw_glow_rectangle(*m_renderer, fill_min, bar_max, glow); + draw_filled_rectangle(*m_renderer, fill_min, bar_max, resolve_bar_paint(color, ratio, true)); m_renderer->add_rectangle(bar_min, bar_max, outline_color); m_text_cursor_right.x += offset + width; @@ -111,8 +110,8 @@ namespace omath::hud const auto fill_min = bar_start - Vector2{0.f, max_bar_height * ratio}; m_renderer->add_filled_rectangle(bar_min, bar_max, bg_color); - draw_glow_rectangle(fill_min, bar_max, glow); - draw_filled_rectangle(fill_min, bar_max, resolve_bar_paint(color, ratio, true)); + draw_glow_rectangle(*m_renderer, fill_min, bar_max, glow); + draw_filled_rectangle(*m_renderer, fill_min, bar_max, resolve_bar_paint(color, ratio, true)); m_renderer->add_rectangle(bar_min, bar_max, outline_color); m_text_cursor_left.x -= offset + width; @@ -152,8 +151,8 @@ namespace omath::hud const auto fill_max = Vector2{bar_start.x + max_bar_width * ratio, bar_start.y}; m_renderer->add_filled_rectangle(bar_min, bar_max, bg_color); - draw_glow_rectangle(bar_min, fill_max, glow); - draw_filled_rectangle(bar_min, fill_max, resolve_bar_paint(color, ratio, false)); + draw_glow_rectangle(*m_renderer, bar_min, fill_max, glow); + draw_filled_rectangle(*m_renderer, bar_min, fill_max, resolve_bar_paint(color, ratio, false)); m_renderer->add_rectangle(bar_min, bar_max, outline_color); m_text_cursor_top.y -= offset + height; @@ -169,74 +168,19 @@ namespace omath::hud return *this; } - void EntityOverlay::draw_dashed_fill(const Vector2& origin, const Vector2& step_dir, - const Vector2& perp_dir, const float full_len, const float filled_len, - const Color& fill_color, const Color& split_color, const float dash_len, - const float gap_len) const - { - if (full_len <= 0.f) - return; - - const float step = dash_len + gap_len; - const float n = std::floor((full_len + gap_len) / step); - if (n < 1.f) - return; - - const float used = n * dash_len + (n - 1.f) * gap_len; - const float offset = (full_len - used) / 2.f; - - const auto fill_rect = [&](const Vector2& a, const Vector2& b, const Color& c) - { - m_renderer->add_filled_rectangle({std::min(a.x, b.x), std::min(a.y, b.y)}, - {std::max(a.x, b.x), std::max(a.y, b.y)}, c); - }; - - // Draw split lines (gaps) across the full bar first - // Leading gap - if (offset > 0.f) - fill_rect(origin, origin + step_dir * offset + perp_dir, split_color); - - for (float i = 0.f; i < n; ++i) - { - const float dash_start = offset + i * step; - const float dash_end = dash_start + dash_len; - const float gap_start = dash_end; - const float gap_end = dash_start + step; - - // Fill dash only up to filled_len - if (dash_start < filled_len) - { - const auto a = origin + step_dir * dash_start; - const auto b = a + step_dir * std::min(dash_len, filled_len - dash_start) + perp_dir; - fill_rect(a, b, fill_color); - } - - // Split line (gap) — always drawn across full bar - if (i < n - 1.f && gap_start < full_len) - { - const auto a = origin + step_dir * gap_start; - const auto b = origin + step_dir * std::min(gap_end, full_len) + perp_dir; - fill_rect(a, b, split_color); - } - } - - // Trailing gap - const float trail_start = offset + n * dash_len + (n - 1.f) * gap_len; - if (trail_start < full_len) - fill_rect(origin + step_dir * trail_start, origin + step_dir * full_len + perp_dir, split_color); - } - - EntityOverlay& EntityOverlay::add_right_dashed_bar(const Color& color, const Color& outline_color, + EntityOverlay& EntityOverlay::add_right_dashed_bar(const widget::BarPaint& color, const Color& outline_color, const Color& bg_color, const float width, float ratio, const float dash_len, const float gap_len, const float offset) { ratio = std::clamp(ratio, 0.f, 1.f); const float height = std::abs(m_canvas.top_right_corner.y - m_canvas.bottom_right_corner.y); const auto bar_start = Vector2{m_text_cursor_right.x + offset, m_canvas.bottom_right_corner.y}; + const auto bar_max = bar_start + Vector2{width, 0.f}; + const auto fill_min = bar_start - Vector2{0.f, height * ratio}; m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2{width, -height}, bg_color); - draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len, - gap_len); + draw_filled_rectangle(*m_renderer, fill_min, bar_max, resolve_bar_paint(color, ratio, true)); + draw_dash_gaps(*m_renderer, bar_start, {0.f, -1.f}, {width, 0.f}, height, outline_color, dash_len, gap_len); m_renderer->add_rectangle(bar_start - Vector2{1.f, 0.f}, bar_start + Vector2{width, -height}, outline_color); m_text_cursor_right.x += offset + width; @@ -244,17 +188,19 @@ namespace omath::hud return *this; } - EntityOverlay& EntityOverlay::add_left_dashed_bar(const Color& color, const Color& outline_color, + EntityOverlay& EntityOverlay::add_left_dashed_bar(const widget::BarPaint& color, const Color& outline_color, const Color& bg_color, const float width, float ratio, const float dash_len, const float gap_len, const float offset) { ratio = std::clamp(ratio, 0.f, 1.f); const float height = std::abs(m_canvas.top_left_corner.y - m_canvas.bottom_left_corner.y); const auto bar_start = Vector2{m_text_cursor_left.x - (offset + width), m_canvas.bottom_left_corner.y}; + const auto bar_max = bar_start + Vector2{width, 0.f}; + const auto fill_min = bar_start - Vector2{0.f, height * ratio}; m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2{width, -height}, bg_color); - draw_dashed_fill(bar_start, {0.f, -1.f}, {width, 0.f}, height, height * ratio, color, outline_color, dash_len, - gap_len); + draw_filled_rectangle(*m_renderer, fill_min, bar_max, resolve_bar_paint(color, ratio, true)); + draw_dash_gaps(*m_renderer, bar_start, {0.f, -1.f}, {width, 0.f}, height, outline_color, dash_len, gap_len); m_renderer->add_rectangle(bar_start - Vector2{1.f, 0.f}, bar_start + Vector2{width, -height}, outline_color); m_text_cursor_left.x -= offset + width; @@ -262,34 +208,36 @@ namespace omath::hud return *this; } - EntityOverlay& EntityOverlay::add_top_dashed_bar(const Color& color, const Color& outline_color, + EntityOverlay& EntityOverlay::add_top_dashed_bar(const widget::BarPaint& color, const Color& outline_color, const Color& bg_color, const float height, float ratio, const float dash_len, const float gap_len, const float offset) { ratio = std::clamp(ratio, 0.f, 1.f); const float bar_w = std::abs(m_canvas.top_left_corner.x - m_canvas.top_right_corner.x); const auto bar_start = Vector2{m_canvas.top_left_corner.x, m_text_cursor_top.y - offset}; + const auto fill_far = bar_start + Vector2{bar_w * ratio, -height}; m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2{bar_w, -height}, bg_color); - draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, -height}, bar_w, bar_w * ratio, color, outline_color, dash_len, - gap_len); + draw_filled_rectangle(*m_renderer, bar_start, fill_far, resolve_bar_paint(color, ratio, false)); + draw_dash_gaps(*m_renderer, bar_start, {1.f, 0.f}, {0.f, -height}, bar_w, outline_color, dash_len, gap_len); m_renderer->add_rectangle(bar_start, bar_start + Vector2{bar_w, -height}, outline_color); m_text_cursor_top.y -= offset + height; return *this; } - EntityOverlay& EntityOverlay::add_bottom_dashed_bar(const Color& color, const Color& outline_color, + EntityOverlay& EntityOverlay::add_bottom_dashed_bar(const widget::BarPaint& color, const Color& outline_color, const Color& bg_color, const float height, float ratio, const float dash_len, const float gap_len, const float offset) { ratio = std::clamp(ratio, 0.f, 1.f); const float bar_w = std::abs(m_canvas.bottom_left_corner.x - m_canvas.bottom_right_corner.x); const auto bar_start = Vector2{m_canvas.bottom_left_corner.x, m_text_cursor_bottom.y + offset}; + const auto fill_far = bar_start + Vector2{bar_w * ratio, height}; m_renderer->add_filled_rectangle(bar_start, bar_start + Vector2{bar_w, height}, bg_color); - draw_dashed_fill(bar_start, {1.f, 0.f}, {0.f, height}, bar_w, bar_w * ratio, color, outline_color, dash_len, - gap_len); + draw_filled_rectangle(*m_renderer, bar_start, fill_far, resolve_bar_paint(color, ratio, false)); + draw_dash_gaps(*m_renderer, bar_start, {1.f, 0.f}, {0.f, height}, bar_w, outline_color, dash_len, gap_len); m_renderer->add_rectangle(bar_start, bar_start + Vector2{bar_w, height}, outline_color); m_text_cursor_bottom.y += offset + height; @@ -395,20 +343,6 @@ namespace omath::hud return *this; } - void EntityOverlay::draw_glow_polyline(const std::span>& points, const widget::Glow& glow, - const float thickness) const - { - const int radius = static_cast(std::ceil(std::max(glow.radius, 0.f))); - const auto value = glow.color.value(); - for (int layer = radius; layer > 0; --layer) - { - const float falloff = 1.f - static_cast(layer - 1) / static_cast(radius + 1); - const Color color{value.x, value.y, value.z, - value.w * std::max(glow.intensity, 0.f) * falloff / static_cast(radius)}; - m_renderer->add_polyline(points, color, thickness + static_cast(layer) * 2.f); - } - } - void EntityOverlay::draw_glow_line(const Vector2& from, const Vector2& to, const widget::Glow& glow, const float thickness) const { @@ -423,115 +357,6 @@ namespace omath::hud } } - void EntityOverlay::draw_glow_rectangle(const Vector2& min, const Vector2& max, - const std::optional& glow) const - { - if (!glow || glow->radius <= 0.f) - return; - - const std::array points = { - Vector2{std::min(min.x, max.x), std::min(min.y, max.y)}, - Vector2{std::max(min.x, max.x), std::min(min.y, max.y)}, - Vector2{std::max(min.x, max.x), std::max(min.y, max.y)}, - Vector2{std::min(min.x, max.x), std::max(min.y, max.y)}, - }; - draw_glow_polyline(points, *glow, 1.f); - } - - void EntityOverlay::draw_canvas_glow(const widget::CanvasGlow& canvas_glow) const - { - const int layers = std::max(canvas_glow.layers, 2); - const auto min = m_canvas.top_left_corner; - const auto max = m_canvas.bottom_right_corner; - const auto value = canvas_glow.glow.color.value(); - const float intensity = std::max(canvas_glow.glow.intensity, 0.f); - - constexpr int corner_segments = 12; - const float max_rounding = std::min(std::abs(max.x - min.x), std::abs(max.y - min.y)) * 0.5f; - const float rounding = std::clamp(canvas_glow.rounding, 0.f, max_rounding); - const float extent = std::max(canvas_glow.glow.radius, 0.f); - const float stroke = std::max(extent / static_cast(layers - 1) * 2.f, 1.f); - for (int layer = 0; layer < layers; ++layer) - { - const float progress = static_cast(layer) / static_cast(layers - 1); - const float expansion = extent * (1.f - progress); - const float falloff = progress * progress * (3.f - 2.f * progress); - const Color color{value.x, value.y, value.z, value.w * intensity * falloff * 0.35f}; - const auto expanded_min = min - Vector2{expansion, expansion}; - const auto expanded_max = max + Vector2{expansion, expansion}; - const float expanded_rounding = rounding + expansion; - const std::array corner_centers = { - expanded_min + Vector2{expanded_rounding, expanded_rounding}, - Vector2{expanded_max.x - expanded_rounding, expanded_min.y + expanded_rounding}, - expanded_max - Vector2{expanded_rounding, expanded_rounding}, - Vector2{expanded_min.x + expanded_rounding, expanded_max.y - expanded_rounding}, - }; - std::array, corner_segments * 4> points; - for (int corner = 0; corner < 4; ++corner) - { - const float start_angle = - std::numbers::pi_v + static_cast(corner) * std::numbers::pi_v * 0.5f; - for (int segment = 0; segment < corner_segments; ++segment) - { - const float arc_progress = static_cast(segment) / static_cast(corner_segments - 1); - const float angle = start_angle + arc_progress * std::numbers::pi_v * 0.5f; - points[corner * corner_segments + segment] = - corner_centers[corner] - + Vector2{std::cos(angle) * expanded_rounding, std::sin(angle) * expanded_rounding}; - } - } - constexpr float clip_extent = std::numeric_limits::max() * 0.25f; - m_renderer->add_polyline_clipped(points, {-clip_extent, -clip_extent}, {clip_extent, min.y}, color, stroke); - m_renderer->add_polyline_clipped(points, {-clip_extent, max.y}, {clip_extent, clip_extent}, color, stroke); - m_renderer->add_polyline_clipped(points, {-clip_extent, min.y}, {min.x, max.y}, color, stroke); - m_renderer->add_polyline_clipped(points, {max.x, min.y}, {clip_extent, max.y}, color, stroke); - } - } - - void EntityOverlay::draw_filled_rectangle(const Vector2& min, const Vector2& max, - const widget::Paint& paint) const - { - const Vector2 top_left{std::min(min.x, max.x), std::min(min.y, max.y)}; - const Vector2 bottom_right{std::max(min.x, max.x), std::max(min.y, max.y)}; - std::visit( - widget::Overloaded{ - [&](const Color& color) - { - m_renderer->add_filled_rectangle(top_left, bottom_right, color); - }, - [&](const Gradient& gradient) - { - m_renderer->add_gradient_rectangle(top_left, bottom_right, gradient); - }, - }, - paint); - } - - widget::Paint EntityOverlay::resolve_bar_paint(const widget::BarPaint& paint, const float ratio, - const bool vertical) - { - return std::visit( - widget::Overloaded{ - [](const Color& color) -> widget::Paint - { - return color; - }, - [](const Gradient& gradient) -> widget::Paint - { - return gradient; - }, - [&](const widget::BarGradient& gradient) -> widget::Paint - { - const float value = std::clamp(ratio, 0.f, 1.f); - const auto current = Color{gradient.empty_color.value() * (1.f - value) - + gradient.full_color.value() * value}; - if (vertical) - return Gradient{current, current, gradient.empty_color, gradient.empty_color}; - return Gradient{gradient.empty_color, current, current, gradient.empty_color}; - }, - }, - paint); - } EntityOverlay& EntityOverlay::add_bottom_bar(const widget::BarPaint& color, const Color& outline_color, const Color& bg_color, const float height, float ratio, const float offset, const std::optional& glow) @@ -544,8 +369,8 @@ namespace omath::hud const auto bar_max = bar_start + Vector2{max_bar_width, height}; const auto fill_max = bar_start + Vector2{max_bar_width * ratio, height}; m_renderer->add_filled_rectangle(bar_min, bar_max, bg_color); - draw_glow_rectangle(bar_min, fill_max, glow); - draw_filled_rectangle(bar_min, fill_max, resolve_bar_paint(color, ratio, false)); + draw_glow_rectangle(*m_renderer, bar_min, fill_max, glow); + draw_filled_rectangle(*m_renderer, bar_min, fill_max, resolve_bar_paint(color, ratio, false)); m_renderer->add_rectangle(bar_min, bar_max, outline_color); m_text_cursor_bottom.y += offset + height; @@ -757,7 +582,7 @@ namespace omath::hud if (box.glow) { const auto points = m_canvas.as_array(); - draw_glow_polyline(points, *box.glow, box.thickness); + draw_glow_polyline(*m_renderer, points, *box.glow, box.thickness); } std::visit( [&](const auto& fill) @@ -769,7 +594,7 @@ namespace omath::hud void EntityOverlay::dispatch(const widget::CanvasGlow& canvas_glow) { - draw_canvas_glow(canvas_glow); + draw_edge_glow(*m_renderer, m_canvas.top_left_corner, m_canvas.bottom_right_corner, canvas_glow); } void EntityOverlay::dispatch(const widget::CorneredBox& cornered_box) diff --git a/source/hud/screen_overlay.cpp b/source/hud/screen_overlay.cpp index 8b08cfdf..7815da5a 100644 --- a/source/hud/screen_overlay.cpp +++ b/source/hud/screen_overlay.cpp @@ -105,6 +105,65 @@ namespace omath::hud return *this; } + ScreenOverlay& ScreenOverlay::add_center_bar(const widget::CenterBar& center_bar) + { + const auto ratio = std::clamp(center_bar.ratio, 0.f, 1.f); + const auto half_length = center_bar.length / 2.f; + + Vector2 bar_min; + Vector2 bar_max; + if (center_bar.side == widget::CenterBar::Side::RIGHT) + { + bar_min = {m_center.x + center_bar.offset, m_center.y - half_length}; + bar_max = {m_center.x + center_bar.offset + center_bar.thickness, m_center.y + half_length}; + } + else + { + bar_min = {m_center.x - center_bar.offset - center_bar.thickness, m_center.y - half_length}; + bar_max = {m_center.x - center_bar.offset, m_center.y + half_length}; + } + + const auto fill_min = Vector2{bar_min.x, bar_max.y - center_bar.length * ratio}; + + m_renderer->add_filled_rectangle(bar_min, bar_max, center_bar.bg); + if (center_bar.glow) + draw_edge_glow(*m_renderer, fill_min, bar_max, *center_bar.glow); + draw_filled_rectangle(*m_renderer, fill_min, bar_max, resolve_bar_paint(center_bar.color, ratio, true)); + m_renderer->add_rectangle(bar_min, bar_max, center_bar.outline); + + return *this; + } + + ScreenOverlay& ScreenOverlay::add_dashed_center_bar(const widget::DashedCenterBar& dashed_center_bar) + { + const auto ratio = std::clamp(dashed_center_bar.ratio, 0.f, 1.f); + const auto half_length = dashed_center_bar.length / 2.f; + + Vector2 bar_min; + Vector2 bar_max; + if (dashed_center_bar.side == widget::CenterBar::Side::RIGHT) + { + bar_min = {m_center.x + dashed_center_bar.offset, m_center.y - half_length}; + bar_max = {m_center.x + dashed_center_bar.offset + dashed_center_bar.thickness, m_center.y + half_length}; + } + else + { + bar_min = {m_center.x - dashed_center_bar.offset - dashed_center_bar.thickness, m_center.y - half_length}; + bar_max = {m_center.x - dashed_center_bar.offset, m_center.y + half_length}; + } + + const auto fill_min = Vector2{bar_min.x, bar_max.y - dashed_center_bar.length * ratio}; + + m_renderer->add_filled_rectangle(bar_min, bar_max, dashed_center_bar.bg); + draw_filled_rectangle(*m_renderer, fill_min, bar_max, resolve_bar_paint(dashed_center_bar.color, ratio, true)); + draw_dash_gaps(*m_renderer, {bar_min.x, bar_max.y}, {0.f, -1.f}, {dashed_center_bar.thickness, 0.f}, + dashed_center_bar.length, dashed_center_bar.outline, dashed_center_bar.dash_len, + dashed_center_bar.gap_len); + m_renderer->add_rectangle(bar_min, bar_max, dashed_center_bar.outline); + + return *this; + } + ScreenOverlay& ScreenOverlay::add_corner(const widget::Corner& corner) { using widget::Anchor; @@ -177,6 +236,16 @@ namespace omath::hud add_hit_marker(hit_marker); } + void ScreenOverlay::dispatch(const widget::CenterBar& center_bar) + { + add_center_bar(center_bar); + } + + void ScreenOverlay::dispatch(const widget::DashedCenterBar& dashed_center_bar) + { + add_dashed_center_bar(dashed_center_bar); + } + void ScreenOverlay::dispatch(const widget::Corner& corner) { add_corner(corner); diff --git a/source/hud/widget_render.cpp b/source/hud/widget_render.cpp index 0075b729..05d65224 100644 --- a/source/hud/widget_render.cpp +++ b/source/hud/widget_render.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace omath::hud { @@ -61,4 +63,175 @@ namespace omath::hud }, paint); } + + void draw_glow_polyline(HudRendererInterface& renderer, const std::span> points, + const widget::Glow& glow, const float thickness) + { + const int radius = static_cast(std::ceil(std::max(glow.radius, 0.f))); + const auto value = glow.color.value(); + for (int layer = radius; layer > 0; --layer) + { + const float falloff = 1.f - static_cast(layer - 1) / static_cast(radius + 1); + const Color color{value.x, value.y, value.z, + value.w * std::max(glow.intensity, 0.f) * falloff / static_cast(radius)}; + renderer.add_polyline(points, color, thickness + static_cast(layer) * 2.f); + } + } + + void draw_glow_rectangle(HudRendererInterface& renderer, const Vector2& min, const Vector2& max, + const std::optional& glow) + { + if (!glow || glow->radius <= 0.f) + return; + + const std::array points = { + Vector2{std::min(min.x, max.x), std::min(min.y, max.y)}, + Vector2{std::max(min.x, max.x), std::min(min.y, max.y)}, + Vector2{std::max(min.x, max.x), std::max(min.y, max.y)}, + Vector2{std::min(min.x, max.x), std::max(min.y, max.y)}, + }; + draw_glow_polyline(renderer, points, *glow, 1.f); + } + + void draw_filled_rectangle(HudRendererInterface& renderer, const Vector2& min, const Vector2& max, + const widget::Paint& paint) + { + const Vector2 top_left{std::min(min.x, max.x), std::min(min.y, max.y)}; + const Vector2 bottom_right{std::max(min.x, max.x), std::max(min.y, max.y)}; + std::visit( + widget::Overloaded{ + [&](const Color& color) + { + renderer.add_filled_rectangle(top_left, bottom_right, color); + }, + [&](const Gradient& gradient) + { + renderer.add_gradient_rectangle(top_left, bottom_right, gradient); + }, + }, + paint); + } + + widget::Paint resolve_bar_paint(const widget::BarPaint& paint, const float ratio, const bool vertical) + { + return std::visit( + widget::Overloaded{ + [](const Color& color) -> widget::Paint + { + return color; + }, + [](const Gradient& gradient) -> widget::Paint + { + return gradient; + }, + [&](const widget::BarGradient& gradient) -> widget::Paint + { + const float value = std::clamp(ratio, 0.f, 1.f); + const auto current = Color{gradient.empty_color.value() * (1.f - value) + + gradient.full_color.value() * value}; + if (vertical) + return Gradient{current, current, gradient.empty_color, gradient.empty_color}; + return Gradient{gradient.empty_color, current, current, gradient.empty_color}; + }, + }, + paint); + } + + void draw_edge_glow(HudRendererInterface& renderer, const Vector2& min, const Vector2& max, + const widget::CanvasGlow& canvas_glow) + { + if (canvas_glow.glow.radius <= 0.f) + return; + + const int layers = std::max(canvas_glow.layers, 2); + const auto value = canvas_glow.glow.color.value(); + const float intensity = std::max(canvas_glow.glow.intensity, 0.f); + + constexpr int corner_segments = 12; + const float max_rounding = std::min(std::abs(max.x - min.x), std::abs(max.y - min.y)) * 0.5f; + const float rounding = std::clamp(canvas_glow.rounding, 0.f, max_rounding); + const float extent = canvas_glow.glow.radius; + const float stroke = std::max(extent / static_cast(layers - 1) * 2.f, 1.f); + for (int layer = 0; layer < layers; ++layer) + { + const float progress = static_cast(layer) / static_cast(layers - 1); + const float expansion = extent * (1.f - progress); + const float falloff = progress * progress * (3.f - 2.f * progress); + const Color color{value.x, value.y, value.z, value.w * intensity * falloff * 0.35f}; + const auto expanded_min = min - Vector2{expansion, expansion}; + const auto expanded_max = max + Vector2{expansion, expansion}; + const float expanded_rounding = rounding + expansion; + const std::array corner_centers = { + expanded_min + Vector2{expanded_rounding, expanded_rounding}, + Vector2{expanded_max.x - expanded_rounding, expanded_min.y + expanded_rounding}, + expanded_max - Vector2{expanded_rounding, expanded_rounding}, + Vector2{expanded_min.x + expanded_rounding, expanded_max.y - expanded_rounding}, + }; + std::array, corner_segments * 4> points; + for (int corner = 0; corner < 4; ++corner) + { + const float start_angle = + std::numbers::pi_v + static_cast(corner) * std::numbers::pi_v * 0.5f; + for (int segment = 0; segment < corner_segments; ++segment) + { + const float arc_progress = static_cast(segment) / static_cast(corner_segments - 1); + const float angle = start_angle + arc_progress * std::numbers::pi_v * 0.5f; + points[corner * corner_segments + segment] = + corner_centers[corner] + + Vector2{std::cos(angle) * expanded_rounding, std::sin(angle) * expanded_rounding}; + } + } + constexpr float clip_extent = std::numeric_limits::max() * 0.25f; + renderer.add_polyline_clipped(points, {-clip_extent, -clip_extent}, {clip_extent, min.y}, color, stroke); + renderer.add_polyline_clipped(points, {-clip_extent, max.y}, {clip_extent, clip_extent}, color, stroke); + renderer.add_polyline_clipped(points, {-clip_extent, min.y}, {min.x, max.y}, color, stroke); + renderer.add_polyline_clipped(points, {max.x, min.y}, {clip_extent, max.y}, color, stroke); + } + } + + void draw_dash_gaps(HudRendererInterface& renderer, const Vector2& origin, const Vector2& step_dir, + const Vector2& perp_dir, const float full_len, const Color& gap_color, + const float dash_len, const float gap_len) + { + if (full_len <= 0.f) + return; + + const float step = dash_len + gap_len; + const float n = std::floor((full_len + gap_len) / step); + if (n < 1.f) + return; + + const float used = n * dash_len + (n - 1.f) * gap_len; + const float offset = (full_len - used) / 2.f; + + const auto fill_rect = [&](const Vector2& a, const Vector2& b) + { + renderer.add_filled_rectangle({std::min(a.x, b.x), std::min(a.y, b.y)}, + {std::max(a.x, b.x), std::max(a.y, b.y)}, gap_color); + }; + + // Leading gap + if (offset > 0.f) + fill_rect(origin, origin + step_dir * offset + perp_dir); + + for (float i = 0.f; i < n; ++i) + { + const float dash_start = offset + i * step; + const float gap_start = dash_start + dash_len; + const float gap_end = dash_start + step; + + // Gap — always drawn across the full bar, regardless of fill amount + if (i < n - 1.f && gap_start < full_len) + { + const auto a = origin + step_dir * gap_start; + const auto b = origin + step_dir * std::min(gap_end, full_len) + perp_dir; + fill_rect(a, b); + } + } + + // Trailing gap + const float trail_start = offset + n * dash_len + (n - 1.f) * gap_len; + if (trail_start < full_len) + fill_rect(origin + step_dir * trail_start, origin + step_dir * full_len + perp_dir); + } } // namespace omath::hud