#include "end_screen.hpp" // ───────────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────────── float EndScreen::Lerp(float a, float b, float t) { return a + (b - a) * t; } // All images are 460×460 and share the same centred offset. SDL_FPoint EndScreen::LayerOffset() const { if (!assets[0].surface) return {0.0f, 0.0f}; float ox = (screen_w - assets[0].surface->w) / 2.0f; float oy = (screen_h - assets[0].surface->h) / 2.0f; return {ox, oy}; } // ───────────────────────────────────────────────────────────────────────────── // Construction / Destruction // ───────────────────────────────────────────────────────────────────────────── EndScreen::EndScreen(SDL_Renderer* r) : renderer(r) { Init(); } EndScreen::~EndScreen() { for (auto& a : assets) if (a.surface) SDL_DestroySurface(a.surface); } // ───────────────────────────────────────────────────────────────────────────── // Init // ───────────────────────────────────────────────────────────────────────────── void EndScreen::Init() { SDL_GetRenderOutputSize(renderer, &screen_w, &screen_h); const char* paths[NUM_LAYERS] = { "quit/quit_text_to.png", "quit/quit_text_be.png", "quit/quit_text_continued.png", "quit/quit_text_nope.png", }; for (int i = 0; i < NUM_LAYERS; ++i) { assets[i].surface = IMG_Load(paths[i]); layer_state[i] = LayerState::WAITING; layer_start_tick[i]= 0; layer_due[i] = 0; } rng.seed(std::random_device{}()); last_time = SDL_GetTicks(); // Extract particles for every layer before starting for (int i = 0; i < NUM_LAYERS; ++i) BuildParticles(i); // Fire layer 0 immediately StartLayer(0); } // ───────────────────────────────────────────────────────────────────────────── // BuildParticles — extract visible pixels from a surface into EndParticles. // current is initialised to screen-centre; origin is the final screen pos. // ───────────────────────────────────────────────────────────────────────────── void EndScreen::BuildParticles(int idx) { auto& surf = assets[idx].surface; if (!surf) return; const SDL_PixelFormatDetails* details = SDL_GetPixelFormatDetails(surf->format); Uint32* pixels = static_cast(surf->pixels); int pitch = surf->pitch / (int)sizeof(Uint32); int w = surf->w, h = surf->h; SDL_FPoint off = LayerOffset(); // same for every layer auto& parts = layer_particles[idx]; parts.clear(); parts.reserve(static_cast(w * h / 4)); // rough upper bound for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { Uint32 p = pixels[y * pitch + x]; Uint8 r, g, b, a; SDL_GetRGBA(p, details, nullptr, &r, &g, &b, &a); if (a > 100) { EndParticle ep; ep.origin = { off.x + static_cast(x), off.y + static_cast(y) }; ep.current = { screen_w / 2.0f, screen_h / 2.0f }; parts.push_back(ep); } } } } // ───────────────────────────────────────────────────────────────────────────── // StartLayer — begin the spread-in animation for one layer // ───────────────────────────────────────────────────────────────────────────── void EndScreen::StartLayer(int idx) { if (idx < 0 || idx >= NUM_LAYERS) return; layer_state[idx] = LayerState::SPREADING_IN; layer_start_tick[idx] = SDL_GetTicks(); // All particles burst from screen centre for (auto& ep : layer_particles[idx]) ep.current = { screen_w / 2.0f, screen_h / 2.0f }; } // ───────────────────────────────────────────────────────────────────────────── // UpdateScreen — call on window resize // ───────────────────────────────────────────────────────────────────────────── void EndScreen::UpdateScreen() { SDL_GetRenderOutputSize(renderer, &screen_w, &screen_h); for (int i = 0; i < NUM_LAYERS; ++i) BuildParticles(i); // Re-centre any currently-spreading layer for (int i = 0; i < NUM_LAYERS; ++i) { if (layer_state[i] == LayerState::SPREADING_IN) { for (auto& ep : layer_particles[i]) ep.current = { screen_w / 2.0f, screen_h / 2.0f }; } } } // ───────────────────────────────────────────────────────────────────────────── // Update // ───────────────────────────────────────────────────────────────────────────── void EndScreen::Update() { Uint64 now = SDL_GetTicks(); last_time = now; // ── Advance any spreading layer ────────────────────────────────────────── for (int i = 0; i < NUM_LAYERS; ++i) { if (layer_state[i] != LayerState::SPREADING_IN) continue; float t = static_cast(now - layer_start_tick[i]) / static_cast(SPREAD_DURATION_MS); if (t >= 1.0f) { // Snap to final positions for (auto& ep : layer_particles[i]) ep.current = ep.origin; layer_state[i] = LayerState::VISIBLE; // Schedule the next layer if (i + 1 < NUM_LAYERS) { Uint64 gap = 0; if (i == 0) gap = GAP_AFTER_0_MS; else if (i == 1) gap = GAP_AFTER_1_MS; else if (i == 2) gap = GAP_AFTER_2_MS; layer_due[i + 1] = now + gap; } } else { float cx = screen_w / 2.0f; float cy = screen_h / 2.0f; for (auto& ep : layer_particles[i]) { ep.current.x = Lerp(cx, ep.origin.x, t); ep.current.y = Lerp(cy, ep.origin.y, t); } } } // ── Trigger any WAITING layer whose due time has arrived ───────────────── for (int i = 1; i < NUM_LAYERS; ++i) { if (layer_state[i] == LayerState::WAITING && layer_due[i] > 0 && now >= layer_due[i]) { StartLayer(i); } } // ── End condition: last layer fully visible ─────────────────────────────── if (layer_state[NUM_LAYERS - 1] == LayerState::VISIBLE) { if (!end_timer_started) { end_timer_started = true; end_timer_start = now; } else if (now - end_timer_start >= END_PAUSE_MS) { running = false; } } } // ───────────────────────────────────────────────────────────────────────────── // Render // Strategy: every non-WAITING layer is drawn as white 1×1 particle rects. // VISIBLE layers have all particles snapped to their origins, so they look // like a solid rendered image. No SDL_RenderTexture is used at all, which // means there is zero risk of one layer's texture rect overwriting another. // ───────────────────────────────────────────────────────────────────────────── void EndScreen::Render() { SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); for (int i = 0; i < NUM_LAYERS; ++i) { if (layer_state[i] == LayerState::WAITING) continue; // Draw every particle — works for both SPREADING_IN and VISIBLE states. for (const auto& ep : layer_particles[i]) { SDL_FRect r { ep.current.x, ep.current.y, 1.0f, 1.0f }; SDL_RenderFillRect(renderer, &r); } } SDL_RenderPresent(renderer); } // ───────────────────────────────────────────────────────────────────────────── // Event loop // ───────────────────────────────────────────────────────────────────────────── namespace EndScreen_Event { void event_Loop(SDL_Renderer* renderer) { EndScreen* scene = new EndScreen(renderer); while (scene->running) { SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_EVENT_QUIT) scene->running = false; if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) scene->running = false; if (e.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED || e.type == SDL_EVENT_WINDOW_RESIZED) scene->UpdateScreen(); } scene->Update(); scene->Render(); } delete scene; } }