Add real-time tilt-shift preview for gallery images
Gallery imports now show the effect live as you adjust parameters, matching the camera preview experience. Uses a downscaled (1024px) source bitmap for fast recomputation via collectLatest, which cancels stale frames when params change mid-computation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dedf445cf6
commit
12051b2a83
3 changed files with 77 additions and 7 deletions
|
|
@ -327,6 +327,15 @@ class ImageCaptureHandler(
|
|||
return rotated
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies tilt-shift effect to a bitmap for real-time preview.
|
||||
* Runs on [Dispatchers.IO]. The caller owns the returned bitmap.
|
||||
*/
|
||||
suspend fun applyTiltShiftPreview(source: Bitmap, params: BlurParameters): Bitmap =
|
||||
withContext(Dispatchers.IO) {
|
||||
applyTiltShiftEffect(source, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies tilt-shift blur effect to a bitmap.
|
||||
* Supports both linear and radial modes.
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ fun CameraScreen(
|
|||
val showControls by viewModel.showControls.collectAsState()
|
||||
val lastSavedUri by viewModel.lastSavedUri.collectAsState()
|
||||
val lastThumbnailBitmap by viewModel.lastThumbnailBitmap.collectAsState()
|
||||
val galleryBitmap by viewModel.galleryBitmap.collectAsState()
|
||||
val isGalleryPreview = galleryBitmap != null
|
||||
val galleryPreviewBitmap by viewModel.galleryPreviewBitmap.collectAsState()
|
||||
val isGalleryPreview = galleryPreviewBitmap != null
|
||||
|
||||
val zoomRatio by viewModel.cameraManager.zoomRatio.collectAsState()
|
||||
val minZoom by viewModel.cameraManager.minZoomRatio.collectAsState()
|
||||
|
|
@ -179,10 +179,10 @@ fun CameraScreen(
|
|||
) {
|
||||
// Main view: gallery preview image or camera GL surface
|
||||
if (isGalleryPreview) {
|
||||
galleryBitmap?.let { bmp ->
|
||||
galleryPreviewBitmap?.let { bmp ->
|
||||
Image(
|
||||
bitmap = bmp.asImageBitmap(),
|
||||
contentDescription = "Gallery image preview. Adjust tilt-shift parameters then tap Apply.",
|
||||
contentDescription = "Gallery image preview with tilt-shift effect. Adjust parameters then tap Apply.",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import android.util.Log
|
|||
import android.view.Surface
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.naiv.tiltshift.camera.CameraManager
|
||||
import no.naiv.tiltshift.camera.ImageCaptureHandler
|
||||
import no.naiv.tiltshift.effect.BlurParameters
|
||||
|
|
@ -29,6 +33,8 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
companion object {
|
||||
private const val TAG = "CameraViewModel"
|
||||
/** Max dimension for the preview source bitmap to keep effect computation fast. */
|
||||
private const val PREVIEW_MAX_DIMENSION = 1024
|
||||
}
|
||||
|
||||
val cameraManager = CameraManager(application)
|
||||
|
|
@ -64,11 +70,19 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
// Gallery preview state
|
||||
private val _galleryBitmap = MutableStateFlow<Bitmap?>(null)
|
||||
val galleryBitmap: StateFlow<Bitmap?> = _galleryBitmap.asStateFlow()
|
||||
|
||||
private val _galleryImageUri = MutableStateFlow<Uri?>(null)
|
||||
val galleryImageUri: StateFlow<Uri?> = _galleryImageUri.asStateFlow()
|
||||
|
||||
/** Downscaled source for fast preview recomputation. */
|
||||
private var galleryPreviewSource: Bitmap? = null
|
||||
|
||||
/** Processed preview bitmap shown in the UI. */
|
||||
private val _galleryPreviewBitmap = MutableStateFlow<Bitmap?>(null)
|
||||
val galleryPreviewBitmap: StateFlow<Bitmap?> = _galleryPreviewBitmap.asStateFlow()
|
||||
|
||||
private var previewJob: Job? = null
|
||||
|
||||
val isGalleryPreview: Boolean get() = _galleryBitmap.value != null
|
||||
|
||||
// Device state
|
||||
|
|
@ -111,6 +125,24 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
if (bitmap != null) {
|
||||
_galleryBitmap.value = bitmap
|
||||
_galleryImageUri.value = uri
|
||||
|
||||
// Create downscaled source for fast preview recomputation
|
||||
galleryPreviewSource = withContext(Dispatchers.IO) {
|
||||
val maxDim = maxOf(bitmap.width, bitmap.height)
|
||||
if (maxDim > PREVIEW_MAX_DIMENSION) {
|
||||
val scale = PREVIEW_MAX_DIMENSION.toFloat() / maxDim
|
||||
Bitmap.createScaledBitmap(
|
||||
bitmap,
|
||||
(bitmap.width * scale).toInt(),
|
||||
(bitmap.height * scale).toInt(),
|
||||
true
|
||||
)
|
||||
} else {
|
||||
bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false)
|
||||
}
|
||||
}
|
||||
|
||||
startPreviewLoop()
|
||||
} else {
|
||||
haptics.error()
|
||||
showError("Failed to load image")
|
||||
|
|
@ -118,11 +150,38 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
}
|
||||
|
||||
/** Reactively recomputes the tilt-shift preview when blur params change. */
|
||||
private fun startPreviewLoop() {
|
||||
previewJob?.cancel()
|
||||
previewJob = viewModelScope.launch {
|
||||
_blurParams.collectLatest { params ->
|
||||
val source = galleryPreviewSource ?: return@collectLatest
|
||||
try {
|
||||
val processed = captureHandler.applyTiltShiftPreview(source, params)
|
||||
val old = _galleryPreviewBitmap.value
|
||||
_galleryPreviewBitmap.value = processed
|
||||
old?.recycle()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Preview computation failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelGalleryPreview() {
|
||||
val old = _galleryBitmap.value
|
||||
previewJob?.cancel()
|
||||
previewJob = null
|
||||
|
||||
val oldGallery = _galleryBitmap.value
|
||||
val oldPreview = _galleryPreviewBitmap.value
|
||||
_galleryBitmap.value = null
|
||||
_galleryImageUri.value = null
|
||||
old?.recycle()
|
||||
_galleryPreviewBitmap.value = null
|
||||
|
||||
oldGallery?.recycle()
|
||||
oldPreview?.recycle()
|
||||
galleryPreviewSource?.recycle()
|
||||
galleryPreviewSource = null
|
||||
}
|
||||
|
||||
fun applyGalleryEffect() {
|
||||
|
|
@ -205,5 +264,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||
cameraManager.release()
|
||||
_lastThumbnailBitmap.value?.recycle()
|
||||
_galleryBitmap.value?.recycle()
|
||||
_galleryPreviewBitmap.value?.recycle()
|
||||
galleryPreviewSource?.recycle()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue