diff --git a/core/src/com/unciv/ui/components/ZoomGestureListener.kt b/core/src/com/unciv/ui/components/ZoomGestureListener.kt index 3dde8e68be..508c10019c 100644 --- a/core/src/com/unciv/ui/components/ZoomGestureListener.kt +++ b/core/src/com/unciv/ui/components/ZoomGestureListener.kt @@ -7,12 +7,22 @@ import com.badlogic.gdx.scenes.scene2d.EventListener import com.badlogic.gdx.scenes.scene2d.InputEvent open class ZoomGestureListener( - halfTapSquareSize: Float, tapCountInterval: Float, longPressDuration: Float, maxFlingDelay: Float + halfTapSquareSize: Float, + tapCountInterval: Float, + longPressDuration: Float, + maxFlingDelay: Float, + stageSize: () -> Vector2 ) : EventListener { private val detector: GestureDetector - constructor() : this(20f, 0.4f, 1.1f, Int.MAX_VALUE.toFloat()) + constructor(stageSize: () -> Vector2) : this( + 20f, + 0.4f, + 1.1f, + Int.MAX_VALUE.toFloat(), + stageSize + ) init { detector = GestureDetector( @@ -22,8 +32,10 @@ open class ZoomGestureListener( maxFlingDelay, object : GestureDetector.GestureAdapter() { - private var pinchCenter: Vector2? = null - private var initialDistance = 0f + // focal point, the center of the two pointers where zooming should be directed to + private var lastFocus: Vector2? = null + // distance between the two pointers performing a pinch + private var lastDistance = 0f override fun pinch( stageInitialPointer1: Vector2, @@ -31,20 +43,44 @@ open class ZoomGestureListener( stagePointer1: Vector2, stagePointer2: Vector2 ): Boolean { - if (pinchCenter == null) { - pinchCenter = stageInitialPointer1.cpy().add(stageInitialPointer2).scl(.5f) - initialDistance = stageInitialPointer1.dst(stageInitialPointer2) + + if (lastFocus == null) { + lastFocus = stageInitialPointer1.cpy().add(stageInitialPointer2).scl(.5f) + lastDistance = stageInitialPointer1.dst(stageInitialPointer2) } - val currentCenter = stagePointer1.cpy().add(stagePointer2).scl(.5f) - val delta = currentCenter.cpy().sub(pinchCenter) - pinchCenter = currentCenter.cpy() - this@ZoomGestureListener.pinch(delta) - this@ZoomGestureListener.zoom(initialDistance, stagePointer1.dst(stagePointer2)) + + // the current focal point is the center of the two pointers + val currentFocus = stagePointer1.cpy().add(stagePointer2).scl(.5f) + + // translation caused by moving the focal point + val translation = currentFocus.cpy().sub(lastFocus) + lastFocus = currentFocus.cpy() + + // scale change caused by changing distance of the two pointers + val currentDistance = stagePointer1.dst(stagePointer2) + val scaleChange = currentDistance / lastDistance + lastDistance = currentDistance + + // Calculate the translation (dx, dy) needed to direct the zoom towards to + // current focal point. Without this correction, the zoom would be directed + // towards the center of the stage. + // - First we calculate the distance from the stage center to the focal point. + // - We then calculate how much the scaling affects that distance by multiplying + // with scaleChange - 1. + val dx = (stageSize().x / 2 - currentFocus.x) * (scaleChange - 1) + val dy = (stageSize().y / 2 - currentFocus.y) * (scaleChange - 1) + + // Add the translation caused by changing the scale (dx, dy) to the translation + // caused by changing the position of the focal point. + translation.add(dx, dy) + + this@ZoomGestureListener.pinch(translation, scaleChange) + return true } override fun pinchStop() { - pinchCenter = null + lastFocus = null this@ZoomGestureListener.pinchStop() } }) @@ -83,7 +119,7 @@ open class ZoomGestureListener( } open fun scrolled(amountX: Float, amountY: Float): Boolean { return false } - open fun zoom(initialDistance: Float, distance: Float) {} - open fun pinch(delta: Vector2) {} + /** [translation] in stage coordinates */ + open fun pinch(translation: Vector2, scaleChange: Float) {} open fun pinchStop() {} } diff --git a/core/src/com/unciv/ui/components/widgets/ZoomableScrollPane.kt b/core/src/com/unciv/ui/components/widgets/ZoomableScrollPane.kt index 073fe79e04..efa8a46656 100644 --- a/core/src/com/unciv/ui/components/widgets/ZoomableScrollPane.kt +++ b/core/src/com/unciv/ui/components/widgets/ZoomableScrollPane.kt @@ -19,7 +19,6 @@ import com.unciv.UncivGame import com.unciv.models.metadata.GameSettings import com.unciv.ui.components.ZoomGestureListener import com.unciv.ui.components.input.KeyboardPanningListener -import kotlin.math.sqrt open class ZoomableScrollPane( @@ -174,7 +173,7 @@ open class ZoomableScrollPane( return zoomListener.isZooming } - inner class ZoomListener : ZoomGestureListener() { + inner class ZoomListener : ZoomGestureListener({ Vector2(stage.width, stage.height) }) { inner class ZoomAction : TemporalAction() { @@ -204,8 +203,6 @@ open class ZoomableScrollPane( } private var zoomAction: ZoomAction? = null - private var lastInitialDistance = 0f - var lastScale = 1f var isZooming = false fun zoomOut(zoomMultiplier: Float = 0.82f) { @@ -246,16 +243,17 @@ open class ZoomableScrollPane( } } - override fun pinch(delta: Vector2) { + override fun pinch(translation: Vector2, scaleChange: Float) { if (!isZooming) { isZooming = true onZoomStartListener?.invoke() } scrollTo( - scrollX - delta.x, - scrollY + delta.y, + scrollX - translation.x / scaleX, + scrollY + translation.y / scaleY, true ) + zoom(scaleX * scaleChange) } override fun pinchStop() { @@ -263,15 +261,6 @@ open class ZoomableScrollPane( onZoomStopListener?.invoke() } - override fun zoom(initialDistance: Float, distance: Float) { - if (lastInitialDistance != initialDistance) { - lastInitialDistance = initialDistance - lastScale = scaleX - } - val scale: Float = sqrt((distance / initialDistance).toDouble()).toFloat() * lastScale - zoom(scale) - } - override fun scrolled(amountX: Float, amountY: Float): Boolean { if (amountX > 0 || amountY > 0) zoomOut()