Add radial mode, UI controls, front camera, update to API 35
- Add radial/elliptical blur mode with aspect ratio control - Add UI sliders for blur intensity, falloff, and shape - Add front camera support with flip button - Update minimum SDK to API 35 (Android 15) - Enable landscape orientation (fullSensor) - Rename app to "Naiv Tilt Shift Camera" - Set APK output name to naiv-tilt-shift - Add project specification document Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8a5fa4811
commit
d3ca23b71c
11 changed files with 679 additions and 94 deletions
|
|
@ -41,8 +41,12 @@ class CameraManager(private val context: Context) {
|
|||
private val _maxZoomRatio = MutableStateFlow(1.0f)
|
||||
val maxZoomRatio: StateFlow<Float> = _maxZoomRatio.asStateFlow()
|
||||
|
||||
private val _isFrontCamera = MutableStateFlow(false)
|
||||
val isFrontCamera: StateFlow<Boolean> = _isFrontCamera.asStateFlow()
|
||||
|
||||
private var surfaceTextureProvider: (() -> SurfaceTexture?)? = null
|
||||
private var surfaceSize: Size = Size(1920, 1080)
|
||||
private var lifecycleOwnerRef: LifecycleOwner? = null
|
||||
|
||||
/**
|
||||
* Starts the camera with the given lifecycle owner.
|
||||
|
|
@ -53,6 +57,7 @@ class CameraManager(private val context: Context) {
|
|||
surfaceTextureProvider: () -> SurfaceTexture?
|
||||
) {
|
||||
this.surfaceTextureProvider = surfaceTextureProvider
|
||||
this.lifecycleOwnerRef = lifecycleOwner
|
||||
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
|
|
@ -82,9 +87,13 @@ class CameraManager(private val context: Context) {
|
|||
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
||||
.build()
|
||||
|
||||
// Get camera selector from lens controller
|
||||
val cameraSelector = lensController.getCurrentLens()?.selector
|
||||
?: CameraSelector.DEFAULT_BACK_CAMERA
|
||||
// Select camera based on front/back preference
|
||||
val cameraSelector = if (_isFrontCamera.value) {
|
||||
CameraSelector.DEFAULT_FRONT_CAMERA
|
||||
} else {
|
||||
// Use lens controller for back camera lens selection
|
||||
lensController.getCurrentLens()?.selector ?: CameraSelector.DEFAULT_BACK_CAMERA
|
||||
}
|
||||
|
||||
try {
|
||||
// Bind use cases to camera
|
||||
|
|
@ -146,10 +155,19 @@ class CameraManager(private val context: Context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Switches to a different lens.
|
||||
* Switches between front and back camera.
|
||||
*/
|
||||
fun switchCamera() {
|
||||
_isFrontCamera.value = !_isFrontCamera.value
|
||||
_zoomRatio.value = 1.0f // Reset zoom when switching
|
||||
lifecycleOwnerRef?.let { bindCameraUseCases(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a different lens (back camera only).
|
||||
*/
|
||||
fun switchLens(lensId: String, lifecycleOwner: LifecycleOwner) {
|
||||
if (lensController.selectLens(lensId)) {
|
||||
if (!_isFrontCamera.value && lensController.selectLens(lensId)) {
|
||||
bindCameraUseCases(lifecycleOwner)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,5 +187,6 @@ class CameraManager(private val context: Context) {
|
|||
camera = null
|
||||
preview = null
|
||||
imageCapture = null
|
||||
lifecycleOwnerRef = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,25 +6,21 @@ import android.graphics.BitmapFactory
|
|||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.location.Location
|
||||
import android.os.Build
|
||||
import android.view.Surface
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import no.naiv.tiltshift.effect.BlurMode
|
||||
import no.naiv.tiltshift.effect.BlurParameters
|
||||
import no.naiv.tiltshift.storage.PhotoSaver
|
||||
import no.naiv.tiltshift.storage.SaveResult
|
||||
import no.naiv.tiltshift.util.OrientationDetector
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Handles capturing photos with the tilt-shift effect applied.
|
||||
|
|
@ -135,7 +131,7 @@ class ImageCaptureHandler(
|
|||
|
||||
/**
|
||||
* Applies tilt-shift blur effect to a bitmap.
|
||||
* This is a software fallback - on newer devices we could use RenderScript/RenderEffect.
|
||||
* Supports both linear and radial modes.
|
||||
*/
|
||||
private fun applyTiltShiftEffect(source: Bitmap, params: BlurParameters): Bitmap {
|
||||
val width = source.width
|
||||
|
|
@ -143,7 +139,6 @@ class ImageCaptureHandler(
|
|||
|
||||
// Create output bitmap
|
||||
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
// For performance, we use a scaled-down version for blur and composite
|
||||
val scaleFactor = 4 // Blur a 1/4 size image for speed
|
||||
|
|
@ -165,7 +160,6 @@ class ImageCaptureHandler(
|
|||
val mask = createGradientMask(width, height, params)
|
||||
|
||||
// Composite: blend original with blurred based on mask
|
||||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
val pixels = IntArray(width * height)
|
||||
val blurredPixels = IntArray(width * height)
|
||||
val maskPixels = IntArray(width * height)
|
||||
|
|
@ -199,6 +193,7 @@ class ImageCaptureHandler(
|
|||
|
||||
/**
|
||||
* Creates a gradient mask for the tilt-shift effect.
|
||||
* Supports both linear and radial modes.
|
||||
*/
|
||||
private fun createGradientMask(width: Int, height: Int, params: BlurParameters): Bitmap {
|
||||
val mask = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
|
|
@ -206,25 +201,47 @@ class ImageCaptureHandler(
|
|||
|
||||
val centerX = width * params.positionX
|
||||
val centerY = height * params.positionY
|
||||
val focusHalfHeight = height * params.size * 0.5f
|
||||
val transitionHeight = focusHalfHeight * 0.5f
|
||||
val focusSize = height * params.size * 0.5f
|
||||
val transitionSize = focusSize * params.falloff
|
||||
|
||||
val cosAngle = cos(params.angle)
|
||||
val sinAngle = sin(params.angle)
|
||||
val screenAspect = width.toFloat() / height.toFloat()
|
||||
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
// Rotate point around focus center
|
||||
val dx = x - centerX
|
||||
val dy = y - centerY
|
||||
val rotatedY = -dx * sinAngle + dy * cosAngle
|
||||
val dist = when (params.mode) {
|
||||
BlurMode.LINEAR -> {
|
||||
// Rotate point around focus center
|
||||
val dx = x - centerX
|
||||
val dy = y - centerY
|
||||
val rotatedY = -dx * sinAngle + dy * cosAngle
|
||||
kotlin.math.abs(rotatedY)
|
||||
}
|
||||
BlurMode.RADIAL -> {
|
||||
// Calculate elliptical distance from center
|
||||
var dx = x - centerX
|
||||
var dy = y - centerY
|
||||
|
||||
// Calculate blur amount based on distance from focus line
|
||||
val dist = kotlin.math.abs(rotatedY)
|
||||
// Adjust for screen aspect ratio
|
||||
dx *= screenAspect
|
||||
|
||||
// Rotate
|
||||
val rotatedX = dx * cosAngle - dy * sinAngle
|
||||
val rotatedY = dx * sinAngle + dy * cosAngle
|
||||
|
||||
// Apply ellipse aspect ratio
|
||||
val adjustedX = rotatedX / params.aspectRatio
|
||||
|
||||
sqrt(adjustedX * adjustedX + rotatedY * rotatedY)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate blur amount based on distance from focus region
|
||||
val blurAmount = when {
|
||||
dist < focusHalfHeight -> 0f
|
||||
dist < focusHalfHeight + transitionHeight -> {
|
||||
(dist - focusHalfHeight) / transitionHeight
|
||||
dist < focusSize -> 0f
|
||||
dist < focusSize + transitionSize -> {
|
||||
(dist - focusSize) / transitionSize
|
||||
}
|
||||
else -> 1f
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue