2

I have a desktop application in which i want to implement zooming and panning behaviour, like in this question, but without scrollbars. However, i don't know how to allow the user to wheel-zoom on an exact point. I'm using Kotlin Compose Multiplatform 1.4.3

Panning works fine, but offset is calculated incorrectly after scaling. The mouse cursor should stay in the same position after zooming.

Here is my code:

fun main() = singleWindowApplication {
    Surface {
        Box {
            PanAndZoom(
                modifier = Modifier
                    .fillMaxSize()
            ) {
                Box(modifier = Modifier.size(300.dp).background(Color.Gray))
            }
        }
    }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PanAndZoom(
    modifier: Modifier = Modifier,
    state: PanAndZoomState = remember { PanAndZoomState() },
    content: @Composable BoxScope.() -> Unit
) {
    Box(
        modifier = modifier
            .onPointerEvent(PointerEventType.Scroll) {
                val change = it.changes.first()
                val delta = change.scrollDelta.y.toInt().sign
                val position = change.position

                val posBeforeZoom = state.screenToWorld(position)

                // Zooming
                state.scale(delta)

                val posAfterZoom = state.screenToWorld(position)
                
                // Incorrect offset calculation
                state.offset += (posBeforeZoom - posAfterZoom)
            }
            .pointerInput(Unit) {
                // Panning
                detectDragGestures { _, dragAmount ->
                    state.offset += dragAmount
                }
            }
            .onGloballyPositioned {
                state.size = it.size
            }
    ) {
        Box(
            modifier = Modifier
                .matchParentSize()
                .graphicsLayer {
                    with(state) {
                        // Applying transformations
                        scaleX = scale
                        scaleY = scale
                        translationX = offset.x
                        translationY = offset.y
                    }
                },
            contentAlignment = Alignment.Center,
            content = content
        )
    }
}

class PanAndZoomState {

    var size: IntSize = IntSize.Zero
    var offset by mutableStateOf(Offset.Zero)
    var scale by mutableStateOf(1f)

    fun scale(delta: Int) {
        scale = (scale * exp(delta * 0.2f)).coerceIn(0.25f, 1.75f)
    }

    fun screenToWorld(screenOffset: Offset): Offset {
        return (screenOffset * scale) + ((size.toOffset() * (1 - scale) ) / 2f)
    }

}

fun IntSize.toOffset(): Offset {
    return Offset(
        x = width.toFloat(),
        y = height.toFloat()
    )
}
Andrew
  • 21
  • 2

0 Answers0