Fix image orientation, reduce sensitivity, fix overlay clipping

Image orientation:
- Actually rotate captured bitmap using ImageProxy.rotationDegrees
- Save with EXIF ORIENTATION_NORMAL (bitmap already correctly oriented)
- Handle front camera mirroring

Gesture sensitivity (halved again):
- Position drag: 0.15x (was 0.3x)
- Rotation: 0.2x (was 0.4x)
- Size pinch: 0.25x (was 0.5x)
- Zoom pinch: 0.4x (was 0.6x)

Overlay drawing:
- Use screen diagonal to calculate extended geometry
- Draw lines and rectangles that extend beyond screen bounds
- Prevents clipping when tilt-shift effect is rotated

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-01-28 15:46:43 +01:00
commit e3e05af0b8
2 changed files with 71 additions and 31 deletions

View file

@ -51,8 +51,11 @@ class ImageCaptureHandler(
object : ImageCapture.OnImageCapturedCallback() { object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(imageProxy: ImageProxy) { override fun onCaptureSuccess(imageProxy: ImageProxy) {
try { try {
// Get rotation from ImageProxy (sensor orientation)
val imageRotation = imageProxy.imageInfo.rotationDegrees
// Convert ImageProxy to Bitmap // Convert ImageProxy to Bitmap
val bitmap = imageProxyToBitmap(imageProxy) var bitmap = imageProxyToBitmap(imageProxy)
imageProxy.close() imageProxy.close()
if (bitmap == null) { if (bitmap == null) {
@ -60,21 +63,19 @@ class ImageCaptureHandler(
return 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) val processedBitmap = applyTiltShiftEffect(bitmap, blurParams)
bitmap.recycle() bitmap.recycle()
// Determine EXIF orientation // Save with EXIF orientation as NORMAL (bitmap is already rotated)
val rotationDegrees = OrientationDetector.rotationToDegrees(deviceRotation)
val exifOrientation = OrientationDetector.degreesToExifOrientation(
rotationDegrees, isFrontCamera
)
// Save with EXIF data
kotlinx.coroutines.runBlocking { kotlinx.coroutines.runBlocking {
val result = photoSaver.saveBitmap( val result = photoSaver.saveBitmap(
processedBitmap, processedBitmap,
exifOrientation, ExifInterface.ORIENTATION_NORMAL,
location location
) )
processedBitmap.recycle() 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? { private fun imageProxyToBitmap(imageProxy: ImageProxy): Bitmap? {
val buffer = imageProxy.planes[0].buffer val buffer = imageProxy.planes[0].buffer
val bytes = ByteArray(buffer.remaining()) val bytes = ByteArray(buffer.remaining())

View file

@ -40,10 +40,10 @@ private enum class GestureType {
} }
// Sensitivity factors for gesture controls (lower = less sensitive) // Sensitivity factors for gesture controls (lower = less sensitive)
private const val POSITION_SENSITIVITY = 0.3f // Drag to move focus line private const val POSITION_SENSITIVITY = 0.15f // Drag to move focus line
private const val ROTATION_SENSITIVITY = 0.4f // Two-finger rotation private const val ROTATION_SENSITIVITY = 0.2f // Two-finger rotation
private const val SIZE_SENSITIVITY = 0.5f // Pinch to resize blur zone private const val SIZE_SENSITIVITY = 0.25f // Pinch to resize blur zone
private const val ZOOM_SENSITIVITY = 0.6f // Pinch to zoom camera private const val ZOOM_SENSITIVITY = 0.4f // Pinch to zoom camera
/** /**
* Overlay that shows tilt-shift effect controls and handles gestures. * Overlay that shows tilt-shift effect controls and handles gestures.
@ -201,11 +201,13 @@ private fun determineGestureType(
/** /**
* Draws the tilt-shift visualization overlay. * Draws the tilt-shift visualization overlay.
* Uses extended geometry so rotated elements don't clip at screen edges.
*/ */
private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) { private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
val width = size.width val width = size.width
val height = size.height val height = size.height
val centerX = width / 2f
val centerY = height * params.position val centerY = height * params.position
val focusHalfHeight = height * params.size * 0.5f val focusHalfHeight = height * params.size * 0.5f
val angleDegrees = params.angle * (180f / PI.toFloat()) 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 blurZoneColor = Color(0x40FFFFFF) // Semi-transparent white
val dashEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f), 0f) val dashEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f), 0f)
rotate(angleDegrees, pivot = Offset(width / 2f, centerY)) { // Calculate diagonal for extended drawing (ensures coverage when rotated)
// Draw blur zone indicators (top and bottom) 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( drawRect(
color = blurZoneColor, color = blurZoneColor,
topLeft = Offset(0f, 0f), topLeft = Offset(centerX - extendedHalf, centerY - focusHalfHeight - extendedHalf),
size = androidx.compose.ui.geometry.Size(width, centerY - focusHalfHeight) 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(0f, centerY + focusHalfHeight), topLeft = Offset(centerX - extendedHalf, centerY + focusHalfHeight),
size = androidx.compose.ui.geometry.Size(width, height - (centerY + focusHalfHeight)) size = androidx.compose.ui.geometry.Size(extendedHalf * 2, extendedHalf)
) )
// Draw focus zone boundary lines // Draw focus zone boundary lines - extended horizontally
drawLine( drawLine(
color = focusLineColor, color = focusLineColor,
start = Offset(0f, centerY - focusHalfHeight), start = Offset(centerX - extendedHalf, centerY - focusHalfHeight),
end = Offset(width, centerY - focusHalfHeight), end = Offset(centerX + extendedHalf, centerY - focusHalfHeight),
strokeWidth = 2.dp.toPx(), strokeWidth = 2.dp.toPx(),
pathEffect = dashEffect pathEffect = dashEffect
) )
drawLine( drawLine(
color = focusLineColor, color = focusLineColor,
start = Offset(0f, centerY + focusHalfHeight), start = Offset(centerX - extendedHalf, centerY + focusHalfHeight),
end = Offset(width, centerY + focusHalfHeight), end = Offset(centerX + extendedHalf, centerY + focusHalfHeight),
strokeWidth = 2.dp.toPx(), strokeWidth = 2.dp.toPx(),
pathEffect = dashEffect pathEffect = dashEffect
) )
// Draw center focus line // Draw center focus line - extended horizontally
drawLine( drawLine(
color = focusLineColor, color = focusLineColor,
start = Offset(0f, centerY), start = Offset(centerX - extendedHalf, centerY),
end = Offset(width, centerY), end = Offset(centerX + extendedHalf, centerY),
strokeWidth = 3.dp.toPx() strokeWidth = 3.dp.toPx()
) )
@ -257,7 +265,7 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
drawCircle( drawCircle(
color = focusLineColor.copy(alpha = 0.5f), color = focusLineColor.copy(alpha = 0.5f),
radius = indicatorRadius, radius = indicatorRadius,
center = Offset(width / 2f, centerY), center = Offset(centerX, centerY),
style = Stroke(width = 2.dp.toPx()) style = Stroke(width = 2.dp.toPx())
) )
@ -265,8 +273,8 @@ private fun DrawScope.drawTiltShiftOverlay(params: BlurParameters) {
val tickLength = 15.dp.toPx() val tickLength = 15.dp.toPx()
drawLine( drawLine(
color = focusLineColor, color = focusLineColor,
start = Offset(width / 2f, centerY - indicatorRadius + tickLength), start = Offset(centerX, centerY - indicatorRadius + tickLength),
end = Offset(width / 2f, centerY - indicatorRadius - 5.dp.toPx()), end = Offset(centerX, centerY - indicatorRadius - 5.dp.toPx()),
strokeWidth = 3.dp.toPx() strokeWidth = 3.dp.toPx()
) )
} }