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() {
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())

View file

@ -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()
)
}