Map pinching, revised (#12776)

* Improved precision of map pinching, was inaccurate

* Consolidated zoom() callback with pinch() to simplify. Added comments on the math.

---------

Co-authored-by: M. Rittweger <m.rittweger@mvolution.de>
This commit is contained in:
sulai 2025-01-11 19:08:00 +01:00 committed by GitHub
parent a5e6622414
commit 08d59dfc2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 31 deletions

View File

@ -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() {}
}

View File

@ -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()