diff --git a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt index baca3ec..704ceb6 100644 --- a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt +++ b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt @@ -51,8 +51,11 @@ class ImageCaptureHandler( object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(imageProxy: ImageProxy) { try { + // Get rotation from ImageProxy (sensor orientation) + val imageRotation = imageProxy.imageInfo.rotationDegrees + // Convert ImageProxy to Bitmap - val bitmap = imageProxyToBitmap(imageProxy) + var bitmap = imageProxyToBitmap(imageProxy) imageProxy.close() if (bitmap == null) { @@ -60,21 +63,19 @@ class ImageCaptureHandler( return } - // Apply tilt-shift effect to captured image + // Rotate bitmap to correct orientation + // Camera sensor is landscape, we need to rotate for portrait + bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera) + + // Apply tilt-shift effect to the correctly oriented image val processedBitmap = applyTiltShiftEffect(bitmap, blurParams) bitmap.recycle() - // Determine EXIF orientation - val rotationDegrees = OrientationDetector.rotationToDegrees(deviceRotation) - val exifOrientation = OrientationDetector.degreesToExifOrientation( - rotationDegrees, isFrontCamera - ) - - // Save with EXIF data + // Save with EXIF orientation as NORMAL (bitmap is already rotated) kotlinx.coroutines.runBlocking { val result = photoSaver.saveBitmap( processedBitmap, - exifOrientation, + ExifInterface.ORIENTATION_NORMAL, location ) processedBitmap.recycle() @@ -94,6 +95,37 @@ class ImageCaptureHandler( ) } + /** + * Rotates a bitmap to the correct orientation. + */ + private fun rotateBitmap(bitmap: Bitmap, rotationDegrees: Int, isFrontCamera: Boolean): Bitmap { + if (rotationDegrees == 0 && !isFrontCamera) { + return bitmap + } + + val matrix = Matrix() + + // Apply rotation + if (rotationDegrees != 0) { + matrix.postRotate(rotationDegrees.toFloat()) + } + + // Mirror for front camera + if (isFrontCamera) { + matrix.postScale(-1f, 1f) + } + + val rotated = Bitmap.createBitmap( + bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true + ) + + if (rotated != bitmap) { + bitmap.recycle() + } + + return rotated + } + private fun imageProxyToBitmap(imageProxy: ImageProxy): Bitmap? { val buffer = imageProxy.planes[0].buffer val bytes = ByteArray(buffer.remaining()) 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 6b9b2d4..ef6406a 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/TiltShiftOverlay.kt @@ -40,10 +40,10 @@ private enum class GestureType { } // 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 +private const val POSITION_SENSITIVITY = 0.15f // Drag to move focus line +private const val ROTATION_SENSITIVITY = 0.2f // Two-finger rotation +private const val SIZE_SENSITIVITY = 0.25f // Pinch to resize blur zone +private const val ZOOM_SENSITIVITY = 0.4f // Pinch to zoom camera /** * Overlay that shows tilt-shift effect controls and handles gestures. @@ -201,11 +201,13 @@ private fun determineGestureType( /** * Draws the tilt-shift visualization overlay. + * Uses extended geometry so rotated elements don't clip at screen edges. */ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { val width = size.width val height = size.height + val centerX = width / 2f val centerY = height * params.position val focusHalfHeight = height * params.size * 0.5f val angleDegrees = params.angle * (180f / PI.toFloat()) @@ -215,40 +217,46 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { 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) + // Calculate diagonal for extended drawing (ensures coverage when rotated) + val diagonal = kotlin.math.sqrt(width * width + height * height) + val extendedHalf = diagonal // Extend lines/rects well beyond screen + + 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(0f, 0f), - size = androidx.compose.ui.geometry.Size(width, centerY - focusHalfHeight) + 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(0f, centerY + focusHalfHeight), - size = androidx.compose.ui.geometry.Size(width, height - (centerY + focusHalfHeight)) + topLeft = Offset(centerX - extendedHalf, centerY + focusHalfHeight), + size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf) ) - // Draw focus zone boundary lines + // Draw focus zone boundary lines - extended horizontally drawLine( color = focusLineColor, - start = Offset(0f, centerY - focusHalfHeight), - end = Offset(width, centerY - focusHalfHeight), + start = Offset(centerX - extendedHalf, centerY - focusHalfHeight), + end = Offset(centerX + extendedHalf, centerY - focusHalfHeight), strokeWidth = 2.dp.toPx(), pathEffect = dashEffect ) drawLine( color = focusLineColor, - start = Offset(0f, centerY + focusHalfHeight), - end = Offset(width, centerY + focusHalfHeight), + start = Offset(centerX - extendedHalf, centerY + focusHalfHeight), + end = Offset(centerX + extendedHalf, centerY + focusHalfHeight), strokeWidth = 2.dp.toPx(), pathEffect = dashEffect ) - // Draw center focus line + // Draw center focus line - extended horizontally drawLine( color = focusLineColor, - start = Offset(0f, centerY), - end = Offset(width, centerY), + start = Offset(centerX - extendedHalf, centerY), + end = Offset(centerX + extendedHalf, centerY), strokeWidth = 3.dp.toPx() ) @@ -257,7 +265,7 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { drawCircle( color = focusLineColor.copy(alpha = 0.5f), radius = indicatorRadius, - center = Offset(width / 2f, centerY), + center = Offset(centerX, centerY), style = Stroke(width = 2.dp.toPx()) ) @@ -265,8 +273,8 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { val tickLength = 15.dp.toPx() drawLine( color = focusLineColor, - start = Offset(width / 2f, centerY - indicatorRadius + tickLength), - end = Offset(width / 2f, centerY - indicatorRadius - 5.dp.toPx()), + start = Offset(centerX, centerY - indicatorRadius + tickLength), + end = Offset(centerX, centerY - indicatorRadius - 5.dp.toPx()), strokeWidth = 3.dp.toPx() ) }