diff --git a/app/src/main/java/com/bitchat/android/ui/globe/GlobeState.kt b/app/src/main/java/com/bitchat/android/ui/globe/GlobeState.kt index 89efc689b..1ef738756 100644 --- a/app/src/main/java/com/bitchat/android/ui/globe/GlobeState.kt +++ b/app/src/main/java/com/bitchat/android/ui/globe/GlobeState.kt @@ -37,7 +37,8 @@ class GlobeState( private set var centerLon by mutableDoubleStateOf(if (startZoomedOut) GlobeMath.normalizeLon(targetLon - 70.0) else targetLon) private set - var zoom by mutableFloatStateOf(if (startZoomedOut) GlobeMath.MIN_ZOOM else 1f) + // The opening rotation starts a little smaller than the whole-globe view and grows into it. + var zoom by mutableFloatStateOf(if (startZoomedOut) INTRO_START_ZOOM else 1f) private set var precision by mutableIntStateOf(initialPrecision.coerceIn(1, GlobeMath.MAX_PRECISION)) private set @@ -93,7 +94,9 @@ class GlobeState( /** Continuous zoom from pinch gestures; precision follows automatically. */ fun zoomBy(factor: Float) { if (factor == 1f) return - zoom = (zoom * factor).coerceIn(GlobeMath.MIN_ZOOM, GlobeMath.MAX_ZOOM) + // An interrupted intro can leave the globe below the minimum; continue from there rather + // than snapping up, while never pinching further out than the current size. + zoom = (zoom * factor).coerceIn(minOf(GlobeMath.MIN_ZOOM, zoom), GlobeMath.MAX_ZOOM) syncPrecisionFromZoom() syncSelection() } @@ -192,7 +195,10 @@ class GlobeState( animateTo(centerLat, centerLon, targetZoom, p, durationMs = 450) } - /** Cinematic intro: spin and zoom from a far view into the target location. */ + /** + * Cinematic intro: turn to the target while gently zooming from [INTRO_START_ZOOM] into the + * frame for [targetPrecision] (the whole globe at level 1). + */ private fun playIntro(targetLat: Double, targetLon: Double, targetPrecision: Int) { if (baseRadiusPx <= 0f) { // viewport not ready yet; retry once attached to layout via caller @@ -255,6 +261,11 @@ class GlobeState( // clamping tighter would make syncSelection() encode the wrong cell. const val MIN_LAT = -89.0 const val MAX_LAT = 89.0 + /** + * Where the opening rotation's gentle zoom-in starts. Within [FLIGHT_ZOOM_RATIO] of the + * whole-globe view, so the intro rotates and zooms together on the glide path. + */ + const val INTRO_START_ZOOM = 0.72f /** Zoom changes beyond this factor fly along [GlobeFlight]; smaller ones glide. */ const val FLIGHT_ZOOM_RATIO = 1.5f const val FLIGHT_MS_PER_UNIT = 230.0 diff --git a/app/src/test/kotlin/com/bitchat/android/ui/globe/GlobeStateTest.kt b/app/src/test/kotlin/com/bitchat/android/ui/globe/GlobeStateTest.kt new file mode 100644 index 000000000..04ff97a75 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/ui/globe/GlobeStateTest.kt @@ -0,0 +1,56 @@ +package com.bitchat.android.ui.globe + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class GlobeStateTest { + + private fun introState() = GlobeState( + targetLat = 57.0, + targetLon = 10.0, + initialPrecision = 1, + startZoomedOut = true + ).apply { setViewport(baseRadiusPx = 475f, screenMinPx = 1080f) } + + @Test + fun intro_startsSlightlySmallerThanTheWholeGlobeAtLevelOne() { + val state = introState() + + assertTrue(state.zoom < GlobeMath.MIN_ZOOM) + assertTrue(state.zoom > 0.5f) + assertEquals(1, state.precision) + assertEquals(1, state.selectedGeohash.length) + } + + @Test + fun wholeGlobeFrame_isLevelOne() { + // The intro lands on the level-1 frame, which is the whole-globe view. + assertEquals( + GlobeMath.MIN_ZOOM, + GlobeMath.zoomForPrecision(1, baseRadiusPx = 475f, screenMinPx = 1080f) + ) + } + + @Test + fun pinchingAnInterruptedIntro_continuesWithoutSnapping() { + val state = introState() + val start = state.zoom + + state.zoomBy(1.1f) + assertEquals(start * 1.1f, state.zoom, 1e-4f) + + state.zoomBy(0.5f) + assertEquals(start * 1.1f, state.zoom, 1e-4f) + } + + @Test + fun pinchingOut_stopsAtTheWholeGlobe() { + val state = GlobeState(57.0, 10.0, initialPrecision = 1, startZoomedOut = false) + .apply { setViewport(475f, 1080f) } + + state.zoomBy(0.5f) + + assertEquals(GlobeMath.MIN_ZOOM, state.zoom) + } +}