From 73c7c8942e6d8305dbdf783dd5563c36e24f2e53 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 4 Apr 2026 22:33:10 +0900 Subject: [PATCH] title screen fade-in transition --- .idea/csv-editor.xml | 23 ------ CLAUDE.md | 2 +- src/net/torvald/terrarum/App.java | 2 +- .../terrarum/modulebasegame/TitleScreen.kt | 74 ++++++++++++++++--- 4 files changed, 67 insertions(+), 34 deletions(-) delete mode 100644 .idea/csv-editor.xml diff --git a/.idea/csv-editor.xml b/.idea/csv-editor.xml deleted file mode 100644 index 9c0d2513d..000000000 --- a/.idea/csv-editor.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index e25a7446d..e41379fd5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,5 +172,5 @@ Both save/restore `camera.position` and call `setToOrtho` with `App.scr.wf/hf` o - **`lateinit var` guards**: Use `::prop.isInitialized` checks in `resize()` and `dispose()` for properties set during async loading steps. - **`ConsistentUpdateRate`**: Accumulates delta time; may call `updateFunction` multiple times before `renderFunction`. Call `.reset()` before transitioning to active rendering to avoid catch-up storms. - **Textures**: Use TGA format. Images with semitransparency must be TGA; opaque images may be PNG. -- **Coordinate system**: Y-down (camera `setToOrtho(true, ...)`). `setCameraPosition(0, 0)` places origin at screen top-left. +- **Coordinate system**: Y-down (camera `setToOrtho(true, ...)`). `setCameraPosition(0, 0)` places origin at screen top-left, which is not LibGDX nor OpenGL works natively, hence the existence of `FlippingSpriteBatch`. If the user reports renders are flipped vertically, try draw()/drawFlipped() accordingly. - **ROUNDWORLD**: World wraps horizontally. `WorldCamera.x` uses `fmod worldWidth`. Lightmap and tile drawing account for wrapping. diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index e3a2e2aff..9e3f53966 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -900,7 +900,7 @@ public class App implements ApplicationListener { logoBatch.end(); } - int logoPosX = (int)(logoPosX0 + Math.round(100 * Math.sin(GLOBAL_RENDER_TIMER / 50.0))); + int logoPosX = logoPosX0;//(int)(logoPosX0 + Math.round(100 * Math.sin(GLOBAL_RENDER_TIMER / 50.0))); // draw logo reflection logoBatch.setShader(shaderReflect); diff --git a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt index f8bfc6e63..f7297f123 100644 --- a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt +++ b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt @@ -5,6 +5,7 @@ import com.badlogic.gdx.Input import com.badlogic.gdx.InputAdapter import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.OrthographicCamera +import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.g2d.TextureRegion @@ -71,6 +72,10 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) { private var glLoadStep = 0 private lateinit var titleLoadingThread: Thread + private var splashCapture: Texture? = null + private var settleFrames = 0 + private var transitionState = 0 // 0=loading, 1=settling, 2=fading, 3=done + private lateinit var demoWorld: GameWorld private lateinit var cameraNodes: FloatArray // camera Y-pos private val cameraNodeWidth = 15 @@ -314,23 +319,73 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) { } - private val introUncoverTime: Second = 0.3f - private var introUncoverDeltaCounter = 0f + private val introFadeTime: Second = 0.5f + private var introFadeDeltaCounter = 0f + private val settleFrameCount = 12 override fun renderImpl(updateRate: Float) { - if (!loadDone) { - App.drawSplash() - processGLLoadStep() - return + when (transitionState) { + // Loading phase: show splash, advance GL load steps + 0 -> { + App.drawSplash() + processGLLoadStep() + if (loadDone) { + // capture the current framebuffer (splash) as a texture + val pixmap = Pixmap.createFromFrameBuffer(0, 0, App.scr.width, App.scr.height) + splashCapture = Texture(pixmap) + pixmap.dispose() + transitionState = 1 + settleFrames = 0 + } + } + // Settle phase: render title screen normally but paint captured splash on top + 1 -> { + renderTitleScreen() + drawSplashCapture(1f) + settleFrames++ + if (settleFrames >= settleFrameCount) { + transitionState = 2 + introFadeDeltaCounter = 0f + } + } + // Fade phase: crossfade from captured splash to title screen + 2 -> { + renderTitleScreen() + introFadeDeltaCounter += Gdx.graphics.deltaTime + val opacity = 1f - (introFadeDeltaCounter / introFadeTime).coerceIn(0f, 1f) + drawSplashCapture(opacity) + if (introFadeDeltaCounter >= introFadeTime) { + splashCapture?.dispose() + splashCapture = null + transitionState = 3 + } + } + // Normal rendering + 3 -> { + renderTitleScreen() + } } + } + private fun renderTitleScreen() { IngameRenderer.setRenderedWorld(demoWorld) - - super.renderImpl(updateRate) - // async update and render + super.renderImpl(0f) gameUpdateGovernor.update(Gdx.graphics.deltaTime, App.UPDATE_RATE, updateScreen, renderScreen) } + private fun drawSplashCapture(opacity: Float) { + val tex = splashCapture ?: return + setCameraPosition(0f, 0f) + blendNormalStraightAlpha(batch) + batch.inUse { + batch.shader = null + batch.color = Color(1f, 1f, 1f, opacity) + // framebuffer capture is Y-flipped; draw flipped + batch.drawFlipped(tex, 0f, 0f) + batch.color = Color.WHITE + } + } + private val updateScreen = { delta: Float -> demoWorld.globalLight = WeatherMixer.globalLightNow @@ -586,6 +641,7 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) { override fun dispose() { if (::uiRemoCon.isInitialized) uiRemoCon.dispose() if (::demoWorld.isInitialized) demoWorld.dispose() + splashCapture?.dispose() warning32bitJavaIcon.texture.dispose() warningAppleRosettaIcon.texture.dispose() }