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.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
// 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),