/* +------------------------------------------------------+ ____/ \____ /| - Open source game framework licensed to freely use, | \ / / | copy, modify and sell without restriction | +--\ ^__^ /--+ | | | ~/ \~ | | - created for | | ~~~~~~~~~~~~ | +------------------------------------------------------+ | SPACE ~~~~~ | / | ~~~~~~~ BOX |/ +-------------*/ #include "Pixels.hpp" #include "extension.hpp" /* Edit a vector in place, giving it the specified magnitude while maintaining the direction */ void sb::set_magnitude(glm::vec2& vector, float magnitude) { vector = glm::normalize(vector) * magnitude; } Box sb::get_texture_box(SDL_Texture* texture) { int width, height; SDL_QueryTexture(texture, nullptr, nullptr, &width, &height); return Box(glm::vec2(0, 0), glm::vec2(width, height)); } glm::vec2 sb::fit_and_preserve_aspect(const glm::vec2& inner, const glm::vec2& outer) { glm::vec2 delta = inner - outer; float aspect = inner.x / inner.y; glm::vec2 fit; if (delta.x > delta.y) { fit.x = outer.x; fit.y = fit.x / aspect; } else { fit.y = outer.y; fit.x = fit.y * aspect; } return fit; } std::vector> sb::get_blinds_boxes(glm::vec2 size, float step, int count) { std::vector blinds; float blind_height = size.y / count; for (int ii = 1; ii <= count; ii++) { blinds.push_back(Box({0, blind_height * ii}, {size.x, 0})); } float inflate_per_frame = blind_height * step; std::vector> frames; float bottom_save; while (blinds[0].height() < blind_height) { frames.push_back({}); for (Box& blind : blinds) { bottom_save = blind.bottom(); blind.expand({0, inflate_per_frame}); blind.bottom(bottom_save); frames.back().push_back(blind); } } return frames; } void sb::populate_pixel_2d_array(SDL_Renderer* renderer, SDL_Texture* texture, std::vector>& pixels) { populate_pixel_2d_array(renderer, texture, pixels, get_texture_box(texture)); } void sb::populate_pixel_2d_array( SDL_Renderer* renderer, SDL_Texture* texture, std::vector>& pixels, const Box& region) { int access; if (SDL_QueryTexture(texture, nullptr, &access, nullptr, nullptr) < 0) { sb::Log::sdl_error("Could not query texture for access flag"); } else { if (access != SDL_TEXTUREACCESS_TARGET) { texture = duplicate_texture(renderer, texture); } if (SDL_SetRenderTarget(renderer, texture) < 0) { sb::Log::sdl_error("Could not set render target"); } else { Uint32 format = SDL_PIXELFORMAT_RGBA32; int bytes_per_pixel = SDL_BYTESPERPIXEL(format); int bytes_per_row = bytes_per_pixel * region.width(); int bytes_total = bytes_per_row * region.height(); Uint8* source = new Uint8[bytes_total]; SDL_Rect int_rect = region; if (SDL_RenderReadPixels(renderer, &int_rect, format, source, bytes_per_row) < 0) { sb::Log::sdl_error("Could not read pixels after setting remapped texture as target"); } else { pixels.reserve(region.width()); for (int x = 0; x < region.width(); x++) { std::vector column; pixels.push_back(column); pixels[x].reserve(region.height()); } for (int y = 0, ii = 0; y < region.height(); y++) { for (int x = 0; x < region.width(); x++) { pixels[x][y] = {source[ii++], source[ii++], source[ii++], source[ii++]}; } } } delete[] source; } } } std::vector sb::get_halo_frames( SDL_Renderer* renderer, float radius, int segment_count, const std::vector& colors, float min_radius, bool fade) { std::vector frames; frames.reserve(segment_count); SDL_Texture* frame; float alpha = 255, alpha_step = 255.0f / segment_count, segment_radius; int color_count = colors.size(); Color color; SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); for (int color_offset = 0; color_offset < color_count; color_offset++) { if (fade) { alpha = alpha_step; } frame = get_filled_texture(renderer, {2 * radius, 2 * radius}, {0, 0, 0, 0}); SDL_SetTextureBlendMode(frame, SDL_BLENDMODE_BLEND); SDL_SetRenderTarget(renderer, frame); for (int segment_ii = 0; segment_ii < segment_count; segment_ii++) { color = colors[(color_offset + segment_ii) % color_count]; color.a = std::round(alpha); segment_radius = min_radius + (segment_count - 1.0f - segment_ii) / (segment_count - 1.0f) * (radius - min_radius); filledCircleRGBA(renderer, radius, radius, static_cast(std::round(segment_radius)), color.r, color.g, color.b, color.a); if (fade) { alpha += alpha_step; } } frames.push_back(frame); } return frames; } std::vector sb::get_portal_frames( SDL_Renderer* renderer, glm::vec2 size, float hue_start, float hue_end, int dy, int count) { std::vector frames; frames.reserve(count); float y_margin = 10; float max_y = size.y - y_margin; std::vector hues = range_count(hue_start, hue_end, count); SDL_Texture* frame; Color color; for (int frame_ii = 0; frame_ii < count; frame_ii++) { frame = get_filled_texture(renderer, size, {255, 255, 255, 0}); SDL_SetRenderTarget(renderer, frame); SDL_SetTextureBlendMode(frame, SDL_BLENDMODE_BLEND); for (int ellipse_ii = 0, y = max_y; y > y_margin - 3; ellipse_ii++, y -= dy) { color.a = y / max_y * 255.0f; color.hsv(hues[glm::mod(ellipse_ii - frame_ii, count)]); aaFilledEllipseRGBA(renderer, size.x / 2, y, size.x / 2, y_margin - 3, color.r, color.g, color.b, color.a); } frames.push_back(frame); } return frames; } void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, const SDL_Color& color, const Box& box) { SDL_SetRenderTarget(renderer, texture); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); SDL_RenderFillRectF(renderer, &box); } void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, const SDL_Color& color) { fill_texture(renderer, texture, color, get_texture_box(texture)); } void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_Texture* tile, const Box& box) { Box texture_box = get_texture_box(texture), tile_box = get_texture_box(tile); SDL_FRect draw_rect; if (SDL_SetRenderTarget(renderer, texture) < 0) { sb::Log::sdl_error("could not set render target"); } else { SDL_Rect int_rect = box; if (SDL_RenderSetClipRect(renderer, &int_rect) < 0) { sb::Log::sdl_error("could not set clip"); } else { for (int x = 0; x < texture_box.width(); x += tile_box.width()) { for (int y = 0; y < texture_box.height(); y += tile_box.height()) { draw_rect = {(float) x, (float) y, tile_box.width(), tile_box.height()}; SDL_RenderCopyF(renderer, tile, nullptr, &draw_rect); } } SDL_RenderSetClipRect(renderer, nullptr); } } } void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_Texture* tile) { fill_texture(renderer, texture, tile, get_texture_box(texture)); } SDL_Texture* sb::get_filled_texture(SDL_Renderer* renderer, glm::vec2 size, const SDL_Color& color, Uint32 format) { SDL_Texture* texture; if ((texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y)) == nullptr) { sb::Log::sdl_error("could not create texture to fill"); } else { fill_texture(renderer, texture, color); } return texture; } SDL_Texture* sb::get_filled_texture(SDL_Renderer* renderer, glm::vec2 size, SDL_Texture* tile, Uint32 format) { SDL_Texture* texture; if ((texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y)) == nullptr) { sb::Log::sdl_error("could not create texture to fill"); } else { fill_texture(renderer, texture, tile); } return texture; } SDL_Texture* sb::get_hue_shifted_texture(SDL_Renderer* renderer, SDL_Texture* base, float offset) { SDL_Texture* hue_shifted_texture = duplicate_texture(renderer, base); Uint32 pixel_format; int w, h; if (SDL_QueryTexture(hue_shifted_texture, &pixel_format, nullptr, &w, &h) < 0) { sb::Log::sdl_error("could not query texture"); } else { SDL_PixelFormat* pixel_format_struct = SDL_AllocFormat(pixel_format); SDL_SetRenderTarget(renderer, hue_shifted_texture); int bytes_per_pixel = SDL_BYTESPERPIXEL(pixel_format); int bytes_per_row = bytes_per_pixel * w; int bytes_total = bytes_per_row * h; int length = bytes_total / 4 + (bytes_total % 4 ? 1 : 0); Uint32* pixels = new Uint32[length]; if (SDL_RenderReadPixels(renderer, NULL, pixel_format, pixels, bytes_per_row) < 0) { sb::Log::sdl_error("Could not read pixels"); } else { Color rgba; for (int ii = 0; ii < length; ii++) { SDL_GetRGBA(pixels[ii], const_cast(pixel_format_struct), &rgba.r, &rgba.g, &rgba.b, &rgba.a); rgba.shift_hue(offset); pixels[ii] = SDL_MapRGBA(const_cast(pixel_format_struct), rgba.r, rgba.g, rgba.b, rgba.a); } if (SDL_UpdateTexture(hue_shifted_texture, NULL, pixels, bytes_per_row) < 0) { sb::Log::sdl_error("Could not apply hue shifted pixels update to texture"); } } delete[] pixels; SDL_FreeFormat(pixel_format_struct); } return hue_shifted_texture; } SDL_Texture* sb::duplicate_texture(SDL_Renderer* renderer, SDL_Texture* base) { Box box = get_texture_box(base); return duplicate_texture(renderer, base, box.size()); } SDL_Texture* sb::duplicate_texture(SDL_Renderer* renderer, SDL_Texture* base, const glm::vec2& size) { SDL_BlendMode original_blend_mode; SDL_GetTextureBlendMode(base, &original_blend_mode); Uint32 format; SDL_QueryTexture(base, &format, nullptr, nullptr, nullptr); SDL_Texture* duplicate = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y); if (duplicate == NULL) { sb::Log::sdl_error("could not create texture from base"); return NULL; } if ((SDL_SetRenderTarget(renderer, duplicate)) < 0) { sb::Log::sdl_error("could not set render target to duplicate"); return NULL; } SDL_SetTextureBlendMode(base, SDL_BLENDMODE_NONE); SDL_SetTextureBlendMode(duplicate, SDL_BLENDMODE_BLEND); if ((SDL_RenderCopyF(renderer, base, nullptr, nullptr)) < 0) { sb::Log::sdl_error("could not render base onto duplicate"); return nullptr; } SDL_SetTextureBlendMode(base, original_blend_mode); SDL_SetTextureBlendMode(duplicate, original_blend_mode); return duplicate; } SDL_Texture* sb::get_remapped_texture( SDL_Renderer* renderer, SDL_Texture* base, const std::map& map) { SDL_Texture* remapped = duplicate_texture(renderer, base); if (remapped == nullptr) { sb::Log::sdl_error("could not duplicate base texture"); return nullptr; } if ((SDL_SetRenderTarget(renderer, remapped)) < 0) { sb::Log::sdl_error("could not set render target to remapped texture"); return nullptr; } Pixels pixels = Pixels(renderer, remapped); for (int x = 0; x < pixels.rect.w; x++) { for (int y = 0; y < pixels.rect.h; y++) { for (auto& [original, replacement] : map) { if (pixels.get(x, y) == original) { pixels.set(replacement, x, y); } } } } pixels.apply(); return remapped; } SDL_Texture* sb::get_remapped_texture( SDL_Renderer* renderer, const std::string& path, const std::map& map) { SDL_Texture* base = IMG_LoadTexture(renderer, path.c_str()); if (base == nullptr) { sb::Log::sdl_error("error loading file"); return nullptr; } SDL_Texture* remapped = get_remapped_texture(renderer, base, map); if (remapped == nullptr) { sb::Log::log("could not remap texture", sb::Log::ERR); return nullptr; } SDL_DestroyTexture(base); return remapped; } #include "superxbr.cpp" /* - Base texture must be set to SDL_TEXTUREACCESS_TARGET - Scale2x implementation based on http://www.scale2x.it/algorithm.html */ SDL_Texture* sb::get_pixel_scaled_texture(SDL_Renderer* renderer, SDL_Texture* base, int count, int version) { if ((SDL_SetRenderTarget(renderer, base)) < 0) { sb::Log::sdl_error("could not set render target to remapped texture"); return nullptr; } glm::ivec2 size = get_texture_box(base).size(); Uint32 format = SDL_PIXELFORMAT_RGBA32; int bytes_per_pixel, bytes_per_row, bytes_total; Uint32 *src, *dst, *src_begin, *dst_begin; for (int ii = 0; ii < count; ii++, size *= 2) { bytes_per_pixel = SDL_BYTESPERPIXEL(format); bytes_per_row = bytes_per_pixel * size.x; bytes_total = bytes_per_row * size.y; if (ii == 0) { src = new Uint32[size.x * size.y]; src_begin = src; if ((SDL_RenderReadPixels(renderer, NULL, format, src, bytes_per_row)) < 0) { sb::Log::sdl_error("could not read pixels after setting remapped texture as target"); return NULL; } } else { src = dst_begin; src_begin = src; } dst = new Uint32[size.x * size.y * 4]; dst_begin = dst; if (version == scaler::scale2x) { Uint32 A, B, C, D, E, F, G, H, I; for (int y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++) { E = *src; B = y == 0 ? E : *(src - size.x); D = x == 0 ? E : *(src - 1); F = x == size.x - 1 ? E : *(src + 1); H = y == size.y - 1 ? E : *(src + size.x); if (y != 0 && x != 0 && y != size.y - 1 && x != size.x - 1) { A = *(src - size.x - 1); C = *(src - size.x + 1); G = *(src + size.x - 1); I = *(src + size.x + 1); } if (x == 0) { A = B; G = H; } if (y == 0) { A = D; C = F; } if (x == size.x - 1) { C = B; I = H; } if (y == size.y - 1) { G = D; I = F; } if (B != H && D != F) { *dst = D == B ? D : E; *(dst + 1) = B == F ? F : E; *(dst + 2 * size.x) = D == H ? D : E; *(dst + 2 * size.x + 1) = H == F ? F : E; } else { *dst = E; *(dst + 1) = E; *(dst + 2 * size.x) = E; *(dst + 2 * size.x + 1) = E; } src++; dst += 2; } dst += 2 * size.x; } } else if (version == scaler::xbr) { scaleSuperXBRT<2>(src, dst, size.x, size.y); } delete[] src_begin; } SDL_Texture* scaled = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y); if (scaled == nullptr) { sb::Log::sdl_error("could not create scaled texture"); } if (SDL_UpdateTexture(scaled, nullptr, dst_begin, bytes_per_row * 2) < 0) { sb::Log::sdl_error("could not copy pixels to scaled texture"); } delete[] dst_begin; return scaled; } std::vector sb::glob(fs::path query) { fs::path basename = query.parent_path(); if (basename == "") { basename = "."; } std::regex expression(query.string()); std::vector files; for (auto& entry: fs::directory_iterator(basename)) { if (std::regex_match(entry.path().string(), expression)) { files.push_back(entry.path()); } } std::sort(files.begin(), files.end()); return files; } SDL_Surface* sb::get_surface_from_pixels(Pixels& pixels) { SDL_Surface* surface = SDL_CreateRGBSurfaceFrom( pixels.source, pixels.rect.w, pixels.rect.h, pixels.format->BitsPerPixel, pixels.get_bytes_per_row(), pixels.format->Rmask, pixels.format->Gmask, pixels.format->Bmask, pixels.format->Amask); if (surface == nullptr) { sb::Log::sdl_error("could not create RGB surface from texture pixel data"); } else { return surface; } return nullptr; } fs::path sb::get_next_file_name(fs::path directory, int zfill, std::string prefix, std::string extension) { std::stringstream file_pattern; file_pattern << prefix << "([0-9]+)" << extension; fs::path query = directory / file_pattern.str(); std::vector files = glob(query); int index = 1; if (files.size()) { const std::string last = files.back().string(); std::smatch matches; std::regex_match(last, matches, std::regex(query.string())); index = std::stoi(matches[1]) + 1; } std::stringstream filename; fs::path path; do { filename << prefix << pad(index++, zfill) << extension; path = directory / filename.str(); filename.str(""); filename.clear(); } while (fs::exists(path)); return path; } std::string sb::file_to_string(const fs::path& path) { std::string contents = ""; #if !defined(__ANDROID__) && !defined(ANDROID) std::ostringstream log_message; if (!fs::exists(path)) { log_message << "No file found at " << path; sb::Log::log(log_message, sb::Log::Level::WARN); } else { std::ifstream file; file.open(path); if (!file.is_open()) { log_message << "Failed to open " << path; sb::Log::log(log_message, sb::Log::Level::WARN); } else { /* Read file using std::string's range constructor, from the beginning of the file stream to end of stream (represented * by {} (?)) */ contents = std::string(std::istreambuf_iterator(file), {}); std::size_t size = file.tellg(); log_message << "Opened file " << path << " (" << (size / 1000.0f) << "KB)"; sb::Log::log(log_message); sb::Log::log(contents, sb::Log::Level::DEBUG); } } #else std::unique_ptr sdl_rw(SDL_RWFromFile(path.c_str(), "r"), SDL_RWclose); if (sdl_rw.get() == nullptr) { __android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "Unable to open file %s", SDL_GetError()); } else { int byte_count = SDL_RWsize(sdl_rw.get()); __android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "File at %s is %fKB", path.c_str(), (byte_count / 1000.0f)); contents.resize(byte_count); int nb_read_total = 0, nb_read = 1; while (nb_read_total < byte_count && nb_read != 0) { nb_read = SDL_RWread(sdl_rw.get(), &contents[nb_read_total], 1, (byte_count - nb_read_total)); nb_read_total += nb_read; } if (nb_read_total != byte_count) { __android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "File could not be read because of a mismatch in file size and number of bytes read"); } else { __android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "%s", contents.c_str()); } } #endif return contents; } fs::path sb::copy_file(fs::path from, fs::path to, bool overwrite_ok) { /* Open source */ SDL_RWops* source_rw; fs::path destination; if ((source_rw = SDL_RWFromFile(from.string().c_str(), "r")) != nullptr) { std::ostringstream message; message << "Copying " << (SDL_RWsize(source_rw) / 1000) << " KB from " << from << " to " << to; sb::Log::log(message); /* Allocate storage for source contents */ std::string content; content.resize(SDL_RWsize(source_rw)); /* Read entire contents in one call and ensure the entire size in bytes was read. */ if (SDL_RWread(source_rw, content.data(), 1, SDL_RWsize(source_rw)) == static_cast(SDL_RWsize(source_rw))) { SDL_RWclose(source_rw); /* Append the filename of the source file to the destination path if destination is a directory. */ if (fs::is_directory(to)) { to /= from.filename(); } if (!fs::exists(to) || overwrite_ok) { /* Open destination, write entire contents in one call and ensure the entire size in bytes was written. */ SDL_RWops* to_rw; if ((to_rw = SDL_RWFromFile(to.string().c_str(), "w")) != nullptr) { if ((SDL_RWwrite(to_rw, content.data(), 1, content.size())) == content.size()) { std::ostringstream message; message << "Wrote CA bundle to internal storage at " << to; sb::Log::log(message); destination = to; } else { sb::Log::sdl_error("Error writing to internal storage"); } SDL_RWclose(to_rw); } else { sb::Log::sdl_error("Error opening internal storage for writing"); } } else { sb::Log::log("Could not copy file: destination already exists and overwrite was not set", sb::Log::WARN); } } else { sb::Log::sdl_error("Error reading file"); } } else { sb::Log::sdl_error("Error getting file handle"); } return destination; } std::shared_ptr sb::extract_area(const std::shared_ptr& source, const sb::Box& area) { /* Create a destination surface with the same format as the source and is the size of the tile. */ std::shared_ptr destination { SDL_CreateRGBSurfaceWithFormat(source->flags, area.w, area.h, source->format->BitsPerPixel, source->format->format), SDL_FreeSurface}; if (destination.get() != nullptr) { /* Blit the source onto the destination */ SDL_Rect rect = area; if (SDL_BlitSurface(source.get(), &rect, destination.get(), nullptr) == 0) { /* Rotate and mirror the tile for compatibility with OpenGL */ std::shared_ptr flipped {rotozoomSurfaceXY(destination.get(), 0, 1, -1, 0), SDL_FreeSurface}; if (flipped.get() != nullptr) { return flipped; } else { std::ostringstream message; message << "Could not rotate source surface for tile extraction. " << SDL_GetError(); throw std::runtime_error(message.str()); } } else { std::ostringstream message; message << "Could not blit source pixels to destination surface for tile extraction. " << SDL_GetError(); throw std::runtime_error(message.str()); } } else { std::ostringstream message; message << "Could not create destination surface for tile extraction. " << SDL_GetError(); throw std::runtime_error(message.str()); } } std::vector> sb::extract_tiles_by_count(const std::shared_ptr& source, const glm::ivec2& count) { glm::ivec2 source_size {source->w, source->h}; /* Get the floor of the division of the source size by the tile count. Then add one to each dimension that has any remainder. This leads * to a tile size that may be bigger than the available pixels in the last tile in the dimension. In that case, the last tile will contain * the rest of the available pixels and will be smaller than the tile size. */ glm::ivec2 tile_size {source_size / count + glm::ivec2(glm::bvec2(glm::mod(glm::fvec2(source_size), glm::fvec2(count))))}; /* Use SDL coordinate system for the area box, meaning at the top of the image y=0, and at the bottom y=height. The Box class uses GL * coordinates by default, but for image manipulation and SDL surfaces, the Box class supports flipping the Y-axis to use SDL coordinates. */ sb::Box area {{0.0f, 0.0f}, tile_size, false}; /* Iterate over rows and columns, moving the area by one tile size each iteration and copying that area into a new surface in the vector * of tiles. */ std::vector> tiles; for (int y = 0; y < count.y; y++) { area.left(0.0f); for (int x = 0; x < count.x; x++) { tiles.push_back(sb::extract_area(source, area)); area.move({tile_size.x, 0.0f}); } area.move({0.0f, tile_size.y}); } return tiles; } int SDL_SetRenderDrawColor(SDL_Renderer* renderer, const Color& color) { return SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); } int SDL_RenderFillRect(SDL_Renderer* renderer, const Box& box) { SDL_Rect rect = box; return SDL_RenderFillRect(renderer, &rect); } int lineColor(SDL_Renderer* renderer, const Segment& segment, const Color& color, std::uint8_t thickness) { if (thickness == 1) { return lineColor(renderer, segment.start().x, segment.start().y, segment.end().x, segment.end().y, color); } else { return thickLineColor(renderer, segment.start().x, segment.start().y, segment.end().x, segment.end().y, thickness, color); } } std::ostream& std::operator<<(std::ostream& out, const SDL_Color& color) { out << "{" << static_cast(color.r) << ", " << static_cast(color.g) << ", " << static_cast(color.b) << ", " << static_cast(color.a) << "}"; return out; }