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 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-01-28 16:02:43 +01:00
commit 2736d42e74

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculateCentroid import androidx.compose.foundation.gestures.calculateCentroid
import androidx.compose.foundation.gestures.calculateRotation
import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -25,6 +24,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.naiv.tiltshift.effect.BlurParameters import no.naiv.tiltshift.effect.BlurParameters
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
@ -39,11 +39,16 @@ private enum class GestureType {
PINCH_ZOOM // Pinch in center to zoom camera PINCH_ZOOM // Pinch in center to zoom camera
} }
// Sensitivity factors for gesture controls (lower = less sensitive) // Sensitivity factor for size pinch (lower = less sensitive)
// Rotation uses 1:1 tracking (no dampening) for natural feel private const val SIZE_SENSITIVITY = 0.3f
private const val POSITION_SENSITIVITY = 0.5f // Drag to move focus center private const val ZOOM_SENSITIVITY = 0.5f
private const val SIZE_SENSITIVITY = 0.3f // Pinch to resize blur zone
private const val ZOOM_SENSITIVITY = 0.5f // Pinch to zoom camera /**
* 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. * Overlay that shows tilt-shift effect controls and handles gestures.
@ -57,6 +62,7 @@ fun TiltShiftOverlay(
) { ) {
var currentGesture by remember { mutableStateOf(GestureType.NONE) } var currentGesture by remember { mutableStateOf(GestureType.NONE) }
var initialAngle by remember { mutableFloatStateOf(0f) } var initialAngle by remember { mutableFloatStateOf(0f) }
var initialTouchAngle by remember { mutableFloatStateOf(0f) }
var initialSize by remember { mutableFloatStateOf(0.3f) } var initialSize by remember { mutableFloatStateOf(0.3f) }
var initialPositionX by remember { mutableFloatStateOf(0.5f) } var initialPositionX by remember { mutableFloatStateOf(0.5f) }
var initialPositionY by remember { mutableFloatStateOf(0.5f) } var initialPositionY by remember { mutableFloatStateOf(0.5f) }
@ -70,10 +76,8 @@ fun TiltShiftOverlay(
currentGesture = GestureType.NONE currentGesture = GestureType.NONE
var previousCentroid = firstDown.position var previousCentroid = firstDown.position
var accumulatedRotation = 0f
var accumulatedZoom = 1f var accumulatedZoom = 1f
var accumulatedDragX = 0f var rotationInitialized = false
var accumulatedDragY = 0f
initialAngle = params.angle initialAngle = params.angle
initialSize = params.size initialSize = params.size
@ -95,7 +99,9 @@ fun TiltShiftOverlay(
when { when {
// Two or more fingers // Two or more fingers
pointers.size >= 2 -> { 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() val zoom = event.calculateZoom()
// Determine gesture type based on touch positions // Determine gesture type based on touch positions
@ -106,17 +112,24 @@ fun TiltShiftOverlay(
size.height.toFloat(), size.height.toFloat(),
params params
) )
rotationInitialized = false
} }
when (currentGesture) { when (currentGesture) {
GestureType.ROTATE -> { GestureType.ROTATE -> {
// 1:1 rotation tracking - no dampening // Initialize rotation reference on first frame
accumulatedRotation += rotation if (!rotationInitialized) {
val newAngle = initialAngle + accumulatedRotation 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)) onParamsChange(params.copy(angle = newAngle))
} }
}
GestureType.PINCH_SIZE -> { GestureType.PINCH_SIZE -> {
// Apply dampening to size change
val dampenedZoom = 1f + (zoom - 1f) * SIZE_SENSITIVITY val dampenedZoom = 1f + (zoom - 1f) * SIZE_SENSITIVITY
accumulatedZoom *= dampenedZoom accumulatedZoom *= dampenedZoom
val newSize = (initialSize * accumulatedZoom) val newSize = (initialSize * accumulatedZoom)
@ -124,7 +137,6 @@ fun TiltShiftOverlay(
onParamsChange(params.copy(size = newSize)) onParamsChange(params.copy(size = newSize))
} }
GestureType.PINCH_ZOOM -> { GestureType.PINCH_ZOOM -> {
// Apply dampening to camera zoom
val dampenedZoom = 1f + (zoom - 1f) * ZOOM_SENSITIVITY val dampenedZoom = 1f + (zoom - 1f) * ZOOM_SENSITIVITY
onZoomChange(dampenedZoom) 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 -> { pointers.size == 1 -> {
if (currentGesture == GestureType.NONE) { if (currentGesture == GestureType.NONE) {
currentGesture = GestureType.DRAG_POSITION currentGesture = GestureType.DRAG_POSITION
// Reset initial position at drag start
initialPositionX = params.positionX
initialPositionY = params.positionY
previousCentroid = centroid
} }
if (currentGesture == GestureType.DRAG_POSITION) { if (currentGesture == GestureType.DRAG_POSITION) {
// 1:1 drag - finger movement directly maps to position change
val deltaX = (centroid.x - previousCentroid.x) / size.width val deltaX = (centroid.x - previousCentroid.x) / size.width
val deltaY = (centroid.y - previousCentroid.y) / size.height val deltaY = (centroid.y - previousCentroid.y) / size.height
accumulatedDragX += deltaX * POSITION_SENSITIVITY val newX = (params.positionX + deltaX).coerceIn(0f, 1f)
accumulatedDragY += deltaY * POSITION_SENSITIVITY val newY = (params.positionY + deltaY).coerceIn(0f, 1f)
val newX = (initialPositionX + accumulatedDragX).coerceIn(0f, 1f)
val newY = (initialPositionY + accumulatedDragY).coerceIn(0f, 1f)
onParamsChange(params.copy(positionX = newX, positionY = newY)) onParamsChange(params.copy(positionX = newX, positionY = newY))
} }
} }
@ -166,6 +181,11 @@ fun TiltShiftOverlay(
/** /**
* Determines the type of two-finger gesture based on touch position. * 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( private fun determineGestureType(
centroid: Offset, centroid: Offset,
@ -186,15 +206,15 @@ private fun determineGestureType(
val distFromCenter = kotlin.math.abs(rotatedY) val distFromCenter = kotlin.math.abs(rotatedY)
return when { return when {
// Near the edges of the blur zone -> size adjustment // Very center of focus zone -> rotation (small area)
distFromCenter > focusHalfHeight * 0.7f && distFromCenter < focusHalfHeight * 1.5f -> { distFromCenter < focusHalfHeight * 0.3f -> {
GestureType.PINCH_SIZE
}
// Inside the focus zone -> rotation
distFromCenter < focusHalfHeight * 0.7f -> {
GestureType.ROTATE 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 -> { else -> {
GestureType.PINCH_ZOOM GestureType.PINCH_ZOOM
} }
@ -225,13 +245,11 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
rotate(angleDegrees, pivot = Offset(centerX, centerY)) { rotate(angleDegrees, pivot = Offset(centerX, centerY)) {
// Draw blur zone indicators (top and bottom) - extended horizontally // Draw blur zone indicators (top and bottom) - extended horizontally
// Top blur zone: from far above to the top edge of focus area
drawRect( drawRect(
color = blurZoneColor, color = blurZoneColor,
topLeft = Offset(centerX - extendedHalf, centerY - focusHalfHeight - extendedHalf), topLeft = Offset(centerX - extendedHalf, centerY - focusHalfHeight - extendedHalf),
size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf) size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf)
) )
// Bottom blur zone: from bottom edge of focus area to far below
drawRect( drawRect(
color = blurZoneColor, color = blurZoneColor,
topLeft = Offset(centerX - extendedHalf, centerY + focusHalfHeight), topLeft = Offset(centerX - extendedHalf, centerY + focusHalfHeight),