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:
parent
3bbf4f9d14
commit
e3e05af0b8
2 changed files with 71 additions and 31 deletions
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue