#include "ui/animated_image.h" #include "core/log.h" #include #define STB_IMAGE_IMPLEMENTATION #define STBI_ONLY_GIF #define STBI_ONLY_PNG #define STBI_ONLY_JPEG #define STBI_ONLY_BMP #include #include #include #include #include namespace Hsdbg { namespace { constexpr float MIN_FRAME_SECONDS = 0.02f; auto bleed_edges(unsigned char* rgba, int width, int height) -> void { for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { unsigned char* pixel = rgba + (static_cast(y) * width + x) * 4; if (pixel[3] != 0) continue; int red = 0; int green = 0; int blue = 0; int count = 0; for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { const int nx = x + dx; const int ny = y + dy; if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue; const unsigned char* near = rgba + (static_cast(ny) * width + nx) * 4; if (near[3] == 0) continue; red += near[0]; green += near[1]; blue += near[2]; ++count; } } if (count == 0) continue; pixel[0] = static_cast(red / count); pixel[1] = static_cast(green / count); pixel[2] = static_cast(blue / count); } } } auto upload(const unsigned char* rgba, int width, int height) -> unsigned int { unsigned int texture = 0; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); glBindTexture(GL_TEXTURE_2D, 0); return texture; } auto read_file(const std::filesystem::path& path) -> std::vector { std::ifstream file(path, std::ios::binary); if (!file.is_open()) return {}; return std::vector(std::istreambuf_iterator(file), std::istreambuf_iterator()); } } AnimatedImage::~AnimatedImage() { unload(); } auto AnimatedImage::unload() -> void { if (!m_frames.empty()) glDeleteTextures(static_cast(m_frames.size()), m_frames.data()); m_frames.clear(); m_delays.clear(); m_width = 0; m_height = 0; m_current = 0; m_accumulator = 0.0f; } auto AnimatedImage::load(const std::filesystem::path& path) -> bool { unload(); const std::vector bytes = read_file(path); if (bytes.empty()) { Log::warn("image: could not read '{}'", path.string()); return false; } const auto length = static_cast(bytes.size()); int width = 0; int height = 0; int comp = 0; int frames = 0; int* delays = nullptr; if (stbi_uc* data = stbi_load_gif_from_memory(bytes.data(), length, &delays, &width, &height, &frames, &comp, 4)) { m_width = width; m_height = height; for (int index = 0; index < frames; ++index) { stbi_uc* frame = data + static_cast(index) * width * height * 4; bleed_edges(frame, width, height); m_frames.push_back(upload(frame, width, height)); const float seconds = delays != nullptr ? static_cast(delays[index]) / 1000.0f : 0.1f; m_delays.push_back(std::max(seconds, MIN_FRAME_SECONDS)); } stbi_image_free(data); std::free(delays); } else if (stbi_uc* still = stbi_load_from_memory(bytes.data(), length, &width, &height, &comp, 4)) { m_width = width; m_height = height; bleed_edges(still, width, height); m_frames.push_back(upload(still, width, height)); m_delays.push_back(MIN_FRAME_SECONDS); stbi_image_free(still); } else { Log::warn("image: could not decode '{}': {}", path.string(), stbi_failure_reason()); return false; } Log::info("image: loaded '{}' ({}x{}, {} frame{})", path.string(), m_width, m_height, m_frames.size(), m_frames.size() == 1 ? "" : "s"); return true; } auto AnimatedImage::advance(float delta_seconds) -> void { if (m_frames.size() < 2) return; m_accumulator += delta_seconds; for (int step = 0; step < 1000 && m_accumulator >= m_delays[m_current]; ++step) { m_accumulator -= m_delays[m_current]; m_current = (m_current + 1) % m_frames.size(); } } auto AnimatedImage::texture() const -> unsigned int { return m_frames.empty() ? 0 : m_frames[m_current]; } }