diff --git a/src/main.cpp b/src/main.cpp index 7b0a8e4..5e96762 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -108,15 +108,35 @@ static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::stri return texture; } +enum class LevelBackgroundPhase { Idle, ZoomOut, ZoomIn }; + struct LevelBackgroundFader { SDL_Texture* currentTex = nullptr; SDL_Texture* nextTex = nullptr; int currentLevel = -1; int queuedLevel = -1; - float fadeElapsedMs = 0.0f; + float phaseElapsedMs = 0.0f; + float phaseDurationMs = 0.0f; float fadeDurationMs = Config::Gameplay::LEVEL_FADE_DURATION; + LevelBackgroundPhase phase = LevelBackgroundPhase::Idle; }; +static float getPhaseDurationMs(const LevelBackgroundFader& fader, LevelBackgroundPhase phase) { + const float total = std::max(1200.0f, fader.fadeDurationMs); + switch (phase) { + case LevelBackgroundPhase::ZoomOut: return total * 0.45f; + case LevelBackgroundPhase::ZoomIn: return total * 0.45f; + case LevelBackgroundPhase::Idle: + default: return 0.0f; + } +} + +static void setPhase(LevelBackgroundFader& fader, LevelBackgroundPhase nextPhase) { + fader.phase = nextPhase; + fader.phaseDurationMs = getPhaseDurationMs(fader, nextPhase); + fader.phaseElapsedMs = 0.0f; +} + static void destroyTexture(SDL_Texture*& tex) { if (tex) { SDL_DestroyTexture(tex); @@ -146,32 +166,90 @@ static bool queueLevelBackground(LevelBackgroundFader& fader, SDL_Renderer* rend destroyTexture(fader.nextTex); fader.nextTex = newTexture; fader.queuedLevel = level; - fader.fadeElapsedMs = 0.0f; if (!fader.currentTex) { + // First background load happens instantly. fader.currentTex = fader.nextTex; fader.currentLevel = fader.queuedLevel; fader.nextTex = nullptr; fader.queuedLevel = -1; + fader.phase = LevelBackgroundPhase::Idle; + fader.phaseElapsedMs = 0.0f; + fader.phaseDurationMs = 0.0f; + } else if (fader.phase == LevelBackgroundPhase::Idle) { + // Kick off fancy transition. + setPhase(fader, LevelBackgroundPhase::ZoomOut); } return true; } static void updateLevelBackgroundFade(LevelBackgroundFader& fader, float frameMs) { - if (!fader.currentTex || !fader.nextTex) { + if (fader.phase == LevelBackgroundPhase::Idle) { return; } - fader.fadeElapsedMs += frameMs; - if (fader.fadeElapsedMs >= fader.fadeDurationMs) { - destroyTexture(fader.currentTex); - fader.currentTex = fader.nextTex; - fader.currentLevel = fader.queuedLevel; - fader.nextTex = nullptr; - fader.queuedLevel = -1; - fader.fadeElapsedMs = 0.0f; + // Guard against missing textures + if (!fader.currentTex && !fader.nextTex) { + fader.phase = LevelBackgroundPhase::Idle; + return; } + + fader.phaseElapsedMs += frameMs; + if (fader.phaseElapsedMs < std::max(1.0f, fader.phaseDurationMs)) { + return; + } + + switch (fader.phase) { + case LevelBackgroundPhase::ZoomOut: + // After zoom-out, swap textures then start zoom-in. + if (fader.nextTex) { + destroyTexture(fader.currentTex); + fader.currentTex = fader.nextTex; + fader.currentLevel = fader.queuedLevel; + fader.nextTex = nullptr; + fader.queuedLevel = -1; + } + setPhase(fader, LevelBackgroundPhase::ZoomIn); + break; + case LevelBackgroundPhase::ZoomIn: + fader.phase = LevelBackgroundPhase::Idle; + fader.phaseElapsedMs = 0.0f; + fader.phaseDurationMs = 0.0f; + break; + case LevelBackgroundPhase::Idle: + default: + fader.phase = LevelBackgroundPhase::Idle; + break; + } +} + +static void renderScaledBackground(SDL_Renderer* renderer, SDL_Texture* tex, int winW, int winH, float scale, Uint8 alpha = 255) { + if (!renderer || !tex) { + return; + } + + scale = std::max(0.5f, scale); + SDL_FRect dest{ + (winW - winW * scale) * 0.5f, + (winH - winH * scale) * 0.5f, + winW * scale, + winH * scale + }; + + SDL_SetTextureAlphaMod(tex, alpha); + SDL_RenderTexture(renderer, tex, nullptr, &dest); + SDL_SetTextureAlphaMod(tex, 255); +} + +static void drawOverlay(SDL_Renderer* renderer, const SDL_FRect& rect, SDL_Color color, Uint8 alpha) { + if (!renderer || alpha == 0) { + return; + } + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha); + SDL_RenderFillRect(renderer, &rect); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); } static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Renderer* renderer, int winW, int winH) { @@ -180,22 +258,36 @@ static void renderLevelBackgrounds(const LevelBackgroundFader& fader, SDL_Render } SDL_FRect fullRect{0.f, 0.f, static_cast(winW), static_cast(winH)}; + const float duration = std::max(1.0f, fader.phaseDurationMs); + const float progress = (fader.phase == LevelBackgroundPhase::Idle) ? 0.0f : std::clamp(fader.phaseElapsedMs / duration, 0.0f, 1.0f); - if (fader.currentTex && fader.nextTex) { - const float duration = std::max(1.0f, fader.fadeDurationMs); - const float alpha = std::clamp(fader.fadeElapsedMs / duration, 0.0f, 1.0f); - - SDL_SetTextureAlphaMod(fader.currentTex, Uint8((1.0f - alpha) * 255.0f)); - SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect); - SDL_SetTextureAlphaMod(fader.currentTex, 255); - - SDL_SetTextureAlphaMod(fader.nextTex, Uint8(alpha * 255.0f)); - SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect); - SDL_SetTextureAlphaMod(fader.nextTex, 255); - } else if (fader.currentTex) { - SDL_RenderTexture(renderer, fader.currentTex, nullptr, &fullRect); - } else if (fader.nextTex) { - SDL_RenderTexture(renderer, fader.nextTex, nullptr, &fullRect); + switch (fader.phase) { + case LevelBackgroundPhase::ZoomOut: { + const float scale = 1.0f + progress * 0.15f; + if (fader.currentTex) { + renderScaledBackground(renderer, fader.currentTex, winW, winH, scale, Uint8((1.0f - progress * 0.4f) * 255.0f)); + drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, Uint8(progress * 200.0f)); + } + break; + } + case LevelBackgroundPhase::ZoomIn: { + const float scale = 1.10f - progress * 0.10f; + const Uint8 alpha = Uint8((0.4f + progress * 0.6f) * 255.0f); + if (fader.currentTex) { + renderScaledBackground(renderer, fader.currentTex, winW, winH, scale, alpha); + } + break; + } + case LevelBackgroundPhase::Idle: + default: + if (fader.currentTex) { + renderScaledBackground(renderer, fader.currentTex, winW, winH, 1.0f, 255); + } else if (fader.nextTex) { + renderScaledBackground(renderer, fader.nextTex, winW, winH, 1.0f, 255); + } else { + drawOverlay(renderer, fullRect, SDL_Color{0, 0, 0, 255}, 255); + } + break; } } @@ -204,7 +296,9 @@ static void resetLevelBackgrounds(LevelBackgroundFader& fader) { destroyTexture(fader.nextTex); fader.currentLevel = -1; fader.queuedLevel = -1; - fader.fadeElapsedMs = 0.0f; + fader.phaseElapsedMs = 0.0f; + fader.phaseDurationMs = 0.0f; + fader.phase = LevelBackgroundPhase::Idle; } // Hover state for level popup ( -1 = none, 0..19 = hovered level )