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:
parent
e53155e8ee
commit
2736d42e74
1 changed files with 48 additions and 30 deletions
|
|
@ -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
|
||||||
onParamsChange(params.copy(angle = newAngle))
|
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 -> {
|
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),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue