From f0b3a1fbc405e274d8445d6c2282cb4ce121d772 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 22 May 2026 21:35:01 +0200 Subject: [PATCH] feat(render): halo neon proporcional al bounding_radius de la shape (opt-out a text) --- source/core/defaults/effects.hpp | 26 +++ source/core/graphics/shape.cpp | 261 ++++++++++++----------- source/core/graphics/shape.hpp | 41 ++-- source/core/graphics/vector_text.cpp | 3 +- source/core/rendering/line_renderer.cpp | 5 +- source/core/rendering/line_renderer.hpp | 7 +- source/core/rendering/shape_renderer.cpp | 108 +++++++--- source/core/rendering/shape_renderer.hpp | 10 +- 8 files changed, 289 insertions(+), 172 deletions(-) diff --git a/source/core/defaults/effects.hpp b/source/core/defaults/effects.hpp index 9ee0896..2eda88c 100644 --- a/source/core/defaults/effects.hpp +++ b/source/core/defaults/effects.hpp @@ -5,6 +5,32 @@ #include +namespace Defaults::FX::Glow { + + // Neon glow per outline gruixut, aplicat automàticament per renderShape. + // Els gruixos d'halo són RÀTIOS del bounding_radius de la shape (escalat + // per scale), de manera que un pentàgon (radi 20) té halo gros i una bala + // (radi 3) té halo subtil. El core (últim pass) usa el gruix de línia + // global (1.5px) — no escala amb la shape. + // + // Cap superior: si la shape és molt gran (logos del títol, intro), el + // bounding_radius es satura a aquest valor — així cap shape té més + // glow que el pentàgon (referència de gameplay). + constexpr float MAX_REFERENCE_RADIUS = 20.0F; + + struct Pass { + float thickness_ratio; // % del bounding_radius*scale. <0 → usa core (gruix global) + float alpha; + }; + constexpr Pass PASSES[] = { + {.thickness_ratio = 0.55F, .alpha = 0.07F}, + {.thickness_ratio = 0.35F, .alpha = 0.14F}, + {.thickness_ratio = 0.20F, .alpha = 0.28F}, + {.thickness_ratio = -1.0F, .alpha = 1.0F}, // core: línia "real" + }; + +} // namespace Defaults::FX::Glow + namespace Defaults::FX::Firework { // Color per defecte. La caller pot fer override (p.ex. heretar del pare), diff --git a/source/core/graphics/shape.cpp b/source/core/graphics/shape.cpp index 6ef9362..da93a4b 100644 --- a/source/core/graphics/shape.cpp +++ b/source/core/graphics/shape.cpp @@ -4,156 +4,171 @@ #include "core/graphics/shape.hpp" #include +#include #include #include #include namespace Graphics { -Shape::Shape(const std::string& filepath) - : center_({.x = 0.0F, .y = 0.0F}), - - nom_("unnamed") { - load(filepath); -} + Shape::Shape(const std::string& filepath) + : center_({.x = 0.0F, .y = 0.0F}), -auto Shape::load(const std::string& filepath) -> bool { - // Llegir file - std::ifstream file(filepath); - if (!file.is_open()) { - std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n'; - return false; + nom_("unnamed") { + load(filepath); } - // Llegir todo el contingut - std::stringstream buffer; - buffer << file.rdbuf(); - std::string contingut = buffer.str(); - file.close(); - - // Parsejar - return parseFile(contingut); -} - -auto Shape::parseFile(const std::string& contingut) -> bool { - std::istringstream iss(contingut); - std::string line; - - while (std::getline(iss, line)) { - // Trim whitespace - line = trim(line); - - // Skip comments and blanks - if (line.empty() || line[0] == '#') { - continue; + auto Shape::load(const std::string& filepath) -> bool { + // Llegir file + std::ifstream file(filepath); + if (!file.is_open()) { + std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n'; + return false; } - // Parse command - if (startsWith(line, "name:")) { - nom_ = trim(extractValue(line)); - } else if (startsWith(line, "scale:")) { - try { - escala_defecte_ = std::stof(extractValue(line)); - } catch (...) { - std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n'; - escala_defecte_ = 1.0F; + // Llegir todo el contingut + std::stringstream buffer; + buffer << file.rdbuf(); + std::string contingut = buffer.str(); + file.close(); + + // Parsejar + return parseFile(contingut); + } + + auto Shape::parseFile(const std::string& contingut) -> bool { + std::istringstream iss(contingut); + std::string line; + + while (std::getline(iss, line)) { + // Trim whitespace + line = trim(line); + + // Skip comments and blanks + if (line.empty() || line[0] == '#') { + continue; } - } else if (startsWith(line, "center:")) { - parseCenter(extractValue(line)); - } else if (startsWith(line, "polyline:")) { - auto points = parsePoints(extractValue(line)); - if (points.size() >= 2) { - primitives_.push_back({PrimitiveType::POLYLINE, points}); - } else { - std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada" - << '\n'; + + // Parse command + if (startsWith(line, "name:")) { + nom_ = trim(extractValue(line)); + } else if (startsWith(line, "scale:")) { + try { + escala_defecte_ = std::stof(extractValue(line)); + } catch (...) { + std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n'; + escala_defecte_ = 1.0F; + } + } else if (startsWith(line, "center:")) { + parseCenter(extractValue(line)); + } else if (startsWith(line, "polyline:")) { + auto points = parsePoints(extractValue(line)); + if (points.size() >= 2) { + primitives_.push_back({PrimitiveType::POLYLINE, points}); + } else { + std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada" + << '\n'; + } + } else if (startsWith(line, "line:")) { + auto points = parsePoints(extractValue(line)); + if (points.size() == 2) { + primitives_.push_back({PrimitiveType::LINE, points}); + } else { + std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points" + << '\n'; + } } - } else if (startsWith(line, "line:")) { - auto points = parsePoints(extractValue(line)); - if (points.size() == 2) { - primitives_.push_back({PrimitiveType::LINE, points}); - } else { - std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points" - << '\n'; + // Comandes desconegudes ignorades silenciosament + } + + if (primitives_.empty()) { + std::cerr << "[Shape] Error: sin primitiva carregada" << '\n'; + return false; + } + + bounding_radius_ = computeBoundingRadius(primitives_, center_); + return true; + } + + auto Shape::computeBoundingRadius(const std::vector& primitives, + const Vec2& center) -> float { + float max_dist_sq = 0.0F; + for (const auto& prim : primitives) { + for (const auto& p : prim.points) { + const float DX = p.x - center.x; + const float DY = p.y - center.y; + max_dist_sq = std::max(max_dist_sq, (DX * DX) + (DY * DY)); } } - // Comandes desconegudes ignorades silenciosament + return std::sqrt(max_dist_sq); } - if (primitives_.empty()) { - std::cerr << "[Shape] Error: sin primitiva carregada" << '\n'; - return false; - } - - return true; -} - -// Helper: trim whitespace -auto Shape::trim(const std::string& str) -> std::string { - const char* whitespace = " \t\n\r"; - size_t start = str.find_first_not_of(whitespace); - if (start == std::string::npos) { - return ""; - } - - size_t end = str.find_last_not_of(whitespace); - return str.substr(start, end - start + 1); -} - -// Helper: startsWith -auto Shape::startsWith(const std::string& str, - const std::string& prefix) -> bool { - if (str.length() < prefix.length()) { - return false; - } - return str.starts_with(prefix); -} - -// Helper: extract value after ':' -auto Shape::extractValue(const std::string& line) -> std::string { - size_t colon = line.find(':'); - if (colon == std::string::npos) { - return ""; - } - return line.substr(colon + 1); -} - -// Helper: parse center "x, y" -void Shape::parseCenter(const std::string& value) { - std::string val = trim(value); - size_t comma = val.find(','); - if (comma != std::string::npos) { - try { - center_.x = std::stof(trim(val.substr(0, comma))); - center_.y = std::stof(trim(val.substr(comma + 1))); - } catch (...) { - std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n'; - center_ = {.x = 0.0F, .y = 0.0F}; + // Helper: trim whitespace + auto Shape::trim(const std::string& str) -> std::string { + const char* whitespace = " \t\n\r"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) { + return ""; } + + size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); } -} -// Helper: parse points "x1,y1 x2,y2 x3,y3" -auto Shape::parsePoints(const std::string& str) -> std::vector { - std::vector points; - std::istringstream iss(trim(str)); - std::string pair; + // Helper: startsWith + auto Shape::startsWith(const std::string& str, + const std::string& prefix) -> bool { + if (str.length() < prefix.length()) { + return false; + } + return str.starts_with(prefix); + } - while (iss >> pair) { // Whitespace-separated - size_t comma = pair.find(','); + // Helper: extract value after ':' + auto Shape::extractValue(const std::string& line) -> std::string { + size_t colon = line.find(':'); + if (colon == std::string::npos) { + return ""; + } + return line.substr(colon + 1); + } + + // Helper: parse center "x, y" + void Shape::parseCenter(const std::string& value) { + std::string val = trim(value); + size_t comma = val.find(','); if (comma != std::string::npos) { try { - float x = std::stof(pair.substr(0, comma)); - float y = std::stof(pair.substr(comma + 1)); - points.push_back({x, y}); + center_.x = std::stof(trim(val.substr(0, comma))); + center_.y = std::stof(trim(val.substr(comma + 1))); } catch (...) { - std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair - << '\n'; + std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n'; + center_ = {.x = 0.0F, .y = 0.0F}; } } } - return points; -} + // Helper: parse points "x1,y1 x2,y2 x3,y3" + auto Shape::parsePoints(const std::string& str) -> std::vector { + std::vector points; + std::istringstream iss(trim(str)); + std::string pair; + + while (iss >> pair) { // Whitespace-separated + size_t comma = pair.find(','); + if (comma != std::string::npos) { + try { + float x = std::stof(pair.substr(0, comma)); + float y = std::stof(pair.substr(comma + 1)); + points.push_back({x, y}); + } catch (...) { + std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair + << '\n'; + } + } + } + + return points; + } } // namespace Graphics diff --git a/source/core/graphics/shape.hpp b/source/core/graphics/shape.hpp index e8fb2a6..fc53eb3 100644 --- a/source/core/graphics/shape.hpp +++ b/source/core/graphics/shape.hpp @@ -11,21 +11,21 @@ namespace Graphics { -// Tipo de primitiva dins de una shape -enum class PrimitiveType : std::uint8_t { - POLYLINE, // Secuencia de points connectats - LINE // Línia individual (2 points) -}; + // Tipo de primitiva dins de una shape + enum class PrimitiveType : std::uint8_t { + POLYLINE, // Secuencia de points connectats + LINE // Línia individual (2 points) + }; -// Primitiva individual (polyline o line) -struct ShapePrimitive { + // Primitiva individual (polyline o line) + struct ShapePrimitive { PrimitiveType type; std::vector points; // 2+ points per polyline, exactament 2 per line -}; + }; -// Clase Shape - representa una shape vectorial carregada desde .shp -class Shape { - public: + // Clase Shape - representa una shape vectorial carregada desde .shp + class Shape { + public: // Constructors Shape() = default; explicit Shape(const std::string& filepath); @@ -42,18 +42,22 @@ class Shape { } [[nodiscard]] auto getCenter() const -> const Vec2& { return center_; } [[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; } + // Distància màx. del center_ al vèrtex més llunyà; ús: dimensionar + // efectes proporcionals a la mida de la shape (halos, glow). + [[nodiscard]] auto getBoundingRadius() const -> float { return bounding_radius_; } [[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); } // Info de depuració [[nodiscard]] auto getName() const -> const std::string& { return nom_; } [[nodiscard]] auto getNumPrimitives() const -> size_t { return primitives_.size(); } - private: + private: std::vector primitives_; - Vec2 center_; // Centro/origin de la shape - float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para - // que el ctor por defecto no deje el campo indeterminado. - std::string nom_; // Nom de la shape (per depuració) + Vec2 center_; // Centro/origin de la shape + float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para + // que el ctor por defecto no deje el campo indeterminado. + float bounding_radius_{0.0F}; // Distància màx. del center_ al vèrtex més llunyà. + std::string nom_; // Nom de la shape (per depuració) // Helpers privats per parsejar. Son estáticos: no necesitan estado // de instancia, trabajan sobre el string pasado por parámetro. @@ -62,6 +66,9 @@ class Shape { [[nodiscard]] static auto extractValue(const std::string& line) -> std::string; void parseCenter(const std::string& value); [[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector; -}; + [[nodiscard]] static auto computeBoundingRadius( + const std::vector& primitives, + const Vec2& center) -> float; + }; } // namespace Graphics diff --git a/source/core/graphics/vector_text.cpp b/source/core/graphics/vector_text.cpp index a6f1d9c..c89c383 100644 --- a/source/core/graphics/vector_text.cpp +++ b/source/core/graphics/vector_text.cpp @@ -221,7 +221,8 @@ namespace Graphics { // Ajustar X e Y para que position represente esquina superior izquierda // (render_shape espera el centro, así que sumamos la mitad de ancho y altura) Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)}; - Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color); + // Text opt-out del glow: HUD/marker s'ha de mantenir net. + Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color, 0.0F, 1.0F, /*glow=*/false); // Avanzar posición current_x += CHAR_WIDTH_SCALED + SPACING_SCALED; diff --git a/source/core/rendering/line_renderer.cpp b/source/core/rendering/line_renderer.cpp index 9707d11..94c98d7 100644 --- a/source/core/rendering/line_renderer.cpp +++ b/source/core/rendering/line_renderer.cpp @@ -22,7 +22,8 @@ namespace Rendering { int y2, float brightness, float thickness, - SDL_Color color) { + SDL_Color color, + float alpha) { if (renderer == nullptr) { return; } @@ -42,7 +43,7 @@ namespace Rendering { const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness; - renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F); + renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, alpha); } void setLineColor(SDL_Color color) { g_current_line_color = color; } diff --git a/source/core/rendering/line_renderer.hpp b/source/core/rendering/line_renderer.hpp index ce5fa1f..0528ca0 100644 --- a/source/core/rendering/line_renderer.hpp +++ b/source/core/rendering/line_renderer.hpp @@ -17,9 +17,13 @@ namespace Rendering { // Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720). // brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo). + // Pre-multiplica el RGB del color (color dim sobre fons negre). // thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness. // color: si alpha==0, se usa el color global del oscilador; si alpha>0 se // usa este color directo (paleta semántica por entidad). + // alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original). + // Valors <1.0 fan que la línia es barregi de veritat sobre el dest + // en comptes de sobrepintar-lo (útil per halos translúcids). void linea(Renderer* renderer, int x1, int y1, @@ -27,7 +31,8 @@ namespace Rendering { int y2, float brightness = 1.0F, float thickness = 0.0F, - SDL_Color color = {0, 0, 0, 0}); + SDL_Color color = {0, 0, 0, 0}, + float alpha = 1.0F); // Color global de las líneas (lo actualiza ColorOscillator vía SDLManager). void setLineColor(SDL_Color color); diff --git a/source/core/rendering/shape_renderer.cpp b/source/core/rendering/shape_renderer.cpp index e18ff64..9cead5f 100644 --- a/source/core/rendering/shape_renderer.cpp +++ b/source/core/rendering/shape_renderer.cpp @@ -3,31 +3,77 @@ #include "core/rendering/shape_renderer.hpp" +#include #include +#include "core/defaults/effects.hpp" +#include "core/graphics/shape.hpp" #include "core/rendering/line_renderer.hpp" namespace Rendering { // Helper: transformar un point con rotación, scale i traslación static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 { - // 1. Centrar el point respecte al centro de la shape - float centered_x = point.x - shape_centre.x; - float centered_y = point.y - shape_centre.y; + const float CENTERED_X = point.x - shape_centre.x; + const float CENTERED_Y = point.y - shape_centre.y; - // 2. Aplicar scale al point - float scaled_x = centered_x * scale; - float scaled_y = centered_y * scale; + const float SCALED_X = CENTERED_X * scale; + const float SCALED_Y = CENTERED_Y * scale; - // 3. Aplicar rotación 2D (Z-axis) - float cos_a = std::cos(angle); - float sin_a = std::sin(angle); + const float COS_A = std::cos(angle); + const float SIN_A = std::sin(angle); - float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); - float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); + const float ROTATED_X = (SCALED_X * COS_A) - (SCALED_Y * SIN_A); + const float ROTATED_Y = (SCALED_X * SIN_A) + (SCALED_Y * COS_A); - // 4. Aplicar traslación a posición mundial - return {.x = rotated_x + position.x, .y = rotated_y + position.y}; + return {.x = ROTATED_X + position.x, .y = ROTATED_Y + position.y}; + } + + // Una passada de renderitzat: itera primitives de la shape i emet línies + // amb el thickness/alpha indicats. Es crida N vegades en glow mode (una + // per pass de halo + core), o 1 vegada quan glow=false. + static void renderSinglePass(Rendering::Renderer* renderer, + const std::shared_ptr& shape, + const Vec2& position, + float angle, + float scale, + float brightness, + SDL_Color color, + float thickness, + float alpha) { + const Vec2& shape_centre = shape->getCenter(); + + // Petita extensió a línies gruixudes per tapar forats entre segments. + // A vèrtex aguts (~108°) un valor alt produeix "espigues" — 15%. + const float EFFECTIVE_T = (thickness > 0.0F) ? thickness : getLineThickness(); + const float EXTEND = (EFFECTIVE_T > 2.0F) ? (EFFECTIVE_T * 0.15F) : 0.0F; + + for (const auto& primitive : shape->getPrimitives()) { + if (primitive.type == Graphics::PrimitiveType::POLYLINE) { + for (size_t i = 0; i < primitive.points.size() - 1; i++) { + Vec2 p1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale); + Vec2 p2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale); + if (EXTEND > 0.0F) { + const float DX = p2.x - p1.x; + const float DY = p2.y - p1.y; + const float LEN = std::sqrt((DX * DX) + (DY * DY)); + if (LEN > 1e-6F) { + const float UX = (DX / LEN) * EXTEND; + const float UY = (DY / LEN) * EXTEND; + p1.x -= UX; + p1.y -= UY; + p2.x += UX; + p2.y += UY; + } + } + linea(renderer, static_cast(p1.x), static_cast(p1.y), static_cast(p2.x), static_cast(p2.y), brightness, thickness, color, alpha); + } + } else if (primitive.points.size() >= 2) { // LINE + const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale); + const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale); + linea(renderer, static_cast(P1.x), static_cast(P1.y), static_cast(P2.x), static_cast(P2.y), brightness, thickness, color, alpha); + } + } } void renderShape(Rendering::Renderer* renderer, @@ -37,7 +83,10 @@ namespace Rendering { float scale, float progress, float brightness, - SDL_Color color) { + SDL_Color color, + float thickness, + float alpha, + bool glow) { if (!shape || !shape->isValid()) { return; } @@ -45,21 +94,26 @@ namespace Rendering { return; } - const Vec2& shape_centre = shape->getCenter(); + if (!glow) { + renderSinglePass(renderer, shape, position, angle, scale, brightness, color, thickness, alpha); + return; + } - for (const auto& primitive : shape->getPrimitives()) { - if (primitive.type == Graphics::PrimitiveType::POLYLINE) { - // POLYLINE: conectar puntos consecutivos. - for (size_t i = 0; i < primitive.points.size() - 1; i++) { - const Vec2 P1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale); - const Vec2 P2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale); - linea(renderer, static_cast(P1.x), static_cast(P1.y), static_cast(P2.x), static_cast(P2.y), brightness, 0.0F, color); - } - } else if (primitive.points.size() >= 2) { // LINE - const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale); - const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale); - linea(renderer, static_cast(P1.x), static_cast(P1.y), static_cast(P2.x), static_cast(P2.y), brightness, 0.0F, color); + // Glow: multi-pass amb halos translúcids proporcionals al tamany de + // la shape. Cada pass amb thickness_ratio<0 usa el thickness/alpha + // que ha passat el caller (és el "core" / línia real). Saturem la + // mida de referència a MAX_REFERENCE_RADIUS perquè shapes molt + // grans (logos) no tinguin halo desproporcionat. + const float RAW_REF = shape->getBoundingRadius() * scale; + const float REFERENCE_SIZE = std::min(RAW_REF, Defaults::FX::Glow::MAX_REFERENCE_RADIUS); + for (const auto& pass : Defaults::FX::Glow::PASSES) { + float pass_thickness = thickness; + float pass_alpha = alpha; + if (pass.thickness_ratio > 0.0F) { + pass_thickness = REFERENCE_SIZE * pass.thickness_ratio; + pass_alpha = pass.alpha * alpha; // respecta el master alpha del caller } + renderSinglePass(renderer, shape, position, angle, scale, brightness, color, pass_thickness, pass_alpha); } } diff --git a/source/core/rendering/shape_renderer.hpp b/source/core/rendering/shape_renderer.hpp index 43eed6b..d65d25f 100644 --- a/source/core/rendering/shape_renderer.hpp +++ b/source/core/rendering/shape_renderer.hpp @@ -21,6 +21,11 @@ namespace Rendering { // - scale: factor de scale (1.0 = mida original) // - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) + // - color: si alpha==0, usa oscil·lador global + // - thickness: gruix de línia. <=0 → usa global (g_current_line_thickness) + // - alpha: alpha que arriba al GPU (default 1.0 = opac). <1.0 = halo real + // - glow: si true, redibuixa la shape amb halos translúcids proporcionals + // al bounding_radius*scale (efecte neon). Si false, single-pass. void renderShape(Rendering::Renderer* renderer, const std::shared_ptr& shape, const Vec2& position, @@ -28,6 +33,9 @@ namespace Rendering { float scale = 1.0F, float progress = 1.0F, float brightness = 1.0F, - SDL_Color color = {0, 0, 0, 0}); // alpha==0 → usa global oscilador + SDL_Color color = {0, 0, 0, 0}, + float thickness = 0.0F, + float alpha = 1.0F, + bool glow = true); } // namespace Rendering