tilt-shift-camera/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt

273 lines
11 KiB
Kotlin
Raw Normal View History

package no.naiv.tiltshift.ui
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
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.input.pointer.PointerEventType
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.cos
import kotlin.math.sin
/**
* Type of gesture being performed.
*/
private enum class GestureType {
NONE,
DRAG_POSITION, // Single finger drag to move focus position
ROTATE, // Two-finger rotation
PINCH_SIZE, // Pinch near blur edges to resize
PINCH_ZOOM // Pinch in center to zoom camera
}
// Sensitivity factors for gesture controls (lower = less sensitive)
private const val POSITION_SENSITIVITY = 0.3f // Drag to move focus line
private const val ROTATION_SENSITIVITY = 0.4f // Two-finger rotation
private const val SIZE_SENSITIVITY = 0.5f // Pinch to resize blur zone
private const val ZOOM_SENSITIVITY = 0.6f // Pinch to zoom camera
/**
* Overlay that shows tilt-shift effect controls and handles gestures.
*/
@Composable
fun TiltShiftOverlay(
params: BlurParameters,
onParamsChange: (BlurParameters) -> Unit,
onZoomChange: (Float) -> Unit,
modifier: Modifier = Modifier
) {
var currentGesture by remember { mutableStateOf(GestureType.NONE) }
var initialZoom by remember { mutableFloatStateOf(1f) }
var initialAngle by remember { mutableFloatStateOf(0f) }
var initialSize by remember { mutableFloatStateOf(0.3f) }
var initialPosition by remember { mutableFloatStateOf(0.5f) }
Canvas(
modifier = modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitEachGesture {
val firstDown = awaitFirstDown(requireUnconsumed = false)
currentGesture = GestureType.NONE
var previousCentroid = firstDown.position
var previousPointerCount = 1
var accumulatedRotation = 0f
var accumulatedZoom = 1f
var accumulatedDragY = 0f
initialAngle = params.angle
initialSize = params.size
initialPosition = params.position
initialZoom = 1f
do {
val event = awaitPointerEvent()
val pointers = event.changes.filter { it.pressed }
if (pointers.isEmpty()) break
val centroid = if (pointers.size >= 2) {
event.calculateCentroid()
} else {
pointers.first().position
}
when {
// Two or more fingers
pointers.size >= 2 -> {
val rotation = event.calculateRotation()
val zoom = event.calculateZoom()
// Determine gesture type based on touch positions
if (currentGesture == GestureType.NONE || currentGesture == GestureType.DRAG_POSITION) {
currentGesture = determineGestureType(
centroid,
size.width.toFloat(),
size.height.toFloat(),
params
)
}
when (currentGesture) {
GestureType.ROTATE -> {
// Apply dampening to rotation
accumulatedRotation += rotation * ROTATION_SENSITIVITY
val newAngle = initialAngle + accumulatedRotation
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)
.coerceIn(BlurParameters.MIN_SIZE, BlurParameters.MAX_SIZE)
onParamsChange(params.copy(size = newSize))
}
GestureType.PINCH_ZOOM -> {
// Apply dampening to camera zoom
val dampenedZoom = 1f + (zoom - 1f) * ZOOM_SENSITIVITY
onZoomChange(dampenedZoom)
}
else -> {}
}
}
// Single finger
pointers.size == 1 -> {
if (currentGesture == GestureType.NONE) {
currentGesture = GestureType.DRAG_POSITION
}
if (currentGesture == GestureType.DRAG_POSITION) {
// Apply dampening to position drag
val deltaY = (centroid.y - previousCentroid.y) / size.height
accumulatedDragY += deltaY * POSITION_SENSITIVITY
val newPosition = (initialPosition + accumulatedDragY).coerceIn(0f, 1f)
onParamsChange(params.copy(position = newPosition))
}
}
}
previousCentroid = centroid
previousPointerCount = pointers.size
// Consume all pointer changes
pointers.forEach { it.consume() }
} while (event.type != PointerEventType.Release)
currentGesture = GestureType.NONE
}
}
) {
drawTiltShiftOverlay(params)
}
}
/**
* Determines the type of two-finger gesture based on touch position.
*/
private fun determineGestureType(
centroid: Offset,
width: Float,
height: Float,
params: BlurParameters
): GestureType {
// Calculate distance from focus center line
val focusCenterY = height * params.position
val focusHalfHeight = height * params.size * 0.5f
// Rotate centroid to align with focus line
val dx = centroid.x - width / 2f
val dy = centroid.y - focusCenterY
val rotatedY = -dx * sin(params.angle) + dy * cos(params.angle)
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 -> {
GestureType.ROTATE
}
// Outside -> camera zoom
else -> {
GestureType.PINCH_ZOOM
}
}
}
/**
* Draws the tilt-shift visualization overlay.
*/
private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
val width = size.width
val height = size.height
val centerY = height * params.position
val focusHalfHeight = height * params.size * 0.5f
val angleDegrees = params.angle * (180f / PI.toFloat())
// Colors for overlay
val focusLineColor = Color(0xFFFFB300) // Amber
val blurZoneColor = Color(0x40FFFFFF) // Semi-transparent white
val dashEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f), 0f)
rotate(angleDegrees, pivot = Offset(width / 2f, centerY)) {
// Draw blur zone indicators (top and bottom)
drawRect(
color = blurZoneColor,
topLeft = Offset(0f, 0f),
size = androidx.compose.ui.geometry.Size(width, centerY - focusHalfHeight)
)
drawRect(
color = blurZoneColor,
topLeft = Offset(0f, centerY + focusHalfHeight),
size = androidx.compose.ui.geometry.Size(width, height - (centerY + focusHalfHeight))
)
// Draw focus zone boundary lines
drawLine(
color = focusLineColor,
start = Offset(0f, centerY - focusHalfHeight),
end = Offset(width, centerY - focusHalfHeight),
strokeWidth = 2.dp.toPx(),
pathEffect = dashEffect
)
drawLine(
color = focusLineColor,
start = Offset(0f, centerY + focusHalfHeight),
end = Offset(width, centerY + focusHalfHeight),
strokeWidth = 2.dp.toPx(),
pathEffect = dashEffect
)
// Draw center focus line
drawLine(
color = focusLineColor,
start = Offset(0f, centerY),
end = Offset(width, centerY),
strokeWidth = 3.dp.toPx()
)
// Draw rotation indicator at center
val indicatorRadius = 30.dp.toPx()
drawCircle(
color = focusLineColor.copy(alpha = 0.5f),
radius = indicatorRadius,
center = Offset(width / 2f, centerY),
style = Stroke(width = 2.dp.toPx())
)
// Draw angle tick mark
val tickLength = 15.dp.toPx()
drawLine(
color = focusLineColor,
start = Offset(width / 2f, centerY - indicatorRadius + tickLength),
end = Offset(width / 2f, centerY - indicatorRadius - 5.dp.toPx()),
strokeWidth = 3.dp.toPx()
)
}
}