From 2736d42e749b24343da4098ae67b7bfd5a001207 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 28 Jan 2026 16:02:43 +0100 Subject: [PATCH] Fix gesture tracking: 1:1 rotation and drag, better zone detection Rotation: - Use direct angle calculation between touch points (atan2) - Track initial touch angle and effect angle separately - Effect rotation now matches finger rotation exactly Position drag: - Remove all sensitivity dampening - 1:1 mapping: finger moves 100px, effect center moves 100px Gesture zones rebalanced: - Rotation: only very center of focus zone (< 30% of focus height) - Size adjustment: large area around effect (30% - 200%) - Camera zoom: only far outside the effect (> 200%) This prevents rotation from dominating size adjustments. Co-Authored-By: Claude Opus 4.5 --- .../no/naiv/tiltshift/ui/TiltShiftOverlay.kt | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt b/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt index 298b127..c68bab3 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.calculateCentroid -import androidx.compose.foundation.gestures.calculateRotation import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -25,6 +24,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import no.naiv.tiltshift.effect.BlurParameters import kotlin.math.PI +import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin @@ -39,11 +39,16 @@ private enum class GestureType { PINCH_ZOOM // Pinch in center to zoom camera } -// Sensitivity factors for gesture controls (lower = less sensitive) -// Rotation uses 1:1 tracking (no dampening) for natural feel -private const val POSITION_SENSITIVITY = 0.5f // Drag to move focus center -private const val SIZE_SENSITIVITY = 0.3f // Pinch to resize blur zone -private const val ZOOM_SENSITIVITY = 0.5f // Pinch to zoom camera +// Sensitivity factor for size pinch (lower = less sensitive) +private const val SIZE_SENSITIVITY = 0.3f +private const val ZOOM_SENSITIVITY = 0.5f + +/** + * Calculates the angle between two touch points. + */ +private fun angleBetweenPoints(p1: Offset, p2: Offset): Float { + return atan2(p2.y - p1.y, p2.x - p1.x) +} /** * Overlay that shows tilt-shift effect controls and handles gestures. @@ -57,6 +62,7 @@ fun TiltShiftOverlay( ) { var currentGesture by remember { mutableStateOf(GestureType.NONE) } var initialAngle by remember { mutableFloatStateOf(0f) } + var initialTouchAngle by remember { mutableFloatStateOf(0f) } var initialSize by remember { mutableFloatStateOf(0.3f) } var initialPositionX by remember { mutableFloatStateOf(0.5f) } var initialPositionY by remember { mutableFloatStateOf(0.5f) } @@ -70,10 +76,8 @@ fun TiltShiftOverlay( currentGesture = GestureType.NONE var previousCentroid = firstDown.position - var accumulatedRotation = 0f var accumulatedZoom = 1f - var accumulatedDragX = 0f - var accumulatedDragY = 0f + var rotationInitialized = false initialAngle = params.angle initialSize = params.size @@ -95,7 +99,9 @@ fun TiltShiftOverlay( when { // Two or more fingers pointers.size >= 2 -> { - val rotation = event.calculateRotation() + val p1 = pointers[0].position + val p2 = pointers[1].position + val currentTouchAngle = angleBetweenPoints(p1, p2) val zoom = event.calculateZoom() // Determine gesture type based on touch positions @@ -106,17 +112,24 @@ fun TiltShiftOverlay( size.height.toFloat(), params ) + rotationInitialized = false } when (currentGesture) { GestureType.ROTATE -> { - // 1:1 rotation tracking - no dampening - accumulatedRotation += rotation - val newAngle = initialAngle + accumulatedRotation - onParamsChange(params.copy(angle = newAngle)) + // Initialize rotation reference on first frame + if (!rotationInitialized) { + initialTouchAngle = currentTouchAngle + initialAngle = params.angle + rotationInitialized = true + } else { + // Direct angle mapping: effect angle = initial + (current touch angle - initial touch angle) + val angleDelta = currentTouchAngle - initialTouchAngle + val newAngle = initialAngle + angleDelta + onParamsChange(params.copy(angle = newAngle)) + } } GestureType.PINCH_SIZE -> { - // Apply dampening to size change val dampenedZoom = 1f + (zoom - 1f) * SIZE_SENSITIVITY accumulatedZoom *= dampenedZoom val newSize = (initialSize * accumulatedZoom) @@ -124,7 +137,6 @@ fun TiltShiftOverlay( onParamsChange(params.copy(size = newSize)) } GestureType.PINCH_ZOOM -> { - // Apply dampening to camera zoom val dampenedZoom = 1f + (zoom - 1f) * ZOOM_SENSITIVITY onZoomChange(dampenedZoom) } @@ -132,19 +144,22 @@ fun TiltShiftOverlay( } } - // Single finger - drag to move focus center (2D) + // Single finger - drag to move focus center 1:1 pointers.size == 1 -> { if (currentGesture == GestureType.NONE) { currentGesture = GestureType.DRAG_POSITION + // Reset initial position at drag start + initialPositionX = params.positionX + initialPositionY = params.positionY + previousCentroid = centroid } if (currentGesture == GestureType.DRAG_POSITION) { + // 1:1 drag - finger movement directly maps to position change val deltaX = (centroid.x - previousCentroid.x) / size.width val deltaY = (centroid.y - previousCentroid.y) / size.height - accumulatedDragX += deltaX * POSITION_SENSITIVITY - accumulatedDragY += deltaY * POSITION_SENSITIVITY - val newX = (initialPositionX + accumulatedDragX).coerceIn(0f, 1f) - val newY = (initialPositionY + accumulatedDragY).coerceIn(0f, 1f) + val newX = (params.positionX + deltaX).coerceIn(0f, 1f) + val newY = (params.positionY + deltaY).coerceIn(0f, 1f) onParamsChange(params.copy(positionX = newX, positionY = newY)) } } @@ -166,6 +181,11 @@ fun TiltShiftOverlay( /** * Determines the type of two-finger gesture based on touch position. + * + * Zones (from center outward): + * - Very center (< 30% of focus height): Rotation + * - Near focus line (30% - 200% of focus height): Size adjustment + * - Far outside (> 200%): Camera zoom */ private fun determineGestureType( centroid: Offset, @@ -186,15 +206,15 @@ private fun determineGestureType( val distFromCenter = kotlin.math.abs(rotatedY) return when { - // Near the edges of the blur zone -> size adjustment - distFromCenter > focusHalfHeight * 0.7f && distFromCenter < focusHalfHeight * 1.5f -> { - GestureType.PINCH_SIZE - } - // Inside the focus zone -> rotation - distFromCenter < focusHalfHeight * 0.7f -> { + // Very center of focus zone -> rotation (small area) + distFromCenter < focusHalfHeight * 0.3f -> { GestureType.ROTATE } - // Outside -> camera zoom + // Anywhere near the blur effect -> size adjustment (large area) + distFromCenter < focusHalfHeight * 2.0f -> { + GestureType.PINCH_SIZE + } + // Far outside -> camera zoom else -> { GestureType.PINCH_ZOOM } @@ -225,13 +245,11 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { rotate(angleDegrees, pivot = Offset(centerX, centerY)) { // Draw blur zone indicators (top and bottom) - extended horizontally - // Top blur zone: from far above to the top edge of focus area drawRect( color = blurZoneColor, topLeft = Offset(centerX - extendedHalf, centerY - focusHalfHeight - extendedHalf), size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf) ) - // Bottom blur zone: from bottom edge of focus area to far below drawRect( color = blurZoneColor, topLeft = Offset(centerX - extendedHalf, centerY + focusHalfHeight),