segon commit
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
#include "core/rendering/gif.hpp"
|
||||
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <iostream> // Para std::cout
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <string> // Para allocator, char_traits, operator==, basic_string
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Función inline para reemplazar el macro READ.
|
||||
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'.
|
||||
inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
|
||||
std::memcpy(dst, buffer, size);
|
||||
buffer += size;
|
||||
}
|
||||
|
||||
// Inicializa el diccionario LZW con los valores iniciales
|
||||
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
|
||||
int size = 1 << code_length;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
|
||||
dictionary[dictionary_ind].prev = -1;
|
||||
dictionary[dictionary_ind].len = 1;
|
||||
}
|
||||
dictionary_ind += 2; // Reservamos espacio para clear y stop codes
|
||||
}
|
||||
|
||||
// Lee los próximos bits del stream de entrada para formar un código
|
||||
inline auto readNextCode(const uint8_t*& input, int& input_length, unsigned int& mask, int code_length) -> int {
|
||||
int code = 0;
|
||||
for (int i = 0; i < (code_length + 1); i++) {
|
||||
if (input_length <= 0) {
|
||||
throw std::runtime_error("Unexpected end of input in decompress");
|
||||
}
|
||||
int bit = ((*input & mask) != 0) ? 1 : 0;
|
||||
mask <<= 1;
|
||||
if (mask == 0x100) {
|
||||
mask = 0x01;
|
||||
input++;
|
||||
input_length--;
|
||||
}
|
||||
code |= (bit << i);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// Encuentra el primer byte de una cadena del diccionario
|
||||
inline auto findFirstByte(const std::vector<DictionaryEntry>& dictionary, int code) -> uint8_t {
|
||||
int ptr = code;
|
||||
while (dictionary[ptr].prev != -1) {
|
||||
ptr = dictionary[ptr].prev;
|
||||
}
|
||||
return dictionary[ptr].byte;
|
||||
}
|
||||
|
||||
// Agrega una nueva entrada al diccionario
|
||||
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
|
||||
uint8_t first_byte;
|
||||
if (code == dictionary_ind) {
|
||||
first_byte = findFirstByte(dictionary, prev);
|
||||
} else {
|
||||
first_byte = findFirstByte(dictionary, code);
|
||||
}
|
||||
|
||||
dictionary[dictionary_ind].byte = first_byte;
|
||||
dictionary[dictionary_ind].prev = prev;
|
||||
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
|
||||
dictionary_ind++;
|
||||
|
||||
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
|
||||
code_length++;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe la cadena decodificada al buffer de salida
|
||||
inline auto writeDecodedString(const std::vector<DictionaryEntry>& dictionary, int code, uint8_t*& out) -> int {
|
||||
int cur_code = code;
|
||||
int match_len = dictionary[cur_code].len;
|
||||
while (cur_code != -1) {
|
||||
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||
if (dictionary[cur_code].prev == cur_code) {
|
||||
std::cerr << "Internal error; self-reference detected." << '\n';
|
||||
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||
}
|
||||
cur_code = dictionary[cur_code].prev;
|
||||
}
|
||||
out += match_len;
|
||||
return match_len;
|
||||
}
|
||||
|
||||
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verifica que el code_length tenga un rango razonable.
|
||||
if (code_length < 2 || code_length > 12) {
|
||||
throw std::runtime_error("Invalid LZW code length");
|
||||
}
|
||||
|
||||
int prev = -1;
|
||||
std::vector<DictionaryEntry> dictionary;
|
||||
int dictionary_ind;
|
||||
unsigned int mask = 0x01;
|
||||
int reset_code_length = code_length;
|
||||
int clear_code = 1 << code_length;
|
||||
int stop_code = clear_code + 1;
|
||||
|
||||
// Inicializamos el diccionario con el tamaño correspondiente.
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
|
||||
// Bucle principal: procesar el stream comprimido.
|
||||
while (input_length > 0) {
|
||||
int code = readNextCode(input, input_length, mask, code_length);
|
||||
|
||||
if (code == clear_code) {
|
||||
// Reinicia el diccionario.
|
||||
code_length = reset_code_length;
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
prev = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == stop_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev > -1 && code_length < 12) {
|
||||
if (code > dictionary_ind) {
|
||||
std::cerr << "code = " << std::hex << code
|
||||
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
||||
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
|
||||
}
|
||||
|
||||
addDictionaryEntry(dictionary, dictionary_ind, code_length, prev, code);
|
||||
}
|
||||
|
||||
prev = code;
|
||||
|
||||
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||
std::cerr << "Invalid LZW code " << code
|
||||
<< ", dictionary size " << dictionary.size() << '\n';
|
||||
throw std::runtime_error("LZW error: invalid code encountered");
|
||||
}
|
||||
|
||||
writeDecodedString(dictionary, code, out);
|
||||
}
|
||||
}
|
||||
|
||||
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<uint8_t> data;
|
||||
uint8_t block_size = *buffer;
|
||||
buffer++;
|
||||
while (block_size != 0) {
|
||||
data.insert(data.end(), buffer, buffer + block_size);
|
||||
buffer += block_size;
|
||||
block_size = *buffer;
|
||||
buffer++;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
ImageDescriptor image_descriptor;
|
||||
// Lee 9 bytes para el image descriptor.
|
||||
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
|
||||
|
||||
uint8_t lzw_code_size;
|
||||
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
|
||||
|
||||
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
|
||||
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
|
||||
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
|
||||
|
||||
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
|
||||
return uncompressed_data;
|
||||
}
|
||||
|
||||
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
ScreenDescriptor screen_descriptor;
|
||||
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
|
||||
buffer += sizeof(ScreenDescriptor);
|
||||
|
||||
std::vector<uint32_t> global_color_table;
|
||||
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||
global_color_table.resize(global_color_table_size);
|
||||
for (int i = 0; i < global_color_table_size; ++i) {
|
||||
uint8_t r = buffer[0];
|
||||
uint8_t g = buffer[1];
|
||||
uint8_t b = buffer[2];
|
||||
global_color_table[i] = (r << 16) | (g << 8) | b;
|
||||
buffer += 3;
|
||||
}
|
||||
}
|
||||
return global_color_table;
|
||||
}
|
||||
|
||||
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
// Opcional: Validar header
|
||||
std::string header_str(reinterpret_cast<char*>(header), 6);
|
||||
if (header_str != "GIF87a" && header_str != "GIF89a") {
|
||||
throw std::runtime_error("Formato de archivo GIF inválido.");
|
||||
}
|
||||
|
||||
// Leer el Screen Descriptor (7 bytes, empaquetado sin padding)
|
||||
ScreenDescriptor screen_descriptor;
|
||||
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
|
||||
|
||||
// Asigna ancho y alto
|
||||
w = screen_descriptor.width;
|
||||
h = screen_descriptor.height;
|
||||
|
||||
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
|
||||
std::vector<RGB> global_color_table;
|
||||
if ((screen_descriptor.fields & 0x80) != 0) {
|
||||
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
|
||||
global_color_table.resize(global_color_table_size);
|
||||
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
|
||||
buffer += 3 * global_color_table_size;
|
||||
}
|
||||
|
||||
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
|
||||
uint8_t block_type = *buffer++;
|
||||
while (block_type != TRAILER) {
|
||||
if (block_type == EXTENSION_INTRODUCER) // 0x21
|
||||
{
|
||||
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión.
|
||||
uint8_t extension_label = *buffer++;
|
||||
switch (extension_label) {
|
||||
case GRAPHIC_CONTROL: // 0xF9
|
||||
{
|
||||
// Procesar Graphic Control Extension:
|
||||
uint8_t block_size = *buffer++; // Normalmente, blockSize == 4
|
||||
buffer += block_size; // Saltamos los 4 bytes del bloque fijo
|
||||
// Saltar los sub-bloques
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APPLICATION_EXTENSION: // 0xFF
|
||||
case COMMENT_EXTENSION: // 0xFE
|
||||
case PLAINTEXT_EXTENSION: // 0x01
|
||||
{
|
||||
// Para estas extensiones, saltamos el bloque fijo y los sub-bloques.
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Si la etiqueta de extensión es desconocida, saltarla también:
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||
// Procesar el Image Descriptor y retornar los datos de imagen
|
||||
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
|
||||
} else {
|
||||
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
block_type = *buffer++;
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
|
||||
auto Gif::loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
|
||||
return processGifStream(buffer, w, h);
|
||||
}
|
||||
|
||||
} // namespace GIF
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
|
||||
#include <vector> // Para vector
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Constantes definidas con constexpr, en lugar de macros
|
||||
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
|
||||
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
|
||||
constexpr uint8_t TRAILER = 0x3B;
|
||||
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
|
||||
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
|
||||
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
|
||||
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct ScreenDescriptor {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t fields;
|
||||
uint8_t background_color_index;
|
||||
uint8_t pixel_aspect_ratio;
|
||||
};
|
||||
|
||||
struct RGB {
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
struct ImageDescriptor {
|
||||
uint16_t image_left_position;
|
||||
uint16_t image_top_position;
|
||||
uint16_t image_width;
|
||||
uint16_t image_height;
|
||||
uint8_t fields;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct DictionaryEntry {
|
||||
uint8_t byte;
|
||||
int prev;
|
||||
int len;
|
||||
};
|
||||
|
||||
struct Extension {
|
||||
uint8_t extension_code;
|
||||
uint8_t block_size;
|
||||
};
|
||||
|
||||
struct GraphicControlExtension {
|
||||
uint8_t fields;
|
||||
uint16_t delay_time;
|
||||
uint8_t transparent_color_index;
|
||||
};
|
||||
|
||||
struct ApplicationExtension {
|
||||
uint8_t application_id[8];
|
||||
uint8_t version[3];
|
||||
};
|
||||
|
||||
struct PlaintextExtension {
|
||||
uint16_t left, top, width, height;
|
||||
uint8_t cell_width, cell_height;
|
||||
uint8_t foreground_color, background_color;
|
||||
};
|
||||
|
||||
class Gif {
|
||||
public:
|
||||
// Descompone (uncompress) el bloque comprimido usando LZW.
|
||||
// Este método puede lanzar std::runtime_error en caso de error.
|
||||
static void decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out);
|
||||
|
||||
// Carga la paleta (global color table) a partir de un buffer,
|
||||
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
|
||||
static auto loadPalette(const uint8_t* buffer) -> std::vector<uint32_t>;
|
||||
|
||||
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
|
||||
// asigna el ancho y alto mediante referencias.
|
||||
static auto loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
|
||||
private:
|
||||
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
|
||||
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
|
||||
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
|
||||
static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
};
|
||||
|
||||
} // namespace GIF
|
||||
@@ -0,0 +1,276 @@
|
||||
#include "core/rendering/palette_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/defaults.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
// ── Conversión string ↔ PaletteSortMode ──────────────────────────────────────
|
||||
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode {
|
||||
const std::string LOWER = toLower(str);
|
||||
if (LOWER == "luminance") { return PaletteSortMode::LUMINANCE; }
|
||||
if (LOWER == "spectrum") { return PaletteSortMode::SPECTRUM; }
|
||||
return PaletteSortMode::ORIGINAL;
|
||||
}
|
||||
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return "luminance";
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return "spectrum";
|
||||
default:
|
||||
return "original";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paleta de referencia ZX Spectrum (16 colores ARGB) ───────────────────────
|
||||
|
||||
namespace {
|
||||
// Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB)
|
||||
constexpr auto redOf(Uint32 c) -> int { return static_cast<int>((c >> 16) & 0xFF); }
|
||||
constexpr auto greenOf(Uint32 c) -> int { return static_cast<int>((c >> 8) & 0xFF); }
|
||||
constexpr auto blueOf(Uint32 c) -> int { return static_cast<int>(c & 0xFF); }
|
||||
|
||||
constexpr auto makeARGB(int r, int g, int b) -> Uint32 {
|
||||
return (0xFFU << 24) | (static_cast<Uint32>(r) << 16) | (static_cast<Uint32>(g) << 8) | static_cast<Uint32>(b);
|
||||
}
|
||||
|
||||
// Paleta ZX Spectrum de referencia (misma que en tools/sort_palette/sort_palette.py)
|
||||
constexpr std::array<Uint32, 16> SPECTRUM_REFERENCE = {
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 216),
|
||||
makeARGB(0, 0, 255),
|
||||
makeARGB(216, 0, 0),
|
||||
makeARGB(255, 0, 0),
|
||||
makeARGB(216, 0, 216),
|
||||
makeARGB(255, 0, 255),
|
||||
makeARGB(0, 216, 0),
|
||||
makeARGB(0, 255, 0),
|
||||
makeARGB(0, 216, 216),
|
||||
makeARGB(0, 255, 255),
|
||||
makeARGB(216, 216, 0),
|
||||
makeARGB(255, 255, 0),
|
||||
makeARGB(216, 216, 216),
|
||||
makeARGB(255, 255, 255),
|
||||
};
|
||||
|
||||
// Luminancia percibida (ITU-R BT.709)
|
||||
auto luminance(Uint32 color) -> double {
|
||||
return (0.2126 * redOf(color)) + (0.7152 * greenOf(color)) + (0.0722 * blueOf(color));
|
||||
}
|
||||
|
||||
// Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar)
|
||||
auto rgbDistanceSq(Uint32 a, Uint32 b) -> int {
|
||||
const int DR = redOf(a) - redOf(b);
|
||||
const int DG = greenOf(a) - greenOf(b);
|
||||
const int DB = blueOf(a) - blueOf(b);
|
||||
return (DR * DR) + (DG * DG) + (DB * DB);
|
||||
}
|
||||
|
||||
// Cuenta los colores activos en la paleta (los que tienen alpha != 0)
|
||||
auto countActiveColors(const Palette& palette) -> size_t {
|
||||
size_t count = 0;
|
||||
for (const auto& c : palette) {
|
||||
if (c == 0) { break; }
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Ordenar por luminancia
|
||||
auto sortByLuminance(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> colors(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::ranges::sort(colors, [](Uint32 a, Uint32 b) {
|
||||
return luminance(a) < luminance(b);
|
||||
});
|
||||
|
||||
Palette result{};
|
||||
result.fill(0);
|
||||
std::ranges::copy(colors, result.begin());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ordenar por similitud con la paleta ZX Spectrum (greedy matching)
|
||||
auto sortBySpectrum(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> available(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::vector<Uint32> result;
|
||||
result.reserve(N);
|
||||
|
||||
// Para cada color de referencia del Spectrum, buscar el más cercano disponible
|
||||
const size_t REFS = std::min(N, SPECTRUM_REFERENCE.size());
|
||||
for (size_t i = 0; i < REFS && !available.empty(); ++i) {
|
||||
const Uint32 REF = SPECTRUM_REFERENCE[i];
|
||||
auto best = std::ranges::min_element(available, [REF](Uint32 a, Uint32 b) {
|
||||
return rgbDistanceSq(a, REF) < rgbDistanceSq(b, REF);
|
||||
});
|
||||
result.push_back(*best);
|
||||
available.erase(best);
|
||||
}
|
||||
|
||||
// Si quedan colores sin asignar, añadirlos al final
|
||||
for (const auto& c : available) {
|
||||
result.push_back(c);
|
||||
}
|
||||
|
||||
Palette out{};
|
||||
out.fill(0);
|
||||
std::ranges::copy(result, out.begin());
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ── PaletteManager ───────────────────────────────────────────────────────────
|
||||
|
||||
PaletteManager::PaletteManager(
|
||||
std::vector<std::string> raw_paths,
|
||||
const std::string& initial_name,
|
||||
PaletteSortMode initial_sort_mode,
|
||||
std::shared_ptr<Surface> game_surface,
|
||||
std::shared_ptr<Surface> border_surface,
|
||||
OnChangeCallback on_change)
|
||||
: palettes_(std::move(raw_paths)),
|
||||
sort_mode_(initial_sort_mode),
|
||||
game_surface_(std::move(game_surface)),
|
||||
border_surface_(std::move(border_surface)),
|
||||
on_change_(std::move(on_change)) {
|
||||
current_ = findIndex(initial_name);
|
||||
|
||||
// Leer y aplicar paleta inicial directamente desde el archivo
|
||||
// (Resource::Cache aún no está disponible en este punto del ciclo de vida)
|
||||
const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_);
|
||||
game_surface_->setPalette(INITIAL_PALETTE);
|
||||
border_surface_->setPalette(INITIAL_PALETTE);
|
||||
|
||||
// Procesar la lista: conservar solo los nombres de archivo (sin ruta)
|
||||
processPathList();
|
||||
}
|
||||
|
||||
void PaletteManager::next() {
|
||||
if (++current_ == palettes_.size()) {
|
||||
current_ = 0;
|
||||
}
|
||||
apply();
|
||||
}
|
||||
|
||||
void PaletteManager::previous() {
|
||||
current_ = (current_ > 0) ? current_ - 1 : palettes_.size() - 1;
|
||||
apply();
|
||||
}
|
||||
|
||||
auto PaletteManager::setByName(const std::string& name) -> bool {
|
||||
const std::string LOWER_NAME = toLower(name + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(palettes_[i]) == LOWER_NAME) {
|
||||
current_ = i;
|
||||
apply();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto PaletteManager::getNames() const -> std::vector<std::string> {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(palettes_.size());
|
||||
for (const auto& p : palettes_) {
|
||||
std::string name = p;
|
||||
const size_t POS = name.find(".pal");
|
||||
if (POS != std::string::npos) { name.erase(POS, 4); }
|
||||
std::ranges::transform(name, name.begin(), ::tolower);
|
||||
names.push_back(std::move(name));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
auto PaletteManager::getCurrentName() const -> std::string {
|
||||
std::string name = palettes_.at(current_);
|
||||
const size_t POS = name.find(".pal");
|
||||
if (POS != std::string::npos) { name.erase(POS, 4); }
|
||||
std::ranges::transform(name, name.begin(), ::tolower);
|
||||
return name;
|
||||
}
|
||||
|
||||
auto PaletteManager::getPrettyName() const -> std::string {
|
||||
std::string name = getCurrentName();
|
||||
std::ranges::replace(name, '-', ' ');
|
||||
return name;
|
||||
}
|
||||
|
||||
void PaletteManager::nextSortMode() {
|
||||
sort_mode_ = static_cast<PaletteSortMode>((static_cast<int>(sort_mode_) + 1) % static_cast<int>(PaletteSortMode::COUNT));
|
||||
Options::video.palette_sort = sortModeToString(sort_mode_);
|
||||
apply();
|
||||
}
|
||||
|
||||
void PaletteManager::setSortMode(PaletteSortMode mode) {
|
||||
sort_mode_ = mode;
|
||||
Options::video.palette_sort = sortModeToString(sort_mode_);
|
||||
apply();
|
||||
}
|
||||
|
||||
auto PaletteManager::getSortMode() const -> PaletteSortMode {
|
||||
return sort_mode_;
|
||||
}
|
||||
|
||||
auto PaletteManager::getSortModeName() const -> std::string {
|
||||
return sortModeToString(sort_mode_);
|
||||
}
|
||||
|
||||
void PaletteManager::apply() {
|
||||
Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_));
|
||||
Palette sorted = sortPalette(raw, sort_mode_);
|
||||
game_surface_->loadPalette(sorted);
|
||||
border_surface_->loadPalette(sorted);
|
||||
|
||||
Options::video.palette = getCurrentName();
|
||||
|
||||
if (on_change_) {
|
||||
on_change_();
|
||||
}
|
||||
}
|
||||
|
||||
auto PaletteManager::findIndex(const std::string& name) const -> size_t {
|
||||
const std::string LOWER_NAME = toLower(name + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(getFileName(palettes_[i])) == LOWER_NAME) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Fallback: buscar la paleta por defecto
|
||||
const std::string DEFAULT_NAME = toLower(std::string(Defaults::Video::PALETTE_NAME) + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(getFileName(palettes_[i])) == DEFAULT_NAME) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PaletteManager::processPathList() {
|
||||
for (auto& palette : palettes_) {
|
||||
palette = getFileName(palette);
|
||||
}
|
||||
}
|
||||
|
||||
auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return sortByLuminance(palette);
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return sortBySpectrum(palette);
|
||||
default:
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Alias de paleta (igual que en surface.hpp; evita incluir todo el header)
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
|
||||
class Surface;
|
||||
|
||||
// Modo de ordenación de paletas
|
||||
enum class PaletteSortMode : int {
|
||||
ORIGINAL = 0, // Paleta tal cual viene del fichero
|
||||
LUMINANCE = 1, // Ordenada por luminancia percibida
|
||||
SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum
|
||||
COUNT = 3
|
||||
};
|
||||
|
||||
// Conversión string ↔ PaletteSortMode
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode;
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string;
|
||||
|
||||
class PaletteManager {
|
||||
public:
|
||||
using OnChangeCallback = std::function<void()>;
|
||||
|
||||
PaletteManager(
|
||||
std::vector<std::string> raw_paths,
|
||||
const std::string& initial_name,
|
||||
PaletteSortMode initial_sort_mode,
|
||||
std::shared_ptr<Surface> game_surface,
|
||||
std::shared_ptr<Surface> border_surface,
|
||||
OnChangeCallback on_change = nullptr);
|
||||
|
||||
void next(); // Avanza a la siguiente paleta
|
||||
void previous(); // Retrocede a la paleta anterior
|
||||
auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
|
||||
void nextSortMode(); // Cicla al siguiente modo de ordenación
|
||||
void setSortMode(PaletteSortMode mode); // Establece un modo de ordenación concreto
|
||||
[[nodiscard]] auto getSortMode() const -> PaletteSortMode; // Devuelve el modo de ordenación actual
|
||||
[[nodiscard]] auto getSortModeName() const -> std::string; // Nombre del modo actual ("ORIGINAL", etc.)
|
||||
|
||||
private:
|
||||
void apply(); // Aplica la paleta actual a ambas surfaces
|
||||
[[nodiscard]] auto findIndex(const std::string& name) const -> size_t; // Localiza paleta por nombre en el vector
|
||||
void processPathList(); // Extrae nombres de archivo de las rutas completas
|
||||
static auto sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette; // Reordena una paleta según el modo
|
||||
|
||||
std::vector<std::string> palettes_;
|
||||
size_t current_{0};
|
||||
PaletteSortMode sort_mode_{PaletteSortMode::ORIGINAL};
|
||||
std::shared_ptr<Surface> game_surface_;
|
||||
std::shared_ptr<Surface> border_surface_;
|
||||
OnChangeCallback on_change_;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "core/rendering/pixel_reveal.hpp"
|
||||
|
||||
#include <algorithm> // Para min, ranges::all_of
|
||||
#include <numeric> // Para iota
|
||||
#include <queue> // Para queue (BFS en modo ORDERED)
|
||||
#include <random> // Para mt19937, shuffle
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Constructor
|
||||
PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps, bool reverse, RevealMode mode)
|
||||
: cover_surface_(std::make_shared<Surface>(width, height)),
|
||||
reveal_order_(height),
|
||||
row_step_(height, 0),
|
||||
width_(width),
|
||||
height_(height),
|
||||
pixels_per_second_(pixels_per_second),
|
||||
step_duration_(step_duration),
|
||||
num_steps_(num_steps),
|
||||
reverse_(reverse),
|
||||
mode_(mode) {
|
||||
// En modo normal: empieza negro sólido (se irá revelando a transparente)
|
||||
// En modo inverso: empieza transparente (se irá cubriendo de negro)
|
||||
const auto INITIAL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::TRANSPARENT) : static_cast<Uint8>(PaletteColor::BLACK);
|
||||
cover_surface_->clear(INITIAL_COLOR);
|
||||
|
||||
if (mode_ == RevealMode::ORDERED) {
|
||||
// Calcula offsets por bisección BFS: 0, N/2, N/4, 3N/4, ...
|
||||
std::vector<int> offsets;
|
||||
offsets.push_back(0);
|
||||
std::queue<std::pair<int, int>> bq;
|
||||
bq.emplace(0, num_steps_);
|
||||
while (static_cast<int>(offsets.size()) < num_steps_) {
|
||||
auto [lo, hi] = bq.front();
|
||||
bq.pop();
|
||||
if (hi - lo <= 1) {
|
||||
continue;
|
||||
}
|
||||
const int MID = (lo + hi) / 2;
|
||||
offsets.push_back(MID);
|
||||
bq.emplace(lo, MID);
|
||||
bq.emplace(MID, hi);
|
||||
}
|
||||
// Genera el orden: para cada offset, todas las columnas col = offset, offset+N, offset+2N, ...
|
||||
std::vector<int> ordered_cols;
|
||||
ordered_cols.reserve(width_);
|
||||
for (const int OFF : offsets) {
|
||||
for (int col = OFF; col < width_; col += num_steps_) {
|
||||
ordered_cols.push_back(col);
|
||||
}
|
||||
}
|
||||
// Todas las filas usan el mismo orden (sin aleatoriedad)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r] = ordered_cols;
|
||||
}
|
||||
} else {
|
||||
// Modo RANDOM: orden aleatorio por fila usando la fila como semilla (reproducible)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r].resize(width_);
|
||||
std::iota(reveal_order_[r].begin(), reveal_order_[r].end(), 0);
|
||||
std::mt19937 rng(static_cast<unsigned int>(r));
|
||||
std::shuffle(reveal_order_[r].begin(), reveal_order_[r].end(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el estado del revelado
|
||||
void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const)
|
||||
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
|
||||
const auto PIXEL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::BLACK) : static_cast<Uint8>(PaletteColor::TRANSPARENT);
|
||||
|
||||
for (int r = 0; r < height_; r++) {
|
||||
const float T_START = static_cast<float>(r) / pixels_per_second_;
|
||||
const float TIME_IN_ROW = time_active - T_START;
|
||||
|
||||
if (TIME_IN_ROW < 0.0F) {
|
||||
continue; // Esta fila aún no ha empezado
|
||||
}
|
||||
|
||||
const int STEPS = std::min(num_steps_, static_cast<int>(TIME_IN_ROW / step_duration_));
|
||||
|
||||
if (STEPS > row_step_[r]) {
|
||||
// Procesa los píxeles de los pasos pendientes
|
||||
for (int step = row_step_[r]; step < STEPS; step++) {
|
||||
const int START_IDX = step * width_ / num_steps_;
|
||||
const int END_IDX = (step == num_steps_ - 1) ? width_ : (step + 1) * width_ / num_steps_;
|
||||
|
||||
for (int idx = START_IDX; idx < END_IDX; idx++) {
|
||||
const int COL = reveal_order_[r][idx];
|
||||
cover_surface_->putPixel(COL, r, PIXEL_COLOR);
|
||||
}
|
||||
}
|
||||
row_step_[r] = STEPS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja la máscara en la posición indicada
|
||||
void PixelReveal::render(int dst_x, int dst_y) const {
|
||||
cover_surface_->render(dst_x, dst_y);
|
||||
}
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
auto PixelReveal::isComplete() const -> bool {
|
||||
return std::ranges::all_of(row_step_, [this](int s) -> bool { return s >= num_steps_; });
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <vector> // Para vector
|
||||
|
||||
class Surface;
|
||||
|
||||
// Efecto de revelado pixel a pixel por filas, de arriba a abajo.
|
||||
// Cada fila se revela en num_steps pasos, con píxeles en orden aleatorio u ordenado (bisección).
|
||||
class PixelReveal {
|
||||
public:
|
||||
// Modo de revelado: aleatorio por fila o en orden de bisección (dithering ordenado 1D)
|
||||
enum class RevealMode { RANDOM,
|
||||
ORDERED };
|
||||
|
||||
// Constructor
|
||||
PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps = 4, bool reverse = false, RevealMode mode = RevealMode::RANDOM);
|
||||
|
||||
~PixelReveal() = default;
|
||||
|
||||
// Actualiza el estado del revelado según el tiempo transcurrido
|
||||
void update(float time_active);
|
||||
|
||||
// Dibuja la máscara de revelado en la posición indicada
|
||||
void render(int dst_x, int dst_y) const;
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
[[nodiscard]] auto isComplete() const -> bool;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Surface> cover_surface_; // Máscara negra que se va haciendo transparente
|
||||
std::vector<std::vector<int>> reveal_order_; // Orden de columnas por fila (aleatorio u ordenado por bisección)
|
||||
std::vector<int> row_step_; // Paso actual de revelado por fila (0..num_steps_)
|
||||
int width_;
|
||||
int height_;
|
||||
float pixels_per_second_; // Filas reveladas por segundo
|
||||
float step_duration_; // Segundos por paso dentro de una fila
|
||||
int num_steps_; // Número de pasos de revelado por fila
|
||||
bool reverse_; // Si true: transparente → negro (ocultar); si false: negro → transparente (revelar)
|
||||
RevealMode mode_; // Modo de revelado: aleatorio u ordenado por bisección
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "core/rendering/render_info.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para transform
|
||||
#include <cmath> // Para round, floor
|
||||
#include <iomanip> // Para setprecision
|
||||
#include <sstream> // Para ostringstream
|
||||
#include <string> // Para string
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "game/options.hpp" // Para Options
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/utils.hpp" // Para prettyName
|
||||
|
||||
// [SINGLETON]
|
||||
RenderInfo* RenderInfo::render_info = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void RenderInfo::init() {
|
||||
RenderInfo::render_info = new RenderInfo();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void RenderInfo::destroy() {
|
||||
delete RenderInfo::render_info;
|
||||
RenderInfo::render_info = nullptr;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto RenderInfo::get() -> RenderInfo* {
|
||||
return RenderInfo::render_info;
|
||||
}
|
||||
|
||||
// Constructor: en DEBUG se activa inmediatamente (notifica a Notifier del offset)
|
||||
RenderInfo::RenderInfo() {
|
||||
#ifdef _DEBUG
|
||||
toggle();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Actualiza la animación de entrada/salida del overlay
|
||||
void RenderInfo::update(float delta_time) {
|
||||
switch (status_) {
|
||||
case Status::RISING:
|
||||
y_ += SLIDE_SPEED * delta_time;
|
||||
if (y_ >= 0.0F) {
|
||||
y_ = 0.0F;
|
||||
status_ = Status::ACTIVE;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
y_ -= SLIDE_SPEED * delta_time;
|
||||
if (y_ <= static_cast<float>(-HEIGHT)) {
|
||||
y_ = static_cast<float>(-HEIGHT);
|
||||
status_ = Status::HIDDEN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza el overlay de información por pantalla
|
||||
void RenderInfo::render() const {
|
||||
if (status_ == Status::HIDDEN) { return; }
|
||||
|
||||
// FPS
|
||||
std::string line = std::to_string(Screen::get()->getLastFPS()) + " fps";
|
||||
|
||||
// Driver GPU
|
||||
const auto& driver = Screen::get()->getGPUDriver();
|
||||
line += " | " + (driver.empty() ? std::string("sdl") : driver);
|
||||
|
||||
// Zoom calculado (alto físico / alto lógico), con coma decimal y sin ceros innecesarios
|
||||
const float ROUNDED = std::round(Screen::get()->getZoomFactor() * 100.0F) / 100.0F;
|
||||
std::string zoom_str;
|
||||
if (ROUNDED == std::floor(ROUNDED)) {
|
||||
zoom_str = std::to_string(static_cast<int>(ROUNDED));
|
||||
} else {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(2) << ROUNDED;
|
||||
zoom_str = oss.str();
|
||||
if (zoom_str.back() == '0') { zoom_str.pop_back(); }
|
||||
std::ranges::replace(zoom_str, '.', ',');
|
||||
}
|
||||
line += " | " + zoom_str + "x";
|
||||
|
||||
// PostFX: muestra shader + preset y supersampling, o nada si está desactivado
|
||||
if (Options::video.shader.enabled) {
|
||||
const bool IS_CRTPI = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI);
|
||||
const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx";
|
||||
std::string preset_name = "-";
|
||||
if (IS_CRTPI) {
|
||||
if (!Options::crtpi_presets.empty()) {
|
||||
preset_name = prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name);
|
||||
}
|
||||
} else {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
preset_name = prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name);
|
||||
}
|
||||
}
|
||||
const bool SHOW_SS = Options::video.supersampling.enabled && !IS_CRTPI;
|
||||
line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : "");
|
||||
}
|
||||
|
||||
// Todo en lowercase
|
||||
std::ranges::transform(line, line.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Constantes visuales (igual que Console)
|
||||
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
|
||||
static constexpr Uint8 MSG_COLOR = 9; // PaletteColor::BRIGHT_GREEN
|
||||
static constexpr int TEXT_SIZE = 6;
|
||||
static constexpr int PADDING_V = (TEXT_SIZE / 2) - 1;
|
||||
|
||||
// Fuente: preferir la de la consola si está disponible
|
||||
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
|
||||
|
||||
// Posición Y: debajo de la consola + offset animado propio
|
||||
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
|
||||
const int Y = CONSOLE_Y + static_cast<int>(y_);
|
||||
|
||||
// Rectángulo de fondo: ancho completo, alto ajustado al texto
|
||||
const SDL_FRect RECT = {
|
||||
.x = 0.0F,
|
||||
.y = static_cast<float>(Y),
|
||||
.w = Options::game.width,
|
||||
.h = static_cast<float>(TEXT_SIZE + (PADDING_V * 2))};
|
||||
|
||||
auto game_surface = Screen::get()->getGameSurface();
|
||||
game_surface->fillRect(&RECT, BG_COLOR);
|
||||
// game_surface->drawRectBorder(&RECT, BORDER_COLOR);
|
||||
text_obj->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
static_cast<int>(Options::game.width / 2),
|
||||
Y + PADDING_V,
|
||||
line,
|
||||
1,
|
||||
MSG_COLOR);
|
||||
}
|
||||
|
||||
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset
|
||||
void RenderInfo::toggle() {
|
||||
switch (status_) {
|
||||
case Status::HIDDEN:
|
||||
status_ = Status::RISING;
|
||||
Screen::get()->updateZoomFactor();
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
|
||||
break;
|
||||
case Status::ACTIVE:
|
||||
status_ = Status::VANISHING;
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
class RenderInfo {
|
||||
public:
|
||||
// Singleton
|
||||
static void init();
|
||||
static void destroy();
|
||||
static auto get() -> RenderInfo*;
|
||||
|
||||
// Métodos principales
|
||||
void update(float delta_time);
|
||||
void render() const;
|
||||
void toggle();
|
||||
|
||||
// Consultas
|
||||
[[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; }
|
||||
|
||||
// Altura fija del overlay (TEXT_SIZE(6) + PADDING_V(2) * 2)
|
||||
static constexpr int HEIGHT = 10;
|
||||
static constexpr float SLIDE_SPEED = 120.0F;
|
||||
|
||||
private:
|
||||
enum class Status { HIDDEN,
|
||||
RISING,
|
||||
ACTIVE,
|
||||
VANISHING };
|
||||
|
||||
// Singleton
|
||||
static RenderInfo* render_info;
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
RenderInfo();
|
||||
~RenderInfo() = default;
|
||||
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{static_cast<float>(-HEIGHT)};
|
||||
};
|
||||
@@ -0,0 +1,750 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para max, min, transform
|
||||
#include <cctype> // Para toupper
|
||||
#include <cmath> // Para round, floor
|
||||
#include <cstring> // Para memcpy
|
||||
#include <fstream> // Para basic_ostream, operator<<, endl, basic_...
|
||||
#include <iostream> // Para cerr
|
||||
#include <iterator> // Para istreambuf_iterator, operator==
|
||||
#include <string> // Para char_traits, string, operator+, operator==
|
||||
|
||||
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
|
||||
// [SINGLETON]
|
||||
Screen* Screen::screen = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Screen::init() {
|
||||
Screen::screen = new Screen();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Screen::destroy() {
|
||||
delete Screen::screen;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Screen::get() -> Screen* {
|
||||
return Screen::screen;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Screen::Screen() {
|
||||
// Arranca SDL VIDEO, crea la ventana y el renderizador
|
||||
initSDLVideo();
|
||||
if (Options::video.fullscreen) { SDL_HideCursor(); }
|
||||
|
||||
// Calcular tamaños y hacer .resize() de los buffers de píxeles
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
|
||||
// Ajusta los tamaños
|
||||
game_surface_dstrect_ = {.x = Options::video.border.width, .y = Options::video.border.height, .w = Options::game.width, .h = Options::game.height};
|
||||
|
||||
// Define el color del borde para el modo de pantalla completa
|
||||
border_color_ = static_cast<Uint8>(PaletteColor::BLACK);
|
||||
|
||||
// Crea la textura donde se dibujan los graficos del juego
|
||||
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width, Options::game.height);
|
||||
if (game_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: game_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(game_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea la textura donde se dibuja el borde que rodea el area de juego
|
||||
border_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
if (border_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: border_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(border_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea las surfaces (PaletteManager aplicará la paleta inicial en su constructor)
|
||||
game_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
|
||||
game_surface_->clear(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
|
||||
border_surface_ = std::make_shared<Surface>(Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Crea el gestor de paletas; aplica la paleta inicial a ambas surfaces
|
||||
palette_manager_ = std::make_unique<PaletteManager>(
|
||||
Resource::List::get()->getListByType(Resource::List::Type::PALETTE),
|
||||
Options::video.palette,
|
||||
sortModeFromString(Options::video.palette_sort),
|
||||
game_surface_,
|
||||
border_surface_,
|
||||
[this]() {
|
||||
// Actualizar caché ARGB del borde cuando cambia la paleta
|
||||
if (border_is_solid_) {
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Cachear el color ARGB inicial del borde (borde sólido por defecto)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
|
||||
// Establece la surface que actuará como renderer para recibir las llamadas a render()
|
||||
renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
|
||||
// Crea el objeto de texto para la pantalla de carga
|
||||
createText();
|
||||
|
||||
// Renderizar una vez la textura vacía para que tenga contenido válido
|
||||
// antes de inicializar los shaders (evita pantalla negra)
|
||||
SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr);
|
||||
SDL_RenderTexture(renderer_, border_texture_, nullptr, nullptr);
|
||||
|
||||
// Ahora sí inicializar los shaders
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Screen::~Screen() {
|
||||
SDL_DestroyTexture(game_texture_);
|
||||
SDL_DestroyTexture(border_texture_);
|
||||
}
|
||||
|
||||
// Limpia el renderer
|
||||
void Screen::clearRenderer(Rgb color) {
|
||||
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
|
||||
SDL_RenderClear(renderer_);
|
||||
}
|
||||
|
||||
// Prepara para empezar a dibujar en la textura de juego
|
||||
void Screen::start() { setRendererSurface(nullptr); }
|
||||
|
||||
// Vuelca el contenido del renderizador en pantalla
|
||||
void Screen::render() {
|
||||
fps_.increment();
|
||||
|
||||
// Renderiza todos los overlays (escribe en game_surface_ CPU-side)
|
||||
renderOverlays();
|
||||
|
||||
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
|
||||
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
|
||||
if (!(shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||
surfaceToTexture();
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
textureToRenderer();
|
||||
}
|
||||
|
||||
// Establece el modo de video
|
||||
void Screen::setVideoMode(bool mode) {
|
||||
// Actualiza las opciones
|
||||
Options::video.fullscreen = mode;
|
||||
|
||||
// Configura el modo de pantalla y ajusta la ventana
|
||||
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||
SDL_SyncWindow(window_);
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Camibia entre pantalla completa y ventana
|
||||
void Screen::toggleVideoMode() {
|
||||
Options::video.fullscreen = !Options::video.fullscreen;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
}
|
||||
|
||||
// Reduce el tamaño de la ventana
|
||||
auto Screen::decWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||
--Options::window.zoom;
|
||||
Options::window.zoom = std::max(Options::window.zoom, 1);
|
||||
|
||||
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aumenta el tamaño de la ventana
|
||||
auto Screen::incWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
const int PREVIOUS_ZOOM = Options::window.zoom;
|
||||
++Options::window.zoom;
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
if (Options::window.zoom != PREVIOUS_ZOOM) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Establece el zoom directamente; false si fuera del rango [1, max_zoom] o en pantalla completa
|
||||
auto Screen::setWindowZoom(int zoom) -> bool {
|
||||
if (Options::video.fullscreen) { return false; }
|
||||
if (zoom < 1 || zoom > Options::window.max_zoom) { return false; }
|
||||
if (zoom == Options::window.zoom) { return false; }
|
||||
Options::window.zoom = zoom;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Devuelve el zoom máximo permitido según la pantalla actual
|
||||
auto Screen::getMaxZoom() -> int {
|
||||
return Options::window.max_zoom;
|
||||
}
|
||||
|
||||
// Cambia el color del borde
|
||||
void Screen::setBorderColor(Uint8 color) {
|
||||
border_color_ = color;
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Actualizar caché ARGB del borde sólido (ocurre una vez por habitación, no cada frame)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
border_is_solid_ = true;
|
||||
}
|
||||
|
||||
// Cambia entre borde visible y no visible
|
||||
void Screen::toggleBorder() {
|
||||
Options::video.border.enabled = !Options::video.border.enabled;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Dibuja las notificaciones
|
||||
void Screen::renderNotifications() const {
|
||||
if (notifications_enabled_) {
|
||||
Notifier::get()->render();
|
||||
}
|
||||
}
|
||||
|
||||
// Activa/desactiva todos los shaders respetando el shader actualmente seleccionado
|
||||
void Screen::toggleShaders() {
|
||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
if (Options::video.shader.enabled) {
|
||||
// Activar: usar el shader actualmente seleccionado
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
} else {
|
||||
// Desactivar: pass-through con POSTFX (pipeline sin efecto)
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
} else {
|
||||
// Backend no inicializado aún — inicializarlo ahora
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader del preset actual sin toggle
|
||||
void Screen::reloadPostFX() {
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
// El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline
|
||||
applyCurrentPostFXPreset();
|
||||
} else if (Options::video.shader.enabled) {
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader CrtPi del preset actual sin toggle
|
||||
void Screen::reloadCrtPi() {
|
||||
if (!shader_backend_) { return; }
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
|
||||
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
|
||||
void Screen::update(float delta_time) {
|
||||
fps_.calculate(SDL_GetTicks());
|
||||
Notifier::get()->update(delta_time);
|
||||
if (Console::get() != nullptr) {
|
||||
Console::get()->update(delta_time);
|
||||
}
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->update(delta_time); }
|
||||
Mouse::updateCursorVisibility();
|
||||
}
|
||||
|
||||
// Calcula el tamaño de la ventana
|
||||
void Screen::adjustWindowSize() {
|
||||
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
|
||||
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
|
||||
|
||||
// Reservamos memoria una sola vez.
|
||||
// Si el buffer es más pequeño que la superficie, crash asegurado.
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
|
||||
|
||||
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
|
||||
if (Options::video.border.enabled) {
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
}
|
||||
|
||||
// Lógica de centrado y redimensionado de ventana SDL
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
int old_w;
|
||||
int old_h;
|
||||
SDL_GetWindowSize(window_, &old_w, &old_h);
|
||||
int old_x;
|
||||
int old_y;
|
||||
SDL_GetWindowPosition(window_, &old_x, &old_y);
|
||||
|
||||
const int NEW_W = window_width_ * Options::window.zoom;
|
||||
const int NEW_H = window_height_ * Options::window.zoom;
|
||||
const int NEW_X = old_x + ((old_w - NEW_W) / 2);
|
||||
const int NEW_Y = old_y + ((old_h - NEW_H) / 2);
|
||||
|
||||
SDL_SetWindowSize(window_, NEW_W, NEW_H);
|
||||
|
||||
// En Wayland, SDL_SetWindowPosition es ignorado por el compositor (limitación de
|
||||
// protocolo: el compositor controla la posición de ventanas toplevel). Solo se
|
||||
// aplica en X11/Windows/macOS donde el posicionado funciona correctamente.
|
||||
// SDL_SyncWindow garantiza que el resize esté completado antes de reposicionar
|
||||
// (evita el race condition en X11).
|
||||
SDL_SyncWindow(window_);
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
const bool IS_WAYLAND = (driver != nullptr && SDL_strcmp(driver, "wayland") == 0);
|
||||
if (!IS_WAYLAND) {
|
||||
SDL_SetWindowPosition(window_, std::max(NEW_X, WINDOWS_DECORATIONS), std::max(NEW_Y, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ajusta el tamaño lógico del renderizador
|
||||
void Screen::adjustRenderLogicalSize() {
|
||||
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
}
|
||||
|
||||
// Recalcula y almacena el factor de zoom. Llamar solo cuando SDL ya ha estabilizado el estado de la ventana.
|
||||
// En ventana: Options::window.zoom (siempre entero).
|
||||
// En fullscreen: mínimo de las escalas en ambos ejes; floor si integer scale está activo.
|
||||
void Screen::updateZoomFactor() {
|
||||
if (!Options::video.fullscreen) {
|
||||
zoom_factor_ = static_cast<float>(Options::window.zoom);
|
||||
return;
|
||||
}
|
||||
if (window_width_ == 0 || window_height_ == 0) {
|
||||
zoom_factor_ = 1.0F;
|
||||
return;
|
||||
}
|
||||
int pw{0};
|
||||
int ph{0};
|
||||
SDL_GetRenderOutputSize(renderer_, &pw, &ph);
|
||||
const float SCALE = std::min(static_cast<float>(pw) / static_cast<float>(window_width_),
|
||||
static_cast<float>(ph) / static_cast<float>(window_height_));
|
||||
zoom_factor_ = Options::video.integer_scale ? std::floor(SCALE) : SCALE;
|
||||
}
|
||||
|
||||
// Establece el renderizador para las surfaces
|
||||
void Screen::setRendererSurface(const std::shared_ptr<Surface>& surface) {
|
||||
(surface) ? renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(surface) : renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
}
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::nextPalette() { palette_manager_->next(); }
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::previousPalette() { palette_manager_->previous(); }
|
||||
|
||||
// Copia la surface a la textura
|
||||
void Screen::surfaceToTexture() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (Options::video.border.enabled) {
|
||||
border_surface_->copyToTexture(renderer_, border_texture_);
|
||||
game_surface_->copyToTexture(renderer_, border_texture_, nullptr, &game_surface_dstrect_);
|
||||
} else {
|
||||
game_surface_->copyToTexture(renderer_, game_texture_);
|
||||
}
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
void Screen::textureToRenderer() {
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
const int GAME_W = Options::game.width;
|
||||
const int GAME_H = Options::game.height;
|
||||
|
||||
if (Options::video.border.enabled) {
|
||||
const int BORDER_W = window_width_;
|
||||
const int BORDER_H = window_height_;
|
||||
const int OFF_X = static_cast<int>(game_surface_dstrect_.x);
|
||||
const int OFF_Y = static_cast<int>(game_surface_dstrect_.y);
|
||||
|
||||
if (border_is_solid_) {
|
||||
// Path A: borde sólido (gameplay normal)
|
||||
// Rellena solo el marco con el color cacheado — sin lookups de paleta.
|
||||
// El área central (juego) se deja sin tocar; el overlay la sobreescribe igualmente.
|
||||
|
||||
// Franjas superior e inferior (ancho completo)
|
||||
std::fill_n(border_pixel_buffer_.data(), OFF_Y * BORDER_W, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(OFF_Y + GAME_H) * BORDER_W],
|
||||
(BORDER_H - OFF_Y - GAME_H) * BORDER_W,
|
||||
border_argb_color_);
|
||||
// Columnas laterales en las filas del área de juego
|
||||
for (int y = OFF_Y; y < OFF_Y + GAME_H; ++y) {
|
||||
std::fill_n(&border_pixel_buffer_[y * BORDER_W], OFF_X, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(y * BORDER_W) + OFF_X + GAME_W],
|
||||
BORDER_W - OFF_X - GAME_W,
|
||||
border_argb_color_);
|
||||
}
|
||||
} else {
|
||||
// Path B: borde dinámico (escena de carga — bandas de colores animadas)
|
||||
// Conversión completa: la escena modifica border_surface_ cada frame
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
}
|
||||
|
||||
// Overlay del juego sobre el centro del buffer (ambos paths)
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
for (int y = 0; y < GAME_H; ++y) {
|
||||
const Uint32* src = &game_pixel_buffer_[y * GAME_W];
|
||||
Uint32* dst = &border_pixel_buffer_[((OFF_Y + y) * BORDER_W) + OFF_X];
|
||||
std::memcpy(dst, src, GAME_W * sizeof(Uint32));
|
||||
}
|
||||
|
||||
shader_backend_->uploadPixels(border_pixel_buffer_.data(), BORDER_W, BORDER_H);
|
||||
} else {
|
||||
// Caso sin borde: subida directa simplificada
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
shader_backend_->uploadPixels(game_pixel_buffer_.data(), GAME_W, GAME_H);
|
||||
}
|
||||
|
||||
shader_backend_->render();
|
||||
} else {
|
||||
// Fallback SDL_Renderer (mantiene tu lógica de texturas SDL)
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
SDL_SetRenderTarget(renderer_, nullptr);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, tex, nullptr, nullptr);
|
||||
SDL_RenderPresent(renderer_);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza todos los overlays (orden: último dibujado queda encima)
|
||||
void Screen::renderOverlays() {
|
||||
renderNotifications(); // Notifier (abajo)
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->render(); } // RenderInfo (medio)
|
||||
if (Console::get() != nullptr) { Console::get()->render(); } // Console (encima)
|
||||
}
|
||||
|
||||
// Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe
|
||||
auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); }
|
||||
|
||||
// Devuelve los nombres de paletas disponibles (minúsculas, sin extensión .pal)
|
||||
auto Screen::getPaletteNames() const -> std::vector<std::string> { return palette_manager_->getNames(); }
|
||||
auto Screen::getPalettePrettyName() const -> std::string { return palette_manager_->getPrettyName(); }
|
||||
void Screen::nextPaletteSortMode() { palette_manager_->nextSortMode(); }
|
||||
void Screen::setPaletteSortMode(PaletteSortMode mode) { palette_manager_->setSortMode(mode); }
|
||||
auto Screen::getPaletteSortModeName() const -> std::string { return palette_manager_->getSortModeName(); }
|
||||
|
||||
// Limpia la game_surface_
|
||||
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderWidth(int width) { Options::video.border.width = width; }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderHeight(int height) { Options::video.border.height = height; }
|
||||
|
||||
// Establece si se ha de ver el borde en el modo ventana
|
||||
void Screen::setBorderEnabled(bool value) { Options::video.border.enabled = value; }
|
||||
|
||||
// Muestra la ventana
|
||||
void Screen::show() { SDL_ShowWindow(window_); }
|
||||
|
||||
// Oculta la ventana
|
||||
void Screen::hide() { SDL_HideWindow(window_); }
|
||||
|
||||
// Establece la visibilidad de las notificaciones
|
||||
void Screen::setNotificationsEnabled(bool value) { notifications_enabled_ = value; }
|
||||
|
||||
// Alterna entre activar y desactivar el escalado entero
|
||||
void Screen::toggleIntegerScale() {
|
||||
Options::video.integer_scale = !Options::video.integer_scale;
|
||||
SDL_SetRenderLogicalPresentation(renderer_, Options::game.width, Options::game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
}
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Alterna entre activar y desactivar el V-Sync
|
||||
void Screen::toggleVSync() {
|
||||
Options::video.vertical_sync = !Options::video.vertical_sync;
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
auto Screen::getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||
auto Screen::getRendererSurface() -> std::shared_ptr<Surface> { return (*renderer_surface_); }
|
||||
auto Screen::getGameSurface() -> std::shared_ptr<Surface> { return game_surface_; }
|
||||
auto Screen::getBorderSurface() -> std::shared_ptr<Surface> {
|
||||
border_is_solid_ = false; // Modificación externa → modo borde dinámico
|
||||
return border_surface_;
|
||||
}
|
||||
|
||||
auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
// Load using ResourceHelper (supports both filesystem and pack)
|
||||
return Resource::Helper::loadFile(filepath);
|
||||
}
|
||||
|
||||
void Screen::setLinearUpscale(bool linear) {
|
||||
Options::video.supersampling.linear_upscale = linear;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setLinearUpscale(linear);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::setDownscaleAlgo(int algo) {
|
||||
Options::video.supersampling.downscale_algo = algo;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setDownscaleAlgo(algo);
|
||||
}
|
||||
}
|
||||
|
||||
auto Screen::getSsTextureSize() const -> std::pair<int, int> {
|
||||
if (!shader_backend_) { return {0, 0}; }
|
||||
return shader_backend_->getSsTextureSize();
|
||||
}
|
||||
|
||||
// Activa/desactiva el supersampling global (Ctrl+F4)
|
||||
void Screen::toggleSupersampling() {
|
||||
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset actual al backend de shaders
|
||||
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::postfx_presets.empty()) {
|
||||
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)];
|
||||
// Supersampling es un toggle global (Options::video.supersampling.enabled), no por preset.
|
||||
// setOversample primero: puede recrear texturas antes de que setPostFXParams
|
||||
// decida si hornear scanlines en CPU o aplicarlas en GPU.
|
||||
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
|
||||
shader_backend_->setPostFXParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset CrtPi actual al backend de shaders
|
||||
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::crtpi_presets.empty()) {
|
||||
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
|
||||
Rendering::CrtPiParams params{
|
||||
.scanline_weight = p.scanline_weight,
|
||||
.scanline_gap_brightness = p.scanline_gap_brightness,
|
||||
.bloom_factor = p.bloom_factor,
|
||||
.input_gamma = p.input_gamma,
|
||||
.output_gamma = p.output_gamma,
|
||||
.mask_brightness = p.mask_brightness,
|
||||
.curvature_x = p.curvature_x,
|
||||
.curvature_y = p.curvature_y,
|
||||
.mask_type = p.mask_type,
|
||||
.enable_scanlines = p.enable_scanlines,
|
||||
.enable_multisample = p.enable_multisample,
|
||||
.enable_gamma = p.enable_gamma,
|
||||
.enable_curvature = p.enable_curvature,
|
||||
.enable_sharper = p.enable_sharper,
|
||||
};
|
||||
shader_backend_->setCrtPiParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Cambia el shader de post-procesado activo y aplica el preset correspondiente
|
||||
void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||
Options::video.shader.current_shader = type;
|
||||
if (!shader_backend_) { return; }
|
||||
if (!Options::video.shader.enabled) {
|
||||
// Shaders desactivados: guardar preferencia pero mantener pass-through
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
return;
|
||||
}
|
||||
shader_backend_->setActiveShader(type);
|
||||
if (type == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Cicla al siguiente shader disponible (preparado para futura UI)
|
||||
void Screen::nextShader() {
|
||||
const Rendering::ShaderType NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
|
||||
? Rendering::ShaderType::CRTPI
|
||||
: Rendering::ShaderType::POSTFX;
|
||||
setActiveShader(NEXT);
|
||||
}
|
||||
|
||||
// Inicializa los shaders
|
||||
// El device GPU se crea siempre (independientemente de postfx) para evitar
|
||||
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
|
||||
void Screen::initShaders() {
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
|
||||
if (!shader_backend_) {
|
||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
const std::string FALLBACK_DRIVER = "none";
|
||||
shader_backend_->setPreferredDriver(Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
|
||||
}
|
||||
shader_backend_->init(window_, tex, "", "");
|
||||
gpu_driver_ = shader_backend_->getDriverName();
|
||||
|
||||
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
|
||||
|
||||
if (Options::video.shader.enabled) {
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
// Pass-through: todos los efectos a 0, el shader solo copia la textura
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
|
||||
// Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica)
|
||||
shader_backend_->setActiveShader(Options::video.shader.current_shader);
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene información sobre la pantalla
|
||||
void Screen::getDisplayInfo() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n** VIDEO SYSTEM **\n";
|
||||
|
||||
int num_displays = 0;
|
||||
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
||||
if (displays != nullptr) {
|
||||
for (int i = 0; i < num_displays; ++i) {
|
||||
SDL_DisplayID instance_id = displays[i];
|
||||
const char* name = SDL_GetDisplayName(instance_id);
|
||||
|
||||
std::cout << "Display " << instance_id << ": " << ((name != nullptr) ? name : "Unknown") << '\n';
|
||||
}
|
||||
|
||||
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
||||
|
||||
// Guarda información del monitor en display_monitor_
|
||||
const char* first_display_name = SDL_GetDisplayName(displays[0]);
|
||||
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
|
||||
display_monitor_.width = static_cast<int>(dm->w);
|
||||
display_monitor_.height = static_cast<int>(dm->h);
|
||||
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
|
||||
|
||||
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||
Options::window.max_zoom = std::min(dm->w / Options::game.width, dm->h / Options::game.height);
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
|
||||
std::cout << "Current display mode: " << static_cast<int>(dm->w) << "x" << static_cast<int>(dm->h) << " @ " << static_cast<int>(dm->refresh_rate) << "Hz\n";
|
||||
|
||||
std::cout << "Window resolution: " << static_cast<int>(Options::game.width) << "x" << static_cast<int>(Options::game.height) << " x" << Options::window.zoom << '\n';
|
||||
|
||||
Options::video.info = std::to_string(static_cast<int>(dm->w)) + "x" +
|
||||
std::to_string(static_cast<int>(dm->h)) + " @ " +
|
||||
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
|
||||
|
||||
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
|
||||
const int MAX_ZOOM = std::min(dm->w / Options::game.width, (dm->h - WINDOWS_DECORATIONS) / Options::game.height);
|
||||
|
||||
// Normaliza los valores de zoom
|
||||
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
|
||||
|
||||
SDL_free(displays);
|
||||
}
|
||||
}
|
||||
|
||||
// Arranca SDL VIDEO y crea la ventana
|
||||
auto Screen::initSDLVideo() -> bool {
|
||||
// Inicializar SDL
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::cerr << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Obtener información de la pantalla
|
||||
getDisplayInfo();
|
||||
|
||||
// Configurar hint para renderizado
|
||||
#ifdef __APPLE__
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
|
||||
std::cout << "WARNING: Failed to set Metal hint!\n";
|
||||
}
|
||||
#endif
|
||||
|
||||
// Crear ventana
|
||||
const auto WINDOW_WIDTH = Options::video.border.enabled ? Options::game.width + (Options::video.border.width * 2) : Options::game.width;
|
||||
const auto WINDOW_HEIGHT = Options::video.border.enabled ? Options::game.height + (Options::video.border.height * 2) : Options::game.height;
|
||||
SDL_WindowFlags window_flags = 0;
|
||||
if (Options::video.fullscreen) {
|
||||
window_flags |= SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
window_ = SDL_CreateWindow(Options::window.caption.c_str(), WINDOW_WIDTH * Options::window.zoom, WINDOW_HEIGHT * Options::window.zoom, window_flags);
|
||||
|
||||
if (window_ == nullptr) {
|
||||
std::cerr << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Crear renderer
|
||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||
if (renderer_ == nullptr) {
|
||||
std::cerr << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configurar renderer
|
||||
const int EXTRA_WIDTH = Options::video.border.enabled ? Options::video.border.width * 2 : 0;
|
||||
const int EXTRA_HEIGHT = Options::video.border.enabled ? Options::video.border.height * 2 : 0;
|
||||
SDL_SetRenderLogicalPresentation(
|
||||
renderer_,
|
||||
Options::game.width + EXTRA_WIDTH,
|
||||
Options::game.height + EXTRA_HEIGHT,
|
||||
Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
|
||||
std::cout << "Video system initialized successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Crea el objeto de texto
|
||||
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Carga la surface de la fuente directamente del archivo
|
||||
auto surface = std::make_shared<Surface>(Resource::List::get()->get("aseprite.gif"));
|
||||
|
||||
// Crea el objeto de texto (el constructor de Text carga el archivo text_file internamente)
|
||||
text_ = std::make_shared<Text>(surface, Resource::List::get()->get("aseprite.fnt"));
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_pixels.h> // Para Uint32
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para std::pair
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/palette_manager.hpp" // Para PaletteManager
|
||||
#include "core/rendering/shader_backend.hpp" // Para Rendering::ShaderType, ShaderBackend
|
||||
#include "utils/utils.hpp" // Para Color
|
||||
class Surface;
|
||||
class Text;
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
// Tipos de filtro
|
||||
enum class Filter : Uint32 {
|
||||
NEAREST = 0,
|
||||
LINEAR = 1,
|
||||
};
|
||||
|
||||
// Singleton
|
||||
static void init(); // Crea el singleton
|
||||
static void destroy(); // Destruye el singleton
|
||||
static auto get() -> Screen*; // Obtiene el singleton
|
||||
|
||||
// Renderizado
|
||||
void clearRenderer(Rgb color = {0x00, 0x00, 0x00}); // Limpia el renderer
|
||||
void clearSurface(Uint8 index); // Limpia la game_surface_
|
||||
void start(); // Prepara para empezar a dibujar en la textura de juego
|
||||
void render(); // Vuelca el contenido del renderizador en pantalla
|
||||
void update(float delta_time); // Actualiza la lógica de la clase
|
||||
|
||||
// Video y ventana
|
||||
void setVideoMode(bool mode); // Establece el modo de video
|
||||
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
|
||||
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
|
||||
auto incWindowZoom() -> bool; // Aumenta el tamaño de la ventana
|
||||
auto setWindowZoom(int zoom) -> bool; // Establece zoom directo; false si fuera de [1, max_zoom]
|
||||
void show(); // Muestra la ventana
|
||||
void hide(); // Oculta la ventana
|
||||
|
||||
// Borde
|
||||
void setBorderColor(Uint8 color); // Cambia el color del borde
|
||||
static void setBorderWidth(int width); // Establece el ancho del borde
|
||||
static void setBorderHeight(int height); // Establece el alto del borde
|
||||
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
|
||||
void toggleBorder(); // Cambia entre borde visible y no visible
|
||||
|
||||
// Paletas y PostFX
|
||||
void nextPalette(); // Cambia a la siguiente paleta
|
||||
void previousPalette(); // Cambia a la paleta anterior
|
||||
auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPalettePrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
void nextPaletteSortMode(); // Cicla al siguiente modo de ordenación de paleta
|
||||
void setPaletteSortMode(PaletteSortMode mode); // Establece modo de ordenación concreto
|
||||
[[nodiscard]] auto getPaletteSortModeName() const -> std::string; // Nombre del modo de ordenación actual
|
||||
void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
|
||||
void toggleSupersampling(); // Activa/desactiva el supersampling global
|
||||
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
|
||||
void reloadCrtPi(); // Recarga el shader CrtPi del preset actual sin toggle
|
||||
void setLinearUpscale(bool linear); // Upscale NEAREST (false) o LINEAR (true) en el paso SS
|
||||
void setDownscaleAlgo(int algo); // 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setActiveShader(Rendering::ShaderType type); // Cambia el shader de post-procesado activo
|
||||
void nextShader(); // Cicla al siguiente shader disponible (para futura UI)
|
||||
|
||||
// Surfaces y notificaciones
|
||||
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
|
||||
void setNotificationsEnabled(bool value); // Establece la visibilidad de las notificaciones
|
||||
void updateZoomFactor(); // Recalcula y almacena el factor de zoom real
|
||||
|
||||
// Getters
|
||||
auto getRenderer() -> SDL_Renderer*;
|
||||
auto getRendererSurface() -> std::shared_ptr<Surface>;
|
||||
auto getBorderSurface() -> std::shared_ptr<Surface>;
|
||||
auto getGameSurface() -> std::shared_ptr<Surface>;
|
||||
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
|
||||
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
|
||||
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); }
|
||||
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
||||
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
|
||||
[[nodiscard]] static auto getMaxZoom() -> int;
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int>;
|
||||
|
||||
private:
|
||||
// Estructuras
|
||||
struct DisplayMonitor {
|
||||
std::string name;
|
||||
int width{0};
|
||||
int height{0};
|
||||
int refresh_rate{0};
|
||||
};
|
||||
|
||||
struct FPS {
|
||||
Uint32 ticks{0}; // Tiempo en milisegundos desde que se comenzó a contar
|
||||
int frame_count{0}; // Número acumulado de frames en el intervalo
|
||||
int last_value{0}; // Número de frames calculado en el último segundo
|
||||
|
||||
void increment() {
|
||||
frame_count++;
|
||||
}
|
||||
|
||||
auto calculate(Uint32 current_ticks) -> int {
|
||||
if (current_ticks - ticks >= 1000) {
|
||||
last_value = frame_count;
|
||||
frame_count = 0;
|
||||
ticks = current_ticks;
|
||||
}
|
||||
return last_value;
|
||||
}
|
||||
};
|
||||
|
||||
// Constantes
|
||||
static constexpr int WINDOWS_DECORATIONS = 35; // Decoraciones de la ventana
|
||||
|
||||
// Singleton
|
||||
static Screen* screen;
|
||||
|
||||
// Métodos privados
|
||||
void renderNotifications() const; // Dibuja las notificaciones
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
|
||||
void surfaceToTexture(); // Copia la surface a la textura
|
||||
void textureToRenderer(); // Copia la textura al renderizador
|
||||
void renderOverlays(); // Renderiza todos los overlays
|
||||
void initShaders(); // Inicializa los shaders
|
||||
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void createText(); // Crea el objeto de texto
|
||||
|
||||
// Constructor y destructor
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
// Objetos SDL
|
||||
SDL_Window* window_{nullptr}; // Ventana de la aplicación
|
||||
SDL_Renderer* renderer_{nullptr}; // Renderizador de la ventana
|
||||
SDL_Texture* game_texture_{nullptr}; // Textura donde se dibuja el juego
|
||||
SDL_Texture* border_texture_{nullptr}; // Textura donde se dibuja el borde del juego
|
||||
|
||||
// Surfaces y renderizado
|
||||
std::shared_ptr<Surface> game_surface_; // Surface principal del juego
|
||||
std::shared_ptr<Surface> border_surface_; // Surface para el borde de la pantalla
|
||||
std::shared_ptr<std::shared_ptr<Surface>> renderer_surface_; // Puntero a la Surface activa
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (OpenGL/Metal/Vulkan)
|
||||
std::shared_ptr<Text> text_; // Objeto para escribir texto
|
||||
|
||||
// Buffers persistentes para evitar .resize() cada frame
|
||||
std::vector<Uint32> game_pixel_buffer_; // Textura de juego
|
||||
std::vector<Uint32> border_pixel_buffer_; // Textura de borde (composición final borde+juego)
|
||||
|
||||
// Caché del borde sólido (gameplay normal)
|
||||
bool border_is_solid_{true}; // true = borde de color sólido; false = borde dinámico (carga)
|
||||
Uint32 border_argb_color_{0}; // Color ARGB pre-convertido del borde sólido
|
||||
|
||||
// Configuración de ventana y pantalla
|
||||
int window_width_{0}; // Ancho de la pantalla o ventana
|
||||
int window_height_{0}; // Alto de la pantalla o ventana
|
||||
float zoom_factor_{1.0F}; // Factor de zoom calculado (alto físico / alto lógico)
|
||||
SDL_FRect game_surface_dstrect_; // Coordenadas donde se dibuja la textura del juego
|
||||
|
||||
// Paletas y colores
|
||||
Uint8 border_color_{0}; // Color del borde
|
||||
std::unique_ptr<PaletteManager> palette_manager_; // Gestor de paletas de color
|
||||
|
||||
// Estado y configuración
|
||||
bool notifications_enabled_{false}; // Indica si se muestran las notificaciones
|
||||
FPS fps_; // Gestor de frames por segundo
|
||||
DisplayMonitor display_monitor_; // Información de la pantalla
|
||||
|
||||
// Shaders
|
||||
std::string info_resolution_; // Texto con la información de la pantalla
|
||||
std::string gpu_driver_; // Nombre del driver GPU (SDL3GPU), capturado en initShaders()
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||
float scanline_strength; // 0 = off, 1 = full
|
||||
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
|
||||
struct CrtPiUniforms {
|
||||
// vec4 #0
|
||||
float scanline_weight; // Ajuste gaussiano (default 6.0)
|
||||
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
|
||||
float input_gamma; // Gamma de entrada (default 2.4)
|
||||
// vec4 #1
|
||||
float output_gamma; // Gamma de salida (default 2.2)
|
||||
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
|
||||
float curvature_x; // Distorsión barrel X (default 0.05)
|
||||
float curvature_y; // Distorsión barrel Y (default 0.10)
|
||||
// vec4 #2
|
||||
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
int enable_scanlines; // 0 = off, 1 = on
|
||||
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
|
||||
int enable_gamma; // 0 = off, 1 = on
|
||||
// vec4 #3
|
||||
int enable_curvature; // 0 = off, 1 = on
|
||||
int enable_sharper; // 0 = off, 1 = on
|
||||
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
|
||||
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||
};
|
||||
|
||||
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
};
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/**
|
||||
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
|
||||
*
|
||||
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
|
||||
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||
* → PostFX render pass → swapchain → present
|
||||
*/
|
||||
class SDL3GPUShader : public ShaderBackend {
|
||||
public:
|
||||
SDL3GPUShader() = default;
|
||||
~SDL3GPUShader() override;
|
||||
|
||||
auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool override;
|
||||
|
||||
void render() override;
|
||||
void setTextureSize(float width, float height) override {}
|
||||
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
||||
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
|
||||
|
||||
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
|
||||
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||
|
||||
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
|
||||
void uploadPixels(const Uint32* pixels, int width, int height) override;
|
||||
|
||||
// Actualiza los parámetros de intensidad de los efectos PostFX
|
||||
void setPostFXParams(const PostFXParams& p) override;
|
||||
|
||||
// Activa/desactiva VSync en el swapchain
|
||||
void setVSync(bool vsync) override;
|
||||
|
||||
// Activa/desactiva escalado entero (integer scale)
|
||||
void setScaleMode(bool integer_scale) override;
|
||||
|
||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||
void setOversample(int factor) override;
|
||||
|
||||
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
|
||||
void setLinearUpscale(bool linear) override;
|
||||
|
||||
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
|
||||
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||
void setActiveShader(ShaderType type) override;
|
||||
|
||||
// Actualiza los parámetros del shader CRT-Pi
|
||||
void setCrtPiParams(const CrtPiParams& p) override;
|
||||
|
||||
// Devuelve el shader activo
|
||||
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
|
||||
|
||||
private:
|
||||
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
static auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||
const uint8_t* spv_code,
|
||||
size_t spv_size,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
auto createPipeline() -> bool;
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
||||
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
|
||||
|
||||
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
|
||||
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
|
||||
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
|
||||
|
||||
int game_width_ = 0; // Dimensiones originales del canvas
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
bool vsync_ = true;
|
||||
bool integer_scale_ = false;
|
||||
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
@@ -0,0 +1,633 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
static const uint8_t kupscale_frag_spv[] = {
|
||||
0x03,
|
||||
0x02,
|
||||
0x23,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x14,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x53,
|
||||
0x4c,
|
||||
0x2e,
|
||||
0x73,
|
||||
0x74,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x34,
|
||||
0x35,
|
||||
0x30,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xc2,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x70,
|
||||
0x70,
|
||||
0x5f,
|
||||
0x73,
|
||||
0x74,
|
||||
0x79,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x6c,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x63,
|
||||
0x6c,
|
||||
0x75,
|
||||
0x64,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6f,
|
||||
0x75,
|
||||
0x74,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6c,
|
||||
0x6f,
|
||||
0x72,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x73,
|
||||
0x63,
|
||||
0x65,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x76,
|
||||
0x5f,
|
||||
0x75,
|
||||
0x76,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x22,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1b,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x36,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xf8,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x57,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xfd,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x38,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00};
|
||||
static const size_t kupscale_frag_spv_size = 628;
|
||||
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/** @brief Identificador del shader de post-procesado activo */
|
||||
enum class ShaderType { POSTFX,
|
||||
CRTPI };
|
||||
|
||||
/**
|
||||
* @brief Parámetros de intensidad de los efectos PostFX
|
||||
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
|
||||
*/
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F; // Intensidad de la viñeta
|
||||
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||
float chroma = 0.0F; // Aberración cromática
|
||||
float mask = 0.0F; // Máscara de fósforo RGB
|
||||
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||
float curvature = 0.0F; // Curvatura barrel CRT
|
||||
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
|
||||
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
|
||||
*/
|
||||
struct CrtPiParams {
|
||||
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
|
||||
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
|
||||
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
|
||||
float input_gamma{2.4F}; // Gamma de entrada (linealización)
|
||||
float output_gamma{2.2F}; // Gamma de salida (codificación)
|
||||
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
|
||||
float curvature_x{0.05F}; // Distorsión barrel eje X
|
||||
float curvature_y{0.10F}; // Distorsión barrel eje Y
|
||||
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
bool enable_scanlines{true}; // Activar efecto de scanlines
|
||||
bool enable_multisample{true}; // Antialiasing analítico de scanlines
|
||||
bool enable_gamma{true}; // Corrección gamma
|
||||
bool enable_curvature{false}; // Distorsión barrel CRT
|
||||
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Interfaz abstracta para backends de renderizado con shaders
|
||||
*
|
||||
* Esta interfaz define el contrato que todos los backends de shaders
|
||||
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
|
||||
*/
|
||||
class ShaderBackend {
|
||||
public:
|
||||
virtual ~ShaderBackend() = default;
|
||||
|
||||
/**
|
||||
* @brief Inicializa el backend de shaders
|
||||
* @param window Ventana SDL
|
||||
* @param texture Textura de backbuffer a la que aplicar shaders
|
||||
* @param vertex_source Código fuente del vertex shader
|
||||
* @param fragment_source Código fuente del fragment shader
|
||||
* @return true si la inicialización fue exitosa
|
||||
*/
|
||||
virtual auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Renderiza la textura con los shaders aplicados
|
||||
*/
|
||||
virtual void render() = 0;
|
||||
|
||||
/**
|
||||
* @brief Establece el tamaño de la textura como parámetro del shader
|
||||
* @param width Ancho de la textura
|
||||
* @param height Alto de la textura
|
||||
*/
|
||||
virtual void setTextureSize(float width, float height) = 0;
|
||||
|
||||
/**
|
||||
* @brief Limpia y libera recursos del backend
|
||||
*/
|
||||
virtual void cleanup() = 0;
|
||||
|
||||
/**
|
||||
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
|
||||
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
|
||||
*/
|
||||
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros de intensidad de los efectos PostFX
|
||||
* @param p Struct con todos los parámetros PostFX
|
||||
*/
|
||||
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva VSync en el swapchain del GPU device
|
||||
*/
|
||||
virtual void setVSync(bool /*vsync*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva el escalado entero (integer scale)
|
||||
*/
|
||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||
*/
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
|
||||
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
|
||||
*/
|
||||
virtual void setLinearUpscale(bool /*linear*/) {}
|
||||
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
|
||||
|
||||
/**
|
||||
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
|
||||
*/
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
/**
|
||||
* @brief Devuelve las dimensiones de la textura de supersampling.
|
||||
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
/**
|
||||
* @brief Verifica si el backend está usando aceleración por hardware
|
||||
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||
*/
|
||||
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
|
||||
* @return Cadena vacía si no disponible
|
||||
*/
|
||||
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
|
||||
|
||||
/**
|
||||
* @brief Establece el driver GPU preferido antes de init().
|
||||
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
|
||||
*/
|
||||
virtual void setPreferredDriver(const std::string& /*driver*/) {}
|
||||
|
||||
/**
|
||||
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
|
||||
* Debe llamarse antes de render(). No recrea pipelines.
|
||||
*/
|
||||
virtual void setActiveShader(ShaderType /*type*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros del shader CRT-Pi.
|
||||
*/
|
||||
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Devuelve el shader de post-procesado activo.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
@@ -0,0 +1,344 @@
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <fstream> // Para basic_ostream, basic_istream, operator<<, basic...
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <sstream> // Para basic_stringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "utils/utils.hpp" // Para printWithDots
|
||||
|
||||
// Helper: Convierte un nodo YAML de frames (array) a vector de SDL_FRect
|
||||
auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
||||
std::vector<SDL_FRect> frames;
|
||||
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
||||
|
||||
for (const auto& frame_index_node : frames_node) {
|
||||
const int NUM_TILE = frame_index_node.get_value<int>();
|
||||
if (NUM_TILE <= max_tiles) {
|
||||
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||
frames.emplace_back(rect);
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
// Carga las animaciones desde un fichero YAML
|
||||
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<AnimationData> animations;
|
||||
|
||||
// Extract filename for logging
|
||||
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||
|
||||
try {
|
||||
// Load YAML file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Unable to load animation file " << FILE_NAME << '\n';
|
||||
throw std::runtime_error("Animation file not found: " + file_path);
|
||||
}
|
||||
|
||||
printWithDots("Animation : ", FILE_NAME, "[ LOADED ]");
|
||||
|
||||
// Parse YAML from string
|
||||
std::string yaml_content(file_data.begin(), file_data.end());
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
surface = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface) {
|
||||
frames_per_row = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
// Constructor con bytes YAML del cache (parsing lazy)
|
||||
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
// Parsear YAML desde los bytes cargados en cache
|
||||
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// Variables para almacenar configuración global
|
||||
float frame_width = 0.0F;
|
||||
float frame_height = 0.0F;
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
// Ahora SÍ podemos acceder al cache (ya está completamente cargado)
|
||||
surface_ = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface_) {
|
||||
frames_per_row = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface_->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations_.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
// Set dimensions
|
||||
setWidth(frame_width);
|
||||
setHeight(frame_height);
|
||||
|
||||
// Inicializar con la primera animación si existe
|
||||
if (!animations_.empty() && !animations_[0].frames.empty()) {
|
||||
setClip(animations_[0].frames[0]);
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor per a subclasses amb surface directa (sense YAML)
|
||||
AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: MovingSprite(std::move(surface), pos) {
|
||||
// animations_ queda buit (protegit per el guard de animate())
|
||||
if (surface_) {
|
||||
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene el indice de la animación a partir del nombre
|
||||
auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto index = -1;
|
||||
|
||||
for (const auto& a : animations_) {
|
||||
index++;
|
||||
if (a.name == name) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calcula el frame correspondiente a la animación (time-based)
|
||||
void AnimatedSprite::animate(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (animations_.empty()) { return; }
|
||||
if (animations_[current_animation_].speed <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acumula el tiempo transcurrido
|
||||
animations_[current_animation_].accumulated_time += delta_time;
|
||||
|
||||
// Calcula el frame actual a partir del tiempo acumulado
|
||||
const int TARGET_FRAME = static_cast<int>(
|
||||
animations_[current_animation_].accumulated_time /
|
||||
animations_[current_animation_].speed);
|
||||
|
||||
// Si alcanza el final de la animación, maneja el loop
|
||||
if (TARGET_FRAME >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
if (animations_[current_animation_].loop == -1) {
|
||||
// Si no hay loop, congela en el último frame
|
||||
animations_[current_animation_].current_frame =
|
||||
static_cast<int>(animations_[current_animation_].frames.size()) - 1;
|
||||
animations_[current_animation_].completed = true;
|
||||
|
||||
// Establece el clip del último frame
|
||||
if (animations_[current_animation_].current_frame >= 0) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Si hay loop, vuelve al frame indicado
|
||||
animations_[current_animation_].accumulated_time =
|
||||
static_cast<float>(animations_[current_animation_].loop) *
|
||||
animations_[current_animation_].speed;
|
||||
animations_[current_animation_].current_frame = animations_[current_animation_].loop;
|
||||
|
||||
// Establece el clip del frame de loop
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Actualiza el frame actual
|
||||
animations_[current_animation_].current_frame = TARGET_FRAME;
|
||||
|
||||
// Establece el clip del frame actual
|
||||
if (animations_[current_animation_].current_frame >= 0 &&
|
||||
animations_[current_animation_].current_frame <
|
||||
static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si ha terminado la animación
|
||||
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||
return animations_[current_animation_].completed;
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
|
||||
const auto NEW_ANIMATION = getIndex(name);
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||
const auto NEW_ANIMATION = index;
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto (time-based)
|
||||
void AnimatedSprite::update(float delta_time) {
|
||||
animate(delta_time);
|
||||
MovingSprite::update(delta_time);
|
||||
}
|
||||
|
||||
// Reinicia la animación
|
||||
void AnimatedSprite::resetAnimation() {
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
}
|
||||
|
||||
// Establece el frame actual de la animación
|
||||
void AnimatedSprite::setCurrentAnimationFrame(int num) {
|
||||
// Descarta valores fuera de rango
|
||||
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
num = 0;
|
||||
}
|
||||
|
||||
// Cambia el valor de la variable
|
||||
animations_[current_animation_].current_frame = num;
|
||||
|
||||
// Escoge el frame correspondiente de la animación
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/sprite/moving_sprite.hpp" // Para SMovingSprite
|
||||
#include "core/resources/resource_types.hpp" // Para AnimationResource
|
||||
|
||||
class Surface;
|
||||
|
||||
class AnimatedSprite : public MovingSprite {
|
||||
public:
|
||||
using Animations = std::vector<std::string>; // Tipo para lista de animaciones
|
||||
|
||||
// Estructura pública de datos de animación
|
||||
struct AnimationData {
|
||||
std::string name; // Nombre de la animacion
|
||||
std::vector<SDL_FRect> frames; // Cada uno de los frames que componen la animación
|
||||
float speed{0.083F}; // Velocidad de la animación (segundos por frame)
|
||||
int loop{0}; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
||||
bool completed{false}; // Indica si ha finalizado la animación
|
||||
int current_frame{0}; // Frame actual
|
||||
float accumulated_time{0.0F}; // Tiempo acumulado para las animaciones (time-based)
|
||||
};
|
||||
|
||||
// Métodos estáticos
|
||||
static auto loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData>; // Carga las animaciones desde fichero YAML
|
||||
|
||||
// Constructores
|
||||
explicit AnimatedSprite(const AnimationResource& cached_data); // Constructor con datos pre-cargados del cache
|
||||
|
||||
~AnimatedSprite() override = default; // Destructor
|
||||
|
||||
void update(float delta_time) override; // Actualiza las variables del objeto (time-based)
|
||||
|
||||
// Consultas de estado
|
||||
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
|
||||
auto getIndex(const std::string& name) -> int; // Obtiene el índice de la animación por nombre
|
||||
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); } // Número de frames de la animación actual
|
||||
|
||||
// Modificadores de animación
|
||||
void setCurrentAnimation(const std::string& name = "default"); // Establece la animación actual por nombre
|
||||
void setCurrentAnimation(int index = 0); // Establece la animación actual por índice
|
||||
void resetAnimation(); // Reinicia la animación
|
||||
void setCurrentAnimationFrame(int num); // Establece el frame actual de la animación
|
||||
void animate(float delta_time); // Calcula el frame correspondiente a la animación actual (time-based)
|
||||
|
||||
protected:
|
||||
// Constructor per a ús de subclasses que gestionen la surface directament (sense YAML)
|
||||
AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
|
||||
private:
|
||||
// Variables miembro
|
||||
std::vector<AnimationData> animations_; // Vector con las diferentes animaciones
|
||||
int current_animation_{0}; // Animación activa
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "core/rendering/sprite/dissolve_sprite.hpp"
|
||||
|
||||
#include <algorithm> // Para min
|
||||
#include <cstdint> // Para uint32_t
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Hash 2D estable per a dithering (rank aleatori per posició de píxel)
|
||||
static auto pixelRank(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Rang per a un píxel tenint en compte direcció (70% direccional + 30% aleatori)
|
||||
auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float {
|
||||
const float RANDOM = pixelRank(col, row);
|
||||
if (dir == DissolveDirection::NONE || frame_h <= 0) {
|
||||
return RANDOM;
|
||||
}
|
||||
|
||||
float y_factor = 0.0F;
|
||||
if (dir == DissolveDirection::DOWN) {
|
||||
y_factor = static_cast<float>(row) / static_cast<float>(frame_h);
|
||||
} else {
|
||||
y_factor = static_cast<float>(frame_h - 1 - row) / static_cast<float>(frame_h);
|
||||
}
|
||||
|
||||
return (y_factor * 0.7F) + (RANDOM * 0.3F);
|
||||
}
|
||||
|
||||
// Constructor per a surface directa (sense AnimationResource)
|
||||
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: AnimatedSprite(std::move(surface), pos) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor
|
||||
DissolveSprite::DissolveSprite(const AnimationResource& data)
|
||||
: AnimatedSprite(data) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
// Inicialitza tots els píxels com a transparents
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstrueix la surface_display_ filtrant píxels per progress_
|
||||
void DissolveSprite::rebuildDisplaySurface() {
|
||||
if (!surface_ || !surface_display_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SDL_FRect CLIP = clip_;
|
||||
const int SX = static_cast<int>(CLIP.x);
|
||||
const int SY = static_cast<int>(CLIP.y);
|
||||
const int SW = static_cast<int>(CLIP.w);
|
||||
const int SH = static_cast<int>(CLIP.h);
|
||||
|
||||
if (SW <= 0 || SH <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto src_data = surface_->getSurfaceData();
|
||||
auto dst_data = surface_display_->getSurfaceData();
|
||||
|
||||
const int SRC_W = static_cast<int>(src_data->width);
|
||||
const int DST_W = static_cast<int>(dst_data->width);
|
||||
const Uint8 TRANSPARENT = surface_->getTransparentColor();
|
||||
|
||||
// Esborra frame anterior si ha canviat
|
||||
if (prev_clip_.w > 0 && prev_clip_.h > 0 &&
|
||||
(prev_clip_.x != CLIP.x || prev_clip_.y != CLIP.y ||
|
||||
prev_clip_.w != CLIP.w || prev_clip_.h != CLIP.h)) {
|
||||
surface_display_->fillRect(&prev_clip_, TRANSPARENT);
|
||||
}
|
||||
|
||||
// Esborra la zona del frame actual (reconstrucció neta)
|
||||
surface_display_->fillRect(&CLIP, TRANSPARENT);
|
||||
|
||||
// Copia píxels filtrats per progress_
|
||||
for (int row = 0; row < SH; ++row) {
|
||||
for (int col = 0; col < SW; ++col) {
|
||||
const Uint8 COLOR = src_data->data[((SY + row) * SRC_W) + (SX + col)];
|
||||
if (COLOR == TRANSPARENT) {
|
||||
continue;
|
||||
}
|
||||
const float RANK = computePixelRank(col, row, SH, direction_);
|
||||
if (RANK >= progress_) {
|
||||
const Uint8 OUT = (COLOR == source_color_) ? target_color_ : COLOR;
|
||||
dst_data->data[((SY + row) * DST_W) + (SX + col)] = OUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev_clip_ = CLIP;
|
||||
needs_rebuild_ = false;
|
||||
}
|
||||
|
||||
// Actualitza animació, moviment i transició temporal
|
||||
void DissolveSprite::update(float delta_time) {
|
||||
const SDL_FRect OLD_CLIP = clip_;
|
||||
AnimatedSprite::update(delta_time);
|
||||
|
||||
// Detecta canvi de frame d'animació
|
||||
if (clip_.x != OLD_CLIP.x || clip_.y != OLD_CLIP.y ||
|
||||
clip_.w != OLD_CLIP.w || clip_.h != OLD_CLIP.h) {
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Actualitza transició temporal si activa
|
||||
if (transition_mode_ != TransitionMode::NONE) {
|
||||
transition_elapsed_ += delta_time * 1000.0F;
|
||||
const float T = std::min(transition_elapsed_ / transition_duration_, 1.0F);
|
||||
progress_ = (transition_mode_ == TransitionMode::DISSOLVING) ? T : (1.0F - T);
|
||||
needs_rebuild_ = true;
|
||||
if (T >= 1.0F) {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_rebuild_) {
|
||||
rebuildDisplaySurface();
|
||||
}
|
||||
}
|
||||
|
||||
// Renderitza: usa surface_display_ (amb color replace) si disponible
|
||||
void DissolveSprite::render() {
|
||||
if (!surface_display_) {
|
||||
AnimatedSprite::render();
|
||||
return;
|
||||
}
|
||||
surface_display_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_, flip_);
|
||||
}
|
||||
|
||||
// Estableix el progrés manualment
|
||||
void DissolveSprite::setProgress(float progress) {
|
||||
progress_ = std::min(std::max(progress, 0.0F), 1.0F);
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia dissolució temporal (visible → invisible)
|
||||
void DissolveSprite::startDissolve(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::DISSOLVING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 0.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia generació temporal (invisible → visible)
|
||||
void DissolveSprite::startGenerate(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::GENERATING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 1.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Atura la transició temporal
|
||||
void DissolveSprite::stopTransition() {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Retorna si la transició ha acabat
|
||||
auto DissolveSprite::isTransitionDone() const -> bool {
|
||||
return transition_mode_ == TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Configura substitució de color per a la reconstrucció
|
||||
void DissolveSprite::setColorReplace(Uint8 source, Uint8 target) {
|
||||
source_color_ = source;
|
||||
target_color_ = target;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SurfaceAnimatedSprite
|
||||
|
||||
class Surface;
|
||||
|
||||
// Direcció de la dissolució
|
||||
enum class DissolveDirection { NONE,
|
||||
DOWN,
|
||||
UP };
|
||||
|
||||
// Sprite que pot dissoldre's o generar-se de forma aleatòria en X mil·lisegons.
|
||||
// progress_ va de 0.0 (totalment visible) a 1.0 (totalment invisible).
|
||||
class DissolveSprite : public AnimatedSprite {
|
||||
public:
|
||||
explicit DissolveSprite(const AnimationResource& data);
|
||||
DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
~DissolveSprite() override = default;
|
||||
|
||||
void update(float delta_time) override;
|
||||
void render() override;
|
||||
|
||||
// Progrés manual [0.0 = totalment visible, 1.0 = totalment invisible]
|
||||
void setProgress(float progress);
|
||||
[[nodiscard]] auto getProgress() const -> float { return progress_; }
|
||||
|
||||
// Inicia una dissolució temporal (visible → invisible en duration_ms)
|
||||
void startDissolve(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
// Inicia una generació temporal (invisible → visible en duration_ms)
|
||||
void startGenerate(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
void stopTransition();
|
||||
[[nodiscard]] auto isTransitionDone() const -> bool;
|
||||
|
||||
// Substitució de color: en reconstruir, substitueix source per target
|
||||
void setColorReplace(Uint8 source, Uint8 target);
|
||||
|
||||
private:
|
||||
enum class TransitionMode { NONE,
|
||||
DISSOLVING,
|
||||
GENERATING };
|
||||
|
||||
std::shared_ptr<Surface> surface_display_; // Superfície amb els píxels filtrats
|
||||
|
||||
float progress_{0.0F}; // [0=visible, 1=invisible]
|
||||
DissolveDirection direction_{DissolveDirection::NONE};
|
||||
TransitionMode transition_mode_{TransitionMode::NONE};
|
||||
float transition_duration_{0.0F};
|
||||
float transition_elapsed_{0.0F};
|
||||
SDL_FRect prev_clip_{.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
bool needs_rebuild_{false};
|
||||
Uint8 source_color_{255}; // 255 = transparent = sense replace per defecte
|
||||
Uint8 target_color_{0};
|
||||
|
||||
void rebuildDisplaySurface();
|
||||
[[nodiscard]] static auto computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float;
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "core/rendering/sprite/moving_sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y),
|
||||
flip_(flip) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite() { Sprite::clear(); }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
|
||||
: Sprite(std::move(surface)) { Sprite::clear(); }
|
||||
|
||||
// Reinicia todas las variables
|
||||
void MovingSprite::clear() {
|
||||
// Resetea posición
|
||||
x_ = 0.0F;
|
||||
y_ = 0.0F;
|
||||
|
||||
// Resetea velocidad
|
||||
vx_ = 0.0F;
|
||||
vy_ = 0.0F;
|
||||
|
||||
// Resetea aceleración
|
||||
ax_ = 0.0F;
|
||||
ay_ = 0.0F;
|
||||
|
||||
// Resetea flip
|
||||
flip_ = SDL_FLIP_NONE;
|
||||
|
||||
Sprite::clear();
|
||||
}
|
||||
|
||||
// Mueve el sprite (time-based)
|
||||
// Nota: vx_, vy_ ahora se interpretan como pixels/segundo
|
||||
// Nota: ax_, ay_ ahora se interpretan como pixels/segundo²
|
||||
void MovingSprite::move(float delta_time) {
|
||||
// Aplica aceleración a velocidad (time-based)
|
||||
vx_ += ax_ * delta_time;
|
||||
vy_ += ay_ * delta_time;
|
||||
|
||||
// Aplica velocidad a posición (time-based)
|
||||
x_ += vx_ * delta_time;
|
||||
y_ += vy_ * delta_time;
|
||||
|
||||
// Actualiza posición entera para renderizado
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Actualiza las variables internas del objeto (time-based)
|
||||
void MovingSprite::update(float delta_time) {
|
||||
move(delta_time);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Establece la posición y_ el tamaño del objeto
|
||||
void MovingSprite::setPos(SDL_FRect rect) {
|
||||
x_ = rect.x;
|
||||
y_ = rect.y;
|
||||
|
||||
pos_ = rect;
|
||||
}
|
||||
|
||||
// Establece el valor de las variables
|
||||
void MovingSprite::setPos(float x, float y) {
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosX(float value) {
|
||||
x_ = value;
|
||||
pos_.x = static_cast<int>(x_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosY(float value) {
|
||||
y_ = value;
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // lines 8-8
|
||||
|
||||
// Clase SMovingSprite. Añade movimiento y flip al sprite
|
||||
class MovingSprite : public Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip);
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
explicit MovingSprite();
|
||||
explicit MovingSprite(std::shared_ptr<Surface> surface);
|
||||
~MovingSprite() override = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
void update(float delta_time) override; // Actualiza variables internas (time-based)
|
||||
void render() override; // Muestra el sprite por pantalla
|
||||
void render(Uint8 source_color, Uint8 target_color) override; // Renderiza con reemplazo de color
|
||||
|
||||
// Gestión de estado
|
||||
void clear() override; // Reinicia todas las variables a cero
|
||||
|
||||
// Getters de posición
|
||||
[[nodiscard]] auto getPosX() const -> float { return x_; }
|
||||
[[nodiscard]] auto getPosY() const -> float { return y_; }
|
||||
|
||||
// Getters de velocidad
|
||||
[[nodiscard]] auto getVelX() const -> float { return vx_; }
|
||||
[[nodiscard]] auto getVelY() const -> float { return vy_; }
|
||||
|
||||
// Getters de aceleración
|
||||
[[nodiscard]] auto getAccelX() const -> float { return ax_; }
|
||||
[[nodiscard]] auto getAccelY() const -> float { return ay_; }
|
||||
|
||||
// Setters de posición
|
||||
void setPos(SDL_FRect rect); // Establece posición y tamaño del objeto
|
||||
void setPos(float x, float y); // Establece posición x, y
|
||||
void setPosX(float value); // Establece posición X
|
||||
void setPosY(float value); // Establece posición Y
|
||||
|
||||
// Setters de velocidad
|
||||
void setVelX(float value) { vx_ = value; }
|
||||
void setVelY(float value) { vy_ = value; }
|
||||
|
||||
// Setters de aceleración
|
||||
void setAccelX(float value) { ax_ = value; }
|
||||
void setAccelY(float value) { ay_ = value; }
|
||||
|
||||
// Gestión de flip (volteo horizontal)
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; } // Establece modo de flip
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; } // Obtiene modo de flip
|
||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } // Alterna flip horizontal
|
||||
|
||||
protected:
|
||||
// Métodos protegidos
|
||||
void move(float delta_time); // Mueve el sprite (time-based)
|
||||
|
||||
// Variables miembro - Posición
|
||||
float x_{0.0F}; // Posición en el eje X
|
||||
float y_{0.0F}; // Posición en el eje Y
|
||||
|
||||
// Variables miembro - Velocidad (pixels/segundo)
|
||||
float vx_{0.0F}; // Velocidad en el eje X
|
||||
float vy_{0.0F}; // Velocidad en el eje Y
|
||||
|
||||
// Variables miembro - Aceleración (pixels/segundo²)
|
||||
float ax_{0.0F}; // Aceleración en el eje X
|
||||
float ay_{0.0F}; // Aceleración en el eje Y
|
||||
|
||||
// Variables miembro - Renderizado
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE}; // Modo de volteo del sprite
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{.x = x, .y = y, .w = w, .h = h},
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
|
||||
: surface_(std::move(surface)),
|
||||
pos_(rect),
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite() = default;
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
|
||||
clip_(pos_) {}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void Sprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
source_color,
|
||||
target_color,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(float x, float y) {
|
||||
pos_.x = x;
|
||||
pos_.y = y;
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(SDL_FPoint p) {
|
||||
pos_.x = p.x;
|
||||
pos_.y = p.y;
|
||||
}
|
||||
|
||||
// Reinicia las variables a cero
|
||||
void Sprite::clear() {
|
||||
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
}
|
||||
|
||||
// Actualiza el estado del sprite (time-based)
|
||||
void Sprite::update(float delta_time) {
|
||||
// Base implementation does nothing (static sprites)
|
||||
(void)delta_time; // Evita warning de parámetro no usado
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <utility>
|
||||
class Surface; // lines 5-5
|
||||
|
||||
// Clase SurfaceSprite
|
||||
class Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
Sprite(std::shared_ptr<Surface>, float x, float y, float w, float h);
|
||||
Sprite(std::shared_ptr<Surface>, SDL_FRect rect);
|
||||
Sprite();
|
||||
explicit Sprite(std::shared_ptr<Surface>);
|
||||
|
||||
// Destructor
|
||||
virtual ~Sprite() = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
virtual void update(float delta_time); // Actualiza el estado del sprite (time-based)
|
||||
virtual void render(); // Muestra el sprite por pantalla
|
||||
virtual void render(Uint8 source_color, Uint8 target_color); // Renderiza con reemplazo de color
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height); // Renderiza amb dissolució vertical (hash 2D, sense parpelleig)
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color); // Idem amb reemplaç de color
|
||||
|
||||
// Gestión de estado
|
||||
virtual void clear(); // Reinicia las variables a cero
|
||||
|
||||
// Obtención de propiedades
|
||||
[[nodiscard]] auto getX() const -> float { return pos_.x; }
|
||||
[[nodiscard]] auto getY() const -> float { return pos_.y; }
|
||||
[[nodiscard]] auto getWidth() const -> float { return pos_.w; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return pos_.h; }
|
||||
[[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; }
|
||||
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
|
||||
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
|
||||
auto getRect() -> SDL_FRect& { return pos_; }
|
||||
|
||||
// Modificación de posición y tamaño
|
||||
void setX(float x) { pos_.x = x; }
|
||||
void setY(float y) { pos_.y = y; }
|
||||
void setWidth(float w) { pos_.w = w; }
|
||||
void setHeight(float h) { pos_.h = h; }
|
||||
void setPosition(float x, float y);
|
||||
void setPosition(SDL_FPoint p);
|
||||
void setPosition(SDL_FRect r) { pos_ = r; }
|
||||
void incX(float value) { pos_.x += value; }
|
||||
void incY(float value) { pos_.y += value; }
|
||||
|
||||
// Modificación de clip y surface
|
||||
void setClip(SDL_FRect rect) { clip_ = rect; }
|
||||
void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
|
||||
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
|
||||
|
||||
protected:
|
||||
// Variables miembro
|
||||
std::shared_ptr<Surface> surface_{nullptr}; // Surface donde estan todos los dibujos del sprite
|
||||
SDL_FRect pos_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Posición y tamaño donde dibujar el sprite
|
||||
SDL_FRect clip_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Rectangulo de origen de la surface que se dibujará en pantalla
|
||||
};
|
||||
@@ -0,0 +1,695 @@
|
||||
// IWYU pragma: no_include <bits/std_abs.h>
|
||||
#include "core/rendering/surface.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para min, max, copy_n, fill
|
||||
#include <cmath> // Para abs
|
||||
#include <cstdint> // Para uint32_t
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ist...
|
||||
#include <iostream> // Para cerr
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access, default...
|
||||
#include <sstream> // Para basic_istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/gif.hpp" // Para Gif
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette {
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
throw std::runtime_error("Error opening file: " + file_path);
|
||||
}
|
||||
|
||||
// Cargar la paleta usando los datos del buffer
|
||||
std::vector<uint32_t> pal = GIF::Gif::loadPalette(buffer.data());
|
||||
if (pal.empty()) {
|
||||
throw std::runtime_error("No palette found in GIF file: " + file_path);
|
||||
}
|
||||
|
||||
// Crear la paleta y copiar los datos desde 'pal'
|
||||
Palette palette = {}; // Inicializa la paleta con ceros
|
||||
std::copy_n(pal.begin(), std::min(pal.size(), palette.size()), palette.begin());
|
||||
|
||||
// Mensaje de depuración
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette {
|
||||
Palette palette{};
|
||||
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)
|
||||
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
throw std::runtime_error("No se pudo abrir el archivo .pal: " + file_path);
|
||||
}
|
||||
|
||||
// Convert bytes to string for parsing
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
int line_number = 0;
|
||||
int color_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
++line_number;
|
||||
|
||||
// Ignorar las tres primeras líneas del archivo
|
||||
if (line_number <= 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Procesar las líneas restantes con valores RGB
|
||||
std::istringstream ss(line);
|
||||
int r;
|
||||
int g;
|
||||
int b;
|
||||
if (ss >> r >> g >> b) {
|
||||
// Construir el color ARGB (A = 255 por defecto)
|
||||
Uint32 color = (255 << 24) | (r << 16) | (g << 8) | b;
|
||||
palette[color_index++] = color;
|
||||
|
||||
// Limitar a un máximo de 256 colores (opcional)
|
||||
if (color_index >= 256) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Surface::Surface(int w, int h)
|
||||
: surface_data_(std::make_shared<SurfaceData>(w, h)),
|
||||
transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) { initializeSubPalette(sub_palette_); }
|
||||
|
||||
Surface::Surface(const std::string& file_path)
|
||||
: transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) {
|
||||
SurfaceData loaded_data = loadSurface(file_path);
|
||||
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded_data));
|
||||
|
||||
initializeSubPalette(sub_palette_);
|
||||
}
|
||||
|
||||
// Carga una superficie desde un archivo
|
||||
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Error opening file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error opening file");
|
||||
}
|
||||
|
||||
// Crear un objeto Gif y llamar a la función loadGif
|
||||
Uint16 w = 0;
|
||||
Uint16 h = 0;
|
||||
std::vector<Uint8> raw_pixels = GIF::Gif::loadGif(buffer.data(), w, h);
|
||||
if (raw_pixels.empty()) {
|
||||
std::cerr << "Error loading GIF from file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error loading GIF");
|
||||
}
|
||||
|
||||
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>,
|
||||
// reservamos un bloque dinámico y copiamos los datos del vector.
|
||||
size_t pixel_count = raw_pixels.size();
|
||||
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>());
|
||||
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
|
||||
|
||||
// Crear y devolver directamente el objeto SurfaceData
|
||||
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return {static_cast<float>(w), static_cast<float>(h), pixels};
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void Surface::loadPalette(const std::string& file_path) {
|
||||
palette_ = ::loadPalette(file_path);
|
||||
}
|
||||
|
||||
// Carga una paleta desde otra paleta
|
||||
void Surface::loadPalette(const Palette& palette) {
|
||||
palette_ = palette;
|
||||
}
|
||||
|
||||
// Establece un color en la paleta
|
||||
void Surface::setColor(int index, Uint32 color) {
|
||||
palette_.at(index) = color;
|
||||
}
|
||||
|
||||
// Rellena la superficie con un color
|
||||
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
||||
}
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||
return; // Coordenadas fuera de rango
|
||||
}
|
||||
|
||||
const int INDEX = x + (y * surface_data_->width);
|
||||
surface_data_->data.get()[INDEX] = color;
|
||||
}
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Rellenar fila a fila con memset (memoria contigua por fila)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
|
||||
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
|
||||
// Dibujar bordes verticales
|
||||
for (int y = y_start; y < y_end; ++y) {
|
||||
// Borde izquierdo
|
||||
const int LEFT_INDEX = x_start + (y * surface_data_->width);
|
||||
surface_data_->data.get()[LEFT_INDEX] = color;
|
||||
|
||||
// Borde derecho
|
||||
const int RIGHT_INDEX = (x_end - 1) + (y * surface_data_->width);
|
||||
surface_data_->data.get()[RIGHT_INDEX] = color;
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja una linea
|
||||
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Calcula las diferencias
|
||||
float dx = std::abs(x2 - x1);
|
||||
float dy = std::abs(y2 - y1);
|
||||
|
||||
// Determina la dirección del incremento
|
||||
float sx = (x1 < x2) ? 1 : -1;
|
||||
float sy = (y1 < y2) ? 1 : -1;
|
||||
|
||||
float err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
// Asegúrate de no dibujar fuera de los límites de la superficie
|
||||
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
|
||||
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
|
||||
}
|
||||
|
||||
// Si alcanzamos el punto final, salimos
|
||||
if (x1 == x2 && y1 == y2) {
|
||||
break;
|
||||
}
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x1 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y1 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data->width - dx);
|
||||
h = std::min(h, surface_data->height - dy);
|
||||
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
|
||||
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
|
||||
int src_x = sx + ix;
|
||||
int src_y = sy + iy;
|
||||
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data_dest->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data_dest->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper para calcular coordenadas con flip
|
||||
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
|
||||
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
}
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void Surface::copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const {
|
||||
if (dest_x < 0 || dest_y < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dest_data[dest_x + (dest_y * dest_width)] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la superficie de origen a la de destino
|
||||
void Surface::render(SDL_FRect* src_rect, SDL_FRect* dst_rect, SDL_FlipMode flip) {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Si srcRect es nullptr, tomar toda la superficie fuente
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float sw = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float sh = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Si dstRect es nullptr, asignar las mismas dimensiones que srcRect
|
||||
float dx = (dst_rect != nullptr) ? dst_rect->x : 0;
|
||||
float dy = (dst_rect != nullptr) ? dst_rect->y : 0;
|
||||
float dw = (dst_rect != nullptr) ? dst_rect->w : sw;
|
||||
float dh = (dst_rect != nullptr) ? dst_rect->h : sh;
|
||||
|
||||
// Asegurarse de que srcRect y dstRect tienen las mismas dimensiones
|
||||
if (sw != dw || sh != dh) {
|
||||
dw = sw; // Respetar las dimensiones de srcRect
|
||||
dh = sh;
|
||||
}
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en src y dst
|
||||
sw = std::min(sw, surface_data_->width - sx);
|
||||
sh = std::min(sh, surface_data_->height - sy);
|
||||
dw = std::min(dw, surface_data->width - dx);
|
||||
dh = std::min(dh, surface_data->height - dy);
|
||||
|
||||
int final_width = std::min(sw, dw);
|
||||
int final_height = std::min(sh, dh);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < final_height; ++iy) {
|
||||
for (int ix = 0; ix < final_width; ++ix) {
|
||||
int src_x = 0;
|
||||
int src_y = 0;
|
||||
calculateFlippedCoords(ix, iy, sx, sy, final_width, final_height, flip, src_x, src_y);
|
||||
|
||||
int dest_x = dx + ix;
|
||||
int dest_y = dy + iy;
|
||||
|
||||
// Verificar límites de destino antes de copiar
|
||||
if (dest_x >= 0 && dest_x < surface_data->width && dest_y >= 0 && dest_y < surface_data->height) {
|
||||
copyPixelIfNotTransparent(surface_data->data.get(), dest_x, dest_y, surface_data->width, src_x, src_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect, SDL_FlipMode flip) const {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verifica que las coordenadas de destino estén dentro de los límites
|
||||
if (dest_x < 0 || dest_y < 0 || dest_x >= surface_data->width || dest_y >= surface_data->height) {
|
||||
continue; // Saltar píxeles fuera del rango del destino
|
||||
}
|
||||
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
surface_data->data[dest_x + (dest_y * surface_data->width)] =
|
||||
(color == source_color) ? target_color : color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash 2D estable per a dithering sense flickering
|
||||
static auto pixelThreshold(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Calcula la densidad de fade para un pixel en posición screen_y
|
||||
static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> float {
|
||||
if (screen_y < fade_h) {
|
||||
return static_cast<float>(fade_h - screen_y) / static_cast<float>(fade_h);
|
||||
}
|
||||
if (screen_y >= canvas_height - fade_h) {
|
||||
return static_cast<float>(screen_y - (canvas_height - fade_h)) / static_cast<float>(fade_h);
|
||||
}
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Idem però reemplaçant un color índex
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture ni SDL_Renderer)
|
||||
void Surface::toARGBBuffer(Uint32* buffer) const {
|
||||
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
|
||||
|
||||
const int WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int HEIGHT = static_cast<int>(surface_data_->height);
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
|
||||
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
|
||||
const size_t PAL_SIZE = palette_.size();
|
||||
|
||||
for (int i = 0; i < WIDTH * HEIGHT; ++i) {
|
||||
Uint8 color_index = src[i];
|
||||
|
||||
// Verificación de seguridad: ¿El índice existe en la paleta?
|
||||
if (color_index < PAL_SIZE) {
|
||||
buffer[i] = palette_[color_index];
|
||||
} else {
|
||||
buffer[i] = 0xFF000000; // Negro opaco si el índice es erróneo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
// Bloquea la textura para modificar los píxeles directamente
|
||||
if (!SDL_LockTexture(texture, nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
// Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware)
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture); // Desbloquea la textura
|
||||
|
||||
// Renderiza la textura en la pantalla completa
|
||||
if (!SDL_RenderTexture(renderer, texture, nullptr, nullptr)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
SDL_Rect lock_rect;
|
||||
if (dest_rect != nullptr) {
|
||||
lock_rect.x = static_cast<int>(dest_rect->x);
|
||||
lock_rect.y = static_cast<int>(dest_rect->y);
|
||||
lock_rect.w = static_cast<int>(dest_rect->w);
|
||||
lock_rect.h = static_cast<int>(dest_rect->h);
|
||||
}
|
||||
|
||||
// Usa lockRect solo si destRect no es nulo
|
||||
if (!SDL_LockTexture(texture, (dest_rect != nullptr) ? &lock_rect : nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture);
|
||||
|
||||
// Renderiza la textura con los rectángulos especificados
|
||||
if (!SDL_RenderTexture(renderer, texture, src_rect, dest_rect)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta principal
|
||||
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verificar que el tamaño mínimo de palette_ sea adecuado
|
||||
static constexpr int PALETTE_SIZE = 19;
|
||||
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
palette_[i] = palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
palette_[1] = palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return palette_[15] == palette_[0];
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta secundaria
|
||||
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Variable estática para almacenar el último tick
|
||||
static Uint32 last_tick_ = 0;
|
||||
|
||||
// Obtener el tiempo actual
|
||||
Uint32 current_tick = SDL_GetTicks();
|
||||
|
||||
// Verificar si ha pasado el tiempo de retardo
|
||||
if (current_tick - last_tick_ < delay) {
|
||||
return false; // No se realiza el fade
|
||||
}
|
||||
|
||||
// Actualizar el último tick
|
||||
last_tick_ = current_tick;
|
||||
|
||||
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
|
||||
static constexpr int SUB_PALETTE_SIZE = 19;
|
||||
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
sub_palette_[i] = sub_palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
sub_palette_[1] = sub_palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return sub_palette_[15] == sub_palette_[0];
|
||||
}
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
|
||||
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array> // Para array
|
||||
#include <memory> // Para default_delete, shared_ptr, __shared_pt...
|
||||
#include <numeric> // Para iota
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Alias
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
using SubPalette = std::array<Uint8, 256>;
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette;
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette;
|
||||
|
||||
struct SurfaceData {
|
||||
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
|
||||
float width; // Ancho de la imagen
|
||||
float height; // Alto de la imagen
|
||||
|
||||
// Constructor por defecto
|
||||
SurfaceData()
|
||||
: data(nullptr),
|
||||
width(0),
|
||||
height(0) {}
|
||||
|
||||
// Constructor que inicializa dimensiones y asigna memoria
|
||||
SurfaceData(float w, float h)
|
||||
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor para inicializar directamente con datos
|
||||
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
|
||||
: data(std::move(pixels)),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor de movimiento
|
||||
SurfaceData(SurfaceData&& other) noexcept = default;
|
||||
|
||||
// Operador de movimiento
|
||||
auto operator=(SurfaceData&& other) noexcept -> SurfaceData& = default;
|
||||
|
||||
// Evita copias accidentales
|
||||
SurfaceData(const SurfaceData&) = delete;
|
||||
auto operator=(const SurfaceData&) -> SurfaceData& = delete;
|
||||
};
|
||||
|
||||
class Surface {
|
||||
private:
|
||||
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
|
||||
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
|
||||
SubPalette sub_palette_; // Paleta para reindexar colores
|
||||
int transparent_color_; // Indice de la paleta que se omite en la copia de datos
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
Surface(int w, int h);
|
||||
explicit Surface(const std::string& file_path);
|
||||
|
||||
// Destructor
|
||||
~Surface() = default;
|
||||
|
||||
// Carga una SurfaceData desde un archivo
|
||||
static auto loadSurface(const std::string& file_path) -> SurfaceData;
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void loadPalette(const std::string& file_path);
|
||||
void loadPalette(const Palette& palette);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
|
||||
void render(float dx, float dy, float sx, float sy, float w, float h);
|
||||
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void renderWithColorReplace(int x, int y, Uint8 source_color = 0, Uint8 target_color = 0, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Idem però reemplaçant un color índex (per a sprites sobre fons del mateix color)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Establece un color en la paleta
|
||||
void setColor(int index, Uint32 color);
|
||||
|
||||
// Rellena la SurfaceData con un color
|
||||
void clear(Uint8 color);
|
||||
|
||||
// Vuelca la SurfaceData a una textura
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture);
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect);
|
||||
|
||||
// Realiza un efecto de fundido en las paletas
|
||||
auto fadePalette() -> bool;
|
||||
auto fadeSubPalette(Uint32 delay = 0) -> bool;
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void resetSubPalette();
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture)
|
||||
void toARGBBuffer(Uint32* buffer) const;
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void putPixel(int x, int y, Uint8 color);
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto getPixel(int x, int y) -> Uint8;
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void fillRect(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void drawRectBorder(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja una linea
|
||||
void drawLine(float x1, float y1, float x2, float y2, Uint8 color);
|
||||
|
||||
// Metodos para gestionar surface_data_
|
||||
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
|
||||
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
|
||||
|
||||
// Obtien ancho y alto
|
||||
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
|
||||
|
||||
// Color transparente
|
||||
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
|
||||
void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; }
|
||||
|
||||
// Paleta
|
||||
void setPalette(const std::array<Uint32, 256>& palette) { palette_ = palette; }
|
||||
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
|
||||
|
||||
// Inicializa la sub paleta
|
||||
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
|
||||
|
||||
private:
|
||||
// Helper para calcular coordenadas con flip
|
||||
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;
|
||||
};
|
||||
@@ -0,0 +1,311 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <iostream> // Para cerr
|
||||
#include <sstream> // Para istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "utils/utils.hpp" // Para getFileName, stringToColor, printWithDots
|
||||
|
||||
// Extrae el siguiente codepoint UTF-8 de la cadena, avanzando 'pos' al byte siguiente
|
||||
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto c = static_cast<unsigned char>(s[pos]);
|
||||
uint32_t cp = 0;
|
||||
size_t extra = 0;
|
||||
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
extra = 0;
|
||||
} else if (c < 0xC0) {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
} // byte de continuación suelto
|
||||
else if (c < 0xE0) {
|
||||
cp = c & 0x1F;
|
||||
extra = 1;
|
||||
} else if (c < 0xF0) {
|
||||
cp = c & 0x0F;
|
||||
extra = 2;
|
||||
} else if (c < 0xF8) {
|
||||
cp = c & 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
pos++;
|
||||
for (size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
||||
auto cb = static_cast<unsigned char>(s[pos]);
|
||||
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
||||
cp = (cp << 6) | (cb & 0x3F);
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
// Convierte un codepoint Unicode a una cadena UTF-8
|
||||
auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::string result;
|
||||
if (cp < 0x80) {
|
||||
result += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
result += static_cast<char>(0xC0 | (cp >> 6));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
result += static_cast<char>(0xE0 | (cp >> 12));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
result += static_cast<char>(0xF0 | (cp >> 18));
|
||||
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Carga un fichero de definición de fuente .fnt
|
||||
// Formato: líneas "clave valor", comentarios con #, gliphos como "codepoint ancho"
|
||||
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto tf = std::make_shared<File>();
|
||||
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
|
||||
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
|
||||
}
|
||||
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
if (line.empty() || line[0] == '#') { continue; }
|
||||
|
||||
std::istringstream ls(line);
|
||||
std::string key;
|
||||
ls >> key;
|
||||
|
||||
if (key == "box_width") {
|
||||
ls >> tf->box_width;
|
||||
} else if (key == "box_height") {
|
||||
ls >> tf->box_height;
|
||||
} else if (key == "columns") {
|
||||
ls >> tf->columns;
|
||||
} else if (key == "cell_spacing") {
|
||||
ls >> tf->cell_spacing;
|
||||
} else if (key == "row_spacing") {
|
||||
ls >> tf->row_spacing;
|
||||
} else {
|
||||
// Línea de glifo: codepoint_decimal ancho_visual
|
||||
uint32_t codepoint = 0;
|
||||
int width = 0;
|
||||
try {
|
||||
codepoint = static_cast<uint32_t>(std::stoul(key));
|
||||
ls >> width;
|
||||
} catch (...) {
|
||||
continue; // línea mal formateada, ignorar
|
||||
}
|
||||
Offset off{};
|
||||
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
||||
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
||||
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
||||
off.w = width;
|
||||
tf->offset[codepoint] = off;
|
||||
++glyph_index;
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||
return tf;
|
||||
}
|
||||
|
||||
// Constructor desde fichero
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
||||
auto tf = loadTextFile(text_file);
|
||||
|
||||
box_height_ = tf->box_height;
|
||||
box_width_ = tf->box_width;
|
||||
offset_ = tf->offset;
|
||||
|
||||
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
}
|
||||
|
||||
// Constructor desde estructura precargada
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
||||
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
||||
box_width_(text_file->box_width),
|
||||
box_height_(text_file->box_height),
|
||||
offset_(text_file->offset) {
|
||||
}
|
||||
|
||||
// Escribe texto en pantalla
|
||||
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, 15);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto en una surface
|
||||
auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||
auto width = length(text, kerning) * zoom;
|
||||
auto height = box_height_ * zoom;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
write(0, 0, text, kerning);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con extras en una surface
|
||||
auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||
auto width = Text::length(text, kerning) + shadow_distance;
|
||||
auto height = box_height_ + shadow_distance;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
writeDX(flags, 0, 0, text, kerning, text_color, shadow_distance, shadow_color, lenght);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con colores
|
||||
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, color);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto con sombra
|
||||
void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int lenght) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, lenght);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe el texto centrado en un punto x
|
||||
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int lenght) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe texto con extras
|
||||
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
||||
const auto SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||
const auto COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||
const auto STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||
|
||||
if (CENTERED) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
}
|
||||
|
||||
if (SHADOWED) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
|
||||
if (STROKED) {
|
||||
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||
for (int dy = -dist; dy <= dist; ++dy) {
|
||||
for (int dx = -dist; dx <= dist; ++dx) {
|
||||
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (COLORED) {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
} else {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene la longitud en pixels de una cadena UTF-8
|
||||
auto Text::length(const std::string& text, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
}
|
||||
|
||||
return shift > 0 ? shift - kerning : 0;
|
||||
}
|
||||
|
||||
// Devuelve el ancho en pixels de un glifo dado su codepoint Unicode
|
||||
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) { return it->second.w + kerning; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Devuelve el clip rect (región en el bitmap) de un glifo dado su codepoint
|
||||
auto Text::getGlyphClip(uint32_t codepoint) const -> SDL_FRect {
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; }
|
||||
return {.x = static_cast<float>(it->second.x),
|
||||
.y = static_cast<float>(it->second.y),
|
||||
.w = static_cast<float>(box_width_),
|
||||
.h = static_cast<float>(box_height_)};
|
||||
}
|
||||
|
||||
// Devuelve el tamaño de la caja de cada caracter
|
||||
auto Text::getCharacterSize() const -> int {
|
||||
return box_width_;
|
||||
}
|
||||
|
||||
// Establece si se usa un tamaño fijo de letra
|
||||
void Text::setFixedWidth(bool value) {
|
||||
fixed_width_ = value;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr, unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // Forward declaration
|
||||
|
||||
// Clase texto. Pinta texto en pantalla a partir de un bitmap con soporte UTF-8
|
||||
class Text {
|
||||
public:
|
||||
// Tipos anidados públicos
|
||||
struct Offset {
|
||||
int x{0}, y{0}, w{0};
|
||||
};
|
||||
|
||||
struct File {
|
||||
int box_width{0}; // Anchura de la caja de cada caracter en el png
|
||||
int box_height{0}; // Altura de la caja de cada caracter en el png
|
||||
int columns{16}; // Número de columnas en el bitmap
|
||||
int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior)
|
||||
int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing)
|
||||
std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
|
||||
// Constructor
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
|
||||
|
||||
// Destructor
|
||||
~Text() = default;
|
||||
|
||||
// Constantes de flags para writeDX
|
||||
static constexpr int COLOR_FLAG = 1;
|
||||
static constexpr int SHADOW_FLAG = 2;
|
||||
static constexpr int CENTER_FLAG = 4;
|
||||
static constexpr int STROKE_FLAG = 8;
|
||||
|
||||
void write(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
|
||||
void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
|
||||
void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
|
||||
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
|
||||
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1); // Escribe texto con extras
|
||||
|
||||
auto writeToSurface(const std::string& text, int zoom = 1, int kerning = 1) -> std::shared_ptr<Surface>; // Escribe el texto en una textura
|
||||
auto writeDXToSurface(Uint8 flags, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1) -> std::shared_ptr<Surface>; // Escribe el texto con extras en una textura
|
||||
|
||||
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; // Obtiene la longitud en pixels de una cadena
|
||||
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el tamaño del caracter
|
||||
[[nodiscard]] auto glyphWidth(uint32_t codepoint, int kerning = 0) const -> int; // Devuelve el ancho en pixels de un glifo
|
||||
[[nodiscard]] auto getGlyphClip(uint32_t codepoint) const -> SDL_FRect; // Devuelve el clip rect del glifo
|
||||
[[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } // Acceso al sprite interno
|
||||
|
||||
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
|
||||
|
||||
static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>; // Carga un fichero de definición de fuente .fnt
|
||||
static auto codepointToUtf8(uint32_t cp) -> std::string; // Convierte un codepoint Unicode a string UTF-8
|
||||
static auto nextCodepoint(const std::string& s, size_t& pos) -> uint32_t; // Extrae el siguiente codepoint UTF-8
|
||||
|
||||
private:
|
||||
// Objetos y punteros
|
||||
std::unique_ptr<Sprite> sprite_ = nullptr; // Objeto con los graficos para el texto
|
||||
|
||||
// Variables
|
||||
int box_width_ = 0; // Anchura de la caja de cada caracter en el png
|
||||
int box_height_ = 0; // Altura de la caja de cada caracter en el png
|
||||
bool fixed_width_ = false; // Indica si el texto se ha de escribir con longitud fija
|
||||
std::unordered_map<uint32_t, Offset> offset_; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
Reference in New Issue
Block a user