diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt index e440b1c..0056e9b 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.ImageBitmap @@ -242,17 +243,28 @@ private fun ZoomableImage(image: ImageBitmap) { modifier = Modifier.fillMaxSize() .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> + detectTransformGestures { centroid, pan, zoom, _ -> // Floor of 1 so the image cannot be pinched smaller than fitted, which is // already the whole of it; a ceiling so it cannot be lost off-screen. - scale = (scale * zoom).coerceIn(1f, 8f) - if (scale > 1f) { - offsetX += pan.x - offsetY += pan.y + val oldScale = scale + val newScale = (oldScale * zoom).coerceIn(1f, 8f) + if (newScale > 1f) { + val offset = + zoomOffset( + Offset(offsetX, offsetY), + centroid, + pan, + oldScale, + newScale, + Offset(size.width / 2f, size.height / 2f), + ) + offsetX = offset.x + offsetY = offset.y } else { offsetX = 0f offsetY = 0f } + scale = newScale } } .graphicsLayer { @@ -263,3 +275,16 @@ private fun ZoomableImage(image: ImageBitmap) { }, ) } + +/** Keeps the image point beneath [centroid] beneath the fingers as its scale changes. */ +internal fun zoomOffset( + offset: Offset, + centroid: Offset, + pan: Offset, + oldScale: Float, + newScale: Float, + viewportCenter: Offset, +): Offset { + val scaleChange = newScale / oldScale + return offset * scaleChange + (centroid - viewportCenter) * (1f - scaleChange) + pan +} diff --git a/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt b/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt new file mode 100644 index 0000000..a7fff9c --- /dev/null +++ b/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt @@ -0,0 +1,37 @@ +package com.example.aiapp + +import androidx.compose.ui.geometry.Offset +import kotlin.test.Test +import kotlin.test.assertEquals + +class SessionImageTest { + @Test + fun `zoom keeps the region panned to in the center`() { + assertEquals( + Offset(240f, -160f), + zoomOffset( + offset = Offset(120f, -80f), + centroid = Offset(500f, 1000f), + pan = Offset.Zero, + oldScale = 2f, + newScale = 4f, + viewportCenter = Offset(500f, 1000f), + ), + ) + } + + @Test + fun `zoom keeps an off-center pinch beneath moving fingers`() { + assertEquals( + Offset(460f, 420f), + zoomOffset( + offset = Offset(100f, -100f), + centroid = Offset(250f, 400f), + pan = Offset(10f, 20f), + oldScale = 2f, + newScale = 4f, + viewportCenter = Offset(500f, 1000f), + ), + ) + } +}