From f099a82eb2b2a6bb64cd51d98ab027f4ce45078c Mon Sep 17 00:00:00 2001 From: hachem Date: Sun, 30 Aug 2026 17:59:14 +0200 Subject: feat: little pepe --- src/ui/animated_image.cpp | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/ui/animated_image.cpp (limited to 'src/ui/animated_image.cpp') diff --git a/src/ui/animated_image.cpp b/src/ui/animated_image.cpp new file mode 100644 index 0000000..d7c6536 --- /dev/null +++ b/src/ui/animated_image.cpp @@ -0,0 +1,207 @@ +#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]; + } +} -- cgit v1.3