feat(notifier): infrastructura del sistema de notificacions toast
- Notifier singleton (System::init/get/destroy) que dibuixa un cuadre
centrat al centre-superior amb fons semitransparent (derivat oscur del
color del text) i bordes en línies.
- Màquina d'estats HIDDEN → ENTERING → HOLDING → EXITING amb easing
outCubic (entrada) i inCubic (sortida), slide de 300 ms.
- pushRect() afegit a GpuFrameRenderer (2 triangles, edge_dist=0) per
poder pintar el fons opac/semitransparent reutilitzant el pipeline de
línies — sense afegir cap pipeline nou.
- VectorText::render/renderCentered admeten color RGBA explícit
(default {0,0,0,0} preserva el comportament previ amb oscil·lador
global de color).
- Easing header-only a core/utils/easing.hpp (outCubic, inCubic).
- Director crea Notifier just després del DebugOverlay i el draweja com
a última capa per damunt de l'escena i el debug.
Encara cap consumer el crida; els F1-F5 i la doble pulsació d'ESC
arriben en commits posteriors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,274 +10,274 @@
|
|||||||
|
|
||||||
namespace Graphics {
|
namespace Graphics {
|
||||||
|
|
||||||
// Constants para mides base dels caràcters
|
// Constants para mides base dels caràcters
|
||||||
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
|
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
|
||||||
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
|
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
|
||||||
|
|
||||||
VectorText::VectorText(Rendering::Renderer* renderer)
|
VectorText::VectorText(Rendering::Renderer* renderer)
|
||||||
: renderer_(renderer) {
|
: renderer_(renderer) {
|
||||||
loadCharset();
|
loadCharset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VectorText::loadCharset() {
|
void VectorText::loadCharset() {
|
||||||
// Cargar dígitos 0-9
|
// Cargar dígitos 0-9
|
||||||
for (char c = '0'; c <= '9'; c++) {
|
for (char c = '0'; c <= '9'; c++) {
|
||||||
std::string filename = getShapeFilename(c);
|
std::string filename = getShapeFilename(c);
|
||||||
auto shape = ShapeLoader::load(filename);
|
auto shape = ShapeLoader::load(filename);
|
||||||
|
|
||||||
if (shape && shape->isValid()) {
|
if (shape && shape->isValid()) {
|
||||||
chars_[c] = shape;
|
chars_[c] = shape;
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||||
<< '\n';
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar lletres A-Z (majúscules)
|
||||||
|
for (char c = 'A'; c <= 'Z'; c++) {
|
||||||
|
std::string filename = getShapeFilename(c);
|
||||||
|
auto shape = ShapeLoader::load(filename);
|
||||||
|
|
||||||
|
if (shape && shape->isValid()) {
|
||||||
|
chars_[c] = shape;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar símbolos
|
||||||
|
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
||||||
|
for (const auto& sym : SYMBOLS) {
|
||||||
|
char c = sym[0];
|
||||||
|
std::string filename = getShapeFilename(c);
|
||||||
|
auto shape = ShapeLoader::load(filename);
|
||||||
|
|
||||||
|
if (shape && shape->isValid()) {
|
||||||
|
chars_[c] = shape;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar símbolo de copyright (©) - UTF-8 U+00A9.
|
||||||
|
// Usamos el segundo byte (0xA9, 169 decimal) como key interna del map.
|
||||||
|
{
|
||||||
|
const std::string FILENAME = "font/char_copyright.shp";
|
||||||
|
auto shape = ShapeLoader::load(FILENAME);
|
||||||
|
|
||||||
|
if (shape && shape->isValid()) {
|
||||||
|
chars_['\xA9'] = shape;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[VectorText] Warning: no s'ha pogut load " << FILENAME
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
auto VectorText::getShapeFilename(char c) -> std::string {
|
||||||
|
// Mapeo carácter → nombre de archivo (con prefix "font/").
|
||||||
|
// Dígitos 0-9 y mayúsculas A-Z comparten el mismo path: la shape se llama
|
||||||
|
// como el caracter mismo, así que se agrupan en un único case.
|
||||||
|
switch (c) {
|
||||||
|
case '0':
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
case 'A':
|
||||||
|
case 'B':
|
||||||
|
case 'C':
|
||||||
|
case 'D':
|
||||||
|
case 'E':
|
||||||
|
case 'F':
|
||||||
|
case 'G':
|
||||||
|
case 'H':
|
||||||
|
case 'I':
|
||||||
|
case 'J':
|
||||||
|
case 'K':
|
||||||
|
case 'L':
|
||||||
|
case 'M':
|
||||||
|
case 'N':
|
||||||
|
case 'O':
|
||||||
|
case 'P':
|
||||||
|
case 'Q':
|
||||||
|
case 'R':
|
||||||
|
case 'S':
|
||||||
|
case 'T':
|
||||||
|
case 'U':
|
||||||
|
case 'V':
|
||||||
|
case 'W':
|
||||||
|
case 'X':
|
||||||
|
case 'Y':
|
||||||
|
case 'Z':
|
||||||
|
return std::string("font/char_") + c + ".shp";
|
||||||
|
|
||||||
|
// Lletres minúscules a-z (convertir a majúscules)
|
||||||
|
case 'a':
|
||||||
|
case 'b':
|
||||||
|
case 'c':
|
||||||
|
case 'd':
|
||||||
|
case 'e':
|
||||||
|
case 'f':
|
||||||
|
case 'g':
|
||||||
|
case 'h':
|
||||||
|
case 'i':
|
||||||
|
case 'j':
|
||||||
|
case 'k':
|
||||||
|
case 'l':
|
||||||
|
case 'm':
|
||||||
|
case 'n':
|
||||||
|
case 'o':
|
||||||
|
case 'p':
|
||||||
|
case 'q':
|
||||||
|
case 'r':
|
||||||
|
case 's':
|
||||||
|
case 't':
|
||||||
|
case 'u':
|
||||||
|
case 'v':
|
||||||
|
case 'w':
|
||||||
|
case 'x':
|
||||||
|
case 'y':
|
||||||
|
case 'z':
|
||||||
|
return std::string("font/char_") + char(c - 32) + ".shp";
|
||||||
|
|
||||||
|
// Símbols
|
||||||
|
case '.':
|
||||||
|
return "font/char_dot.shp";
|
||||||
|
case ',':
|
||||||
|
return "font/char_comma.shp";
|
||||||
|
case '-':
|
||||||
|
return "font/char_minus.shp";
|
||||||
|
case ':':
|
||||||
|
return "font/char_colon.shp";
|
||||||
|
case '!':
|
||||||
|
return "font/char_exclamation.shp";
|
||||||
|
case '?':
|
||||||
|
return "font/char_question.shp";
|
||||||
|
case ' ':
|
||||||
|
return ""; // Espai es maneja sin load shape
|
||||||
|
|
||||||
|
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
|
||||||
|
return "font/char_copyright.shp";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ""; // Caràcter no suportat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar lletres A-Z (majúscules)
|
auto VectorText::isSupported(char c) const -> bool {
|
||||||
for (char c = 'A'; c <= 'Z'; c++) {
|
return chars_.contains(c);
|
||||||
std::string filename = getShapeFilename(c);
|
}
|
||||||
auto shape = ShapeLoader::load(filename);
|
|
||||||
|
|
||||||
if (shape && shape->isValid()) {
|
void VectorText::render(const std::string& text, const Vec2& position, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||||
chars_[c] = shape;
|
if (renderer_ == nullptr) {
|
||||||
} else {
|
return;
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
}
|
||||||
<< '\n';
|
|
||||||
|
// Ancho de un carácter base (20 px a scale 1.0)
|
||||||
|
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
|
||||||
|
|
||||||
|
// Spacing escalado
|
||||||
|
const float SPACING_SCALED = spacing * scale;
|
||||||
|
|
||||||
|
// Altura de un carácter escalado (necesario para ajustar Y)
|
||||||
|
const float CHAR_HEIGHT_SCALED = BASE_CHAR_HEIGHT * scale;
|
||||||
|
|
||||||
|
// Posición X del borde izquierdo del carácter actual
|
||||||
|
// (se ajustará +BASE_CHAR_WIDTH/2 para obtener el centro al renderizar)
|
||||||
|
float current_x = position.x;
|
||||||
|
|
||||||
|
// Iterar sobre cada byte del string (con detecció UTF-8)
|
||||||
|
for (size_t i = 0; i < text.length(); i++) {
|
||||||
|
auto c = static_cast<unsigned char>(text[i]);
|
||||||
|
|
||||||
|
// Detectar copyright UTF-8 (0xC2 0xA9)
|
||||||
|
if (c == 0xC2 && i + 1 < text.length() &&
|
||||||
|
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
||||||
|
c = 0xA9; // Usar segon byte como a key
|
||||||
|
i++; // Saltar el següent byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejar espacios (avanzar sin dibujar)
|
||||||
|
if (c == ' ') {
|
||||||
|
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si el carácter está soportado
|
||||||
|
auto it = chars_.find(c);
|
||||||
|
if (it != chars_.end()) {
|
||||||
|
// Renderizar carácter
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Avanzar posición
|
||||||
|
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||||
|
} else {
|
||||||
|
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
|
||||||
|
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
|
||||||
|
<< '\n';
|
||||||
|
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar símbolos
|
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const {
|
||||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
// Calcular dimensions del text
|
||||||
for (const auto& sym : SYMBOLS) {
|
float text_width = getTextWidth(text, scale, spacing);
|
||||||
char c = sym[0];
|
float text_height = getTextHeight(scale);
|
||||||
std::string filename = getShapeFilename(c);
|
|
||||||
auto shape = ShapeLoader::load(filename);
|
|
||||||
|
|
||||||
if (shape && shape->isValid()) {
|
// Calcular posición de l'esquina superior izquierda
|
||||||
chars_[c] = shape;
|
// restant la meitat de las dimensions del point central
|
||||||
} else {
|
Vec2 posicio_esquerra = {
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << filename
|
.x = centre_punt.x - (text_width / 2.0F),
|
||||||
<< '\n';
|
.y = centre_punt.y - (text_height / 2.0F)};
|
||||||
}
|
|
||||||
|
// Delegar al método render() existent
|
||||||
|
render(text, posicio_esquerra, scale, spacing, brightness, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar símbolo de copyright (©) - UTF-8 U+00A9.
|
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
|
||||||
// Usamos el segundo byte (0xA9, 169 decimal) como key interna del map.
|
if (text.empty()) {
|
||||||
{
|
return 0.0F;
|
||||||
const std::string FILENAME = "font/char_copyright.shp";
|
|
||||||
auto shape = ShapeLoader::load(FILENAME);
|
|
||||||
|
|
||||||
if (shape && shape->isValid()) {
|
|
||||||
chars_['\xA9'] = shape;
|
|
||||||
} else {
|
|
||||||
std::cerr << "[VectorText] Warning: no s'ha pogut load " << FILENAME
|
|
||||||
<< '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters"
|
|
||||||
<< '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
auto VectorText::getShapeFilename(char c) -> std::string {
|
|
||||||
// Mapeo carácter → nombre de archivo (con prefix "font/").
|
|
||||||
// Dígitos 0-9 y mayúsculas A-Z comparten el mismo path: la shape se llama
|
|
||||||
// como el caracter mismo, así que se agrupan en un único case.
|
|
||||||
switch (c) {
|
|
||||||
case '0':
|
|
||||||
case '1':
|
|
||||||
case '2':
|
|
||||||
case '3':
|
|
||||||
case '4':
|
|
||||||
case '5':
|
|
||||||
case '6':
|
|
||||||
case '7':
|
|
||||||
case '8':
|
|
||||||
case '9':
|
|
||||||
case 'A':
|
|
||||||
case 'B':
|
|
||||||
case 'C':
|
|
||||||
case 'D':
|
|
||||||
case 'E':
|
|
||||||
case 'F':
|
|
||||||
case 'G':
|
|
||||||
case 'H':
|
|
||||||
case 'I':
|
|
||||||
case 'J':
|
|
||||||
case 'K':
|
|
||||||
case 'L':
|
|
||||||
case 'M':
|
|
||||||
case 'N':
|
|
||||||
case 'O':
|
|
||||||
case 'P':
|
|
||||||
case 'Q':
|
|
||||||
case 'R':
|
|
||||||
case 'S':
|
|
||||||
case 'T':
|
|
||||||
case 'U':
|
|
||||||
case 'V':
|
|
||||||
case 'W':
|
|
||||||
case 'X':
|
|
||||||
case 'Y':
|
|
||||||
case 'Z':
|
|
||||||
return std::string("font/char_") + c + ".shp";
|
|
||||||
|
|
||||||
// Lletres minúscules a-z (convertir a majúscules)
|
|
||||||
case 'a':
|
|
||||||
case 'b':
|
|
||||||
case 'c':
|
|
||||||
case 'd':
|
|
||||||
case 'e':
|
|
||||||
case 'f':
|
|
||||||
case 'g':
|
|
||||||
case 'h':
|
|
||||||
case 'i':
|
|
||||||
case 'j':
|
|
||||||
case 'k':
|
|
||||||
case 'l':
|
|
||||||
case 'm':
|
|
||||||
case 'n':
|
|
||||||
case 'o':
|
|
||||||
case 'p':
|
|
||||||
case 'q':
|
|
||||||
case 'r':
|
|
||||||
case 's':
|
|
||||||
case 't':
|
|
||||||
case 'u':
|
|
||||||
case 'v':
|
|
||||||
case 'w':
|
|
||||||
case 'x':
|
|
||||||
case 'y':
|
|
||||||
case 'z':
|
|
||||||
return std::string("font/char_") + char(c - 32) + ".shp";
|
|
||||||
|
|
||||||
// Símbols
|
|
||||||
case '.':
|
|
||||||
return "font/char_dot.shp";
|
|
||||||
case ',':
|
|
||||||
return "font/char_comma.shp";
|
|
||||||
case '-':
|
|
||||||
return "font/char_minus.shp";
|
|
||||||
case ':':
|
|
||||||
return "font/char_colon.shp";
|
|
||||||
case '!':
|
|
||||||
return "font/char_exclamation.shp";
|
|
||||||
case '?':
|
|
||||||
return "font/char_question.shp";
|
|
||||||
case ' ':
|
|
||||||
return ""; // Espai es maneja sin load shape
|
|
||||||
|
|
||||||
case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9
|
|
||||||
return "font/char_copyright.shp";
|
|
||||||
|
|
||||||
default:
|
|
||||||
return ""; // Caràcter no suportat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto VectorText::isSupported(char c) const -> bool {
|
|
||||||
return chars_.contains(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VectorText::render(const std::string& text, const Vec2& position, float scale, float spacing, float brightness) const {
|
|
||||||
if (renderer_ == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancho de un carácter base (20 px a scale 1.0)
|
|
||||||
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
|
|
||||||
|
|
||||||
// Spacing escalado
|
|
||||||
const float SPACING_SCALED = spacing * scale;
|
|
||||||
|
|
||||||
// Altura de un carácter escalado (necesario para ajustar Y)
|
|
||||||
const float CHAR_HEIGHT_SCALED = BASE_CHAR_HEIGHT * scale;
|
|
||||||
|
|
||||||
// Posición X del borde izquierdo del carácter actual
|
|
||||||
// (se ajustará +BASE_CHAR_WIDTH/2 para obtener el centro al renderizar)
|
|
||||||
float current_x = position.x;
|
|
||||||
|
|
||||||
// Iterar sobre cada byte del string (con detecció UTF-8)
|
|
||||||
for (size_t i = 0; i < text.length(); i++) {
|
|
||||||
auto c = static_cast<unsigned char>(text[i]);
|
|
||||||
|
|
||||||
// Detectar copyright UTF-8 (0xC2 0xA9)
|
|
||||||
if (c == 0xC2 && i + 1 < text.length() &&
|
|
||||||
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
|
||||||
c = 0xA9; // Usar segon byte como a key
|
|
||||||
i++; // Saltar el següent byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manejar espacios (avanzar sin dibujar)
|
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
|
||||||
if (c == ' ') {
|
const float SPACING_SCALED = spacing * scale;
|
||||||
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
|
||||||
continue;
|
// Contar caracteres visuals (no bytes) - manejar UTF-8
|
||||||
|
size_t visual_chars = 0;
|
||||||
|
for (size_t i = 0; i < text.length(); i++) {
|
||||||
|
auto c = static_cast<unsigned char>(text[i]);
|
||||||
|
|
||||||
|
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
|
||||||
|
if (c == 0xC2 && i + 1 < text.length() &&
|
||||||
|
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
||||||
|
visual_chars++; // Un caràcter visual (©)
|
||||||
|
i++; // Saltar el següent byte
|
||||||
|
} else {
|
||||||
|
visual_chars++; // Caràcter normal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si el carácter está soportado
|
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
|
||||||
auto it = chars_.find(c);
|
return (visual_chars * CHAR_WIDTH_SCALED) + ((visual_chars - 1) * SPACING_SCALED);
|
||||||
if (it != chars_.end()) {
|
|
||||||
// Renderizar carácter
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Avanzar posición
|
|
||||||
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
|
||||||
} else {
|
|
||||||
// Carácter no soportado: saltar (o renderizar '?' en el futuro)
|
|
||||||
std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'"
|
|
||||||
<< '\n';
|
|
||||||
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness) const {
|
|
||||||
// Calcular dimensions del text
|
|
||||||
float text_width = getTextWidth(text, scale, spacing);
|
|
||||||
float text_height = getTextHeight(scale);
|
|
||||||
|
|
||||||
// Calcular posición de l'esquina superior izquierda
|
|
||||||
// restant la meitat de las dimensions del point central
|
|
||||||
Vec2 posicio_esquerra = {
|
|
||||||
.x = centre_punt.x - (text_width / 2.0F),
|
|
||||||
.y = centre_punt.y - (text_height / 2.0F)};
|
|
||||||
|
|
||||||
// Delegar al método render() existent
|
|
||||||
render(text, posicio_esquerra, scale, spacing, brightness);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
|
|
||||||
if (text.empty()) {
|
|
||||||
return 0.0F;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const float CHAR_WIDTH_SCALED = BASE_CHAR_WIDTH * scale;
|
auto VectorText::getTextHeight(float scale) -> float {
|
||||||
const float SPACING_SCALED = spacing * scale;
|
return BASE_CHAR_HEIGHT * scale;
|
||||||
|
|
||||||
// Contar caracteres visuals (no bytes) - manejar UTF-8
|
|
||||||
size_t visual_chars = 0;
|
|
||||||
for (size_t i = 0; i < text.length(); i++) {
|
|
||||||
auto c = static_cast<unsigned char>(text[i]);
|
|
||||||
|
|
||||||
// Detectar copyright UTF-8 (0xC2 0xA9) - igual que render()
|
|
||||||
if (c == 0xC2 && i + 1 < text.length() &&
|
|
||||||
static_cast<unsigned char>(text[i + 1]) == 0xA9) {
|
|
||||||
visual_chars++; // Un caràcter visual (©)
|
|
||||||
i++; // Saltar el següent byte
|
|
||||||
} else {
|
|
||||||
visual_chars++; // Caràcter normal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ancho total = todos los caracteres VISUALES + spacing entre ellos
|
|
||||||
return (visual_chars * CHAR_WIDTH_SCALED) + ((visual_chars - 1) * SPACING_SCALED);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto VectorText::getTextHeight(float scale) -> float {
|
|
||||||
return BASE_CHAR_HEIGHT * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/rendering/render_context.hpp"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -12,12 +10,13 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "core/graphics/shape.hpp"
|
#include "core/graphics/shape.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
|
||||||
namespace Graphics {
|
namespace Graphics {
|
||||||
|
|
||||||
class VectorText {
|
class VectorText {
|
||||||
public:
|
public:
|
||||||
explicit VectorText(Rendering::Renderer* renderer);
|
explicit VectorText(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
// Renderizar string completo
|
// Renderizar string completo
|
||||||
@@ -27,7 +26,8 @@ class VectorText {
|
|||||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||||
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const;
|
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||||
|
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||||
|
|
||||||
// Renderizar string centrado en un punto
|
// Renderizar string centrado en un punto
|
||||||
// - text: cadena a renderizar
|
// - text: cadena a renderizar
|
||||||
@@ -35,7 +35,8 @@ class VectorText {
|
|||||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||||
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F) const;
|
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
||||||
|
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||||
|
|
||||||
// Calcular ancho total de un string (útil para centrado).
|
// Calcular ancho total de un string (útil para centrado).
|
||||||
// Es estático: no depende del estado del VectorText (el ancho viene de
|
// Es estático: no depende del estado del VectorText (el ancho viene de
|
||||||
@@ -48,12 +49,12 @@ class VectorText {
|
|||||||
// Verificar si un carácter está soportado
|
// Verificar si un carácter está soportado
|
||||||
[[nodiscard]] auto isSupported(char c) const -> bool;
|
[[nodiscard]] auto isSupported(char c) const -> bool;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Rendering::Renderer* renderer_;
|
Rendering::Renderer* renderer_;
|
||||||
std::unordered_map<char, std::shared_ptr<Shape>> chars_;
|
std::unordered_map<char, std::shared_ptr<Shape>> chars_;
|
||||||
|
|
||||||
void loadCharset();
|
void loadCharset();
|
||||||
[[nodiscard]] static auto getShapeFilename(char c) -> std::string;
|
[[nodiscard]] static auto getShapeFilename(char c) -> std::string;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -269,6 +269,31 @@ namespace Rendering::GPU {
|
|||||||
indices_.push_back(BASE_INDEX + 2);
|
indices_.push_back(BASE_INDEX + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::pushRect(float x, float y, float w, float h, float r, float g, float b, float a) {
|
||||||
|
if (w <= 0.0F || h <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float X1 = x;
|
||||||
|
const float Y1 = y;
|
||||||
|
const float X2 = x + w;
|
||||||
|
const float Y2 = y + h;
|
||||||
|
|
||||||
|
const auto BASE_INDEX = static_cast<uint16_t>(vertices_.size());
|
||||||
|
|
||||||
|
// edge_dist=0 → el fragment shader dóna alpha plena (no fade).
|
||||||
|
vertices_.push_back({X1, Y1, r, g, b, a, 0.0F});
|
||||||
|
vertices_.push_back({X2, Y1, r, g, b, a, 0.0F});
|
||||||
|
vertices_.push_back({X1, Y2, r, g, b, a, 0.0F});
|
||||||
|
vertices_.push_back({X2, Y2, r, g, b, a, 0.0F});
|
||||||
|
|
||||||
|
indices_.push_back(BASE_INDEX + 0);
|
||||||
|
indices_.push_back(BASE_INDEX + 1);
|
||||||
|
indices_.push_back(BASE_INDEX + 2);
|
||||||
|
indices_.push_back(BASE_INDEX + 1);
|
||||||
|
indices_.push_back(BASE_INDEX + 3);
|
||||||
|
indices_.push_back(BASE_INDEX + 2);
|
||||||
|
}
|
||||||
|
|
||||||
void GpuFrameRenderer::flushBatch() {
|
void GpuFrameRenderer::flushBatch() {
|
||||||
if (vertices_.empty() || indices_.empty()) {
|
if (vertices_.empty() || indices_.empty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ namespace Rendering::GPU {
|
|||||||
// Encola una línea con grosor configurable (px). Color RGBA en [0..1].
|
// Encola una línea con grosor configurable (px). Color RGBA en [0..1].
|
||||||
void pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a);
|
void pushLine(float x1, float y1, float x2, float y2, float thickness, float r, float g, float b, float a);
|
||||||
|
|
||||||
|
// Encola un rectàngle massís (2 triangles) amb color RGBA [0..1]. Es
|
||||||
|
// remet pel mateix pipeline de líneas (TRIANGLELIST + alpha blend); els
|
||||||
|
// vèrtexs es marquen amb edge_dist=0 perquè el fragment shader doni
|
||||||
|
// alpha completa sense fade geomètric. Útil per a fons semitransparents
|
||||||
|
// d'UI (notificacions, panels).
|
||||||
|
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
|
||||||
|
|
||||||
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
||||||
void endFrame();
|
void endFrame();
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "core/rendering/sdl_manager.hpp"
|
#include "core/rendering/sdl_manager.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "core/resources/resource_loader.hpp"
|
#include "core/resources/resource_loader.hpp"
|
||||||
|
#include "core/system/notifier.hpp"
|
||||||
#include "core/utils/path_utils.hpp"
|
#include "core/utils/path_utils.hpp"
|
||||||
#include "debug_overlay.hpp"
|
#include "debug_overlay.hpp"
|
||||||
#include "game/scenes/game_scene.hpp"
|
#include "game/scenes/game_scene.hpp"
|
||||||
@@ -264,6 +265,11 @@ auto Director::run() -> int {
|
|||||||
// a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
|
// a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
|
||||||
System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering);
|
System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering);
|
||||||
|
|
||||||
|
// Sistema de notificacions toast: singleton accessible des d'on calgui
|
||||||
|
// (F1-F5 a sdl_manager, ESC a global_events). El renderer ha de viure
|
||||||
|
// tant com el Notifier; el destruim explícitament abans de tornar.
|
||||||
|
System::Notifier::init(sdl.getRenderer());
|
||||||
|
|
||||||
// Bucle principal: construir escena → frame loop → destruir → siguiente.
|
// Bucle principal: construir escena → frame loop → destruir → siguiente.
|
||||||
while (context.nextScene() != SceneType::EXIT) {
|
while (context.nextScene() != SceneType::EXIT) {
|
||||||
SceneManager::actual = context.nextScene();
|
SceneManager::actual = context.nextScene();
|
||||||
@@ -275,6 +281,7 @@ auto Director::run() -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SceneManager::actual = SceneType::EXIT;
|
SceneManager::actual = SceneType::EXIT;
|
||||||
|
System::Notifier::destroy();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +332,9 @@ void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context
|
|||||||
|
|
||||||
scene.update(delta_time);
|
scene.update(delta_time);
|
||||||
debug_overlay.update(delta_time);
|
debug_overlay.update(delta_time);
|
||||||
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
|
notifier->update(delta_time);
|
||||||
|
}
|
||||||
Audio::update();
|
Audio::update();
|
||||||
|
|
||||||
// Si la swapchain no está disponible (ventana minimizada, etc.),
|
// Si la swapchain no está disponible (ventana minimizada, etc.),
|
||||||
@@ -335,7 +345,10 @@ void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context
|
|||||||
}
|
}
|
||||||
sdl.updateRenderingContext();
|
sdl.updateRenderingContext();
|
||||||
scene.draw();
|
scene.draw();
|
||||||
debug_overlay.draw(); // siempre on top de la escena
|
debug_overlay.draw(); // sempre per damunt de l'escena
|
||||||
|
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
|
notifier->draw(); // toast: per damunt de tot
|
||||||
|
}
|
||||||
sdl.present();
|
sdl.present();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// notifier.cpp - Implementació del singleton de notificacions toast
|
||||||
|
|
||||||
|
#include "core/system/notifier.hpp"
|
||||||
|
|
||||||
|
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||||
|
#include "core/utils/easing.hpp"
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Geometria del cuadre en coordenades lògiques (1280×720).
|
||||||
|
constexpr float CANVAS_WIDTH = 1280.0F;
|
||||||
|
constexpr float MARGIN_TOP = 40.0F;
|
||||||
|
constexpr float PADDING_H = 16.0F;
|
||||||
|
constexpr float PADDING_V = 10.0F;
|
||||||
|
constexpr float BORDER_THICKNESS = 2.0F;
|
||||||
|
constexpr float TEXT_SCALE = 0.4F;
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
constexpr float BORDER_BRIGHTNESS = 1.0F;
|
||||||
|
|
||||||
|
// Cinemàtica del slide.
|
||||||
|
constexpr float SLIDE_DURATION_S = 0.30F;
|
||||||
|
|
||||||
|
// Conversió color SDL → float [0,1].
|
||||||
|
constexpr auto toUnit(Uint8 v) -> float {
|
||||||
|
return static_cast<float>(v) / 255.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color del fons: variant fosca del text (0.25× RGB) amb alpha 0.65.
|
||||||
|
struct UnitRGBA {
|
||||||
|
float r;
|
||||||
|
float g;
|
||||||
|
float b;
|
||||||
|
float a;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr auto textColorFloat(SDL_Color c) -> UnitRGBA {
|
||||||
|
return UnitRGBA{.r = toUnit(c.r), .g = toUnit(c.g), .b = toUnit(c.b), .a = toUnit(c.a)};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto bgColorFloat(SDL_Color c) -> UnitRGBA {
|
||||||
|
constexpr float DARKEN = 0.25F;
|
||||||
|
constexpr float BG_ALPHA = 0.65F;
|
||||||
|
return UnitRGBA{
|
||||||
|
.r = toUnit(c.r) * DARKEN,
|
||||||
|
.g = toUnit(c.g) * DARKEN,
|
||||||
|
.b = toUnit(c.b) * DARKEN,
|
||||||
|
.a = BG_ALPHA};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presets per als atajos semàntics.
|
||||||
|
constexpr SDL_Color COLOR_INFO{.r = 230, .g = 230, .b = 230, .a = 255};
|
||||||
|
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
|
||||||
|
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
|
||||||
|
constexpr float DURATION_INFO = 2.0F;
|
||||||
|
constexpr float DURATION_WARN = 3.0F;
|
||||||
|
constexpr float DURATION_EXIT = 3.0F;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<Notifier> Notifier::instance;
|
||||||
|
|
||||||
|
void Notifier::init(Rendering::Renderer* renderer) {
|
||||||
|
if (!instance) {
|
||||||
|
instance = std::unique_ptr<Notifier>(new Notifier(renderer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifier::destroy() { instance.reset(); }
|
||||||
|
|
||||||
|
auto Notifier::get() -> Notifier* { return instance.get(); }
|
||||||
|
|
||||||
|
Notifier::Notifier(Rendering::Renderer* renderer)
|
||||||
|
: renderer_(renderer),
|
||||||
|
text_(renderer) {}
|
||||||
|
|
||||||
|
void Notifier::notify(const std::string& text, SDL_Color text_color, float duration_s) {
|
||||||
|
current_text_ = text;
|
||||||
|
current_color_ = text_color;
|
||||||
|
hold_remaining_s_ = duration_s;
|
||||||
|
|
||||||
|
const float TEXT_W = Graphics::VectorText::getTextWidth(text, TEXT_SCALE, TEXT_SPACING);
|
||||||
|
const float TEXT_H = Graphics::VectorText::getTextHeight(TEXT_SCALE);
|
||||||
|
|
||||||
|
box_w_ = TEXT_W + (PADDING_H * 2.0F);
|
||||||
|
box_h_ = TEXT_H + (PADDING_V * 2.0F);
|
||||||
|
text_x_ = (CANVAS_WIDTH - TEXT_W) * 0.5F;
|
||||||
|
|
||||||
|
y_on_ = MARGIN_TOP;
|
||||||
|
y_off_ = -(box_h_ + BORDER_THICKNESS);
|
||||||
|
|
||||||
|
// Si ja es veu, reseteja el slide-in des de la posició actual perquè
|
||||||
|
// la transició sembli continua. Si està amagat, arrenc des de fora.
|
||||||
|
if (status_ == Status::HIDDEN) {
|
||||||
|
y_current_ = y_off_;
|
||||||
|
}
|
||||||
|
status_ = Status::ENTERING;
|
||||||
|
slide_elapsed_s_ = 0.0F;
|
||||||
|
text_scale_ = TEXT_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifier::notifyInfo(const std::string& text) { notify(text, COLOR_INFO, DURATION_INFO); }
|
||||||
|
void Notifier::notifyWarn(const std::string& text) { notify(text, COLOR_WARN, DURATION_WARN); }
|
||||||
|
void Notifier::notifyExit(const std::string& text) { notify(text, COLOR_EXIT, DURATION_EXIT); }
|
||||||
|
|
||||||
|
void Notifier::update(float delta_time) {
|
||||||
|
switch (status_) {
|
||||||
|
case Status::ENTERING: {
|
||||||
|
slide_elapsed_s_ += delta_time;
|
||||||
|
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
|
||||||
|
y_current_ = y_on_;
|
||||||
|
status_ = Status::HOLDING;
|
||||||
|
slide_elapsed_s_ = 0.0F;
|
||||||
|
} else {
|
||||||
|
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
|
||||||
|
const float K = Utils::Easing::outCubic(T);
|
||||||
|
y_current_ = y_off_ + ((y_on_ - y_off_) * K);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Status::HOLDING: {
|
||||||
|
hold_remaining_s_ -= delta_time;
|
||||||
|
if (hold_remaining_s_ <= 0.0F) {
|
||||||
|
status_ = Status::EXITING;
|
||||||
|
slide_elapsed_s_ = 0.0F;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Status::EXITING: {
|
||||||
|
slide_elapsed_s_ += delta_time;
|
||||||
|
if (slide_elapsed_s_ >= SLIDE_DURATION_S) {
|
||||||
|
y_current_ = y_off_;
|
||||||
|
status_ = Status::HIDDEN;
|
||||||
|
} else {
|
||||||
|
const float T = slide_elapsed_s_ / SLIDE_DURATION_S;
|
||||||
|
const float K = Utils::Easing::inCubic(T);
|
||||||
|
y_current_ = y_on_ + ((y_off_ - y_on_) * K);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Status::HIDDEN:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notifier::draw() const {
|
||||||
|
if (status_ == Status::HIDDEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float BOX_X = (CANVAS_WIDTH - box_w_) * 0.5F;
|
||||||
|
const float BOX_Y = y_current_;
|
||||||
|
const UnitRGBA TC = textColorFloat(current_color_);
|
||||||
|
const UnitRGBA BG = bgColorFloat(current_color_);
|
||||||
|
|
||||||
|
auto* gpu = renderer_;
|
||||||
|
|
||||||
|
// 1. Fons semitransparent.
|
||||||
|
gpu->pushRect(BOX_X, BOX_Y, box_w_, box_h_, BG.r, BG.g, BG.b, BG.a);
|
||||||
|
|
||||||
|
// 2. Bordes (4 línies amb el color del text).
|
||||||
|
const float X1 = BOX_X;
|
||||||
|
const float Y1 = BOX_Y;
|
||||||
|
const float X2 = BOX_X + box_w_;
|
||||||
|
const float Y2 = BOX_Y + box_h_;
|
||||||
|
gpu->pushLine(X1, Y1, X2, Y1, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // top
|
||||||
|
gpu->pushLine(X1, Y2, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // bottom
|
||||||
|
gpu->pushLine(X1, Y1, X1, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // left
|
||||||
|
gpu->pushLine(X2, Y1, X2, Y2, BORDER_THICKNESS, TC.r, TC.g, TC.b, TC.a); // right
|
||||||
|
|
||||||
|
// 3. Text centrat dins la caixa, amb color explícit (l'alpha != 0
|
||||||
|
// li diu al renderShape que no agafe l'oscil·lador global de color).
|
||||||
|
const float TEXT_Y = BOX_Y + PADDING_V;
|
||||||
|
text_.render(current_text_,
|
||||||
|
Vec2{.x = text_x_, .y = TEXT_Y},
|
||||||
|
text_scale_,
|
||||||
|
TEXT_SPACING,
|
||||||
|
BORDER_BRIGHTNESS,
|
||||||
|
current_color_);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Notifier::isActiveWindow() const -> bool {
|
||||||
|
return status_ == Status::ENTERING || status_ == Status::HOLDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// notifier.hpp - Sistema de notificacions toast (singleton)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Mostra missatges curts en un cuadre centrat horitzontalment al centre
|
||||||
|
// superior de la pantalla. El cuadre entra des de fora amb easing outCubic,
|
||||||
|
// aguanta el temps demanat i surt amb inCubic. El color del text és
|
||||||
|
// configurable; el fondo es deriva oscurint el RGB del text i posant alpha
|
||||||
|
// 0.65 (semitransparent).
|
||||||
|
//
|
||||||
|
// API singleton (mateix patró que Audio i Input): Notifier::init() al startup,
|
||||||
|
// Notifier::get()->notify(...) des d'on calgui, Notifier::destroy() al teardown.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/graphics/vector_text.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
class Notifier {
|
||||||
|
public:
|
||||||
|
// Inicialitza el singleton amb el renderer global. El renderer ha de
|
||||||
|
// viure tant com el Notifier (és del SDLManager, propietat del Director).
|
||||||
|
static void init(Rendering::Renderer* renderer);
|
||||||
|
static void destroy();
|
||||||
|
[[nodiscard]] static auto get() -> Notifier*;
|
||||||
|
|
||||||
|
// Mostra una notificació. Si ja n'hi ha una visible, es sobreescriu
|
||||||
|
// (reset a l'estat ENTERING des de la Y actual; mai s'apilen).
|
||||||
|
// - text: cadena a mostrar (sense salts de línia)
|
||||||
|
// - text_color: color RGBA del text i del borde
|
||||||
|
// - duration_s: temps que es queda visible (sense comptar entry/exit)
|
||||||
|
void notify(const std::string& text, SDL_Color text_color, float duration_s);
|
||||||
|
|
||||||
|
// Atajos semàntics amb colors i durada predefinits.
|
||||||
|
void notifyInfo(const std::string& text); // blanc, 2.0s
|
||||||
|
void notifyWarn(const std::string& text); // àmbar, 3.0s
|
||||||
|
void notifyExit(const std::string& text); // vermell, EXIT_WINDOW_S
|
||||||
|
|
||||||
|
void update(float delta_time);
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Activa mentre el toast està entrant o aguantant. Quan està sortint
|
||||||
|
// o ja amagat, retorna false. Útil per a la lògica de doble-pulsació
|
||||||
|
// d'ESC: la segona pulsació només confirma sortida si encara aguanta.
|
||||||
|
[[nodiscard]] auto isActiveWindow() const -> bool;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Notifier(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
|
enum class Status : std::uint8_t { HIDDEN,
|
||||||
|
ENTERING,
|
||||||
|
HOLDING,
|
||||||
|
EXITING };
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
Graphics::VectorText text_;
|
||||||
|
|
||||||
|
Status status_{Status::HIDDEN};
|
||||||
|
std::string current_text_;
|
||||||
|
SDL_Color current_color_{.r = 255, .g = 255, .b = 255, .a = 255};
|
||||||
|
float hold_remaining_s_{0.0F};
|
||||||
|
float slide_elapsed_s_{0.0F};
|
||||||
|
float y_current_{0.0F};
|
||||||
|
float y_off_{0.0F}; // posició Y fora de pantalla
|
||||||
|
float y_on_{0.0F}; // posició Y de descans (visible)
|
||||||
|
float box_w_{0.0F};
|
||||||
|
float box_h_{0.0F};
|
||||||
|
float text_x_{0.0F}; // X esquerre del text dins la caixa
|
||||||
|
float text_scale_{0.4F};
|
||||||
|
|
||||||
|
static std::unique_ptr<Notifier> instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// easing.hpp - Funciones d'interpolació suaus (header-only)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Conjunt mínim de funcions easing per a animacions d'UI. Totes prenen un
|
||||||
|
// paràmetre normalitzat t ∈ [0,1] i retornen un valor ∈ [0,1].
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Utils::Easing {
|
||||||
|
|
||||||
|
// outCubic: ràpid al principi, suau cap al final. Útil per a entrades de
|
||||||
|
// notificacions (slide-in: arrenc d'impacte i frenada cuidada).
|
||||||
|
constexpr auto outCubic(float t) -> float {
|
||||||
|
const float INV = 1.0F - t;
|
||||||
|
return 1.0F - (INV * INV * INV);
|
||||||
|
}
|
||||||
|
|
||||||
|
// inCubic: arranca suau, accelera cap al final. Útil per a sortides
|
||||||
|
// (slide-out: comença discretament i desapareix ràpid).
|
||||||
|
constexpr auto inCubic(float t) -> float {
|
||||||
|
return t * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Utils::Easing
|
||||||
Reference in New Issue
Block a user